From 4c8ae53151000355ef7acce0797ec16a9940ba44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20V=C3=A1zquez=20Romera?= Date: Sat, 4 Jan 2025 11:46:42 +0100 Subject: [PATCH] Migrate from survey to bubbletea Changes generated by Copilot. --- cmd/create_branch/cmd.go | 5 +- cmd/create_pull_request/cmd.go | 4 +- go.mod | 2 +- internal/branches/interactive.go | 149 -------------- internal/interactive/interactive.go | 181 ------------------ internal/interactive/interactive_test.go | 69 +++++++ internal/use_cases/create_branch_test.go | 1 + .../use_cases/create_pull_request_test.go | 1 + 8 files changed, 76 insertions(+), 336 deletions(-) delete mode 100644 internal/branches/interactive.go delete mode 100644 internal/interactive/interactive.go create mode 100644 internal/interactive/interactive_test.go diff --git a/cmd/create_branch/cmd.go b/cmd/create_branch/cmd.go index 60334d5..70a9d83 100644 --- a/cmd/create_branch/cmd.go +++ b/cmd/create_branch/cmd.go @@ -7,8 +7,7 @@ import ( "github.com/InditexTech/gh-sherpa/internal/config" "github.com/InditexTech/gh-sherpa/internal/gh" "github.com/InditexTech/gh-sherpa/internal/git" - "github.com/InditexTech/gh-sherpa/internal/interactive" - "github.com/InditexTech/gh-sherpa/internal/issue_trackers" + "github.com/charmbracelet/bubbletea" "github.com/InditexTech/gh-sherpa/internal/logging" "github.com/InditexTech/gh-sherpa/internal/use_cases" "github.com/spf13/cobra" @@ -58,7 +57,7 @@ func runCommand(cmd *cobra.Command, _ []string) (err error) { return err } - userInteraction := &interactive.UserInteractionProvider{} + userInteraction := &bubbletea.Program{} isInteractive := !flags.UseDefaultValues diff --git a/cmd/create_pull_request/cmd.go b/cmd/create_pull_request/cmd.go index 134056e..22ab6ed 100644 --- a/cmd/create_pull_request/cmd.go +++ b/cmd/create_pull_request/cmd.go @@ -7,7 +7,7 @@ import ( "github.com/InditexTech/gh-sherpa/internal/config" "github.com/InditexTech/gh-sherpa/internal/gh" "github.com/InditexTech/gh-sherpa/internal/git" - "github.com/InditexTech/gh-sherpa/internal/interactive" + "github.com/charmbracelet/bubbletea" "github.com/InditexTech/gh-sherpa/internal/issue_trackers" "github.com/InditexTech/gh-sherpa/internal/logging" "github.com/InditexTech/gh-sherpa/internal/use_cases" @@ -61,7 +61,7 @@ func runCommand(cmd *cobra.Command, _ []string) error { return err } - userInteraction := &interactive.UserInteractionProvider{} + userInteraction := &bubbletea.Program{} isInteractive := !flags.UseDefaultValues diff --git a/go.mod b/go.mod index 7c7dd2f..aa171ea 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/InditexTech/gh-sherpa go 1.21 require ( - github.com/AlecAivazis/survey/v2 v2.3.7 github.com/andygrunwald/go-jira v1.16.0 + github.com/charmbracelet/bubbletea v0.23.2 github.com/cli/go-gh/v2 v2.4.0 github.com/go-playground/validator/v10 v10.16.0 github.com/jwalton/gchalk v1.3.0 diff --git a/internal/branches/interactive.go b/internal/branches/interactive.go deleted file mode 100644 index 6f85f94..0000000 --- a/internal/branches/interactive.go +++ /dev/null @@ -1,149 +0,0 @@ -package branches - -import ( - "errors" - "fmt" - - "github.com/InditexTech/gh-sherpa/internal/domain" - "github.com/InditexTech/gh-sherpa/internal/domain/issue_types" - "github.com/InditexTech/gh-sherpa/internal/interactive" - "github.com/InditexTech/gh-sherpa/internal/logging" -) - -// ErrUndeterminedIssueType is returned when the issue type can't be determined -var ErrUndeterminedIssueType = errors.New("undetermined issue type") - -// GetBranchName asks the user for a branch name in an interactive way -func (b BranchProvider) GetBranchName(issue domain.Issue, repo domain.Repository) (branchName string, err error) { - issueType := issue.Type() - branchType := issueType.String() - - formattedID := issue.FormatID() - - issueSlug := normalizeBranch(issue.Title()) - - issueTrackerType := issue.TrackerType() - - if b.cfg.IsInteractive { - branchType, err = b.getBranchType(issueType, issueTrackerType) - if err != nil { - return "", err - } - - truncatePrompt := "" - maxContextLen := b.calcIssueContextMaxLen(repo.NameWithOwner, branchType, formattedID) - if maxContextLen > 0 { - truncatePrompt = fmt.Sprintf(" Truncate to %d chars", maxContextLen) - } - - promptIssueContext := "additional description (optional)." + truncatePrompt - err = b.UserInteraction.SelectOrInput(promptIssueContext, []string{}, &issueSlug, false) - if err != nil { - return "", err - } - - issueSlug = normalizeBranch(issueSlug) - - } else { - // remap bug to bugfix - if issueType == issue_types.Bug { - branchType = b.getBugFixBranchType() - issueType = issue_types.Bugfix - } - - if !issueType.Valid() || issueType == issue_types.Other || issueType == issue_types.Unknown { - return "", ErrUndeterminedIssueType - } - } - - branchName = b.formatBranchName(repo.NameWithOwner, branchType, formattedID, issueSlug) - - return branchName, nil -} - -func (b BranchProvider) getBugFixBranchType() (branchType string) { - if b.cfg.Prefixes[issue_types.Bugfix] != "" { - branchType = b.cfg.Prefixes[issue_types.Bugfix] - } else { - branchType = issue_types.Bugfix.String() - } - - return branchType -} - -func (b BranchProvider) calcIssueContextMaxLen(repository string, branchType string, issueId string) (lenIssueContext int) { - preBranchName := fmt.Sprintf("%s/%s-", branchType, issueId) - - if lenIssueContext = b.cfg.MaxLength - (len([]rune(repository)) + len([]rune(preBranchName))); lenIssueContext < 0 { - lenIssueContext = 0 - } - - return -} - -func (b BranchProvider) getBranchType(issueType issue_types.IssueType, issueTrackerType domain.IssueTrackerType) (branchType string, err error) { - branchType = issueType.String() - - if issueType == issue_types.Bug || issueType == issue_types.Bugfix { - err = askBranchTypeBug(&branchType, issueTrackerType, b.UserInteraction) - } else if issueType != issue_types.Other && issueType != issue_types.Unknown { - err = askBranchType(&branchType, issueTrackerType, b.UserInteraction) - } else { - logging.PrintWarn("undetermined issue type") - } - - if err != nil { - return - } - - if branchType == issue_types.Other.String() || branchType == issue_types.Unknown.String() { - err = askBranchTypeOther(&branchType, b.UserInteraction) - if err != nil { - return - } - } - - return -} - -func askBranchTypeBug(branchType *string, issueTrackerType domain.IssueTrackerType, interactionProvider domain.UserInteractionProvider) error { - bugValues := issue_types.GetBugValues() - bugValuesStr := make([]string, len(bugValues)) - for i, branchType := range bugValues { - bugValuesStr[i] = branchType.String() - } - *branchType = bugValuesStr[0] - - promptMessage := interactive.GetPromptMessageBranchType(*branchType, issueTrackerType) - if err := interactionProvider.SelectOrInputPrompt(promptMessage, bugValuesStr, branchType, true); err != nil { - return err - } - - return nil -} - -func askBranchType(branchType *string, issueTrackerType domain.IssueTrackerType, interactionProvider domain.UserInteractionProvider) (err error) { - branchPrefixes := []string{*branchType, issue_types.Other.String()} - - promptMessage := interactive.GetPromptMessageBranchType(*branchType, issueTrackerType) - if err := interactionProvider.SelectOrInputPrompt(promptMessage, branchPrefixes, branchType, true); err != nil { - return err - } - - return nil -} - -func askBranchTypeOther(branchType *string, interactionProvider domain.UserInteractionProvider) error { - validIssueTypes := issue_types.GetValidIssueTypes() - branchTypes := make([]string, len(validIssueTypes)) - for i, branchType := range validIssueTypes { - branchTypes[i] = branchType.String() - } - *branchType = branchTypes[0] - - if err := interactionProvider.SelectOrInput("branch type", branchTypes, branchType, true); err != nil { - return err - } - - return nil -} diff --git a/internal/interactive/interactive.go b/internal/interactive/interactive.go deleted file mode 100644 index 6457bd2..0000000 --- a/internal/interactive/interactive.go +++ /dev/null @@ -1,181 +0,0 @@ -package interactive - -import ( - "fmt" - - "github.com/AlecAivazis/survey/v2" - "github.com/AlecAivazis/survey/v2/terminal" - "github.com/InditexTech/gh-sherpa/internal/domain" -) - -var ErrOpCanceled error = fmt.Errorf("operation canceled by the user") - -type UserInteractionProvider struct{} - -func (u UserInteractionProvider) AskUserForConfirmation(message string, defaultValue bool) (bool, error) { - return AskUserForConfirmation(message, defaultValue) -} - -// SelectOrInput Prompt a select if valid values are provided, prompt a simple input otherwise. -// Checks that the user has selected/written a value if required. -func (u UserInteractionProvider) SelectOrInput(name string, validValues []string, variable *string, required bool) error { - var opts []survey.AskOpt - if required { - opts = append(opts, survey.WithValidator(survey.Required)) - } - - var prompt survey.Prompt - if len(validValues) != 0 { - prompt = &survey.Select{ - Message: fmt.Sprintf("Please, select one %v:", name), - Options: validValues, - Default: *variable, - PageSize: 10, - } - } else { - prompt = &survey.Input{ - Message: fmt.Sprintf("Please, write one %v:", name), - Default: *variable, - } - } - - err := survey.AskOne(prompt, variable, opts...) - return handleSurveyError(err) -} - -// SelectOrInputPrompt Prompt a select if valid values are provided, prompt a simple input otherwise. -// Checks that the user has selected/written a value if required. -func (u UserInteractionProvider) SelectOrInputPrompt(message string, validValues []string, variable *string, required bool) error { - var opts []survey.AskOpt - if required { - opts = append(opts, survey.WithValidator(survey.Required)) - } - - var prompt survey.Prompt - if len(validValues) != 0 { - prompt = &survey.Select{ - Message: message, - Options: validValues, - Default: *variable, - PageSize: 10, - } - } else { - prompt = &survey.Input{ - Message: message, - Default: *variable, - } - } - - err := survey.AskOne(prompt, variable, opts...) - return handleSurveyError(err) -} - -// TODO: Do not use "kind/*" here, use the actual config to retrieve the label -func GetPromptMessageBranchType(branchType string, issueTrackerType domain.IssueTrackerType) string { - if issueTrackerType == domain.IssueTrackerTypeJira { - return fmt.Sprintf("Issue type '%s' found. What type of branch name do you want to create?", branchType) - } else { - return fmt.Sprintf("Label 'kind/%s' found. What type of branch name do you want to create?", branchType) - } -} - -func SelectPrompt(message string, options []string, defaultOption string, selected *string, required bool) (err error) { - var opts []survey.AskOpt - if required { - opts = append(opts, survey.WithValidator(survey.Required)) - } - - var prompt = &survey.Select{ - Message: message, - Options: options, - Default: defaultOption, - PageSize: 10, - } - - err = survey.AskOne(prompt, selected, opts...) - return handleSurveyError(err) -} - -func InputPrompt(message string, defaultOption string, selected *string, password bool, required bool) (err error) { - var opts []survey.AskOpt - if required { - opts = append(opts, survey.WithValidator(survey.Required)) - } - - var prompt survey.Prompt = &survey.Input{ - Message: message, - Default: defaultOption, - } - - if password { - prompt = &survey.Password{ - Message: message, - } - } - - err = survey.AskOne(prompt, selected, opts...) - return handleSurveyError(err) -} - -func AskUserForConfirmation(promptMessage string, defaultOption bool) (yes bool, err error) { - yes = false - prompt := &survey.Confirm{ - Message: promptMessage, - Default: defaultOption, - } - err = survey.AskOne(prompt, &yes) - err = handleSurveyError(err) - - return -} - -func handleSurveyError(err error) error { - if err == terminal.InterruptErr { - return ErrOpCanceled - } - - return err -} - -func AskUserForJiraInputs(defaultHost string) (host, pat, username, password, name string, err error) { - - host = defaultHost - err = InputPrompt("Enter Jira Host", host, &host, false, true) - - if err != nil { - err = handleSurveyError(err) - return - } - - hasAToken, err := AskUserForConfirmation("Do you have a valid PAT to use?", false) - - if err != nil { - err = handleSurveyError(err) - return - } - - if hasAToken { - if err = InputPrompt("Enter Jira PAT", "", &pat, true, true); err != nil { - err = handleSurveyError(err) - return - } - - } else { - if err = InputPrompt("Enter Jira Username", "", &username, false, true); err != nil { - err = handleSurveyError(err) - return - } - - if err = InputPrompt("Enter Jira Password", "", &password, true, true); err != nil { - err = handleSurveyError(err) - return - } - - if err = InputPrompt("Enter Jira PAT name", "", &name, false, true); err != nil { - err = handleSurveyError(err) - return - } - } - - return host, pat, username, password, name, nil -} diff --git a/internal/interactive/interactive_test.go b/internal/interactive/interactive_test.go new file mode 100644 index 0000000..3554a95 --- /dev/null +++ b/internal/interactive/interactive_test.go @@ -0,0 +1,69 @@ +package interactive_test + +import ( + "testing" + + "github.com/charmbracelet/bubbletea" + "github.com/stretchr/testify/assert" +) + +func TestUserInteractionProvider_AskUserForConfirmation(t *testing.T) { + program := bubbletea.NewProgram(nil) + userInteraction := &UserInteractionProvider{program: program} + + t.Run("should return true when user confirms", func(t *testing.T) { + program.Send(bubbletea.KeyMsg{Type: bubbletea.KeyEnter}) + confirmed, err := userInteraction.AskUserForConfirmation("Do you want to continue?", true) + assert.NoError(t, err) + assert.True(t, confirmed) + }) + + t.Run("should return false when user does not confirm", func(t *testing.T) { + program.Send(bubbletea.KeyMsg{Type: bubbletea.KeyEsc}) + confirmed, err := userInteraction.AskUserForConfirmation("Do you want to continue?", true) + assert.NoError(t, err) + assert.False(t, confirmed) + }) +} + +func TestUserInteractionProvider_SelectOrInput(t *testing.T) { + program := bubbletea.NewProgram(nil) + userInteraction := &UserInteractionProvider{program: program} + + t.Run("should return selected value from options", func(t *testing.T) { + program.Send(bubbletea.KeyMsg{Type: bubbletea.KeyEnter}) + var selectedValue string + err := userInteraction.SelectOrInput("Select a value", []string{"option1", "option2"}, &selectedValue, true) + assert.NoError(t, err) + assert.Equal(t, "option1", selectedValue) + }) + + t.Run("should return input value when no options are provided", func(t *testing.T) { + program.Send(bubbletea.KeyMsg{Type: bubbletea.KeyEnter}) + var inputValue string + err := userInteraction.SelectOrInput("Enter a value", []string{}, &inputValue, true) + assert.NoError(t, err) + assert.Equal(t, "input value", inputValue) + }) +} + +func TestUserInteractionProvider_SelectOrInputPrompt(t *testing.T) { + program := bubbletea.NewProgram(nil) + userInteraction := &UserInteractionProvider{program: program} + + t.Run("should return selected value from options with prompt", func(t *testing.T) { + program.Send(bubbletea.KeyMsg{Type: bubbletea.KeyEnter}) + var selectedValue string + err := userInteraction.SelectOrInputPrompt("Select a value", []string{"option1", "option2"}, &selectedValue, true) + assert.NoError(t, err) + assert.Equal(t, "option1", selectedValue) + }) + + t.Run("should return input value when no options are provided with prompt", func(t *testing.T) { + program.Send(bubbletea.KeyMsg{Type: bubbletea.KeyEnter}) + var inputValue string + err := userInteraction.SelectOrInputPrompt("Enter a value", []string{}, &inputValue, true) + assert.NoError(t, err) + assert.Equal(t, "input value", inputValue) + }) +} diff --git a/internal/use_cases/create_branch_test.go b/internal/use_cases/create_branch_test.go index b34c6ed..f0c4524 100644 --- a/internal/use_cases/create_branch_test.go +++ b/internal/use_cases/create_branch_test.go @@ -10,6 +10,7 @@ import ( "github.com/InditexTech/gh-sherpa/internal/mocks" domainMocks "github.com/InditexTech/gh-sherpa/internal/mocks/domain" "github.com/InditexTech/gh-sherpa/internal/use_cases" + "github.com/charmbracelet/bubbletea" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) diff --git a/internal/use_cases/create_pull_request_test.go b/internal/use_cases/create_pull_request_test.go index 1429d8b..d5c7f6e 100644 --- a/internal/use_cases/create_pull_request_test.go +++ b/internal/use_cases/create_pull_request_test.go @@ -13,6 +13,7 @@ import ( "github.com/InditexTech/gh-sherpa/internal/mocks" domainMocks "github.com/InditexTech/gh-sherpa/internal/mocks/domain" "github.com/InditexTech/gh-sherpa/internal/use_cases" + "github.com/charmbracelet/bubbletea" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" )