Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use survey for prompt module #94

Merged
merged 2 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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