From 0a9f6b5da62000833fd92f56cfb53d712f70d8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= <batolettre@gmail.com> Date: Tue, 14 Jul 2020 23:01:57 +0200 Subject: [PATCH] bug: improve the comment Id code structure --- bug/comment.go | 48 +++++++++++++++++++----------- bug/comment_test.go | 27 +++++++++-------- bug/op_add_comment.go | 2 +- bug/op_create.go | 2 +- bug/snapshot.go | 5 ++++ cache/repo_cache_bug.go | 48 ++++++++++++++++++++++++++++++ commands/comment_edit.go | 8 ++--- commands/select/select.go | 24 --------------- doc/man/git-bug-comment-edit.1 | 36 ++++++++++++++++++++++ doc/man/git-bug-comment.1 | 2 +- doc/md/git-bug_comment.md | 1 + doc/md/git-bug_comment_edit.md | 24 +++++++++++++++ misc/bash_completion/git-bug | 29 ++++++++++++++++++ misc/powershell_completion/git-bug | 8 +++++ misc/zsh_completion/git-bug | 10 +++++++ 15 files changed, 213 insertions(+), 61 deletions(-) create mode 100644 doc/man/git-bug-comment-edit.1 create mode 100644 doc/md/git-bug_comment_edit.md diff --git a/bug/comment.go b/bug/comment.go index f79e1293..1a9ca05a 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" @@ -21,8 +23,6 @@ type Comment struct { UnixTime timestamp.Timestamp } -const compiledCommentIdFormat = "BCBCBCBBBCBBBBCBBBBCBBBBCBBBBCBBBBCBBBBC" - // Id return the Comment identifier func (c Comment) Id() entity.Id { if c.id == "" { @@ -33,34 +33,48 @@ func (c Comment) Id() entity.Id { return c.id } -func DeriveCommentId(bugId entity.ID, commentId entity.ID) entity.ID { - commentIdString := commentId - bugIdString := bugId - 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 += string(bugIdString[0]) - bugIdString = bugIdString[1:] + id.WriteByte(bugId[0]) + bugId = bugId[1:] } else { - id += string(commentIdString[0]) - commentIdString = commentIdString[1:] + id.WriteByte(commentId[0]) + commentId = commentId[1:] } } - return id + return entity.Id(id.String()) } func SplitCommentId(prefix string) (bugPrefix string, commentPrefix string) { - commentIdPrefix := "" - bugIdPrefix := "" + var bugIdPrefix strings.Builder + var commentIdPrefix strings.Builder - for i, char := range id { + for i, char := range prefix { if compiledCommentIdFormat[i] == 'B' { - bugIdPrefix += string(char) + bugIdPrefix.WriteRune(char) } else { - commentIdPrefix += string(char) + commentIdPrefix.WriteRune(char) } } - return bugIdPrefix, commentIdPrefix + return bugIdPrefix.String(), commentIdPrefix.String() } // FormatTimeRel format the UnixTime of the comment for human consumption diff --git a/bug/comment_test.go b/bug/comment_test.go index c2b29d93..a73ae43f 100644 --- a/bug/comment_test.go +++ b/bug/comment_test.go @@ -4,21 +4,24 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/MichaelMure/git-bug/entity" ) -func TestCompileUnpackCommentId(t *testing.T) { - id1 := "abcdefghijklmnopqrstuvwxyz1234" - id2 := "ABCDEFGHIJ" - expectedId := "aAbBcCdefDghijEklmnFopqrGstuvHwxyzI1234J" +func TestCommentId(t *testing.T) { + bugId := entity.Id("abcdefghijklmnopqrstuvwxyz1234__________") + opId := entity.Id("ABCDEFGHIJ______________________________") + expectedId := entity.Id("aAbBcCdefDghijEklmnFopqrGstuvHwxyzI1234J") - compiledId := CompileCommentId(id1, id2) - assert.Equal(t, expectedId, compiledId) + mergedId := DeriveCommentId(bugId, opId) + assert.Equal(t, expectedId, mergedId) - unpackedId1, unpackedId2 := UnpackCommentId(compiledId) - assert.Equal(t, id1, unpackedId1) - assert.Equal(t, id2, unpackedId2) + // full length + splitBugId, splitCommentId := SplitCommentId(mergedId.String()) + assert.Equal(t, string(bugId[:30]), splitBugId) + assert.Equal(t, string(opId[:10]), splitCommentId) - unpackedId1, unpackedId2 = UnpackCommentId(expectedId[:6]) - assert.Equal(t, unpackedId1, id1[:3]) - assert.Equal(t, unpackedId2, id2[:3]) + splitBugId, splitCommentId = SplitCommentId(string(expectedId[:6])) + assert.Equal(t, string(bugId[:3]), splitBugId) + assert.Equal(t, string(opId[:3]), splitCommentId) } diff --git a/bug/op_add_comment.go b/bug/op_add_comment.go index 9a513f52..df426ee0 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 := entity.Id(CompileCommentId(snapshot.Id().String(), op.Id().String())) + commentId := DeriveCommentId(snapshot.Id(), op.Id()) comment := Comment{ id: commentId, Message: op.Message, diff --git a/bug/op_create.go b/bug/op_create.go index 19ce4834..54bb61aa 100644 --- a/bug/op_create.go +++ b/bug/op_create.go @@ -39,7 +39,7 @@ func (op *CreateOperation) Apply(snapshot *Snapshot) { snapshot.Title = op.Title - commentId := entity.Id(CompileCommentId(snapshot.Id().String(), op.Id().String())) + commentId := DeriveCommentId(snapshot.Id(), op.Id()) comment := Comment{ id: commentId, Message: op.Message, diff --git a/bug/snapshot.go b/bug/snapshot.go index 11df04b2..0005b930 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 30692363..6c5be86f 100644 --- a/cache/repo_cache_bug.go +++ b/cache/repo_cache_bug.go @@ -3,6 +3,7 @@ package cache import ( "bytes" "encoding/gob" + "errors" "fmt" "os" "path" @@ -205,6 +206,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 { c.muBug.RLock() diff --git a/commands/comment_edit.go b/commands/comment_edit.go index 856e13c7..61132967 100644 --- a/commands/comment_edit.go +++ b/commands/comment_edit.go @@ -3,7 +3,6 @@ package commands import ( "github.com/spf13/cobra" - _select "github.com/MichaelMure/git-bug/commands/select" "github.com/MichaelMure/git-bug/input" ) @@ -17,7 +16,7 @@ func newCommentEditCommand() *cobra.Command { options := commentEditOptions{} cmd := &cobra.Command{ - Use: "edit <id>", + Use: "edit <commentid>", Short: "Edit an existing comment on a bug.", Args: cobra.ExactArgs(1), PreRunE: loadBackendEnsureUser(env), @@ -40,8 +39,7 @@ func newCommentEditCommand() *cobra.Command { } func runCommentEdit(env *Env, opts commentEditOptions, args []string) error { - b, c, err := _select.ResolveComment(env.backend, args[0]) - + b, commentId, err := env.backend.ResolveComment(args[0]) if err != nil { return err } @@ -64,7 +62,7 @@ func runCommentEdit(env *Env, opts commentEditOptions, args []string) error { } } - _, err = b.EditComment(c, opts.message) + _, err = b.EditComment(commentId, opts.message) if err != nil { return err } diff --git a/commands/select/select.go b/commands/select/select.go index cb427c05..cf861fcc 100644 --- a/commands/select/select.go +++ b/commands/select/select.go @@ -69,30 +69,6 @@ func ResolveBug(repo *cache.RepoCache, args []string) (*cache.BugCache, []string return nil, nil, ErrNoValidId } -func ResolveComment(repo *cache.RepoCache, fullId string) (*cache.BugCache, entity.Id, error) { - bugId, _ := bug.SplitCommentId(fullId) - b, _, err := ResolveBug(repo, []string{bugId}) - if err != nil { - return nil, entity.UnsetId, err - } - - matching := make([]entity.Id, 0, 5) - - for _, comment := range b.Snapshot().Comments { - if comment.Id().HasPrefix(fullId) { - matching = append(matching, comment.Id()) - } - } - - if len(matching) > 1 { - return nil, entity.UnsetId, entity.NewErrMultipleMatch("comment", matching) - } else if len(matching) == 0 { - return nil, entity.UnsetId, errors.New("comment doesn't exist") - } - - return b, matching[0], nil -} - // Select will select a bug for future use func Select(repo *cache.RepoCache, id entity.Id) error { selectPath := selectFilePath(repo) diff --git a/doc/man/git-bug-comment-edit.1 b/doc/man/git-bug-comment-edit.1 new file mode 100644 index 00000000..e20621c1 --- /dev/null +++ b/doc/man/git-bug-comment-edit.1 @@ -0,0 +1,36 @@ +.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 9beb7a06..e4dce2e6 100644 --- a/doc/man/git-bug-comment.1 +++ b/doc/man/git-bug-comment.1 @@ -25,4 +25,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 5a79cf6f..14271f69 100644 --- a/doc/md/git-bug_comment.md +++ b/doc/md/git-bug_comment.md @@ -20,4 +20,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 00000000..0633e479 --- /dev/null +++ b/doc/md/git-bug_comment_edit.md @@ -0,0 +1,24 @@ +## git-bug comment edit + +Edit an existing comment on a bug. + +### Synopsis + +Edit an existing comment on a bug. + +``` +git-bug comment edit <commentid> [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 4152725f..cd9ad1c0 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -654,6 +654,34 @@ _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=") + flags+=("--message=") + two_word_flags+=("--message") + two_word_flags+=("-m") + local_nonpersistent_flags+=("--message=") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _git-bug_comment() { last_command="git-bug_comment" @@ -662,6 +690,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 40831a11..973e4d35 100644 --- a/misc/powershell_completion/git-bug +++ b/misc/powershell_completion/git-bug @@ -117,6 +117,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' { @@ -126,6 +127,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 } diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug index fbd52be5..91730e29 100644 --- a/misc/zsh_completion/git-bug +++ b/misc/zsh_completion/git-bug @@ -235,6 +235,7 @@ function _git-bug_comment { cmnds) commands=( "add:Add a new comment to a bug." + "edit:Edit an existing comment on a bug." ) _describe "command" commands ;; @@ -244,6 +245,9 @@ function _git-bug_comment { add) _git-bug_comment_add ;; + edit) + _git-bug_comment_edit + ;; esac } @@ -253,6 +257,12 @@ function _git-bug_comment_add { '(-m --message)'{-m,--message}'[Provide the new message from the command line]:' } +function _git-bug_comment_edit { + _arguments \ + '(-F --file)'{-F,--file}'[Take the message from the given file. Use - to read the message from the standard input]:' \ + '(-m --message)'{-m,--message}'[Provide the new message from the command line]:' +} + function _git-bug_deselect { _arguments } -- GitLab