Issue 58/trim branch name right or left (#59)

* Bump gopkg.in/yaml v2 -> v3

* Add branch_trim_direction (default to right)

* tmux: remove Formater.remote()

Formater.remote() is just remoteBranch() followed by divergence()

* tmux: implement and test branch_trim_direction (left|right)

* tmux: s/BranchTrimDirection/BranchTrim

* README.md: update and add branch_trim

* tmux: remove dependency to testify

* tmux: use io.Discard in test
This commit is contained in:
Aurélien Rainone 2022-01-23 17:36:31 +01:00 committed by GitHub
parent 98d8874553
commit e614142f54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 261 additions and 151 deletions

View file

@ -59,36 +59,37 @@ Note that `tmux v2.1` was released in 2015 so you're probably better off updatin
`gitmux` output can be customized via a configuration file in YAML format.
The gitmux configuration file is in YAML format.
This is the default gitmux configuration file, in YAML format:
```yaml
tmux:
symbols:
branch: '⎇ '
hashprefix: ':'
ahead: ↑·
behind: ↓·
staged: '● '
conflict: '✖ '
modified: '✚ '
untracked: '… '
stashed: '⚑ '
clean: ✔
styles:
clear: '#[fg=default]'
state: '#[fg=red,bold]'
branch: '#[fg=white,bold]'
remote: '#[fg=cyan]'
staged: '#[fg=green,bold]'
conflict: '#[fg=red,bold]'
modified: '#[fg=red,bold]'
untracked: '#[fg=magenta,bold]'
stashed: '#[fg=cyan,bold]'
clean: '#[fg=green,bold]'
divergence: "#[fg=yellow]"
layout: [branch, .., remote, " - ", flags]
options:
branch_max_len: 0
symbols:
branch: '⎇ '
hashprefix: ':'
ahead: ↑·
behind: ↓·
staged: '● '
conflict: '✖ '
modified: '✚ '
untracked: '… '
stashed: '⚑ '
clean: ✔
styles:
clear: '#[fg=default]'
state: '#[fg=red,bold]'
branch: '#[fg=white,bold]'
remote: '#[fg=cyan]'
staged: '#[fg=green,bold]'
conflict: '#[fg=red,bold]'
modified: '#[fg=red,bold]'
untracked: '#[fg=magenta,bold]'
stashed: '#[fg=cyan,bold]'
clean: '#[fg=green,bold]'
divergence: '#[fg=default]'
layout: [branch, .., remote-branch, divergence, ' - ', flags]
options:
branch_max_len: 0
branch_trim: right
```
First, save the default configuration to a new file:
@ -209,9 +210,10 @@ layout: [flags, " ", branch]
This is the list of additional configuration `options`:
| Option | Description | Default |
| :--------------- | :--------------------------------------------------------- | :------------- |
| `branch_max_len` | Maximum displayed length for local and remote branch names | `0` (no limit) |
| Option | Description | Default |
| :--------------- | :--------------------------------------------------------- | :----------------- |
| `branch_max_len` | Maximum displayed length for local and remote branch names | `0` (no limit) |
| `branch_trim` | Trim left or right end of the branch (`right` or `left`) | `right` (trailing) |
## Troubleshooting

View file

@ -8,7 +8,7 @@ import (
"time"
"github.com/arl/gitstatus"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"github.com/arl/gitmux/json"
"github.com/arl/gitmux/tmux"

4
go.mod
View file

@ -4,6 +4,6 @@ go 1.15
require (
github.com/arl/gitstatus v0.4.3
github.com/stretchr/testify v1.7.0
gopkg.in/yaml.v2 v2.4.0
github.com/stretchr/testify v1.7.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)

5
go.sum
View file

@ -10,7 +10,6 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -5,12 +5,12 @@ import (
"fmt"
"io"
"strings"
"unicode/utf8"
"github.com/arl/gitstatus"
"gopkg.in/yaml.v3"
)
const truncateSymbol string = "..."
// Config is the configuration of the Git status tmux formatter.
type Config struct {
// Symbols contains the symbols printed before the Git status components.
@ -53,9 +53,32 @@ type styles struct {
Divergence string // Divergence is the style string printed before divergence count/symbols.
}
const (
dirLeft direction = "left"
dirRight direction = "right"
)
type direction string
func (d *direction) UnmarshalYAML(value *yaml.Node) error {
s := ""
if err := value.Decode(&s); err != nil {
return fmt.Errorf("error decoding 'direction': %v", s)
}
switch direction(s) {
case dirLeft:
*d = dirLeft
case dirRight:
*d = dirRight
default:
return fmt.Errorf("'direction': unexpected value %v", s)
}
return nil
}
type options struct {
// BranchMaxLen is the maximum displayed length for local and remote branch names.
BranchMaxLen int `yaml:"branch_max_len"`
BranchMaxLen int `yaml:"branch_max_len"`
BranchTrim direction `yaml:"branch_trim"`
}
// DefaultCfg is the default tmux configuration.
@ -88,6 +111,7 @@ var DefaultCfg = Config{
Layout: []string{"branch", "..", "remote-branch", "divergence", " - ", "flags"},
Options: options{
BranchMaxLen: 0,
BranchTrim: dirRight,
},
}
@ -98,40 +122,37 @@ type Formater struct {
st *gitstatus.Status
}
// Truncates branch name if longer than maxlen. If isremote, the leading
// "<remote>/" is ignored when counting length.
func truncateBranchName(name string, maxlen int, isremote bool) string {
remoteName := ""
branchName := name
const (
idxRemote = 0
idxBranch = 1
numItems = 2
)
if isremote {
a := strings.SplitAfterN(name, "/", numItems)
if len(a) == numItems {
remoteName = a[idxRemote]
branchName = a[idxBranch]
}
// truncate returns s, truncated so that it is no more than max characters long.
// Depending on the provided direction, truncation is performed right or left.
// If max is zero, negative or greater than the number of rnues in s, truncate
// just returns s. However, if truncation is applied, then the last 3 chars (or
// 3 first, depending on provided direction) are replaced with "...".
//
// NOTE: If max is lower than 3, in other words if we can't even have ellispis,
// then truncate just truncates the maximum number of characters, without
// bothering with ellipsis.
func truncate(s string, max int, dir direction) string {
slen := utf8.RuneCountInString(s)
if max <= 0 || slen <= max {
return s
}
// To count length of characters and extract substring from UTF-8 strings.
branchNameRune := []rune(branchName)
truncateSymbolRune := []rune(truncateSymbol)
runes := []rune(s)
ell := []rune("...")
if maxlen > 0 && maxlen < len(branchNameRune) {
nameLen := maxlen - len(truncateSymbolRune)
if nameLen > 0 {
branchName = string(branchNameRune[:nameLen]) + truncateSymbol
} else {
branchName = string(truncateSymbolRune[:maxlen])
}
if max < 3 {
ell = nil // Just truncate s since even ellipsis don't fit.
}
return remoteName + branchName
switch dir {
case dirRight:
runes = runes[:max-len(ell)]
runes = append(runes, ell...)
case dirLeft:
runes = runes[len(runes)+len(ell)-max:]
runes = append(ell, runes...)
}
return string(runes)
}
// Format writes st as json into w.
@ -141,8 +162,8 @@ func (f *Formater) Format(w io.Writer, st *gitstatus.Status) error {
// overall working tree state
if f.st.IsInitial {
fmt.Fprintf(w, "%s%s [no commits yet]", f.Styles.Branch,
truncateBranchName(f.st.LocalBranch, f.Options.BranchMaxLen, false))
branch := truncate(f.st.LocalBranch, f.Options.BranchMaxLen, f.Options.BranchTrim)
fmt.Fprintf(w, "%s%s [no commits yet]", f.Styles.Branch, branch)
f.flags()
_, err := f.b.WriteTo(w)
@ -161,7 +182,8 @@ func (f *Formater) format() {
case "branch":
f.specialState()
case "remote":
f.remote()
f.remoteBranch()
f.divergence()
case "remote-branch":
f.remoteBranch()
case "divergence":
@ -200,25 +222,15 @@ func (f *Formater) specialState() {
f.currentRef()
}
func (f *Formater) remote() {
func (f *Formater) remoteBranch() {
if f.st.RemoteBranch == "" {
return
}
f.clear()
fmt.Fprintf(&f.b, "%s%s", f.Styles.Remote,
truncateBranchName(f.st.RemoteBranch, f.Options.BranchMaxLen, true))
f.divergence()
}
func (f *Formater) remoteBranch() {
if f.st.RemoteBranch != "" {
f.clear()
fmt.Fprintf(&f.b, "%s%s", f.Styles.Remote,
truncateBranchName(f.st.RemoteBranch, f.Options.BranchMaxLen, true))
}
branch := truncate(f.st.RemoteBranch, f.Options.BranchMaxLen, f.Options.BranchTrim)
fmt.Fprintf(&f.b, "%s%s", f.Styles.Remote, branch)
}
func (f *Formater) divergence() {
@ -249,12 +261,11 @@ func (f *Formater) currentRef() {
if f.st.IsDetached {
fmt.Fprintf(&f.b, "%s%s%s", f.Styles.Branch, f.Symbols.HashPrefix, f.st.HEAD)
return
}
fmt.Fprintf(&f.b, "%s%s", f.Styles.Branch,
truncateBranchName(f.st.LocalBranch, f.Options.BranchMaxLen, false))
branch := truncate(f.st.LocalBranch, f.Options.BranchMaxLen, f.Options.BranchTrim)
fmt.Fprintf(&f.b, "%s%s", f.Styles.Branch, branch)
}
func (f *Formater) flags() {

View file

@ -1,11 +1,9 @@
package tmux
import (
"os"
"io"
"testing"
"github.com/stretchr/testify/require"
"github.com/arl/gitstatus"
)
@ -76,15 +74,17 @@ func TestFlags(t *testing.T) {
want: "StyleClear" + "StyleConflictSymbolConflict42 StyleUntrackedSymbolUntracked17",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &Formater{
Config: Config{Styles: tc.styles, Symbols: tc.symbols, Layout: tc.layout},
st: tc.st,
Config: Config{Styles: tt.styles, Symbols: tt.symbols, Layout: tt.layout},
st: tt.st,
}
f.flags()
require.EqualValues(t, tc.want, f.b.String())
if got := f.b.String(); got != tt.want {
t.Errorf("got:\n%s\n\nwant:\n%s\n", got, tt.want)
}
})
}
}
@ -166,75 +166,145 @@ func TestDivergence(t *testing.T) {
want: "StyleClear" + " ↑·128↓·41",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &Formater{
Config: Config{Styles: tc.styles, Symbols: tc.symbols},
st: tc.st,
Config: Config{Styles: tt.styles, Symbols: tt.symbols},
st: tt.st,
}
f.divergence()
require.EqualValues(t, tc.want, f.b.String())
if got := f.b.String(); got != tt.want {
t.Errorf("got:\n%s\n\nwant:\n%s\n", got, tt.want)
}
})
}
}
func TestTruncateBranchName(t *testing.T) {
func TestTruncate(t *testing.T) {
tests := []struct {
name string
branchName string
maxLen int
isRemote bool
want string
s string
max int
dir direction
want string
}{
/* trim right */
{
name: "no limit",
branchName: "foo/bar-baz",
maxLen: 0,
isRemote: false,
want: "foo/bar-baz",
s: "br",
max: 1,
dir: dirRight,
want: "b",
},
{
name: "no truncate",
branchName: "foo/bar-baz",
maxLen: 11,
isRemote: false,
want: "foo/bar-baz",
s: "br",
max: 3,
dir: dirRight,
want: "br",
},
{
name: "truncate",
branchName: "foo/bar-baz",
maxLen: 10,
isRemote: false,
want: "foo/bar...",
s: "super-long-branch",
max: 3,
dir: dirRight,
want: "...",
},
{
name: "truncate remote",
branchName: "remote/foo/bar-baz",
maxLen: 10,
isRemote: true,
want: "remote/foo/bar...",
s: "super-long-branch",
max: 15,
dir: dirRight,
want: "super-long-b...",
},
{
name: "truncate to 1",
branchName: "foo/bar-baz",
maxLen: 1,
isRemote: false,
want: ".",
s: "super-long-branch",
max: 17,
dir: dirRight,
want: "super-long-branch",
},
{
name: "truncate utf-8 name",
branchName: "foo/测试这个名字",
maxLen: 9,
isRemote: false,
want: "foo/测试...",
s: "长長的-树樹枝",
max: 6,
dir: dirRight,
want: "长長的...",
},
{
s: "super-long-branch",
max: 32,
dir: dirRight,
want: "super-long-branch",
},
{
s: "super-long-branch",
max: 0,
dir: dirRight,
want: "super-long-branch",
},
{
s: "super-long-branch",
max: -1,
dir: dirRight,
want: "super-long-branch",
},
/* trim left */
{
s: "br",
max: 1,
dir: dirLeft,
want: "r",
},
{
s: "br",
max: 3,
dir: dirLeft,
want: "br",
},
{
s: "super-long-branch",
max: 3,
dir: dirLeft,
want: "...",
},
{
s: "super-long-branch",
max: 15,
dir: dirLeft,
want: "...-long-branch",
},
{
s: "super-long-branch",
max: 17,
dir: dirLeft,
want: "super-long-branch",
},
{
s: "长長的-树樹枝",
max: 6,
dir: dirLeft,
want: "...树樹枝",
},
{
s: "super-long-branch",
max: 32,
dir: dirLeft,
want: "super-long-branch",
},
{
s: "super-long-branch",
max: 0,
dir: dirLeft,
want: "super-long-branch",
},
{
s: "super-long-branch",
max: -1,
dir: dirLeft,
want: "super-long-branch",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
branchName := truncateBranchName(tc.branchName, tc.maxLen, tc.isRemote)
require.EqualValues(t, tc.want, branchName)
for _, tt := range tests {
t.Run("", func(t *testing.T) {
if got := truncate(tt.s, tt.max, tt.dir); got != tt.want {
t.Errorf("truncate(%q, %d, %s) = %q, want %q", tt.s, tt.max, tt.dir, got, tt.want)
}
})
}
}
@ -360,6 +430,7 @@ func TestFormat(t *testing.T) {
layout: []string{"branch", " ", "remote"},
options: options{
BranchMaxLen: 9,
BranchTrim: dirRight,
},
st: &gitstatus.Status{
Porcelain: gitstatus.Porcelain{
@ -370,7 +441,33 @@ func TestFormat(t *testing.T) {
want: "StyleClear" + "StyleBranch" + "SymbolBranch" +
"StyleClear" + "StyleBranch" + "branch..." +
"StyleClear" + " " +
"StyleClear" + "StyleRemote" + "remote/branch...",
"StyleClear" + "StyleRemote" + "remote...",
},
{
name: "branch and remote, branch_max_len not zero and trim left",
styles: styles{
Clear: "StyleClear",
Branch: "StyleBranch",
Remote: "StyleRemote",
},
symbols: symbols{
Branch: "SymbolBranch",
},
layout: []string{"branch", " ", "remote"},
options: options{
BranchMaxLen: 9,
BranchTrim: dirLeft,
},
st: &gitstatus.Status{
Porcelain: gitstatus.Porcelain{
LocalBranch: "nameBranch",
RemoteBranch: "remote/nameBranch",
},
},
want: "StyleClear" + "StyleBranch" + "SymbolBranch" +
"StyleClear" + "StyleBranch" + "...Branch" +
"StyleClear" + " " +
"StyleClear" + "StyleRemote" + "...Branch",
},
{
name: "issue-32",
@ -391,19 +488,20 @@ func TestFormat(t *testing.T) {
"StyleClear" + "StyleBranch" + "branchName",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &Formater{
Config: Config{Styles: tc.styles, Symbols: tc.symbols, Layout: tc.layout, Options: tc.options},
Config: Config{Styles: tt.styles, Symbols: tt.symbols, Layout: tt.layout, Options: tt.options},
}
if err := f.Format(os.Stdout, tc.st); err != nil {
if err := f.Format(io.Discard, tt.st); err != nil {
t.Fatalf("Format error: %s", err)
}
f.format()
require.EqualValues(t, tc.want, f.b.String())
if got := f.b.String(); got != tt.want {
t.Errorf("got:\n%s\n\nwant:\n%s\n", got, tt.want)
}
})
}
}