Detailed changes
@@ -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
@@ -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)
}
@@ -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,
@@ -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,
@@ -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
}
@@ -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()
@@ -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
}
@@ -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)
@@ -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
@@ -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
@@ -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.
@@ -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.
+
@@ -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=()
@@ -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
}
@@ -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
}