aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml4
-rw-r--r--Gopkg.lock16
-rw-r--r--Gopkg.toml4
-rw-r--r--README.md1
-rw-r--r--bridge/core/bridge.go10
-rw-r--r--bridge/github/export.go2
-rw-r--r--bug/label.go66
-rw-r--r--bug/label_test.go17
-rw-r--r--cache/repo_cache.go35
-rw-r--r--commands/bridge.go3
-rw-r--r--commands/comment.go7
-rw-r--r--commands/label_rm.go3
-rw-r--r--commands/ls.go28
-rw-r--r--commands/select.go3
-rw-r--r--commands/webui.go2
-rw-r--r--doc/model.md34
-rw-r--r--git-bug.go4
-rw-r--r--graphql/resolvers/label.go2
-rw-r--r--identity/identity.go8
-rw-r--r--repository/config.go49
-rw-r--r--repository/config_git.go225
-rw-r--r--repository/config_mem.go84
-rw-r--r--repository/git.go191
-rw-r--r--repository/git_test.go33
-rw-r--r--repository/git_testing.go5
-rw-r--r--repository/mock_repo.go74
-rw-r--r--repository/repo.go21
-rw-r--r--termui/bug_table.go46
-rw-r--r--termui/label_select.go10
-rw-r--r--termui/msg_popup.go2
-rw-r--r--termui/show_bug.go11
-rw-r--r--termui/termui.go3
-rw-r--r--util/text/left_padded.go42
-rw-r--r--util/text/left_padded_test.go56
-rw-r--r--util/text/text.go330
-rw-r--r--util/text/text_test.go376
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/.gitignore1
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/.travis.yml16
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/LICENSE21
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/Readme.md71
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/align.go67
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/escapes.go95
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/go.mod8
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/go.sum9
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/left_pad.go50
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/len.go45
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/trim.go28
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/truncate.go24
-rw-r--r--vendor/github.com/MichaelMure/go-term-text/wrap.go334
-rw-r--r--vendor/github.com/theckman/goconstraint/go1.10/gte/constraint.go8
-rw-r--r--vendor/github.com/theckman/goconstraint/go1.10/gte/go110.go7
-rw-r--r--vendor/github.com/theckman/goconstraint/go1.9/gte/constraint.go8
-rw-r--r--vendor/github.com/theckman/goconstraint/go1.9/gte/go19.go7
53 files changed, 1396 insertions, 1210 deletions
diff --git a/.travis.yml b/.travis.yml
index 5a46a981..cac34dc7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,13 @@
matrix:
include:
- language: go
- go: "1.9"
- - language: go
go: "1.10"
- language: go
go: "1.11"
- language: go
go: "1.12"
+ - language: go
+ go: "1.13"
- language: node_js
node_js: 8
before_install:
diff --git a/Gopkg.lock b/Gopkg.lock
index 90f03d57..96f1e968 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -25,6 +25,14 @@
version = "v0.9.2"
[[projects]]
+ digest = "1:b46ef47d5fcc120e6fc1f75e75106f31cbb51fe9981234b5c191d0083d8f9867"
+ name = "github.com/MichaelMure/go-term-text"
+ packages = ["."]
+ pruneopts = "UT"
+ revision = "60f9049b9d18b9370b8ed1247fe4334af5db131a"
+ version = "v0.2.1"
+
+[[projects]]
branch = "master"
digest = "1:38a84d9b4cf50b3e8eb2b54f218413ac163076e3a7763afe5fa15a4eb15fbda6"
name = "github.com/MichaelMure/gocui"
@@ -313,9 +321,9 @@
version = "v1.2.2"
[[projects]]
- digest = "1:823766f4e1833bd562339317d905475fe46789da3863b2da7a1871f9f12bb4b3"
+ digest = "1:8d784da270f610f505e2c9785faec4c81d4a6a6a1034412685a4bf2cffe24cef"
name = "github.com/theckman/goconstraint"
- packages = ["go1.9/gte"]
+ packages = ["go1.10/gte"]
pruneopts = "UT"
revision = "93babf24513d0e8277635da8169fcc5a46ae3f6a"
version = "v1.11.0"
@@ -464,6 +472,7 @@
"github.com/99designs/gqlgen/graphql",
"github.com/99designs/gqlgen/graphql/introspection",
"github.com/99designs/gqlgen/handler",
+ "github.com/MichaelMure/go-term-text",
"github.com/MichaelMure/gocui",
"github.com/blang/semver",
"github.com/cheekybits/genny/generic",
@@ -471,7 +480,6 @@
"github.com/fatih/color",
"github.com/gorilla/mux",
"github.com/icrowley/fake",
- "github.com/mattn/go-runewidth",
"github.com/phayes/freeport",
"github.com/pkg/errors",
"github.com/shurcooL/githubv4",
@@ -482,7 +490,7 @@
"github.com/spf13/cobra/doc",
"github.com/stretchr/testify/assert",
"github.com/stretchr/testify/require",
- "github.com/theckman/goconstraint/go1.9/gte",
+ "github.com/theckman/goconstraint/go1.10/gte",
"github.com/vektah/gqlgen/client",
"github.com/vektah/gqlparser",
"github.com/vektah/gqlparser/ast",
diff --git a/Gopkg.toml b/Gopkg.toml
index a428f904..28e0f135 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -79,3 +79,7 @@
[[constraint]]
branch = "master"
name = "golang.org/x/sync"
+
+[[constraint]]
+ name = "github.com/MichaelMure/go-term-text"
+ version = "0.2.1"
diff --git a/README.md b/README.md
index 05181246..42501ebc 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@
</div>
`git-bug` is a bug tracker that:
+
- **fully embed in git**: you only need your git repository to have a bug tracker
- **is distributed**: use your normal git remote to collaborate, push and pull your bugs !
- **works offline**: in a plane or under the sea ? keep reading and writing bugs
diff --git a/bridge/core/bridge.go b/bridge/core/bridge.go
index 6fd28b03..47a89389 100644
--- a/bridge/core/bridge.go
+++ b/bridge/core/bridge.go
@@ -134,7 +134,7 @@ func DefaultBridge(repo *cache.RepoCache) (*Bridge, error) {
// ConfiguredBridges return the list of bridge that are configured for the given
// repo
func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
- configs, err := repo.ReadConfigs(bridgeConfigKeyPrefix + ".")
+ configs, err := repo.LocalConfig().ReadAll(bridgeConfigKeyPrefix + ".")
if err != nil {
return nil, errors.Wrap(err, "can't read configured bridges")
}
@@ -171,7 +171,7 @@ func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
func BridgeExist(repo repository.RepoCommon, name string) bool {
keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
- conf, err := repo.ReadConfigs(keyPrefix)
+ conf, err := repo.LocalConfig().ReadAll(keyPrefix)
return err == nil && len(conf) > 0
}
@@ -188,7 +188,7 @@ func RemoveBridge(repo repository.RepoCommon, name string) error {
}
keyPrefix := fmt.Sprintf("git-bug.bridge.%s", name)
- return repo.RmConfigs(keyPrefix)
+ return repo.LocalConfig().RemoveAll(keyPrefix)
}
// Configure run the target specific configuration process
@@ -211,7 +211,7 @@ func (b *Bridge) storeConfig(conf Configuration) error {
for key, val := range conf {
storeKey := fmt.Sprintf("git-bug.bridge.%s.%s", b.Name, key)
- err := b.repo.StoreConfig(storeKey, val)
+ err := b.repo.LocalConfig().StoreString(storeKey, val)
if err != nil {
return errors.Wrap(err, "error while storing bridge configuration")
}
@@ -235,7 +235,7 @@ func (b *Bridge) ensureConfig() error {
func loadConfig(repo repository.RepoCommon, name string) (Configuration, error) {
keyPrefix := fmt.Sprintf("git-bug.bridge.%s.", name)
- pairs, err := repo.ReadConfigs(keyPrefix)
+ pairs, err := repo.LocalConfig().ReadAll(keyPrefix)
if err != nil {
return nil, errors.Wrap(err, "error while reading bridge configuration")
}
diff --git a/bridge/github/export.go b/bridge/github/export.go
index b239eff9..a79256fc 100644
--- a/bridge/github/export.go
+++ b/bridge/github/export.go
@@ -576,7 +576,7 @@ func (ge *githubExporter) getOrCreateGithubLabelID(ctx context.Context, gc *gith
}
// RGBA to hex color
- rgba := label.RGBA()
+ rgba := label.Color().RGBA()
hexColor := fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
diff --git a/bug/label.go b/bug/label.go
index 0d6d4142..1344d97e 100644
--- a/bug/label.go
+++ b/bug/label.go
@@ -15,32 +15,34 @@ func (l Label) String() string {
return string(l)
}
+type LabelColor color.RGBA
+
// RGBA from a Label computed in a deterministic way
-func (l Label) RGBA() color.RGBA {
+func (l Label) Color() LabelColor {
id := 0
hash := sha1.Sum([]byte(l))
// colors from: https://material-ui.com/style/color/
- colors := []color.RGBA{
- color.RGBA{R: 244, G: 67, B: 54, A: 255}, // red
- color.RGBA{R: 233, G: 30, B: 99, A: 255}, // pink
- color.RGBA{R: 156, G: 39, B: 176, A: 255}, // purple
- color.RGBA{R: 103, G: 58, B: 183, A: 255}, // deepPurple
- color.RGBA{R: 63, G: 81, B: 181, A: 255}, // indigo
- color.RGBA{R: 33, G: 150, B: 243, A: 255}, // blue
- color.RGBA{R: 3, G: 169, B: 244, A: 255}, // lightBlue
- color.RGBA{R: 0, G: 188, B: 212, A: 255}, // cyan
- color.RGBA{R: 0, G: 150, B: 136, A: 255}, // teal
- color.RGBA{R: 76, G: 175, B: 80, A: 255}, // green
- color.RGBA{R: 139, G: 195, B: 74, A: 255}, // lightGreen
- color.RGBA{R: 205, G: 220, B: 57, A: 255}, // lime
- color.RGBA{R: 255, G: 235, B: 59, A: 255}, // yellow
- color.RGBA{R: 255, G: 193, B: 7, A: 255}, // amber
- color.RGBA{R: 255, G: 152, B: 0, A: 255}, // orange
- color.RGBA{R: 255, G: 87, B: 34, A: 255}, // deepOrange
- color.RGBA{R: 121, G: 85, B: 72, A: 255}, // brown
- color.RGBA{R: 158, G: 158, B: 158, A: 255}, // grey
- color.RGBA{R: 96, G: 125, B: 139, A: 255}, // blueGrey
+ colors := []LabelColor{
+ LabelColor{R: 244, G: 67, B: 54, A: 255}, // red
+ LabelColor{R: 233, G: 30, B: 99, A: 255}, // pink
+ LabelColor{R: 156, G: 39, B: 176, A: 255}, // purple
+ LabelColor{R: 103, G: 58, B: 183, A: 255}, // deepPurple
+ LabelColor{R: 63, G: 81, B: 181, A: 255}, // indigo
+ LabelColor{R: 33, G: 150, B: 243, A: 255}, // blue
+ LabelColor{R: 3, G: 169, B: 244, A: 255}, // lightBlue
+ LabelColor{R: 0, G: 188, B: 212, A: 255}, // cyan
+ LabelColor{R: 0, G: 150, B: 136, A: 255}, // teal
+ LabelColor{R: 76, G: 175, B: 80, A: 255}, // green
+ LabelColor{R: 139, G: 195, B: 74, A: 255}, // lightGreen
+ LabelColor{R: 205, G: 220, B: 57, A: 255}, // lime
+ LabelColor{R: 255, G: 235, B: 59, A: 255}, // yellow
+ LabelColor{R: 255, G: 193, B: 7, A: 255}, // amber
+ LabelColor{R: 255, G: 152, B: 0, A: 255}, // orange
+ LabelColor{R: 255, G: 87, B: 34, A: 255}, // deepOrange
+ LabelColor{R: 121, G: 85, B: 72, A: 255}, // brown
+ LabelColor{R: 158, G: 158, B: 158, A: 255}, // grey
+ LabelColor{R: 96, G: 125, B: 139, A: 255}, // blueGrey
}
for _, char := range hash {
@@ -50,6 +52,28 @@ func (l Label) RGBA() color.RGBA {
return colors[id]
}
+func (lc LabelColor) RGBA() color.RGBA {
+ return color.RGBA(lc)
+}
+
+type Term256 int
+
+func (lc LabelColor) Term256() Term256 {
+ red := Term256(lc.R) * 6 / 256
+ green := Term256(lc.G) * 6 / 256
+ blue := Term256(lc.B) * 6 / 256
+
+ return red*36 + green*6 + blue + 16
+}
+
+func (t Term256) Escape() string {
+ return fmt.Sprintf("\x1b[38;5;%dm", t)
+}
+
+func (t Term256) Unescape() string {
+ return "\x1b[0m"
+}
+
func (l Label) Validate() error {
str := string(l)
diff --git a/bug/label_test.go b/bug/label_test.go
index f87c7411..225e1352 100644
--- a/bug/label_test.go
+++ b/bug/label_test.go
@@ -1,36 +1,35 @@
package bug
import (
- "image/color"
"testing"
"github.com/stretchr/testify/require"
)
func TestLabelRGBA(t *testing.T) {
- rgba := Label("test").RGBA()
- expected := color.RGBA{R: 255, G: 87, B: 34, A: 255}
+ rgba := Label("test").Color()
+ expected := LabelColor{R: 255, G: 87, B: 34, A: 255}
require.Equal(t, expected, rgba)
}
func TestLabelRGBASimilar(t *testing.T) {
- rgba := Label("test1").RGBA()
- expected := color.RGBA{R: 0, G: 188, B: 212, A: 255}
+ rgba := Label("test1").Color()
+ expected := LabelColor{R: 0, G: 188, B: 212, A: 255}
require.Equal(t, expected, rgba)
}
func TestLabelRGBAReverse(t *testing.T) {
- rgba := Label("tset").RGBA()
- expected := color.RGBA{R: 233, G: 30, B: 99, A: 255}
+ rgba := Label("tset").Color()
+ expected := LabelColor{R: 233, G: 30, B: 99, A: 255}
require.Equal(t, expected, rgba)
}
func TestLabelRGBAEqual(t *testing.T) {
- color1 := Label("test").RGBA()
- color2 := Label("test").RGBA()
+ color1 := Label("test").Color()
+ color2 := Label("test").Color()
require.Equal(t, color1, color2)
}
diff --git a/cache/repo_cache.go b/cache/repo_cache.go
index bc095856..ec4cf436 100644
--- a/cache/repo_cache.go
+++ b/cache/repo_cache.go
@@ -99,6 +99,16 @@ func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
return c, c.write()
}
+// LocalConfig give access to the repository scoped configuration
+func (c *RepoCache) LocalConfig() repository.Config {
+ return c.repo.LocalConfig()
+}
+
+// GlobalConfig give access to the git global configuration
+func (c *RepoCache) GlobalConfig() repository.Config {
+ return c.repo.GlobalConfig()
+}
+
// GetPath returns the path to the repo.
func (c *RepoCache) GetPath() string {
return c.repo.GetPath()
@@ -124,31 +134,6 @@ func (c *RepoCache) GetUserEmail() (string, error) {
return c.repo.GetUserEmail()
}
-// StoreConfig store a single key/value pair in the config of the repo
-func (c *RepoCache) StoreConfig(key string, value string) error {
- return c.repo.StoreConfig(key, value)
-}
-
-// ReadConfigs read all key/value pair matching the key prefix
-func (c *RepoCache) ReadConfigs(keyPrefix string) (map[string]string, error) {
- return c.repo.ReadConfigs(keyPrefix)
-}
-
-// ReadConfigBool read a single boolean value from the config
-func (c *RepoCache) ReadConfigBool(key string) (bool, error) {
- return c.repo.ReadConfigBool(key)
-}
-
-// ReadConfigBool read a single string value from the config
-func (c *RepoCache) ReadConfigString(key string) (string, error) {
- return c.repo.ReadConfigString(key)
-}
-
-// RmConfigs remove all key/value pair matching the key prefix
-func (c *RepoCache) RmConfigs(keyPrefix string) error {
- return c.repo.RmConfigs(keyPrefix)
-}
-
func (c *RepoCache) lock() error {
lockPath := repoLockFilePath(c.repo)
diff --git a/commands/bridge.go b/commands/bridge.go
index 2566fd06..3c398e6b 100644
--- a/commands/bridge.go
+++ b/commands/bridge.go
@@ -3,10 +3,11 @@ package commands
import (
"fmt"
+ "github.com/spf13/cobra"
+
"github.com/MichaelMure/git-bug/bridge"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/util/interrupt"
- "github.com/spf13/cobra"
)
func runBridge(cmd *cobra.Command, args []string) error {
diff --git a/commands/comment.go b/commands/comment.go
index 33bae65d..4be39a84 100644
--- a/commands/comment.go
+++ b/commands/comment.go
@@ -3,13 +3,14 @@ package commands
import (
"fmt"
+ "github.com/MichaelMure/go-term-text"
+ "github.com/spf13/cobra"
+
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/commands/select"
"github.com/MichaelMure/git-bug/util/colors"
"github.com/MichaelMure/git-bug/util/interrupt"
- "github.com/MichaelMure/git-bug/util/text"
- "github.com/spf13/cobra"
)
func runComment(cmd *cobra.Command, args []string) error {
@@ -41,7 +42,7 @@ func commentsTextOutput(comments []bug.Comment) {
fmt.Printf("Author: %s\n", colors.Magenta(comment.Author.DisplayName()))
fmt.Printf("Id: %s\n", colors.Cyan(comment.Id().Human()))
fmt.Printf("Date: %s\n\n", comment.FormatTime())
- fmt.Println(text.LeftPad(comment.Message, 4))
+ fmt.Println(text.LeftPadLines(comment.Message, 4))
}
}
diff --git a/commands/label_rm.go b/commands/label_rm.go
index a0c1c56d..11300c78 100644
--- a/commands/label_rm.go
+++ b/commands/label_rm.go
@@ -3,10 +3,11 @@ package commands
import (
"fmt"
+ "github.com/spf13/cobra"
+
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/commands/select"
"github.com/MichaelMure/git-bug/util/interrupt"
- "github.com/spf13/cobra"
)
func runLabelRm(cmd *cobra.Command, args []string) error {
diff --git a/commands/ls.go b/commands/ls.go
index 9c32642e..70a948e6 100644
--- a/commands/ls.go
+++ b/commands/ls.go
@@ -4,11 +4,12 @@ import (
"fmt"
"strings"
+ text "github.com/MichaelMure/go-term-text"
+ "github.com/spf13/cobra"
+
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/util/colors"
"github.com/MichaelMure/git-bug/util/interrupt"
- "github.com/MichaelMure/git-bug/util/text"
- "github.com/spf13/cobra"
)
var (
@@ -65,17 +66,30 @@ func runLsBug(cmd *cobra.Command, args []string) error {
name = b.LegacyAuthor.DisplayName()
}
+ var labelsTxt strings.Builder
+ for _, l := range b.Labels {
+ lc256 := l.Color().Term256()
+ labelsTxt.WriteString(lc256.Escape())
+ labelsTxt.WriteString(" ◼")
+ labelsTxt.WriteString(lc256.Unescape())
+ }
+
// truncate + pad if needed
- titleFmt := text.LeftPadMaxLine(b.Title, 50, 0)
+ labelsFmt := text.TruncateMax(labelsTxt.String(), 10)
+ titleFmt := text.LeftPadMaxLine(b.Title, 50-text.Len(labelsFmt), 0)
authorFmt := text.LeftPadMaxLine(name, 15, 0)
- fmt.Printf("%s %s\t%s\t%s\tC:%d L:%d\n",
+ comments := fmt.Sprintf("%4d 💬", b.LenComments)
+ if b.LenComments > 9999 {
+ comments = " ∞ 💬"
+ }
+
+ fmt.Printf("%s %s\t%s\t%s\t%s\n",
colors.Cyan(b.Id.Human()),
colors.Yellow(b.Status),
- titleFmt,
+ titleFmt+labelsFmt,
colors.Magenta(authorFmt),
- b.LenComments,
- len(b.Labels),
+ comments,
)
}
diff --git a/commands/select.go b/commands/select.go
index 7c40df5c..f2ae33ca 100644
--- a/commands/select.go
+++ b/commands/select.go
@@ -4,10 +4,11 @@ import (
"errors"
"fmt"
+ "github.com/spf13/cobra"
+
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/commands/select"
"github.com/MichaelMure/git-bug/util/interrupt"
- "github.com/spf13/cobra"
)
func runSelect(cmd *cobra.Command, args []string) error {
diff --git a/commands/webui.go b/commands/webui.go
index d6b6a661..8e735e55 100644
--- a/commands/webui.go
+++ b/commands/webui.go
@@ -100,7 +100,7 @@ func runWebUI(cmd *cobra.Command, args []string) error {
fmt.Printf("Graphql Playground: http://%s/playground\n", addr)
fmt.Println("Press Ctrl+c to quit")
- configOpen, err := repo.ReadConfigBool(webUIOpenConfigKey)
+ configOpen, err := repo.LocalConfig().ReadBool(webUIOpenConfigKey)
if err == repository.ErrNoConfigEntry {
// default to true
configOpen = true
diff --git a/doc/model.md b/doc/model.md
index ad5e6ce9..c6d12bf5 100644
--- a/doc/model.md
+++ b/doc/model.md
@@ -8,23 +8,33 @@ The biggest problem when creating a distributed bug tracker is that there is no
To deal with this problem, you need a way to merge these changes in a meaningful way.
-Instead of storing directly the final bug data, we store a series of edit `Operation`s. One such operation could looks like this:
+Instead of storing the final bug data directly, we store a series of edit `Operation`s.
+
+Note: In git-bug internally it is a golang struct, but in the git repo it is stored as JSON, as seen later.
+
+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:
```json
-{
- "type": "SET_TITLE",
- "author": {
- "id": "5034cd36acf1a2dadb52b2db17f620cc050eb65c"
+[
+ {
+ "type": "SET_TITLE",
+ "author": {
+ "id": "5034cd36acf1a2dadb52b2db17f620cc050eb65c"
+ },
+ "timestamp": 1533640589,
+ "title": "This title is better"
},
- "timestamp": 1533640589,
- "title": "This title is better"
-}
+ {
+ "type": "ADD_COMMENT",
+ "author": {
+ "id": "5034cd36acf1a2dadb52b2db17f620cc050eb65c"
+ },
+ "timestamp": 1533640612,
+ "message": "A new comment"
+ }
+]
```
-Note: Json provided for readability. Internally it's a golang struct.
-
-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 is arbitrary serialized data.
-
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.
diff --git a/git-bug.go b/git-bug.go
index cf59182f..50415ae2 100644
--- a/git-bug.go
+++ b/git-bug.go
@@ -9,8 +9,8 @@ package main
import (
"github.com/MichaelMure/git-bug/commands"
- // minimal go version is 1.9
- _ "github.com/theckman/goconstraint/go1.9/gte"
+ // minimal go version is 1.10
+ _ "github.com/theckman/goconstraint/go1.10/gte"
)
func main() {
diff --git a/graphql/resolvers/label.go b/graphql/resolvers/label.go
index 690bf7f6..0368a1e6 100644
--- a/graphql/resolvers/label.go
+++ b/graphql/resolvers/label.go
@@ -19,7 +19,7 @@ func (labelResolver) Name(ctx context.Context, obj *bug.Label) (string, error) {
}
func (labelResolver) Color(ctx context.Context, obj *bug.Label) (*color.RGBA, error) {
- rgba := obj.RGBA()
+ rgba := obj.Color().RGBA()
return &rgba, nil
}
diff --git a/identity/identity.go b/identity/identity.go
index 765b77cd..b7d44a4b 100644
--- a/identity/identity.go
+++ b/identity/identity.go
@@ -220,7 +220,7 @@ func NewFromGitUser(repo repository.Repo) (*Identity, error) {
// IsUserIdentitySet tell if the user identity is correctly set.
func IsUserIdentitySet(repo repository.RepoCommon) (bool, error) {
- configs, err := repo.ReadConfigs(identityConfigKey)
+ configs, err := repo.LocalConfig().ReadAll(identityConfigKey)
if err != nil {
return false, err
}
@@ -234,12 +234,12 @@ func IsUserIdentitySet(repo repository.RepoCommon) (bool, error) {
// SetUserIdentity store the user identity's id in the git config
func SetUserIdentity(repo repository.RepoCommon, identity *Identity) error {
- return repo.StoreConfig(identityConfigKey, identity.Id().String())
+ return repo.LocalConfig().StoreString(identityConfigKey, identity.Id().String())
}
// GetUserIdentity read the current user identity, set with a git config entry
func GetUserIdentity(repo repository.Repo) (*Identity, error) {
- configs, err := repo.ReadConfigs(identityConfigKey)
+ configs, err := repo.LocalConfig().ReadAll(identityConfigKey)
if err != nil {
return nil, err
}
@@ -263,7 +263,7 @@ func GetUserIdentity(repo repository.Repo) (*Identity, error) {
i, err := ReadLocal(repo, id)
if err == ErrIdentityNotExist {
- innerErr := repo.RmConfigs(identityConfigKey)
+ innerErr := repo.LocalConfig().RemoveAll(identityConfigKey)
if innerErr != nil {
_, _ = fmt.Fprintln(os.Stderr, errors.Wrap(innerErr, "can't clear user identity").Error())
}
diff --git a/repository/config.go b/repository/config.go
new file mode 100644
index 00000000..ec5094e0
--- /dev/null
+++ b/repository/config.go
@@ -0,0 +1,49 @@
+package repository
+
+import (
+ "strconv"
+ "time"
+)
+
+// Config represent the common function interacting with the repository config storage
+type Config interface {
+ // Store writes a single key/value pair in the config
+ StoreString(key, value string) error
+
+ // Store writes a key and timestamp value to the config
+ StoreTimestamp(key string, value time.Time) error
+
+ // Store writes a key and boolean value to the config
+ StoreBool(key string, value bool) error
+
+ // ReadAll reads all key/value pair matching the key prefix
+ ReadAll(keyPrefix string) (map[string]string, error)
+
+ // ReadBool read a single boolean value from the config
+ // Return ErrNoConfigEntry or ErrMultipleConfigEntry if
+ // there is zero or more than one entry for this key
+ ReadBool(key string) (bool, error)
+
+ // ReadBool read a single string value from the config
+ // Return ErrNoConfigEntry or ErrMultipleConfigEntry if
+ // there is zero or more than one entry for this key
+ ReadString(key string) (string, error)
+
+ // ReadTimestamp read a single timestamp value from the config
+ // Return ErrNoConfigEntry or ErrMultipleConfigEntry if
+ // there is zero or more than one entry for this key
+ ReadTimestamp(key string) (*time.Time, error)
+
+ // RemoveAll removes all key/value pair matching the key prefix
+ RemoveAll(keyPrefix string) error
+}
+
+func parseTimestamp(s string) (*time.Time, error) {
+ timestamp, err := strconv.Atoi(s)
+ if err != nil {
+ return nil, err
+ }
+
+ t := time.Unix(int64(timestamp), 0)
+ return &t, nil
+}
diff --git a/repository/config_git.go b/repository/config_git.go
new file mode 100644
index 00000000..eac882a2
--- /dev/null
+++ b/repository/config_git.go
@@ -0,0 +1,225 @@
+package repository
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/blang/semver"
+ "github.com/pkg/errors"
+)
+
+var _ Config = &gitConfig{}
+
+type gitConfig struct {
+ repo *GitRepo
+ localityFlag string
+}
+
+func newGitConfig(repo *GitRepo, global bool) *gitConfig {
+ localityFlag := "--local"
+ if global {
+ localityFlag = "--global"
+ }
+ return &gitConfig{
+ repo: repo,
+ localityFlag: localityFlag,
+ }
+}
+
+// StoreString store a single key/value pair in the config of the repo
+func (gc *gitConfig) StoreString(key string, value string) error {
+ _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--replace-all", key, value)
+ return err
+}
+
+func (gc *gitConfig) StoreBool(key string, value bool) error {
+ return gc.StoreString(key, strconv.FormatBool(value))
+}
+
+func (gc *gitConfig) StoreTimestamp(key string, value time.Time) error {
+ return gc.StoreString(key, strconv.Itoa(int(value.Unix())))
+}
+
+// ReadAll read all key/value pair matching the key prefix
+func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) {
+ stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--get-regexp", keyPrefix)
+
+ // / \
+ // / ! \
+ // -------
+ //
+ // There can be a legitimate error here, but I see no portable way to
+ // distinguish them from the git error that say "no matching value exist"
+ if err != nil {
+ return nil, nil
+ }
+
+ lines := strings.Split(stdout, "\n")
+
+ result := make(map[string]string, len(lines))
+
+ for _, line := range lines {
+ if strings.TrimSpace(line) == "" {
+ continue
+ }
+
+ parts := strings.Fields(line)
+ if len(parts) != 2 {
+ return nil, fmt.Errorf("bad git config: %s", line)
+ }
+
+ result[parts[0]] = parts[1]
+ }
+
+ return result, nil
+}
+
+func (gc *gitConfig) ReadString(key string) (string, error) {
+ stdout, err := gc.repo.runGitCommand("config", gc.localityFlag, "--get-all", key)
+
+ // / \
+ // / ! \
+ // -------
+ //
+ // There can be a legitimate error here, but I see no portable way to
+ // distinguish them from the git error that say "no matching value exist"
+ if err != nil {
+ return "", ErrNoConfigEntry
+ }
+
+ lines := strings.Split(stdout, "\n")
+
+ if len(lines) == 0 {
+ return "", ErrNoConfigEntry
+ }
+ if len(lines) > 1 {
+ return "", ErrMultipleConfigEntry
+ }
+
+ return lines[0], nil
+}
+
+func (gc *gitConfig) ReadBool(key string) (bool, error) {
+ val, err := gc.ReadString(key)
+ if err != nil {
+ return false, err
+ }
+
+ return strconv.ParseBool(val)
+}
+
+func (gc *gitConfig) ReadTimestamp(key string) (*time.Time, error) {
+ value, err := gc.ReadString(key)
+ if err != nil {
+ return nil, err
+ }
+ return parseTimestamp(value)
+}
+
+func (gc *gitConfig) rmSection(keyPrefix string) error {
+ _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--remove-section", keyPrefix)
+ return err
+}
+
+func (gc *gitConfig) unsetAll(keyPrefix string) error {
+ _, err := gc.repo.runGitCommand("config", gc.localityFlag, "--unset-all", keyPrefix)
+ return err
+}
+
+// return keyPrefix section
+// example: sectionFromKey(a.b.c.d) return a.b.c
+func sectionFromKey(keyPrefix string) string {
+ s := strings.Split(keyPrefix, ".")
+ if len(s) == 1 {
+ return keyPrefix
+ }
+
+ return strings.Join(s[:len(s)-1], ".")
+}
+
+// rmConfigs with git version lesser than 2.18
+func (gc *gitConfig) rmConfigsGitVersionLT218(keyPrefix string) error {
+ // try to remove key/value pair by key
+ err := gc.unsetAll(keyPrefix)
+ if err != nil {
+ return gc.rmSection(keyPrefix)
+ }
+
+ m, err := gc.ReadAll(sectionFromKey(keyPrefix))
+ if err != nil {
+ return err
+ }
+
+ // if section doesn't have any left key/value remove the section
+ if len(m) == 0 {
+ return gc.rmSection(sectionFromKey(keyPrefix))
+ }
+
+ return nil
+}
+
+// RmConfigs remove all key/value pair matching the key prefix
+func (gc *gitConfig) RemoveAll(keyPrefix string) error {
+ // starting from git 2.18.0 sections are automatically deleted when the last existing
+ // key/value is removed. Before 2.18.0 we should remove the section
+ // see https://github.com/git/git/blob/master/Documentation/RelNotes/2.18.0.txt#L379
+ lt218, err := gc.gitVersionLT218()
+ if err != nil {
+ return errors.Wrap(err, "getting git version")
+ }
+
+ if lt218 {
+ return gc.rmConfigsGitVersionLT218(keyPrefix)
+ }
+
+ err = gc.unsetAll(keyPrefix)
+ if err != nil {
+ return gc.rmSection(keyPrefix)
+ }
+
+ return nil
+}
+
+func (gc *gitConfig) gitVersion() (*semver.Version, error) {
+ versionOut, err := gc.repo.runGitCommand("version")
+ if err != nil {
+ return nil, err
+ }
+ return parseGitVersion(versionOut)
+}
+
+func parseGitVersion(versionOut string) (*semver.Version, error) {
+ // extract the version and truncate potential bad parts
+ // ex: 2.23.0.rc1 instead of 2.23.0-rc1
+ r := regexp.MustCompile(`(\d+\.){1,2}\d+`)
+
+ extracted := r.FindString(versionOut)
+ if extracted == "" {
+ return nil, fmt.Errorf("unreadable git version %s", versionOut)
+ }
+
+ version, err := semver.Make(extracted)
+ if err != nil {
+ return nil, err
+ }
+
+ return &version, nil
+}
+
+func (gc *gitConfig) gitVersionLT218() (bool, error) {
+ version, err := gc.gitVersion()
+ if err != nil {
+ return false, err
+ }
+
+ version218string := "2.18.0"
+ gitVersion218, err := semver.Make(version218string)
+ if err != nil {
+ return false, err
+ }
+
+ return version.LT(gitVersion218), nil
+}
diff --git a/repository/config_mem.go b/repository/config_mem.go
new file mode 100644
index 00000000..e2cffd9c
--- /dev/null
+++ b/repository/config_mem.go
@@ -0,0 +1,84 @@
+package repository
+
+import (
+ "strconv"
+ "strings"
+ "time"
+)
+
+var _ Config = &memConfig{}
+
+type memConfig struct {
+ config map[string]string
+}
+
+func newMemConfig(config map[string]string) *memConfig {
+ return &memConfig{config: config}
+}
+
+func (mc *memConfig) StoreString(key, value string) error {
+ mc.config[key] = value
+ return nil
+}
+
+func (mc *memConfig) StoreBool(key string, value bool) error {
+ return mc.StoreString(key, strconv.FormatBool(value))
+}
+
+func (mc *memConfig) StoreTimestamp(key string, value time.Time) error {
+ return mc.StoreString(key, strconv.Itoa(int(value.Unix())))
+}
+
+func (mc *memConfig) ReadAll(keyPrefix string) (map[string]string, error) {
+ result := make(map[string]string)
+ for key, val := range mc.config {
+ if strings.HasPrefix(key, keyPrefix) {
+ result[key] = val
+ }
+ }
+ return result, nil
+}
+
+func (mc *memConfig) ReadString(key string) (string, error) {
+ // unlike git, the mock can only store one value for the same key
+ val, ok := mc.config[key]
+ if !ok {
+ return "", ErrNoConfigEntry
+ }
+
+ return val, nil
+}
+
+func (mc *memConfig) ReadBool(key string) (bool, error) {
+ // unlike git, the mock can only store one value for the same key
+ val, ok := mc.config[key]
+ if !ok {
+ return false, ErrNoConfigEntry
+ }
+
+ return strconv.ParseBool(val)
+}
+
+func (mc *memConfig) ReadTimestamp(key string) (*time.Time, error) {
+ value, err := mc.ReadString(key)
+ if err != nil {
+ return nil, err
+ }
+ timestamp, err := strconv.Atoi(value)
+ if err != nil {
+ return nil, err
+ }
+
+ t := time.Unix(int64(timestamp), 0)
+ return &t, nil
+}
+
+// RmConfigs remove all key/value pair matching the key prefix
+func (mc *memConfig) RemoveAll(keyPrefix string) error {
+ for key := range mc.config {
+ if strings.HasPrefix(key, keyPrefix) {
+ delete(mc.config, key)
+ }
+ }
+ return nil
+}
diff --git a/repository/git.go b/repository/git.go
index 93016cd9..2b00d1f2 100644
--- a/repository/git.go
+++ b/repository/git.go
@@ -7,11 +7,8 @@ import (
"io"
"os/exec"
"path"
- "regexp"
- "strconv"
"strings"
- "github.com/blang/semver"
"github.com/pkg/errors"
"github.com/MichaelMure/git-bug/util/git"
@@ -33,16 +30,26 @@ type GitRepo struct {
editClock *lamport.Persisted
}
+// LocalConfig give access to the repository scoped configuration
+func (repo *GitRepo) LocalConfig() Config {
+ return newGitConfig(repo, false)
+}
+
+// GlobalConfig give access to the git global configuration
+func (repo *GitRepo) GlobalConfig() Config {
+ return newGitConfig(repo, true)
+}
+
// Run the given git command with the given I/O reader/writers, returning an error if it fails.
func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error {
- repopath:=repo.Path
- if repopath==".git" {
+ repopath := repo.Path
+ if repopath == ".git" {
// seeduvax> trangely the git command sometimes fail for very unknown
// reason wihtout this replacement.
// observed with rev-list command when git-bug is called from git
// hook script, even the same command with same args runs perfectly
- // when called directly from the same hook script.
- repopath=""
+ // when called directly from the same hook script.
+ repopath = ""
}
// fmt.Printf("[%s] Running git %s\n", repopath, strings.Join(args, " "))
@@ -125,7 +132,7 @@ func NewGitRepo(path string, witnesser Witnesser) (*GitRepo, error) {
// InitGitRepo create a new empty git repo at the given path
func InitGitRepo(path string) (*GitRepo, error) {
- repo := &GitRepo{Path: path+"/.git"}
+ repo := &GitRepo{Path: path + "/.git"}
err := repo.createClocks()
if err != nil {
return nil, err
@@ -197,174 +204,6 @@ func (repo *GitRepo) GetRemotes() (map[string]string, error) {
return remotes, nil
}
-// StoreConfig store a single key/value pair in the config of the repo
-func (repo *GitRepo) StoreConfig(key string, value string) error {
- _, err := repo.runGitCommand("config", "--replace-all", key, value)
-
- return err
-}
-
-// ReadConfigs read all key/value pair matching the key prefix
-func (repo *GitRepo) ReadConfigs(keyPrefix string) (map[string]string, error) {
- stdout, err := repo.runGitCommand("config", "--get-regexp", keyPrefix)
-
- // / \
- // / ! \
- // -------
- //
- // There can be a legitimate error here, but I see no portable way to
- // distinguish them from the git error that say "no matching value exist"
- if err != nil {
- return nil, nil
- }
-
- lines := strings.Split(stdout, "\n")
-
- result := make(map[string]string, len(lines))
-
- for _, line := range lines {
- if strings.TrimSpace(line) == "" {
- continue
- }
-
- parts := strings.Fields(line)
- if len(parts) != 2 {
- return nil, fmt.Errorf("bad git config: %s", line)
- }
-
- result[parts[0]] = parts[1]
- }
-
- return result, nil
-}
-
-func (repo *GitRepo) ReadConfigBool(key string) (bool, error) {
- val, err := repo.ReadConfigString(key)
- if err != nil {
- return false, err
- }
-
- return strconv.ParseBool(val)
-}
-
-func (repo *GitRepo) ReadConfigString(key string) (string, error) {
- stdout, err := repo.runGitCommand("config", "--get-all", key)
-
- // / \
- // / ! \
- // -------
- //
- // There can be a legitimate error here, but I see no portable way to
- // distinguish them from the git error that say "no matching value exist"
- if err != nil {
- return "", ErrNoConfigEntry
- }
-
- lines := strings.Split(stdout, "\n")
-
- if len(lines) == 0 {
- return "", ErrNoConfigEntry
- }
- if len(lines) > 1 {
- return "", ErrMultipleConfigEntry
- }
-
- return lines[0], nil
-}
-
-func (repo *GitRepo) rmSection(keyPrefix string) error {
- _, err := repo.runGitCommand("config", "--remove-section", keyPrefix)
- return err
-}
-
-func (repo *GitRepo) unsetAll(keyPrefix string) error {
- _, err := repo.runGitCommand("config", "--unset-all", keyPrefix)
- return err
-}
-
-// return keyPrefix section
-// example: sectionFromKey(a.b.c.d) return a.b.c
-func sectionFromKey(keyPrefix string) string {
- s := strings.Split(keyPrefix, ".")
- if len(s) == 1 {
- return keyPrefix
- }
-
- return strings.Join(s[:len(s)-1], ".")
-}
-
-// rmConfigs with git version lesser than 2.18
-func (repo *GitRepo) rmConfigsGitVersionLT218(keyPrefix string) error {
- // try to remove key/value pair by key
- err := repo.unsetAll(keyPrefix)
- if err != nil {
- return repo.rmSection(keyPrefix)
- }
-
- m, err := repo.ReadConfigs(sectionFromKey(keyPrefix))
- if err != nil {
- return err
- }
-
- // if section doesn't have any left key/value remove the section
- if len(m) == 0 {
- return repo.rmSection(sectionFromKey(keyPrefix))
- }
-
- return nil
-}
-
-// RmConfigs remove all key/value pair matching the key prefix
-func (repo *GitRepo) RmConfigs(keyPrefix string) error {
- // starting from git 2.18.0 sections are automatically deleted when the last existing
- // key/value is removed. Before 2.18.0 we should remove the section
- // see https://github.com/git/git/blob/master/Documentation/RelNotes/2.18.0.txt#L379
- lt218, err := repo.gitVersionLT218()
- if err != nil {
- return errors.Wrap(err, "getting git version")
- }
-
- if lt218 {
- return repo.rmConfigsGitVersionLT218(keyPrefix)
- }
-
- err = repo.unsetAll(keyPrefix)
- if err != nil {
- return repo.rmSection(keyPrefix)
- }
-
- return nil
-}
-
-func (repo *GitRepo) gitVersionLT218() (bool, error) {
- versionOut, err := repo.runGitCommand("version")
- if err != nil {
- return false, err
- }
-
- // extract the version and truncate potential bad parts
- // ex: 2.23.0.rc1 instead of 2.23.0-rc1
- r := regexp.MustCompile(`(\d+\.){1,2}\d+`)
-
- extracted := r.FindString(versionOut)
- if extracted == "" {
- return false, fmt.Errorf("unreadable git version %s", versionOut)
- }
-
- version, err := semver.Make(extracted)
- if err != nil {
- return false, err
- }
-
- version218string := "2.18.0"
- gitVersion218, err := semver.Make(version218string)
- if err != nil {
- return false, err
- }
-
- return version.LT(gitVersion218), nil
-}
-
// FetchRefs fetch git refs from a remote
func (repo *GitRepo) FetchRefs(remote, refSpec string) (string, error) {
stdout, err := repo.runGitCommand("fetch", remote, refSpec)
diff --git a/repository/git_test.go b/repository/git_test.go
index 20bf6ec3..de442e39 100644
--- a/repository/git_test.go
+++ b/repository/git_test.go
@@ -11,56 +11,57 @@ func TestConfig(t *testing.T) {
repo := CreateTestRepo(false)
defer CleanupTestRepos(t, repo)
- err := repo.StoreConfig("section.key", "value")
+ err := repo.LocalConfig().StoreString("section.key", "value")
assert.NoError(t, err)
- val, err := repo.ReadConfigString("section.key")
+ val, err := repo.LocalConfig().ReadString("section.key")
+ assert.NoError(t, err)
assert.Equal(t, "value", val)
- err = repo.StoreConfig("section.true", "true")
+ err = repo.LocalConfig().StoreString("section.true", "true")
assert.NoError(t, err)
- val2, err := repo.ReadConfigBool("section.true")
+ val2, err := repo.LocalConfig().ReadBool("section.true")
+ assert.NoError(t, err)
assert.Equal(t, true, val2)
- configs, err := repo.ReadConfigs("section")
+ configs, err := repo.LocalConfig().ReadAll("section")
assert.NoError(t, err)
assert.Equal(t, configs, map[string]string{
"section.key": "value",
"section.true": "true",
})
- err = repo.RmConfigs("section.true")
+ err = repo.LocalConfig().RemoveAll("section.true")
assert.NoError(t, err)
- configs, err = repo.ReadConfigs("section")
+ configs, err = repo.LocalConfig().ReadAll("section")
assert.NoError(t, err)
assert.Equal(t, configs, map[string]string{
"section.key": "value",
})
- _, err = repo.ReadConfigBool("section.true")
+ _, err = repo.LocalConfig().ReadBool("section.true")
assert.Equal(t, ErrNoConfigEntry, err)
- err = repo.RmConfigs("section.nonexistingkey")
+ err = repo.LocalConfig().RemoveAll("section.nonexistingkey")
assert.Error(t, err)
- err = repo.RmConfigs("section.key")
+ err = repo.LocalConfig().RemoveAll("section.key")
assert.NoError(t, err)
- _, err = repo.ReadConfigString("section.key")
+ _, err = repo.LocalConfig().ReadString("section.key")
assert.Equal(t, ErrNoConfigEntry, err)
- err = repo.RmConfigs("nonexistingsection")
+ err = repo.LocalConfig().RemoveAll("nonexistingsection")
assert.Error(t, err)
- err = repo.RmConfigs("section")
+ err = repo.LocalConfig().RemoveAll("section")
assert.Error(t, err)
- _, err = repo.ReadConfigString("section.key")
+ _, err = repo.LocalConfig().ReadString("section.key")
assert.Error(t, err)
- err = repo.RmConfigs("section.key")
+ err = repo.LocalConfig().RemoveAll("section.key")
assert.Error(t, err)
-
}
diff --git a/repository/git_testing.go b/repository/git_testing.go
index 36c53c1d..37a15d93 100644
--- a/repository/git_testing.go
+++ b/repository/git_testing.go
@@ -31,10 +31,11 @@ func CreateTestRepo(bare bool) *GitRepo {
log.Fatal(err)
}
- if err := repo.StoreConfig("user.name", "testuser"); err != nil {
+ config := repo.LocalConfig()
+ if err := config.StoreString("user.name", "testuser"); err != nil {
log.Fatal("failed to set user.name for test repository: ", err)
}
- if err := repo.StoreConfig("user.email", "testuser@example.com"); err != nil {
+ if err := config.StoreString("user.email", "testuser@example.com"); err != nil {
log.Fatal("failed to set user.email for test repository: ", err)
}
diff --git a/repository/mock_repo.go b/repository/mock_repo.go
index 23534b89..26c02ede 100644
--- a/repository/mock_repo.go
+++ b/repository/mock_repo.go
@@ -3,8 +3,6 @@ package repository
import (
"crypto/sha1"
"fmt"
- "strconv"
- "strings"
"github.com/MichaelMure/git-bug/util/git"
"github.com/MichaelMure/git-bug/util/lamport"
@@ -14,13 +12,14 @@ var _ ClockedRepo = &mockRepoForTest{}
// mockRepoForTest defines an instance of Repo that can be used for testing.
type mockRepoForTest struct {
- config map[string]string
- blobs map[git.Hash][]byte
- trees map[git.Hash]string
- commits map[git.Hash]commit
- refs map[string]git.Hash
- createClock lamport.Clock
- editClock lamport.Clock
+ config map[string]string
+ globalConfig map[string]string
+ blobs map[git.Hash][]byte
+ trees map[git.Hash]string
+ commits map[git.Hash]commit
+ refs map[string]git.Hash
+ createClock lamport.Clock
+ editClock lamport.Clock
}
type commit struct {
@@ -40,6 +39,16 @@ func NewMockRepoForTest() *mockRepoForTest {
}
}
+// LocalConfig give access to the repository scoped configuration
+func (r *mockRepoForTest) LocalConfig() Config {
+ return newMemConfig(r.config)
+}
+
+// GlobalConfig give access to the git global configuration
+func (r *mockRepoForTest) GlobalConfig() Config {
+ return newMemConfig(r.globalConfig)
+}
+
// GetPath returns the path to the repo.
func (r *mockRepoForTest) GetPath() string {
return "~/mockRepo/"
@@ -66,53 +75,6 @@ func (r *mockRepoForTest) GetRemotes() (map[string]string, error) {
}, nil
}
-func (r *mockRepoForTest) StoreConfig(key string, value string) error {
- r.config[key] = value
- return nil
-}
-
-func (r *mockRepoForTest) ReadConfigs(keyPrefix string) (map[string]string, error) {
- result := make(map[string]string)
-
- for key, val := range r.config {
- if strings.HasPrefix(key, keyPrefix) {
- result[key] = val
- }
- }
-
- return result, nil
-}
-
-func (r *mockRepoForTest) ReadConfigBool(key string) (bool, error) {
- // unlike git, the mock can only store one value for the same key
- val, ok := r.config[key]
- if !ok {
- return false, ErrNoConfigEntry
- }
-
- return strconv.ParseBool(val)
-}
-
-func (r *mockRepoForTest) ReadConfigString(key string) (string, error) {
- // unlike git, the mock can only store one value for the same key
- val, ok := r.config[key]
- if !ok {
- return "", ErrNoConfigEntry
- }
-
- return val, nil
-}
-
-// RmConfigs remove all key/value pair matching the key prefix
-func (r *mockRepoForTest) RmConfigs(keyPrefix string) error {
- for key := range r.config {
- if strings.HasPrefix(key, keyPrefix) {
- delete(r.config, key)
- }
- }
- return nil
-}
-
// PushRefs push git refs to a remote
func (r *mockRepoForTest) PushRefs(remote string, refSpec string) (string, error) {
return "", nil
diff --git a/repository/repo.go b/repository/repo.go
index 44204493..7d655bde 100644
--- a/repository/repo.go
+++ b/repository/repo.go
@@ -30,24 +30,11 @@ type RepoCommon interface {
// GetRemotes returns the configured remotes repositories.
GetRemotes() (map[string]string, error)
- // StoreConfig store a single key/value pair in the config of the repo
- StoreConfig(key string, value string) error
+ // LocalConfig give access to the repository scoped configuration
+ LocalConfig() Config
- // ReadConfigs read all key/value pair matching the key prefix
- ReadConfigs(keyPrefix string) (map[string]string, error)
-
- // ReadConfigBool read a single boolean value from the config
- // Return ErrNoConfigEntry or ErrMultipleConfigEntry if there is zero or more than one entry
- // for this key
- ReadConfigBool(key string) (bool, error)
-
- // ReadConfigBool read a single string value from the config
- // Return ErrNoConfigEntry or ErrMultipleConfigEntry if there is zero or more than one entry
- // for this key
- ReadConfigString(key string) (string, error)
-
- // RmConfigs remove all key/value pair matching the key prefix
- RmConfigs(keyPrefix string) error
+ // GlobalConfig give access to the git global configuration
+ GlobalConfig() Config
}
// Repo represents a source code repository.
diff --git a/termui/bug_table.go b/termui/bug_table.go
index 8d69d665..c432c94a 100644
--- a/termui/bug_table.go
+++ b/termui/bug_table.go
@@ -3,14 +3,16 @@ package termui
import (
"bytes"
"fmt"
+ "strings"
"time"
+ "github.com/MichaelMure/go-term-text"
+ "github.com/MichaelMure/gocui"
+ "github.com/dustin/go-humanize"
+
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/util/colors"
- "github.com/MichaelMure/git-bug/util/text"
- "github.com/MichaelMure/gocui"
- "github.com/dustin/go-humanize"
)
const bugTableView = "bugTableView"
@@ -275,8 +277,8 @@ func (bt *bugTable) getColumnWidths(maxX int) map[string]int {
left := maxX - 5 - m["id"] - m["status"]
- m["summary"] = 10
- left -= m["summary"]
+ m["comments"] = 10
+ left -= m["comments"]
m["lastEdit"] = 19
left -= m["lastEdit"]
@@ -290,10 +292,21 @@ func (bt *bugTable) render(v *gocui.View, maxX int) {
columnWidths := bt.getColumnWidths(maxX)
for _, excerpt := range bt.excerpts {
- summaryTxt := fmt.Sprintf("C:%-2d L:%-2d",
- excerpt.LenComments,
- len(excerpt.Labels),
- )
+ summaryTxt := fmt.Sprintf("%4d 💬", excerpt.LenComments)
+ if excerpt.LenComments <= 0 {
+ summaryTxt = ""
+ }
+ if excerpt.LenComments > 9999 {
+ summaryTxt = " ∞ 💬"
+ }
+
+ var labelsTxt strings.Builder
+ for _, l := range excerpt.Labels {
+ lc256 := l.Color().Term256()
+ labelsTxt.WriteString(lc256.Escape())
+ labelsTxt.WriteString(" ◼")
+ labelsTxt.WriteString(lc256.Unescape())
+ }
var authorDisplayName string
if excerpt.AuthorId != "" {
@@ -310,17 +323,19 @@ func (bt *bugTable) render(v *gocui.View, maxX int) {
id := text.LeftPadMaxLine(excerpt.Id.Human(), columnWidths["id"], 1)
status := text.LeftPadMaxLine(excerpt.Status.String(), columnWidths["status"], 1)
- title := text.LeftPadMaxLine(excerpt.Title, columnWidths["title"], 1)
+ labels := text.TruncateMax(labelsTxt.String(), minInt(columnWidths["title"]-2, 10))
+ title := text.LeftPadMaxLine(excerpt.Title, columnWidths["title"]-text.Len(labels), 1)
author := text.LeftPadMaxLine(authorDisplayName, columnWidths["author"], 1)
- summary := text.LeftPadMaxLine(summaryTxt, columnWidths["summary"], 1)
+ comments := text.LeftPadMaxLine(summaryTxt, columnWidths["comments"], 1)
lastEdit := text.LeftPadMaxLine(humanize.Time(lastEditTime), columnWidths["lastEdit"], 1)
- _, _ = fmt.Fprintf(v, "%s %s %s %s %s %s\n",
+ _, _ = fmt.Fprintf(v, "%s %s %s%s %s %s %s\n",
colors.Cyan(id),
colors.Yellow(status),
title,
+ labels,
colors.Magenta(author),
- summary,
+ comments,
lastEdit,
)
}
@@ -333,12 +348,11 @@ func (bt *bugTable) renderHeader(v *gocui.View, maxX int) {
status := text.LeftPadMaxLine("STATUS", columnWidths["status"], 1)
title := text.LeftPadMaxLine("TITLE", columnWidths["title"], 1)
author := text.LeftPadMaxLine("AUTHOR", columnWidths["author"], 1)
- summary := text.LeftPadMaxLine("SUMMARY", columnWidths["summary"], 1)
+ comments := text.LeftPadMaxLine("COMMENTS", columnWidths["comments"], 1)
lastEdit := text.LeftPadMaxLine("LAST EDIT", columnWidths["lastEdit"], 1)
_, _ = fmt.Fprintf(v, "\n")
- _, _ = fmt.Fprintf(v, "%s %s %s %s %s %s\n", id, status, title, author, summary, lastEdit)
-
+ _, _ = fmt.Fprintf(v, "%s %s %s %s %s %s\n", id, status, title, author, comments, lastEdit)
}
func (bt *bugTable) renderFooter(v *gocui.View, maxX int) {
diff --git a/termui/label_select.go b/termui/label_select.go
index 131703f9..39edbdb1 100644
--- a/termui/label_select.go
+++ b/termui/label_select.go
@@ -4,9 +4,10 @@ import (
"fmt"
"strings"
+ "github.com/MichaelMure/gocui"
+
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
- "github.com/MichaelMure/gocui"
)
const labelSelectView = "labelSelectView"
@@ -127,7 +128,12 @@ func (ls *labelSelect) layout(g *gocui.Gui) error {
if ls.labelSelect[i] {
selectBox = " [x] "
}
- fmt.Fprint(v, selectBox, label)
+
+ lc := label.Color()
+ lc256 := lc.Term256()
+ labelStr := lc256.Escape() + "◼ " + lc256.Unescape() + label.String()
+ fmt.Fprint(v, selectBox, labelStr)
+
y0 += 2
}
diff --git a/termui/msg_popup.go b/termui/msg_popup.go
index 4452427e..99180c99 100644
--- a/termui/msg_popup.go
+++ b/termui/msg_popup.go
@@ -3,7 +3,7 @@ package termui
import (
"fmt"
- "github.com/MichaelMure/git-bug/util/text"
+ "github.com/MichaelMure/go-term-text"
"github.com/MichaelMure/gocui"
)
diff --git a/termui/show_bug.go b/termui/show_bug.go
index 228b85b0..50478b8f 100644
--- a/termui/show_bug.go
+++ b/termui/show_bug.go
@@ -5,12 +5,13 @@ import (
"fmt"
"strings"
+ "github.com/MichaelMure/go-term-text"
+ "github.com/MichaelMure/gocui"
+
"github.com/MichaelMure/git-bug/bug"
"github.com/MichaelMure/git-bug/cache"
"github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/util/colors"
- "github.com/MichaelMure/git-bug/util/text"
- "github.com/MichaelMure/gocui"
)
const showBugView = "showBugView"
@@ -429,13 +430,15 @@ func (sb *showBug) renderSidebar(g *gocui.Gui, sideView *gocui.View) error {
labelStr := make([]string, len(snap.Labels))
for i, l := range snap.Labels {
- labelStr[i] = string(l)
+ lc := l.Color()
+ lc256 := lc.Term256()
+ labelStr[i] = lc256.Escape() + "◼ " + lc256.Unescape() + l.String()
}
labels := strings.Join(labelStr, "\n")
labels, lines := text.WrapLeftPadded(labels, maxX, 2)
- content := fmt.Sprintf("%s\n\n%s", colors.Bold("Labels"), labels)
+ content := fmt.Sprintf("%s\n\n%s", colors.Bold(" Labels"), labels)
v, err := sb.createSideView(g, "sideLabels", x0, y0, maxX, lines+2)
if err != nil {
diff --git a/termui/termui.go b/termui/termui.go
index 5d3bb0c1..8aece020 100644
--- a/termui/termui.go
+++ b/termui/termui.go
@@ -66,11 +66,12 @@ func Run(cache *cache.RepoCache) error {
return err
}
+
return nil
}
func initGui(action func(ui *termUI) error) {
- g, err := gocui.NewGui(gocui.OutputNormal)
+ g, err := gocui.NewGui(gocui.Output256)
if err != nil {
ui.gError <- err
diff --git a/util/text/left_padded.go b/util/text/left_padded.go
deleted file mode 100644
index eae65d34..00000000
--- a/util/text/left_padded.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package text
-
-import (
- "bytes"
- "fmt"
- "github.com/mattn/go-runewidth"
- "strings"
-)
-
-// LeftPadMaxLine pads a string on the left by a specified amount and pads the
-// string on the right to fill the maxLength
-func LeftPadMaxLine(text string, length, leftPad int) string {
- var rightPart string = text
-
- scrWidth := runewidth.StringWidth(text)
- // truncate and ellipse if needed
- if scrWidth+leftPad > length {
- rightPart = runewidth.Truncate(text, length-leftPad, "…")
- } else if scrWidth+leftPad < length {
- rightPart = runewidth.FillRight(text, length-leftPad)
- }
-
- return fmt.Sprintf("%s%s",
- strings.Repeat(" ", leftPad),
- rightPart,
- )
-}
-
-// LeftPad left pad each line of the given text
-func LeftPad(text string, leftPad int) string {
- var result bytes.Buffer
-
- pad := strings.Repeat(" ", leftPad)
-
- for _, line := range strings.Split(text, "\n") {
- result.WriteString(pad)
- result.WriteString(line)
- result.WriteString("\n")
- }
-
- return result.String()
-}
diff --git a/util/text/left_padded_test.go b/util/text/left_padded_test.go
deleted file mode 100644
index 0be79e32..00000000
--- a/util/text/left_padded_test.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package text
-
-import "testing"
-
-func TestLeftPadMaxLine(t *testing.T) {
- cases := []struct {
- input, output string
- maxValueLength int
- leftPad int
- }{
- {
- "foo",
- "foo ",
- 4,
- 0,
- },
- {
- "foofoofoo",
- "foo…",
- 4,
- 0,
- },
- {
- "foo",
- "foo ",
- 10,
- 0,
- },
- {
- "foo",
- " f…",
- 4,
- 2,
- },
- {
- "foofoofoo",
- " foo…",
- 6,
- 2,
- },
- {
- "foo",
- " foo ",
- 10,
- 2,
- },
- }
-
- for i, tc := range cases {
- result := LeftPadMaxLine(tc.input, tc.maxValueLength, tc.leftPad)
- if result != tc.output {
- t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%s`\n\nActual Output:\n\n`%s`",
- i, tc.input, tc.output, result)
- }
- }
-}
diff --git a/util/text/text.go b/util/text/text.go
deleted file mode 100644
index 39584d5d..00000000
--- a/util/text/text.go
+++ /dev/null
@@ -1,330 +0,0 @@
-package text
-
-import (
- "github.com/mattn/go-runewidth"
- "strings"
- "unicode/utf8"
-)
-
-// Force runewidth not to treat ambiguous runes as wide chars, so that things
-// like unicode ellipsis/up/down/left/right glyphs can have correct runewidth
-// and can be displayed correctly in terminals.
-func init() {
- runewidth.DefaultCondition.EastAsianWidth = false
-}
-
-// Wrap a text for an exact line size
-// Handle properly terminal color escape code
-func Wrap(text string, lineWidth int) (string, int) {
- return WrapLeftPadded(text, lineWidth, 0)
-}
-
-// Wrap a text for an exact line size with a left padding
-// Handle properly terminal color escape code
-func WrapLeftPadded(text string, lineWidth int, leftPad int) (string, int) {
- var lines []string
- nbLine := 0
- pad := strings.Repeat(" ", leftPad)
-
- // tabs are formatted as 4 spaces
- text = strings.Replace(text, "\t", " ", -1)
- // NOTE: text is first segmented into lines so that softwrapLine can handle.
- for _, line := range strings.Split(text, "\n") {
- if line == "" || strings.TrimSpace(line) == "" {
- lines = append(lines, "")
- nbLine++
- } else {
- wrapped := softwrapLine(line, lineWidth-leftPad)
- firstLine := true
- for _, seg := range strings.Split(wrapped, "\n") {
- if firstLine {
- lines = append(lines, pad+strings.TrimRight(seg, " "))
- firstLine = false
- } else {
- lines = append(lines, pad+strings.TrimSpace(seg))
- }
- nbLine++
- }
- }
- }
- return strings.Join(lines, "\n"), nbLine
-}
-
-// Break a line into several lines so that each line consumes at most
-// 'textWidth' cells. Lines break at groups of white spaces and multibyte
-// chars. Nothing is removed from the original text so that it behaves like a
-// softwrap.
-//
-// Required: The line shall not contain '\n'
-//
-// WRAPPING ALGORITHM: The line is broken into non-breakable chunks, then line
-// breaks ("\n") are inserted between these groups so that the total length
-// between breaks does not exceed the required width. Words that are longer than
-// the textWidth are broken into pieces no longer than textWidth.
-//
-func softwrapLine(line string, textWidth int) string {
- // NOTE: terminal escapes are stripped out of the line so the algorithm is
- // simpler. Do not try to mix them in the wrapping algorithm, as it can get
- // complicated quickly.
- line1, termEscapes := extractTermEscapes(line)
-
- chunks := segmentLine(line1)
- // Reverse the chunk array so we can use it as a stack.
- for i, j := 0, len(chunks)-1; i < j; i, j = i+1, j-1 {
- chunks[i], chunks[j] = chunks[j], chunks[i]
- }
- var line2 string = ""
- var width int = 0
- for len(chunks) > 0 {
- thisWord := chunks[len(chunks)-1]
- wl := wordLen(thisWord)
- if width+wl <= textWidth {
- line2 += chunks[len(chunks)-1]
- chunks = chunks[:len(chunks)-1]
- width += wl
- if width == textWidth && len(chunks) > 0 {
- // NOTE: new line begins when current line is full and there are more
- // chunks to come.
- line2 += "\n"
- width = 0
- }
- } else if wl > textWidth {
- // NOTE: By default, long words are splited to fill the remaining space.
- // But if the long words is the first non-space word in the middle of the
- // line, preceeding spaces shall not be counted in word spliting.
- splitWidth := textWidth - width
- if strings.HasSuffix(line2, "\n"+strings.Repeat(" ", width)) {
- splitWidth += width
- }
- left, right := splitWord(chunks[len(chunks)-1], splitWidth)
- chunks[len(chunks)-1] = right
- line2 += left + "\n"
- width = 0
- } else {
- line2 += "\n"
- width = 0
- }
- }
-
- line3 := applyTermEscapes(line2, termEscapes)
- return line3
-}
-
-// EscapeItem: Storage of terminal escapes in a line. 'item' is the actural
-// escape command, and 'pos' is the index in the rune array where the 'item'
-// shall be inserted back. For example, the escape item in "F\x1b33mox" is
-// {"\x1b33m", 1}.
-type escapeItem struct {
- item string
- pos int
-}
-
-// Extract terminal escapes out of a line, returns a new line without terminal
-// escapes and a slice of escape items. The terminal escapes can be inserted
-// back into the new line at rune index 'item.pos' to recover the original line.
-//
-// Required: The line shall not contain "\n"
-//
-func extractTermEscapes(line string) (string, []escapeItem) {
- var termEscapes []escapeItem
- var line1 string
-
- pos := 0
- item := ""
- occupiedRuneCount := 0
- inEscape := false
- for i, r := range []rune(line) {
- if r == '\x1b' {
- pos = i
- item = string(r)
- inEscape = true
- continue
- }
- if inEscape {
- item += string(r)
- if r == 'm' {
- termEscapes = append(termEscapes, escapeItem{item, pos - occupiedRuneCount})
- occupiedRuneCount += utf8.RuneCountInString(item)
- inEscape = false
- }
- continue
- }
- line1 += string(r)
- }
-
- return line1, termEscapes
-}
-
-// Apply the extracted terminal escapes to the edited line. The only edit
-// allowed is to insert "\n" like that in softwrapLine. Callers shall ensure
-// this since this function is not able to check it.
-func applyTermEscapes(line string, escapes []escapeItem) string {
- if len(escapes) == 0 {
- return line
- }
-
- var out string = ""
-
- currPos := 0
- currItem := 0
- for _, r := range line {
- if currItem < len(escapes) && currPos == escapes[currItem].pos {
- // NOTE: We avoid terminal escapes at the end of a line by move them one
- // pass the end of line, so that algorithms who trim right spaces are
- // happy. But algorithms who trim left spaces are still unhappy.
- if r == '\n' {
- out += "\n" + escapes[currItem].item
- } else {
- out += escapes[currItem].item + string(r)
- currPos++
- }
- currItem++
- } else {
- if r != '\n' {
- currPos++
- }
- out += string(r)
- }
- }
-
- // Don't forget the trailing escape, if any.
- if currItem == len(escapes)-1 && currPos == escapes[currItem].pos {
- out += escapes[currItem].item
- }
-
- return out
-}
-
-// Segment a line into chunks, where each chunk consists of chars with the same
-// type and is not breakable.
-func segmentLine(s string) []string {
- var chunks []string
-
- var word string
- wordType := none
- flushWord := func() {
- chunks = append(chunks, word)
- word = ""
- wordType = none
- }
-
- for _, r := range s {
- // A WIDE_CHAR itself constitutes a chunk.
- thisType := runeType(r)
- if thisType == wideChar {
- if wordType != none {
- flushWord()
- }
- chunks = append(chunks, string(r))
- continue
- }
- // Other type of chunks starts with a char of that type, and ends with a
- // char with different type or end of string.
- if thisType != wordType {
- if wordType != none {
- flushWord()
- }
- word = string(r)
- wordType = thisType
- } else {
- word += string(r)
- }
- }
- if word != "" {
- flushWord()
- }
-
- return chunks
-}
-
-// Rune categories
-//
-// These categories are so defined that each category forms a non-breakable
-// chunk. It IS NOT the same as unicode code point categories.
-//
-const (
- none int = iota
- wideChar
- invisible
- shortUnicode
- space
- visibleAscii
-)
-
-// Determine the category of a rune.
-func runeType(r rune) int {
- rw := runewidth.RuneWidth(r)
- if rw > 1 {
- return wideChar
- } else if rw == 0 {
- return invisible
- } else if r > 127 {
- return shortUnicode
- } else if r == ' ' {
- return space
- } else {
- return visibleAscii
- }
-}
-
-// wordLen return the length of a word, while ignoring the terminal escape
-// sequences
-func wordLen(word string) int {
- length := 0
- escape := false
-
- for _, char := range word {
- if char == '\x1b' {
- escape = true
- }
- if !escape {
- length += runewidth.RuneWidth(rune(char))
- }
- if char == 'm' {
- escape = false
- }
- }
-
- return length
-}
-
-// splitWord split a word at the given length, while ignoring the terminal escape sequences
-func splitWord(word string, length int) (string, string) {
- runes := []rune(word)
- var result []rune
- added := 0
- escape := false
-
- if length == 0 {
- return "", word
- }
-
- for _, r := range runes {
- if r == '\x1b' {
- escape = true
- }
-
- width := runewidth.RuneWidth(r)
- if width+added > length {
- // wide character made the length overflow
- break
- }
-
- result = append(result, r)
-
- if !escape {
- added += width
- if added >= length {
- break
- }
- }
-
- if r == 'm' {
- escape = false
- }
- }
-
- leftover := runes[len(result):]
-
- return string(result), string(leftover)
-}
diff --git a/util/text/text_test.go b/util/text/text_test.go
deleted file mode 100644
index 5be25409..00000000
--- a/util/text/text_test.go
+++ /dev/null
@@ -1,376 +0,0 @@
-package text
-
-import (
- "reflect"
- "strings"
- "testing"
-)
-
-func TestWrap(t *testing.T) {
- cases := []struct {
- Input, Output string
- Lim int
- }{
- // A simple word passes through.
- {
- "foo",
- "foo",
- 4,
- },
- // Word breaking
- {
- "foobarbaz",
- "foob\narba\nz",
- 4,
- },
- // Lines are broken at whitespace.
- {
- "foo bar baz",
- "foo\nbar\nbaz",
- 4,
- },
- // Word breaking
- {
- "foo bars bazzes",
- "foo\nbars\nbazz\nes",
- 4,
- },
- // A word that would run beyond the width is wrapped.
- {
- "fo sop",
- "fo\nsop",
- 4,
- },
- // A tab counts as 4 characters.
- {
- "foo\nb\t r\n baz",
- "foo\nb\nr\n baz",
- 4,
- },
- // Trailing whitespace is removed after used for wrapping.
- // Runs of whitespace on which a line is broken are removed.
- {
- "foo \nb ar ",
- "foo\n\nb\nar\n",
- 4,
- },
- // An explicit line break at the end of the input is preserved.
- {
- "foo bar baz\n",
- "foo\nbar\nbaz\n",
- 4,
- },
- // Explicit break are always preserved.
- {
- "\nfoo bar\n\n\nbaz\n",
- "\nfoo\nbar\n\n\nbaz\n",
- 4,
- },
- // Ignore complete words with terminal color sequence
- {
- "foo \x1b[31mbar\x1b[0m baz",
- "foo\n\x1b[31mbar\x1b[0m\nbaz",
- 4,
- },
- // Handle words with colors sequence inside the word
- {
- "foo b\x1b[31mbar\x1b[0mr baz",
- "foo\nb\x1b[31mbar\n\x1b[0mr\nbaz",
- 4,
- },
- // Break words with colors sequence inside the word
- {
- "foo bb\x1b[31mbar\x1b[0mr baz",
- "foo\nbb\x1b[31mba\nr\x1b[0mr\nbaz",
- 4,
- },
- // Complete example:
- {
- " This is a list: \n\n\t* foo\n\t* bar\n\n\n\t* baz \nBAM ",
- " This\nis a\nlist:\n\n *\nfoo\n *\nbar\n\n\n *\nbaz\nBAM\n",
- 6,
- },
- // Handle chinese (wide characters)
- {
- "一只敏捷的狐狸跳过了一只懒狗。",
- "一只敏捷的狐\n狸跳过了一只\n懒狗。",
- 12,
- },
- // Handle chinese with colors
- {
- "一只敏捷的\x1b[31m狐狸跳过\x1b[0m了一只懒狗。",
- "一只敏捷的\x1b[31m狐\n狸跳过\x1b[0m了一只\n懒狗。",
- 12,
- },
- // Handle mixed wide and short characters
- {
- "敏捷 A quick 的狐狸 fox 跳过 jumps over a lazy 了一只懒狗 dog。",
- "敏捷 A quick\n的狐狸 fox\n跳过 jumps\nover a lazy\n了一只懒狗\ndog。",
- 12,
- },
- // Handle mixed wide and short characters with color
- {
- "敏捷 A \x1b31mquick 的狐狸 fox 跳\x1b0m过 jumps over a lazy 了一只懒狗 dog。",
- "敏捷 A \x1b31mquick\n的狐狸 fox\n跳\x1b0m过 jumps\nover a lazy\n了一只懒狗\ndog。",
- 12,
- },
- }
-
- for i, tc := range cases {
- actual, lines := Wrap(tc.Input, tc.Lim)
- if actual != tc.Output {
- t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%s`\n\nActual Output:\n\n`%s`",
- i, tc.Input, tc.Output, actual)
- }
-
- expected := len(strings.Split(tc.Output, "\n"))
- if expected != lines {
- t.Fatalf("Case %d Nb lines mismatch\nExpected:%d\nActual:%d",
- i, expected, lines)
- }
- }
-}
-
-func TestWrapLeftPadded(t *testing.T) {
- cases := []struct {
- input, output string
- lim, pad int
- }{
- {
- "The Lorem ipsum text is typically composed of pseudo-Latin words. It is commonly used as placeholder text to examine or demonstrate the visual effects of various graphic design.",
- ` The Lorem ipsum text is typically composed of
- pseudo-Latin words. It is commonly used as placeholder
- text to examine or demonstrate the visual effects of
- various graphic design.`,
- 59, 4,
- },
- // Handle Chinese
- {
- "婞一枳郲逴靲屮蜧曀殳,掫乇峔掮傎溒兀緉冘仜。郼牪艽螗媷錵朸一詅掜豗怙刉笀丌,楀棶乇矹迡搦囷圣亍昄漚粁仈祂。覂一洳袶揙楱亍滻瘯毌,掗屮柅軡菵腩乜榵毌夯。勼哻怌婇怤灟葠雺奷朾恦扰衪岨坋誁乇芚誙腞。冇笉妺悆浂鱦賌廌灱灱觓坋佫呬耴跣兀枔蓔輈。嵅咍犴膰痭瘰机一靬涽捊矷尒玶乇,煚塈丌岰陊鉖怞戉兀甿跾觓夬侄。棩岧汌橩僁螗玎一逭舴圂衪扐衲兀,嵲媕亍衩衿溽昃夯丌侄蒰扂丱呤。毰侘妅錣廇螉仴一暀淖蚗佶庂咺丌,輀鈁乇彽洢溦洰氶乇构碨洐巿阹。",
- ` 婞一枳郲逴靲屮蜧曀殳,掫乇峔掮傎溒兀緉冘仜。郼牪艽螗媷
- 錵朸一詅掜豗怙刉笀丌,楀棶乇矹迡搦囷圣亍昄漚粁仈祂。覂
- 一洳袶揙楱亍滻瘯毌,掗屮柅軡菵腩乜榵毌夯。勼哻怌婇怤灟
- 葠雺奷朾恦扰衪岨坋誁乇芚誙腞。冇笉妺悆浂鱦賌廌灱灱觓坋
- 佫呬耴跣兀枔蓔輈。嵅咍犴膰痭瘰机一靬涽捊矷尒玶乇,煚塈
- 丌岰陊鉖怞戉兀甿跾觓夬侄。棩岧汌橩僁螗玎一逭舴圂衪扐衲
- 兀,嵲媕亍衩衿溽昃夯丌侄蒰扂丱呤。毰侘妅錣廇螉仴一暀淖
- 蚗佶庂咺丌,輀鈁乇彽洢溦洰氶乇构碨洐巿阹。`,
- 59, 4,
- },
- // Handle long unbreakable words in a full stentence
- {
- "OT: there are alternatives to maintainer-/user-set priority, e.g. \"[user pain](http://www.lostgarden.com/2008/05/improving-bug-triage-with-user-pain.html)\".",
- ` OT: there are alternatives to maintainer-/user-set
- priority, e.g. "[user pain](http://www.lostgarden.com/
- 2008/05/improving-bug-triage-with-user-pain.html)".`,
- 58, 4,
- },
- }
-
- for i, tc := range cases {
- actual, lines := WrapLeftPadded(tc.input, tc.lim, tc.pad)
- if actual != tc.output {
- t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n`\n%s`\n\nActual Output:\n`\n%s\n%s`",
- i, tc.input, tc.output,
- "|"+strings.Repeat("-", tc.lim-2)+"|",
- actual)
- }
-
- expected := len(strings.Split(tc.output, "\n"))
- if expected != lines {
- t.Fatalf("Case %d Nb lines mismatch\nExpected:%d\nActual:%d",
- i, expected, lines)
- }
- }
-}
-
-func TestWordLen(t *testing.T) {
- cases := []struct {
- Input string
- Length int
- }{
- // A simple word
- {
- "foo",
- 3,
- },
- // A simple word with colors
- {
- "\x1b[31mbar\x1b[0m",
- 3,
- },
- // Handle prefix and suffix properly
- {
- "foo\x1b[31mfoobarHoy\x1b[0mbaaar",
- 17,
- },
- // Handle chinese
- {
- "快檢什麼望對",
- 12,
- },
- // Handle chinese with colors
- {
- "快\x1b[31m檢什麼\x1b[0m望對",
- 12,
- },
- }
-
- for i, tc := range cases {
- l := wordLen(tc.Input)
- if l != tc.Length {
- t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%d`\n\nActual Output:\n\n`%d`",
- i, tc.Input, tc.Length, l)
- }
- }
-}
-
-func TestSplitWord(t *testing.T) {
- cases := []struct {
- Input string
- Length int
- Result, Leftover string
- }{
- // A simple word passes through.
- {
- "foo",
- 4,
- "foo", "",
- },
- // Cut at the right place
- {
- "foobarHoy",
- 4,
- "foob", "arHoy",
- },
- // A simple word passes through with colors
- {
- "\x1b[31mbar\x1b[0m",
- 4,
- "\x1b[31mbar\x1b[0m", "",
- },
- // Cut at the right place with colors
- {
- "\x1b[31mfoobarHoy\x1b[0m",
- 4,
- "\x1b[31mfoob", "arHoy\x1b[0m",
- },
- // Handle prefix and suffix properly
- {
- "foo\x1b[31mfoobarHoy\x1b[0mbaaar",
- 4,
- "foo\x1b[31mf", "oobarHoy\x1b[0mbaaar",
- },
- // Cut properly with length = 0
- {
- "foo",
- 0,
- "", "foo",
- },
- // Handle chinese
- {
- "快檢什麼望對",
- 4,
- "快檢", "什麼望對",
- },
- {
- "快檢什麼望對",
- 5,
- "快檢", "什麼望對",
- },
- // Handle chinese with colors
- {
- "快\x1b[31m檢什麼\x1b[0m望對",
- 4,
- "快\x1b[31m檢", "什麼\x1b[0m望對",
- },
- }
-
- for i, tc := range cases {
- result, leftover := splitWord(tc.Input, tc.Length)
- if result != tc.Result || leftover != tc.Leftover {
- t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%s` - `%s`\n\nActual Output:\n\n`%s` - `%s`",
- i, tc.Input, tc.Result, tc.Leftover, result, leftover)
- }
- }
-}
-
-func TestExtractApplyTermEscapes(t *testing.T) {
- cases := []struct {
- Input string
- Output string
- TermEscapes []escapeItem
- }{
- // A plain ascii line with escapes.
- {
- "This \x1b[31mis an\x1b[0m example.",
- "This is an example.",
- []escapeItem{{"\x1b[31m", 5}, {"\x1b[0m", 10}},
- },
- // Escape at the end
- {
- "This \x1b[31mis an example.\x1b[0m",
- "This is an example.",
- []escapeItem{{"\x1b[31m", 5}, {"\x1b[0m", 19}},
- },
- // A plain wide line with escapes.
- {
- "一只敏捷\x1b[31m的狐狸\x1b[0m跳过了一只懒狗。",
- "一只敏捷的狐狸跳过了一只懒狗。",
- []escapeItem{{"\x1b[31m", 4}, {"\x1b[0m", 7}},
- },
- // A normal-wide mixed line with escapes.
- {
- "一只 A Quick 敏捷\x1b[31m的狐 Fox 狸\x1b[0m跳过了Dog一只懒狗。",
- "一只 A Quick 敏捷的狐 Fox 狸跳过了Dog一只懒狗。",
- []escapeItem{{"\x1b[31m", 13}, {"\x1b[0m", 21}},
- },
- }
-
- for i, tc := range cases {
- line2, escapes := extractTermEscapes(tc.Input)
- if line2 != tc.Output || !reflect.DeepEqual(escapes, tc.TermEscapes) {
- t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\nLine: `%s`\nEscapes: `%+v`\n\nActual Output:\n\nLine: `%s`\nEscapes: `%+v`\n\n",
- i, tc.Input, tc.Output, tc.TermEscapes, line2, escapes)
- }
- line3 := applyTermEscapes(line2, escapes)
- if line3 != tc.Input {
- t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Result:\n\n`%s`\n\nActual Result:\n\n`%s`\n\n",
- i, tc.Input, tc.Input, line3)
- }
- }
-}
-
-func TestSegmentLines(t *testing.T) {
- cases := []struct {
- Input string
- Output []string
- }{
- // A plain ascii line with escapes.
- {
- "This is an example.",
- []string{"This", " ", "is", " ", "an", " ", "example."},
- },
- // A plain wide line with escapes.
- {
- "一只敏捷的狐狸跳过了一只懒狗。",
- []string{"一", "只", "敏", "捷", "的", "狐", "狸", "跳", "过",
- "了", "一", "只", "懒", "狗", "。"},
- },
- // A complex stentence.
- {
- "This is a 'complex' example, where 一只 and English 混合了。",
- []string{"This", " ", "is", " ", "a", " ", "'complex'", " ", "example,",
- " ", "where", " ", "一", "只", " ", "and", " ", "English", " ", "混",
- "合", "了", "。"},
- },
- }
-
- for i, tc := range cases {
- chunks := segmentLine(tc.Input)
- if !reflect.DeepEqual(chunks, tc.Output) {
- t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`[%s]`\n\nActual Output:\n\n`[%s]`\n\n",
- i, tc.Input, strings.Join(tc.Output, ", "), strings.Join(chunks, ", "))
- }
- }
-}
diff --git a/vendor/github.com/MichaelMure/go-term-text/.gitignore b/vendor/github.com/MichaelMure/go-term-text/.gitignore
new file mode 100644
index 00000000..9f11b755
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/.gitignore
@@ -0,0 +1 @@
+.idea/
diff --git a/vendor/github.com/MichaelMure/go-term-text/.travis.yml b/vendor/github.com/MichaelMure/go-term-text/.travis.yml
new file mode 100644
index 00000000..496ca056
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/.travis.yml
@@ -0,0 +1,16 @@
+language: go
+
+go:
+ - 1.10.x
+ - 1.11.x
+ - 1.12.x
+
+env:
+ - GO111MODULE=on
+
+script:
+ - go build
+ - go test -v -bench=. -race -coverprofile=coverage.txt -covermode=atomic ./...
+
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/vendor/github.com/MichaelMure/go-term-text/LICENSE b/vendor/github.com/MichaelMure/go-term-text/LICENSE
new file mode 100644
index 00000000..5ba12bf4
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Michael Muré
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/MichaelMure/go-term-text/Readme.md b/vendor/github.com/MichaelMure/go-term-text/Readme.md
new file mode 100644
index 00000000..457b4472
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/Readme.md
@@ -0,0 +1,71 @@
+# go-term-text
+
+[![Build Status](https://travis-ci.org/MichaelMure/go-term-text.svg?branch=master)](https://travis-ci.org/MichaelMure/go-term-text)
+[![GoDoc](https://godoc.org/github.com/MichaelMure/go-term-text?status.svg)](https://godoc.org/github.com/MichaelMure/go-term-text)
+[![Go Report Card](https://goreportcard.com/badge/github.com/MichaelMure/go-term-text)](https://goreportcard.com/report/github.com/MichaelMure/go-term-text)
+[![codecov](https://codecov.io/gh/MichaelMure/go-term-text/branch/master/graph/badge.svg)](https://codecov.io/gh/MichaelMure/go-term-text)
+[![GitHub license](https://img.shields.io/github/license/MichaelMure/go-term-text.svg)](https://github.com/MichaelMure/go-term-text/blob/master/LICENSE)
+[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/the-git-bug/Lobby)
+
+`go-term-text` is a go package implementing a collection of algorithms to help format and manipulate text for the terminal.
+
+In particular, `go-term-text`:
+- support wide characters (chinese, japanese ...) and emoji
+- handle properly ANSI escape sequences
+
+Included algorithms cover:
+- wrapping with padding and indentation
+- padding
+- text length
+- trimming
+- alignment
+- escape sequence extraction and reapplication
+- truncation
+
+## Example
+
+```go
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ text "github.com/MichaelMure/go-term-text"
+)
+
+func main() {
+ input := "The \x1b[1mLorem ipsum\x1b[0m text is typically composed of " +
+ "pseudo-Latin words. It is commonly used as \x1b[3mplaceholder\x1b[0m" +
+ " text to examine or demonstrate the \x1b[9mvisual effects\x1b[0m of " +
+ "various graphic design. 一只 A Quick \x1b[31m敏捷的狐 Fox " +
+ "狸跳过了\x1b[0mDog一只懒狗。"
+
+ output, n := text.WrapWithPadIndent(input, 60,
+ "\x1b[34m<-indent-> \x1b[0m", "\x1b[33m<-pad-> \x1b[0m")
+
+ fmt.Printf("output has %d lines\n\n", n)
+
+ fmt.Println("|" + strings.Repeat("-", 58) + "|")
+ fmt.Println(output)
+ fmt.Println("|" + strings.Repeat("-", 58) + "|")
+}
+```
+
+This will print:
+
+![example output](/img/example.png)
+
+For more details, have a look at the [GoDoc](https://godoc.org/github.com/MichaelMure/go-term-text).
+
+## Origin
+
+This package has been extracted from the [git-bug](https://github.com/MichaelMure/git-bug) project. As such, its aim is to support this project and not to provide an all-in-one solution. Contributions as welcome though.
+
+## Contribute
+
+PRs accepted.
+
+## License
+
+MIT
diff --git a/vendor/github.com/MichaelMure/go-term-text/align.go b/vendor/github.com/MichaelMure/go-term-text/align.go
new file mode 100644
index 00000000..8262a4de
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/align.go
@@ -0,0 +1,67 @@
+package text
+
+import (
+ "strings"
+)
+
+type Alignment int
+
+const (
+ NoAlign Alignment = iota
+ AlignLeft
+ AlignCenter
+ AlignRight
+)
+
+// LineAlign align the given line as asked and apply the needed padding to match the given
+// lineWidth, while ignoring the terminal escape sequences.
+// If the given lineWidth is too small to fit the given line, it's returned without
+// padding, overflowing lineWidth.
+func LineAlign(line string, lineWidth int, align Alignment) string {
+ switch align {
+ case NoAlign:
+ return line
+ case AlignLeft:
+ return LineAlignLeft(line, lineWidth)
+ case AlignCenter:
+ return LineAlignCenter(line, lineWidth)
+ case AlignRight:
+ return LineAlignRight(line, lineWidth)
+ }
+ panic("unknown alignment")
+}
+
+// LineAlignLeft align the given line on the left while ignoring the terminal escape sequences.
+// If the given lineWidth is too small to fit the given line, it's returned without
+// padding, overflowing lineWidth.
+func LineAlignLeft(line string, lineWidth int) string {
+ return TrimSpace(line)
+}
+
+// LineAlignCenter align the given line on the center and apply the needed left
+// padding, while ignoring the terminal escape sequences.
+// If the given lineWidth is too small to fit the given line, it's returned without
+// padding, overflowing lineWidth.
+func LineAlignCenter(line string, lineWidth int) string {
+ trimmed := TrimSpace(line)
+ totalPadLen := lineWidth - Len(trimmed)
+ if totalPadLen < 0 {
+ totalPadLen = 0
+ }
+ pad := strings.Repeat(" ", totalPadLen/2)
+ return pad + trimmed
+}
+
+// LineAlignRight align the given line on the right and apply the needed left
+// padding to match the given lineWidth, while ignoring the terminal escape sequences.
+// If the given lineWidth is too small to fit the given line, it's returned without
+// padding, overflowing lineWidth.
+func LineAlignRight(line string, lineWidth int) string {
+ trimmed := TrimSpace(line)
+ padLen := lineWidth - Len(trimmed)
+ if padLen < 0 {
+ padLen = 0
+ }
+ pad := strings.Repeat(" ", padLen)
+ return pad + trimmed
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/escapes.go b/vendor/github.com/MichaelMure/go-term-text/escapes.go
new file mode 100644
index 00000000..19f78c92
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/escapes.go
@@ -0,0 +1,95 @@
+package text
+
+import (
+ "strings"
+ "unicode/utf8"
+)
+
+// EscapeItem hold the description of terminal escapes in a line.
+// 'item' is the actual escape command
+// 'pos' is the index in the rune array where the 'item' shall be inserted back.
+// For example, the escape item in "F\x1b33mox" is {"\x1b33m", 1}.
+type EscapeItem struct {
+ Item string
+ Pos int
+}
+
+// ExtractTermEscapes extract terminal escapes out of a line and returns a new
+// line without terminal escapes and a slice of escape items. The terminal escapes
+// can be inserted back into the new line at rune index 'item.pos' to recover the
+// original line.
+//
+// Required: The line shall not contain "\n"
+func ExtractTermEscapes(line string) (string, []EscapeItem) {
+ var termEscapes []EscapeItem
+ var line1 strings.Builder
+
+ pos := 0
+ item := ""
+ occupiedRuneCount := 0
+ inEscape := false
+ for i, r := range []rune(line) {
+ if r == '\x1b' {
+ pos = i
+ item = string(r)
+ inEscape = true
+ continue
+ }
+ if inEscape {
+ item += string(r)
+ if r == 'm' {
+ termEscapes = append(termEscapes, EscapeItem{item, pos - occupiedRuneCount})
+ occupiedRuneCount += utf8.RuneCountInString(item)
+ inEscape = false
+ }
+ continue
+ }
+ line1.WriteRune(r)
+ }
+
+ return line1.String(), termEscapes
+}
+
+// ApplyTermEscapes apply the extracted terminal escapes to the edited line.
+// Escape sequences need to be ordered by their position.
+// If the position is < 0, the escape is applied at the beginning of the line.
+// If the position is > len(line), the escape is applied at the end of the line.
+func ApplyTermEscapes(line string, escapes []EscapeItem) string {
+ if len(escapes) == 0 {
+ return line
+ }
+
+ var out strings.Builder
+
+ currPos := 0
+ currItem := 0
+ for _, r := range line {
+ for currItem < len(escapes) && currPos >= escapes[currItem].Pos {
+ out.WriteString(escapes[currItem].Item)
+ currItem++
+ }
+ out.WriteRune(r)
+ currPos++
+ }
+
+ // Don't forget the trailing escapes, if any.
+ for currItem < len(escapes) {
+ out.WriteString(escapes[currItem].Item)
+ currItem++
+ }
+
+ return out.String()
+}
+
+// OffsetEscapes is a utility function to offset the position of a
+// collection of EscapeItem.
+func OffsetEscapes(escapes []EscapeItem, offset int) []EscapeItem {
+ result := make([]EscapeItem, len(escapes))
+ for i, e := range escapes {
+ result[i] = EscapeItem{
+ Item: e.Item,
+ Pos: e.Pos + offset,
+ }
+ }
+ return result
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/go.mod b/vendor/github.com/MichaelMure/go-term-text/go.mod
new file mode 100644
index 00000000..162c5dac
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/go.mod
@@ -0,0 +1,8 @@
+module github.com/MichaelMure/go-term-text
+
+go 1.10
+
+require (
+ github.com/mattn/go-runewidth v0.0.4
+ github.com/stretchr/testify v1.3.0
+)
diff --git a/vendor/github.com/MichaelMure/go-term-text/go.sum b/vendor/github.com/MichaelMure/go-term-text/go.sum
new file mode 100644
index 00000000..0aaedf16
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/go.sum
@@ -0,0 +1,9 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
diff --git a/vendor/github.com/MichaelMure/go-term-text/left_pad.go b/vendor/github.com/MichaelMure/go-term-text/left_pad.go
new file mode 100644
index 00000000..a63fedb9
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/left_pad.go
@@ -0,0 +1,50 @@
+package text
+
+import (
+ "bytes"
+ "strings"
+
+ "github.com/mattn/go-runewidth"
+)
+
+// LeftPadMaxLine pads a line on the left by a specified amount and pads the
+// string on the right to fill the maxLength.
+// If the given string is too long, it is truncated with an ellipsis.
+// Handle properly terminal color escape code
+func LeftPadMaxLine(line string, length, leftPad int) string {
+ cleaned, escapes := ExtractTermEscapes(line)
+
+ scrWidth := runewidth.StringWidth(cleaned)
+ // truncate and ellipse if needed
+ if scrWidth+leftPad > length {
+ cleaned = runewidth.Truncate(cleaned, length-leftPad, "…")
+ } else if scrWidth+leftPad < length {
+ cleaned = runewidth.FillRight(cleaned, length-leftPad)
+ }
+
+ rightPart := ApplyTermEscapes(cleaned, escapes)
+ pad := strings.Repeat(" ", leftPad)
+
+ return pad + rightPart
+}
+
+// LeftPad left pad each line of the given text
+func LeftPadLines(text string, leftPad int) string {
+ var result bytes.Buffer
+
+ pad := strings.Repeat(" ", leftPad)
+
+ lines := strings.Split(text, "\n")
+
+ for i, line := range lines {
+ result.WriteString(pad)
+ result.WriteString(line)
+
+ // no additional line break at the end
+ if i < len(lines)-1 {
+ result.WriteString("\n")
+ }
+ }
+
+ return result.String()
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/len.go b/vendor/github.com/MichaelMure/go-term-text/len.go
new file mode 100644
index 00000000..c6bcaeac
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/len.go
@@ -0,0 +1,45 @@
+package text
+
+import (
+ "strings"
+
+ "github.com/mattn/go-runewidth"
+)
+
+// Len return the length of a string in a terminal, while ignoring the terminal
+// escape sequences.
+func Len(text string) int {
+ length := 0
+ escape := false
+
+ for _, char := range text {
+ if char == '\x1b' {
+ escape = true
+ }
+ if !escape {
+ length += runewidth.RuneWidth(char)
+ }
+ if char == 'm' {
+ escape = false
+ }
+ }
+
+ return length
+}
+
+// MaxLineLen return the length in a terminal of the longest line, while
+// ignoring the terminal escape sequences.
+func MaxLineLen(text string) int {
+ lines := strings.Split(text, "\n")
+
+ max := 0
+
+ for _, line := range lines {
+ length := Len(line)
+ if length > max {
+ max = length
+ }
+ }
+
+ return max
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/trim.go b/vendor/github.com/MichaelMure/go-term-text/trim.go
new file mode 100644
index 00000000..eaf2ca0c
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/trim.go
@@ -0,0 +1,28 @@
+package text
+
+import (
+ "strings"
+ "unicode"
+)
+
+// TrimSpace remove the leading and trailing whitespace while ignoring the
+// terminal escape sequences.
+// Returns the number of trimmed space on both side.
+func TrimSpace(line string) string {
+ cleaned, escapes := ExtractTermEscapes(line)
+
+ // trim left while counting
+ left := 0
+ trimmed := strings.TrimLeftFunc(cleaned, func(r rune) bool {
+ if unicode.IsSpace(r) {
+ left++
+ return true
+ }
+ return false
+ })
+
+ trimmed = strings.TrimRightFunc(trimmed, unicode.IsSpace)
+
+ escapes = OffsetEscapes(escapes, -left)
+ return ApplyTermEscapes(trimmed, escapes)
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/truncate.go b/vendor/github.com/MichaelMure/go-term-text/truncate.go
new file mode 100644
index 00000000..b51bb39e
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/truncate.go
@@ -0,0 +1,24 @@
+package text
+
+import "github.com/mattn/go-runewidth"
+
+// TruncateMax truncate a line if its length is greater
+// than the given length. Otherwise, the line is returned
+// as is. If truncating occur, an ellipsis is inserted at
+// the end.
+// Handle properly terminal color escape code
+func TruncateMax(line string, length int) string {
+ if length <= 0 {
+ return "…"
+ }
+
+ l := Len(line)
+ if l <= length || l == 0 {
+ return line
+ }
+
+ cleaned, escapes := ExtractTermEscapes(line)
+ truncated := runewidth.Truncate(cleaned, length-1, "")
+
+ return ApplyTermEscapes(truncated, escapes) + "…"
+}
diff --git a/vendor/github.com/MichaelMure/go-term-text/wrap.go b/vendor/github.com/MichaelMure/go-term-text/wrap.go
new file mode 100644
index 00000000..2fd6ed5f
--- /dev/null
+++ b/vendor/github.com/MichaelMure/go-term-text/wrap.go
@@ -0,0 +1,334 @@
+package text
+
+import (
+ "strings"
+
+ "github.com/mattn/go-runewidth"
+)
+
+// Force runewidth not to treat ambiguous runes as wide chars, so that things
+// like unicode ellipsis/up/down/left/right glyphs can have correct runewidth
+// and can be displayed correctly in terminals.
+func init() {
+ runewidth.DefaultCondition.EastAsianWidth = false
+}
+
+// Wrap a text for a given line size.
+// Handle properly terminal color escape code
+func Wrap(text string, lineWidth int) (string, int) {
+ return WrapLeftPadded(text, lineWidth, 0)
+}
+
+// WrapLeftPadded wrap a text for a given line size with a left padding.
+// Handle properly terminal color escape code
+func WrapLeftPadded(text string, lineWidth int, leftPad int) (string, int) {
+ pad := strings.Repeat(" ", leftPad)
+ return WrapWithPad(text, lineWidth, pad)
+}
+
+// WrapWithPad wrap a text for a given line size with a custom left padding
+// Handle properly terminal color escape code
+func WrapWithPad(text string, lineWidth int, pad string) (string, int) {
+ return WrapWithPadIndent(text, lineWidth, pad, pad)
+}
+
+// WrapWithPad wrap a text for a given line size with a custom left padding
+// This function also align the result depending on the requested alignment.
+// Handle properly terminal color escape code
+func WrapWithPadAlign(text string, lineWidth int, pad string, align Alignment) (string, int) {
+ return WrapWithPadIndentAlign(text, lineWidth, pad, pad, align)
+}
+
+// WrapWithPadIndent wrap a text for a given line size with a custom left padding
+// and a first line indent. The padding is not effective on the first line, indent
+// is used instead, which allow to implement indents and outdents.
+// Handle properly terminal color escape code
+func WrapWithPadIndent(text string, lineWidth int, indent string, pad string) (string, int) {
+ return WrapWithPadIndentAlign(text, lineWidth, indent, pad, NoAlign)
+}
+
+// WrapWithPadIndentAlign wrap a text for a given line size with a custom left padding
+// and a first line indent. The padding is not effective on the first line, indent
+// is used instead, which allow to implement indents and outdents.
+// This function also align the result depending on the requested alignment.
+// Handle properly terminal color escape code
+func WrapWithPadIndentAlign(text string, lineWidth int, indent string, pad string, align Alignment) (string, int) {
+ var lines []string
+ nbLine := 0
+
+ // Start with the indent
+ padStr := indent
+ padLen := Len(indent)
+
+ // tabs are formatted as 4 spaces
+ text = strings.Replace(text, "\t", " ", -1)
+
+ // NOTE: text is first segmented into lines so that softwrapLine can handle.
+ for i, line := range strings.Split(text, "\n") {
+ // on the second line, use the padding instead
+ if i == 1 {
+ padStr = pad
+ padLen = Len(pad)
+ }
+
+ if line == "" || strings.TrimSpace(line) == "" {
+ // nothing in the line, we just add the non-empty part of the padding
+ lines = append(lines, strings.TrimRight(padStr, " "))
+ nbLine++
+ continue
+ }
+
+ wrapped := softwrapLine(line, lineWidth-padLen)
+ split := strings.Split(wrapped, "\n")
+
+ if i == 0 && len(split) > 1 {
+ // the very first line got wrapped
+ // that means we need to switch to the normal padding
+ // use the first wrapped line, ignore everything else and
+ // wrap the remaining of the line with the normal padding.
+
+ content := LineAlign(strings.TrimRight(split[0], " "), lineWidth-padLen, align)
+ lines = append(lines, padStr+content)
+ nbLine++
+ line = strings.TrimPrefix(line, split[0])
+ line = strings.TrimLeft(line, " ")
+
+ padStr = pad
+ padLen = Len(pad)
+ wrapped = softwrapLine(line, lineWidth-padLen)
+ split = strings.Split(wrapped, "\n")
+ }
+
+ for j, seg := range split {
+ if j == 0 {
+ // keep the left padding of the wrapped line
+ content := LineAlign(strings.TrimRight(seg, " "), lineWidth-padLen, align)
+ lines = append(lines, padStr+content)
+ } else {
+ content := LineAlign(strings.TrimSpace(seg), lineWidth-padLen, align)
+ lines = append(lines, padStr+content)
+ }
+ nbLine++
+ }
+ }
+
+ return strings.Join(lines, "\n"), nbLine
+}
+
+// Break a line into several lines so that each line consumes at most
+// 'textWidth' cells. Lines break at groups of white spaces and multibyte
+// chars. Nothing is removed from the original text so that it behaves like a
+// softwrap.
+//
+// Required: The line shall not contain '\n'
+//
+// WRAPPING ALGORITHM: The line is broken into non-breakable chunks, then line
+// breaks ("\n") are inserted between these groups so that the total length
+// between breaks does not exceed the required width. Words that are longer than
+// the textWidth are broken into pieces no longer than textWidth.
+func softwrapLine(line string, textWidth int) string {
+ escaped, escapes := ExtractTermEscapes(line)
+
+ chunks := segmentLine(escaped)
+ // Reverse the chunk array so we can use it as a stack.
+ for i, j := 0, len(chunks)-1; i < j; i, j = i+1, j-1 {
+ chunks[i], chunks[j] = chunks[j], chunks[i]
+ }
+
+ // for readability, minimal implementation of a stack:
+
+ pop := func() string {
+ result := chunks[len(chunks)-1]
+ chunks = chunks[:len(chunks)-1]
+ return result
+ }
+
+ push := func(chunk string) {
+ chunks = append(chunks, chunk)
+ }
+
+ peek := func() string {
+ return chunks[len(chunks)-1]
+ }
+
+ empty := func() bool {
+ return len(chunks) == 0
+ }
+
+ var out strings.Builder
+
+ // helper to write in the output while interleaving the escape
+ // sequence at the correct places.
+ // note: the final algorithm will add additional line break in the original
+ // text. Those line break are *not* fed to this helper so the positions don't
+ // need to be offset, which make the whole thing much easier.
+ currPos := 0
+ currItem := 0
+ outputString := func(s string) {
+ for _, r := range s {
+ for currItem < len(escapes) && currPos == escapes[currItem].Pos {
+ out.WriteString(escapes[currItem].Item)
+ currItem++
+ }
+ out.WriteRune(r)
+ currPos++
+ }
+ }
+
+ width := 0
+
+ for !empty() {
+ wl := Len(peek())
+
+ if width+wl <= textWidth {
+ // the chunk fit in the available space
+ outputString(pop())
+ width += wl
+ if width == textWidth && !empty() {
+ // only add line break when there is more chunk to come
+ out.WriteRune('\n')
+ width = 0
+ }
+ } else if wl > textWidth {
+ // words too long for a full line are split to fill the remaining space.
+ // But if the long words is the first non-space word in the middle of the
+ // line, preceding spaces shall not be counted in word splitting.
+ splitWidth := textWidth - width
+ if strings.HasSuffix(out.String(), "\n"+strings.Repeat(" ", width)) {
+ splitWidth += width
+ }
+ left, right := splitWord(pop(), splitWidth)
+ // remainder is pushed back to the stack for next round
+ push(right)
+ outputString(left)
+ out.WriteRune('\n')
+ width = 0
+ } else {
+ // normal line overflow, we add a line break and try again
+ out.WriteRune('\n')
+ width = 0
+ }
+ }
+
+ // Don't forget the trailing escapes, if any.
+ for currItem < len(escapes) && currPos >= escapes[currItem].Pos {
+ out.WriteString(escapes[currItem].Item)
+ currItem++
+ }
+
+ return out.String()
+}
+
+// Segment a line into chunks, where each chunk consists of chars with the same
+// type and is not breakable.
+func segmentLine(s string) []string {
+ var chunks []string
+
+ var word string
+ wordType := none
+ flushWord := func() {
+ chunks = append(chunks, word)
+ word = ""
+ wordType = none
+ }
+
+ for _, r := range s {
+ // A WIDE_CHAR itself constitutes a chunk.
+ thisType := runeType(r)
+ if thisType == wideChar {
+ if wordType != none {
+ flushWord()
+ }
+ chunks = append(chunks, string(r))
+ continue
+ }
+ // Other type of chunks starts with a char of that type, and ends with a
+ // char with different type or end of string.
+ if thisType != wordType {
+ if wordType != none {
+ flushWord()
+ }
+ word = string(r)
+ wordType = thisType
+ } else {
+ word += string(r)
+ }
+ }
+ if word != "" {
+ flushWord()
+ }
+
+ return chunks
+}
+
+type RuneType int
+
+// Rune categories
+//
+// These categories are so defined that each category forms a non-breakable
+// chunk. It IS NOT the same as unicode code point categories.
+const (
+ none RuneType = iota
+ wideChar
+ invisible
+ shortUnicode
+ space
+ visibleAscii
+)
+
+// Determine the category of a rune.
+func runeType(r rune) RuneType {
+ rw := runewidth.RuneWidth(r)
+ if rw > 1 {
+ return wideChar
+ } else if rw == 0 {
+ return invisible
+ } else if r > 127 {
+ return shortUnicode
+ } else if r == ' ' {
+ return space
+ } else {
+ return visibleAscii
+ }
+}
+
+// splitWord split a word at the given length, while ignoring the terminal escape sequences
+func splitWord(word string, length int) (string, string) {
+ runes := []rune(word)
+ var result []rune
+ added := 0
+ escape := false
+
+ if length == 0 {
+ return "", word
+ }
+
+ for _, r := range runes {
+ if r == '\x1b' {
+ escape = true
+ }
+
+ width := runewidth.RuneWidth(r)
+ if width+added > length {
+ // wide character made the length overflow
+ break
+ }
+
+ result = append(result, r)
+
+ if !escape {
+ added += width
+ if added >= length {
+ break
+ }
+ }
+
+ if r == 'm' {
+ escape = false
+ }
+ }
+
+ leftover := runes[len(result):]
+
+ return string(result), string(leftover)
+}
diff --git a/vendor/github.com/theckman/goconstraint/go1.10/gte/constraint.go b/vendor/github.com/theckman/goconstraint/go1.10/gte/constraint.go
new file mode 100644
index 00000000..33dc1161
--- /dev/null
+++ b/vendor/github.com/theckman/goconstraint/go1.10/gte/constraint.go
@@ -0,0 +1,8 @@
+// The contents of this file has been released in to the Public Domain.
+
+// Package gtego110 should only be used as a blank import. If imported, it
+// will only compile if the Go runtime version is >= 1.10.
+package gtego110
+
+// This will fail to compile if the Go runtime version isn't >= 1.10.
+var _ = __SOFTWARE_REQUIRES_GO_VERSION_1_10__
diff --git a/vendor/github.com/theckman/goconstraint/go1.10/gte/go110.go b/vendor/github.com/theckman/goconstraint/go1.10/gte/go110.go
new file mode 100644
index 00000000..5d586ff9
--- /dev/null
+++ b/vendor/github.com/theckman/goconstraint/go1.10/gte/go110.go
@@ -0,0 +1,7 @@
+// The contents of this file has been released in to the Public Domain.
+
+// +build go1.10
+
+package gtego110
+
+const __SOFTWARE_REQUIRES_GO_VERSION_1_10__ = uint8(0)
diff --git a/vendor/github.com/theckman/goconstraint/go1.9/gte/constraint.go b/vendor/github.com/theckman/goconstraint/go1.9/gte/constraint.go
deleted file mode 100644
index ed67fefc..00000000
--- a/vendor/github.com/theckman/goconstraint/go1.9/gte/constraint.go
+++ /dev/null
@@ -1,8 +0,0 @@
-// The contents of this file has been released in to the Public Domain.
-
-// Package gtego19 should only be used as a blank import. If imported, it
-// will only compile if the Go runtime version is >= 1.9.
-package gtego19
-
-// This will fail to compile if the Go runtime version isn't >= 1.9.
-var _ = __SOFTWARE_REQUIRES_GO_VERSION_1_9__
diff --git a/vendor/github.com/theckman/goconstraint/go1.9/gte/go19.go b/vendor/github.com/theckman/goconstraint/go1.9/gte/go19.go
deleted file mode 100644
index 446a5ee3..00000000
--- a/vendor/github.com/theckman/goconstraint/go1.9/gte/go19.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// The contents of this file has been released in to the Public Domain.
-
-// +build go1.9
-
-package gtego19
-
-const __SOFTWARE_REQUIRES_GO_VERSION_1_9__ = uint8(0)