aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/go.yml2
-rw-r--r--bridge/gitlab/config.go2
-rw-r--r--bug/op_add_comment_test.go10
-rw-r--r--bug/op_create_test.go3
-rw-r--r--bug/op_edit_comment_test.go3
-rw-r--r--bug/op_label_change_test.go3
-rw-r--r--bug/op_noop_test.go3
-rw-r--r--bug/op_set_metadata_test.go3
-rw-r--r--bug/op_set_status_test.go3
-rw-r--r--bug/op_set_title_test.go3
-rw-r--r--bug/operation.go10
-rw-r--r--commands/bridge_auth_addtoken.go2
-rw-r--r--commands/bridge_auth_rm.go3
-rw-r--r--commands/bridge_auth_show.go3
-rw-r--r--commands/bridge_configure.go1
-rw-r--r--commands/bridge_pull.go3
-rw-r--r--commands/bridge_push.go3
-rw-r--r--commands/bridge_rm.go3
-rw-r--r--commands/comment.go1
-rw-r--r--commands/comment_add.go1
-rw-r--r--commands/helper_completion.go342
-rw-r--r--commands/label.go1
-rw-r--r--commands/label_add.go1
-rw-r--r--commands/label_rm.go1
-rw-r--r--commands/ls.go34
-rw-r--r--commands/ls_test.go43
-rw-r--r--commands/pull.go1
-rw-r--r--commands/push.go1
-rw-r--r--commands/rm.go1
-rw-r--r--commands/root.go8
-rw-r--r--commands/select.go1
-rw-r--r--commands/show.go6
-rw-r--r--commands/status.go1
-rw-r--r--commands/title.go1
-rw-r--r--commands/title_edit.go1
-rw-r--r--commands/user.go6
-rw-r--r--commands/user_adopt.go1
-rw-r--r--commands/user_ls.go1
-rw-r--r--doc/bug-graph-1.pngbin0 -> 56863 bytes
-rw-r--r--doc/merge1.pngbin0 -> 56989 bytes
-rw-r--r--doc/merge2.pngbin0 -> 65479 bytes
-rw-r--r--doc/model.md160
-rw-r--r--doc/operations.pngbin0 -> 12842 bytes
-rw-r--r--entity/dag/entity.go2
-rw-r--r--entity/dag/example_test.go383
-rw-r--r--entity/dag/operation_pack.go2
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--misc/bash_completion/git-bug1569
-rw-r--r--misc/gen_completion.go73
-rw-r--r--query/parser_test.go4
-rw-r--r--util/text/validate.go2
52 files changed, 1201 insertions, 1516 deletions
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 21fff7dc..27353e90 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -51,5 +51,5 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- - name: Check Code Formation
+ - name: Check Code Formatting
run: find . -name "*.go" | while read line; do [ -z "$(gofmt -d "$line" | head)" ] || exit 1; done
diff --git a/bridge/gitlab/config.go b/bridge/gitlab/config.go
index 4a714d09..c9d95adb 100644
--- a/bridge/gitlab/config.go
+++ b/bridge/gitlab/config.go
@@ -200,7 +200,7 @@ func promptToken(baseUrl string) (*auth.Token, error) {
fmt.Println("'api' access scope: to be able to make api calls")
fmt.Println()
- re := regexp.MustCompile(`^[a-zA-Z0-9\-\_]{20}$`)
+ re := regexp.MustCompile(`^(glpat-)?[a-zA-Z0-9\-\_]{20}$`)
var login string
diff --git a/bug/op_add_comment_test.go b/bug/op_add_comment_test.go
index fb6fa8ed..446bdb18 100644
--- a/bug/op_add_comment_test.go
+++ b/bug/op_add_comment_test.go
@@ -5,7 +5,6 @@ import (
"testing"
"time"
- "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/MichaelMure/git-bug/identity"
@@ -22,18 +21,17 @@ func TestAddCommentSerialize(t *testing.T) {
before := NewAddCommentOp(rene, unix, "message", nil)
data, err := json.Marshal(before)
- assert.NoError(t, err)
+ require.NoError(t, err)
var after AddCommentOperation
err = json.Unmarshal(data, &after)
- assert.NoError(t, err)
+ require.NoError(t, err)
// enforce creating the ID
before.Id()
- // Replace the identity stub with the real thing
- assert.Equal(t, rene.Id(), after.Author().Id())
+ // Replace the identity as it's not serialized
after.Author_ = rene
- assert.Equal(t, before, &after)
+ require.Equal(t, before, &after)
}
diff --git a/bug/op_create_test.go b/bug/op_create_test.go
index 25b87cfe..7696d065 100644
--- a/bug/op_create_test.go
+++ b/bug/op_create_test.go
@@ -76,8 +76,7 @@ func TestCreateSerialize(t *testing.T) {
// enforce creating the ID
before.Id()
- // Replace the identity stub with the real thing
- require.Equal(t, rene.Id(), after.Author().Id())
+ // Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
diff --git a/bug/op_edit_comment_test.go b/bug/op_edit_comment_test.go
index 5ba94706..62034a0b 100644
--- a/bug/op_edit_comment_test.go
+++ b/bug/op_edit_comment_test.go
@@ -93,8 +93,7 @@ func TestEditCommentSerialize(t *testing.T) {
// enforce creating the ID
before.Id()
- // Replace the identity stub with the real thing
- require.Equal(t, rene.Id(), after.Author().Id())
+ // Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
diff --git a/bug/op_label_change_test.go b/bug/op_label_change_test.go
index 40dc4f0d..1892724e 100644
--- a/bug/op_label_change_test.go
+++ b/bug/op_label_change_test.go
@@ -30,8 +30,7 @@ func TestLabelChangeSerialize(t *testing.T) {
// enforce creating the ID
before.Id()
- // Replace the identity stub with the real thing
- require.Equal(t, rene.Id(), after.Author().Id())
+ // Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
diff --git a/bug/op_noop_test.go b/bug/op_noop_test.go
index 0e3727c2..2bbfa219 100644
--- a/bug/op_noop_test.go
+++ b/bug/op_noop_test.go
@@ -32,8 +32,7 @@ func TestNoopSerialize(t *testing.T) {
// enforce creating the ID
before.Id()
- // Replace the identity stub with the real thing
- assert.Equal(t, rene.Id(), after.Author().Id())
+ // Replace the identity as it's not serialized
after.Author_ = rene
assert.Equal(t, before, &after)
diff --git a/bug/op_set_metadata_test.go b/bug/op_set_metadata_test.go
index 78f7d883..62c1c942 100644
--- a/bug/op_set_metadata_test.go
+++ b/bug/op_set_metadata_test.go
@@ -119,8 +119,7 @@ func TestSetMetadataSerialize(t *testing.T) {
// enforce creating the ID
before.Id()
- // Replace the identity stub with the real thing
- require.Equal(t, rene.Id(), after.Author().Id())
+ // Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
diff --git a/bug/op_set_status_test.go b/bug/op_set_status_test.go
index 83ff22ae..75cadae2 100644
--- a/bug/op_set_status_test.go
+++ b/bug/op_set_status_test.go
@@ -30,8 +30,7 @@ func TestSetStatusSerialize(t *testing.T) {
// enforce creating the ID
before.Id()
- // Replace the identity stub with the real thing
- require.Equal(t, rene.Id(), after.Author().Id())
+ // Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
diff --git a/bug/op_set_title_test.go b/bug/op_set_title_test.go
index 7059c4c7..2a227709 100644
--- a/bug/op_set_title_test.go
+++ b/bug/op_set_title_test.go
@@ -30,8 +30,7 @@ func TestSetTitleSerialize(t *testing.T) {
// enforce creating the ID
before.Id()
- // Replace the identity stub with the real thing
- require.Equal(t, rene.Id(), after.Author().Id())
+ // Replace the identity as it's not serialized
after.Author_ = rene
require.Equal(t, before, &after)
diff --git a/bug/operation.go b/bug/operation.go
index 2e86921a..b5c6b1de 100644
--- a/bug/operation.go
+++ b/bug/operation.go
@@ -135,7 +135,7 @@ func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resol
// OpBase implement the common code for all operations
type OpBase struct {
OperationType OperationType `json:"type"`
- Author_ identity.Interface `json:"author"`
+ Author_ identity.Interface `json:"-"` // not serialized
// TODO: part of the data model upgrade, this should eventually be a timestamp + lamport
UnixTime int64 `json:"timestamp"`
Metadata map[string]string `json:"metadata,omitempty"`
@@ -178,7 +178,6 @@ func (base *OpBase) UnmarshalJSON(data []byte) error {
aux := struct {
OperationType OperationType `json:"type"`
- Author json.RawMessage `json:"author"`
UnixTime int64 `json:"timestamp"`
Metadata map[string]string `json:"metadata,omitempty"`
Nonce []byte `json:"nonce"`
@@ -188,14 +187,7 @@ func (base *OpBase) UnmarshalJSON(data []byte) error {
return err
}
- // delegate the decoding of the identity
- author, err := identity.UnmarshalJSON(aux.Author)
- if err != nil {
- return err
- }
-
base.OperationType = aux.OperationType
- base.Author_ = author
base.UnixTime = aux.UnixTime
base.Metadata = aux.Metadata
base.Nonce = aux.Nonce
diff --git a/commands/bridge_auth_addtoken.go b/commands/bridge_auth_addtoken.go
index c0458fda..dfdc66b6 100644
--- a/commands/bridge_auth_addtoken.go
+++ b/commands/bridge_auth_addtoken.go
@@ -41,10 +41,12 @@ func newBridgeAuthAddTokenCommand() *cobra.Command {
flags.StringVarP(&options.target, "target", "t", "",
fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ",")))
+ cmd.RegisterFlagCompletionFunc("target", completeFrom(bridge.Targets()))
flags.StringVarP(&options.login,
"login", "l", "", "The login in the remote bug-tracker")
flags.StringVarP(&options.user,
"user", "u", "", "The user to add the token to. Default is the current user")
+ cmd.RegisterFlagCompletionFunc("user", completeUser(env))
return cmd
}
diff --git a/commands/bridge_auth_rm.go b/commands/bridge_auth_rm.go
index fa73ad11..a28057de 100644
--- a/commands/bridge_auth_rm.go
+++ b/commands/bridge_auth_rm.go
@@ -16,7 +16,8 @@ func newBridgeAuthRm() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
return runBridgeAuthRm(env, args)
},
- Args: cobra.ExactArgs(1),
+ Args: cobra.ExactArgs(1),
+ ValidArgsFunction: completeBridgeAuth(env),
}
return cmd
diff --git a/commands/bridge_auth_show.go b/commands/bridge_auth_show.go
index f174cdb7..7233bb51 100644
--- a/commands/bridge_auth_show.go
+++ b/commands/bridge_auth_show.go
@@ -21,7 +21,8 @@ func newBridgeAuthShow() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runBridgeAuthShow(env, args)
}),
- Args: cobra.ExactArgs(1),
+ Args: cobra.ExactArgs(1),
+ ValidArgsFunction: completeBridgeAuth(env),
}
return cmd
diff --git a/commands/bridge_configure.go b/commands/bridge_configure.go
index 6533e497..bbfc13be 100644
--- a/commands/bridge_configure.go
+++ b/commands/bridge_configure.go
@@ -97,6 +97,7 @@ git bug bridge configure \
flags.StringVarP(&options.name, "name", "n", "", "A distinctive name to identify the bridge")
flags.StringVarP(&options.target, "target", "t", "",
fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ",")))
+ cmd.RegisterFlagCompletionFunc("target", completeFrom(bridge.Targets()))
flags.StringVarP(&options.params.URL, "url", "u", "", "The URL of the remote repository")
flags.StringVarP(&options.params.BaseURL, "base-url", "b", "", "The base URL of your remote issue tracker")
flags.StringVarP(&options.params.Login, "login", "l", "", "The login on your remote issue tracker")
diff --git a/commands/bridge_pull.go b/commands/bridge_pull.go
index 3155ebf4..9370e088 100644
--- a/commands/bridge_pull.go
+++ b/commands/bridge_pull.go
@@ -32,7 +32,8 @@ func newBridgePullCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runBridgePull(env, options, args)
}),
- Args: cobra.MaximumNArgs(1),
+ Args: cobra.MaximumNArgs(1),
+ ValidArgsFunction: completeBridge(env),
}
flags := cmd.Flags()
diff --git a/commands/bridge_push.go b/commands/bridge_push.go
index a232f0f0..ef1f2d3e 100644
--- a/commands/bridge_push.go
+++ b/commands/bridge_push.go
@@ -23,7 +23,8 @@ func newBridgePushCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runBridgePush(env, args)
}),
- Args: cobra.MaximumNArgs(1),
+ Args: cobra.MaximumNArgs(1),
+ ValidArgsFunction: completeBridge(env),
}
return cmd
diff --git a/commands/bridge_rm.go b/commands/bridge_rm.go
index 121a35ad..0306944e 100644
--- a/commands/bridge_rm.go
+++ b/commands/bridge_rm.go
@@ -16,7 +16,8 @@ func newBridgeRm() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runBridgeRm(env, args)
}),
- Args: cobra.ExactArgs(1),
+ Args: cobra.ExactArgs(1),
+ ValidArgsFunction: completeBridge(env),
}
return cmd
diff --git a/commands/comment.go b/commands/comment.go
index 90657e4a..b4b4628b 100644
--- a/commands/comment.go
+++ b/commands/comment.go
@@ -18,6 +18,7 @@ func newCommentCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runComment(env, args)
}),
+ ValidArgsFunction: completeBug(env),
}
cmd.AddCommand(newCommentAddCommand())
diff --git a/commands/comment_add.go b/commands/comment_add.go
index 11ba5ad3..f308428c 100644
--- a/commands/comment_add.go
+++ b/commands/comment_add.go
@@ -25,6 +25,7 @@ func newCommentAddCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runCommentAdd(env, options, args)
}),
+ ValidArgsFunction: completeBug(env),
}
flags := cmd.Flags()
diff --git a/commands/helper_completion.go b/commands/helper_completion.go
new file mode 100644
index 00000000..3a089e35
--- /dev/null
+++ b/commands/helper_completion.go
@@ -0,0 +1,342 @@
+package commands
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ "github.com/MichaelMure/git-bug/bridge"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
+ "github.com/MichaelMure/git-bug/bug"
+ "github.com/MichaelMure/git-bug/cache"
+ _select "github.com/MichaelMure/git-bug/commands/select"
+)
+
+type validArgsFunction func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective)
+
+func completionHandlerError(err error) (completions []string, directives cobra.ShellCompDirective) {
+ return nil, cobra.ShellCompDirectiveError
+}
+
+func completeBridge(env *Env) validArgsFunction {
+ return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+ if err := loadBackend(env)(cmd, args); err != nil {
+ return completionHandlerError(err)
+ }
+ defer func() {
+ _ = env.backend.Close()
+ }()
+
+ bridges, err := bridge.ConfiguredBridges(env.backend)
+ if err != nil {
+ return completionHandlerError(err)
+ }
+
+ completions = make([]string, len(bridges))
+ for i, bridge := range bridges {
+ completions[i] = bridge + "\t" + "Bridge"
+ }
+
+ return completions, cobra.ShellCompDirectiveNoFileComp
+ }
+}
+
+func completeBridgeAuth(env *Env) validArgsFunction {
+ return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+ if err := loadBackend(env)(cmd, args); err != nil {
+ return completionHandlerError(err)
+ }
+ defer func() {
+ _ = env.backend.Close()
+ }()
+
+ creds, err := auth.List(env.backend)
+ if err != nil {
+ return completionHandlerError(err)
+ }
+
+ completions = make([]string, len(creds))
+ for i, cred := range creds {
+ meta := make([]string, 0, len(cred.Metadata()))
+ for k, v := range cred.Metadata() {
+ meta = append(meta, k+":"+v)
+ }
+ sort.Strings(meta)
+ metaFmt := strings.Join(meta, ",")
+
+ completions[i] = cred.ID().Human() + "\t" + cred.Target() + " " + string(cred.Kind()) + " " + metaFmt
+ }
+
+ return completions, cobra.ShellCompDirectiveNoFileComp
+ }
+}
+
+func completeBug(env *Env) validArgsFunction {
+ return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+ if err := loadBackend(env)(cmd, args); err != nil {
+ return completionHandlerError(err)
+ }
+ defer func() {
+ _ = env.backend.Close()
+ }()
+
+ return completeBugWithBackend(env.backend, toComplete)
+ }
+}
+
+func completeBugWithBackend(backend *cache.RepoCache, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+ allIds := backend.AllBugsIds()
+ bugExcerpt := make([]*cache.BugExcerpt, len(allIds))
+ for i, id := range allIds {
+ var err error
+ bugExcerpt[i], err = backend.ResolveBugExcerpt(id)
+ if err != nil {
+ return completionHandlerError(err)
+ }
+ }
+
+ for i, id := range allIds {
+ if strings.Contains(id.String(), strings.TrimSpace(toComplete)) {
+ completions = append(completions, id.Human()+"\t"+bugExcerpt[i].Title)
+ }
+ }
+
+ return completions, cobra.ShellCompDirectiveNoFileComp
+}
+
+func completeBugAndLabels(env *Env, addOrRemove bool) validArgsFunction {
+ return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+ if err := loadBackend(env)(cmd, args); err != nil {
+ return completionHandlerError(err)
+ }
+ defer func() {
+ _ = env.backend.Close()
+ }()
+
+ b, args, err := _select.ResolveBug(env.backend, args)
+ if err == _select.ErrNoValidId {
+ // we need a bug first to complete labels
+ return completeBugWithBackend(env.backend, toComplete)
+ }
+ if err != nil {
+ return completionHandlerError(err)
+ }
+
+ snap := b.Snapshot()
+
+ seenLabels := map[bug.Label]bool{}
+ for _, label := range args {
+ seenLabels[bug.Label(label)] = addOrRemove
+ }
+
+ var labels []bug.Label
+ if addOrRemove {
+ for _, label := range snap.Labels {
+ seenLabels[label] = true
+ }
+
+ allLabels := env.backend.ValidLabels()
+ labels = make([]bug.Label, 0, len(allLabels))
+ for _, label := range allLabels {
+ if !seenLabels[label] {
+ labels = append(labels, label)
+ }
+ }
+ } else {
+ labels = make([]bug.Label, 0, len(snap.Labels))
+ for _, label := range snap.Labels {
+ if seenLabels[label] {
+ labels = append(labels, label)
+ }
+ }
+ }
+
+ completions = make([]string, len(labels))
+ for i, label := range labels {
+ completions[i] = string(label) + "\t" + "Label"
+ }
+
+ return completions, cobra.ShellCompDirectiveNoFileComp
+ }
+}
+
+func completeFrom(choices []string) validArgsFunction {
+ return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return choices, cobra.ShellCompDirectiveNoFileComp
+ }
+}
+
+func completeGitRemote(env *Env) validArgsFunction {
+ return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+ if err := loadBackend(env)(cmd, args); err != nil {
+ return completionHandlerError(err)
+ }
+ defer func() {
+ _ = env.backend.Close()
+ }()
+
+ remoteMap, err := env.backend.GetRemotes()
+ if err != nil {
+ return completionHandlerError(err)
+ }
+ completions = make([]string, 0, len(remoteMap))
+ for remote, url := range remoteMap {
+ completions = append(completions, remote+"\t"+"Remote: "+url)
+ }
+ sort.Strings(completions)
+ return completions, cobra.ShellCompDirectiveNoFileComp
+ }
+}
+
+func completeLabel(env *Env) validArgsFunction {
+ return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+ if err := loadBackend(env)(cmd, args); err != nil {
+ return completionHandlerError(err)
+ }
+ defer func() {
+ _ = env.backend.Close()
+ }()
+
+ labels := env.backend.ValidLabels()
+ completions = make([]string, len(labels))
+ for i, label := range labels {
+ if strings.Contains(label.String(), " ") {
+ completions[i] = fmt.Sprintf("\"%s\"\tLabel", label.String())
+ } else {
+ completions[i] = fmt.Sprintf("%s\tLabel", label.String())
+ }
+ }
+ return completions, cobra.ShellCompDirectiveNoFileComp
+ }
+}
+
+func completeLs(env *Env) validArgsFunction {
+ return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+ if strings.HasPrefix(toComplete, "status:") {
+ completions = append(completions, "status:open\tOpen bugs")
+ completions = append(completions, "status:closed\tClosed bugs")
+ return completions, cobra.ShellCompDirectiveDefault
+ }
+
+ byPerson := []string{"author:", "participant:", "actor:"}
+ byLabel := []string{"label:", "no:"}
+ needBackend := false
+ for _, key := range append(byPerson, byLabel...) {
+ if strings.HasPrefix(toComplete, key) {
+ needBackend = true
+ }
+ }
+
+ if needBackend {
+ if err := loadBackend(env)(cmd, args); err != nil {
+ return completionHandlerError(err)
+ }
+ defer func() {
+ _ = env.backend.Close()
+ }()
+ }
+
+ for _, key := range byPerson {
+ if !strings.HasPrefix(toComplete, key) {
+ continue
+ }
+ ids := env.backend.AllIdentityIds()
+ completions = make([]string, len(ids))
+ for i, id := range ids {
+ user, err := env.backend.ResolveIdentityExcerpt(id)
+ if err != nil {
+ return completionHandlerError(err)
+ }
+ var handle string
+ if user.Login != "" {
+ handle = user.Login
+ } else {
+ // "author:John Doe" does not work yet, so use the first name.
+ handle = strings.Split(user.Name, " ")[0]
+ }
+ completions[i] = key + handle + "\t" + user.DisplayName()
+ }
+ return completions, cobra.ShellCompDirectiveNoFileComp
+ }
+
+ for _, key := range byLabel {
+ if !strings.HasPrefix(toComplete, key) {
+ continue
+ }
+ labels := env.backend.ValidLabels()
+ completions = make([]string, len(labels))
+ for i, label := range labels {
+ if strings.Contains(label.String(), " ") {
+ completions[i] = key + "\"" + string(label) + "\""
+ } else {
+ completions[i] = key + string(label)
+ }
+ }
+ return completions, cobra.ShellCompDirectiveNoFileComp
+ }
+
+ completions = []string{
+ "actor:\tFilter by actor",
+ "author:\tFilter by author",
+ "label:\tFilter by label",
+ "no:\tExclude bugs by label",
+ "participant:\tFilter by participant",
+ "status:\tFilter by open/close status",
+ "title:\tFilter by title",
+ }
+ return completions, cobra.ShellCompDirectiveNoSpace
+ }
+}
+
+func completeUser(env *Env) validArgsFunction {
+ return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+ if err := loadBackend(env)(cmd, args); err != nil {
+ return completionHandlerError(err)
+ }
+ defer func() {
+ _ = env.backend.Close()
+ }()
+
+ ids := env.backend.AllIdentityIds()
+ completions = make([]string, len(ids))
+ for i, id := range ids {
+ user, err := env.backend.ResolveIdentityExcerpt(id)
+ if err != nil {
+ return completionHandlerError(err)
+ }
+ completions[i] = user.Id.Human() + "\t" + user.DisplayName()
+ }
+ return completions, cobra.ShellCompDirectiveNoFileComp
+ }
+}
+
+func completeUserForQuery(env *Env) validArgsFunction {
+ return func(cmd *cobra.Command, args []string, toComplete string) (completions []string, directives cobra.ShellCompDirective) {
+ if err := loadBackend(env)(cmd, args); err != nil {
+ return completionHandlerError(err)
+ }
+ defer func() {
+ _ = env.backend.Close()
+ }()
+
+ ids := env.backend.AllIdentityIds()
+ completions = make([]string, len(ids))
+ for i, id := range ids {
+ user, err := env.backend.ResolveIdentityExcerpt(id)
+ if err != nil {
+ return completionHandlerError(err)
+ }
+ var handle string
+ if user.Login != "" {
+ handle = user.Login
+ } else {
+ // "author:John Doe" does not work yet, so use the first name.
+ handle = strings.Split(user.Name, " ")[0]
+ }
+ completions[i] = handle + "\t" + user.DisplayName()
+ }
+ return completions, cobra.ShellCompDirectiveNoFileComp
+ }
+}
diff --git a/commands/label.go b/commands/label.go
index d108b089..906974a5 100644
--- a/commands/label.go
+++ b/commands/label.go
@@ -16,6 +16,7 @@ func newLabelCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runLabel(env, args)
}),
+ ValidArgsFunction: completeBug(env),
}
cmd.AddCommand(newLabelAddCommand())
diff --git a/commands/label_add.go b/commands/label_add.go
index c60ecfeb..65439a4a 100644
--- a/commands/label_add.go
+++ b/commands/label_add.go
@@ -17,6 +17,7 @@ func newLabelAddCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runLabelAdd(env, args)
}),
+ ValidArgsFunction: completeBugAndLabels(env, true),
}
return cmd
diff --git a/commands/label_rm.go b/commands/label_rm.go
index 1cdcd248..3f4e1958 100644
--- a/commands/label_rm.go
+++ b/commands/label_rm.go
@@ -17,6 +17,7 @@ func newLabelRmCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runLabelRm(env, args)
}),
+ ValidArgsFunction: completeBugAndLabels(env, false),
}
return cmd
diff --git a/commands/ls.go b/commands/ls.go
index db4145d0..da5ea8ce 100644
--- a/commands/ls.go
+++ b/commands/ls.go
@@ -56,6 +56,7 @@ git bug ls status:open --by creation "foo bar" baz
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runLs(env, options, args)
}),
+ ValidArgsFunction: completeLs(env),
}
flags := cmd.Flags()
@@ -63,26 +64,36 @@ git bug ls status:open --by creation "foo bar" baz
flags.StringSliceVarP(&options.statusQuery, "status", "s", nil,
"Filter by status. Valid values are [open,closed]")
+ cmd.RegisterFlagCompletionFunc("status", completeFrom([]string{"open", "closed"}))
flags.StringSliceVarP(&options.authorQuery, "author", "a", nil,
"Filter by author")
flags.StringSliceVarP(&options.metadataQuery, "metadata", "m", nil,
"Filter by metadata. Example: github-url=URL")
+ cmd.RegisterFlagCompletionFunc("author", completeUserForQuery(env))
flags.StringSliceVarP(&options.participantQuery, "participant", "p", nil,
"Filter by participant")
+ cmd.RegisterFlagCompletionFunc("participant", completeUserForQuery(env))
flags.StringSliceVarP(&options.actorQuery, "actor", "A", nil,
"Filter by actor")
+ cmd.RegisterFlagCompletionFunc("actor", completeUserForQuery(env))
flags.StringSliceVarP(&options.labelQuery, "label", "l", nil,
"Filter by label")
+ cmd.RegisterFlagCompletionFunc("label", completeLabel(env))
flags.StringSliceVarP(&options.titleQuery, "title", "t", nil,
"Filter by title")
flags.StringSliceVarP(&options.noQuery, "no", "n", nil,
"Filter by absence of something. Valid values are [label]")
+ cmd.RegisterFlagCompletionFunc("no", completeLabel(env))
flags.StringVarP(&options.sortBy, "by", "b", "creation",
"Sort the results by a characteristic. Valid values are [id,creation,edit]")
+ cmd.RegisterFlagCompletionFunc("by", completeFrom([]string{"id", "creation", "edit"}))
flags.StringVarP(&options.sortDirection, "direction", "d", "asc",
"Select the sorting direction. Valid values are [asc,desc]")
+ cmd.RegisterFlagCompletionFunc("direction", completeFrom([]string{"asc", "desc"}))
flags.StringVarP(&options.outputFormat, "format", "f", "default",
"Select the output formatting style. Valid values are [default,plain,json,org-mode]")
+ cmd.RegisterFlagCompletionFunc("format",
+ completeFrom([]string{"default", "plain", "json", "org-mode"}))
return cmd
}
@@ -92,13 +103,9 @@ func runLs(env *Env, opts lsOptions, args []string) error {
var err error
if len(args) >= 1 {
- // either the shell or cobra remove the quotes, we need them back for the parsing
- for i, arg := range args {
- if strings.Contains(arg, " ") {
- args[i] = fmt.Sprintf("\"%s\"", arg)
- }
- }
- assembled := strings.Join(args, " ")
+ // either the shell or cobra remove the quotes, we need them back for the query parsing
+ assembled := repairQuery(args)
+
q, err = query.Parse(assembled)
if err != nil {
return err
@@ -142,6 +149,19 @@ func runLs(env *Env, opts lsOptions, args []string) error {
}
}
+func repairQuery(args []string) string {
+ for i, arg := range args {
+ split := strings.Split(arg, ":")
+ for j, s := range split {
+ if strings.Contains(s, " ") {
+ split[j] = fmt.Sprintf("\"%s\"", s)
+ }
+ }
+ args[i] = strings.Join(split, ":")
+ }
+ return strings.Join(args, " ")
+}
+
type JSONBugExcerpt struct {
Id string `json:"id"`
HumanId string `json:"human_id"`
diff --git a/commands/ls_test.go b/commands/ls_test.go
new file mode 100644
index 00000000..aff94e03
--- /dev/null
+++ b/commands/ls_test.go
@@ -0,0 +1,43 @@
+package commands
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func Test_repairQuery(t *testing.T) {
+ cases := []struct {
+ args []string
+ output string
+ }{
+ {
+ []string{""},
+ "",
+ },
+ {
+ []string{"foo"},
+ "foo",
+ },
+ {
+ []string{"foo", "bar"},
+ "foo bar",
+ },
+ {
+ []string{"foo bar", "baz"},
+ "\"foo bar\" baz",
+ },
+ {
+ []string{"foo:bar", "baz"},
+ "foo:bar baz",
+ },
+ {
+ []string{"foo:bar boo", "baz"},
+ "foo:\"bar boo\" baz",
+ },
+ }
+
+ for _, tc := range cases {
+ require.Equal(t, tc.output, repairQuery(tc.args))
+ }
+}
diff --git a/commands/pull.go b/commands/pull.go
index f3a31414..29c9f034 100644
--- a/commands/pull.go
+++ b/commands/pull.go
@@ -18,6 +18,7 @@ func newPullCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runPull(env, args)
}),
+ ValidArgsFunction: completeGitRemote(env),
}
return cmd
diff --git a/commands/push.go b/commands/push.go
index 9d6ca7df..adba6bef 100644
--- a/commands/push.go
+++ b/commands/push.go
@@ -16,6 +16,7 @@ func newPushCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runPush(env, args)
}),
+ ValidArgsFunction: completeGitRemote(env),
}
return cmd
diff --git a/commands/rm.go b/commands/rm.go
index 8205c128..2e1d924d 100644
--- a/commands/rm.go
+++ b/commands/rm.go
@@ -17,6 +17,7 @@ func newRmCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runRm(env, args)
}),
+ ValidArgsFunction: completeBug(env),
}
flags := cmd.Flags()
diff --git a/commands/root.go b/commands/root.go
index e7848363..e012bd83 100644
--- a/commands/root.go
+++ b/commands/root.go
@@ -50,14 +50,6 @@ the same git remote you are already using to collaborate with other people.
SilenceUsage: true,
DisableAutoGenTag: true,
-
- // Custom bash code to connect the git completion for "git bug" to the
- // git-bug completion for "git-bug"
- BashCompletionFunction: `
-_git_bug() {
- __start_git-bug "$@"
-}
-`,
}
cmd.AddCommand(newAddCommand())
diff --git a/commands/select.go b/commands/select.go
index 34d00a32..f9e6ece7 100644
--- a/commands/select.go
+++ b/commands/select.go
@@ -31,6 +31,7 @@ The complementary command is "git bug deselect" performing the opposite operatio
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runSelect(env, args)
}),
+ ValidArgsFunction: completeBug(env),
}
return cmd
diff --git a/commands/show.go b/commands/show.go
index 55140357..16747214 100644
--- a/commands/show.go
+++ b/commands/show.go
@@ -29,13 +29,17 @@ func newShowCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runShow(env, options, args)
}),
+ ValidArgsFunction: completeBug(env),
}
flags := cmd.Flags()
flags.SortFlags = false
+ fields := []string{"author", "authorEmail", "createTime", "lastEdit", "humanId",
+ "id", "labels", "shortId", "status", "title", "actors", "participants"}
flags.StringVarP(&options.fields, "field", "", "",
- "Select field to display. Valid values are [author,authorEmail,createTime,lastEdit,humanId,id,labels,shortId,status,title,actors,participants]")
+ "Select field to display. Valid values are ["+strings.Join(fields, ",")+"]")
+ cmd.RegisterFlagCompletionFunc("by", completeFrom(fields))
flags.StringVarP(&options.format, "format", "f", "default",
"Select the output formatting style. Valid values are [default,json,org-mode]")
diff --git a/commands/status.go b/commands/status.go
index c1e45c5f..c3e860b6 100644
--- a/commands/status.go
+++ b/commands/status.go
@@ -15,6 +15,7 @@ func newStatusCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runStatus(env, args)
}),
+ ValidArgsFunction: completeBug(env),
}
cmd.AddCommand(newStatusCloseCommand())
diff --git a/commands/title.go b/commands/title.go
index c4293530..f99c6eff 100644
--- a/commands/title.go
+++ b/commands/title.go
@@ -15,6 +15,7 @@ func newTitleCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runTitle(env, args)
}),
+ ValidArgsFunction: completeBug(env),
}
cmd.AddCommand(newTitleEditCommand())
diff --git a/commands/title_edit.go b/commands/title_edit.go
index 810c5e62..a9e7fe4b 100644
--- a/commands/title_edit.go
+++ b/commands/title_edit.go
@@ -24,6 +24,7 @@ func newTitleEditCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runTitleEdit(env, options, args)
}),
+ ValidArgsFunction: completeBug(env),
}
flags := cmd.Flags()
diff --git a/commands/user.go b/commands/user.go
index b6a2e485..0fe3be4d 100644
--- a/commands/user.go
+++ b/commands/user.go
@@ -3,6 +3,7 @@ package commands
import (
"errors"
"fmt"
+ "strings"
"github.com/spf13/cobra"
@@ -24,6 +25,7 @@ func newUserCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runUser(env, options, args)
}),
+ ValidArgsFunction: completeUser(env),
}
cmd.AddCommand(newUserAdoptCommand())
@@ -33,8 +35,10 @@ func newUserCommand() *cobra.Command {
flags := cmd.Flags()
flags.SortFlags = false
+ fields := []string{"email", "humanId", "id", "lastModification", "lastModificationLamports", "login", "metadata", "name"}
flags.StringVarP(&options.fields, "field", "f", "",
- "Select field to display. Valid values are [email,humanId,id,lastModification,lastModificationLamports,login,metadata,name]")
+ "Select field to display. Valid values are ["+strings.Join(fields, ",")+"]")
+ cmd.RegisterFlagCompletionFunc("field", completeFrom(fields))
return cmd
}
diff --git a/commands/user_adopt.go b/commands/user_adopt.go
index 166063ae..afef94ea 100644
--- a/commands/user_adopt.go
+++ b/commands/user_adopt.go
@@ -15,6 +15,7 @@ func newUserAdoptCommand() *cobra.Command {
RunE: closeBackend(env, func(cmd *cobra.Command, args []string) error {
return runUserAdopt(env, args)
}),
+ ValidArgsFunction: completeUser(env),
}
return cmd
diff --git a/commands/user_ls.go b/commands/user_ls.go
index 98800b87..341f0dc1 100644
--- a/commands/user_ls.go
+++ b/commands/user_ls.go
@@ -32,6 +32,7 @@ func newUserLsCommand() *cobra.Command {
flags.StringVarP(&options.format, "format", "f", "default",
"Select the output formatting style. Valid values are [default,json]")
+ cmd.RegisterFlagCompletionFunc("format", completeFrom([]string{"default", "json"}))
return cmd
}
diff --git a/doc/bug-graph-1.png b/doc/bug-graph-1.png
new file mode 100644
index 00000000..5f6f931f
--- /dev/null
+++ b/doc/bug-graph-1.png
Binary files differ
diff --git a/doc/merge1.png b/doc/merge1.png
new file mode 100644
index 00000000..7ba24173
--- /dev/null
+++ b/doc/merge1.png
Binary files differ
diff --git a/doc/merge2.png b/doc/merge2.png
new file mode 100644
index 00000000..614be5e8
--- /dev/null
+++ b/doc/merge2.png
Binary files differ
diff --git a/doc/model.md b/doc/model.md
index c4252e6c..da76761c 100644
--- a/doc/model.md
+++ b/doc/model.md
@@ -1,111 +1,127 @@
-# Data model
+Entities data model
+===================
If you are not familiar with [git internals](https://git-scm.com/book/en/v1/Git-Internals), you might first want to read about them, as the `git-bug` data model is built on top of them.
-The biggest problem when creating a distributed bug tracker is that there is no central authoritative server (doh!). This implies some constraints.
+## Entities (bugs, ...) are a series of edit operations
-## Anybody can create and edit bugs at the same time as you
+As entities are stored and edited in multiple process at the same time, it's not possible to store the current state like it would be done in a normal application. If two process change the same entity and later try to merge the states, we wouldn't know which change takes precedence or how to merge those states.
-To deal with this problem, you need a way to merge these changes in a meaningful way.
+To deal with this problem, you need a way to merge these changes in a meaningful way. Instead of storing the final bug data directly, we store a series of edit `Operation`s. This is a common idea, notably with [Operation-based CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type#Operation-based_CRDTs).
-Instead of storing the final bug data directly, we store a series of edit `Operation`s.
+![ordered operations](operations.png)
-Note: In git-bug internally it is a golang struct, but in the git repo it is stored as JSON, as seen later.
+To get the final state of an entity, we apply these `Operation`s in the correct order on an empty state to compute ("compile") our view.
-These `Operation`s are aggregated in an `OperationPack`, a simple array. An `OperationPack` represents an edit session of a bug. We store this pack in git as a git `Blob`; that consists of a string containing a JSON array of operations. One such pack -- here with two operations -- might look like this:
+## Entities are stored in git objects
+
+An `Operation` is a piece of data including:
+- a type identifier
+- an author (a reference to another entity)
+- a timestamp (there is also 1 or 2 Lamport time that we will describe later)
+- all the data required by that operation type (a message, a status ...)
+- a random nonce to ensure we have enough entropy, as the operation identifier is a hash of that data (more on that later)
+
+These `Operation`s are aggregated in an `OperationPack`, a simple array. An `OperationPack` represents an edit session of a bug. As the operation's author is the same for all the `OperationPack` we only store it once.
+
+We store this pack in git as a git `Blob`; that consists of a string containing a JSON array of operations. One such pack -- here with two operations -- might look like this:
```json
-[
- {
- "type": "SET_TITLE",
- "author": {
- "id": "5034cd36acf1a2dadb52b2db17f620cc050eb65c"
- },
- "timestamp": 1533640589,
- "title": "This title is better"
+{
+ "author": {
+ "id": "04bf6c1a69bb8e9679644874c85f82e337b40d92df9d8d4176f1c5e5c6627058"
},
- {
- "type": "ADD_COMMENT",
- "author": {
- "id": "5034cd36acf1a2dadb52b2db17f620cc050eb65c"
+ "ops": [
+ {
+ "type": 3,
+ "timestamp": 1647377254,
+ "nonce": "SRQwUWTJCXAmQBIS+1ctKgOcbF0=",
+ "message": "Adding a comment",
+ "files": null
},
- "timestamp": 1533640612,
- "message": "A new comment"
- }
-]
+ {
+ "type": 4,
+ "timestamp": 1647377257,
+ "nonce": "la/HaRPMvD77/cJSJOUzKWuJdY8=",
+ "status": 1
+ }
+ ]
+}
```
-To reference our `OperationPack`, we create a git `Tree`; it references our `OperationPack` `Blob` under `"\ops"`. If any edit operation includes a media (for instance in a message), we can store that media as a `Blob` and reference it here under `"/media"`.
+To reference our `OperationPack`, we create a git `Tree`; it references our `OperationPack` `Blob` under `"/ops"`. If any edit operation includes a media (for instance in a message), we can store that media as a `Blob` and reference it here under `"/media"`.
To complete the picture, we create a git `Commit` that references our `Tree`. Each time we add more `Operation`s to our bug, we add a new `Commit` with the same data-structure to form a chain of `Commit`s.
This chain of `Commit`s is made available as a git `Reference` under `refs/bugs/<bug-id>`. We can later use this reference to push our data to a git remote. As git will push any data needed as well, everything will be pushed to the remote, including the media.
-For convenience and performance, each `Tree` references the very first `OperationPack` of the bug under `"/root"`. That way we can easily access the very first `Operation`, the `CREATE` operation. This operation contains important data for the bug, like the author.
-
Here is the complete picture:
-```
- refs/bugs/<bug-id>
- |
- |
- |
- +-----------+ +-----------+ "ops" +-----------+
- | Commit |----------> Tree |---------+------------| Blob | (OperationPack)
- +-----------+ +-----------+ | +-----------+
- | |
- | |
- | | "root" +-----------+
- +-----------+ +-----------+ +------------| Blob | (OperationPack)
- | Commit |----------> Tree |-- ... | +-----------+
- +-----------+ +-----------+ |
- | |
- | | "media" +-----------+ +-----------+
- | +------------| Tree |---+--->| Blob | bug.jpg
- +-----------+ +-----------+ +-----------+ | +-----------+
- | Commit |----------> Tree |-- ... |
- +-----------+ +-----------+ | +-----------+
- +--->| Blob | demo.mp4
- +-----------+
-```
-
-Now that we have this, we can easily merge our bugs without conflict. When pulling bug updates from a remote, we will simply add our new operations (that is, new `Commit`s), if any, at the end of the chain. In git terms, it's just a `rebase`.
+![git graph of a simple bug](bug-graph-1.png)
-## You can't have a simple consecutive index for your bugs
+## Time is unreliable
-The same way git can't have a simple counter as identifier for its commits as SVN does, we can't have consecutive identifiers for bugs.
+It would be very tempting to use the `Operation`'s timestamp to give us the order to compile the final state. However, you can't rely on the time provided by other people (their clock might be off) for anything other than just display. This is a fundamental limitation of distributed system, and even more so when actors might want to game the system.
-`git-bug` uses as identifier the hash of the first commit in the chain of commits of the bug. As this hash is ultimately computed with the content of the `CREATE` operation that includes title, message and a timestamp, it will be unique and prevent collision.
+Instead, we are going to use [Lamport logical clock](https://en.wikipedia.org/wiki/Lamport_timestamps). A Lamport clock is a simple counter of events. This logical clock gives us a partial ordering:
+- if L1 < L2, L1 happened before L2
+- if L1 > L2, L1 happened after L2
+- if L1 == L2, we can't tell which happened first: it's a concurrent edition
-The same way as git does, this hash is displayed truncated to a 7 characters string to a human user. Note that when specifying a bug id in a command, you can enter as few character as you want, as long as there is no ambiguity. If multiple bugs match your prefix, `git-bug` will complain and display the potential matches.
-## You can't rely on the time provided by other people (their clock might by off) for anything other than just display
+Each time we are appending something to the data (create an Entity, add an `Operation`) a logical time will be attached, with the highest time value we are aware of plus one. This declares a causality in the event and allows ordering entities and operations.
-When in the context of a single bug, events are already ordered without the need of a timestamp. An `OperationPack` is an ordered array of operations. A chain of commits orders `OperationPack`s amongst each other.
+The first commit of an Entity will have both a creation time and edit time clock, while a later commit will only have an edit time clock. These clocks value are serialized directly in the `Tree` entry name (for example: `"create-clock-4"`). As a Tree entry needs to reference something, we reference the git `Blob` with an empty content. As all of these entries will reference the same `Blob`, no network transfer is needed as long as you already have any entity in your repository.
-Now, to be able to order bugs by creation or last edition time, `git-bug` uses a [Lamport logical clock](https://en.wikipedia.org/wiki/Lamport_timestamps). A Lamport clock is a simple counter of events. When a new bug is created, its creation time will be the highest time value we are aware of plus one. This declares a causality in the event and allows to order bugs.
-
-When bugs are pushed/pulled to a git remote, it might happen that bugs get the same logical time. This means that they were created or edited concurrently. In this case, `git-bug` will use the timestamp as a second layer of sorting. While the timestamp might be incorrect due to a badly set clock, the drift in sorting is bounded by the first sorting using the logical clock. That means that if users synchronize their bugs regularly, the timestamp will rarely be used, and should still provide a kinda accurate sorting when needed.
-
-These clocks are stored in the chain of commits of each bug, as entries in each main git `Tree`. The first commit will have both a creation time and edit time clock, while a later commit will only have an edit time clock. A naive way could be to serialize the clock in a git `Blob` and reference it in the `Tree` as `"create-clock"` for example. The problem is that it would generate a lot of blobs that would need to be exchanged later for what is basically just a number.
-
-Instead, the clock value is serialized directly in the `Tree` entry name (for example: `"create-clock-4"`). As a Tree entry needs to reference something, we reference the git `Blob` with an empty content. As all of these entries will reference the same `Blob`, no network transfer is needed as long as you already have any bug in your repository.
-
-
-Example of Tree of the first commit of a bug:
+Example of Tree of the first commit of an entity:
```
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 create-clock-14
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 edit-clock-137
100644 blob a020a85baa788e12699a4d83dd735578f0d78c75 ops
-100644 blob a020a85baa788e12699a4d83dd735578f0d78c75 root
```
Note that both `"ops"` and `"root"` entry reference the same OperationPack as it's the first commit in the chain.
-
-Example of Tree of a later commit of a bug:
+Example of Tree of a later commit of an entity:
```
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 edit-clock-154
100644 blob 68383346c1a9503f28eec888efd300e9fc179ca0 ops
-100644 blob a020a85baa788e12699a4d83dd735578f0d78c75 root
```
-Note that the `"root"` entry still references the same root `OperationPack`. Also, all the clocks reference the same empty `Blob`.
+
+## Entities and Operation's ID
+
+`Operation`s can be referenced in the data model or by users with an identifier. This identifier is computed from the `Operation`'s data itself, with a hash of that data: `id = hash(json(op))`
+
+For entities, `git-bug` uses as identifier the hash of the first `Operation` of the entity, as serialized on disk.
+
+The same way as git does, this hash is displayed truncated to a 7 characters string to a human user. Note that when specifying a bug id in a command, you can enter as few characters as you want, as long as there is no ambiguity. If multiple entities match your prefix, `git-bug` will complain and display the potential matches.
+
+## Entities support conflict resolution
+
+Now that we have all that, we can finally merge our entities without conflict and collaborate with other users. Let's start by getting rid of two simple scenario:
+- if we simply pull updates, we move forward our local reference. We get an update of our graph that we read as usual.
+- if we push fast-forward updates, we move forward the remote reference and other users can update their reference as well.
+
+The tricky part happens when we have concurrent edition. If we pull updates while we have local changes (non-straightforward in git term), git-bug create the equivalent of a merge commit to merge both branches into a DAG. This DAG has a single root containing the first operation, but can have branches that get merged back into a single head pointed by the reference.
+
+As we don't have a purely linear series of commits/`Operations`s, we need a deterministic ordering to always apply operations in the same order.
+
+git-bug apply the following algorithm:
+1. load and read all the commits and the associated `OperationPack`s
+2. make sure that the Lamport clocks respect the DAG structure: a parent commit/`OperationPack` (that is, towards the head) cannot have a clock that is higher or equal than its direct child. If such a problem happen, the commit is refused/discarded.
+3. individual `Operation`s are assembled together and ordered given the following priorities:
+ 1. the edition's lamport clock if not concurrent
+ 2. the lexicographic order of the `OperationPack`'s identifier
+
+Step 2 is providing and enforcing a constraint over the `Operation`'s logical clocks. What that means is that we inherit the implicit ordering given by the DAG. Later, logical clocks refine that ordering. This, coupled with signed commit has the nice property of limiting how this data model can be abused.
+
+Here is an example of such an ordering. We can see that:
+- Lamport clocks respect the DAG structure
+- the final `Operation` order is [A,B,C,D,E,F], according to those clocks
+
+![merge scenario 1](merge1.png)
+
+When we have a concurrent edition, we apply a secondary ordering based on the `OperationPack`'s identifier:
+
+![merge scenario 2](merge2.png)
+
+This secondary ordering doesn't carry much meaning, but it's unbiased and hard to abuse. \ No newline at end of file
diff --git a/doc/operations.png b/doc/operations.png
new file mode 100644
index 00000000..79b6c8e7
--- /dev/null
+++ b/doc/operations.png
Binary files differ
diff --git a/entity/dag/entity.go b/entity/dag/entity.go
index 0760cdec..f3229b7e 100644
--- a/entity/dag/entity.go
+++ b/entity/dag/entity.go
@@ -205,7 +205,7 @@ func read(def Definition, repo repository.ClockedRepo, resolver identity.Resolve
if oppSlice[i].EditTime != oppSlice[j].EditTime {
return oppSlice[i].EditTime < oppSlice[j].EditTime
}
- // We have equal EditTime, which means we have concurrent edition over different machines and we
+ // We have equal EditTime, which means we have concurrent edition over different machines, and we
// can't tell which one came first. So, what now? We still need a total ordering and the most stable possible.
// As a secondary ordering, we can order based on a hash of the serialized Operations in the
// operationPack. It doesn't carry much meaning but it's unbiased and hard to abuse.
diff --git a/entity/dag/example_test.go b/entity/dag/example_test.go
new file mode 100644
index 00000000..d034e59d
--- /dev/null
+++ b/entity/dag/example_test.go
@@ -0,0 +1,383 @@
+package dag_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "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"
+)
+
+// This file explains how to define a replicated data structure, stored and using git as a medium for
+// synchronisation. To do this, we'll use the entity/dag package, which will do all the complex handling.
+//
+// The example we'll use here is a small shared configuration with two fields. One of them is special as
+// it also defines who is allowed to change said configuration. Note: this example is voluntarily a bit
+// complex with operation linking to identities and logic rules, to show that how something more complex
+// than a toy would look like. That said, it's still a simplified example: in git-bug for example, more
+// layers are added for caching, memory handling and to provide an easier to use API.
+//
+// Let's start by defining the document/structure we are going to share:
+
+// Snapshot is the compiled view of a ProjectConfig
+type Snapshot struct {
+ // Administrator is the set of users with the higher level of access
+ Administrator map[identity.Interface]struct{}
+ // SignatureRequired indicate that all git commit need to be signed
+ SignatureRequired bool
+}
+
+// HasAdministrator returns true if the given identity is included in the administrator.
+func (snap *Snapshot) HasAdministrator(i identity.Interface) bool {
+ for admin, _ := range snap.Administrator {
+ if admin.Id() == i.Id() {
+ return true
+ }
+ }
+ return false
+}
+
+// Now, we will not edit this configuration directly. Instead, we are going to apply "operations" on it.
+// Those are the ones that will be stored and shared. Doing things that way allow merging concurrent editing
+// and deal with conflict.
+//
+// Here, we will define three operations:
+// - SetSignatureRequired is a simple operation that set or unset the SignatureRequired boolean
+// - AddAdministrator is more complex and add a new administrator in the Administrator set
+// - RemoveAdministrator is the counterpart the remove administrators
+//
+// Note: there is some amount of boilerplate for operations. In a real project, some of that can be
+// factorized and simplified.
+
+// Operation is the operation interface acting on Snapshot
+type Operation interface {
+ dag.Operation
+
+ // Apply the operation to a Snapshot to create the final state
+ Apply(snapshot *Snapshot)
+}
+
+type OperationType int
+
+const (
+ _ OperationType = iota
+ SetSignatureRequiredOp
+ AddAdministratorOp
+ RemoveAdministratorOp
+)
+
+// SetSignatureRequired is an operation to set/unset if git signature are required.
+type SetSignatureRequired struct {
+ author identity.Interface
+ OperationType OperationType `json:"type"`
+ Value bool `json:"value"`
+}
+
+func NewSetSignatureRequired(author identity.Interface, value bool) *SetSignatureRequired {
+ return &SetSignatureRequired{author: author, OperationType: SetSignatureRequiredOp, Value: value}
+}
+
+func (ssr *SetSignatureRequired) Id() entity.Id {
+ // the Id of the operation is the hash of the serialized data.
+ // we could memorize the Id when deserializing, but that will do
+ data, _ := json.Marshal(ssr)
+ return entity.DeriveId(data)
+}
+
+func (ssr *SetSignatureRequired) Validate() error {
+ if ssr.author == nil {
+ return fmt.Errorf("author not set")
+ }
+ return ssr.author.Validate()
+}
+
+func (ssr *SetSignatureRequired) Author() identity.Interface {
+ return ssr.author
+}
+
+// Apply is the function that makes changes on the snapshot
+func (ssr *SetSignatureRequired) Apply(snapshot *Snapshot) {
+ // check that we are allowed to change the config
+ if _, ok := snapshot.Administrator[ssr.author]; !ok {
+ return
+ }
+ snapshot.SignatureRequired = ssr.Value
+}
+
+// AddAdministrator is an operation to add a new administrator in the set
+type AddAdministrator struct {
+ author identity.Interface
+ OperationType OperationType `json:"type"`
+ ToAdd []identity.Interface `json:"to_add"`
+}
+
+// addAdministratorJson is a helper struct to deserialize identities with a concrete type.
+type addAdministratorJson struct {
+ ToAdd []identity.IdentityStub `json:"to_add"`
+}
+
+func NewAddAdministratorOp(author identity.Interface, toAdd ...identity.Interface) *AddAdministrator {
+ return &AddAdministrator{author: author, OperationType: AddAdministratorOp, ToAdd: toAdd}
+}
+
+func (aa *AddAdministrator) Id() entity.Id {
+ // we could memorize the Id when deserializing, but that will do
+ data, _ := json.Marshal(aa)
+ return entity.DeriveId(data)
+}
+
+func (aa *AddAdministrator) Validate() error {
+ // Let's enforce an arbitrary rule
+ if len(aa.ToAdd) == 0 {
+ return fmt.Errorf("nothing to add")
+ }
+ if aa.author == nil {
+ return fmt.Errorf("author not set")
+ }
+ return aa.author.Validate()
+}
+
+func (aa *AddAdministrator) Author() identity.Interface {
+ return aa.author
+}
+
+// Apply is the function that makes changes on the snapshot
+func (aa *AddAdministrator) Apply(snapshot *Snapshot) {
+ // check that we are allowed to change the config ... or if there is no admin yet
+ if !snapshot.HasAdministrator(aa.author) && len(snapshot.Administrator) != 0 {
+ return
+ }
+ for _, toAdd := range aa.ToAdd {
+ snapshot.Administrator[toAdd] = struct{}{}
+ }
+}
+
+// RemoveAdministrator is an operation to remove an administrator from the set
+type RemoveAdministrator struct {
+ author identity.Interface
+ OperationType OperationType `json:"type"`
+ ToRemove []identity.Interface `json:"to_remove"`
+}
+
+// removeAdministratorJson is a helper struct to deserialize identities with a concrete type.
+type removeAdministratorJson struct {
+ ToRemove []identity.Interface `json:"to_remove"`
+}
+
+func NewRemoveAdministratorOp(author identity.Interface, toRemove ...identity.Interface) *RemoveAdministrator {
+ return &RemoveAdministrator{author: author, OperationType: RemoveAdministratorOp, ToRemove: toRemove}
+}
+
+func (ra *RemoveAdministrator) Id() entity.Id {
+ // the Id of the operation is the hash of the serialized data.
+ // we could memorize the Id when deserializing, but that will do
+ data, _ := json.Marshal(ra)
+ return entity.DeriveId(data)
+}
+
+func (ra *RemoveAdministrator) Validate() error {
+ // Let's enforce some rules. If we return an error, this operation will be
+ // considered invalid and will not be included in our data.
+ if len(ra.ToRemove) == 0 {
+ return fmt.Errorf("nothing to remove")
+ }
+ if ra.author == nil {
+ return fmt.Errorf("author not set")
+ }
+ return ra.author.Validate()
+}
+
+func (ra *RemoveAdministrator) Author() identity.Interface {
+ return ra.author
+}
+
+// Apply is the function that makes changes on the snapshot
+func (ra *RemoveAdministrator) Apply(snapshot *Snapshot) {
+ // check if we are allowed to make changes
+ if !snapshot.HasAdministrator(ra.author) {
+ return
+ }
+ // special rule: we can't end up with no administrator
+ stillSome := false
+ for admin, _ := range snapshot.Administrator {
+ if admin != ra.author {
+ stillSome = true
+ break
+ }
+ }
+ if !stillSome {
+ return
+ }
+ // apply
+ for _, toRemove := range ra.ToRemove {
+ delete(snapshot.Administrator, toRemove)
+ }
+}
+
+// Now, let's create the main object (the entity) we are going to manipulate: ProjectConfig.
+// This object wrap a dag.Entity, which makes it inherit some methods and provide all the complex
+// DAG handling. Additionally, ProjectConfig is the place where we can add functions specific for that type.
+
+type ProjectConfig struct {
+ // this is really all we need
+ *dag.Entity
+}
+
+func NewProjectConfig() *ProjectConfig {
+ return &ProjectConfig{Entity: dag.New(def)}
+}
+
+// a Definition describes a few properties of the Entity, a sort of configuration to manipulate the
+// DAG of operations
+var def = dag.Definition{
+ Typename: "project config",
+ Namespace: "conf",
+ OperationUnmarshaler: operationUnmarshaller,
+ FormatVersion: 1,
+}
+
+// operationUnmarshaller is a function doing the de-serialization of the JSON data into our own
+// concrete Operations. If needed, we can use the resolver to connect to other entities.
+func operationUnmarshaller(author identity.Interface, raw json.RawMessage, resolver identity.Resolver) (dag.Operation, error) {
+ var t struct {
+ OperationType OperationType `json:"type"`
+ }
+
+ if err := json.Unmarshal(raw, &t); err != nil {
+ return nil, err
+ }
+
+ var value interface{}
+
+ switch t.OperationType {
+ case AddAdministratorOp:
+ value = &addAdministratorJson{}
+ case RemoveAdministratorOp:
+ value = &removeAdministratorJson{}
+ case SetSignatureRequiredOp:
+ value = &SetSignatureRequired{}
+ default:
+ panic(fmt.Sprintf("unknown operation type %v", t.OperationType))
+ }
+
+ err := json.Unmarshal(raw, &value)
+ if err != nil {
+ return nil, err
+ }
+
+ var op Operation
+
+ switch value := value.(type) {
+ case *SetSignatureRequired:
+ value.author = author
+ op = value
+ case *addAdministratorJson:
+ // We need something less straightforward to deserialize and resolve identities
+ aa := &AddAdministrator{
+ author: author,
+ OperationType: AddAdministratorOp,
+ ToAdd: make([]identity.Interface, len(value.ToAdd)),
+ }
+ for i, stub := range value.ToAdd {
+ iden, err := resolver.ResolveIdentity(stub.Id())
+ if err != nil {
+ return nil, err
+ }
+ aa.ToAdd[i] = iden
+ }
+ op = aa
+ case *removeAdministratorJson:
+ // We need something less straightforward to deserialize and resolve identities
+ ra := &RemoveAdministrator{
+ author: author,
+ OperationType: RemoveAdministratorOp,
+ ToRemove: make([]identity.Interface, len(value.ToRemove)),
+ }
+ for i, stub := range value.ToRemove {
+ iden, err := resolver.ResolveIdentity(stub.Id())
+ if err != nil {
+ return nil, err
+ }
+ ra.ToRemove[i] = iden
+ }
+ op = ra
+ default:
+ panic(fmt.Sprintf("unknown operation type %T", value))
+ }
+
+ return op, nil
+}
+
+// Compile compute a view of the final state. This is what we would use to display the state
+// in a user interface.
+func (pc ProjectConfig) Compile() *Snapshot {
+ // Note: this would benefit from caching, but it's a simple example
+ snap := &Snapshot{
+ // default value
+ Administrator: make(map[identity.Interface]struct{}),
+ SignatureRequired: false,
+ }
+ for _, op := range pc.Operations() {
+ op.(Operation).Apply(snap)
+ }
+ return snap
+}
+
+// Read is a helper to load a ProjectConfig from a Repository
+func Read(repo repository.ClockedRepo, id entity.Id) (*ProjectConfig, error) {
+ e, err := dag.Read(def, repo, identity.NewSimpleResolver(repo), id)
+ if err != nil {
+ return nil, err
+ }
+ return &ProjectConfig{Entity: e}, nil
+}
+
+func Example_entity() {
+ // Note: this example ignore errors for readability
+ // Note: variable names get a little confusing as we are simulating both side in the same function
+
+ // Let's start by defining two git repository and connecting them as remote
+ repoRenePath, _ := os.MkdirTemp("", "")
+ repoIsaacPath, _ := os.MkdirTemp("", "")
+ repoRene, _ := repository.InitGoGitRepo(repoRenePath)
+ repoIsaac, _ := repository.InitGoGitRepo(repoIsaacPath)
+ _ = repoRene.AddRemote("origin", repoIsaacPath)
+ _ = repoIsaac.AddRemote("origin", repoRenePath)
+
+ // Now we need identities and to propagate them
+ rene, _ := identity.NewIdentity(repoRene, "René Descartes", "rene@descartes.fr")
+ isaac, _ := identity.NewIdentity(repoRene, "Isaac Newton", "isaac@newton.uk")
+ _ = rene.Commit(repoRene)
+ _ = isaac.Commit(repoRene)
+ _ = identity.Pull(repoIsaac, "origin")
+
+ // create a new entity
+ confRene := NewProjectConfig()
+
+ // add some operations
+ confRene.Append(NewAddAdministratorOp(rene, rene))
+ confRene.Append(NewAddAdministratorOp(rene, isaac))
+ confRene.Append(NewSetSignatureRequired(rene, true))
+
+ // Rene commits on its own repo
+ _ = confRene.Commit(repoRene)
+
+ // Isaac pull and read the config
+ _ = dag.Pull(def, repoIsaac, identity.NewSimpleResolver(repoIsaac), "origin", isaac)
+ confIsaac, _ := Read(repoIsaac, confRene.Id())
+
+ // Compile gives the current state of the config
+ snapshot := confIsaac.Compile()
+ for admin, _ := range snapshot.Administrator {
+ fmt.Println(admin.DisplayName())
+ }
+
+ // Isaac add more operations
+ confIsaac.Append(NewSetSignatureRequired(isaac, false))
+ reneFromIsaacRepo, _ := identity.ReadLocal(repoIsaac, rene.Id())
+ confIsaac.Append(NewRemoveAdministratorOp(isaac, reneFromIsaacRepo))
+ _ = confIsaac.Commit(repoIsaac)
+}
diff --git a/entity/dag/operation_pack.go b/entity/dag/operation_pack.go
index 8ca2ff56..9b42f9bf 100644
--- a/entity/dag/operation_pack.go
+++ b/entity/dag/operation_pack.go
@@ -359,5 +359,5 @@ func (pk PGPKeyring) DecryptionKeys() []openpgp.Key {
// }
// }
// return result
- return nil
+ panic("not implemented")
}
diff --git a/go.mod b/go.mod
index 597aaafb..0cca0abc 100644
--- a/go.mod
+++ b/go.mod
@@ -28,7 +28,7 @@ require (
github.com/spf13/cobra v1.4.0
github.com/stretchr/testify v1.7.1
github.com/vektah/gqlparser/v2 v2.4.1
- github.com/xanzy/go-gitlab v0.59.0
+ github.com/xanzy/go-gitlab v0.64.0
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
diff --git a/go.sum b/go.sum
index 6a7bfe8d..afb542a8 100644
--- a/go.sum
+++ b/go.sum
@@ -378,8 +378,8 @@ github.com/vektah/gqlparser/v2 v2.4.1 h1:QOyEn8DAPMUMARGMeshKDkDgNmVoEaEGiDB0uWx
github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
-github.com/xanzy/go-gitlab v0.59.0 h1:fAr6rT/YIdfmBavYgI42+Op7yAAex2Y4xOfvbjN9hxQ=
-github.com/xanzy/go-gitlab v0.59.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM=
+github.com/xanzy/go-gitlab v0.64.0 h1:rMgQdW9S1w3qvNAH2LYpFd2xh7KNLk+JWJd7sorNuTc=
+github.com/xanzy/go-gitlab v0.64.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM=
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug
index 2320c1c0..85d2a15b 100644
--- a/misc/bash_completion/git-bug
+++ b/misc/bash_completion/git-bug
@@ -1,4 +1,4 @@
-# bash completion for git-bug -*- shell-script -*-
+# bash completion V2 for git-bug -*- shell-script -*-
__git-bug_debug()
{
@@ -7,64 +7,43 @@ __git-bug_debug()
fi
}
-# Homebrew on Macs have version 1.3 of bash-completion which doesn't include
-# _init_completion. This is a very minimal version of that function.
+# Macs have bash3 for which the bash-completion package doesn't include
+# _init_completion. This is a minimal version of that function.
__git-bug_init_completion()
{
COMPREPLY=()
_get_comp_words_by_ref "$@" cur prev words cword
}
-__git-bug_index_of_word()
-{
- local w word=$1
- shift
- index=0
- for w in "$@"; do
- [[ $w = "$word" ]] && return
- index=$((index+1))
- done
- index=-1
-}
-
-__git-bug_contains_word()
-{
- local w word=$1; shift
- for w in "$@"; do
- [[ $w = "$word" ]] && return
- done
- return 1
-}
-
-__git-bug_handle_go_custom_completion()
-{
- __git-bug_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}"
-
- local shellCompDirectiveError=1
- local shellCompDirectiveNoSpace=2
- local shellCompDirectiveNoFileComp=4
- local shellCompDirectiveFilterFileExt=8
- local shellCompDirectiveFilterDirs=16
-
- local out requestComp lastParam lastChar comp directive args
+# This function calls the git-bug program to obtain the completion
+# results and the directive. It fills the 'out' and 'directive' vars.
+__git-bug_get_completion_results() {
+ local requestComp lastParam lastChar args
# Prepare the command to request completions for the program.
# Calling ${words[0]} instead of directly git-bug allows to handle aliases
args=("${words[@]:1}")
- requestComp="${words[0]} __completeNoDesc ${args[*]}"
+ requestComp="${words[0]} __complete ${args[*]}"
lastParam=${words[$((${#words[@]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1}
- __git-bug_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}"
+ __git-bug_debug "lastParam ${lastParam}, lastChar ${lastChar}"
if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
# If the last parameter is complete (there is a space following it)
# We add an extra empty parameter so we can indicate this to the go method.
- __git-bug_debug "${FUNCNAME[0]}: Adding extra empty parameter"
- requestComp="${requestComp} \"\""
+ __git-bug_debug "Adding extra empty parameter"
+ requestComp="${requestComp} ''"
fi
- __git-bug_debug "${FUNCNAME[0]}: calling ${requestComp}"
+ # When completing a flag with an = (e.g., git-bug -n=<TAB>)
+ # bash focuses on the part after the =, so we need to remove
+ # the flag part from $cur
+ if [[ "${cur}" == -*=* ]]; then
+ cur="${cur#*=}"
+ fi
+
+ __git-bug_debug "Calling ${requestComp}"
# Use eval to handle any environment variables and such
out=$(eval "${requestComp}" 2>/dev/null)
@@ -76,24 +55,36 @@ __git-bug_handle_go_custom_completion()
# There is not directive specified
directive=0
fi
- __git-bug_debug "${FUNCNAME[0]}: the completion directive is: ${directive}"
- __git-bug_debug "${FUNCNAME[0]}: the completions are: ${out[*]}"
+ __git-bug_debug "The completion directive is: ${directive}"
+ __git-bug_debug "The completions are: ${out[*]}"
+}
+
+__git-bug_process_completion_results() {
+ local shellCompDirectiveError=1
+ local shellCompDirectiveNoSpace=2
+ local shellCompDirectiveNoFileComp=4
+ local shellCompDirectiveFilterFileExt=8
+ local shellCompDirectiveFilterDirs=16
if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
# Error code. No completion.
- __git-bug_debug "${FUNCNAME[0]}: received error from custom completion go code"
+ __git-bug_debug "Received error from custom completion go code"
return
else
if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
- __git-bug_debug "${FUNCNAME[0]}: activating no space"
+ __git-bug_debug "Activating no space"
compopt -o nospace
+ else
+ __git-bug_debug "No space directive not supported in this version of bash"
fi
fi
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
- __git-bug_debug "${FUNCNAME[0]}: activating no file completion"
+ __git-bug_debug "Activating no file completion"
compopt +o default
+ else
+ __git-bug_debug "No file completion directive not supported in this version of bash"
fi
fi
fi
@@ -101,6 +92,7 @@ __git-bug_handle_go_custom_completion()
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
# File extension filtering
local fullFilter filter filteringCmd
+
# Do not use quotes around the $out variable or else newline
# characters will be kept.
for filter in ${out[*]}; do
@@ -112,1400 +104,219 @@ __git-bug_handle_go_custom_completion()
$filteringCmd
elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
# File completion for directories only
- local subdir
+
# Use printf to strip any trailing newline
+ local subdir
subdir=$(printf "%s" "${out[0]}")
if [ -n "$subdir" ]; then
__git-bug_debug "Listing directories in $subdir"
- __git-bug_handle_subdirs_in_dir_flag "$subdir"
+ pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
else
__git-bug_debug "Listing directories in ."
_filedir -d
fi
else
- while IFS='' read -r comp; do
- COMPREPLY+=("$comp")
- done < <(compgen -W "${out[*]}" -- "$cur")
+ __git-bug_handle_completion_types
fi
+
+ __git-bug_handle_special_char "$cur" :
+ __git-bug_handle_special_char "$cur" =
}
-__git-bug_handle_reply()
-{
- __git-bug_debug "${FUNCNAME[0]}"
- local comp
- case $cur in
- -*)
- if [[ $(type -t compopt) = "builtin" ]]; then
- compopt -o nospace
- fi
- local allflags
- if [ ${#must_have_one_flag[@]} -ne 0 ]; then
- allflags=("${must_have_one_flag[@]}")
- else
- allflags=("${flags[*]} ${two_word_flags[*]}")
- fi
- while IFS='' read -r comp; do
+__git-bug_handle_completion_types() {
+ __git-bug_debug "__git-bug_handle_completion_types: COMP_TYPE is $COMP_TYPE"
+
+ case $COMP_TYPE in
+ 37|42)
+ # Type: menu-complete/menu-complete-backward and insert-completions
+ # If the user requested inserting one completion at a time, or all
+ # completions at once on the command-line we must remove the descriptions.
+ # https://github.com/spf13/cobra/issues/1508
+ local tab comp
+ tab=$(printf '\t')
+ while IFS='' read -r comp; do
+ # Strip any description
+ comp=${comp%%$tab*}
+ # Only consider the completions that match
+ comp=$(compgen -W "$comp" -- "$cur")
+ if [ -n "$comp" ]; then
COMPREPLY+=("$comp")
- done < <(compgen -W "${allflags[*]}" -- "$cur")
- if [[ $(type -t compopt) = "builtin" ]]; then
- [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace
fi
+ done < <(printf "%s\n" "${out[@]}")
+ ;;
- # complete after --flag=abc
- if [[ $cur == *=* ]]; then
- if [[ $(type -t compopt) = "builtin" ]]; then
- compopt +o nospace
- fi
+ *)
+ # Type: complete (normal completion)
+ __git-bug_handle_standard_completion_case
+ ;;
+ esac
+}
- local index flag
- flag="${cur%=*}"
- __git-bug_index_of_word "${flag}" "${flags_with_completion[@]}"
- COMPREPLY=()
- if [[ ${index} -ge 0 ]]; then
- PREFIX=""
- cur="${cur#*=}"
- ${flags_completion[${index}]}
- if [ -n "${ZSH_VERSION:-}" ]; then
- # zsh completion needs --flag= prefix
- eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )"
- fi
- fi
- fi
+__git-bug_handle_standard_completion_case() {
+ local tab comp
+ tab=$(printf '\t')
- if [[ -z "${flag_parsing_disabled}" ]]; then
- # If flag parsing is enabled, we have completed the flags and can return.
- # If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough
- # to possibly call handle_go_custom_completion.
- return 0;
- fi
- ;;
- esac
+ local longest=0
+ # Look for the longest completion so that we can format things nicely
+ while IFS='' read -r comp; do
+ # Strip any description before checking the length
+ comp=${comp%%$tab*}
+ # Only consider the completions that match
+ comp=$(compgen -W "$comp" -- "$cur")
+ if ((${#comp}>longest)); then
+ longest=${#comp}
+ fi
+ done < <(printf "%s\n" "${out[@]}")
- # check if we are handling a flag with special work handling
- local index
- __git-bug_index_of_word "${prev}" "${flags_with_completion[@]}"
- if [[ ${index} -ge 0 ]]; then
- ${flags_completion[${index}]}
- return
- fi
+ local completions=()
+ while IFS='' read -r comp; do
+ if [ -z "$comp" ]; then
+ continue
+ fi
- # we are parsing a flag and don't have a special handler, no completion
- if [[ ${cur} != "${words[cword]}" ]]; then
- return
- fi
+ __git-bug_debug "Original comp: $comp"
+ comp="$(__git-bug_format_comp_descriptions "$comp" "$longest")"
+ __git-bug_debug "Final comp: $comp"
+ completions+=("$comp")
+ done < <(printf "%s\n" "${out[@]}")
- local completions
- completions=("${commands[@]}")
- if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then
- completions+=("${must_have_one_noun[@]}")
- elif [[ -n "${has_completion_function}" ]]; then
- # if a go completion function is provided, defer to that function
- __git-bug_handle_go_custom_completion
- fi
- if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then
- completions+=("${must_have_one_flag[@]}")
- fi
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${completions[*]}" -- "$cur")
- if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then
- while IFS='' read -r comp; do
- COMPREPLY+=("$comp")
- done < <(compgen -W "${noun_aliases[*]}" -- "$cur")
- fi
-
- if [[ ${#COMPREPLY[@]} -eq 0 ]]; then
- if declare -F __git-bug_custom_func >/dev/null; then
- # try command name qualified custom func
- __git-bug_custom_func
- else
- # otherwise fall back to unqualified for compatibility
- declare -F __custom_func >/dev/null && __custom_func
- fi
- fi
-
- # available in bash-completion >= 2, not always present on macOS
- if declare -F __ltrim_colon_completions >/dev/null; then
- __ltrim_colon_completions "$cur"
- fi
-
- # If there is only 1 completion and it is a flag with an = it will be completed
- # but we don't want a space after the =
- if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then
- compopt -o nospace
+ # If there is a single completion left, remove the description text
+ if [ ${#COMPREPLY[*]} -eq 1 ]; then
+ __git-bug_debug "COMPREPLY[0]: ${COMPREPLY[0]}"
+ comp="${COMPREPLY[0]%% *}"
+ __git-bug_debug "Removed description from single completion, which is now: ${comp}"
+ COMPREPLY=()
+ COMPREPLY+=("$comp")
fi
}
-# The arguments should be in the form "ext1|ext2|extn"
-__git-bug_handle_filename_extension_flag()
-{
- local ext="$1"
- _filedir "@(${ext})"
-}
-
-__git-bug_handle_subdirs_in_dir_flag()
+__git-bug_handle_special_char()
{
- local dir="$1"
- pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
+ local comp="$1"
+ local char=$2
+ if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then
+ local word=${comp%"${comp##*${char}}"}
+ local idx=${#COMPREPLY[*]}
+ while [[ $((--idx)) -ge 0 ]]; do
+ COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"}
+ done
+ fi
}
-__git-bug_handle_flag()
+__git-bug_format_comp_descriptions()
{
- __git-bug_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
+ local tab
+ tab=$(printf '\t')
+ local comp="$1"
+ local longest=$2
- # if a command required a flag, and we found it, unset must_have_one_flag()
- local flagname=${words[c]}
- local flagvalue=""
- # if the word contained an =
- if [[ ${words[c]} == *"="* ]]; then
- flagvalue=${flagname#*=} # take in as flagvalue after the =
- flagname=${flagname%=*} # strip everything after the =
- flagname="${flagname}=" # but put the = back
- fi
- __git-bug_debug "${FUNCNAME[0]}: looking for ${flagname}"
- if __git-bug_contains_word "${flagname}" "${must_have_one_flag[@]}"; then
- must_have_one_flag=()
- fi
+ # Properly format the description string which follows a tab character if there is one
+ if [[ "$comp" == *$tab* ]]; then
+ desc=${comp#*$tab}
+ comp=${comp%%$tab*}
- # if you set a flag which only applies to this command, don't show subcommands
- if __git-bug_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then
- commands=()
- fi
+ # $COLUMNS stores the current shell width.
+ # Remove an extra 4 because we add 2 spaces and 2 parentheses.
+ maxdesclength=$(( COLUMNS - longest - 4 ))
- # keep flag value with flagname as flaghash
- # flaghash variable is an associative array which is only supported in bash > 3.
- if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
- if [ -n "${flagvalue}" ] ; then
- flaghash[${flagname}]=${flagvalue}
- elif [ -n "${words[ $((c+1)) ]}" ] ; then
- flaghash[${flagname}]=${words[ $((c+1)) ]}
+ # Make sure we can fit a description of at least 8 characters
+ # if we are to align the descriptions.
+ if [[ $maxdesclength -gt 8 ]]; then
+ # Add the proper number of spaces to align the descriptions
+ for ((i = ${#comp} ; i < longest ; i++)); do
+ comp+=" "
+ done
else
- flaghash[${flagname}]="true" # pad "true" for bool flag
+ # Don't pad the descriptions so we can fit more text after the completion
+ maxdesclength=$(( COLUMNS - ${#comp} - 4 ))
fi
- fi
- # skip the argument to a two word flag
- if [[ ${words[c]} != *"="* ]] && __git-bug_contains_word "${words[c]}" "${two_word_flags[@]}"; then
- __git-bug_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument"
- c=$((c+1))
- # if we are looking for a flags value, don't show commands
- if [[ $c -eq $cword ]]; then
- commands=()
+ # If there is enough space for any description text,
+ # truncate the descriptions that are too long for the shell width
+ if [ $maxdesclength -gt 0 ]; then
+ if [ ${#desc} -gt $maxdesclength ]; then
+ desc=${desc:0:$(( maxdesclength - 1 ))}
+ desc+="…"
+ fi
+ comp+=" ($desc)"
fi
fi
- c=$((c+1))
-
+ # Must use printf to escape all special characters
+ printf "%q" "${comp}"
}
-__git-bug_handle_noun()
-{
- __git-bug_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
-
- if __git-bug_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then
- must_have_one_noun=()
- elif __git-bug_contains_word "${words[c]}" "${noun_aliases[@]}"; then
- must_have_one_noun=()
- fi
-
- nouns+=("${words[c]}")
- c=$((c+1))
-}
-
-__git-bug_handle_command()
+__start_git-bug()
{
- __git-bug_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
+ local cur prev words cword split
- local next_command
- if [[ -n ${last_command} ]]; then
- next_command="_${last_command}_${words[c]//:/__}"
- else
- if [[ $c -eq 0 ]]; then
- next_command="_git-bug_root_command"
- else
- next_command="_${words[c]//:/__}"
- fi
- fi
- c=$((c+1))
- __git-bug_debug "${FUNCNAME[0]}: looking for ${next_command}"
- declare -F "$next_command" >/dev/null && $next_command
-}
+ COMPREPLY=()
-__git-bug_handle_word()
-{
- if [[ $c -ge $cword ]]; then
- __git-bug_handle_reply
- return
- fi
- __git-bug_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
- if [[ "${words[c]}" == -* ]]; then
- __git-bug_handle_flag
- elif __git-bug_contains_word "${words[c]}" "${commands[@]}"; then
- __git-bug_handle_command
- elif [[ $c -eq 0 ]]; then
- __git-bug_handle_command
- elif __git-bug_contains_word "${words[c]}" "${command_aliases[@]}"; then
- # aliashash variable is an associative array which is only supported in bash > 3.
- if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
- words[c]=${aliashash[${words[c]}]}
- __git-bug_handle_command
- else
- __git-bug_handle_noun
- fi
+ # Call _init_completion from the bash-completion package
+ # to prepare the arguments properly
+ if declare -F _init_completion >/dev/null 2>&1; then
+ _init_completion -n "=:" || return
else
- __git-bug_handle_noun
+ __git-bug_init_completion -n "=:" || return
fi
- __git-bug_handle_word
-}
-
-
-_git_bug() {
- __start_git-bug "$@"
-}
-
-_git-bug_add()
-{
- last_command="git-bug_add"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--title=")
- two_word_flags+=("--title")
- two_word_flags+=("-t")
- local_nonpersistent_flags+=("--title")
- local_nonpersistent_flags+=("--title=")
- local_nonpersistent_flags+=("-t")
- flags+=("--message=")
- two_word_flags+=("--message")
- two_word_flags+=("-m")
- local_nonpersistent_flags+=("--message")
- local_nonpersistent_flags+=("--message=")
- local_nonpersistent_flags+=("-m")
- flags+=("--file=")
- two_word_flags+=("--file")
- two_word_flags+=("-F")
- local_nonpersistent_flags+=("--file")
- local_nonpersistent_flags+=("--file=")
- local_nonpersistent_flags+=("-F")
- flags+=("--non-interactive")
- local_nonpersistent_flags+=("--non-interactive")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_bridge_auth_add-token()
-{
- last_command="git-bug_bridge_auth_add-token"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--target=")
- two_word_flags+=("--target")
- two_word_flags+=("-t")
- local_nonpersistent_flags+=("--target")
- local_nonpersistent_flags+=("--target=")
- local_nonpersistent_flags+=("-t")
- flags+=("--login=")
- two_word_flags+=("--login")
- two_word_flags+=("-l")
- local_nonpersistent_flags+=("--login")
- local_nonpersistent_flags+=("--login=")
- local_nonpersistent_flags+=("-l")
- flags+=("--user=")
- two_word_flags+=("--user")
- two_word_flags+=("-u")
- local_nonpersistent_flags+=("--user")
- local_nonpersistent_flags+=("--user=")
- local_nonpersistent_flags+=("-u")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_bridge_auth_rm()
-{
- last_command="git-bug_bridge_auth_rm"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_bridge_auth_show()
-{
- last_command="git-bug_bridge_auth_show"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_bridge_auth()
-{
- last_command="git-bug_bridge_auth"
-
- command_aliases=()
-
- commands=()
- commands+=("add-token")
- commands+=("rm")
- commands+=("show")
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_bridge_configure()
-{
- last_command="git-bug_bridge_configure"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--name=")
- two_word_flags+=("--name")
- two_word_flags+=("-n")
- local_nonpersistent_flags+=("--name")
- local_nonpersistent_flags+=("--name=")
- local_nonpersistent_flags+=("-n")
- flags+=("--target=")
- two_word_flags+=("--target")
- two_word_flags+=("-t")
- local_nonpersistent_flags+=("--target")
- local_nonpersistent_flags+=("--target=")
- local_nonpersistent_flags+=("-t")
- flags+=("--url=")
- two_word_flags+=("--url")
- two_word_flags+=("-u")
- local_nonpersistent_flags+=("--url")
- local_nonpersistent_flags+=("--url=")
- local_nonpersistent_flags+=("-u")
- flags+=("--base-url=")
- two_word_flags+=("--base-url")
- two_word_flags+=("-b")
- local_nonpersistent_flags+=("--base-url")
- local_nonpersistent_flags+=("--base-url=")
- local_nonpersistent_flags+=("-b")
- flags+=("--login=")
- two_word_flags+=("--login")
- two_word_flags+=("-l")
- local_nonpersistent_flags+=("--login")
- local_nonpersistent_flags+=("--login=")
- local_nonpersistent_flags+=("-l")
- flags+=("--credential=")
- two_word_flags+=("--credential")
- two_word_flags+=("-c")
- local_nonpersistent_flags+=("--credential")
- local_nonpersistent_flags+=("--credential=")
- local_nonpersistent_flags+=("-c")
- flags+=("--token=")
- two_word_flags+=("--token")
- local_nonpersistent_flags+=("--token")
- local_nonpersistent_flags+=("--token=")
- flags+=("--token-stdin")
- local_nonpersistent_flags+=("--token-stdin")
- flags+=("--owner=")
- two_word_flags+=("--owner")
- two_word_flags+=("-o")
- local_nonpersistent_flags+=("--owner")
- local_nonpersistent_flags+=("--owner=")
- local_nonpersistent_flags+=("-o")
- flags+=("--project=")
- two_word_flags+=("--project")
- two_word_flags+=("-p")
- local_nonpersistent_flags+=("--project")
- local_nonpersistent_flags+=("--project=")
- local_nonpersistent_flags+=("-p")
- flags+=("--non-interactive")
- local_nonpersistent_flags+=("--non-interactive")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_bridge_pull()
-{
- last_command="git-bug_bridge_pull"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--no-resume")
- flags+=("-n")
- local_nonpersistent_flags+=("--no-resume")
- local_nonpersistent_flags+=("-n")
- flags+=("--since=")
- two_word_flags+=("--since")
- two_word_flags+=("-s")
- local_nonpersistent_flags+=("--since")
- local_nonpersistent_flags+=("--since=")
- local_nonpersistent_flags+=("-s")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_bridge_push()
-{
- last_command="git-bug_bridge_push"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_bridge_rm()
-{
- last_command="git-bug_bridge_rm"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_bridge()
-{
- last_command="git-bug_bridge"
-
- command_aliases=()
-
- commands=()
- commands+=("auth")
- commands+=("configure")
- commands+=("pull")
- commands+=("push")
- commands+=("rm")
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_commands()
-{
- last_command="git-bug_commands"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--pretty")
- flags+=("-p")
- local_nonpersistent_flags+=("--pretty")
- local_nonpersistent_flags+=("-p")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_comment_add()
-{
- last_command="git-bug_comment_add"
-
- 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")
- flags+=("--non-interactive")
- local_nonpersistent_flags+=("--non-interactive")
-
- must_have_one_flag=()
- must_have_one_noun=()
- 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")
- flags+=("--non-interactive")
- local_nonpersistent_flags+=("--non-interactive")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_comment()
-{
- last_command="git-bug_comment"
-
- command_aliases=()
-
- commands=()
- commands+=("add")
- commands+=("edit")
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-_git-bug_deselect()
-{
- last_command="git-bug_deselect"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_label_add()
-{
- last_command="git-bug_label_add"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_label_rm()
-{
- last_command="git-bug_label_rm"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_label()
-{
- last_command="git-bug_label"
-
- command_aliases=()
-
- commands=()
- commands+=("add")
- commands+=("rm")
+ __git-bug_debug
+ __git-bug_debug "========= starting completion logic =========="
+ __git-bug_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword"
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
+ # The user could have moved the cursor backwards on the command-line.
+ # We need to trigger completion from the $cword location, so we need
+ # to truncate the command-line ($words) up to the $cword location.
+ words=("${words[@]:0:$cword+1}")
+ __git-bug_debug "Truncated words[*]: ${words[*]},"
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_ls()
-{
- last_command="git-bug_ls"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--status=")
- two_word_flags+=("--status")
- two_word_flags+=("-s")
- local_nonpersistent_flags+=("--status")
- local_nonpersistent_flags+=("--status=")
- local_nonpersistent_flags+=("-s")
- flags+=("--author=")
- two_word_flags+=("--author")
- two_word_flags+=("-a")
- local_nonpersistent_flags+=("--author")
- local_nonpersistent_flags+=("--author=")
- local_nonpersistent_flags+=("-a")
- flags+=("--metadata=")
- two_word_flags+=("--metadata")
- two_word_flags+=("-m")
- local_nonpersistent_flags+=("--metadata")
- local_nonpersistent_flags+=("--metadata=")
- local_nonpersistent_flags+=("-m")
- flags+=("--participant=")
- two_word_flags+=("--participant")
- two_word_flags+=("-p")
- local_nonpersistent_flags+=("--participant")
- local_nonpersistent_flags+=("--participant=")
- local_nonpersistent_flags+=("-p")
- flags+=("--actor=")
- two_word_flags+=("--actor")
- two_word_flags+=("-A")
- local_nonpersistent_flags+=("--actor")
- local_nonpersistent_flags+=("--actor=")
- local_nonpersistent_flags+=("-A")
- flags+=("--label=")
- two_word_flags+=("--label")
- two_word_flags+=("-l")
- local_nonpersistent_flags+=("--label")
- local_nonpersistent_flags+=("--label=")
- local_nonpersistent_flags+=("-l")
- flags+=("--title=")
- two_word_flags+=("--title")
- two_word_flags+=("-t")
- local_nonpersistent_flags+=("--title")
- local_nonpersistent_flags+=("--title=")
- local_nonpersistent_flags+=("-t")
- flags+=("--no=")
- two_word_flags+=("--no")
- two_word_flags+=("-n")
- local_nonpersistent_flags+=("--no")
- local_nonpersistent_flags+=("--no=")
- local_nonpersistent_flags+=("-n")
- flags+=("--by=")
- two_word_flags+=("--by")
- two_word_flags+=("-b")
- local_nonpersistent_flags+=("--by")
- local_nonpersistent_flags+=("--by=")
- local_nonpersistent_flags+=("-b")
- flags+=("--direction=")
- two_word_flags+=("--direction")
- two_word_flags+=("-d")
- local_nonpersistent_flags+=("--direction")
- local_nonpersistent_flags+=("--direction=")
- local_nonpersistent_flags+=("-d")
- flags+=("--format=")
- two_word_flags+=("--format")
- two_word_flags+=("-f")
- local_nonpersistent_flags+=("--format")
- local_nonpersistent_flags+=("--format=")
- local_nonpersistent_flags+=("-f")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
+ local out directive
+ __git-bug_get_completion_results
+ __git-bug_process_completion_results
}
-_git-bug_ls-id()
-{
- last_command="git-bug_ls-id"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_ls-label()
-{
- last_command="git-bug_ls-label"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_pull()
-{
- last_command="git-bug_pull"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_push()
-{
- last_command="git-bug_push"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_rm()
-{
- last_command="git-bug_rm"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_select()
-{
- last_command="git-bug_select"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_show()
-{
- last_command="git-bug_show"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--field=")
- two_word_flags+=("--field")
- local_nonpersistent_flags+=("--field")
- local_nonpersistent_flags+=("--field=")
- flags+=("--format=")
- two_word_flags+=("--format")
- two_word_flags+=("-f")
- local_nonpersistent_flags+=("--format")
- local_nonpersistent_flags+=("--format=")
- local_nonpersistent_flags+=("-f")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_status_close()
-{
- last_command="git-bug_status_close"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_status_open()
-{
- last_command="git-bug_status_open"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_status()
-{
- last_command="git-bug_status"
-
- command_aliases=()
-
- commands=()
- commands+=("close")
- commands+=("open")
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_termui()
-{
- last_command="git-bug_termui"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_title_edit()
-{
- last_command="git-bug_title_edit"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--title=")
- two_word_flags+=("--title")
- two_word_flags+=("-t")
- local_nonpersistent_flags+=("--title")
- local_nonpersistent_flags+=("--title=")
- local_nonpersistent_flags+=("-t")
- flags+=("--non-interactive")
- local_nonpersistent_flags+=("--non-interactive")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_title()
-{
- last_command="git-bug_title"
-
- command_aliases=()
-
- commands=()
- commands+=("edit")
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_user_adopt()
-{
- last_command="git-bug_user_adopt"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_user_create()
-{
- last_command="git-bug_user_create"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--avatar=")
- two_word_flags+=("--avatar")
- two_word_flags+=("-a")
- local_nonpersistent_flags+=("--avatar")
- local_nonpersistent_flags+=("--avatar=")
- local_nonpersistent_flags+=("-a")
- flags+=("--email=")
- two_word_flags+=("--email")
- two_word_flags+=("-e")
- local_nonpersistent_flags+=("--email")
- local_nonpersistent_flags+=("--email=")
- local_nonpersistent_flags+=("-e")
- flags+=("--name=")
- two_word_flags+=("--name")
- two_word_flags+=("-n")
- local_nonpersistent_flags+=("--name")
- local_nonpersistent_flags+=("--name=")
- local_nonpersistent_flags+=("-n")
- flags+=("--non-interactive")
- local_nonpersistent_flags+=("--non-interactive")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_user_ls()
-{
- last_command="git-bug_user_ls"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--format=")
- two_word_flags+=("--format")
- two_word_flags+=("-f")
- local_nonpersistent_flags+=("--format")
- local_nonpersistent_flags+=("--format=")
- local_nonpersistent_flags+=("-f")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_user()
-{
- last_command="git-bug_user"
-
- command_aliases=()
-
- commands=()
- commands+=("adopt")
- commands+=("create")
- commands+=("ls")
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--field=")
- two_word_flags+=("--field")
- two_word_flags+=("-f")
- local_nonpersistent_flags+=("--field")
- local_nonpersistent_flags+=("--field=")
- local_nonpersistent_flags+=("-f")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_version()
-{
- last_command="git-bug_version"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--number")
- flags+=("-n")
- local_nonpersistent_flags+=("--number")
- local_nonpersistent_flags+=("-n")
- flags+=("--commit")
- flags+=("-c")
- local_nonpersistent_flags+=("--commit")
- local_nonpersistent_flags+=("-c")
- flags+=("--all")
- flags+=("-a")
- local_nonpersistent_flags+=("--all")
- local_nonpersistent_flags+=("-a")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_webui()
-{
- last_command="git-bug_webui"
-
- command_aliases=()
-
- commands=()
-
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
-
- flags+=("--host=")
- two_word_flags+=("--host")
- local_nonpersistent_flags+=("--host")
- local_nonpersistent_flags+=("--host=")
- flags+=("--open")
- local_nonpersistent_flags+=("--open")
- flags+=("--no-open")
- local_nonpersistent_flags+=("--no-open")
- flags+=("--port=")
- two_word_flags+=("--port")
- two_word_flags+=("-p")
- local_nonpersistent_flags+=("--port")
- local_nonpersistent_flags+=("--port=")
- local_nonpersistent_flags+=("-p")
- flags+=("--read-only")
- local_nonpersistent_flags+=("--read-only")
- flags+=("--query=")
- two_word_flags+=("--query")
- two_word_flags+=("-q")
- local_nonpersistent_flags+=("--query")
- local_nonpersistent_flags+=("--query=")
- local_nonpersistent_flags+=("-q")
-
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
-
-_git-bug_root_command()
-{
- last_command="git-bug"
-
- command_aliases=()
-
- commands=()
- commands+=("add")
- commands+=("bridge")
- commands+=("commands")
- commands+=("comment")
- commands+=("deselect")
- commands+=("label")
- commands+=("ls")
- commands+=("ls-id")
- commands+=("ls-label")
- commands+=("pull")
- commands+=("push")
- commands+=("rm")
- commands+=("select")
- commands+=("show")
- commands+=("status")
- commands+=("termui")
- if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
- command_aliases+=("tui")
- aliashash["tui"]="termui"
- fi
- commands+=("title")
- commands+=("user")
- commands+=("version")
- commands+=("webui")
+if [[ $(type -t compopt) = "builtin" ]]; then
+ complete -o default -F __start_git-bug git-bug
+else
+ complete -o default -o nospace -F __start_git-bug git-bug
+fi
- flags=()
- two_word_flags=()
- local_nonpersistent_flags=()
- flags_with_completion=()
- flags_completion=()
+# ex: ts=4 sw=4 et filetype=sh
+_git_bug() {
+ local cur prev words cword split
- must_have_one_flag=()
- must_have_one_noun=()
- noun_aliases=()
-}
+ COMPREPLY=()
-__start_git-bug()
-{
- local cur prev words cword split
- declare -A flaghash 2>/dev/null || :
- declare -A aliashash 2>/dev/null || :
+ # Call _init_completion from the bash-completion package
+ # to prepare the arguments properly
if declare -F _init_completion >/dev/null 2>&1; then
- _init_completion -s || return
+ _init_completion -n "=:" || return
else
- __git-bug_init_completion -n "=" || return
+ __git-bug_init_completion -n "=:" || return
fi
- local c=0
- local flag_parsing_disabled=
- local flags=()
- local two_word_flags=()
- local local_nonpersistent_flags=()
- local flags_with_completion=()
- local flags_completion=()
- local commands=("git-bug")
- local command_aliases=()
- local must_have_one_flag=()
- local must_have_one_noun=()
- local has_completion_function=""
- local last_command=""
- local nouns=()
- local noun_aliases=()
+ # START PATCH
+ # replace in the array ("git","bug", ...) to ("git-bug", ...) and adjust the index in cword
+ words=("git-bug" "${words[@]:2}")
+ cword=$(($cword-1))
+ # END PATCH
- __git-bug_handle_word
-}
+ __git-bug_debug
+ __git-bug_debug "========= starting completion logic =========="
+ __git-bug_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword"
-if [[ $(type -t compopt) = "builtin" ]]; then
- complete -o default -F __start_git-bug git-bug
-else
- complete -o default -o nospace -F __start_git-bug git-bug
-fi
+ # The user could have moved the cursor backwards on the command-line.
+ # We need to trigger completion from the $cword location, so we need
+ # to truncate the command-line ($words) up to the $cword location.
+ words=("${words[@]:0:$cword+1}")
+ __git-bug_debug "Truncated words[*]: ${words[*]},"
-# ex: ts=4 sw=4 et filetype=sh
+ local out directive
+ __git-bug_get_completion_results
+ __git-bug_process_completion_results
+}
diff --git a/misc/gen_completion.go b/misc/gen_completion.go
index c073e67e..1f86124d 100644
--- a/misc/gen_completion.go
+++ b/misc/gen_completion.go
@@ -40,25 +40,86 @@ func main() {
}
func genBash(root *cobra.Command) error {
- cwd, _ := os.Getwd()
- dir := filepath.Join(cwd, "misc", "bash_completion", "git-bug")
- return root.GenBashCompletionFile(dir)
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ f, err := os.Create(filepath.Join(cwd, "misc", "bash_completion", "git-bug"))
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ const patch = `
+_git_bug() {
+ local cur prev words cword split
+
+ COMPREPLY=()
+
+ # Call _init_completion from the bash-completion package
+ # to prepare the arguments properly
+ if declare -F _init_completion >/dev/null 2>&1; then
+ _init_completion -n "=:" || return
+ else
+ __git-bug_init_completion -n "=:" || return
+ fi
+
+ # START PATCH
+ # replace in the array ("git","bug", ...) to ("git-bug", ...) and adjust the index in cword
+ words=("git-bug" "${words[@]:2}")
+ cword=$(($cword-1))
+ # END PATCH
+
+ __git-bug_debug
+ __git-bug_debug "========= starting completion logic =========="
+ __git-bug_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword"
+
+ # The user could have moved the cursor backwards on the command-line.
+ # We need to trigger completion from the $cword location, so we need
+ # to truncate the command-line ($words) up to the $cword location.
+ words=("${words[@]:0:$cword+1}")
+ __git-bug_debug "Truncated words[*]: ${words[*]},"
+
+ local out directive
+ __git-bug_get_completion_results
+ __git-bug_process_completion_results
+}
+`
+ err = root.GenBashCompletionV2(f, true)
+ if err != nil {
+ return err
+ }
+
+ // Custom bash code to connect the git completion for "git bug" to the
+ // git-bug completion for "git-bug"
+ _, err = f.WriteString(patch)
+
+ return err
}
func genFish(root *cobra.Command) error {
- cwd, _ := os.Getwd()
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
dir := filepath.Join(cwd, "misc", "fish_completion", "git-bug")
return root.GenFishCompletionFile(dir, true)
}
func genPowerShell(root *cobra.Command) error {
- cwd, _ := os.Getwd()
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
path := filepath.Join(cwd, "misc", "powershell_completion", "git-bug")
return root.GenPowerShellCompletionFile(path)
}
func genZsh(root *cobra.Command) error {
- cwd, _ := os.Getwd()
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
path := filepath.Join(cwd, "misc", "zsh_completion", "git-bug")
return root.GenZshCompletionFile(path)
}
diff --git a/query/parser_test.go b/query/parser_test.go
index cef01ffd..f71a7b42 100644
--- a/query/parser_test.go
+++ b/query/parser_test.go
@@ -62,6 +62,10 @@ func TestParse(t *testing.T) {
}},
{"sort:unknown", nil},
+ {"label:\"foo:bar\"", &Query{
+ Filters: Filters{Label: []string{"foo:bar"}},
+ }},
+
// KVV
{`metadata:key:"https://www.example.com/"`, &Query{
Filters: Filters{Metadata: []StringPair{{"key", "https://www.example.com/"}}},
diff --git a/util/text/validate.go b/util/text/validate.go
index 4c3f7065..f25a56b4 100644
--- a/util/text/validate.go
+++ b/util/text/validate.go
@@ -33,7 +33,7 @@ func Safe(s string) bool {
return true
}
-// Safe will tell if a character in the string is considered unsafe
+// SafeOneLine will tell if a character in the string is considered unsafe
// Currently trigger on all unicode control character
func SafeOneLine(s string) bool {
for _, r := range s {