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")
}
+177 -37
View File
@@ -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
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()
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()
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()
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
}
+32
View File
@@ -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()
+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) {
// wipe the users map
Users = map[string]User{}
+70 -1
View File
@@ -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
@@ -140,3 +181,31 @@ func Test_Commit(t* testing.T) {
}
}
// 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