From ec8d38a048fde54d61eae3f750d53085fd42273e Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Wed, 4 Sep 2024 14:28:06 -0700 Subject: [PATCH 01/18] init command --- cmd/generate/config/config.go | 51 +++++++++++++++++++++++++++++++++++ cmd/generate/generate.go | 3 +++ 2 files changed, 54 insertions(+) create mode 100644 cmd/generate/config/config.go diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go new file mode 100644 index 0000000..cd70f54 --- /dev/null +++ b/cmd/generate/config/config.go @@ -0,0 +1,51 @@ +package config + +import ( + "errors" + "github.com/spf13/cobra" + + "github.com/open-sauced/pizza-cli/pkg/config" +) + +// Options for the codeowners generation command +type Options struct { + // the path to the git repository on disk to generate a codeowners file for + path string + + tty bool + loglevel int + + config *config.Spec +} + +const codeownersLongDesc string = `WARNING: Proof of concept feature. + +Generates a ~/.sauced.yaml configuration file. The attribution of emails to given entities +is based on the repository this command is ran in.` + +func NewConfigCommand() *cobra.Command { + opts := &Options{} + print(opts.path); + + cmd := &cobra.Command{ + Use: "config path/to/repo [flags]", + Short: "Generates a \"~/.sauced.yaml\" config based on the current repository", + Long: codeownersLongDesc, + Args: func(_ *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("you must provide exactly one argument: the path to the repository") + } + + path := args[0] + print(path) + + return nil + }, + + RunE: func(cmd *cobra.Command, _ []string) error { + return nil + }, + } + + return cmd +} diff --git a/cmd/generate/generate.go b/cmd/generate/generate.go index b3ff321..6d1a29f 100644 --- a/cmd/generate/generate.go +++ b/cmd/generate/generate.go @@ -6,6 +6,8 @@ import ( "github.com/spf13/cobra" "github.com/open-sauced/pizza-cli/cmd/generate/codeowners" + + "github.com/open-sauced/pizza-cli/cmd/generate/config" ) const generateLongDesc string = `WARNING: Proof of concept feature. @@ -28,6 +30,7 @@ func NewGenerateCommand() *cobra.Command { } cmd.AddCommand(codeowners.NewCodeownersCommand()) + cmd.AddCommand(config.NewConfigCommand()) return cmd } From 104bf47830a7374e7d672c750bbd05ceafcbdcd0 Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Wed, 4 Sep 2024 16:45:19 -0700 Subject: [PATCH 02/18] print every commit and its author email and name --- cmd/generate/config/config.go | 50 ++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index cd70f54..a74f0c9 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -2,6 +2,12 @@ package config import ( "errors" + "fmt" + "os" + "path/filepath" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" "github.com/spf13/cobra" "github.com/open-sauced/pizza-cli/pkg/config" @@ -12,8 +18,9 @@ type Options struct { // the path to the git repository on disk to generate a codeowners file for path string - tty bool - loglevel int + previousDays int + tty bool + loglevel int config *config.Spec } @@ -25,7 +32,6 @@ is based on the repository this command is ran in.` func NewConfigCommand() *cobra.Command { opts := &Options{} - print(opts.path); cmd := &cobra.Command{ Use: "config path/to/repo [flags]", @@ -37,15 +43,47 @@ func NewConfigCommand() *cobra.Command { } path := args[0] - print(path) - return nil + // Validate that the path is a real path on disk and accessible by the user + absPath, err := filepath.Abs(path) + if err != nil { + return err + } + + if _, err := os.Stat(absPath); os.IsNotExist(err) { + return fmt.Errorf("the provided path does not exist: %w", err) + } + + opts.path = absPath + return nil }, RunE: func(cmd *cobra.Command, _ []string) error { - return nil + // TODO: error checking based on given command + + return run(opts, cmd) }, } return cmd } + +func run(opts *Options, cmd *cobra.Command) error { + configuration := &config.Spec{} + fmt.Println("CONFIG", configuration) + + // Open repo + repo, err := git.PlainOpen(opts.path) + if err != nil { + return fmt.Errorf("error opening repo: %w", err) + } + + commitIter, err := repo.CommitObjects() + + commitIter.ForEach(func(c *object.Commit) error { + fmt.Println("COMMIT", c.Author.Email, c.Author.Name) + return nil + }) + + return nil +} From 1d5fdebd98d98bed3c6e71bbb4e747a68477e2c6 Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Wed, 4 Sep 2024 17:06:48 -0700 Subject: [PATCH 03/18] remove passed cmd to run, create attributionMap based on commits --- cmd/generate/config/config.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index a74f0c9..fd71278 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -1,10 +1,12 @@ package config import ( + "encoding/json" "errors" "fmt" "os" "path/filepath" + "slices" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" @@ -61,16 +63,16 @@ func NewConfigCommand() *cobra.Command { RunE: func(cmd *cobra.Command, _ []string) error { // TODO: error checking based on given command - return run(opts, cmd) + return run(opts) }, } return cmd } -func run(opts *Options, cmd *cobra.Command) error { - configuration := &config.Spec{} - fmt.Println("CONFIG", configuration) +func run(opts *Options) error { + attributionMap := make(map[string][]string) + fmt.Println("CONFIG", attributionMap) // Open repo repo, err := git.PlainOpen(opts.path) @@ -81,9 +83,20 @@ func run(opts *Options, cmd *cobra.Command) error { commitIter, err := repo.CommitObjects() commitIter.ForEach(func(c *object.Commit) error { - fmt.Println("COMMIT", c.Author.Email, c.Author.Name) + name := c.Author.Name + email := c.Author.Email + + doesEmailExist := slices.Contains(attributionMap[name], email) + if !doesEmailExist { + attributionMap[name] = append(attributionMap[name], email) + } + return nil }) + // for pretty print test + test, err := json.MarshalIndent(attributionMap, "", " ") + fmt.Println(string(test)) + return nil } From 62c2793a284696538f584acec451ec83eed1bd3d Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Thu, 5 Sep 2024 10:51:44 -0700 Subject: [PATCH 04/18] remove ununsed Options props, create .sauced.yaml file --- cmd/generate/config/config.go | 21 ++++++++++----------- cmd/generate/config/output.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 cmd/generate/config/output.go diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index fd71278..56b6d5e 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -1,7 +1,6 @@ package config import ( - "encoding/json" "errors" "fmt" "os" @@ -18,12 +17,7 @@ import ( // Options for the codeowners generation command type Options struct { // the path to the git repository on disk to generate a codeowners file for - path string - - previousDays int - tty bool - loglevel int - + path string config *config.Spec } @@ -72,7 +66,6 @@ func NewConfigCommand() *cobra.Command { func run(opts *Options) error { attributionMap := make(map[string][]string) - fmt.Println("CONFIG", attributionMap) // Open repo repo, err := git.PlainOpen(opts.path) @@ -86,17 +79,23 @@ func run(opts *Options) error { name := c.Author.Name email := c.Author.Email + // TODO: edge case- same email multiple names + // eg: 'coding@zeu.dev' = 'zeudev' & 'Zeu Capua' + + // AUTOMATIC: set every name and associated emails doesEmailExist := slices.Contains(attributionMap[name], email) if !doesEmailExist { attributionMap[name] = append(attributionMap[name], email) } + // TODO: INTERACTIVE: per unique email, set a name (existing or new) + return nil }) - // for pretty print test - test, err := json.MarshalIndent(attributionMap, "", " ") - fmt.Println(string(test)) + // generate an output file + // default: `~/.sauced.yaml` + generateOutputFile(".sauced.yaml", attributionMap) return nil } diff --git a/cmd/generate/config/output.go b/cmd/generate/config/output.go new file mode 100644 index 0000000..77e2ca5 --- /dev/null +++ b/cmd/generate/config/output.go @@ -0,0 +1,34 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/open-sauced/pizza-cli/pkg/config" + "github.com/open-sauced/pizza-cli/pkg/utils" +) + +func generateOutputFile(outputPath string, attributionMap map[string][]string) error { + // Open the file for writing + homeDir, err := os.UserHomeDir() + file, err := os.Create(filepath.Join(homeDir, outputPath)) + if err != nil { + return fmt.Errorf("error creating %s file: %w", outputPath, err) + } + defer file.Close() + + var config config.Spec + config.Attributions = attributionMap + + // for pretty print test + yaml, err := utils.OutputYAML(config) + + if err != nil { + return fmt.Errorf("Failed to turn into YAML") + } + + file.WriteString(yaml) + + return nil +} From 06e9bf1398ddc940dc16349b89f7492ba9b69c38 Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Thu, 5 Sep 2024 11:15:38 -0700 Subject: [PATCH 05/18] implement --output-path flag --- cmd/generate/config/config.go | 25 +++++++++++++++++-------- cmd/generate/config/output.go | 5 +---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index 56b6d5e..dd549ca 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -10,18 +10,18 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" "github.com/spf13/cobra" - - "github.com/open-sauced/pizza-cli/pkg/config" ) -// Options for the codeowners generation command +// Options for the config generation command type Options struct { // the path to the git repository on disk to generate a codeowners file for path string - config *config.Spec + + // where the '.sauced.yaml' file will go + outputPath string } -const codeownersLongDesc string = `WARNING: Proof of concept feature. +const configLongDesc string = `WARNING: Proof of concept feature. Generates a ~/.sauced.yaml configuration file. The attribution of emails to given entities is based on the repository this command is ran in.` @@ -32,7 +32,7 @@ func NewConfigCommand() *cobra.Command { cmd := &cobra.Command{ Use: "config path/to/repo [flags]", Short: "Generates a \"~/.sauced.yaml\" config based on the current repository", - Long: codeownersLongDesc, + Long: configLongDesc, Args: func(_ *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("you must provide exactly one argument: the path to the repository") @@ -57,10 +57,13 @@ func NewConfigCommand() *cobra.Command { RunE: func(cmd *cobra.Command, _ []string) error { // TODO: error checking based on given command + opts.outputPath, _ = cmd.Flags().GetString("output-path"); + return run(opts) }, } + cmd.PersistentFlags().StringP("output-path", "o", "~/", "Directory to create the `.sauced.yaml` file.") return cmd } @@ -94,8 +97,14 @@ func run(opts *Options) error { }) // generate an output file - // default: `~/.sauced.yaml` - generateOutputFile(".sauced.yaml", attributionMap) + // default: `~/.sauced.yaml` + if opts.outputPath == "~/" { + homeDir, _ := os.UserHomeDir() + generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) + } else { + generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) + } + return nil } diff --git a/cmd/generate/config/output.go b/cmd/generate/config/output.go index 77e2ca5..8cc4723 100644 --- a/cmd/generate/config/output.go +++ b/cmd/generate/config/output.go @@ -3,16 +3,13 @@ package config import ( "fmt" "os" - "path/filepath" "github.com/open-sauced/pizza-cli/pkg/config" "github.com/open-sauced/pizza-cli/pkg/utils" ) func generateOutputFile(outputPath string, attributionMap map[string][]string) error { - // Open the file for writing - homeDir, err := os.UserHomeDir() - file, err := os.Create(filepath.Join(homeDir, outputPath)) + file, err := os.Create(outputPath) if err != nil { return fmt.Errorf("error creating %s file: %w", outputPath, err) } From 702ccefbbcc933bca9cca9ffd0e77faf3089cfa6 Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Fri, 6 Sep 2024 13:28:56 -0700 Subject: [PATCH 06/18] wip interactive mode --- cmd/generate/config/config.go | 132 ++++++++++++++++++++++++++++++---- go.mod | 5 +- go.sum | 2 + 3 files changed, 126 insertions(+), 13 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index dd549ca..6bc9eb6 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -9,16 +9,26 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" + "github.com/jpmcb/gopherlogs" + "github.com/open-sauced/pizza-cli/pkg/logging" "github.com/spf13/cobra" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" ) // Options for the config generation command type Options struct { // the path to the git repository on disk to generate a codeowners file for - path string + path string // where the '.sauced.yaml' file will go outputPath string + + // whether to use interactive mode + isInteractive bool } const configLongDesc string = `WARNING: Proof of concept feature. @@ -57,17 +67,21 @@ func NewConfigCommand() *cobra.Command { RunE: func(cmd *cobra.Command, _ []string) error { // TODO: error checking based on given command - opts.outputPath, _ = cmd.Flags().GetString("output-path"); + opts.outputPath, _ = cmd.Flags().GetString("output-path") + opts.isInteractive, _ = cmd.Flags().GetBool("interactive") return run(opts) }, } cmd.PersistentFlags().StringP("output-path", "o", "~/", "Directory to create the `.sauced.yaml` file.") + cmd.PersistentFlags().BoolP("interactive", "i", true, "Whether to be interactive") return cmd } func run(opts *Options) error { + logger, err := gopherlogs.NewLogger() + attributionMap := make(map[string][]string) // Open repo @@ -84,20 +98,30 @@ func run(opts *Options) error { // TODO: edge case- same email multiple names // eg: 'coding@zeu.dev' = 'zeudev' & 'Zeu Capua' - - // AUTOMATIC: set every name and associated emails - doesEmailExist := slices.Contains(attributionMap[name], email) - if !doesEmailExist { - attributionMap[name] = append(attributionMap[name], email) + + if !opts.isInteractive { + doesEmailExist := slices.Contains(attributionMap[name], email) + if !doesEmailExist { + // AUTOMATIC: set every name and associated emails + attributionMap[name] = append(attributionMap[name], email) + } + } else { + // TODO: INTERACTIVE: per unique email, set a name (existing or new or ignore) + var uniqueEmails []string + if slices.Contains(uniqueEmails, email) { + uniqueEmails = append(uniqueEmails, email) + } + program := tea.NewProgram(initialModel(uniqueEmails)) + if _, err := program.Run(); err != nil { + logger.V(logging.LogError).Info(err.Error()) + } } - - // TODO: INTERACTIVE: per unique email, set a name (existing or new) - return nil }) + // generate an output file - // default: `~/.sauced.yaml` + // default: `~/.sauced.yaml` if opts.outputPath == "~/" { homeDir, _ := os.UserHomeDir() generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) @@ -105,6 +129,90 @@ func run(opts *Options) error { generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) } - return nil } + +// Bubbletea for Interactive Mode + +type model struct { + textInput textinput.Model + help help.Model + keymap keymap + + attributionMap map[string][]string + uniqueEmails []string + currentIndex int +} + +type keymap struct{} + +func (k keymap) ShortHelp() []key.Binding { + return []key.Binding{ + key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "complete")), + key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")), + key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")), + key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")), + } +} + +func (k keymap) FullHelp() [][]key.Binding { + return [][]key.Binding{k.ShortHelp()} +} + +func initialModel(uniqueEmails []string) model { + ti := textinput.New() + ti.Placeholder = "name" + ti.Focus() + ti.ShowSuggestions = true + + return model{ + textInput: ti, + help: help.New(), + keymap: keymap{}, + + attributionMap: make(map[string][]string), + uniqueEmails: uniqueEmails, + currentIndex: 0, + } +} + +func (m model) Init() tea.Cmd { + return textinput.Blink +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + currentEmail := m.uniqueEmails[m.currentIndex] + + + existingUsers := make([]string, len(m.attributionMap)) + for k := range m.attributionMap { + existingUsers = append(existingUsers, k) + } + + m.textInput.SetSuggestions(existingUsers) + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + + case tea.KeyEnter: + m.attributionMap[currentEmail] = append(m.attributionMap[currentEmail], m.textInput.Value()) + } + } + + m.textInput, cmd = m.textInput.Update(msg) + + return m, cmd +} + +func (m model) View() string { + return fmt.Sprintf( + "Found email %s - who to attribute to?: %s\n\n%s\n", + m.uniqueEmails[m.currentIndex], + m.textInput.View(), + m.help.View(m.keymap), + ) +} diff --git a/go.mod b/go.mod index a28aa98..dd05559 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,10 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require github.com/charmbracelet/bubbletea v0.27.1 // indirect +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/charmbracelet/bubbletea v0.27.1 // indirect +) require ( dario.cat/mergo v1.0.0 // indirect diff --git a/go.sum b/go.sum index 3adc853..51da393 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= From 852cd6196abdf3503028b0f3fb69177f7e272fa1 Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Fri, 6 Sep 2024 14:23:40 -0700 Subject: [PATCH 07/18] go through each email with autocomplete --- cmd/generate/config/config.go | 68 +++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index 6bc9eb6..fc9f713 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -9,8 +9,6 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/jpmcb/gopherlogs" - "github.com/open-sauced/pizza-cli/pkg/logging" "github.com/spf13/cobra" "github.com/charmbracelet/bubbles/help" @@ -80,8 +78,6 @@ func NewConfigCommand() *cobra.Command { } func run(opts *Options) error { - logger, err := gopherlogs.NewLogger() - attributionMap := make(map[string][]string) // Open repo @@ -92,13 +88,14 @@ func run(opts *Options) error { commitIter, err := repo.CommitObjects() + var uniqueEmails []string commitIter.ForEach(func(c *object.Commit) error { name := c.Author.Name email := c.Author.Email // TODO: edge case- same email multiple names // eg: 'coding@zeu.dev' = 'zeudev' & 'Zeu Capua' - + if !opts.isInteractive { doesEmailExist := slices.Contains(attributionMap[name], email) if !doesEmailExist { @@ -106,19 +103,18 @@ func run(opts *Options) error { attributionMap[name] = append(attributionMap[name], email) } } else { - // TODO: INTERACTIVE: per unique email, set a name (existing or new or ignore) - var uniqueEmails []string - if slices.Contains(uniqueEmails, email) { + if !slices.Contains(uniqueEmails, email) { uniqueEmails = append(uniqueEmails, email) } - program := tea.NewProgram(initialModel(uniqueEmails)) - if _, err := program.Run(); err != nil { - logger.V(logging.LogError).Info(err.Error()) - } } return nil }) + // TODO: INTERACTIVE: per unique email, set a name (existing or new or ignore) + program := tea.NewProgram(initialModel(uniqueEmails)) + if _, err := program.Run(); err != nil { + return fmt.Errorf(err.Error()) + } // generate an output file // default: `~/.sauced.yaml` @@ -136,12 +132,12 @@ func run(opts *Options) error { type model struct { textInput textinput.Model - help help.Model - keymap keymap + help help.Model + keymap keymap attributionMap map[string][]string - uniqueEmails []string - currentIndex int + uniqueEmails []string + currentIndex int } type keymap struct{} @@ -151,7 +147,9 @@ func (k keymap) ShortHelp() []key.Binding { key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "complete")), key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")), key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")), + key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "ignore email")), key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")), + key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), } } @@ -167,12 +165,12 @@ func initialModel(uniqueEmails []string) model { return model{ textInput: ti, - help: help.New(), - keymap: keymap{}, + help: help.New(), + keymap: keymap{}, attributionMap: make(map[string][]string), - uniqueEmails: uniqueEmails, - currentIndex: 0, + uniqueEmails: uniqueEmails, + currentIndex: 0, } } @@ -184,23 +182,31 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd currentEmail := m.uniqueEmails[m.currentIndex] - - existingUsers := make([]string, len(m.attributionMap)) + existingUsers := make([]string, 0, len(m.attributionMap)) for k := range m.attributionMap { - existingUsers = append(existingUsers, k) + existingUsers = append(existingUsers, k) } m.textInput.SetSuggestions(existingUsers) switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.Type { - case tea.KeyCtrlC, tea.KeyEsc: - return m, tea.Quit - - case tea.KeyEnter: - m.attributionMap[currentEmail] = append(m.attributionMap[currentEmail], m.textInput.Value()) + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + + case tea.KeyCtrlI: + m.currentIndex++ + return m, nil + + case tea.KeyEnter: + m.attributionMap[m.textInput.Value()] = append(m.attributionMap[currentEmail], currentEmail) + m.currentIndex++ + if m.currentIndex > len(m.attributionMap) { + return m, tea.Quit } + return m, nil + } } m.textInput, cmd = m.textInput.Update(msg) @@ -212,7 +218,7 @@ func (m model) View() string { return fmt.Sprintf( "Found email %s - who to attribute to?: %s\n\n%s\n", m.uniqueEmails[m.currentIndex], - m.textInput.View(), + m.textInput.View(), m.help.View(m.keymap), ) } From 6908e1626ab7adac8102ce4483f8f4f6d1a5511c Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Mon, 9 Sep 2024 03:07:29 -0700 Subject: [PATCH 08/18] tidy go.mod --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 603715d..757f5ea 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22 require ( github.com/charmbracelet/bubbles v0.19.0 + github.com/charmbracelet/bubbletea v0.27.1 github.com/charmbracelet/lipgloss v0.13.0 github.com/cli/browser v1.3.0 github.com/go-git/go-git/v5 v5.12.0 @@ -17,8 +18,7 @@ require ( ) require ( - github.com/charmbracelet/bubbletea v0.27.1 // indirect - github.com/charmbracelet/bubbletea v0.27.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect From 545974ee2e4fbc8b9c2c3effdf3299d940c6ea2e Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Mon, 9 Sep 2024 03:48:49 -0700 Subject: [PATCH 09/18] make interactive mode non default, working interactive mode --- cmd/generate/config/config.go | 71 ++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index fc9f713..5b8b5e5 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "slices" + "strings" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" @@ -72,8 +73,8 @@ func NewConfigCommand() *cobra.Command { }, } - cmd.PersistentFlags().StringP("output-path", "o", "~/", "Directory to create the `.sauced.yaml` file.") - cmd.PersistentFlags().BoolP("interactive", "i", true, "Whether to be interactive") + cmd.PersistentFlags().StringP("output-path", "o", "./", "Directory to create the `.sauced.yaml` file.") + cmd.PersistentFlags().BoolP("interactive", "i", false, "Whether to be interactive") return cmd } @@ -111,18 +112,18 @@ func run(opts *Options) error { }) // TODO: INTERACTIVE: per unique email, set a name (existing or new or ignore) - program := tea.NewProgram(initialModel(uniqueEmails)) - if _, err := program.Run(); err != nil { - return fmt.Errorf(err.Error()) - } - - // generate an output file - // default: `~/.sauced.yaml` - if opts.outputPath == "~/" { - homeDir, _ := os.UserHomeDir() - generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) + if opts.isInteractive { + program := tea.NewProgram(initialModel(opts, uniqueEmails)) + if _, err := program.Run(); err != nil { + return fmt.Errorf(err.Error()) + } } else { - generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) + if opts.outputPath == "~/" { + homeDir, _ := os.UserHomeDir() + generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) + } else { + generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) + } } return nil @@ -135,6 +136,7 @@ type model struct { help help.Model keymap keymap + opts *Options attributionMap map[string][]string uniqueEmails []string currentIndex int @@ -144,7 +146,6 @@ type keymap struct{} func (k keymap) ShortHelp() []key.Binding { return []key.Binding{ - key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "complete")), key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")), key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")), key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "ignore email")), @@ -157,7 +158,7 @@ func (k keymap) FullHelp() [][]key.Binding { return [][]key.Binding{k.ShortHelp()} } -func initialModel(uniqueEmails []string) model { +func initialModel(opts *Options, uniqueEmails []string) model { ti := textinput.New() ti.Placeholder = "name" ti.Focus() @@ -168,6 +169,7 @@ func initialModel(uniqueEmails []string) model { help: help.New(), keymap: keymap{}, + opts: opts, attributionMap: make(map[string][]string), uniqueEmails: uniqueEmails, currentIndex: 0, @@ -200,11 +202,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case tea.KeyEnter: - m.attributionMap[m.textInput.Value()] = append(m.attributionMap[currentEmail], currentEmail) - m.currentIndex++ - if m.currentIndex > len(m.attributionMap) { - return m, tea.Quit + if len(strings.Trim(m.textInput.Value(), " ")) == 0 { + return m, nil + } + m.attributionMap[m.textInput.Value()] = append(m.attributionMap[m.textInput.Value()], currentEmail) + m.textInput.Reset() + if m.currentIndex+1 >= len(m.uniqueEmails) { + return m, runOutputGeneration(m.opts, m.attributionMap) } + + m.currentIndex++ return m, nil } } @@ -215,10 +222,32 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m model) View() string { + currentEmail := "" + if m.currentIndex < len(m.uniqueEmails) { + currentEmail = m.uniqueEmails[m.currentIndex] + } + return fmt.Sprintf( - "Found email %s - who to attribute to?: %s\n\n%s\n", - m.uniqueEmails[m.currentIndex], + "Found email %s - who to attribute to?: \n%s\n\n%s\n", + currentEmail, m.textInput.View(), m.help.View(m.keymap), ) } + +func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea.Cmd { + // generate an output file + // default: `./.sauced.yaml` + // fallback for home directories + return func() tea.Msg { + if opts.outputPath == "~/" { + homeDir, _ := os.UserHomeDir() + generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) + } else { + generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) + } + + return tea.Quit() + } + +} From 583c49a1d2db5ca1c2631d9648280be7b64de455 Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Mon, 9 Sep 2024 10:31:17 -0700 Subject: [PATCH 10/18] add more error handling --- cmd/generate/config/config.go | 42 ++++++++++++++++++++++++++--------- cmd/generate/config/output.go | 10 ++++++--- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index 5b8b5e5..8fbab34 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -84,19 +84,20 @@ func run(opts *Options) error { // Open repo repo, err := git.PlainOpen(opts.path) if err != nil { - return fmt.Errorf("error opening repo: %w", err) + return fmt.Errorf("Error opening repo: %w", err) } commitIter, err := repo.CommitObjects() + if err != nil { + return fmt.Errorf("Error opening repo commits: %w", err) + } + var uniqueEmails []string - commitIter.ForEach(func(c *object.Commit) error { + err = commitIter.ForEach(func(c *object.Commit) error { name := c.Author.Name email := c.Author.Email - // TODO: edge case- same email multiple names - // eg: 'coding@zeu.dev' = 'zeudev' & 'Zeu Capua' - if !opts.isInteractive { doesEmailExist := slices.Contains(attributionMap[name], email) if !doesEmailExist { @@ -111,18 +112,31 @@ func run(opts *Options) error { return nil }) - // TODO: INTERACTIVE: per unique email, set a name (existing or new or ignore) + if err != nil { + return fmt.Errorf("Error iterating over repo commits: %w", err) + } + + // INTERACTIVE: per unique email, set a name (existing or new or ignore) if opts.isInteractive { program := tea.NewProgram(initialModel(opts, uniqueEmails)) if _, err := program.Run(); err != nil { - return fmt.Errorf(err.Error()) + return fmt.Errorf("Error running interactive mode: %w", err) } } else { + // generate an output file + // default: `./.sauced.yaml` + // fallback for home directories if opts.outputPath == "~/" { homeDir, _ := os.UserHomeDir() - generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) + err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) + if err != nil { + return fmt.Errorf("Error generating output file: %w", err) + } } else { - generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) + err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) + if err != nil { + return fmt.Errorf("Error generating output file: %w", err) + } } } @@ -242,9 +256,15 @@ func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea. return func() tea.Msg { if opts.outputPath == "~/" { homeDir, _ := os.UserHomeDir() - generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) + err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) + if err != nil { + return fmt.Errorf("Error generating output file: %w", err) + } } else { - generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) + err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) + if err != nil { + return fmt.Errorf("Error generating output file: %w", err) + } } return tea.Quit() diff --git a/cmd/generate/config/output.go b/cmd/generate/config/output.go index 8cc4723..f5a06aa 100644 --- a/cmd/generate/config/output.go +++ b/cmd/generate/config/output.go @@ -11,7 +11,7 @@ import ( func generateOutputFile(outputPath string, attributionMap map[string][]string) error { file, err := os.Create(outputPath) if err != nil { - return fmt.Errorf("error creating %s file: %w", outputPath, err) + return fmt.Errorf("Error creating %s file: %w", outputPath, err) } defer file.Close() @@ -22,10 +22,14 @@ func generateOutputFile(outputPath string, attributionMap map[string][]string) e yaml, err := utils.OutputYAML(config) if err != nil { - return fmt.Errorf("Failed to turn into YAML") + return fmt.Errorf("Failed to turn into YAML: %w", err) } - file.WriteString(yaml) + _, err = file.WriteString(yaml) + + if err != nil { + return fmt.Errorf("Failed to turn into YAML: %w", err) + } return nil } From 613ced5710d145b91dae6bc7b2496cc6d2248aea Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Mon, 9 Sep 2024 11:10:38 -0700 Subject: [PATCH 11/18] change else to else if, replace one case switch to if statement --- cmd/generate/config/config.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index 8fbab34..17d2431 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -104,10 +104,8 @@ func run(opts *Options) error { // AUTOMATIC: set every name and associated emails attributionMap[name] = append(attributionMap[name], email) } - } else { - if !slices.Contains(uniqueEmails, email) { - uniqueEmails = append(uniqueEmails, email) - } + } else if !slices.Contains(uniqueEmails, email) { + uniqueEmails = append(uniqueEmails, email) } return nil }) @@ -205,9 +203,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.textInput.SetSuggestions(existingUsers) - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.Type { + keyMsg, ok := msg.(tea.KeyMsg) + + if ok { + switch keyMsg.Type { case tea.KeyCtrlC, tea.KeyEsc: return m, tea.Quit From 33c205787394de76db7992345b66194040e72057 Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Mon, 9 Sep 2024 11:41:34 -0700 Subject: [PATCH 12/18] lint via golangci-liint --fix --- cmd/generate/config/config.go | 7 +++---- cmd/generate/generate.go | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index 17d2431..4742544 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -8,14 +8,13 @@ import ( "slices" "strings" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/spf13/cobra" - "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/spf13/cobra" ) // Options for the config generation command diff --git a/cmd/generate/generate.go b/cmd/generate/generate.go index 6d1a29f..fee0146 100644 --- a/cmd/generate/generate.go +++ b/cmd/generate/generate.go @@ -6,7 +6,6 @@ import ( "github.com/spf13/cobra" "github.com/open-sauced/pizza-cli/cmd/generate/codeowners" - "github.com/open-sauced/pizza-cli/cmd/generate/config" ) From 5e7e4bcba1f1c933556240035df91f213944e3b4 Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Mon, 9 Sep 2024 17:31:09 -0700 Subject: [PATCH 13/18] lowercase error messages --- cmd/generate/config/config.go | 16 ++++++++-------- cmd/generate/config/output.go | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index 4742544..c543b2a 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -83,13 +83,13 @@ func run(opts *Options) error { // Open repo repo, err := git.PlainOpen(opts.path) if err != nil { - return fmt.Errorf("Error opening repo: %w", err) + return fmt.Errorf("error opening repo: %w", err) } commitIter, err := repo.CommitObjects() if err != nil { - return fmt.Errorf("Error opening repo commits: %w", err) + return fmt.Errorf("error opening repo commits: %w", err) } var uniqueEmails []string @@ -110,14 +110,14 @@ func run(opts *Options) error { }) if err != nil { - return fmt.Errorf("Error iterating over repo commits: %w", err) + return fmt.Errorf("error iterating over repo commits: %w", err) } // INTERACTIVE: per unique email, set a name (existing or new or ignore) if opts.isInteractive { program := tea.NewProgram(initialModel(opts, uniqueEmails)) if _, err := program.Run(); err != nil { - return fmt.Errorf("Error running interactive mode: %w", err) + return fmt.Errorf("error running interactive mode: %w", err) } } else { // generate an output file @@ -127,12 +127,12 @@ func run(opts *Options) error { homeDir, _ := os.UserHomeDir() err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) if err != nil { - return fmt.Errorf("Error generating output file: %w", err) + return fmt.Errorf("error generating output file: %w", err) } } else { err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) if err != nil { - return fmt.Errorf("Error generating output file: %w", err) + return fmt.Errorf("error generating output file: %w", err) } } } @@ -256,12 +256,12 @@ func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea. homeDir, _ := os.UserHomeDir() err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) if err != nil { - return fmt.Errorf("Error generating output file: %w", err) + return fmt.Errorf("error generating output file: %w", err) } } else { err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) if err != nil { - return fmt.Errorf("Error generating output file: %w", err) + return fmt.Errorf("error generating output file: %w", err) } } diff --git a/cmd/generate/config/output.go b/cmd/generate/config/output.go index f5a06aa..f8db065 100644 --- a/cmd/generate/config/output.go +++ b/cmd/generate/config/output.go @@ -11,7 +11,7 @@ import ( func generateOutputFile(outputPath string, attributionMap map[string][]string) error { file, err := os.Create(outputPath) if err != nil { - return fmt.Errorf("Error creating %s file: %w", outputPath, err) + return fmt.Errorf("error creating %s file: %w", outputPath, err) } defer file.Close() @@ -22,13 +22,13 @@ func generateOutputFile(outputPath string, attributionMap map[string][]string) e yaml, err := utils.OutputYAML(config) if err != nil { - return fmt.Errorf("Failed to turn into YAML: %w", err) + return fmt.Errorf("failed to turn into YAML: %w", err) } _, err = file.WriteString(yaml) if err != nil { - return fmt.Errorf("Failed to turn into YAML: %w", err) + return fmt.Errorf("failed to turn into YAML: %w", err) } return nil From 38dda02d412ad9114985c416fdff08cd78390863 Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Mon, 9 Sep 2024 17:35:46 -0700 Subject: [PATCH 14/18] remove todo and empty lines --- cmd/generate/config/config.go | 3 --- go.mod | 1 - 2 files changed, 4 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index c543b2a..22d0156 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -63,8 +63,6 @@ func NewConfigCommand() *cobra.Command { }, RunE: func(cmd *cobra.Command, _ []string) error { - // TODO: error checking based on given command - opts.outputPath, _ = cmd.Flags().GetString("output-path") opts.isInteractive, _ = cmd.Flags().GetBool("interactive") @@ -267,5 +265,4 @@ func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea. return tea.Quit() } - } diff --git a/go.mod b/go.mod index 757f5ea..cbf822d 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,6 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - ) require ( From 75eccb8d10e28d155bd2b2fd4f6e3f807eaa2a57 Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Mon, 9 Sep 2024 17:46:21 -0700 Subject: [PATCH 15/18] move bubbletea program to spec.go --- cmd/generate/config/config.go | 140 +-------------------------------- cmd/generate/config/spec.go | 142 ++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 136 deletions(-) create mode 100644 cmd/generate/config/spec.go diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index 22d0156..5c571a4 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -6,11 +6,7 @@ import ( "os" "path/filepath" "slices" - "strings" - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" @@ -27,11 +23,11 @@ type Options struct { // whether to use interactive mode isInteractive bool -} -const configLongDesc string = `WARNING: Proof of concept feature. + ttyDisabled bool +} -Generates a ~/.sauced.yaml configuration file. The attribution of emails to given entities +const configLongDesc string = `Generates a ~/.sauced.yaml configuration file. The attribution of emails to given entities is based on the repository this command is ran in.` func NewConfigCommand() *cobra.Command { @@ -65,6 +61,7 @@ func NewConfigCommand() *cobra.Command { RunE: func(cmd *cobra.Command, _ []string) error { opts.outputPath, _ = cmd.Flags().GetString("output-path") opts.isInteractive, _ = cmd.Flags().GetBool("interactive") + opts.ttyDisabled, _ = cmd.Flags().GetBool("tty-disable") return run(opts) }, @@ -137,132 +134,3 @@ func run(opts *Options) error { return nil } - -// Bubbletea for Interactive Mode - -type model struct { - textInput textinput.Model - help help.Model - keymap keymap - - opts *Options - attributionMap map[string][]string - uniqueEmails []string - currentIndex int -} - -type keymap struct{} - -func (k keymap) ShortHelp() []key.Binding { - return []key.Binding{ - key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")), - key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")), - key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "ignore email")), - key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")), - key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), - } -} - -func (k keymap) FullHelp() [][]key.Binding { - return [][]key.Binding{k.ShortHelp()} -} - -func initialModel(opts *Options, uniqueEmails []string) model { - ti := textinput.New() - ti.Placeholder = "name" - ti.Focus() - ti.ShowSuggestions = true - - return model{ - textInput: ti, - help: help.New(), - keymap: keymap{}, - - opts: opts, - attributionMap: make(map[string][]string), - uniqueEmails: uniqueEmails, - currentIndex: 0, - } -} - -func (m model) Init() tea.Cmd { - return textinput.Blink -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - currentEmail := m.uniqueEmails[m.currentIndex] - - existingUsers := make([]string, 0, len(m.attributionMap)) - for k := range m.attributionMap { - existingUsers = append(existingUsers, k) - } - - m.textInput.SetSuggestions(existingUsers) - - keyMsg, ok := msg.(tea.KeyMsg) - - if ok { - switch keyMsg.Type { - case tea.KeyCtrlC, tea.KeyEsc: - return m, tea.Quit - - case tea.KeyCtrlI: - m.currentIndex++ - return m, nil - - case tea.KeyEnter: - if len(strings.Trim(m.textInput.Value(), " ")) == 0 { - return m, nil - } - m.attributionMap[m.textInput.Value()] = append(m.attributionMap[m.textInput.Value()], currentEmail) - m.textInput.Reset() - if m.currentIndex+1 >= len(m.uniqueEmails) { - return m, runOutputGeneration(m.opts, m.attributionMap) - } - - m.currentIndex++ - return m, nil - } - } - - m.textInput, cmd = m.textInput.Update(msg) - - return m, cmd -} - -func (m model) View() string { - currentEmail := "" - if m.currentIndex < len(m.uniqueEmails) { - currentEmail = m.uniqueEmails[m.currentIndex] - } - - return fmt.Sprintf( - "Found email %s - who to attribute to?: \n%s\n\n%s\n", - currentEmail, - m.textInput.View(), - m.help.View(m.keymap), - ) -} - -func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea.Cmd { - // generate an output file - // default: `./.sauced.yaml` - // fallback for home directories - return func() tea.Msg { - if opts.outputPath == "~/" { - homeDir, _ := os.UserHomeDir() - err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) - if err != nil { - return fmt.Errorf("error generating output file: %w", err) - } - } else { - err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) - if err != nil { - return fmt.Errorf("error generating output file: %w", err) - } - } - - return tea.Quit() - } -} diff --git a/cmd/generate/config/spec.go b/cmd/generate/config/spec.go new file mode 100644 index 0000000..b8732ba --- /dev/null +++ b/cmd/generate/config/spec.go @@ -0,0 +1,142 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +// Bubbletea for Interactive Mode + +type model struct { + textInput textinput.Model + help help.Model + keymap keymap + + opts *Options + attributionMap map[string][]string + uniqueEmails []string + currentIndex int +} + +type keymap struct{} + +func (k keymap) ShortHelp() []key.Binding { + return []key.Binding{ + key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")), + key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")), + key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "ignore email")), + key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")), + key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), + } +} + +func (k keymap) FullHelp() [][]key.Binding { + return [][]key.Binding{k.ShortHelp()} +} + +func initialModel(opts *Options, uniqueEmails []string) model { + ti := textinput.New() + ti.Placeholder = "name" + ti.Focus() + ti.ShowSuggestions = true + + return model{ + textInput: ti, + help: help.New(), + keymap: keymap{}, + + opts: opts, + attributionMap: make(map[string][]string), + uniqueEmails: uniqueEmails, + currentIndex: 0, + } +} + +func (m model) Init() tea.Cmd { + return textinput.Blink +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + currentEmail := m.uniqueEmails[m.currentIndex] + + existingUsers := make([]string, 0, len(m.attributionMap)) + for k := range m.attributionMap { + existingUsers = append(existingUsers, k) + } + + m.textInput.SetSuggestions(existingUsers) + + keyMsg, ok := msg.(tea.KeyMsg) + + if ok { + switch keyMsg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + + case tea.KeyCtrlI: + m.currentIndex++ + return m, nil + + case tea.KeyEnter: + if len(strings.Trim(m.textInput.Value(), " ")) == 0 { + return m, nil + } + m.attributionMap[m.textInput.Value()] = append(m.attributionMap[m.textInput.Value()], currentEmail) + m.textInput.Reset() + if m.currentIndex+1 >= len(m.uniqueEmails) { + return m, runOutputGeneration(m.opts, m.attributionMap) + } + + m.currentIndex++ + return m, nil + } + } + + m.textInput, cmd = m.textInput.Update(msg) + + return m, cmd +} + +func (m model) View() string { + currentEmail := "" + if m.currentIndex < len(m.uniqueEmails) { + currentEmail = m.uniqueEmails[m.currentIndex] + } + + return fmt.Sprintf( + "Found email %s - who to attribute to?: \n%s\n\n%s\n", + currentEmail, + m.textInput.View(), + m.help.View(m.keymap), + ) +} + +func runOutputGeneration(opts *Options, attributionMap map[string][]string) tea.Cmd { + // generate an output file + // default: `./.sauced.yaml` + // fallback for home directories + return func() tea.Msg { + if opts.outputPath == "~/" { + homeDir, _ := os.UserHomeDir() + err := generateOutputFile(filepath.Join(homeDir, ".sauced.yaml"), attributionMap) + if err != nil { + return fmt.Errorf("error generating output file: %w", err) + } + } else { + err := generateOutputFile(filepath.Join(opts.outputPath, ".sauced.yaml"), attributionMap) + if err != nil { + return fmt.Errorf("error generating output file: %w", err) + } + } + + return tea.Quit() + } +} From be30445a9546f9884fbce635358260876336fa69 Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Mon, 9 Sep 2024 17:49:11 -0700 Subject: [PATCH 16/18] check for tty-disable flag --- cmd/generate/config/config.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index 5c571a4..99e9825 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -24,6 +24,7 @@ type Options struct { // whether to use interactive mode isInteractive bool + // from global config ttyDisabled bool } @@ -92,7 +93,7 @@ func run(opts *Options) error { name := c.Author.Name email := c.Author.Email - if !opts.isInteractive { + if opts.ttyDisabled || !opts.isInteractive { doesEmailExist := slices.Contains(attributionMap[name], email) if !doesEmailExist { // AUTOMATIC: set every name and associated emails @@ -109,7 +110,7 @@ func run(opts *Options) error { } // INTERACTIVE: per unique email, set a name (existing or new or ignore) - if opts.isInteractive { + if opts.isInteractive && !opts.ttyDisabled { program := tea.NewProgram(initialModel(opts, uniqueEmails)) if _, err := program.Run(); err != nil { return fmt.Errorf("error running interactive mode: %w", err) From bfdd66918c433934b629de1582624d97665d013e Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Tue, 10 Sep 2024 13:07:30 -0700 Subject: [PATCH 17/18] change placeholder/help copy --- cmd/generate/config/config.go | 4 ++-- cmd/generate/config/spec.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/generate/config/config.go b/cmd/generate/config/config.go index 99e9825..e5e3e19 100644 --- a/cmd/generate/config/config.go +++ b/cmd/generate/config/config.go @@ -28,7 +28,7 @@ type Options struct { ttyDisabled bool } -const configLongDesc string = `Generates a ~/.sauced.yaml configuration file. The attribution of emails to given entities +const configLongDesc string = `Generates a ".sauced.yaml" configuration file. The attribution of emails to given entities is based on the repository this command is ran in.` func NewConfigCommand() *cobra.Command { @@ -36,7 +36,7 @@ func NewConfigCommand() *cobra.Command { cmd := &cobra.Command{ Use: "config path/to/repo [flags]", - Short: "Generates a \"~/.sauced.yaml\" config based on the current repository", + Short: "Generates a \".sauced.yaml\" config based on the current repository", Long: configLongDesc, Args: func(_ *cobra.Command, args []string) error { if len(args) != 1 { diff --git a/cmd/generate/config/spec.go b/cmd/generate/config/spec.go index b8732ba..4b24c8d 100644 --- a/cmd/generate/config/spec.go +++ b/cmd/generate/config/spec.go @@ -29,8 +29,8 @@ type keymap struct{} func (k keymap) ShortHelp() []key.Binding { return []key.Binding{ - key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next")), - key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev")), + key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next suggestion")), + key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "prev suggestion")), key.NewBinding(key.WithKeys("ctrl+i"), key.WithHelp("ctrl+i", "ignore email")), key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")), key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), @@ -43,7 +43,7 @@ func (k keymap) FullHelp() [][]key.Binding { func initialModel(opts *Options, uniqueEmails []string) model { ti := textinput.New() - ti.Placeholder = "name" + ti.Placeholder = "username" ti.Focus() ti.ShowSuggestions = true From 315a97b9c4cdfbda43c6a1b24fe5b23aa46ca51a Mon Sep 17 00:00:00 2001 From: Zeu Capua <coding@zeu.dev> Date: Tue, 10 Sep 2024 13:42:09 -0700 Subject: [PATCH 18/18] check if last attribution is ignored --- cmd/generate/config/spec.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/generate/config/spec.go b/cmd/generate/config/spec.go index 4b24c8d..5284cc9 100644 --- a/cmd/generate/config/spec.go +++ b/cmd/generate/config/spec.go @@ -83,6 +83,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyCtrlI: m.currentIndex++ + if m.currentIndex+1 >= len(m.uniqueEmails) { + return m, runOutputGeneration(m.opts, m.attributionMap) + } return m, nil case tea.KeyEnter: