Skip to content

Commit

Permalink
Merge pull request #94 from johnmanjiro13/feature/survey
Browse files Browse the repository at this point in the history
feat: Use survey for prompt module
  • Loading branch information
johnmanjiro13 committed Jun 12, 2023
2 parents 564bc4f + 8c05027 commit 2615f92
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 124 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ jobs:
- name: build
run: go build .
- name: test
# We would like to use -race option but promptui has a data race bug, so we don't use it.
run: go test -v -coverprofile="coverage.txt" -covermode=atomic ./...
run: go test -v -race -coverprofile="coverage.txt" -covermode=atomic ./...
- name: upload coverage to codecov
uses: codecov/codecov-action@v3
76 changes: 32 additions & 44 deletions bumper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ package bump
import (
"bytes"
"fmt"
"io"
"os"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/Masterminds/semver/v3"
"github.com/manifoldco/promptui"
)

//go:generate mockgen -source=$GOFILE -package=mock -destination=./mock/mock_${GOPACKAGE}.go
Expand All @@ -19,6 +17,13 @@ type Gh interface {
CreateRelease(version string, repo string, isCurrent bool, option *ReleaseOption) (sout, eout *bytes.Buffer, err error)
}

//go:generate mockgen -source=$GOFILE -package=mock -destination=./mock/mock_${GOPACKAGE}.go
type Prompter interface {
Input(question string, validator survey.Validator) (string, error)
Select(question string, options []string) (string, error)
Confirm(question string) (bool, error)
}

type ReleaseOption struct {
IsDraft bool
IsPrerelease bool
Expand All @@ -32,6 +37,7 @@ type ReleaseOption struct {

type bumper struct {
gh Gh
prompter Prompter
repository string
isCurrent bool
isDraft bool
Expand All @@ -47,7 +53,10 @@ type bumper struct {
}

func NewBumper(gh Gh) *bumper {
return &bumper{gh: gh}
return &bumper{
gh: gh,
prompter: newPrompter(),
}
}

func (b *bumper) WithRepository(repository string) error {
Expand Down Expand Up @@ -130,15 +139,15 @@ func (b *bumper) Bump() error {
return err
}
} else {
nextVer, err = nextVersion(current, os.Stdin, os.Stdout)
nextVer, err = b.nextVersion(current)
if err != nil {
return err
}
}

// Skip approval if --yes is set
if !b.yes {
ok, err := approve(nextVer, os.Stdin, os.Stdout)
ok, err := b.approve(nextVer)
if err != nil {
return err
}
Expand Down Expand Up @@ -179,7 +188,7 @@ func (b *bumper) currentVersion() (current *semver.Version, isInitial bool, err
sout, eout, err := b.gh.ViewRelease(b.repository, b.isCurrent)
if err != nil {
if strings.Contains(eout.String(), "release not found") {
current, err = newVersion(os.Stdin, os.Stdout)
current, err = b.newVersion()
if err != nil {
return nil, false, err
}
Expand All @@ -196,40 +205,33 @@ func (b *bumper) currentVersion() (current *semver.Version, isInitial bool, err
return current, false, nil
}

func newVersion(sin io.ReadCloser, sout io.WriteCloser) (*semver.Version, error) {
validate := func(input string) error {
func (b *bumper) newVersion() (*semver.Version, error) {
validate := func(v interface{}) error {
input, ok := v.(string)
if !ok {
return fmt.Errorf("invalid input type. input: %v", v)
}
_, err := semver.NewVersion(input)
if err != nil {
return fmt.Errorf("invalid version. err: %w", err)
}
return nil
}

prompt := promptui.Prompt{
Label: "New version",
Validate: validate,
Stdin: sin,
Stdout: sout,
}
result, err := prompt.Run()
version, err := b.prompter.Input("New version", validate)
if err != nil {
return nil, fmt.Errorf("failed to prompt. err: %w", err)
}
return semver.NewVersion(result)
return semver.NewVersion(version)
}

func nextVersion(current *semver.Version, sin io.ReadCloser, sout io.WriteCloser) (*semver.Version, error) {
prompt := promptui.Select{
Label: fmt.Sprintf("Select next version. current: %s", current.Original()),
Items: []string{"patch", "minor", "major"},
Stdin: sin,
Stdout: sout,
}
_, bumpType, err := prompt.Run()
func (b *bumper) nextVersion(current *semver.Version) (*semver.Version, error) {
question := fmt.Sprintf("Select next version. current: %s", current.Original())
options := []string{"patch", "minor", "major"}
bumpType, err := b.prompter.Select(question, options)
if err != nil {
return nil, fmt.Errorf("failed to prompt. err: %w", err)
}

return incrementVersion(current, bumpType)
}

Expand All @@ -248,27 +250,13 @@ func incrementVersion(current *semver.Version, bumpType string) (*semver.Version
return &next, nil
}

func approve(next *semver.Version, sin io.ReadCloser, sout io.WriteCloser) (bool, error) {
validate := func(input string) error {
if input != "y" && input != "yes" && input != "n" && input != "no" {
return fmt.Errorf("invalid character. press y/n")
}
return nil
}
prompt := promptui.Prompt{
Label: fmt.Sprintf("Create release %s ? [y/n]", next.Original()),
Validate: validate,
Stdin: sin,
Stdout: sout,
}
result, err := prompt.Run()
func (b *bumper) approve(next *semver.Version) (bool, error) {
question := fmt.Sprintf("Create release %s ?", next.Original())
isApproved, err := b.prompter.Confirm(question)
if err != nil {
return false, fmt.Errorf("failed to prompt. err: %w", err)
}
if result == "y" || result == "yes" {
return true, nil
}
return false, nil
return isApproved, nil
}

func (b *bumper) createRelease(version string) (string, error) {
Expand Down
168 changes: 104 additions & 64 deletions bumper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package bump_test
import (
"bytes"
"fmt"
"io"
"strings"
"testing"

"github.com/Masterminds/semver/v3"
Expand All @@ -18,12 +16,114 @@ import (
const (
repoDocs = `name: johnmanjiro13/gh-bump
description: gh extension for bumping version of a repository`
tagList = `v0.2.1 Latest v0.2.1 2021-12-08T04:19:16Z`
tagList = `v0.1.0 Latest v0.1.0 2021-12-08T04:19:16Z`
releaseView = `title: v0.1.0
tag: v0.1.0`
)

var arrowDownAndEnter = []byte{14, 10}
func TestBumper_Bump(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

gh := mock.NewMockGh(ctrl)
prompter := mock.NewMockPrompter(ctrl)

t.Run("bump semver", func(t *testing.T) {
tests := map[string]struct {
bumpType string
next string
}{
"patch": {bumpType: "patch", next: "v0.1.1"},
"minor": {bumpType: "minor", next: "v0.2.0"},
"major": {bumpType: "major", next: "v1.0.0"},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
bumper := bump.NewBumper(gh)
bumper.SetPrompter(prompter)
gh.EXPECT().ListRelease(bumper.Repository(), bumper.IsCurrent()).Return(bytes.NewBufferString(tagList), nil, nil)
gh.EXPECT().ViewRelease(bumper.Repository(), bumper.IsCurrent()).Return(bytes.NewBufferString(releaseView), nil, nil)

prompter.EXPECT().Select("Select next version. current: v0.1.0", []string{"patch", "minor", "major"}).Return(tt.bumpType, nil)
prompter.EXPECT().Confirm(fmt.Sprintf("Create release %s ?", tt.next)).Return(true, nil)
gh.EXPECT().CreateRelease(tt.next, bumper.Repository(), bumper.IsCurrent(), &bump.ReleaseOption{}).Return(nil, nil, nil)
assert.NoError(t, bumper.Bump())
})
}
})

t.Run("bump semver with option", func(t *testing.T) {
tests := map[string]struct {
bumpType string
next string
hasError bool
}{
"patch": {bumpType: "patch", next: "v0.1.1", hasError: false},
"minor": {bumpType: "minor", next: "v0.2.0", hasError: false},
"major": {bumpType: "major", next: "v1.0.0", hasError: false},
"invalid": {bumpType: "invalid", next: "", hasError: true},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
bumper := bump.NewBumper(gh)
bumper.SetPrompter(prompter)
if tt.hasError {
assert.ErrorIsf(t, bumper.WithBumpType(tt.bumpType), bump.ErrInvalidBumpType, "got %s", tt.bumpType)
return
}

assert.NoError(t, bumper.WithBumpType(tt.bumpType))
gh.EXPECT().ListRelease(bumper.Repository(), bumper.IsCurrent()).Return(bytes.NewBufferString(tagList), nil, nil)
gh.EXPECT().ViewRelease(bumper.Repository(), bumper.IsCurrent()).Return(bytes.NewBufferString(releaseView), nil, nil)
prompter.EXPECT().Confirm(fmt.Sprintf("Create release %s ?", tt.next)).Return(true, nil)
gh.EXPECT().CreateRelease(tt.next, bumper.Repository(), bumper.IsCurrent(), &bump.ReleaseOption{}).Return(nil, nil, nil)
assert.NoError(t, bumper.Bump())
})
}
})

t.Run("cancel bump", func(t *testing.T) {
bumper := bump.NewBumper(gh)
bumper.SetPrompter(prompter)
gh.EXPECT().ListRelease(bumper.Repository(), bumper.IsCurrent()).Return(bytes.NewBufferString(tagList), nil, nil)
gh.EXPECT().ViewRelease(bumper.Repository(), bumper.IsCurrent()).Return(bytes.NewBufferString(releaseView), nil, nil)

prompter.EXPECT().Select("Select next version. current: v0.1.0", []string{"patch", "minor", "major"}).Return("patch", nil)
prompter.EXPECT().Confirm("Create release v0.1.1 ?").Return(false, nil)
assert.NoError(t, bumper.Bump())
})

t.Run("bump another repository", func(t *testing.T) {
bumper := bump.NewBumper(gh)
bumper.SetPrompter(prompter)

const repo = "johnmanjiro13/gh-bump"
assert.NoError(t, bumper.WithRepository("johnmanjiro13/gh-bump"))

gh.EXPECT().ListRelease(bumper.Repository(), bumper.IsCurrent()).Return(bytes.NewBufferString(tagList), nil, nil)
gh.EXPECT().ViewRelease(bumper.Repository(), bumper.IsCurrent()).Return(bytes.NewBufferString(releaseView), nil, nil)

prompter.EXPECT().Select("Select next version. current: v0.1.0", []string{"patch", "minor", "major"}).Return("patch", nil)
prompter.EXPECT().Confirm("Create release v0.1.1 ?").Return(true, nil)
gh.EXPECT().CreateRelease("v0.1.1", repo, false, &bump.ReleaseOption{}).Return(nil, nil, nil)
assert.NoError(t, bumper.Bump())
})

t.Run("bump with -y option", func(t *testing.T) {
bumper := bump.NewBumper(gh)
bumper.SetPrompter(prompter)

bumper.WithYes()
gh.EXPECT().ListRelease(bumper.Repository(), bumper.IsCurrent()).Return(bytes.NewBufferString(tagList), nil, nil)
gh.EXPECT().ViewRelease(bumper.Repository(), bumper.IsCurrent()).Return(bytes.NewBufferString(releaseView), nil, nil)

prompter.EXPECT().Select("Select next version. current: v0.1.0", []string{"patch", "minor", "major"}).Return("patch", nil)
gh.EXPECT().CreateRelease("v0.1.1", bumper.Repository(), bumper.IsCurrent(), &bump.ReleaseOption{}).Return(nil, nil, nil)
assert.NoError(t, bumper.Bump())
})
}

func TestBumper_WithRepository(t *testing.T) {
tests := map[string]struct {
Expand Down Expand Up @@ -236,66 +336,6 @@ func TestBumper_currentVersion(t *testing.T) {
})
}

type mockWriteCloser struct {
bytes.Buffer
}

func (m *mockWriteCloser) Close() error {
return nil
}

func TestNewVersion(t *testing.T) {
sin := io.NopCloser(strings.NewReader("v0.1.0\n"))
sout := &mockWriteCloser{bytes.Buffer{}}
newVer, err := bump.NewVersion(sin, sout)
assert.NoError(t, err)
assert.Equal(t, semver.MustParse("v0.1.0"), newVer)
}

func TestNextVersion(t *testing.T) {
sin := io.NopCloser(strings.NewReader(string(arrowDownAndEnter)))
sout := &mockWriteCloser{bytes.Buffer{}}
current := semver.MustParse("v0.1.0")
nextVer, err := bump.NextVersion(current, sin, sout)
fmt.Println(sout.String())
assert.NoError(t, err)
assert.Equal(t, semver.MustParse("v0.2.0"), nextVer)
}

func TestApprove(t *testing.T) {
tests := map[string]struct {
text string
want bool
}{
"approve with yes": {
text: "yes\n",
want: true,
},
"approve with y": {
text: "y\n",
want: true,
},
"disapprove with no": {
text: "no\n",
want: false,
},
"disapprove with n": {
text: "n\n",
want: false,
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
sin := io.NopCloser(strings.NewReader(tt.text))
sout := &mockWriteCloser{bytes.Buffer{}}
got, err := bump.Approve(semver.MustParse("v0.1.0"), sin, sout)
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

func TestIncrementVersion(t *testing.T) {
current := semver.MustParse("v0.1.0")

Expand Down
7 changes: 4 additions & 3 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ var (
ListReleases = (*bumper).listReleases
CreateRelease = (*bumper).createRelease
CurrentVersion = (*bumper).currentVersion
NewVersion = newVersion
NextVersion = nextVersion
IncrementVersion = incrementVersion
Approve = approve
)

func (b *bumper) SetPrompter(prompter Prompter) {
b.prompter = prompter
}

func (b *bumper) Repository() string {
return b.repository
}
Expand Down

0 comments on commit 2615f92

Please sign in to comment.