Merge pull request #65 from Slug-Boi/feat_github_profile_create

This commit is contained in:
Theis
2025-04-09 13:32:08 +02:00
committed by GitHub
6 changed files with 417 additions and 47 deletions
+78
View File
@@ -0,0 +1,78 @@
/*
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
*/
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 <github username>",
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")
}
+185 -45
View File
@@ -4,7 +4,6 @@ package tui
// from the Bubbles component library. // from the Bubbles component library.
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@@ -14,7 +13,6 @@ import (
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
//"github.com/inancgumus/screen"
) )
var ( var (
@@ -36,6 +34,63 @@ type model_ca struct {
inputs []textinput.Model inputs []textinput.Model
quitting bool quitting bool
exclude 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 var parent_m *model
@@ -45,13 +100,13 @@ func createAuthorModel(old_m *model) model_ca {
m := model_ca{ m := model_ca{
inputs: make([]textinput.Model, 5), inputs: make([]textinput.Model, 5),
errorModel: intitialErrorModel(),
} }
var t textinput.Model var t textinput.Model
for i := range m.inputs { for i := range m.inputs {
t = textinput.New() t = textinput.New()
t.Cursor.Style = cursorStyle t.Cursor.Style = cursorStyle
//t.CharLimit = 32
switch i { switch i {
case 0: case 0:
@@ -75,11 +130,71 @@ func createAuthorModel(old_m *model) model_ca {
return m 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 { func tempAuthorModel(old_m *model) model_ca {
parent_m = old_m parent_m = old_m
m := model_ca{ m := model_ca{
inputs: make([]textinput.Model, 2), inputs: make([]textinput.Model, 2),
errorModel: intitialErrorModel(),
} }
var t textinput.Model var t textinput.Model
@@ -110,13 +225,35 @@ func (m model_ca) Init() tea.Cmd {
return textinput.Blink 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) { func (m model_ca) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.errorModel.visible {
return updateErrorPopup(m, msg)
}
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
case "ctrl+c", "esc": case "ctrl+c", "esc":
m.inputs = nil m.inputs = nil
return nil, nil if parent_m.keys != nil {
return nil, nil
}
return m, tea.Quit
// Set focus to next input // Set focus to next input
case "tab", "shift+tab", "enter", "up", "down": 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 !tempAuthorToggle {
if s == "enter" && m.focusIndex == len(m.inputs)+1 { if s == "enter" && m.focusIndex == len(m.inputs)+1 {
m.quitting = true m.quitting = true
m.AddAuthor() m.errorModel.visible = m.AddAuthor()
return model{list: parent_m.list}, tea.ClearScreen 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) { } else if s == "enter" && m.focusIndex == len(m.inputs) {
// toggle exclude // toggle exclude
m.exclude = !m.exclude m.exclude = !m.exclude
@@ -136,8 +282,17 @@ func (m model_ca) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} else { } else {
if s == "enter" && m.focusIndex == len(m.inputs) { if s == "enter" && m.focusIndex == len(m.inputs) {
m.quitting = true m.quitting = true
m.TempAddAuthor() m.errorModel.visible = m.TempAddAuthor()
return model{list: parent_m.list}, tea.ClearScreen 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 { 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 { if m.quitting {
return "" return ""
} }
@@ -237,19 +399,13 @@ func (m model_ca) View() string {
return b.String() return b.String()
} }
func (m *model_ca) AddAuthor() { func (m *model_ca) AddAuthor() bool {
if len(m.inputs) > 0 && if len(m.inputs) > 0 &&
m.inputs[0].Value() != "" && m.inputs[0].Value() != "" &&
m.inputs[1].Value() != "" && m.inputs[1].Value() != "" &&
m.inputs[2].Value() != "" && m.inputs[2].Value() != "" &&
m.inputs[3].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 var groups []string
if m.inputs[4].Value() == "" { if m.inputs[4].Value() == "" {
groups = []string{} groups = []string{}
@@ -257,9 +413,6 @@ func (m *model_ca) AddAuthor() {
groups = strings.Split(m.inputs[4].Value(), "|") groups = strings.Split(m.inputs[4].Value(), "|")
} }
// create and add the user to the users map // create and add the user to the users map
usr := utils.User{ usr := utils.User{
Shortname: m.inputs[0].Value(), Shortname: m.inputs[0].Value(),
@@ -270,42 +423,29 @@ func (m *model_ca) AddAuthor() {
Groups: groups, Groups: groups,
} }
utils.Users[m.inputs[0].Value()] = usr utils.CreateAuthor(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())
author := m.inputs[0].Value() author := m.inputs[0].Value()
item_str := utils.Users[author].Username + " - " + utils.Users[author].Email if parent_m.keys != nil {
dupProtect[item_str] = author item_str := utils.Users[author].Username + " - " + utils.Users[author].Email
parent_m.list.InsertItem(len(parent_m.list.Items())+1, item(item_str)) 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() != "" { if len(m.inputs) > 1 && m.inputs[0].Value() != "" && m.inputs[1].Value() != "" {
item_str := 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() dupProtect[item_str] = m.inputs[0].Value() + ":" + m.inputs[1].Value()
i := item(item_str) i := item(item_str)
parent_m.list.InsertItem(len(parent_m.list.Items())+1, item(item_str)) parent_m.list.InsertItem(len(parent_m.list.Items())+1, item(item_str))
selectToggle(i) selectToggle(i)
return false
} }
return true
} }
+32
View File
@@ -73,6 +73,38 @@ func CheckAuthorFile() string {
return authorfile 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) { func DeleteOneAuthor(author string) {
author_file := Find_authorfile() author_file := Find_authorfile()
+44
View File
@@ -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{},
}
}
+7
View File
@@ -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) { func Define_users(author_file string) {
// wipe the users map // wipe the users map
Users = map[string]User{} Users = map[string]User{}
+71 -2
View File
@@ -1,9 +1,11 @@
package utils_test package utils_test
import ( import (
"github.com/Slug-Boi/cocommit/src/cmd/utils" "encoding/json"
"os" "os"
"testing" "testing"
"github.com/Slug-Boi/cocommit/src/cmd/utils"
) )
const author_data = ` 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 // Author tests END
// User tests BEGIN // User tests BEGIN
@@ -139,4 +180,32 @@ func Test_Commit(t* testing.T) {
t.Errorf("Commit() = %v; want Test commit message\n", commit) t.Errorf("Commit() = %v; want Test commit message\n", commit)
} }
} }
// Commit tests END // 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