diff --git a/src/cmd/gh.go b/src/cmd/gh.go new file mode 100644 index 0000000..b29de2c --- /dev/null +++ b/src/cmd/gh.go @@ -0,0 +1,78 @@ +/* +Copyright © 2025 NAME HERE +*/ +package cmd + +import ( + "fmt" + + "github.com/Slug-Boi/cocommit/src/cmd/tui" + "github.com/Slug-Boi/cocommit/src/cmd/utils" + //"github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" +) + +// ghProfileCmd represents the ghProfile command +func GHCmd () *cobra.Command { + return &cobra.Command{ + Use: "gh ", + Short: "This command will create add a github profile to your author list for use in commits", + Long: `This command will create add a github profile to your author list. + You just have to run the command with a username of the github profile you want to add. + The email will be added manually by following the TUI or adding the email flag to the command.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + email, _ := cmd.Flags().GetString("email") + shortname, _ := cmd.Flags().GetString("shortname") + longname, _ := cmd.Flags().GetString("longname") + username, _ := cmd.Flags().GetString("username") + groups, _ := cmd.Flags().GetStringSlice("groups") + exclude, _ := cmd.Flags().GetBool("exclude") + + user := utils.FetchGithubProfile(args[0]) + + // Update values if flags are set + if shortname != "" { + user.Shortname = shortname + } + if longname != "" { + user.Longname = longname + } + if username != "" { + user.Username = username + } + if len(groups) > 0 { + user.Groups = groups + } + if exclude { + user.Ex = true + } + + if email != "" { + user.Email = email + if utils.CheckUserFields(user) { + utils.CreateAuthor(user) + // print sucess message + //fmt.Print(lipgloss.NewStyle().Foreground(lipgloss.Color("170")).Render("Author added successfully")) + fmt.Print("Author added successfully\n") + } else { + panic("Invalid author data") + } + } else { + // run the TUI to get the email + tui.EntryGHAuthorModel(user) + } + }, + } +} + +func init() { + ghCmd := GHCmd() + rootCmd.AddCommand(ghCmd) + ghCmd.Flags().StringP("email", "@", "", "Email to be used for the author") + ghCmd.Flags().StringP("longname", "n", "", "Name to be used for the author") + ghCmd.Flags().StringP("username", "u", "", "Username to be used for the author") + ghCmd.Flags().StringP("shortname", "s", "", "Shortname to be used for the author") + ghCmd.Flags().BoolP("exclude", "e", false, "Exclude the author from the list of authors") + ghCmd.Flags().StringSliceP("groups", "g", []string{}, "Groups to add the author to") +} diff --git a/src/cmd/tui/tui_author.go b/src/cmd/tui/tui_author.go index 7ec54d3..ff84351 100644 --- a/src/cmd/tui/tui_author.go +++ b/src/cmd/tui/tui_author.go @@ -4,7 +4,6 @@ package tui // from the Bubbles component library. import ( - "encoding/json" "fmt" "os" "strings" @@ -14,7 +13,6 @@ import ( "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - //"github.com/inancgumus/screen" ) var ( @@ -36,6 +34,63 @@ type model_ca struct { inputs []textinput.Model quitting bool exclude bool + errorModel *errorModel +} + +// Error popup model +type errorModel struct { + missing []string + visible bool +} + +func errorGetMissingFields(m model_ca) { + inpLen := len(m.inputs) + if !tempAuthorToggle { + inpLen -= 1 + } + + if len(m.inputs) > 0 { + for i := 0; i < inpLen; i++ { + if m.inputs[i].Value() == "" { + m.errorModel.missing = append(m.errorModel.missing, "- "+strings.Split(m.inputs[i].Placeholder,"(")[0]) + } + } + } else { + m.errorModel.missing = append(m.errorModel.missing, "GIGA ERROR NO INPUTS") + } + +} + +func (e errorModel) View() string { + var sb strings.Builder + sb.WriteString("Error") + if len(e.missing) > 0 { + sb.WriteString("\nMissing fields: \n") + sb.WriteString(strings.Join(e.missing, "\n")) + } + + // Create centered content + content := lipgloss.JoinVertical( + lipgloss.Left, // Changed from Center to Left for better alignment + sb.String(), + "\n\n[enter/esc]", + + ) + + // Create the error box + errorBox := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("9")). + Padding(1, 2). + Width(40). + Foreground(lipgloss.Color("9")). + Background(lipgloss.Color("0")). + Align(lipgloss.Center). + Render(content) + + return lipgloss.NewStyle(). + Padding(1, 0). + Render(errorBox) } var parent_m *model @@ -45,13 +100,13 @@ func createAuthorModel(old_m *model) model_ca { m := model_ca{ inputs: make([]textinput.Model, 5), + errorModel: intitialErrorModel(), } var t textinput.Model for i := range m.inputs { t = textinput.New() t.Cursor.Style = cursorStyle - //t.CharLimit = 32 switch i { case 0: @@ -75,11 +130,71 @@ func createAuthorModel(old_m *model) model_ca { return m } +func intitialErrorModel() *errorModel { + return &errorModel{ + missing: []string{}, + visible: false, + } +} + +func createGHAuthorModel(old_m *model, user utils.User) model_ca { + parent_m = old_m + + m := model_ca{ + inputs: make([]textinput.Model, 5), + errorModel: intitialErrorModel(), + } + + var t textinput.Model + for i := range m.inputs { + t = textinput.New() + t.Cursor.Style = cursorStyle + + switch i { + case 0: + t.Placeholder = "Shortname (e.g. jo)" + t.SetValue(user.Shortname) + t.Focus() + t.PromptStyle = focusedStyle + t.TextStyle = focusedStyle + case 1: + t.Placeholder = "Long name (e.g. JohnDoe)" + t.SetValue(user.Longname) + case 2: + t.Placeholder = "Username (e.g. JohnDoe-gh)" + t.SetValue(user.Username) + case 3: + t.Placeholder = "Email (e.g. JohnDoe@domain.do" + t.SetValue("") + case 4: + t.Placeholder = "Group tags (e.g. gr1|gr2)" + t.SetValue(strings.Join(user.Groups, "|")) + } + + m.inputs[i] = t + } + + return m +} + +func EntryGHAuthorModel(user utils.User) { + model := createGHAuthorModel(&model{}, user) + + print(model.inputs[0].Value()) + + if _, err := tea.NewProgram(model).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} + + func tempAuthorModel(old_m *model) model_ca { parent_m = old_m m := model_ca{ inputs: make([]textinput.Model, 2), + errorModel: intitialErrorModel(), } var t textinput.Model @@ -110,13 +225,35 @@ func (m model_ca) Init() tea.Cmd { return textinput.Blink } +func updateErrorPopup(m model_ca, msg tea.Msg) (tea.Model, tea.Cmd) { + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "enter", "esc", "ctrl+c": + m.errorModel.missing = []string{} + m.errorModel.visible = false + return m, nil + } + } + + return m, nil +} + func (m model_ca) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.errorModel.visible { + return updateErrorPopup(m, msg) + } + switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": m.inputs = nil - return nil, nil + if parent_m.keys != nil { + return nil, nil + } + return m, tea.Quit // Set focus to next input case "tab", "shift+tab", "enter", "up", "down": @@ -126,8 +263,17 @@ func (m model_ca) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if !tempAuthorToggle { if s == "enter" && m.focusIndex == len(m.inputs)+1 { m.quitting = true - m.AddAuthor() - return model{list: parent_m.list}, tea.ClearScreen + m.errorModel.visible = m.AddAuthor() + if m.errorModel.visible { + m.quitting = false + return m, nil + } + if parent_m.keys != nil { + return model{list: parent_m.list}, tea.ClearScreen + } else { + m.quitting = true + return m, tea.Quit + } } else if s == "enter" && m.focusIndex == len(m.inputs) { // toggle exclude m.exclude = !m.exclude @@ -136,8 +282,17 @@ func (m model_ca) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { if s == "enter" && m.focusIndex == len(m.inputs) { m.quitting = true - m.TempAddAuthor() - return model{list: parent_m.list}, tea.ClearScreen + m.errorModel.visible = m.TempAddAuthor() + if m.errorModel.visible { + m.quitting = false + return m, nil + } + if parent_m.keys != nil { + return model{list: parent_m.list}, tea.ClearScreen + } else { + m.quitting = true + return m, tea.Quit + } } } @@ -192,6 +347,13 @@ func (m *model_ca) updateInputs(msg tea.Msg) tea.Cmd { } func (m model_ca) View() string { + if m.errorModel.visible { + if len(m.errorModel.missing) == 0 { + errorGetMissingFields(m) + } + return m.errorModel.View() + } + if m.quitting { return "" } @@ -237,19 +399,13 @@ func (m model_ca) View() string { return b.String() } -func (m *model_ca) AddAuthor() { +func (m *model_ca) AddAuthor() bool { if len(m.inputs) > 0 && m.inputs[0].Value() != "" && m.inputs[1].Value() != "" && m.inputs[2].Value() != "" && m.inputs[3].Value() != "" { - author_file := utils.Find_authorfile() - f, err := os.OpenFile(author_file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) - if err != nil { - panic(err) - } - - defer f.Close() + var groups []string if m.inputs[4].Value() == "" { groups = []string{} @@ -257,9 +413,6 @@ func (m *model_ca) AddAuthor() { groups = strings.Split(m.inputs[4].Value(), "|") } - - - // create and add the user to the users map usr := utils.User{ Shortname: m.inputs[0].Value(), @@ -270,42 +423,29 @@ func (m *model_ca) AddAuthor() { Groups: groups, } - utils.Users[m.inputs[0].Value()] = usr - utils.Users[m.inputs[1].Value()] = usr - - - utils.Authors.Authors[m.inputs[1].Value()] = usr - - data, err := json.MarshalIndent(utils.Authors, "", " ") - if err != nil { - panic(fmt.Sprintf("Error marshalling json: %v", err)) - - } - - // write the data to the file - f.Truncate(0) - f.Seek(0, 0) - f.Write(data) - f.Close() - - // redefine the users map for the tui to use - utils.Define_users(utils.Find_authorfile()) + utils.CreateAuthor(usr) author := m.inputs[0].Value() - item_str := utils.Users[author].Username + " - " + utils.Users[author].Email - dupProtect[item_str] = author - parent_m.list.InsertItem(len(parent_m.list.Items())+1, item(item_str)) - - } + if parent_m.keys != nil { + item_str := utils.Users[author].Username + " - " + utils.Users[author].Email + dupProtect[item_str] = author + parent_m.list.InsertItem(len(parent_m.list.Items())+1, item(item_str)) + } + return false + } + return true } -func (m *model_ca) TempAddAuthor() { +func (m *model_ca) TempAddAuthor() bool { if len(m.inputs) > 1 && m.inputs[0].Value() != "" && m.inputs[1].Value() != "" { item_str := m.inputs[0].Value() + " - " + m.inputs[1].Value() dupProtect[item_str] = m.inputs[0].Value() + ":" + m.inputs[1].Value() i := item(item_str) parent_m.list.InsertItem(len(parent_m.list.Items())+1, item(item_str)) selectToggle(i) + + return false } + return true } diff --git a/src/cmd/utils/author_file_utils.go b/src/cmd/utils/author_file_utils.go index bb1c6c4..0cc4d8a 100644 --- a/src/cmd/utils/author_file_utils.go +++ b/src/cmd/utils/author_file_utils.go @@ -73,6 +73,38 @@ func CheckAuthorFile() string { return authorfile } +func CreateAuthor(user User) { + Users[user.Shortname] = user + Users[user.Longname] = user + + // Specifically for the json file + Authors.Authors[user.Longname] = user + + data, err := json.MarshalIndent(Authors, "", " ") + if err != nil { + panic(fmt.Sprintf("Error marshalling json: %v", err)) + + } + + // open author_file + author_file := Find_authorfile() + f, err := os.OpenFile(author_file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + panic(err) + } + + defer f.Close() + + // write the data to the file + f.Truncate(0) + f.Seek(0, 0) + f.Write(data) + f.Close() + + // redefine the users map for the tui to use + Define_users(Find_authorfile()) +} + func DeleteOneAuthor(author string) { author_file := Find_authorfile() diff --git a/src/cmd/utils/gh_p_fetcher.go b/src/cmd/utils/gh_p_fetcher.go new file mode 100644 index 0000000..96f3d96 --- /dev/null +++ b/src/cmd/utils/gh_p_fetcher.go @@ -0,0 +1,44 @@ +package utils + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" +) + +type GithubProfile struct { + Login string `json:"login"` + Name string `json:"name"` +} + +func FetchGithubProfile(username string) User { + // Fetch the github profile and create a user with everything except the email + + url := fmt.Sprintf("https://api.github.com/users/%s", username) + + resp, err := http.Get(url) + if err != nil { + panic(fmt.Sprint("Error fetching github profile: ", err)) + } + defer resp.Body.Close() + + // Parse the response and create a user + var profile GithubProfile + err = json.NewDecoder(resp.Body).Decode(&profile) + if err != nil { + panic(fmt.Sprint("Error parsing github profile: ", err)) + } + + // Create a user with the github profile + return User{ + Shortname: strings.ToLower(profile.Name[:2]), + Longname: profile.Name, + Username: profile.Login, + Email: "", + Ex: false, + Groups: []string{}, + } + +} + diff --git a/src/cmd/utils/user_util.go b/src/cmd/utils/user_util.go index baeeb51..1ae8eea 100644 --- a/src/cmd/utils/user_util.go +++ b/src/cmd/utils/user_util.go @@ -41,6 +41,13 @@ func ContainsUser(users []User, user User) bool { }) } +func CheckUserFields(user User) bool { + if user.Shortname == "" || user.Longname == "" || user.Username == "" || user.Email == "" { + return false + } + return true +} + func Define_users(author_file string) { // wipe the users map Users = map[string]User{} diff --git a/src/cmd/utils/util_test.go b/src/cmd/utils/util_test.go index 58823b6..2494836 100644 --- a/src/cmd/utils/util_test.go +++ b/src/cmd/utils/util_test.go @@ -1,9 +1,11 @@ package utils_test import ( - "github.com/Slug-Boi/cocommit/src/cmd/utils" + "encoding/json" "os" "testing" + + "github.com/Slug-Boi/cocommit/src/cmd/utils" ) const author_data = ` @@ -77,6 +79,45 @@ func Test_DeleteAuthor(t *testing.T) { } } +func Test_CreateAuthor(t *testing.T) { + setup() + defer teardown() + + // Test CreateAuthor + author := utils.User{ + Shortname: "epic", + Longname: "Test", + Username: "TestUser", + Email: "bestemailever@github.io", + Ex: false, + Groups: []string{"test"}, + } + utils.CreateAuthor(author) + // Check if author was added + _, ok := utils.Users["epic"] + if !ok { + t.Errorf("CreateAuthor() did not add author") + } + + // Check if author was added to the file + author_file := utils.Find_authorfile() + author_data, err := os.ReadFile(author_file) + if err != nil { + t.Errorf("Error reading file: %v", err) + } + + //unmarshal the data + var authors utils.Author + err = json.Unmarshal(author_data, &authors) + if err != nil { + t.Errorf("Error unmarshalling file: %v", err) + } + if authors.Authors["Test"].Shortname != "epic" { + t.Errorf("CreateAuthor() did not add author to file: %v", authors.Authors) + } +} + + // Author tests END // User tests BEGIN @@ -139,4 +180,32 @@ func Test_Commit(t* testing.T) { t.Errorf("Commit() = %v; want Test commit message\n", commit) } } -// Commit tests END \ No newline at end of file +// Commit tests END + +// Github tests BEGIN +func Test_FetchGHProfile(t *testing.T) { + setup() + defer teardown() + // Test FetchGithubProfile + profile := utils.FetchGithubProfile("Slug-Boi") + if profile.Username != "Slug-Boi" { + t.Errorf("FetchGithubProfile() = %v; want Slug-Boi", profile.Username) + } + if profile.Email != "" { + t.Errorf("FetchGithubProfile() = %v; want empty email", profile.Email) + } + if profile.Shortname != "th" { + t.Errorf("FetchGithubProfile() = %v; want th", profile.Shortname) + } + if profile.Longname != "Theis" { + t.Errorf("FetchGithubProfile() = %v; want Theis", profile.Longname) + } + if profile.Ex != false { + t.Errorf("FetchGithubProfile() = %v; want false", profile.Ex) + } + if len(profile.Groups) != 0 { + t.Errorf("FetchGithubProfile() = %v; want 0", len(profile.Groups)) + } +} +// Github tests END +