From 7163b2283b4542a4d4abfe9a71963f122322bde7 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 8 Nov 2020 19:15:06 +0100 Subject: bug: Id from first operation data, not git + remove root link --- bug/op_create.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) (limited to 'bug/op_create.go') diff --git a/bug/op_create.go b/bug/op_create.go index 9bb40d35d..3c8ce6587 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -1,6 +1,7 @@ package bug import ( + "crypto/rand" "encoding/json" "fmt" "strings" @@ -17,6 +18,10 @@ var _ Operation = &CreateOperation{} // CreateOperation define the initial creation of a bug type CreateOperation struct { OpBase + // mandatory random bytes to ensure a better randomness of the data of the first + // operation of a bug, used to later generate the ID + // len(Nonce) should be > 20 and < 64 bytes + Nonce []byte `json:"nonce"` Title string `json:"title"` Message string `json:"message"` Files []repository.Hash `json:"files"` @@ -66,14 +71,19 @@ 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") } - if strings.Contains(op.Title, "\n") { return fmt.Errorf("title should be a single line") } - if !text.Safe(op.Title) { return fmt.Errorf("title is not fully printable") } @@ -98,6 +108,7 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error { } aux := struct { + Nonce []byte `json:"nonce"` Title string `json:"title"` Message string `json:"message"` Files []repository.Hash `json:"files"` @@ -109,6 +120,7 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error { } op.OpBase = base + op.Nonce = aux.Nonce op.Title = aux.Title op.Message = aux.Message op.Files = aux.Files @@ -119,9 +131,19 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error { // Sign post method for gqlgen func (op *CreateOperation) IsAuthored() {} +func makeNonce(len int) []byte { + result := make([]byte, len) + _, err := rand.Read(result) + if err != nil { + panic(err) + } + return result +} + func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation { return &CreateOperation{ OpBase: newOpBase(CreateOp, author, unixTime), + Nonce: makeNonce(20), Title: title, Message: message, Files: files, -- cgit v1.2.3 From 2788c5fc87507974d3237d4edc233fda3f784b35 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 29 Nov 2020 20:22:09 +0100 Subject: bug: don't store the id in Bug, match how it's done for Identity --- bug/bug.go | 42 +++++++++--------------------------------- bug/op_create.go | 17 ++++++++++++++++- bug/with_snapshot.go | 2 +- 3 files changed, 26 insertions(+), 35 deletions(-) (limited to 'bug/op_create.go') diff --git a/bug/bug.go b/bug/bug.go index 86227c6ba..fb36bfd85 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -44,16 +44,12 @@ var _ entity.Interface = &Bug{} // how it will be persisted inside Git. This is the data structure // used to merge two different version of the same Bug. type Bug struct { - // A Lamport clock is a logical clock that allow to order event // inside a distributed system. // It must be the first field in this struct due to https://github.com/golang/go/issues/599 createTime lamport.Time editTime lamport.Time - // Id used as unique identifier - id entity.Id - lastCommit repository.Hash // all the committed operations @@ -66,9 +62,8 @@ type Bug struct { // NewBug create a new Bug func NewBug() *Bug { - // No id yet // No logical clock yet - return &Bug{id: entity.UnsetId} + return &Bug{} } // ReadLocal will read a local bug from its hash @@ -111,9 +106,7 @@ func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref s return nil, fmt.Errorf("empty bug") } - bug := Bug{ - id: id, - } + bug := Bug{} // Load each OperationPack for _, hash := range hashes { @@ -164,7 +157,7 @@ func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref s if len(bug.packs[0].Operations) == 0 { return nil, fmt.Errorf("first OperationPack is empty") } - if bug.id != bug.packs[0].Operations[0].Id() { + if id != bug.packs[0].Operations[0].Id() { return nil, fmt.Errorf("bug ID doesn't match the first operation ID") } @@ -319,13 +312,6 @@ func (bug *Bug) Validate() error { return fmt.Errorf("first operation should be a Create op") } - // The bug Id should be the id of the first operation - if bug.FirstOp().Id() != bug.id { - fmt.Println("bug", bug.id.String()) - fmt.Println("op", bug.FirstOp().Id().String()) - return fmt.Errorf("bug id should be the first commit hash") - } - // Check that there is no more CreateOp op // Check that there is no colliding operation's ID it := NewOperationIterator(bug) @@ -354,7 +340,6 @@ func (bug *Bug) Append(op Operation) { if op.base().OperationType != CreateOp { panic("first operation should be a Create") } - bug.id = op.Id() } bug.staging.Append(op) } @@ -454,15 +439,10 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error { bug.packs = append(bug.packs, bug.staging) bug.staging = OperationPack{} - // if it was the first commit, use the Id of the first op (create) - if bug.id == "" || bug.id == entity.UnsetId { - bug.id = bug.packs[0].Operations[0].Id() - } - // Create or update the Git reference for this bug // When pushing later, the remote will ensure that this ref update // is fast-forward, that is no data has been overwritten - ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.id) + ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.Id().String()) return repo.UpdateRef(ref, hash) } @@ -488,7 +468,7 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { // Reading the other side is still necessary to validate remote data, at least // for new operations - if bug.id != otherBug.id { + if bug.Id() != otherBug.Id() { return false, errors.New("merging unrelated bugs is not supported") } @@ -562,7 +542,7 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { bug.packs = newPacks // Update the git ref - err = repo.UpdateRef(bugsRefPattern+bug.id.String(), bug.lastCommit) + err = repo.UpdateRef(bugsRefPattern+bug.Id().String(), bug.lastCommit) if err != nil { return false, err } @@ -572,12 +552,8 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { // Id return the Bug identifier func (bug *Bug) Id() entity.Id { - if bug.id == "" || bug.id == entity.UnsetId { - // simply panic as it would be a coding error - // (using an id of a bug without operation yet) - panic("no id yet") - } - return bug.id + // id is the id of the first operation + return bug.FirstOp().Id() } // CreateLamportTime return the Lamport time of creation @@ -629,7 +605,7 @@ func (bug *Bug) LastOp() Operation { // Compile a bug in a easily usable snapshot func (bug *Bug) Compile() Snapshot { snap := Snapshot{ - id: bug.id, + id: bug.Id(), Status: OpenStatus, } diff --git a/bug/op_create.go b/bug/op_create.go index 3c8ce6587..41e0fca19 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -38,6 +38,21 @@ func (op *CreateOperation) Id() entity.Id { return idOperation(op) } +// OVERRIDE +func (op *CreateOperation) SetMetadata(key string, value string) { + // sanity check: we make sure we are not in the following scenario: + // - the bug is created with a first operation + // - Id() is used + // - metadata are added, which will change the Id + // - Id() is used again + + if op.id != entity.UnsetId { + panic("usage of Id() after changing the first operation") + } + + op.OpBase.SetMetadata(key, value) +} + func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.addActor(op.Author) snapshot.addParticipant(op.Author) @@ -95,7 +110,7 @@ func (op *CreateOperation) Validate() error { return nil } -// UnmarshalJSON is a two step JSON unmarshaling +// 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 { diff --git a/bug/with_snapshot.go b/bug/with_snapshot.go index 2b2439df8..41192d396 100644 --- a/bug/with_snapshot.go +++ b/bug/with_snapshot.go @@ -47,7 +47,7 @@ func (b *WithSnapshot) Commit(repo repository.ClockedRepo) error { return nil } - b.snap.id = b.Bug.id + b.snap.id = b.Bug.Id() return nil } -- cgit v1.2.3 From d96284da646cc1d3e3d7d3b2f7a1ab0e8e7a4d88 Mon Sep 17 00:00:00 2001 From: vince Date: Thu, 9 Jul 2020 14:59:47 +0800 Subject: Change the comment ID to use both bug and comment ID references. Add comment edit command This commit adds the comment edit command, which provides a CLI tool that allows a user to edit a comment. --- bug/comment.go | 46 ++++++++++++++++++++++++ bug/comment_test.go | 27 +++++++++++++++ bug/op_add_comment.go | 5 +-- bug/op_create.go | 5 +-- bug/snapshot.go | 5 +++ cache/repo_cache_bug.go | 47 +++++++++++++++++++++++++ commands/comment.go | 1 + commands/comment_edit.go | 71 ++++++++++++++++++++++++++++++++++++++ commands/show.go | 3 +- doc/man/git-bug-comment-edit.1 | 35 +++++++++++++++++++ doc/man/git-bug-comment.1 | 2 +- doc/md/git-bug_comment.md | 1 + doc/md/git-bug_comment_edit.md | 20 +++++++++++ misc/bash_completion/git-bug | 33 ++++++++++++++++++ misc/powershell_completion/git-bug | 8 +++++ 15 files changed, 303 insertions(+), 6 deletions(-) create mode 100644 bug/comment_test.go create mode 100644 commands/comment_edit.go create mode 100644 doc/man/git-bug-comment-edit.1 create mode 100644 doc/md/git-bug_comment_edit.md (limited to 'bug/op_create.go') diff --git a/bug/comment.go b/bug/comment.go index 4c9d118eb..1a9ca05af 100644 --- a/bug/comment.go +++ b/bug/comment.go @@ -1,6 +1,8 @@ package bug import ( + "strings" + "github.com/dustin/go-humanize" "github.com/MichaelMure/git-bug/entity" @@ -31,6 +33,50 @@ func (c Comment) Id() entity.Id { return c.id } +const compiledCommentIdFormat = "BCBCBCBBBCBBBBCBBBBCBBBBCBBBBCBBBBCBBBBC" + +// DeriveCommentId compute a merged Id for a comment holding information from +// both the Bug's Id and the Comment's Id. This allow to later find efficiently +// a Comment because we can access the bug directly instead of searching for a +// Bug that has a Comment matching the Id. +// +// To allow the use of an arbitrary length prefix of this merged Id, Ids from Bug +// and Comment are interleaved with this irregular pattern to give the best chance +// to find the Comment even with a 7 character prefix. +// +// A complete merged Id hold 30 characters for the Bug and 10 for the Comment, +// which give a key space of 36^30 for the Bug (~5 * 10^46) and 36^10 for the +// Comment (~3 * 10^15). This asymmetry assume a reasonable number of Comment +// within a Bug, while still allowing for a vast key space for Bug (that is, a +// globally merged bug database) with a low risk of collision. +func DeriveCommentId(bugId entity.Id, commentId entity.Id) entity.Id { + var id strings.Builder + for _, char := range compiledCommentIdFormat { + if char == 'B' { + id.WriteByte(bugId[0]) + bugId = bugId[1:] + } else { + id.WriteByte(commentId[0]) + commentId = commentId[1:] + } + } + return entity.Id(id.String()) +} + +func SplitCommentId(prefix string) (bugPrefix string, commentPrefix string) { + var bugIdPrefix strings.Builder + var commentIdPrefix strings.Builder + + for i, char := range prefix { + if compiledCommentIdFormat[i] == 'B' { + bugIdPrefix.WriteRune(char) + } else { + commentIdPrefix.WriteRune(char) + } + } + return bugIdPrefix.String(), commentIdPrefix.String() +} + // FormatTimeRel format the UnixTime of the comment for human consumption func (c Comment) FormatTimeRel() string { return humanize.Time(c.UnixTime.Time()) diff --git a/bug/comment_test.go b/bug/comment_test.go new file mode 100644 index 000000000..423d10d8f --- /dev/null +++ b/bug/comment_test.go @@ -0,0 +1,27 @@ +package bug + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/MichaelMure/git-bug/entity" +) + +func TestCommentId(t *testing.T) { + bugId := entity.Id("abcdefghijklmnopqrstuvwxyz1234__________") + opId := entity.Id("ABCDEFGHIJ______________________________") + expectedId := entity.Id("aAbBcCdefDghijEklmnFopqrGstuvHwxyzI1234J") + + mergedId := DeriveCommentId(bugId, opId) + require.Equal(t, expectedId, mergedId) + + // full length + splitBugId, splitCommentId := SplitCommentId(mergedId.String()) + require.Equal(t, string(bugId[:30]), splitBugId) + require.Equal(t, string(opId[:10]), splitCommentId) + + splitBugId, splitCommentId = SplitCommentId(string(expectedId[:6])) + require.Equal(t, string(bugId[:3]), splitBugId) + require.Equal(t, string(opId[:3]), splitCommentId) +} diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index 3f19e42e7..df426ee0c 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -36,8 +36,9 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) { snapshot.addActor(op.Author) snapshot.addParticipant(op.Author) + commentId := DeriveCommentId(snapshot.Id(), op.Id()) comment := Comment{ - id: op.Id(), + id: commentId, Message: op.Message, Author: op.Author, Files: op.Files, @@ -47,7 +48,7 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) { snapshot.Comments = append(snapshot.Comments, comment) item := &AddCommentTimelineItem{ - CommentTimelineItem: NewCommentTimelineItem(op.Id(), comment), + CommentTimelineItem: NewCommentTimelineItem(commentId, comment), } snapshot.Timeline = append(snapshot.Timeline, item) diff --git a/bug/op_create.go b/bug/op_create.go index 41e0fca19..15fb69b55 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -59,8 +59,9 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.Title = op.Title + commentId := DeriveCommentId(snapshot.Id(), op.Id()) comment := Comment{ - id: op.Id(), + id: commentId, Message: op.Message, Author: op.Author, UnixTime: timestamp.Timestamp(op.UnixTime), @@ -72,7 +73,7 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.Timeline = []TimelineItem{ &CreateTimelineItem{ - CommentTimelineItem: NewCommentTimelineItem(op.Id(), comment), + CommentTimelineItem: NewCommentTimelineItem(commentId, comment), }, } } diff --git a/bug/snapshot.go b/bug/snapshot.go index 11df04b21..0005b9301 100644 --- a/bug/snapshot.go +++ b/bug/snapshot.go @@ -28,6 +28,11 @@ type Snapshot struct { // Return the Bug identifier func (snap *Snapshot) Id() entity.Id { + if snap.id == "" { + // simply panic as it would be a coding error + // (using an id of a bug not stored yet) + panic("no id yet") + } return snap.id } diff --git a/cache/repo_cache_bug.go b/cache/repo_cache_bug.go index 1701f66d0..cfcbb72d6 100644 --- a/cache/repo_cache_bug.go +++ b/cache/repo_cache_bug.go @@ -261,6 +261,53 @@ func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, erro return matching[0], nil } +// ResolveComment search for a Bug/Comment combination matching the merged +// bug/comment Id prefix. Returns the Bug containing the Comment and the Comment's +// Id. +func (c *RepoCache) ResolveComment(prefix string) (*BugCache, entity.Id, error) { + bugPrefix, _ := bug.SplitCommentId(prefix) + bugCandidate := make([]entity.Id, 0, 5) + + // build a list of possible matching bugs + c.muBug.RLock() + for _, excerpt := range c.bugExcerpts { + if excerpt.Id.HasPrefix(bugPrefix) { + bugCandidate = append(bugCandidate, excerpt.Id) + } + } + c.muBug.RUnlock() + + matchingBugIds := make([]entity.Id, 0, 5) + matchingCommentId := entity.UnsetId + var matchingBug *BugCache + + // search for matching comments + // searching every bug candidate allow for some collision with the bug prefix only, + // before being refined with the full comment prefix + for _, bugId := range bugCandidate { + b, err := c.ResolveBug(bugId) + if err != nil { + return nil, entity.UnsetId, err + } + + for _, comment := range b.Snapshot().Comments { + if comment.Id().HasPrefix(prefix) { + matchingBugIds = append(matchingBugIds, bugId) + matchingBug = b + matchingCommentId = comment.Id() + } + } + } + + if len(matchingBugIds) > 1 { + return nil, entity.UnsetId, entity.NewErrMultipleMatch("bug/comment", matchingBugIds) + } else if len(matchingBugIds) == 0 { + return nil, entity.UnsetId, errors.New("comment doesn't exist") + } + + return matchingBug, matchingCommentId, nil +} + // QueryBugs return the id of all Bug matching the given Query func (c *RepoCache) QueryBugs(q *query.Query) ([]entity.Id, error) { c.muBug.RLock() diff --git a/commands/comment.go b/commands/comment.go index d8995c3ed..eb90624aa 100644 --- a/commands/comment.go +++ b/commands/comment.go @@ -22,6 +22,7 @@ func newCommentCommand() *cobra.Command { } cmd.AddCommand(newCommentAddCommand()) + cmd.AddCommand(newCommentEditCommand()) return cmd } diff --git a/commands/comment_edit.go b/commands/comment_edit.go new file mode 100644 index 000000000..61132967b --- /dev/null +++ b/commands/comment_edit.go @@ -0,0 +1,71 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/input" +) + +type commentEditOptions struct { + messageFile string + message string +} + +func newCommentEditCommand() *cobra.Command { + env := newEnv() + options := commentEditOptions{} + + cmd := &cobra.Command{ + Use: "edit ", + Short: "Edit an existing comment on a bug.", + Args: cobra.ExactArgs(1), + PreRunE: loadBackendEnsureUser(env), + PostRunE: closeBackend(env), + RunE: func(cmd *cobra.Command, args []string) error { + return runCommentEdit(env, options, args) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + + flags.StringVarP(&options.messageFile, "file", "F", "", + "Take the message from the given file. Use - to read the message from the standard input") + + flags.StringVarP(&options.message, "message", "m", "", + "Provide the new message from the command line") + + return cmd +} + +func runCommentEdit(env *Env, opts commentEditOptions, args []string) error { + b, commentId, err := env.backend.ResolveComment(args[0]) + if err != nil { + return err + } + + if opts.messageFile != "" && opts.message == "" { + opts.message, err = input.BugCommentFileInput(opts.messageFile) + if err != nil { + return err + } + } + + if opts.messageFile == "" && opts.message == "" { + opts.message, err = input.BugCommentEditorInput(env.backend, "") + if err == input.ErrEmptyMessage { + env.err.Println("Empty message, aborting.") + return nil + } + if err != nil { + return err + } + } + + _, err = b.EditComment(commentId, opts.message) + if err != nil { + return err + } + + return b.Commit() +} diff --git a/commands/show.go b/commands/show.go index 9ebd1926d..10087f927 100644 --- a/commands/show.go +++ b/commands/show.go @@ -158,8 +158,9 @@ func showDefaultFormatter(env *Env, snapshot *bug.Snapshot) error { for i, comment := range snapshot.Comments { var message string - env.out.Printf("%s#%d %s <%s>\n\n", + env.out.Printf("%s%s #%d %s <%s>\n\n", indent, + comment.Id().Human(), i, comment.Author.DisplayName(), comment.Author.Email(), diff --git a/doc/man/git-bug-comment-edit.1 b/doc/man/git-bug-comment-edit.1 new file mode 100644 index 000000000..e3cb2daf0 --- /dev/null +++ b/doc/man/git-bug-comment-edit.1 @@ -0,0 +1,35 @@ +.nh +.TH "GIT\-BUG" "1" "Apr 2019" "Generated from git\-bug's source code" "" + +.SH NAME +.PP +git\-bug\-comment\-edit \- Edit an existing comment on a bug. + + +.SH SYNOPSIS +.PP +\fBgit\-bug comment edit [flags]\fP + + +.SH DESCRIPTION +.PP +Edit an existing comment on a bug. + + +.SH OPTIONS +.PP +\fB\-F\fP, \fB\-\-file\fP="" + Take the message from the given file. Use \- to read the message from the standard input + +.PP +\fB\-m\fP, \fB\-\-message\fP="" + Provide the new message from the command line + +.PP +\fB\-h\fP, \fB\-\-help\fP[=false] + help for edit + + +.SH SEE ALSO +.PP +\fBgit\-bug\-comment(1)\fP diff --git a/doc/man/git-bug-comment.1 b/doc/man/git-bug-comment.1 index 7cad5a0d4..cb0740bb8 100644 --- a/doc/man/git-bug-comment.1 +++ b/doc/man/git-bug-comment.1 @@ -24,4 +24,4 @@ Display or add comments to a bug. .SH SEE ALSO .PP -\fBgit\-bug(1)\fP, \fBgit\-bug\-comment\-add(1)\fP +\fBgit\-bug(1)\fP, \fBgit\-bug\-comment\-add(1)\fP, \fBgit\-bug\-comment\-edit(1)\fP diff --git a/doc/md/git-bug_comment.md b/doc/md/git-bug_comment.md index 6ac7c45ba..48050a979 100644 --- a/doc/md/git-bug_comment.md +++ b/doc/md/git-bug_comment.md @@ -16,4 +16,5 @@ git-bug comment [ID] [flags] * [git-bug](git-bug.md) - A bug tracker embedded in Git. * [git-bug comment add](git-bug_comment_add.md) - Add a new comment to a bug. +* [git-bug comment edit](git-bug_comment_edit.md) - Edit an existing comment on a bug. diff --git a/doc/md/git-bug_comment_edit.md b/doc/md/git-bug_comment_edit.md new file mode 100644 index 000000000..2546dff16 --- /dev/null +++ b/doc/md/git-bug_comment_edit.md @@ -0,0 +1,20 @@ +## git-bug comment edit + +Edit an existing comment on a bug. + +``` +git-bug comment edit [flags] +``` + +### Options + +``` + -F, --file string Take the message from the given file. Use - to read the message from the standard input + -m, --message string Provide the new message from the command line + -h, --help help for edit +``` + +### SEE ALSO + +* [git-bug comment](git-bug_comment.md) - Display or add comments to a bug. + diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 912e87b48..b3103e885 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -722,6 +722,38 @@ _git-bug_comment_add() noun_aliases=() } +_git-bug_comment_edit() +{ + last_command="git-bug_comment_edit" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--file=") + two_word_flags+=("--file") + two_word_flags+=("-F") + local_nonpersistent_flags+=("--file") + local_nonpersistent_flags+=("--file=") + local_nonpersistent_flags+=("-F") + flags+=("--message=") + two_word_flags+=("--message") + two_word_flags+=("-m") + local_nonpersistent_flags+=("--message") + local_nonpersistent_flags+=("--message=") + local_nonpersistent_flags+=("-m") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _git-bug_comment() { last_command="git-bug_comment" @@ -730,6 +762,7 @@ _git-bug_comment() commands=() commands+=("add") + commands+=("edit") flags=() two_word_flags=() diff --git a/misc/powershell_completion/git-bug b/misc/powershell_completion/git-bug index 29c282378..5ff155156 100644 --- a/misc/powershell_completion/git-bug +++ b/misc/powershell_completion/git-bug @@ -118,6 +118,7 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock { } 'git-bug;comment' { [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new comment to a bug.') + [CompletionResult]::new('edit', 'edit', [CompletionResultType]::ParameterValue, 'Edit an existing comment on a bug.') break } 'git-bug;comment;add' { @@ -127,6 +128,13 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock { [CompletionResult]::new('--message', 'message', [CompletionResultType]::ParameterName, 'Provide the new message from the command line') break } + 'git-bug;comment;edit' { + [CompletionResult]::new('-F', 'F', [CompletionResultType]::ParameterName, 'Take the message from the given file. Use - to read the message from the standard input') + [CompletionResult]::new('--file', 'file', [CompletionResultType]::ParameterName, 'Take the message from the given file. Use - to read the message from the standard input') + [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Provide the new message from the command line') + [CompletionResult]::new('--message', 'message', [CompletionResultType]::ParameterName, 'Provide the new message from the command line') + break + } 'git-bug;deselect' { break } -- cgit v1.2.3 From fcf43915e1736fe0b56f8f06386f68d9b56da7a8 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Mon, 30 Nov 2020 00:41:50 +0100 Subject: bug: fix tests --- bug/op_create.go | 7 +++++++ bug/op_create_test.go | 7 +++++-- bug/op_edit_comment_test.go | 30 +++++++++++++++--------------- bug/op_set_metadata_test.go | 12 ++++++------ 4 files changed, 33 insertions(+), 23 deletions(-) (limited to 'bug/op_create.go') diff --git a/bug/op_create.go b/bug/op_create.go index 15fb69b55..044ddd72b 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -54,6 +54,13 @@ func (op *CreateOperation) SetMetadata(key string, value string) { } 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") + } + + snapshot.id = op.Id() + snapshot.addActor(op.Author) snapshot.addParticipant(op.Author) diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 533aec2e9..73a657784 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -29,14 +29,17 @@ func TestCreate(t *testing.T) { id := create.Id() require.NoError(t, id.Validate()) + commentId := DeriveCommentId(create.Id(), create.Id()) + comment := Comment{ - id: id, + id: commentId, Author: rene, Message: "message", UnixTime: timestamp.Timestamp(create.UnixTime), } expected := Snapshot{ + id: create.Id(), Title: "title", Comments: []Comment{ comment, @@ -47,7 +50,7 @@ func TestCreate(t *testing.T) { CreateTime: create.Time(), Timeline: []TimelineItem{ &CreateTimelineItem{ - CommentTimelineItem: NewCommentTimelineItem(id, comment), + CommentTimelineItem: NewCommentTimelineItem(commentId, comment), }, }, } diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index 92ee7539b..a73309329 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -43,35 +43,35 @@ func TestEdit(t *testing.T) { id3 := comment2.Id() require.NoError(t, id3.Validate()) - edit := NewEditCommentOp(rene, unix, id1, "create edited", nil) + edit := NewEditCommentOp(rene, unix, snapshot.Comments[0].Id(), "create edited", nil) edit.Apply(&snapshot) - require.Equal(t, len(snapshot.Timeline), 4) - require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) - require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 1) - require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1) + require.Len(t, snapshot.Timeline, 4) + require.Len(t, snapshot.Timeline[0].(*CreateTimelineItem).History, 2) + require.Len(t, snapshot.Timeline[1].(*AddCommentTimelineItem).History, 1) + require.Len(t, snapshot.Timeline[3].(*AddCommentTimelineItem).History, 1) require.Equal(t, snapshot.Comments[0].Message, "create edited") require.Equal(t, snapshot.Comments[1].Message, "comment 1") require.Equal(t, snapshot.Comments[2].Message, "comment 2") - edit2 := NewEditCommentOp(rene, unix, id2, "comment 1 edited", nil) + edit2 := NewEditCommentOp(rene, unix, snapshot.Comments[1].Id(), "comment 1 edited", nil) edit2.Apply(&snapshot) - require.Equal(t, len(snapshot.Timeline), 4) - require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) - require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2) - require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 1) + require.Len(t, snapshot.Timeline, 4) + require.Len(t, snapshot.Timeline[0].(*CreateTimelineItem).History, 2) + require.Len(t, snapshot.Timeline[1].(*AddCommentTimelineItem).History, 2) + require.Len(t, snapshot.Timeline[3].(*AddCommentTimelineItem).History, 1) require.Equal(t, snapshot.Comments[0].Message, "create edited") require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited") require.Equal(t, snapshot.Comments[2].Message, "comment 2") - edit3 := NewEditCommentOp(rene, unix, id3, "comment 2 edited", nil) + edit3 := NewEditCommentOp(rene, unix, snapshot.Comments[2].Id(), "comment 2 edited", nil) edit3.Apply(&snapshot) - require.Equal(t, len(snapshot.Timeline), 4) - require.Equal(t, len(snapshot.Timeline[0].(*CreateTimelineItem).History), 2) - require.Equal(t, len(snapshot.Timeline[1].(*AddCommentTimelineItem).History), 2) - require.Equal(t, len(snapshot.Timeline[3].(*AddCommentTimelineItem).History), 2) + require.Len(t, snapshot.Timeline, 4) + require.Len(t, snapshot.Timeline[0].(*CreateTimelineItem).History, 2) + require.Len(t, snapshot.Timeline[1].(*AddCommentTimelineItem).History, 2) + require.Len(t, snapshot.Timeline[3].(*AddCommentTimelineItem).History, 2) require.Equal(t, snapshot.Comments[0].Message, "create edited") require.Equal(t, snapshot.Comments[1].Message, "comment 1 edited") require.Equal(t, snapshot.Comments[2].Message, "comment 2 edited") diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index c90f192af..c0c91617c 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -46,14 +46,14 @@ func TestSetMetadata(t *testing.T) { snapshot.Operations = append(snapshot.Operations, op1) createMetadata := snapshot.Operations[0].AllMetadata() - require.Equal(t, len(createMetadata), 2) + 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.Equal(t, len(commentMetadata), 1) + require.Len(t, commentMetadata, 1) require.Equal(t, commentMetadata["key2"], "value2") op2 := NewSetMetadataOp(rene, unix, id2, map[string]string{ @@ -65,12 +65,12 @@ func TestSetMetadata(t *testing.T) { snapshot.Operations = append(snapshot.Operations, op2) createMetadata = snapshot.Operations[0].AllMetadata() - require.Equal(t, len(createMetadata), 2) + require.Len(t, createMetadata, 2) require.Equal(t, createMetadata["key"], "value") require.Equal(t, createMetadata["key2"], "value") commentMetadata = snapshot.Operations[1].AllMetadata() - require.Equal(t, len(commentMetadata), 2) + require.Len(t, commentMetadata, 2) // original key is not overrided require.Equal(t, commentMetadata["key2"], "value2") // new key is set @@ -85,14 +85,14 @@ func TestSetMetadata(t *testing.T) { snapshot.Operations = append(snapshot.Operations, op3) createMetadata = snapshot.Operations[0].AllMetadata() - require.Equal(t, len(createMetadata), 2) + 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.Equal(t, len(commentMetadata), 2) + require.Len(t, commentMetadata, 2) require.Equal(t, commentMetadata["key2"], "value2") require.Equal(t, commentMetadata["key3"], "value3") } -- cgit v1.2.3 From db7074301b6af895b1a47ecd12a5028ac809abfc Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Mon, 30 Nov 2020 01:55:30 +0100 Subject: entity: generalize the combined Ids, use 64 length --- bug/comment.go | 46 ----------------------------- bug/comment_test.go | 27 ----------------- bug/op_add_comment.go | 2 +- bug/op_create.go | 2 +- bug/op_create_test.go | 3 +- cache/repo_cache_bug.go | 2 +- entity/id.go | 2 +- entity/id_interleaved.go | 68 +++++++++++++++++++++++++++++++++++++++++++ entity/id_interleaved_test.go | 36 +++++++++++++++++++++++ 9 files changed, 110 insertions(+), 78 deletions(-) delete mode 100644 bug/comment_test.go create mode 100644 entity/id_interleaved.go create mode 100644 entity/id_interleaved_test.go (limited to 'bug/op_create.go') diff --git a/bug/comment.go b/bug/comment.go index 1a9ca05af..4c9d118eb 100644 --- a/bug/comment.go +++ b/bug/comment.go @@ -1,8 +1,6 @@ package bug import ( - "strings" - "github.com/dustin/go-humanize" "github.com/MichaelMure/git-bug/entity" @@ -33,50 +31,6 @@ func (c Comment) Id() entity.Id { return c.id } -const compiledCommentIdFormat = "BCBCBCBBBCBBBBCBBBBCBBBBCBBBBCBBBBCBBBBC" - -// DeriveCommentId compute a merged Id for a comment holding information from -// both the Bug's Id and the Comment's Id. This allow to later find efficiently -// a Comment because we can access the bug directly instead of searching for a -// Bug that has a Comment matching the Id. -// -// To allow the use of an arbitrary length prefix of this merged Id, Ids from Bug -// and Comment are interleaved with this irregular pattern to give the best chance -// to find the Comment even with a 7 character prefix. -// -// A complete merged Id hold 30 characters for the Bug and 10 for the Comment, -// which give a key space of 36^30 for the Bug (~5 * 10^46) and 36^10 for the -// Comment (~3 * 10^15). This asymmetry assume a reasonable number of Comment -// within a Bug, while still allowing for a vast key space for Bug (that is, a -// globally merged bug database) with a low risk of collision. -func DeriveCommentId(bugId entity.Id, commentId entity.Id) entity.Id { - var id strings.Builder - for _, char := range compiledCommentIdFormat { - if char == 'B' { - id.WriteByte(bugId[0]) - bugId = bugId[1:] - } else { - id.WriteByte(commentId[0]) - commentId = commentId[1:] - } - } - return entity.Id(id.String()) -} - -func SplitCommentId(prefix string) (bugPrefix string, commentPrefix string) { - var bugIdPrefix strings.Builder - var commentIdPrefix strings.Builder - - for i, char := range prefix { - if compiledCommentIdFormat[i] == 'B' { - bugIdPrefix.WriteRune(char) - } else { - commentIdPrefix.WriteRune(char) - } - } - return bugIdPrefix.String(), commentIdPrefix.String() -} - // FormatTimeRel format the UnixTime of the comment for human consumption func (c Comment) FormatTimeRel() string { return humanize.Time(c.UnixTime.Time()) diff --git a/bug/comment_test.go b/bug/comment_test.go deleted file mode 100644 index 423d10d8f..000000000 --- a/bug/comment_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package bug - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/entity" -) - -func TestCommentId(t *testing.T) { - bugId := entity.Id("abcdefghijklmnopqrstuvwxyz1234__________") - opId := entity.Id("ABCDEFGHIJ______________________________") - expectedId := entity.Id("aAbBcCdefDghijEklmnFopqrGstuvHwxyzI1234J") - - mergedId := DeriveCommentId(bugId, opId) - require.Equal(t, expectedId, mergedId) - - // full length - splitBugId, splitCommentId := SplitCommentId(mergedId.String()) - require.Equal(t, string(bugId[:30]), splitBugId) - require.Equal(t, string(opId[:10]), splitCommentId) - - splitBugId, splitCommentId = SplitCommentId(string(expectedId[:6])) - require.Equal(t, string(bugId[:3]), splitBugId) - require.Equal(t, string(opId[:3]), splitCommentId) -} diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index df426ee0c..e52c46fde 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -36,7 +36,7 @@ func (op *AddCommentOperation) Apply(snapshot *Snapshot) { snapshot.addActor(op.Author) snapshot.addParticipant(op.Author) - commentId := DeriveCommentId(snapshot.Id(), op.Id()) + commentId := entity.CombineIds(snapshot.Id(), op.Id()) comment := Comment{ id: commentId, Message: op.Message, diff --git a/bug/op_create.go b/bug/op_create.go index 044ddd72b..1e944d136 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -66,7 +66,7 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.Title = op.Title - commentId := DeriveCommentId(snapshot.Id(), op.Id()) + commentId := entity.CombineIds(snapshot.Id(), op.Id()) comment := Comment{ id: commentId, Message: op.Message, diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 73a657784..456357c4b 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/timestamp" @@ -29,7 +30,7 @@ func TestCreate(t *testing.T) { id := create.Id() require.NoError(t, id.Validate()) - commentId := DeriveCommentId(create.Id(), create.Id()) + commentId := entity.CombineIds(create.Id(), create.Id()) comment := Comment{ id: commentId, diff --git a/cache/repo_cache_bug.go b/cache/repo_cache_bug.go index cfcbb72d6..90b9a892b 100644 --- a/cache/repo_cache_bug.go +++ b/cache/repo_cache_bug.go @@ -265,7 +265,7 @@ func (c *RepoCache) resolveBugMatcher(f func(*BugExcerpt) bool) (entity.Id, erro // bug/comment Id prefix. Returns the Bug containing the Comment and the Comment's // Id. func (c *RepoCache) ResolveComment(prefix string) (*BugCache, entity.Id, error) { - bugPrefix, _ := bug.SplitCommentId(prefix) + bugPrefix, _ := entity.SeparateIds(prefix) bugCandidate := make([]entity.Id, 0, 5) // build a list of possible matching bugs diff --git a/entity/id.go b/entity/id.go index 9e7240127..b602452e7 100644 --- a/entity/id.go +++ b/entity/id.go @@ -65,7 +65,7 @@ func (i Id) MarshalGQL(w io.Writer) { // IsValid tell if the Id is valid func (i Id) Validate() error { - // Special case to + // Special case to detect outdated repo if len(i) == 40 { return fmt.Errorf("outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade") } diff --git a/entity/id_interleaved.go b/entity/id_interleaved.go new file mode 100644 index 000000000..5423afeed --- /dev/null +++ b/entity/id_interleaved.go @@ -0,0 +1,68 @@ +package entity + +import ( + "strings" +) + +// CombineIds compute a merged Id holding information from both the primary Id +// and the secondary Id. +// +// This allow to later find efficiently a secondary element because we can access +// the primary one directly instead of searching for a primary that has a +// secondary matching the Id. +// +// An example usage is Comment in a Bug. The interleaved Id will hold part of the +// Bug Id and part of the Comment Id. +// +// To allow the use of an arbitrary length prefix of this Id, Ids from primary +// and secondary are interleaved with this irregular pattern to give the +// best chance to find the secondary even with a 7 character prefix. +// +// Format is: PSPSPSPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPP +// +// A complete interleaved Id hold 50 characters for the primary and 14 for the +// secondary, which give a key space of 36^50 for the primary (~6 * 10^77) and +// 36^14 for the secondary (~6 * 10^21). This asymmetry assume a reasonable number +// of secondary within a primary Entity, while still allowing for a vast key space +// for the primary (that is, a globally merged database) with a low risk of collision. +// +// Here is the breakdown of several common prefix length: +// +// 5: 3P, 2S +// 7: 4P, 3S +// 10: 6P, 4S +// 16: 11P, 5S +func CombineIds(primary Id, secondary Id) Id { + var id strings.Builder + + for i := 0; i < idLength; i++ { + switch { + default: + id.WriteByte(primary[0]) + primary = primary[1:] + case i == 1, i == 3, i == 5, i == 9, i >= 10 && i%5 == 4: + id.WriteByte(secondary[0]) + secondary = secondary[1:] + } + } + + return Id(id.String()) +} + +// SeparateIds extract primary and secondary prefix from an arbitrary length prefix +// of an Id created with CombineIds. +func SeparateIds(prefix string) (primaryPrefix string, secondaryPrefix string) { + var primary strings.Builder + var secondary strings.Builder + + for i, r := range prefix { + switch { + default: + primary.WriteRune(r) + case i == 1, i == 3, i == 5, i == 9, i >= 10 && i%5 == 4: + secondary.WriteRune(r) + } + } + + return primary.String(), secondary.String() +} diff --git a/entity/id_interleaved_test.go b/entity/id_interleaved_test.go new file mode 100644 index 000000000..ef9218c97 --- /dev/null +++ b/entity/id_interleaved_test.go @@ -0,0 +1,36 @@ +package entity + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInterleaved(t *testing.T) { + primary := Id("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX______________") + secondary := Id("YZ0123456789+/________________________________________________") + expectedId := Id("aYbZc0def1ghij2klmn3opqr4stuv5wxyz6ABCD7EFGH8IJKL9MNOP+QRST/UVWX") + + interleaved := CombineIds(primary, secondary) + require.Equal(t, expectedId, interleaved) + + // full length + splitPrimary, splitSecondary := SeparateIds(interleaved.String()) + require.Equal(t, string(primary[:50]), splitPrimary) + require.Equal(t, string(secondary[:14]), splitSecondary) + + // partial + splitPrimary, splitSecondary = SeparateIds(string(expectedId[:7])) + require.Equal(t, string(primary[:4]), splitPrimary) + require.Equal(t, string(secondary[:3]), splitSecondary) + + // partial + splitPrimary, splitSecondary = SeparateIds(string(expectedId[:10])) + require.Equal(t, string(primary[:6]), splitPrimary) + require.Equal(t, string(secondary[:4]), splitSecondary) + + // partial + splitPrimary, splitSecondary = SeparateIds(string(expectedId[:16])) + require.Equal(t, string(primary[:11]), splitPrimary) + require.Equal(t, string(secondary[:5]), splitSecondary) +} -- cgit v1.2.3 From 3f6ef50883492f77995a7e27872d0b5ae17b9d6a Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 14 Feb 2021 11:36:32 +0100 Subject: bug: migrate to the DAG entity structure! --- api/graphql/resolvers/operations.go | 12 +- api/graphql/resolvers/query.go | 13 - bridge/github/export.go | 2 +- bridge/github/import_test.go | 27 +- bridge/gitlab/export.go | 2 +- bridge/gitlab/import_test.go | 27 +- bridge/jira/export.go | 2 +- bug/bug.go | 594 +++++---------------------------- bug/bug_actions.go | 106 +----- bug/bug_actions_test.go | 394 ---------------------- bug/bug_test.go | 190 ----------- bug/clocks.go | 40 --- bug/err.go | 17 + bug/git_tree.go | 84 ----- bug/identity.go | 27 -- bug/interface.go | 8 +- bug/op_add_comment.go | 14 +- bug/op_add_comment_test.go | 4 +- bug/op_create.go | 16 +- bug/op_create_test.go | 4 +- bug/op_edit_comment.go | 10 +- bug/op_edit_comment_test.go | 4 +- bug/op_label_change.go | 12 +- bug/op_label_change_test.go | 4 +- bug/op_noop.go | 8 +- bug/op_noop_test.go | 4 +- bug/op_set_metadata.go | 21 +- bug/op_set_metadata_test.go | 4 +- bug/op_set_status.go | 12 +- bug/op_set_status_test.go | 4 +- bug/op_set_title.go | 24 +- bug/op_set_title_test.go | 4 +- bug/operation.go | 152 ++++++--- bug/operation_iterator.go | 72 ---- bug/operation_iterator_test.go | 79 ----- bug/operation_pack.go | 187 ----------- bug/operation_pack_test.go | 78 ----- bug/operation_test.go | 4 +- bug/sorting.go | 8 +- bug/with_snapshot.go | 6 - cache/bug_cache.go | 4 +- cache/bug_excerpt.go | 2 +- cache/repo_cache.go | 2 +- cache/repo_cache_bug.go | 2 +- cache/repo_cache_common.go | 15 +- cache/repo_cache_test.go | 4 + entity/TODO | 9 - entity/merge.go | 6 +- go.sum | 1 + misc/random_bugs/create_random_bugs.go | 46 --- tests/read_bugs_test.go | 4 +- 51 files changed, 335 insertions(+), 2040 deletions(-) delete mode 100644 bug/bug_actions_test.go delete mode 100644 bug/bug_test.go delete mode 100644 bug/clocks.go create mode 100644 bug/err.go delete mode 100644 bug/git_tree.go delete mode 100644 bug/identity.go delete mode 100644 bug/operation_iterator.go delete mode 100644 bug/operation_iterator_test.go delete mode 100644 bug/operation_pack.go delete mode 100644 bug/operation_pack_test.go delete mode 100644 entity/TODO (limited to 'bug/op_create.go') diff --git a/api/graphql/resolvers/operations.go b/api/graphql/resolvers/operations.go index 8d3e5bba2..0ede9f136 100644 --- a/api/graphql/resolvers/operations.go +++ b/api/graphql/resolvers/operations.go @@ -19,7 +19,7 @@ func (createOperationResolver) ID(_ context.Context, obj *bug.CreateOperation) ( } func (createOperationResolver) Author(_ context.Context, obj *bug.CreateOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (createOperationResolver) Date(_ context.Context, obj *bug.CreateOperation) (*time.Time, error) { @@ -36,7 +36,7 @@ func (addCommentOperationResolver) ID(_ context.Context, obj *bug.AddCommentOper } func (addCommentOperationResolver) Author(_ context.Context, obj *bug.AddCommentOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (addCommentOperationResolver) Date(_ context.Context, obj *bug.AddCommentOperation) (*time.Time, error) { @@ -57,7 +57,7 @@ func (editCommentOperationResolver) Target(_ context.Context, obj *bug.EditComme } func (editCommentOperationResolver) Author(_ context.Context, obj *bug.EditCommentOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (editCommentOperationResolver) Date(_ context.Context, obj *bug.EditCommentOperation) (*time.Time, error) { @@ -74,7 +74,7 @@ func (labelChangeOperationResolver) ID(_ context.Context, obj *bug.LabelChangeOp } func (labelChangeOperationResolver) Author(_ context.Context, obj *bug.LabelChangeOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (labelChangeOperationResolver) Date(_ context.Context, obj *bug.LabelChangeOperation) (*time.Time, error) { @@ -91,7 +91,7 @@ func (setStatusOperationResolver) ID(_ context.Context, obj *bug.SetStatusOperat } func (setStatusOperationResolver) Author(_ context.Context, obj *bug.SetStatusOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (setStatusOperationResolver) Date(_ context.Context, obj *bug.SetStatusOperation) (*time.Time, error) { @@ -112,7 +112,7 @@ func (setTitleOperationResolver) ID(_ context.Context, obj *bug.SetTitleOperatio } func (setTitleOperationResolver) Author(_ context.Context, obj *bug.SetTitleOperation) (models.IdentityWrapper, error) { - return models.NewLoadedIdentity(obj.Author), nil + return models.NewLoadedIdentity(obj.Author()), nil } func (setTitleOperationResolver) Date(_ context.Context, obj *bug.SetTitleOperation) (*time.Time, error) { diff --git a/api/graphql/resolvers/query.go b/api/graphql/resolvers/query.go index 4ad7ae0c9..b20035555 100644 --- a/api/graphql/resolvers/query.go +++ b/api/graphql/resolvers/query.go @@ -14,19 +14,6 @@ type rootQueryResolver struct { cache *cache.MultiRepoCache } -func (r rootQueryResolver) DefaultRepository(_ context.Context) (*models.Repository, error) { - repo, err := r.cache.DefaultRepo() - - if err != nil { - return nil, err - } - - return &models.Repository{ - Cache: r.cache, - Repo: repo, - }, nil -} - func (r rootQueryResolver) Repository(_ context.Context, ref *string) (*models.Repository, error) { var repo *cache.RepoCache var err error diff --git a/bridge/github/export.go b/bridge/github/export.go index 57b52ee06..1a59fbb3d 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -294,7 +294,7 @@ func (ge *githubExporter) exportBug(ctx context.Context, b *cache.BugCache, out continue } - opAuthor := op.GetAuthor() + opAuthor := op.Author() client, err := ge.getClientForIdentity(opAuthor.Id()) if err != nil { continue diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index 84bf774e5..324d54211 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -182,29 +182,24 @@ func TestGithubImporter(t *testing.T) { for i, op := range tt.bug.Operations { require.IsType(t, ops[i], op) + require.Equal(t, op.Author().Name(), ops[i].Author().Name()) - switch op.(type) { + switch op := op.(type) { case *bug.CreateOperation: - require.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title) - require.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message) - require.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name()) + require.Equal(t, op.Title, ops[i].(*bug.CreateOperation).Title) + require.Equal(t, op.Message, ops[i].(*bug.CreateOperation).Message) case *bug.SetStatusOperation: - require.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status) - require.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name()) + require.Equal(t, op.Status, ops[i].(*bug.SetStatusOperation).Status) case *bug.SetTitleOperation: - require.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was) - require.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title) - require.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name()) + require.Equal(t, op.Was, ops[i].(*bug.SetTitleOperation).Was) + require.Equal(t, op.Title, ops[i].(*bug.SetTitleOperation).Title) case *bug.LabelChangeOperation: - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added) - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed) - require.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name()) + require.ElementsMatch(t, op.Added, ops[i].(*bug.LabelChangeOperation).Added) + require.ElementsMatch(t, op.Removed, ops[i].(*bug.LabelChangeOperation).Removed) case *bug.AddCommentOperation: - require.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message) - require.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.AddCommentOperation).Message) case *bug.EditCommentOperation: - require.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message) - require.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.EditCommentOperation).Message) default: panic("unknown operation type") diff --git a/bridge/gitlab/export.go b/bridge/gitlab/export.go index c3aa62021..9c3864ecc 100644 --- a/bridge/gitlab/export.go +++ b/bridge/gitlab/export.go @@ -267,7 +267,7 @@ func (ge *gitlabExporter) exportBug(ctx context.Context, b *cache.BugCache, out continue } - opAuthor := op.GetAuthor() + opAuthor := op.Author() client, err := ge.getIdentityClient(opAuthor.Id()) if err != nil { continue diff --git a/bridge/gitlab/import_test.go b/bridge/gitlab/import_test.go index 2956ad8b6..1405e43bb 100644 --- a/bridge/gitlab/import_test.go +++ b/bridge/gitlab/import_test.go @@ -138,29 +138,24 @@ func TestGitlabImport(t *testing.T) { for i, op := range tt.bug.Operations { require.IsType(t, ops[i], op) + require.Equal(t, op.Author().Name(), ops[i].Author().Name()) - switch op.(type) { + switch op := op.(type) { case *bug.CreateOperation: - require.Equal(t, op.(*bug.CreateOperation).Title, ops[i].(*bug.CreateOperation).Title) - require.Equal(t, op.(*bug.CreateOperation).Message, ops[i].(*bug.CreateOperation).Message) - require.Equal(t, op.(*bug.CreateOperation).Author.Name(), ops[i].(*bug.CreateOperation).Author.Name()) + require.Equal(t, op.Title, ops[i].(*bug.CreateOperation).Title) + require.Equal(t, op.Message, ops[i].(*bug.CreateOperation).Message) case *bug.SetStatusOperation: - require.Equal(t, op.(*bug.SetStatusOperation).Status, ops[i].(*bug.SetStatusOperation).Status) - require.Equal(t, op.(*bug.SetStatusOperation).Author.Name(), ops[i].(*bug.SetStatusOperation).Author.Name()) + require.Equal(t, op.Status, ops[i].(*bug.SetStatusOperation).Status) case *bug.SetTitleOperation: - require.Equal(t, op.(*bug.SetTitleOperation).Was, ops[i].(*bug.SetTitleOperation).Was) - require.Equal(t, op.(*bug.SetTitleOperation).Title, ops[i].(*bug.SetTitleOperation).Title) - require.Equal(t, op.(*bug.SetTitleOperation).Author.Name(), ops[i].(*bug.SetTitleOperation).Author.Name()) + require.Equal(t, op.Was, ops[i].(*bug.SetTitleOperation).Was) + require.Equal(t, op.Title, ops[i].(*bug.SetTitleOperation).Title) case *bug.LabelChangeOperation: - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Added, ops[i].(*bug.LabelChangeOperation).Added) - require.ElementsMatch(t, op.(*bug.LabelChangeOperation).Removed, ops[i].(*bug.LabelChangeOperation).Removed) - require.Equal(t, op.(*bug.LabelChangeOperation).Author.Name(), ops[i].(*bug.LabelChangeOperation).Author.Name()) + require.ElementsMatch(t, op.Added, ops[i].(*bug.LabelChangeOperation).Added) + require.ElementsMatch(t, op.Removed, ops[i].(*bug.LabelChangeOperation).Removed) case *bug.AddCommentOperation: - require.Equal(t, op.(*bug.AddCommentOperation).Message, ops[i].(*bug.AddCommentOperation).Message) - require.Equal(t, op.(*bug.AddCommentOperation).Author.Name(), ops[i].(*bug.AddCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.AddCommentOperation).Message) case *bug.EditCommentOperation: - require.Equal(t, op.(*bug.EditCommentOperation).Message, ops[i].(*bug.EditCommentOperation).Message) - require.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name()) + require.Equal(t, op.Message, ops[i].(*bug.EditCommentOperation).Message) default: panic("unknown operation type") diff --git a/bridge/jira/export.go b/bridge/jira/export.go index e61679668..34f41d097 100644 --- a/bridge/jira/export.go +++ b/bridge/jira/export.go @@ -309,7 +309,7 @@ func (je *jiraExporter) exportBug(ctx context.Context, b *cache.BugCache, out ch continue } - opAuthor := op.GetAuthor() + opAuthor := op.Author() client, err := je.getClientForIdentity(opAuthor.Id()) if err != nil { out <- core.NewExportError( diff --git a/bug/bug.go b/bug/bug.go index 0c66f8acd..9d19a42cb 100644 --- a/bug/bug.go +++ b/bug/bug.go @@ -2,222 +2,62 @@ 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/repository" - "github.com/MichaelMure/git-bug/util/lamport" ) -const bugsRefPattern = "refs/bugs/" -const bugsRemoteRefPattern = "refs/remotes/%s/bugs/" - -const opsEntryName = "ops" -const mediaEntryName = "media" - -const createClockEntryPrefix = "create-clock-" -const createClockEntryPattern = "create-clock-%d" -const editClockEntryPrefix = "edit-clock-" -const editClockEntryPattern = "edit-clock-%d" - -const creationClockName = "bug-create" -const editClockName = "bug-edit" +var _ Interface = &Bug{} +var _ entity.Interface = &Bug{} -var ErrBugNotExist = errors.New("bug doesn't exist") +// 1: original format +// 2: no more legacy identities +// 3: Ids are generated from the create operation serialized data instead of from the first git commit +// 4: with DAG entity framework +const formatVersion = 4 -func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch { - return entity.NewErrMultipleMatch("bug", matching) +var def = dag.Definition{ + Typename: "bug", + Namespace: "bugs", + OperationUnmarshaler: operationUnmarshaller, + FormatVersion: formatVersion, } -func NewErrMultipleMatchOp(matching []entity.Id) *entity.ErrMultipleMatch { - return entity.NewErrMultipleMatch("operation", matching) -} - -var _ Interface = &Bug{} -var _ entity.Interface = &Bug{} +var ClockLoader = dag.ClockLoader(def) // Bug hold the data of a bug thread, organized in a way close to // how it will be persisted inside Git. This is the data structure // used to merge two different version of the same Bug. type Bug struct { - // A Lamport clock is a logical clock that allow to order event - // inside a distributed system. - // It must be the first field in this struct due to https://github.com/golang/go/issues/599 - createTime lamport.Time - editTime lamport.Time - - lastCommit repository.Hash - - // all the committed operations - packs []OperationPack - - // a temporary pack of operations used for convenience to pile up new operations - // before a commit - staging OperationPack + *dag.Entity } // NewBug create a new Bug func NewBug() *Bug { - // No logical clock yet - return &Bug{} -} - -// ReadLocal will read a local bug from its hash -func ReadLocal(repo repository.ClockedRepo, id entity.Id) (*Bug, error) { - ref := bugsRefPattern + id.String() - return read(repo, identity.NewSimpleResolver(repo), ref) -} - -// ReadLocalWithResolver will read a local bug from its hash -func ReadLocalWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, id entity.Id) (*Bug, error) { - ref := bugsRefPattern + id.String() - return read(repo, identityResolver, ref) -} - -// ReadRemote will read a remote bug from its hash -func ReadRemote(repo repository.ClockedRepo, remote string, id entity.Id) (*Bug, error) { - ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id.String() - return read(repo, identity.NewSimpleResolver(repo), ref) -} - -// ReadRemoteWithResolver will read a remote bug from its hash -func ReadRemoteWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, remote string, id entity.Id) (*Bug, error) { - ref := fmt.Sprintf(bugsRemoteRefPattern, remote) + id.String() - return read(repo, identityResolver, ref) -} - -// read will read and parse a Bug from git -func read(repo repository.ClockedRepo, identityResolver identity.Resolver, ref string) (*Bug, error) { - id := entity.RefToId(ref) - - if err := id.Validate(); err != nil { - return nil, errors.Wrap(err, "invalid ref ") - } - - hashes, err := repo.ListCommits(ref) - if err != nil { - return nil, ErrBugNotExist - } - if len(hashes) == 0 { - return nil, fmt.Errorf("empty bug") - } - - bug := Bug{} - - // Load each OperationPack - for _, hash := range hashes { - tree, err := readTree(repo, hash) - if err != nil { - return nil, err - } - - // Due to rebase, edit Lamport time are not necessarily ordered - if tree.editTime > bug.editTime { - bug.editTime = tree.editTime - } - - // Update the clocks - err = repo.Witness(creationClockName, bug.createTime) - if err != nil { - return nil, errors.Wrap(err, "failed to update create lamport clock") - } - err = repo.Witness(editClockName, bug.editTime) - if err != nil { - return nil, errors.Wrap(err, "failed to update edit lamport clock") - } - - data, err := repo.ReadData(tree.opsEntry.Hash) - if err != nil { - return nil, errors.Wrap(err, "failed to read git blob data") - } - - opp := &OperationPack{} - err = json.Unmarshal(data, &opp) - if err != nil { - return nil, errors.Wrap(err, "failed to decode OperationPack json") - } - - // tag the pack with the commit hash - opp.commitHash = hash - bug.lastCommit = hash - - // if it's the first OperationPack read - if len(bug.packs) == 0 { - bug.createTime = tree.createTime - } - - bug.packs = append(bug.packs, *opp) - } - - // Bug Id is the Id of the first operation - if len(bug.packs[0].Operations) == 0 { - return nil, fmt.Errorf("first OperationPack is empty") - } - if id != bug.packs[0].Operations[0].Id() { - return nil, fmt.Errorf("bug ID doesn't match the first operation ID") + return &Bug{ + Entity: dag.New(def), } +} - // Make sure that the identities are properly loaded - err = bug.EnsureIdentities(identityResolver) +// 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, nil + return &Bug{Entity: e}, nil } -// RemoveBug will remove a local bug from its entity.Id -func RemoveBug(repo repository.ClockedRepo, id entity.Id) error { - var fullMatches []string - - refs, err := repo.ListRefs(bugsRefPattern + id.String()) +// ReadWithResolver will read a bug from its Id, with a custom identity.Resolver +func ReadWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, id entity.Id) (*Bug, error) { + e, err := dag.Read(def, repo, identityResolver, id) if err != nil { - return err - } - if len(refs) > 1 { - return NewErrMultipleMatchBug(entity.RefsToIds(refs)) - } - if len(refs) == 1 { - // we have the bug locally - fullMatches = append(fullMatches, refs[0]) - } - - remotes, err := repo.GetRemotes() - if err != nil { - return err - } - - for remote := range remotes { - remotePrefix := fmt.Sprintf(bugsRemoteRefPattern+id.String(), remote) - remoteRefs, err := repo.ListRefs(remotePrefix) - if err != nil { - return err - } - if len(remoteRefs) > 1 { - return NewErrMultipleMatchBug(entity.RefsToIds(refs)) - } - if len(remoteRefs) == 1 { - // found the bug in a remote - fullMatches = append(fullMatches, remoteRefs[0]) - } - } - - if len(fullMatches) == 0 { - return ErrBugNotExist - } - - for _, ref := range fullMatches { - err = repo.RemoveRef(ref) - if err != nil { - return err - } + return nil, err } - - return nil + return &Bug{Entity: e}, nil } type StreamedBug struct { @@ -225,50 +65,33 @@ type StreamedBug struct { Err error } -// ReadAllLocal read and parse all local bugs -func ReadAllLocal(repo repository.ClockedRepo) <-chan StreamedBug { - return readAll(repo, identity.NewSimpleResolver(repo), bugsRefPattern) +// ReadAll read and parse all local bugs +func ReadAll(repo repository.ClockedRepo) <-chan StreamedBug { + return readAll(repo, identity.NewSimpleResolver(repo)) } -// ReadAllLocalWithResolver read and parse all local bugs -func ReadAllLocalWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug { - return readAll(repo, identityResolver, bugsRefPattern) -} - -// ReadAllRemote read and parse all remote bugs for a given remote -func ReadAllRemote(repo repository.ClockedRepo, remote string) <-chan StreamedBug { - refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote) - return readAll(repo, identity.NewSimpleResolver(repo), refPrefix) -} - -// ReadAllRemoteWithResolver read and parse all remote bugs for a given remote -func ReadAllRemoteWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver, remote string) <-chan StreamedBug { - refPrefix := fmt.Sprintf(bugsRemoteRefPattern, remote) - return readAll(repo, identityResolver, refPrefix) +// ReadAllWithResolver read and parse all local bugs +func ReadAllWithResolver(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug { + return readAll(repo, identityResolver) } // Read and parse all available bug with a given ref prefix -func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver, refPrefix string) <-chan StreamedBug { +func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver) <-chan StreamedBug { out := make(chan StreamedBug) go func() { defer close(out) - refs, err := repo.ListRefs(refPrefix) - if err != nil { - out <- StreamedBug{Err: err} - return - } - - for _, ref := range refs { - b, err := read(repo, identityResolver, ref) - - if err != nil { - out <- StreamedBug{Err: err} - return + for streamedEntity := range dag.ReadAll(def, repo, identityResolver) { + if streamedEntity.Err != nil { + out <- StreamedBug{ + Err: streamedEntity.Err, + } + } else { + out <- StreamedBug{ + Bug: &Bug{Entity: streamedEntity.Entity}, + } } - - out <- StreamedBug{Bug: b} } }() @@ -277,345 +100,78 @@ func readAll(repo repository.ClockedRepo, identityResolver identity.Resolver, re // ListLocalIds list all the available local bug ids func ListLocalIds(repo repository.Repo) ([]entity.Id, error) { - refs, err := repo.ListRefs(bugsRefPattern) - if err != nil { - return nil, err - } - - return entity.RefsToIds(refs), nil + return dag.ListLocalIds(def, repo) } // Validate check if the Bug data is valid func (bug *Bug) Validate() error { - // non-empty - if len(bug.packs) == 0 && bug.staging.IsEmpty() { - return fmt.Errorf("bug has no operations") - } - - // check if each pack and operations are valid - for _, pack := range bug.packs { - if err := pack.Validate(); err != nil { - return err - } - } - - // check if staging is valid if needed - if !bug.staging.IsEmpty() { - if err := bug.staging.Validate(); err != nil { - return errors.Wrap(err, "staging") - } + if err := bug.Entity.Validate(); err != nil { + return err } // The very first Op should be a CreateOp firstOp := bug.FirstOp() - if firstOp == nil || firstOp.base().OperationType != CreateOp { + if firstOp == nil || firstOp.Type() != CreateOp { return fmt.Errorf("first operation should be a Create op") } // Check that there is no more CreateOp op - // Check that there is no colliding operation's ID - it := NewOperationIterator(bug) - createCount := 0 - ids := make(map[entity.Id]struct{}) - for it.Next() { - if it.Value().base().OperationType == CreateOp { - createCount++ + for i, op := range bug.Operations() { + if i == 0 { + continue } - if _, ok := ids[it.Value().Id()]; ok { - return fmt.Errorf("id collision: %s", it.Value().Id()) + if op.Type() == CreateOp { + return fmt.Errorf("only one Create op allowed") } - ids[it.Value().Id()] = struct{}{} - } - - if createCount != 1 { - return fmt.Errorf("only one Create op allowed") } return nil } -// Append an operation into the staging area, to be committed later +// Append add a new Operation to the Bug func (bug *Bug) Append(op Operation) { - if len(bug.packs) == 0 && len(bug.staging.Operations) == 0 { - if op.base().OperationType != CreateOp { - panic("first operation should be a Create") - } - } - bug.staging.Append(op) + bug.Entity.Append(op) } -// Commit write the staging area in Git and move the operations to the packs -func (bug *Bug) Commit(repo repository.ClockedRepo) error { - if !bug.NeedCommit() { - return fmt.Errorf("can't commit a bug with no pending operation") - } - - if err := bug.Validate(); err != nil { - return errors.Wrap(err, "can't commit a bug with invalid data") - } - - // update clocks - var err error - bug.editTime, err = repo.Increment(editClockName) - if err != nil { - return err - } - if bug.lastCommit == "" { - bug.createTime, err = repo.Increment(creationClockName) - if err != nil { - return err - } +// Operations return the ordered operations +func (bug *Bug) Operations() []Operation { + source := bug.Entity.Operations() + result := make([]Operation, len(source)) + for i, op := range source { + result[i] = op.(Operation) } - - // Write the Ops as a Git blob containing the serialized array - hash, err := bug.staging.Write(repo) - if err != nil { - return err - } - - // Make a Git tree referencing this blob - tree := []repository.TreeEntry{ - // the last pack of ops - {ObjectType: repository.Blob, Hash: hash, Name: opsEntryName}, - } - - // Store the logical clocks as well - // --> edit clock for each OperationPack/commits - // --> create clock only for the first OperationPack/commits - // - // To avoid having one blob for each clock value, clocks are serialized - // directly into the entry name - emptyBlobHash, err := repo.StoreData([]byte{}) - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: emptyBlobHash, - Name: fmt.Sprintf(editClockEntryPattern, bug.editTime), - }) - if bug.lastCommit == "" { - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: emptyBlobHash, - Name: fmt.Sprintf(createClockEntryPattern, bug.createTime), - }) - } - - // Reference, if any, all the files required by the ops - // Git will check that they actually exist in the storage and will make sure - // to push/pull them as needed. - mediaTree := makeMediaTree(bug.staging) - if len(mediaTree) > 0 { - mediaTreeHash, err := repo.StoreTree(mediaTree) - if err != nil { - return err - } - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Tree, - Hash: mediaTreeHash, - Name: mediaEntryName, - }) - } - - // Store the tree - hash, err = repo.StoreTree(tree) - if err != nil { - return err - } - - // Write a Git commit referencing the tree, with the previous commit as parent - if bug.lastCommit != "" { - hash, err = repo.StoreCommit(hash, bug.lastCommit) - } else { - hash, err = repo.StoreCommit(hash) - } - if err != nil { - return err - } - - bug.lastCommit = hash - bug.staging.commitHash = hash - bug.packs = append(bug.packs, bug.staging) - bug.staging = OperationPack{} - - // Create or update the Git reference for this bug - // When pushing later, the remote will ensure that this ref update - // is fast-forward, that is no data has been overwritten - ref := fmt.Sprintf("%s%s", bugsRefPattern, bug.Id().String()) - return repo.UpdateRef(ref, hash) + return result } -func (bug *Bug) CommitAsNeeded(repo repository.ClockedRepo) error { - if !bug.NeedCommit() { - return nil - } - return bug.Commit(repo) -} - -func (bug *Bug) NeedCommit() bool { - return !bug.staging.IsEmpty() -} - -// Merge a different version of the same bug by rebasing operations of this bug -// that are not present in the other on top of the chain of operations of the -// other version. -func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) { - var otherBug = bugFromInterface(other) - - // Note: a faster merge should be possible without actually reading and parsing - // all operations pack of our side. - // Reading the other side is still necessary to validate remote data, at least - // for new operations - - if bug.Id() != otherBug.Id() { - return false, errors.New("merging unrelated bugs is not supported") - } - - if len(otherBug.staging.Operations) > 0 { - return false, errors.New("merging a bug with a non-empty staging is not supported") - } - - if bug.lastCommit == "" || otherBug.lastCommit == "" { - return false, errors.New("can't merge a bug that has never been stored") - } - - ancestor, err := repo.FindCommonAncestor(bug.lastCommit, otherBug.lastCommit) - if err != nil { - return false, errors.Wrap(err, "can't find common ancestor") - } - - ancestorIndex := 0 - newPacks := make([]OperationPack, 0, len(bug.packs)) - - // Find the root of the rebase - for i, pack := range bug.packs { - newPacks = append(newPacks, pack) - - if pack.commitHash == ancestor { - ancestorIndex = i - break - } - } - - if len(otherBug.packs) == ancestorIndex+1 { - // Nothing to rebase, return early - return false, nil - } - - // get other bug's extra packs - for i := ancestorIndex + 1; i < len(otherBug.packs); i++ { - // clone is probably not necessary - newPack := otherBug.packs[i].Clone() - - newPacks = append(newPacks, newPack) - bug.lastCommit = newPack.commitHash - } - - // rebase our extra packs - for i := ancestorIndex + 1; i < len(bug.packs); i++ { - pack := bug.packs[i] - - // get the referenced git tree - treeHash, err := repo.GetTreeHash(pack.commitHash) - - if err != nil { - return false, err - } - - // create a new commit with the correct ancestor - hash, err := repo.StoreCommit(treeHash, bug.lastCommit) - - if err != nil { - return false, err - } - - // replace the pack - newPack := pack.Clone() - newPack.commitHash = hash - newPacks = append(newPacks, newPack) - - // update the bug - bug.lastCommit = hash +// Compile a bug in a easily usable snapshot +func (bug *Bug) Compile() Snapshot { + snap := Snapshot{ + id: bug.Id(), + Status: OpenStatus, } - bug.packs = newPacks - - // Update the git ref - err = repo.UpdateRef(bugsRefPattern+bug.Id().String(), bug.lastCommit) - if err != nil { - return false, err + for _, op := range bug.Operations() { + op.Apply(&snap) + snap.Operations = append(snap.Operations, op) } - return true, nil -} - -// Id return the Bug identifier -func (bug *Bug) Id() entity.Id { - // id is the id of the first operation - return bug.FirstOp().Id() -} - -// CreateLamportTime return the Lamport time of creation -func (bug *Bug) CreateLamportTime() lamport.Time { - return bug.createTime -} - -// EditLamportTime return the Lamport time of the last edit -func (bug *Bug) EditLamportTime() lamport.Time { - return bug.editTime + return snap } // Lookup for the very first operation of the bug. // For a valid Bug, this operation should be a CreateOp func (bug *Bug) FirstOp() Operation { - for _, pack := range bug.packs { - for _, op := range pack.Operations { - return op - } + if fo := bug.Entity.FirstOp(); fo != nil { + return fo.(Operation) } - - if !bug.staging.IsEmpty() { - return bug.staging.Operations[0] - } - return nil } // Lookup for the very last operation of the bug. // For a valid Bug, should never be nil func (bug *Bug) LastOp() Operation { - if !bug.staging.IsEmpty() { - return bug.staging.Operations[len(bug.staging.Operations)-1] + if lo := bug.Entity.LastOp(); lo != nil { + return lo.(Operation) } - - if len(bug.packs) == 0 { - return nil - } - - lastPack := bug.packs[len(bug.packs)-1] - - if len(lastPack.Operations) == 0 { - return nil - } - - return lastPack.Operations[len(lastPack.Operations)-1] -} - -// Compile a bug in a easily usable snapshot -func (bug *Bug) Compile() Snapshot { - snap := Snapshot{ - id: bug.Id(), - Status: OpenStatus, - } - - it := NewOperationIterator(bug) - - for it.Next() { - op := it.Value() - op.Apply(&snap) - snap.Operations = append(snap.Operations, op) - } - - return snap + return nil } diff --git a/bug/bug_actions.go b/bug/bug_actions.go index bf894ef8a..6ca5ffd77 100644 --- a/bug/bug_actions.go +++ b/bug/bug_actions.go @@ -1,12 +1,10 @@ package bug import ( - "fmt" - "strings" - "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/repository" ) @@ -14,23 +12,23 @@ import ( // Fetch retrieve updates from a remote // This does not change the local bugs state func Fetch(repo repository.Repo, remote string) (string, error) { - return repo.FetchRefs(remote, "bugs") + return dag.Fetch(def, repo, remote) } // Push update a remote with the local changes func Push(repo repository.Repo, remote string) (string, error) { - return repo.PushRefs(remote, "bugs") + return dag.Push(def, repo, remote) } // Pull will do a Fetch + MergeAll // This function will return an error if a merge fail -func Pull(repo repository.ClockedRepo, remote string) error { +func Pull(repo repository.ClockedRepo, remote string, author identity.Interface) error { _, err := Fetch(repo, remote) if err != nil { return err } - for merge := range MergeAll(repo, remote) { + for merge := range MergeAll(repo, remote, author) { if merge.Err != nil { return merge.Err } @@ -42,95 +40,19 @@ func Pull(repo repository.ClockedRepo, remote string) error { return nil } -// MergeAll will merge all the available remote bug: -// -// - If the remote has new commit, the local bug is updated to match the same history -// (fast-forward update) -// - if the local bug has new commits but the remote don't, nothing is changed -// - if both local and remote bug have new commits (that is, we have a concurrent edition), -// new local commits are rewritten at the head of the remote history (that is, a rebase) -func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeResult { - out := make(chan entity.MergeResult) - +// MergeAll will merge all the available remote bug +// Note: an author is necessary for the case where a merge commit is created, as this commit will +// have an author and may be signed if a signing key is available. +func MergeAll(repo repository.ClockedRepo, remote string, author identity.Interface) <-chan entity.MergeResult { // no caching for the merge, we load everything from git even if that means multiple // copy of the same entity in memory. The cache layer will intercept the results to // invalidate entities if necessary. identityResolver := identity.NewSimpleResolver(repo) - go func() { - defer close(out) - - remoteRefSpec := fmt.Sprintf(bugsRemoteRefPattern, remote) - remoteRefs, err := repo.ListRefs(remoteRefSpec) - if err != nil { - out <- entity.MergeResult{Err: err} - return - } - - for _, remoteRef := range remoteRefs { - refSplit := strings.Split(remoteRef, "/") - id := entity.Id(refSplit[len(refSplit)-1]) - - if err := id.Validate(); err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error()) - continue - } - - remoteBug, err := read(repo, identityResolver, remoteRef) - - if err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error()) - continue - } - - // Check for error in remote data - if err := remoteBug.Validate(); err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error()) - continue - } - - localRef := bugsRefPattern + remoteBug.Id().String() - localExist, err := repo.RefExist(localRef) - - if err != nil { - out <- entity.NewMergeError(err, id) - continue - } - - // the bug is not local yet, simply create the reference - if !localExist { - err := repo.CopyRef(remoteRef, localRef) - - if err != nil { - out <- entity.NewMergeError(err, id) - return - } - - out <- entity.NewMergeNewStatus(id, remoteBug) - continue - } - - localBug, err := read(repo, identityResolver, localRef) - - if err != nil { - out <- entity.NewMergeError(errors.Wrap(err, "local bug is not readable"), id) - return - } - - updated, err := localBug.Merge(repo, remoteBug) - - if err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error()) - return - } - - if updated { - out <- entity.NewMergeUpdatedStatus(id, localBug) - } else { - out <- entity.NewMergeNothingStatus(id) - } - } - }() + return dag.MergeAll(def, repo, identityResolver, remote, author) +} - return out +// RemoveBug will remove a local bug from its entity.Id +func RemoveBug(repo repository.ClockedRepo, id entity.Id) error { + return dag.Remove(def, repo, id) } diff --git a/bug/bug_actions_test.go b/bug/bug_actions_test.go deleted file mode 100644 index fc6710634..000000000 --- a/bug/bug_actions_test.go +++ /dev/null @@ -1,394 +0,0 @@ -package bug - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -func TestPushPull(t *testing.T) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - assert.True(t, bug1.NeedCommit()) - err = bug1.Commit(repoA) - require.NoError(t, err) - assert.False(t, bug1.NeedCommit()) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote --> B - _, err = Push(repoA, "origin") - require.NoError(t, err) - - err = Pull(repoB, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoB)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - // B --> remote --> A - reneB, err := identity.ReadLocal(repoA, reneA.Id()) - require.NoError(t, err) - - bug2, _, err := Create(reneB, time.Now().Unix(), "bug2", "message") - require.NoError(t, err) - err = bug2.Commit(repoB) - require.NoError(t, err) - - _, err = Push(repoB, "origin") - require.NoError(t, err) - - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs = allBugs(t, ReadAllLocal(repoA)) - - if len(bugs) != 2 { - t.Fatal("Unexpected number of bugs") - } -} - -func allBugs(t testing.TB, bugs <-chan StreamedBug) []*Bug { - var result []*Bug - for streamed := range bugs { - if streamed.Err != nil { - t.Fatal(streamed.Err) - } - result = append(result, streamed.Bug) - } - return result -} - -func TestRebaseTheirs(t *testing.T) { - _RebaseTheirs(t) -} - -func BenchmarkRebaseTheirs(b *testing.B) { - for n := 0; n < b.N; n++ { - _RebaseTheirs(b) - } -} - -func _RebaseTheirs(t testing.TB) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - assert.True(t, bug1.NeedCommit()) - err = bug1.Commit(repoA) - require.NoError(t, err) - assert.False(t, bug1.NeedCommit()) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote - - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - require.NoError(t, err) - - bug2, err := ReadLocal(repoB, bug1.Id()) - require.NoError(t, err) - assert.False(t, bug2.NeedCommit()) - - reneB, err := identity.ReadLocal(repoA, reneA.Id()) - require.NoError(t, err) - - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message2") - require.NoError(t, err) - assert.True(t, bug2.NeedCommit()) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message3") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message4") - require.NoError(t, err) - err = bug2.Commit(repoB) - require.NoError(t, err) - assert.False(t, bug2.NeedCommit()) - - // B --> remote - _, err = Push(repoB, "origin") - require.NoError(t, err) - - // remote --> A - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoB)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug3, err := ReadLocal(repoA, bug1.Id()) - require.NoError(t, err) - - if nbOps(bug3) != 4 { - t.Fatal("Unexpected number of operations") - } -} - -func TestRebaseOurs(t *testing.T) { - _RebaseOurs(t) -} - -func BenchmarkRebaseOurs(b *testing.B) { - for n := 0; n < b.N; n++ { - _RebaseOurs(b) - } -} - -func _RebaseOurs(t testing.TB) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message2") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message3") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message4") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message5") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message6") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message7") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message8") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message9") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message10") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - // remote --> A - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoA)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug2, err := ReadLocal(repoA, bug1.Id()) - require.NoError(t, err) - - if nbOps(bug2) != 10 { - t.Fatal("Unexpected number of operations") - } -} - -func nbOps(b *Bug) int { - it := NewOperationIterator(b) - counter := 0 - for it.Next() { - counter++ - } - return counter -} - -func TestRebaseConflict(t *testing.T) { - _RebaseConflict(t) -} - -func BenchmarkRebaseConflict(b *testing.B) { - for n := 0; n < b.N; n++ { - _RebaseConflict(b) - } -} - -func _RebaseConflict(t testing.TB) { - repoA, repoB, remote := repository.SetupGoGitReposAndRemote() - defer repository.CleanupTestRepos(repoA, repoB, remote) - - reneA, err := identity.NewIdentity(repoA, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = reneA.Commit(repoA) - require.NoError(t, err) - - bug1, _, err := Create(reneA, time.Now().Unix(), "bug1", "message") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - // distribute the identity - _, err = identity.Push(repoA, "origin") - require.NoError(t, err) - err = identity.Pull(repoB, "origin") - require.NoError(t, err) - - // A --> remote - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message2") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message3") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message4") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message5") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message6") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message7") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message8") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message9") - require.NoError(t, err) - _, err = AddComment(bug1, reneA, time.Now().Unix(), "message10") - require.NoError(t, err) - err = bug1.Commit(repoA) - require.NoError(t, err) - - bug2, err := ReadLocal(repoB, bug1.Id()) - require.NoError(t, err) - - reneB, err := identity.ReadLocal(repoA, reneA.Id()) - require.NoError(t, err) - - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message11") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message12") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message13") - require.NoError(t, err) - err = bug2.Commit(repoB) - require.NoError(t, err) - - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message14") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message15") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message16") - require.NoError(t, err) - err = bug2.Commit(repoB) - require.NoError(t, err) - - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message17") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message18") - require.NoError(t, err) - _, err = AddComment(bug2, reneB, time.Now().Unix(), "message19") - require.NoError(t, err) - err = bug2.Commit(repoB) - require.NoError(t, err) - - // A --> remote - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // remote --> B - err = Pull(repoB, "origin") - require.NoError(t, err) - - bugs := allBugs(t, ReadAllLocal(repoB)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug3, err := ReadLocal(repoB, bug1.Id()) - require.NoError(t, err) - - if nbOps(bug3) != 19 { - t.Fatal("Unexpected number of operations") - } - - // B --> remote - _, err = Push(repoB, "origin") - require.NoError(t, err) - - // remote --> A - err = Pull(repoA, "origin") - require.NoError(t, err) - - bugs = allBugs(t, ReadAllLocal(repoA)) - - if len(bugs) != 1 { - t.Fatal("Unexpected number of bugs") - } - - bug4, err := ReadLocal(repoA, bug1.Id()) - require.NoError(t, err) - - if nbOps(bug4) != 19 { - t.Fatal("Unexpected number of operations") - } -} diff --git a/bug/bug_test.go b/bug/bug_test.go deleted file mode 100644 index a8987ac11..000000000 --- a/bug/bug_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package bug - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -func TestBugId(t *testing.T) { - repo := repository.NewMockRepo() - - bug1 := NewBug() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - - bug1.Append(createOp) - - err = bug1.Commit(repo) - - if err != nil { - t.Fatal(err) - } - - bug1.Id() -} - -func TestBugValidity(t *testing.T) { - repo := repository.NewMockRepo() - - bug1 := NewBug() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - - if bug1.Validate() == nil { - t.Fatal("Empty bug should be invalid") - } - - bug1.Append(createOp) - - if bug1.Validate() != nil { - t.Fatal("Bug with just a CreateOp should be valid") - } - - err = bug1.Commit(repo) - if err != nil { - t.Fatal(err) - } - - bug1.Append(createOp) - - if bug1.Validate() == nil { - t.Fatal("Bug with multiple CreateOp should be invalid") - } - - err = bug1.Commit(repo) - if err == nil { - t.Fatal("Invalid bug should not commit") - } -} - -func TestBugCommitLoad(t *testing.T) { - repo := repository.NewMockRepo() - - bug1 := NewBug() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1") - addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil) - - bug1.Append(createOp) - bug1.Append(setTitleOp) - - require.True(t, bug1.NeedCommit()) - - err = bug1.Commit(repo) - require.Nil(t, err) - require.False(t, bug1.NeedCommit()) - - bug2, err := ReadLocal(repo, bug1.Id()) - require.NoError(t, err) - equivalentBug(t, bug1, bug2) - - // add more op - - bug1.Append(addCommentOp) - - require.True(t, bug1.NeedCommit()) - - err = bug1.Commit(repo) - require.Nil(t, err) - require.False(t, bug1.NeedCommit()) - - bug3, err := ReadLocal(repo, bug1.Id()) - require.NoError(t, err) - equivalentBug(t, bug1, bug3) -} - -func equivalentBug(t *testing.T, expected, actual *Bug) { - require.Equal(t, len(expected.packs), len(actual.packs)) - - for i := range expected.packs { - for j := range expected.packs[i].Operations { - actual.packs[i].Operations[j].base().id = expected.packs[i].Operations[j].base().id - } - } - - require.Equal(t, expected, actual) -} - -func TestBugRemove(t *testing.T) { - repo := repository.CreateGoGitTestRepo(false) - remoteA := repository.CreateGoGitTestRepo(true) - remoteB := repository.CreateGoGitTestRepo(true) - defer repository.CleanupTestRepos(repo, remoteA, remoteB) - - err := repo.AddRemote("remoteA", remoteA.GetLocalRemote()) - require.NoError(t, err) - - err = repo.AddRemote("remoteB", remoteB.GetLocalRemote()) - require.NoError(t, err) - - // generate a bunch of bugs - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - for i := 0; i < 100; i++ { - b := NewBug() - createOp := NewCreateOp(rene, time.Now().Unix(), "title", fmt.Sprintf("message%v", i), nil) - b.Append(createOp) - err = b.Commit(repo) - require.NoError(t, err) - } - - // and one more for testing - b := NewBug() - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - b.Append(createOp) - err = b.Commit(repo) - require.NoError(t, err) - - _, err = Push(repo, "remoteA") - require.NoError(t, err) - - _, err = Push(repo, "remoteB") - require.NoError(t, err) - - _, err = Fetch(repo, "remoteA") - require.NoError(t, err) - - _, err = Fetch(repo, "remoteB") - require.NoError(t, err) - - err = RemoveBug(repo, b.Id()) - require.NoError(t, err) - - _, err = ReadLocal(repo, b.Id()) - require.Error(t, ErrBugNotExist, err) - - _, err = ReadRemote(repo, "remoteA", b.Id()) - require.Error(t, ErrBugNotExist, err) - - _, err = ReadRemote(repo, "remoteB", b.Id()) - require.Error(t, ErrBugNotExist, err) - - ids, err := ListLocalIds(repo) - require.NoError(t, err) - require.Len(t, ids, 100) -} diff --git a/bug/clocks.go b/bug/clocks.go deleted file mode 100644 index 58fce923e..000000000 --- a/bug/clocks.go +++ /dev/null @@ -1,40 +0,0 @@ -package bug - -import ( - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -// ClockLoader is the repository.ClockLoader for the Bug entity -var ClockLoader = repository.ClockLoader{ - Clocks: []string{creationClockName, editClockName}, - Witnesser: func(repo repository.ClockedRepo) error { - // We don't care about the actual identity so an IdentityStub will do - resolver := identity.NewStubResolver() - for b := range ReadAllLocalWithResolver(repo, resolver) { - if b.Err != nil { - return b.Err - } - - createClock, err := repo.GetOrCreateClock(creationClockName) - if err != nil { - return err - } - err = createClock.Witness(b.Bug.createTime) - if err != nil { - return err - } - - editClock, err := repo.GetOrCreateClock(editClockName) - if err != nil { - return err - } - err = editClock.Witness(b.Bug.editTime) - if err != nil { - return err - } - } - - return nil - }, -} diff --git a/bug/err.go b/bug/err.go new file mode 100644 index 000000000..1bd174bb3 --- /dev/null +++ b/bug/err.go @@ -0,0 +1,17 @@ +package bug + +import ( + "errors" + + "github.com/MichaelMure/git-bug/entity" +) + +var ErrBugNotExist = errors.New("bug doesn't exist") + +func NewErrMultipleMatchBug(matching []entity.Id) *entity.ErrMultipleMatch { + return entity.NewErrMultipleMatch("bug", matching) +} + +func NewErrMultipleMatchOp(matching []entity.Id) *entity.ErrMultipleMatch { + return entity.NewErrMultipleMatch("operation", matching) +} diff --git a/bug/git_tree.go b/bug/git_tree.go deleted file mode 100644 index a5583bda4..000000000 --- a/bug/git_tree.go +++ /dev/null @@ -1,84 +0,0 @@ -package bug - -import ( - "fmt" - "strings" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" -) - -type gitTree struct { - opsEntry repository.TreeEntry - createTime lamport.Time - editTime lamport.Time -} - -func readTree(repo repository.RepoData, hash repository.Hash) (*gitTree, error) { - tree := &gitTree{} - - entries, err := repo.ReadTree(hash) - if err != nil { - return nil, errors.Wrap(err, "can't list git tree entries") - } - - opsFound := false - - for _, entry := range entries { - if entry.Name == opsEntryName { - tree.opsEntry = entry - opsFound = true - continue - } - if strings.HasPrefix(entry.Name, createClockEntryPrefix) { - n, err := fmt.Sscanf(entry.Name, createClockEntryPattern, &tree.createTime) - if err != nil { - return nil, errors.Wrap(err, "can't read create lamport time") - } - if n != 1 { - return nil, fmt.Errorf("could not parse create time lamport value") - } - } - if strings.HasPrefix(entry.Name, editClockEntryPrefix) { - n, err := fmt.Sscanf(entry.Name, editClockEntryPattern, &tree.editTime) - if err != nil { - return nil, errors.Wrap(err, "can't read edit lamport time") - } - if n != 1 { - return nil, fmt.Errorf("could not parse edit time lamport value") - } - } - } - - if !opsFound { - return nil, errors.New("invalid tree, missing the ops entry") - } - - return tree, nil -} - -func makeMediaTree(pack OperationPack) []repository.TreeEntry { - var tree []repository.TreeEntry - counter := 0 - added := make(map[repository.Hash]interface{}) - - for _, ops := range pack.Operations { - for _, file := range ops.GetFiles() { - if _, has := added[file]; !has { - tree = append(tree, repository.TreeEntry{ - ObjectType: repository.Blob, - Hash: file, - // The name is not important here, we only need to - // reference the blob. - Name: fmt.Sprintf("file%d", counter), - }) - counter++ - added[file] = struct{}{} - } - } - } - - return tree -} diff --git a/bug/identity.go b/bug/identity.go deleted file mode 100644 index 2eb2bcaf0..000000000 --- a/bug/identity.go +++ /dev/null @@ -1,27 +0,0 @@ -package bug - -import ( - "github.com/MichaelMure/git-bug/identity" -) - -// EnsureIdentities walk the graph of operations and make sure that all Identity -// are properly loaded. That is, it replace all the IdentityStub with the full -// Identity, loaded through a Resolver. -func (bug *Bug) EnsureIdentities(resolver identity.Resolver) error { - it := NewOperationIterator(bug) - - for it.Next() { - op := it.Value() - base := op.base() - - if stub, ok := base.Author.(*identity.IdentityStub); ok { - i, err := resolver.ResolveIdentity(stub.Id()) - if err != nil { - return err - } - - base.Author = i - } - } - return nil -} diff --git a/bug/interface.go b/bug/interface.go index 5c8f27293..e71496a95 100644 --- a/bug/interface.go +++ b/bug/interface.go @@ -16,17 +16,15 @@ type Interface interface { // Append an operation into the staging area, to be committed later Append(op Operation) + // Operations return the ordered operations + Operations() []Operation + // Indicate 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(repo repository.ClockedRepo) error - // Merge a different version of the same bug by rebasing operations of this bug - // that are not present in the other on top of the chain of operations of the - // other version. - Merge(repo repository.Repo, other Interface) (bool, error) - // Lookup for the very first operation of the bug. // For a valid Bug, this operation should be a CreateOp FirstOp() Operation diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index e52c46fde..fd00860b2 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -24,23 +24,19 @@ type AddCommentOperation struct { // Sign-post method for gqlgen func (op *AddCommentOperation) IsOperation() {} -func (op *AddCommentOperation) base() *OpBase { - return &op.OpBase -} - func (op *AddCommentOperation) Id() entity.Id { - return idOperation(op) + return 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_) commentId := entity.CombineIds(snapshot.Id(), op.Id()) comment := Comment{ id: commentId, Message: op.Message, - Author: op.Author, + Author: op.Author_, Files: op.Files, UnixTime: timestamp.Timestamp(op.UnixTime), } @@ -59,7 +55,7 @@ func (op *AddCommentOperation) GetFiles() []repository.Hash { } func (op *AddCommentOperation) Validate() error { - if err := opBaseValidate(op, AddCommentOp); err != nil { + if err := op.OpBase.Validate(op, AddCommentOp); err != nil { return err } diff --git a/bug/op_add_comment_test.go b/bug/op_add_comment_test.go index 3b41d62d3..fb6fa8ed9 100644 --- a/bug/op_add_comment_test.go +++ b/bug/op_add_comment_test.go @@ -32,8 +32,8 @@ func TestAddCommentSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + assert.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene assert.Equal(t, before, &after) } diff --git a/bug/op_create.go b/bug/op_create.go index 1e944d136..2423e5714 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -30,12 +30,8 @@ type CreateOperation struct { // Sign-post method for gqlgen func (op *CreateOperation) IsOperation() {} -func (op *CreateOperation) base() *OpBase { - return &op.OpBase -} - func (op *CreateOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } // OVERRIDE @@ -61,8 +57,8 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.id = op.Id() - snapshot.addActor(op.Author) - snapshot.addParticipant(op.Author) + snapshot.addActor(op.Author_) + snapshot.addParticipant(op.Author_) snapshot.Title = op.Title @@ -70,12 +66,12 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { comment := Comment{ id: commentId, 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{ @@ -90,7 +86,7 @@ func (op *CreateOperation) GetFiles() []repository.Hash { } func (op *CreateOperation) Validate() error { - if err := opBaseValidate(op, CreateOp); err != nil { + if err := op.OpBase.Validate(op, CreateOp); err != nil { return err } diff --git a/bug/op_create_test.go b/bug/op_create_test.go index 456357c4b..1b359deea 100644 --- a/bug/op_create_test.go +++ b/bug/op_create_test.go @@ -79,8 +79,8 @@ func TestCreateSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index 5bfc36bf6..f9e30e62b 100644 --- a/bug/op_edit_comment.go +++ b/bug/op_edit_comment.go @@ -27,19 +27,15 @@ type EditCommentOperation struct { // Sign-post method for gqlgen func (op *EditCommentOperation) IsOperation() {} -func (op *EditCommentOperation) base() *OpBase { - return &op.OpBase -} - func (op *EditCommentOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *EditCommentOperation) Apply(snapshot *Snapshot) { // Todo: currently any message can be edited, even by a different author // crypto signature are needed. - snapshot.addActor(op.Author) + snapshot.addActor(op.Author_) var target TimelineItem @@ -85,7 +81,7 @@ func (op *EditCommentOperation) GetFiles() []repository.Hash { } func (op *EditCommentOperation) Validate() error { - if err := opBaseValidate(op, EditCommentOp); err != nil { + if err := op.OpBase.Validate(op, EditCommentOp); err != nil { return err } diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go index a73309329..777f5f87a 100644 --- a/bug/op_edit_comment_test.go +++ b/bug/op_edit_comment_test.go @@ -97,8 +97,8 @@ func TestEditCommentSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_label_change.go b/bug/op_label_change.go index fefe2402a..5e51534d6 100644 --- a/bug/op_label_change.go +++ b/bug/op_label_change.go @@ -24,17 +24,13 @@ type LabelChangeOperation struct { // Sign-post method for gqlgen func (op *LabelChangeOperation) IsOperation() {} -func (op *LabelChangeOperation) base() *OpBase { - return &op.OpBase -} - func (op *LabelChangeOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } // Apply apply the operation func (op *LabelChangeOperation) Apply(snapshot *Snapshot) { - snapshot.addActor(op.Author) + snapshot.addActor(op.Author_) // Add in the set AddLoop: @@ -66,7 +62,7 @@ AddLoop: item := &LabelChangeTimelineItem{ id: op.Id(), - Author: op.Author, + Author: op.Author_, UnixTime: timestamp.Timestamp(op.UnixTime), Added: op.Added, Removed: op.Removed, @@ -76,7 +72,7 @@ AddLoop: } func (op *LabelChangeOperation) Validate() error { - if err := opBaseValidate(op, LabelChangeOp); err != nil { + if err := op.OpBase.Validate(op, LabelChangeOp); err != nil { return err } diff --git a/bug/op_label_change_test.go b/bug/op_label_change_test.go index 96716ffe4..40dc4f0dc 100644 --- a/bug/op_label_change_test.go +++ b/bug/op_label_change_test.go @@ -31,8 +31,8 @@ func TestLabelChangeSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_noop.go b/bug/op_noop.go index 6364f9187..d59666ae3 100644 --- a/bug/op_noop.go +++ b/bug/op_noop.go @@ -19,12 +19,8 @@ type NoOpOperation struct { // Sign-post method for gqlgen func (op *NoOpOperation) IsOperation() {} -func (op *NoOpOperation) base() *OpBase { - return &op.OpBase -} - func (op *NoOpOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *NoOpOperation) Apply(snapshot *Snapshot) { @@ -32,7 +28,7 @@ func (op *NoOpOperation) Apply(snapshot *Snapshot) { } func (op *NoOpOperation) Validate() error { - return opBaseValidate(op, NoOpOp) + return op.OpBase.Validate(op, NoOpOp) } // UnmarshalJSON is a two step JSON unmarshaling diff --git a/bug/op_noop_test.go b/bug/op_noop_test.go index ce2f98afa..0e3727c2c 100644 --- a/bug/op_noop_test.go +++ b/bug/op_noop_test.go @@ -33,8 +33,8 @@ func TestNoopSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - assert.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + assert.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene assert.Equal(t, before, &after) } diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go index 23d11461c..c4988b84f 100644 --- a/bug/op_set_metadata.go +++ b/bug/op_set_metadata.go @@ -20,38 +20,25 @@ type SetMetadataOperation struct { // Sign-post method for gqlgen func (op *SetMetadataOperation) IsOperation() {} -func (op *SetMetadataOperation) base() *OpBase { - return &op.OpBase -} - func (op *SetMetadataOperation) Id() entity.Id { - return idOperation(op) + return idOperation(op, &op.OpBase) } func (op *SetMetadataOperation) Apply(snapshot *Snapshot) { for _, target := range snapshot.Operations { if target.Id() == op.Target { - base := target.base() - - if base.extraMetadata == nil { - base.extraMetadata = make(map[string]string) - } - // Apply the metadata in an immutable way: if a metadata already // exist, it's not possible to override it. - for key, val := range op.NewMetadata { - if _, exist := base.extraMetadata[key]; !exist { - base.extraMetadata[key] = val - } + for key, value := range op.NewMetadata { + target.setExtraMetadataImmutable(key, value) } - return } } } func (op *SetMetadataOperation) Validate() error { - if err := opBaseValidate(op, SetMetadataOp); err != nil { + if err := op.OpBase.Validate(op, SetMetadataOp); err != nil { return err } diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go index c0c91617c..78f7d883a 100644 --- a/bug/op_set_metadata_test.go +++ b/bug/op_set_metadata_test.go @@ -120,8 +120,8 @@ func TestSetMetadataSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_set_status.go b/bug/op_set_status.go index eb2c0ba4d..fcf33f8c5 100644 --- a/bug/op_set_status.go +++ b/bug/op_set_status.go @@ -21,21 +21,17 @@ type SetStatusOperation struct { // Sign-post method for gqlgen func (op *SetStatusOperation) IsOperation() {} -func (op *SetStatusOperation) base() *OpBase { - return &op.OpBase -} - func (op *SetStatusOperation) Id() entity.Id { - return idOperation(op) + return 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, } @@ -44,7 +40,7 @@ func (op *SetStatusOperation) Apply(snapshot *Snapshot) { } func (op *SetStatusOperation) Validate() error { - if err := opBaseValidate(op, SetStatusOp); err != nil { + if err := op.OpBase.Validate(op, SetStatusOp); err != nil { return err } diff --git a/bug/op_set_status_test.go b/bug/op_set_status_test.go index 3b26282f7..83ff22aef 100644 --- a/bug/op_set_status_test.go +++ b/bug/op_set_status_test.go @@ -31,8 +31,8 @@ func TestSetStatusSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/op_set_title.go b/bug/op_set_title.go index ddd98f0e0..7fd7c20d1 100644 --- a/bug/op_set_title.go +++ b/bug/op_set_title.go @@ -24,21 +24,17 @@ type SetTitleOperation struct { // Sign-post method for gqlgen func (op *SetTitleOperation) IsOperation() {} -func (op *SetTitleOperation) base() *OpBase { - return &op.OpBase -} - func (op *SetTitleOperation) Id() entity.Id { - return idOperation(op) + return 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, @@ -48,7 +44,7 @@ func (op *SetTitleOperation) Apply(snapshot *Snapshot) { } func (op *SetTitleOperation) Validate() error { - if err := opBaseValidate(op, SetTitleOp); err != nil { + if err := op.OpBase.Validate(op, SetTitleOp); err != nil { return err } @@ -132,19 +128,17 @@ func (s *SetTitleTimelineItem) IsAuthored() {} // Convenience function to apply the operation func SetTitle(b Interface, author identity.Interface, unixTime int64, title string) (*SetTitleOperation, error) { - it := NewOperationIterator(b) - - var lastTitleOp Operation - for it.Next() { - op := it.Value() - if op.base().OperationType == SetTitleOp { + var lastTitleOp *SetTitleOperation + for _, op := range b.Operations() { + switch op := op.(type) { + case *SetTitleOperation: lastTitleOp = op } } var was string if lastTitleOp != nil { - was = lastTitleOp.(*SetTitleOperation).Title + was = lastTitleOp.Title } else { was = b.FirstOp().(*CreateOperation).Title } diff --git a/bug/op_set_title_test.go b/bug/op_set_title_test.go index 6ae325bed..7059c4c7a 100644 --- a/bug/op_set_title_test.go +++ b/bug/op_set_title_test.go @@ -31,8 +31,8 @@ func TestSetTitleSerialize(t *testing.T) { before.Id() // Replace the identity stub with the real thing - require.Equal(t, rene.Id(), after.base().Author.Id()) - after.Author = rene + require.Equal(t, rene.Id(), after.Author().Id()) + after.Author_ = rene require.Equal(t, before, &after) } diff --git a/bug/operation.go b/bug/operation.go index bdaa2016e..71a5c15dc 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -8,6 +8,7 @@ import ( "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/repository" ) @@ -29,34 +30,31 @@ const ( // Operation define the interface to fulfill for an edit operation of a Bug type Operation interface { - // base return the OpBase of the Operation, for package internal use - base() *OpBase - // Id return the identifier of the operation, to be used for back references - Id() entity.Id + dag.Operation + + // Type return the type of the operation + Type() OperationType + // Time return the time when the operation was added Time() time.Time // GetFiles return the files needed by this operation GetFiles() []repository.Hash // Apply the operation to a Snapshot to create the final state Apply(snapshot *Snapshot) - // Validate check if the operation is valid (ex: a title is a single line) - Validate() error // 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 - // GetAuthor return the author identity - GetAuthor() identity.Interface + + setExtraMetadataImmutable(key string, value string) // sign-post method for gqlgen IsOperation() } -func idOperation(op Operation) entity.Id { - base := op.base() - +func idOperation(op Operation, base *OpBase) entity.Id { if base.id == "" { // something went really wrong panic("op's id not set") @@ -77,10 +75,69 @@ func idOperation(op Operation) entity.Id { return base.id } +func operationUnmarshaller(author identity.Interface, raw json.RawMessage) (dag.Operation, error) { + var t struct { + OperationType OperationType `json:"type"` + } + + if err := json.Unmarshal(raw, &t); err != nil { + return nil, err + } + + var op Operation + + switch t.OperationType { + case AddCommentOp: + op = &AddCommentOperation{} + case CreateOp: + op = &CreateOperation{} + case EditCommentOp: + op = &EditCommentOperation{} + case LabelChangeOp: + op = &LabelChangeOperation{} + case NoOpOp: + op = &NoOpOperation{} + case SetMetadataOp: + op = &SetMetadataOperation{} + case SetStatusOp: + op = &SetStatusOperation{} + case SetTitleOp: + op = &SetTitleOperation{} + default: + panic(fmt.Sprintf("unknown operation type %v", t.OperationType)) + } + + err := json.Unmarshal(raw, &op) + if err != nil { + return nil, err + } + + switch op := op.(type) { + case *AddCommentOperation: + op.Author_ = author + case *CreateOperation: + 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:"author"` + Author_ identity.Interface `json:"author"` // 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"` @@ -95,15 +152,15 @@ type OpBase struct { func newOpBase(opType OperationType, author identity.Interface, unixTime int64) OpBase { return OpBase{ OperationType: opType, - Author: author, + Author_: author, UnixTime: unixTime, id: entity.UnsetId, } } -func (op *OpBase) UnmarshalJSON(data []byte) error { +func (base *OpBase) UnmarshalJSON(data []byte) error { // Compute the Id when loading the op from disk. - op.id = entity.DeriveId(data) + base.id = entity.DeriveId(data) aux := struct { OperationType OperationType `json:"type"` @@ -122,39 +179,43 @@ func (op *OpBase) UnmarshalJSON(data []byte) error { return err } - op.OperationType = aux.OperationType - op.Author = author - op.UnixTime = aux.UnixTime - op.Metadata = aux.Metadata + base.OperationType = aux.OperationType + base.Author_ = author + base.UnixTime = aux.UnixTime + base.Metadata = aux.Metadata return nil } +func (base *OpBase) Type() OperationType { + return base.OperationType +} + // Time return the time when the operation was added -func (op *OpBase) Time() time.Time { - return time.Unix(op.UnixTime, 0) +func (base *OpBase) Time() time.Time { + return time.Unix(base.UnixTime, 0) } // GetFiles return the files needed by this operation -func (op *OpBase) GetFiles() []repository.Hash { +func (base *OpBase) GetFiles() []repository.Hash { return nil } // Validate check the OpBase for errors -func opBaseValidate(op Operation, opType OperationType) error { - if op.base().OperationType != opType { - return fmt.Errorf("incorrect operation type (expected: %v, actual: %v)", opType, op.base().OperationType) +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 op.base().Author == nil { + if base.Author_ == nil { return fmt.Errorf("author not set") } - if err := op.base().Author.Validate(); err != nil { + if err := op.Author().Validate(); err != nil { return errors.Wrap(err, "author") } @@ -168,46 +229,55 @@ func opBaseValidate(op Operation, opType OperationType) error { } // SetMetadata store arbitrary metadata about the operation -func (op *OpBase) SetMetadata(key string, value string) { - if op.Metadata == nil { - op.Metadata = make(map[string]string) +func (base *OpBase) SetMetadata(key string, value string) { + if base.Metadata == nil { + base.Metadata = make(map[string]string) } - op.Metadata[key] = value - op.id = entity.UnsetId + base.Metadata[key] = value + base.id = entity.UnsetId } // GetMetadata retrieve arbitrary metadata about the operation -func (op *OpBase) GetMetadata(key string) (string, bool) { - val, ok := op.Metadata[key] +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 = op.extraMetadata[key] + val, ok = base.extraMetadata[key] return val, ok } // AllMetadata return all metadata for this operation -func (op *OpBase) AllMetadata() map[string]string { +func (base *OpBase) AllMetadata() map[string]string { result := make(map[string]string) - for key, val := range op.extraMetadata { + for key, val := range base.extraMetadata { result[key] = val } // Original metadata take precedence - for key, val := range op.Metadata { + for key, val := range base.Metadata { result[key] = val } return result } -// GetAuthor return author identity -func (op *OpBase) GetAuthor() identity.Interface { - return op.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 + } +} + +// Author return author identity +func (base *OpBase) Author() identity.Interface { + return base.Author_ } diff --git a/bug/operation_iterator.go b/bug/operation_iterator.go deleted file mode 100644 index f42b1776a..000000000 --- a/bug/operation_iterator.go +++ /dev/null @@ -1,72 +0,0 @@ -package bug - -type OperationIterator struct { - bug *Bug - packIndex int - opIndex int -} - -func NewOperationIterator(bug Interface) *OperationIterator { - return &OperationIterator{ - bug: bugFromInterface(bug), - packIndex: 0, - opIndex: -1, - } -} - -func (it *OperationIterator) Next() bool { - // Special case of the staging area - if it.packIndex == len(it.bug.packs) { - pack := it.bug.staging - it.opIndex++ - return it.opIndex < len(pack.Operations) - } - - if it.packIndex >= len(it.bug.packs) { - return false - } - - pack := it.bug.packs[it.packIndex] - - it.opIndex++ - - if it.opIndex < len(pack.Operations) { - return true - } - - // Note: this iterator doesn't handle the empty pack case - it.opIndex = 0 - it.packIndex++ - - // Special case of the non-empty staging area - if it.packIndex == len(it.bug.packs) && len(it.bug.staging.Operations) > 0 { - return true - } - - return it.packIndex < len(it.bug.packs) -} - -func (it *OperationIterator) Value() Operation { - // Special case of the staging area - if it.packIndex == len(it.bug.packs) { - pack := it.bug.staging - - if it.opIndex >= len(pack.Operations) { - panic("Iterator is not valid anymore") - } - - return pack.Operations[it.opIndex] - } - - if it.packIndex >= len(it.bug.packs) { - panic("Iterator is not valid anymore") - } - - pack := it.bug.packs[it.packIndex] - - if it.opIndex >= len(pack.Operations) { - panic("Iterator is not valid anymore") - } - - return pack.Operations[it.opIndex] -} diff --git a/bug/operation_iterator_test.go b/bug/operation_iterator_test.go deleted file mode 100644 index 81d87a5fd..000000000 --- a/bug/operation_iterator_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package bug - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/identity" - "github.com/MichaelMure/git-bug/repository" -) - -func ExampleOperationIterator() { - b := NewBug() - - // add operations - - it := NewOperationIterator(b) - - for it.Next() { - // do something with each operations - _ = it.Value() - } -} - -func TestOpIterator(t *testing.T) { - repo := repository.NewMockRepo() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - err = rene.Commit(repo) - require.NoError(t, err) - - unix := time.Now().Unix() - - createOp := NewCreateOp(rene, unix, "title", "message", nil) - addCommentOp := NewAddCommentOp(rene, unix, "message2", nil) - setStatusOp := NewSetStatusOp(rene, unix, ClosedStatus) - labelChangeOp := NewLabelChangeOperation(rene, unix, []Label{"added"}, []Label{"removed"}) - - var i int - genTitleOp := func() Operation { - i++ - return NewSetTitleOp(rene, unix, fmt.Sprintf("title%d", i), "") - } - - bug1 := NewBug() - - // first pack - bug1.Append(createOp) - bug1.Append(addCommentOp) - bug1.Append(setStatusOp) - bug1.Append(labelChangeOp) - err = bug1.Commit(repo) - require.NoError(t, err) - - // second pack - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - err = bug1.Commit(repo) - require.NoError(t, err) - - // staging - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - bug1.Append(genTitleOp()) - - it := NewOperationIterator(bug1) - - counter := 0 - for it.Next() { - _ = it.Value() - counter++ - } - - require.Equal(t, 10, counter) -} diff --git a/bug/operation_pack.go b/bug/operation_pack.go deleted file mode 100644 index 74d15f508..000000000 --- a/bug/operation_pack.go +++ /dev/null @@ -1,187 +0,0 @@ -package bug - -import ( - "encoding/json" - "fmt" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" -) - -// 1: original format -// 2: no more legacy identities -// 3: Ids are generated from the create operation serialized data instead of from the first git commit -const formatVersion = 3 - -// OperationPack represent an ordered set of operation to apply -// to a Bug. These operations are stored in a single Git commit. -// -// These commits will be linked together in a linear chain of commits -// inside Git to form the complete ordered chain of operation to -// apply to get the final state of the Bug -type OperationPack struct { - Operations []Operation - - // Private field so not serialized - commitHash repository.Hash -} - -func (opp *OperationPack) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Version uint `json:"version"` - Operations []Operation `json:"ops"` - }{ - Version: formatVersion, - Operations: opp.Operations, - }) -} - -func (opp *OperationPack) UnmarshalJSON(data []byte) error { - aux := struct { - Version uint `json:"version"` - Operations []json.RawMessage `json:"ops"` - }{} - - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - if aux.Version < formatVersion { - return entity.NewErrOldFormatVersion(aux.Version) - } - if aux.Version > formatVersion { - return entity.NewErrNewFormatVersion(aux.Version) - } - - for _, raw := range aux.Operations { - var t struct { - OperationType OperationType `json:"type"` - } - - if err := json.Unmarshal(raw, &t); err != nil { - return err - } - - // delegate to specialized unmarshal function - op, err := opp.unmarshalOp(raw, t.OperationType) - if err != nil { - return err - } - - opp.Operations = append(opp.Operations, op) - } - - return nil -} - -func (opp *OperationPack) unmarshalOp(raw []byte, _type OperationType) (Operation, error) { - switch _type { - case AddCommentOp: - op := &AddCommentOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case CreateOp: - op := &CreateOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case EditCommentOp: - op := &EditCommentOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case LabelChangeOp: - op := &LabelChangeOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case NoOpOp: - op := &NoOpOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case SetMetadataOp: - op := &SetMetadataOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case SetStatusOp: - op := &SetStatusOperation{} - err := json.Unmarshal(raw, &op) - return op, err - case SetTitleOp: - op := &SetTitleOperation{} - err := json.Unmarshal(raw, &op) - return op, err - default: - return nil, fmt.Errorf("unknown operation type %v", _type) - } -} - -// Append a new operation to the pack -func (opp *OperationPack) Append(op Operation) { - opp.Operations = append(opp.Operations, op) -} - -// IsEmpty tell if the OperationPack is empty -func (opp *OperationPack) IsEmpty() bool { - return len(opp.Operations) == 0 -} - -// IsValid tell if the OperationPack is considered valid -func (opp *OperationPack) Validate() error { - if opp.IsEmpty() { - return fmt.Errorf("empty") - } - - for _, op := range opp.Operations { - if err := op.Validate(); err != nil { - return errors.Wrap(err, "op") - } - } - - return nil -} - -// Write will serialize and store the OperationPack as a git blob and return -// its hash -func (opp *OperationPack) Write(repo repository.ClockedRepo) (repository.Hash, error) { - // make sure we don't write invalid data - err := opp.Validate() - if err != nil { - return "", errors.Wrap(err, "validation error") - } - - // First, make sure that all the identities are properly Commit as well - // TODO: this might be downgraded to "make sure it exist in git" but then, what make - // sure no data is lost on identities ? - for _, op := range opp.Operations { - if op.base().Author.NeedCommit() { - return "", fmt.Errorf("identity need commmit") - } - } - - data, err := json.Marshal(opp) - if err != nil { - return "", err - } - - hash, err := repo.StoreData(data) - if err != nil { - return "", err - } - - return hash, nil -} - -// Make a deep copy -func (opp *OperationPack) Clone() OperationPack { - - clone := OperationPack{ - Operations: make([]Operation, len(opp.Operations)), - commitHash: opp.commitHash, - } - - for i, op := range opp.Operations { - clone.Operations[i] = op - } - - return clone -} diff --git a/bug/operation_pack_test.go b/bug/operation_pack_test.go deleted file mode 100644 index 02d72f0f9..000000000 --- a/bug/operation_pack_test.go +++ /dev/null @@ -1,78 +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" -) - -func TestOperationPackSerialize(t *testing.T) { - opp := &OperationPack{} - - repo := repository.NewMockRepo() - - rene, err := identity.NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - - createOp := NewCreateOp(rene, time.Now().Unix(), "title", "message", nil) - setTitleOp := NewSetTitleOp(rene, time.Now().Unix(), "title2", "title1") - addCommentOp := NewAddCommentOp(rene, time.Now().Unix(), "message2", nil) - setStatusOp := NewSetStatusOp(rene, time.Now().Unix(), ClosedStatus) - labelChangeOp := NewLabelChangeOperation(rene, time.Now().Unix(), []Label{"added"}, []Label{"removed"}) - - opp.Append(createOp) - opp.Append(setTitleOp) - opp.Append(addCommentOp) - opp.Append(setStatusOp) - opp.Append(labelChangeOp) - - opMeta := NewSetTitleOp(rene, time.Now().Unix(), "title3", "title2") - opMeta.SetMetadata("key", "value") - opp.Append(opMeta) - - require.Equal(t, 1, len(opMeta.Metadata)) - - opFile := NewAddCommentOp(rene, time.Now().Unix(), "message", []repository.Hash{ - "abcdef", - "ghijkl", - }) - opp.Append(opFile) - - require.Equal(t, 2, len(opFile.Files)) - - data, err := json.Marshal(opp) - require.NoError(t, err) - - var opp2 *OperationPack - err = json.Unmarshal(data, &opp2) - require.NoError(t, err) - - ensureIds(opp) - ensureAuthors(t, opp, opp2) - - require.Equal(t, opp, opp2) -} - -func ensureIds(opp *OperationPack) { - for _, op := range opp.Operations { - op.Id() - } -} - -func ensureAuthors(t *testing.T, opp1 *OperationPack, opp2 *OperationPack) { - require.Equal(t, len(opp1.Operations), len(opp2.Operations)) - for i := 0; i < len(opp1.Operations); i++ { - op1 := opp1.Operations[i] - op2 := opp2.Operations[i] - - // ensure we have equivalent authors (IdentityStub vs Identity) then - // enforce equality - require.Equal(t, op1.base().Author.Id(), op2.base().Author.Id()) - op1.base().Author = op2.base().Author - } -} diff --git a/bug/operation_test.go b/bug/operation_test.go index f66938ade..619f2b437 100644 --- a/bug/operation_test.go +++ b/bug/operation_test.go @@ -45,7 +45,7 @@ func TestValidate(t *testing.T) { 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, + Author_: rene, UnixTime: 0, OperationType: CreateOp, }, @@ -121,7 +121,7 @@ func TestID(t *testing.T) { require.NoError(t, id2.Validate()) require.Equal(t, id1, id2) - b2, err := ReadLocal(repo, b.Id()) + b2, err := Read(repo, b.Id()) require.NoError(t, err) op3 := b2.FirstOp() diff --git a/bug/sorting.go b/bug/sorting.go index d1c370d30..2e64b92dc 100644 --- a/bug/sorting.go +++ b/bug/sorting.go @@ -7,11 +7,11 @@ func (b BugsByCreationTime) Len() int { } func (b BugsByCreationTime) Less(i, j int) bool { - if b[i].createTime < b[j].createTime { + if b[i].CreateLamportTime() < b[j].CreateLamportTime() { return true } - if b[i].createTime > b[j].createTime { + if b[i].CreateLamportTime() > b[j].CreateLamportTime() { return false } @@ -35,11 +35,11 @@ func (b BugsByEditTime) Len() int { } func (b BugsByEditTime) Less(i, j int) bool { - if b[i].editTime < b[j].editTime { + if b[i].EditLamportTime() < b[j].EditLamportTime() { return true } - if b[i].editTime > b[j].editTime { + if b[i].EditLamportTime() > b[j].EditLamportTime() { return false } diff --git a/bug/with_snapshot.go b/bug/with_snapshot.go index 41192d396..9b706d61d 100644 --- a/bug/with_snapshot.go +++ b/bug/with_snapshot.go @@ -50,9 +50,3 @@ func (b *WithSnapshot) Commit(repo repository.ClockedRepo) error { b.snap.id = b.Bug.Id() return nil } - -// Merge intercept Bug.Merge() and clear the snapshot -func (b *WithSnapshot) Merge(repo repository.Repo, other Interface) (bool, error) { - b.snap = nil - return b.Bug.Merge(repo, other) -} diff --git a/cache/bug_cache.go b/cache/bug_cache.go index ca526f7b3..bbe9830f9 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -51,9 +51,7 @@ func (c *BugCache) ResolveOperationWithMetadata(key string, value string) (entit // preallocate but empty matching := make([]entity.Id, 0, 5) - it := bug.NewOperationIterator(c.bug) - for it.Next() { - op := it.Value() + for _, op := range c.bug.Operations() { opValue, ok := op.GetMetadata(key) if ok && value == opValue { matching = append(matching, op.Id()) diff --git a/cache/bug_excerpt.go b/cache/bug_excerpt.go index 6a9e7f75a..152bdacfe 100644 --- a/cache/bug_excerpt.go +++ b/cache/bug_excerpt.go @@ -87,7 +87,7 @@ func NewBugExcerpt(b bug.Interface, snap *bug.Snapshot) *BugExcerpt { } switch snap.Author.(type) { - case *identity.Identity, *IdentityCache: + case *identity.Identity, *identity.IdentityStub, *IdentityCache: e.AuthorId = snap.Author.Id() default: panic("unhandled identity type") diff --git a/cache/repo_cache.go b/cache/repo_cache.go index ab3e1bcb6..58022bdad 100644 --- a/cache/repo_cache.go +++ b/cache/repo_cache.go @@ -195,7 +195,7 @@ func (c *RepoCache) buildCache() error { c.bugExcerpts = make(map[entity.Id]*BugExcerpt) - allBugs := bug.ReadAllLocal(c.repo) + allBugs := bug.ReadAll(c.repo) // wipe the index just to be sure err := c.repo.ClearBleveIndex("bug") diff --git a/cache/repo_cache_bug.go b/cache/repo_cache_bug.go index 9f011c045..c05f30cfd 100644 --- a/cache/repo_cache_bug.go +++ b/cache/repo_cache_bug.go @@ -151,7 +151,7 @@ func (c *RepoCache) ResolveBug(id entity.Id) (*BugCache, error) { } c.muBug.RUnlock() - b, err := bug.ReadLocalWithResolver(c.repo, newIdentityCacheResolver(c), id) + b, err := bug.ReadWithResolver(c.repo, newIdentityCacheResolver(c), id) if err != nil { return nil, err } diff --git a/cache/repo_cache_common.go b/cache/repo_cache_common.go index 5dc19d22e..e23315f92 100644 --- a/cache/repo_cache_common.go +++ b/cache/repo_cache_common.go @@ -95,6 +95,12 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { go func() { defer close(out) + author, err := c.GetUserIdentity() + if err != nil { + out <- entity.NewMergeError(err, "") + return + } + results := identity.MergeAll(c.repo, remote) for result := range results { out <- result @@ -112,7 +118,7 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { } } - results = bug.MergeAll(c.repo, remote) + results = bug.MergeAll(c.repo, remote, author) for result := range results { out <- result @@ -130,11 +136,10 @@ func (c *RepoCache) MergeAll(remote string) <-chan entity.MergeResult { } } - err := c.write() - - // No easy way out here .. + err = c.write() if err != nil { - panic(err) + out <- entity.NewMergeError(err, "") + return } }() diff --git a/cache/repo_cache_test.go b/cache/repo_cache_test.go index 9cdd584df..a85fde66b 100644 --- a/cache/repo_cache_test.go +++ b/cache/repo_cache_test.go @@ -123,6 +123,10 @@ func TestPushPull(t *testing.T) { require.NoError(t, err) err = cacheA.SetUserIdentity(reneA) require.NoError(t, err) + isaacB, err := cacheB.NewIdentity("Isaac Newton", "isaac@newton.uk") + require.NoError(t, err) + err = cacheB.SetUserIdentity(isaacB) + require.NoError(t, err) // distribute the identity _, err = cacheA.Push("origin") diff --git a/entity/TODO b/entity/TODO deleted file mode 100644 index 9f33dd09c..000000000 --- a/entity/TODO +++ /dev/null @@ -1,9 +0,0 @@ -- is the pack Lamport clock really useful vs only topological sort? - - topological order is enforced on the clocks, so what's the point? - - is EditTime equivalent to PackTime? no, avoid the gaps. Is it better? - --> PackTime is contained within a bug and might avoid extreme reordering? -- how to do commit signature? -- how to avoid id collision between Operations? -- write tests for actions -- migrate Bug to the new structure -- migrate Identity to the new structure? \ No newline at end of file diff --git a/entity/merge.go b/entity/merge.go index 1b68b4de2..0661b7fc1 100644 --- a/entity/merge.go +++ b/entity/merge.go @@ -42,13 +42,15 @@ func (mr MergeResult) String() string { case MergeStatusNothing: return "nothing to do" case MergeStatusError: - return fmt.Sprintf("merge error on %s: %s", mr.Id, mr.Err.Error()) + if mr.Id != "" { + return fmt.Sprintf("merge error on %s: %s", mr.Id, mr.Err.Error()) + } + return fmt.Sprintf("merge error: %s", mr.Err.Error()) default: panic("unknown merge status") } } -// TODO: Interface --> *Entity ? func NewMergeNewStatus(id Id, entity Interface) MergeResult { return MergeResult{ Id: id, diff --git a/go.sum b/go.sum index 9d0a8c825..e316fb66e 100644 --- a/go.sum +++ b/go.sum @@ -575,6 +575,7 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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= diff --git a/misc/random_bugs/create_random_bugs.go b/misc/random_bugs/create_random_bugs.go index 3d93135ec..a69918f43 100644 --- a/misc/random_bugs/create_random_bugs.go +++ b/misc/random_bugs/create_random_bugs.go @@ -111,52 +111,6 @@ func generateRandomBugsWithSeed(opts Options, seed int64) []*bug.Bug { return result } -func GenerateRandomOperationPacks(packNumber int, opNumber int) []*bug.OperationPack { - return GenerateRandomOperationPacksWithSeed(packNumber, opNumber, time.Now().UnixNano()) -} - -func GenerateRandomOperationPacksWithSeed(packNumber int, opNumber int, seed int64) []*bug.OperationPack { - // Note: this is a bit crude, only generate a Create + Comments - - panic("this piece of code needs to be updated to make sure that the identities " + - "are properly commit before usage. That is, generateRandomPersons() need to be called.") - - rand.Seed(seed) - fake.Seed(seed) - - result := make([]*bug.OperationPack, packNumber) - - for i := 0; i < packNumber; i++ { - opp := &bug.OperationPack{} - - var op bug.Operation - - op = bug.NewCreateOp( - randomPerson(), - time.Now().Unix(), - fake.Sentence(), - paragraphs(), - nil, - ) - - opp.Append(op) - - for j := 0; j < opNumber-1; j++ { - op = bug.NewAddCommentOp( - randomPerson(), - time.Now().Unix(), - paragraphs(), - nil, - ) - opp.Append(op) - } - - result[i] = opp - } - - return result -} - func person(repo repository.RepoClock) (*identity.Identity, error) { return identity.NewIdentity(repo, fake.FullName(), fake.EmailAddress()) } diff --git a/tests/read_bugs_test.go b/tests/read_bugs_test.go index 53b84fd54..b19836895 100644 --- a/tests/read_bugs_test.go +++ b/tests/read_bugs_test.go @@ -14,7 +14,7 @@ func TestReadBugs(t *testing.T) { random_bugs.FillRepoWithSeed(repo, 15, 42) - bugs := bug.ReadAllLocal(repo) + bugs := bug.ReadAll(repo) for b := range bugs { if b.Err != nil { t.Fatal(b.Err) @@ -30,7 +30,7 @@ func benchmarkReadBugs(bugNumber int, t *testing.B) { t.ResetTimer() for n := 0; n < t.N; n++ { - bugs := bug.ReadAllLocal(repo) + bugs := bug.ReadAll(repo) for b := range bugs { if b.Err != nil { t.Fatal(b.Err) -- cgit v1.2.3 From bd09541752ef4db008500d238762ebe7f2f7be39 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 20 Feb 2021 14:37:06 +0100 Subject: entity: no sign-post needed --- bug/op_add_comment.go | 3 --- bug/op_create.go | 3 --- bug/op_edit_comment.go | 3 --- bug/op_label_change.go | 3 --- bug/op_noop.go | 3 --- bug/op_set_metadata.go | 3 --- bug/op_set_status.go | 3 --- bug/op_set_title.go | 3 --- bug/operation.go | 3 --- 9 files changed, 27 deletions(-) (limited to 'bug/op_create.go') diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index fd00860b2..4cba200f4 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -21,9 +21,6 @@ type AddCommentOperation struct { Files []repository.Hash `json:"files"` } -// Sign-post method for gqlgen -func (op *AddCommentOperation) IsOperation() {} - func (op *AddCommentOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_create.go b/bug/op_create.go index 2423e5714..e3e38ade4 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -27,9 +27,6 @@ type CreateOperation struct { Files []repository.Hash `json:"files"` } -// Sign-post method for gqlgen -func (op *CreateOperation) IsOperation() {} - func (op *CreateOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index e08aeaad9..653ab71e5 100644 --- a/bug/op_edit_comment.go +++ b/bug/op_edit_comment.go @@ -24,9 +24,6 @@ type EditCommentOperation struct { Files []repository.Hash `json:"files"` } -// Sign-post method for gqlgen -func (op *EditCommentOperation) IsOperation() {} - func (op *EditCommentOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_label_change.go b/bug/op_label_change.go index d682fe54d..8b0e5ec89 100644 --- a/bug/op_label_change.go +++ b/bug/op_label_change.go @@ -21,9 +21,6 @@ type LabelChangeOperation struct { Removed []Label `json:"removed"` } -// Sign-post method for gqlgen -func (op *LabelChangeOperation) IsOperation() {} - func (op *LabelChangeOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_noop.go b/bug/op_noop.go index 81efdb257..1b11e6941 100644 --- a/bug/op_noop.go +++ b/bug/op_noop.go @@ -16,9 +16,6 @@ type NoOpOperation struct { OpBase } -// Sign-post method for gqlgen -func (op *NoOpOperation) IsOperation() {} - func (op *NoOpOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go index 4e596728c..ca19a8384 100644 --- a/bug/op_set_metadata.go +++ b/bug/op_set_metadata.go @@ -17,9 +17,6 @@ type SetMetadataOperation struct { NewMetadata map[string]string `json:"new_metadata"` } -// Sign-post method for gqlgen -func (op *SetMetadataOperation) IsOperation() {} - func (op *SetMetadataOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_set_status.go b/bug/op_set_status.go index ca8c434a9..e22ded54c 100644 --- a/bug/op_set_status.go +++ b/bug/op_set_status.go @@ -18,9 +18,6 @@ type SetStatusOperation struct { Status Status `json:"status"` } -// Sign-post method for gqlgen -func (op *SetStatusOperation) IsOperation() {} - func (op *SetStatusOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/op_set_title.go b/bug/op_set_title.go index 899b4fa37..c6a26746c 100644 --- a/bug/op_set_title.go +++ b/bug/op_set_title.go @@ -21,9 +21,6 @@ type SetTitleOperation struct { Was string `json:"was"` } -// Sign-post method for gqlgen -func (op *SetTitleOperation) IsOperation() {} - func (op *SetTitleOperation) Id() entity.Id { return idOperation(op, &op.OpBase) } diff --git a/bug/operation.go b/bug/operation.go index 71a5c15dc..0423c2295 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -49,9 +49,6 @@ type Operation interface { AllMetadata() map[string]string setExtraMetadataImmutable(key string, value string) - - // sign-post method for gqlgen - IsOperation() } func idOperation(op Operation, base *OpBase) entity.Id { -- cgit v1.2.3 From f1d4a19af81fcc05ae9d90e018ff141f6521335a Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 14 Mar 2021 18:39:04 +0100 Subject: bug: nonce on all operation to prevent id collision --- bug/op_create.go | 15 --------------- bug/operation.go | 26 ++++++++++++++++++++++++++ entity/dag/operation.go | 16 +++++++++++++--- entity/id.go | 2 +- identity/version.go | 2 +- 5 files changed, 41 insertions(+), 20 deletions(-) (limited to 'bug/op_create.go') diff --git a/bug/op_create.go b/bug/op_create.go index e3e38ade4..37e1ddc56 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -1,7 +1,6 @@ package bug import ( - "crypto/rand" "encoding/json" "fmt" "strings" @@ -18,10 +17,6 @@ var _ Operation = &CreateOperation{} // CreateOperation define the initial creation of a bug type CreateOperation struct { OpBase - // mandatory random bytes to ensure a better randomness of the data of the first - // operation of a bug, used to later generate the ID - // len(Nonce) should be > 20 and < 64 bytes - Nonce []byte `json:"nonce"` Title string `json:"title"` Message string `json:"message"` Files []repository.Hash `json:"files"` @@ -147,19 +142,9 @@ func (op *CreateOperation) UnmarshalJSON(data []byte) error { // Sign post method for gqlgen func (op *CreateOperation) IsAuthored() {} -func makeNonce(len int) []byte { - result := make([]byte, len) - _, err := rand.Read(result) - if err != nil { - panic(err) - } - return result -} - func NewCreateOp(author identity.Interface, unixTime int64, title, message string, files []repository.Hash) *CreateOperation { return &CreateOperation{ OpBase: newOpBase(CreateOp, author, unixTime), - Nonce: makeNonce(20), Title: title, Message: message, Files: files, diff --git a/bug/operation.go b/bug/operation.go index 0423c2295..d01f1cc99 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -1,6 +1,7 @@ package bug import ( + "crypto/rand" "encoding/json" "fmt" "time" @@ -138,6 +139,12 @@ type OpBase struct { // 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, @@ -151,10 +158,20 @@ func newOpBase(opType OperationType, author identity.Interface, unixTime int64) 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) @@ -164,6 +181,7 @@ func (base *OpBase) UnmarshalJSON(data []byte) error { Author json.RawMessage `json:"author"` UnixTime int64 `json:"timestamp"` Metadata map[string]string `json:"metadata,omitempty"` + Nonce []byte `json:"nonce"` }{} if err := json.Unmarshal(data, &aux); err != nil { @@ -180,6 +198,7 @@ func (base *OpBase) UnmarshalJSON(data []byte) error { base.Author_ = author base.UnixTime = aux.UnixTime base.Metadata = aux.Metadata + base.Nonce = aux.Nonce return nil } @@ -222,6 +241,13 @@ func (base *OpBase) Validate(op Operation, opType OperationType) error { } } + 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 } diff --git a/entity/dag/operation.go b/entity/dag/operation.go index b0a78de6f..94974a825 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -10,13 +10,23 @@ import ( // data structure and storage. type Operation interface { // Id return the Operation identifier + // // Some care need to be taken to define a correct Id derivation and enough entropy in the data used to avoid // collisions. Notably: - // - the Id of the first Operation will be used as the Id of the Entity. Collision need to be avoided across entities of the same type - // (example: no collision within the "bug" namespace). + // - the Id of the first Operation will be used as the Id of the Entity. Collision need to be avoided across entities + // of the same type (example: no collision within the "bug" namespace). // - collisions can also happen within the set of Operations of an Entity. Simple Operation might not have enough // entropy to yield unique Ids (example: two "close" operation within the same second, same author). - // A common way to derive an Id will be to use the DeriveId function on the serialized operation data. + // If this is a concern, it is recommended to include a piece of random data in the operation's data, to guarantee + // 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 + // 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 // Validate check if the Operation data is valid Validate() error diff --git a/entity/id.go b/entity/id.go index b602452e7..c8dbdb94d 100644 --- a/entity/id.go +++ b/entity/id.go @@ -18,7 +18,7 @@ const UnsetId = Id("unset") // Id is an identifier for an entity or part of an entity type Id string -// DeriveId generate an Id from some data, taken from a root part of the entity. +// DeriveId generate an Id from the serialization of the object or part of the object. func DeriveId(data []byte) Id { // My understanding is that sha256 is enough to prevent collision (git use that, so ...?) // If you read this code, I'd be happy to be schooled. diff --git a/identity/version.go b/identity/version.go index cbd56a983..1c35831e0 100644 --- a/identity/version.go +++ b/identity/version.go @@ -37,7 +37,7 @@ type version struct { keys []*Key // mandatory random bytes to ensure a better randomness of the data of the first - // version of a bug, used to later generate the ID + // version of an identity, used to later generate the ID // len(Nonce) should be > 20 and < 64 bytes // It has no functional purpose and should be ignored. // TODO: optional after first version? -- cgit v1.2.3 From 214abe4dea1984086e45d1399538fb12aa010642 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sat, 20 Feb 2021 15:48:44 +0100 Subject: WIP operation with files --- bug/op_add_comment.go | 2 ++ bug/op_create.go | 2 ++ bug/op_edit_comment.go | 2 ++ bug/operation.go | 17 ++++++--------- entity/dag/common_test.go | 15 ++++++++----- entity/dag/operation.go | 9 ++++++++ entity/dag/operation_pack.go | 45 ++++++++++++++++++++++++++++++++++++--- entity/dag/operation_pack_test.go | 20 +++++++++++++++-- 8 files changed, 91 insertions(+), 21 deletions(-) (limited to 'bug/op_create.go') diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index 4cba200f4..f835866b5 100644 --- a/bug/op_add_comment.go +++ b/bug/op_add_comment.go @@ -5,6 +5,7 @@ import ( "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/repository" "github.com/MichaelMure/git-bug/util/text" @@ -12,6 +13,7 @@ import ( ) var _ Operation = &AddCommentOperation{} +var _ dag.OperationWithFiles = &AddCommentOperation{} // AddCommentOperation will add a new comment in the bug type AddCommentOperation struct { diff --git a/bug/op_create.go b/bug/op_create.go index 37e1ddc56..75b60bd8b 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -6,6 +6,7 @@ import ( "strings" "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/text" @@ -13,6 +14,7 @@ import ( ) var _ Operation = &CreateOperation{} +var _ dag.OperationWithFiles = &CreateOperation{} // CreateOperation define the initial creation of a bug type CreateOperation struct { diff --git a/bug/op_edit_comment.go b/bug/op_edit_comment.go index 653ab71e5..3e6634e4c 100644 --- a/bug/op_edit_comment.go +++ b/bug/op_edit_comment.go @@ -7,6 +7,7 @@ import ( "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/repository" "github.com/MichaelMure/git-bug/util/timestamp" @@ -15,6 +16,7 @@ import ( ) var _ Operation = &EditCommentOperation{} +var _ dag.OperationWithFiles = &EditCommentOperation{} // EditCommentOperation will change a comment in the bug type EditCommentOperation struct { diff --git a/bug/operation.go b/bug/operation.go index d01f1cc99..8daa2cde9 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -11,7 +11,6 @@ import ( "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" ) // OperationType is an operation type identifier @@ -38,10 +37,9 @@ type Operation interface { // Time return the time when the operation was added Time() time.Time - // GetFiles return the files needed by this operation - GetFiles() []repository.Hash // 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 @@ -212,11 +210,6 @@ func (base *OpBase) Time() time.Time { return time.Unix(base.UnixTime, 0) } -// GetFiles return the files needed by this operation -func (base *OpBase) GetFiles() []repository.Hash { - return nil -} - // Validate check the OpBase for errors func (base *OpBase) Validate(op Operation, opType OperationType) error { if base.OperationType != opType { @@ -235,9 +228,11 @@ func (base *OpBase) Validate(op Operation, opType OperationType) error { return errors.Wrap(err, "author") } - for _, hash := range op.GetFiles() { - if !hash.IsValid() { - return fmt.Errorf("file with invalid hash %v", hash) + if op, ok := op.(dag.OperationWithFiles); ok { + for _, hash := range op.GetFiles() { + if !hash.IsValid() { + return fmt.Errorf("file with invalid hash %v", hash) + } } } diff --git a/entity/dag/common_test.go b/entity/dag/common_test.go index fa15cd1f4..1898451d0 100644 --- a/entity/dag/common_test.go +++ b/entity/dag/common_test.go @@ -23,10 +23,11 @@ type op1 struct { OperationType int `json:"type"` Field1 string `json:"field_1"` + Files []repository.Hash } -func newOp1(author identity.Interface, field1 string) *op1 { - return &op1{author: author, OperationType: 1, Field1: field1} +func newOp1(author identity.Interface, field1 string, files ...repository.Hash) *op1 { + return &op1{author: author, OperationType: 1, Field1: field1, Files: files} } func (o *op1) Id() entity.Id { @@ -34,11 +35,15 @@ func (o *op1) Id() entity.Id { return entity.DeriveId(data) } +func (o *op1) Validate() error { return nil } + func (o *op1) Author() identity.Interface { return o.author } -func (o *op1) Validate() error { return nil } +func (o *op1) GetFiles() []repository.Hash { + return o.Files +} type op2 struct { author identity.Interface @@ -56,12 +61,12 @@ func (o *op2) Id() entity.Id { return entity.DeriveId(data) } +func (o *op2) Validate() error { return nil } + func (o *op2) Author() identity.Interface { return o.author } -func (o *op2) Validate() error { return nil } - func unmarshaler(author identity.Interface, raw json.RawMessage) (Operation, error) { var t struct { OperationType int `json:"type"` diff --git a/entity/dag/operation.go b/entity/dag/operation.go index 94974a825..1bfb3d3df 100644 --- a/entity/dag/operation.go +++ b/entity/dag/operation.go @@ -3,6 +3,7 @@ package dag import ( "github.com/MichaelMure/git-bug/entity" "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/repository" ) // Operation is a piece of data defining a change to reflect on the state of an Entity. @@ -33,3 +34,11 @@ type Operation interface { // Author returns the author of this operation Author() identity.Interface } + +// OperationWithFiles is an extended Operation that has files dependency, stored in git. +type OperationWithFiles interface { + Operation + + // GetFiles return the files needed by this operation + GetFiles() []repository.Hash +} diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go index a436fd332..72063c604 100644 --- a/entity/dag/operation_pack.go +++ b/entity/dag/operation_pack.go @@ -15,10 +15,8 @@ import ( "github.com/MichaelMure/git-bug/util/lamport" ) -// TODO: extra data tree -const extraEntryName = "extra" - const opsEntryName = "ops" +const extraEntryName = "extra" const versionEntryPrefix = "version-" const createClockEntryPrefix = "create-clock-" const editClockEntryPrefix = "edit-clock-" @@ -118,6 +116,7 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm // Make a Git tree referencing this blob and encoding the other values: // - format version // - clocks + // - extra data tree := []repository.TreeEntry{ {ObjectType: repository.Blob, Hash: emptyBlobHash, Name: fmt.Sprintf(versionEntryPrefix+"%d", def.FormatVersion)}, @@ -133,6 +132,17 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm Name: fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime), }) } + if extraTree := opp.makeExtraTree(); len(extraTree) > 0 { + extraTreeHash, err := repo.StoreTree(extraTree) + if err != nil { + return "", err + } + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Tree, + Hash: extraTreeHash, + Name: extraEntryName, + }) + } // Store the tree treeHash, err := repo.StoreTree(tree) @@ -163,6 +173,35 @@ func (opp *operationPack) Write(def Definition, repo repository.Repo, parentComm return commitHash, nil } +func (opp *operationPack) makeExtraTree() []repository.TreeEntry { + var tree []repository.TreeEntry + counter := 0 + added := make(map[repository.Hash]interface{}) + + for _, ops := range opp.Operations { + ops, ok := ops.(OperationWithFiles) + if !ok { + continue + } + + for _, file := range ops.GetFiles() { + if _, has := added[file]; !has { + tree = append(tree, repository.TreeEntry{ + ObjectType: repository.Blob, + Hash: file, + // The name is not important here, we only need to + // reference the blob. + Name: fmt.Sprintf("file%d", counter), + }) + counter++ + added[file] = struct{}{} + } + } + } + + return tree +} + // readOperationPack read the operationPack encoded in git at the given Tree hash. // // Validity of the Lamport clocks is left for the caller to decide. diff --git a/entity/dag/operation_pack_test.go b/entity/dag/operation_pack_test.go index a12382afe..0fe98dc7d 100644 --- a/entity/dag/operation_pack_test.go +++ b/entity/dag/operation_pack_test.go @@ -1,6 +1,7 @@ package dag import ( + "math/rand" "testing" "github.com/stretchr/testify/require" @@ -11,10 +12,16 @@ import ( func TestOperationPackReadWrite(t *testing.T) { repo, id1, _, resolver, def := makeTestContext() + blobHash1, err := repo.StoreData(randomData()) + require.NoError(t, err) + + blobHash2, err := repo.StoreData(randomData()) + require.NoError(t, err) + opp := &operationPack{ Author: id1, Operations: []Operation{ - newOp1(id1, "foo"), + newOp1(id1, "foo", blobHash1, blobHash2), newOp2(id1, "bar"), }, CreateTime: 123, @@ -36,7 +43,7 @@ func TestOperationPackReadWrite(t *testing.T) { opp3 := &operationPack{ Author: id1, Operations: []Operation{ - newOp1(id1, "foo"), + newOp1(id1, "foo", blobHash1, blobHash2), newOp2(id1, "bar"), }, CreateTime: 123, @@ -86,3 +93,12 @@ func TestOperationPackSignedReadWrite(t *testing.T) { } require.Equal(t, opp.Id(), opp3.Id()) } + +func randomData() []byte { + var letterRunes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + b := make([]byte, 32) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return b +} -- cgit v1.2.3