Skip to content

Commit

Permalink
support custom formatter
Browse files Browse the repository at this point in the history
fix: #48
  • Loading branch information
kyoh86 committed Dec 8, 2019
1 parent 16e2b3d commit 0c109ea
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 84 deletions.
2 changes: 1 addition & 1 deletion command/empty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestEmpty(t *testing.T) {
}))
assert.NoError(t, Get(ctx, false, false, false, mustRepo("kyoh86/gogh")))
assert.NoError(t, Fork(ctx, false, false, false, false, "", "", mustRepo("kyoh86/gogh")))
assert.NoError(t, List(ctx, gogh.ProjectListFormatShort, false, false, ""))
assert.NoError(t, List(ctx, gogh.ShortFormatter(), false, false, ""))
proj1 := filepath.Join(tmp, "github.com", "kyoh86", "gogh", ".git")
require.NoError(t, os.MkdirAll(proj1, 0755))
assert.NoError(t, Root(ctx, false))
Expand Down
66 changes: 66 additions & 0 deletions command/formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package command

import (
"fmt"
"strings"

"github.com/kyoh86/gogh/gogh"
)

// ProjectListFormat specifies how gogh prints a project.
type ProjectListFormat struct {
label string
formatter gogh.ProjectListFormatter
}

func (f *ProjectListFormat) Set(value string) error {
switch value {
case ProjectListFormatLabelRelPath:
f.formatter = gogh.RelPathFormatter()
return nil
case ProjectListFormatLabelFullPath:
f.formatter = gogh.FullPathFormatter()
return nil
case ProjectListFormatLabelURL:
f.formatter = gogh.URLFormatter()
return nil
case ProjectListFormatLabelShort:
f.formatter = gogh.ShortFormatter()
return nil
}
if strings.HasPrefix(value, "custom:") {
er, err := gogh.CustomFormatter(strings.TrimPrefix(value, "custom:"))
if err != nil {
return fmt.Errorf("format custom: must have following valid template %w", err)
}
f.formatter = er
return nil
}
return fmt.Errorf("format must be one of %s or 'custom:<advanced format>', got '%s'", strings.Join(ProjectListFormats(), ","), value)
}

func (f *ProjectListFormat) Formatter() gogh.ProjectListFormatter {
return f.formatter
}

// ProjectListFormat choices.
const (
ProjectListFormatLabelShort = "short"
ProjectListFormatLabelFullPath = "full"
ProjectListFormatLabelURL = "url"
ProjectListFormatLabelRelPath = "relative"
)

func (f ProjectListFormat) String() string {
return f.label
}

// ProjectListFormats shows all of ProjectListFormat constants.
func ProjectListFormats() []string {
return []string{
ProjectListFormatLabelShort,
ProjectListFormatLabelFullPath,
ProjectListFormatLabelURL,
ProjectListFormatLabelRelPath,
}
}
7 changes: 1 addition & 6 deletions command/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,12 @@ import (
)

// List local projects
func List(ctx gogh.Context, format gogh.ProjectListFormat, primary bool, isPublic bool, query string) error {
func List(ctx gogh.Context, formatter gogh.ProjectListFormatter, primary bool, isPublic bool, query string) error {
var walk gogh.Walker = gogh.Walk
if primary {
walk = gogh.WalkInPrimary
}

formatter, err := format.Formatter()
if err != nil {
return err
}

if err := gogh.Query(ctx, query, walk, func(p *gogh.Project) error {
if isPublic {
repo, err := gogh.ParseProject(p)
Expand Down
39 changes: 28 additions & 11 deletions command/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/stretchr/testify/require"
)

func ExampleList() {
func ExampleList_url() {
tmp, _ := ioutil.TempDir(os.TempDir(), "gogh-test")
defer os.RemoveAll(tmp)
_ = os.MkdirAll(filepath.Join(tmp, "example.com", "kyoh86", "gogh", ".git"), 0755)
Expand All @@ -24,7 +24,7 @@ func ExampleList() {
VRoot: []string{tmp},
GitHub: config.GitHubConfig{Host: "example.com"},
},
gogh.ProjectListFormatURL,
gogh.URLFormatter(),
true,
false,
"",
Expand All @@ -36,28 +36,45 @@ func ExampleList() {
// https://example.com/owner/name
}

func TestList(t *testing.T) {
func ExampleList_custom() {
tmp, _ := ioutil.TempDir(os.TempDir(), "gogh-test")
defer os.RemoveAll(tmp)
require.NoError(t, os.MkdirAll(filepath.Join(tmp, "example.com", "kyoh86", "gogh", ".git"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(tmp, "example.com", "owner", "name", ".git"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(tmp, "example.com", "owner", "empty"), 0755))
_ = os.MkdirAll(filepath.Join(tmp, "example.com", "kyoh86", "gogh", ".git"), 0755)
_ = os.MkdirAll(filepath.Join(tmp, "example.com", "owner", "name", ".git"), 0755)
_ = os.MkdirAll(filepath.Join(tmp, "example.com", "owner", "empty"), 0755)

assert.Error(t, command.List(&config.Config{
fmter, err := gogh.CustomFormatter("{{short .}};;{{relative .}}")
if err != nil {
panic(err)
}
if err := command.List(&config.Config{
VRoot: []string{tmp},
GitHub: config.GitHubConfig{Host: "example.com"},
},
gogh.ProjectListFormat("invalid format"),
false,
fmter,
true,
false,
"",
), "invalid format")
); err != nil {
panic(err)
}
// Unordered output:
// gogh;;example.com/kyoh86/gogh
// name;;example.com/owner/name
}

func TestList(t *testing.T) {
tmp, _ := ioutil.TempDir(os.TempDir(), "gogh-test")
defer os.RemoveAll(tmp)
require.NoError(t, os.MkdirAll(filepath.Join(tmp, "example.com", "kyoh86", "gogh", ".git"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(tmp, "example.com", "owner", "name", ".git"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(tmp, "example.com", "owner", "empty"), 0755))

assert.Error(t, command.List(&config.Config{
VRoot: []string{tmp, "/\x00"},
GitHub: config.GitHubConfig{Host: "example.com"},
},
gogh.ProjectListFormatURL,
gogh.URLFormatter(),
false,
false,
"",
Expand Down
99 changes: 99 additions & 0 deletions gogh/custom_formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package gogh

import (
"bytes"
"fmt"
"io"
"text/template"
)

type customListFormatter struct {
template *template.Template
*shortListFormatter
*fullPathFormatter
*urlFormatter
*relPathFormatter
}

func CustomFormatter(format string) (ProjectListFormatter, error) {
// share list
simple := &simpleCollector{}

// tmp.Funcs(
short := &shortListFormatter{
dups: map[string]bool{},
simpleCollector: simple,
}
full := &fullPathFormatter{
simpleCollector: simple,
}
url := &urlFormatter{
simpleCollector: simple,
}
rel := &relPathFormatter{
simpleCollector: simple,
}
// NOTE: FuncMap is the type of the map defining the mapping from names to functions.
// Each function must have either a single return value, or two return values of
// which the second has type error. In that case, if the second (error)
// return value evaluates to non-nil during execution, execution terminates and
// Execute returns that error.
//
// When template execution invokes a function with an argument list, that list
// must be assignable to the function's parameter types. Functions meant to
// apply to arguments of arbitrary type can use parameters of type interface{} or
// of type reflect.Value. Similarly, functions meant to return a result of arbitrary
// type can return interface{} or reflect.Value.
tmp, err := template.New("").Funcs(template.FuncMap{
"short": formatToTemplateFunc(short.format),
"full": formatToTemplateFunc(full.format),
"url": formatToTemplateFunc(url.format),
"relative": formatToTemplateFunc(rel.format),
"null": func() string { return "\x00" },
}).Parse(format)
if err != nil {
return nil, fmt.Errorf("failed to parse custom format %w", err)
}

return &customListFormatter{
template: tmp,
shortListFormatter: short,
fullPathFormatter: full,
urlFormatter: url,
relPathFormatter: rel,
}, nil
}

func formatToTemplateFunc(format func(w io.Writer, project *Project) error) func(project *Project) (string, error) {
return func(project *Project) (string, error) {
buf := new(bytes.Buffer)
if err := format(buf, project); err != nil {
return "", err
}
return buf.String(), nil
}
}
func (f *customListFormatter) format(w io.Writer, project *Project) error {
return f.template.Execute(w, project)
}

func (f *customListFormatter) Add(r *Project) {
// add to short list formatter (it has special "Add" func)
f.shortListFormatter.Add(r)
}

func (f *customListFormatter) Len() int {
return f.shortListFormatter.Len()
}

func (f *customListFormatter) PrintAll(w io.Writer, sep string) error {
for _, project := range f.shortListFormatter.list {
if err := f.format(w, project); err != nil {
return err
}
if _, err := fmt.Fprint(w, sep); err != nil {
return err
}
}
return nil
}
63 changes: 63 additions & 0 deletions gogh/custom_formatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package gogh

import (
"bytes"
"strings"
"testing"

"github.com/kyoh86/gogh/internal/context"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCustomListFormatter(t *testing.T) {
t.Run("null separator", func(t *testing.T) {
project1, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo")
require.NoError(t, err)
formatter, err := CustomFormatter("{{short .}}{{null}}{{full .}}{{null}}{{relative .}}")
require.NoError(t, err)
formatter.Add(project1)
assert.Equal(t, 1, formatter.Len())
var buf bytes.Buffer
require.NoError(t, formatter.PrintAll(&buf, "\r\n"))
expected := "foo\x00/go/src/github.com/kyoh86/foo\x00github.com/kyoh86/foo\r\n"
assert.Equal(t, expected, buf.String())
})
t.Run("normal separator", func(t *testing.T) {
project1, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/foo")
require.NoError(t, err)
project2, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/bar")
require.NoError(t, err)
project3, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh87/bar")
require.NoError(t, err)
project4, err := parseProject(&context.MockContext{MGitHubHost: "example.com"}, "/go/src", "/go/src/example.com/kyoh86/bar")
require.NoError(t, err)
project5, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/go/src", "/go/src/github.com/kyoh86/baz")
require.NoError(t, err)
project6, err := parseProject(&context.MockContext{MGitHubHost: "github.com"}, "/foo", "/foo/github.com/kyoh86/baz")
require.NoError(t, err)

formatter, err := CustomFormatter("{{short .}};;{{full .}};;{{relative .}}")
require.NoError(t, err)
formatter.Add(project1)
formatter.Add(project2)
formatter.Add(project3)
formatter.Add(project4)
formatter.Add(project5)
formatter.Add(project6)
assert.Equal(t, 6, formatter.Len())
var buf bytes.Buffer
require.NoError(t, formatter.PrintAll(&buf, "\n"))
expected := `
foo ;; /go/src/ github.com/kyoh86/foo ;;github.com/kyoh86/foo
github.com/kyoh86/bar ;; /go/src/ github.com/kyoh86/bar ;;github.com/kyoh86/bar
kyoh87/bar ;; /go/src/ github.com/kyoh87/bar ;;github.com/kyoh87/bar
example.com/kyoh86/bar ;; /go/src/ example.com/kyoh86/bar;;example.com/kyoh86/bar
/go/src/github.com/kyoh86/baz ;; /go/src/ github.com/kyoh86/baz ;;github.com/kyoh86/baz
/foo/github.com/kyoh86/baz ;; /foo/ github.com/kyoh86/baz ;;github.com/kyoh86/baz
`
expected = strings.Replace(expected, " ", "", -1)
expected = strings.TrimLeft(expected, "\n")
assert.Equal(t, expected, buf.String())
})
}
Loading

0 comments on commit 0c109ea

Please sign in to comment.