This commit is contained in:
Slug-Boi
2025-04-10 18:19:45 +02:00
10 changed files with 963 additions and 137 deletions
+15 -2
View File
@@ -27,6 +27,7 @@ This will require the user to have commitizen installed on their system.`,
cflag, _ := cmd.Flags().GetBool("cli") cflag, _ := cmd.Flags().GetBool("cli")
gflag, _ := cmd.Flags().GetString("git") gflag, _ := cmd.Flags().GetString("git")
gpflag, _ := cmd.Flags().GetBool("git-push") gpflag, _ := cmd.Flags().GetBool("git-push")
gpflagsflag, _ := cmd.Flags().GetString("git-push-flags")
// run execute commands again as root run will not call this part // run execute commands again as root run will not call this part
message = utils.Cz_Call() message = utils.Cz_Call()
@@ -53,7 +54,10 @@ This will require the user to have commitizen installed on their system.`,
if gflag != "" { if gflag != "" {
git_flags = strings.Split(gflag, " ") git_flags = strings.Split(gflag, " ")
} }
utils.GitWrapper(message, git_flags) err := utils.GitWrapper(message, git_flags)
if err != nil {
fmt.Println("Error committing:", err)
}
if update { if update {
update_msg() update_msg()
@@ -63,8 +67,16 @@ This will require the user to have commitizen installed on their system.`,
fmt.Println(message) fmt.Println(message)
} }
var gp_flags []string
if gpflagsflag != "" {
gp_flags = strings.Split(gpflagsflag, " ")
}
if gpflag { if gpflag {
utils.GitPush() err := utils.GitPush(gp_flags)
if err != nil {
fmt.Println("Error pushing to git:", err)
}
} }
}, },
} }
@@ -75,4 +87,5 @@ func init() {
czCmd.Flags().BoolP("print-output", "o", false, "Print the commit message") czCmd.Flags().BoolP("print-output", "o", false, "Print the commit message")
czCmd.Flags().BoolP("cli", "c", false, "[co-author1] [co-author2] ...") czCmd.Flags().BoolP("cli", "c", false, "[co-author1] [co-author2] ...")
czCmd.Flags().BoolP("git-push", "p", false, "Runs the git push command after the commit") czCmd.Flags().BoolP("git-push", "p", false, "Runs the git push command after the commit")
czCmd.Flags().StringP("git-push-flags", "f", "", "Passes the flags specified to the git push command")
} }
+20 -4
View File
@@ -56,6 +56,7 @@ var rootCmd = &cobra.Command{
vflag, _ := cmd.Flags().GetBool("version") vflag, _ := cmd.Flags().GetBool("version")
gflag, _ := cmd.Flags().GetString("git") gflag, _ := cmd.Flags().GetString("git")
gpflag, _ := cmd.Flags().GetBool("git-push") gpflag, _ := cmd.Flags().GetBool("git-push")
gpflagsflag, _ := cmd.Flags().GetString("git-push-flags")
if vflag { if vflag {
fmt.Println("Cocommit version:", Coco_Version) fmt.Println("Cocommit version:", Coco_Version)
@@ -109,13 +110,24 @@ var rootCmd = &cobra.Command{
return return
} }
utils.GitWrapper(message, git_flags) err := utils.GitWrapper(message, git_flags)
if err != nil {
fmt.Println("Error committing:", err)
}
// prints the commit message to the console if the print flag is set // prints the commit message to the console if the print flag is set
if pflag { if pflag {
fmt.Println(message) fmt.Println(message)
} }
var gp_flags []string
if gpflagsflag != "" {
gp_flags = strings.Split(gpflagsflag, " ")
}
if gpflag { if gpflag {
utils.GitPush() err := utils.GitPush(gp_flags)
if err != nil {
fmt.Println("Error pushing to remote:", err)
}
} }
}, },
} }
@@ -127,11 +139,14 @@ func Execute() {
check_update() check_update()
// author file check // author file check
author_file := utils.CheckAuthorFile() author_file, err := utils.CheckAuthorFile(os.Stdin, os.Stdout)
if err != nil {
panic(fmt.Sprintf("Error checking author file: %v", err))
}
// define users // define users
utils.Define_users(author_file) utils.Define_users(author_file)
err := rootCmd.Execute() err = rootCmd.Execute()
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)
} }
@@ -185,4 +200,5 @@ func init() {
rootCmd.Flags().BoolP("version", "v", false, "Prints the version of the cocommit cli tool") rootCmd.Flags().BoolP("version", "v", false, "Prints the version of the cocommit cli tool")
rootCmd.Flags().StringP("git", "g", "", "Adds the given flags to the git command") rootCmd.Flags().StringP("git", "g", "", "Adds the given flags to the git command")
rootCmd.Flags().BoolP("git-push", "p", false, "Runs git push after the commit") rootCmd.Flags().BoolP("git-push", "p", false, "Runs git push after the commit")
rootCmd.Flags().StringP("git-push-flags", "f", "", "Adds the given flags to the git push command")
} }
+1 -1
View File
@@ -50,7 +50,7 @@ func errorGetMissingFields(m model_ca) {
} }
if len(m.inputs) > 0 { if len(m.inputs) > 0 {
for i := 0; i < inpLen; i++ { for i := 0; i < inpLen-1; i++ {
if m.inputs[i].Value() == "" { if m.inputs[i].Value() == "" {
m.errorModel.missing = append(m.errorModel.missing, "- "+strings.Split(m.inputs[i].Placeholder," (")[0]) m.errorModel.missing = append(m.errorModel.missing, "- "+strings.Split(m.inputs[i].Placeholder," (")[0])
} }
+2 -4
View File
@@ -87,8 +87,7 @@ func intialModel_US(author_file string) tea.Model {
model, err := newExample() model, err := newExample()
if err != nil { if err != nil {
fmt.Println("Could not initialize Bubble Tea model:", err) panic(fmt.Sprintf("Could not initialize Bubble Tea model: %v", err))
os.Exit(1)
} }
return model return model
} }
@@ -96,8 +95,7 @@ func intialModel_US(author_file string) tea.Model {
func loadData(author_file string) { func loadData(author_file string) {
file, err := os.Open(author_file) file, err := os.Open(author_file)
if err != nil { if err != nil {
fmt.Println("Could not open file:", err) panic(fmt.Sprintf("Could not open author file: %v", err))
os.Exit(1)
} }
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
+247 -6
View File
@@ -2,12 +2,14 @@ package tui
import ( import (
"bytes" "bytes"
"fmt"
"os" "os"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/Slug-Boi/cocommit/src/cmd/utils" "github.com/Slug-Boi/cocommit/src/cmd/utils"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/exp/teatest" "github.com/charmbracelet/x/exp/teatest"
) )
@@ -77,12 +79,47 @@ func TestShowUser(t *testing.T) {
return bytes.Contains(bts, []byte("\"Authors\": {")) return bytes.Contains(bts, []byte("\"Authors\": {"))
}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2)) }, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2))
keyPress(tm, "enter")
keyPress(tm, "q") keyPress(tm, "q")
tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))
} }
func TestShowUserPanicFileNotFound(t *testing.T) {
setup()
defer teardown()
// Use defer with recover to catch panics
defer func() {
if r := recover(); r != nil {
t.Logf("Recovered from expected panic: %v", r)
// You can optionally verify the panic message here
if !strings.Contains(fmt.Sprint(r), "Could not open author file:") {
t.Errorf("Unexpected panic message: %v", r)
}
}
}()
m := intialModel_US("non_existent_file")
tm := teatest.NewTestModel(
t, m,
teatest.WithInitialTermSize(300, 300),
)
// Verify error message appears in output
teatest.WaitFor(t, tm.Output(), func(bts []byte) bool {
return bytes.Contains(bts, []byte("could not open author file"))
}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2))
// Send quit command
keyPress(tm, "q")
// Wait for clean shutdown
tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))
}
// tui_show_users TESTS END // tui_show_users TESTS END
// tui_author TESTS BEGIN // tui_author TESTS BEGIN
@@ -132,6 +169,89 @@ func TestEntryTA(t *testing.T) {
} }
} }
func TestErrorGetMissingFields(t *testing.T) {
setup()
defer teardown()
// Test case 1: No inputs
m := createAuthorModel(nil)
errorGetMissingFields(m)
if len(m.errorModel.missing) != 4 {
t.Errorf("Expected 4 missing fields, got %d\n%v", len(m.errorModel.missing), m.errorModel.missing)
}
m = createAuthorModel(nil)
m.inputs[0].SetValue("")
m.inputs[1].SetValue("value")
m.inputs[2].SetValue("")
m.inputs[3].SetValue("value")
tempAuthorToggle = false
errorGetMissingFields(m)
expectedMissing := []string{"- Shortname", "- Username"}
if len(m.errorModel.missing) != len(expectedMissing) {
t.Errorf("Expected %d missing fields, got %d", len(expectedMissing), len(m.errorModel.missing))
}
for i, missing := range expectedMissing {
if m.errorModel.missing[i] != missing {
t.Errorf("Expected '%s', got '%s'", missing, m.errorModel.missing[i])
}
}
m = createAuthorModel(nil)
m.inputs[0].SetValue("value1")
m.inputs[1].SetValue("value2")
m.inputs[2].SetValue("value3")
m.inputs[3].SetValue("value4")
m.inputs[4].SetValue("value5")
tempAuthorToggle = true
errorGetMissingFields(m)
if len(m.errorModel.missing) != 0 {
t.Errorf("Expected no missing fields, got %v", m.errorModel.missing)
}
}
func Test_EntryCA_TriggerError(t *testing.T) {
setup()
defer teardown()
m := listModel()
tm := teatest.NewTestModel(
t, m, teatest.WithInitialTermSize(300, 300),
)
keyPress(tm, "C")
keyPress(tm, "enter")
tm.Type("test")
keyPress(tm, "enter")
tm.Type("testing2")
keyPress(tm, "enter")
keyPress(tm, "enter")
keyPress(tm, "tab")
keyPress(tm, "enter")
keyPress(tm, "esc")
keyPress(tm, "esc")
keyPress(tm, "esc")
fm := tm.FinalModel(t)
mm, ok := fm.(model)
if !ok {
t.Errorf("Expected model_ca, got %T", fm)
}
if len(mm.list.Items()) != 2 {
t.Errorf("Expected 2 inputs, got %d\n%v", len(mm.list.Items()), mm.list.Items())
}
}
func Test_EntryCA(t *testing.T) { func Test_EntryCA(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@@ -196,6 +316,67 @@ func Test_EntryCA(t *testing.T) {
} }
func TestModelCAInit(t *testing.T) {
setup()
defer teardown()
m := model_ca{}
cmd := m.Init()
if cmd == nil {
t.Errorf("Expected a non-nil command, got nil")
}
if cmd() != textinput.Blink() {
t.Errorf("Expected textinput.Blink command, got a different command")
}
}
func TestCreateGHAuthorModel(t *testing.T) {
setup()
defer teardown()
// Define a test user
testUser := utils.User{
Shortname: "gh",
Longname: "GitHubUser",
Username: "GitHubUser-gh",
Email: "github@user.com",
Groups: []string{"grp1", "grp2"},
}
// Create the model using the test user
m := createGHAuthorModel(nil, testUser)
// Verify the inputs are correctly initialized
if m.inputs[0].Value() != testUser.Shortname {
t.Errorf("Expected Shortname '%s', got '%s'", testUser.Shortname, m.inputs[0].Value())
}
if m.inputs[1].Value() != testUser.Longname {
t.Errorf("Expected Longname '%s', got '%s'", testUser.Longname, m.inputs[1].Value())
}
if m.inputs[2].Value() != testUser.Username {
t.Errorf("Expected Username '%s', got '%s'", testUser.Username, m.inputs[2].Value())
}
if m.inputs[3].Value() != "" {
t.Errorf("Expected Email to be empty, got '%s'", m.inputs[3].Value())
}
expectedGroups := strings.Join(testUser.Groups, "|")
if m.inputs[4].Value() != expectedGroups {
t.Errorf("Expected Groups '%s', got '%s'", expectedGroups, m.inputs[4].Value())
}
// Verify the first input is focused
if !m.inputs[0].Focused() {
t.Errorf("Expected first input to be focused")
}
}
// tui_author TESTS END // tui_author TESTS END
// tui_commit_message TESTS BEGIN // tui_commit_message TESTS BEGIN
@@ -208,6 +389,8 @@ func Test_EntryCM(t *testing.T) {
t, m, teatest.WithInitialTermSize(300, 300), t, m, teatest.WithInitialTermSize(300, 300),
) )
tm.Type("test commit message") tm.Type("test commit message")
keyPress(tm, "shift+tab")
tm.Type("new line")
keyPress(tm, "enter") keyPress(tm, "enter")
@@ -217,11 +400,57 @@ func Test_EntryCM(t *testing.T) {
t.Errorf("Expected model_cm, got %T", fm) t.Errorf("Expected model_cm, got %T", fm)
} }
if m.textarea.Value() != "test commit message" { if m.textarea.Value() != "test commit message\nnew line" {
t.Errorf("Expected 'test commit message', got %s", m.textarea.Value()) t.Errorf("Expected 'test commit message', got %s", m.textarea.Value())
} }
} }
func Test_EntryCM_Quit(t *testing.T) {
setup()
defer teardown()
m := initialModel_cm()
tm := teatest.NewTestModel(
t, m, teatest.WithInitialTermSize(300, 300),
)
keyPress(tm, "esc")
fm := tm.FinalModel(t)
m, ok := fm.(model_cm)
if !ok {
t.Errorf("Expected model_cm, got %T", fm)
}
if m.textarea.Value() != "" {
t.Errorf("Expected empty textarea, got %s", m.textarea.Value())
}
}
// cannot test sigkill as it does not play nicely with these types of tests :(
func Test_EntryCM_Unfocuse(t *testing.T) {
setup()
defer teardown()
m := initialModel_cm()
tm := teatest.NewTestModel(
t, m, teatest.WithInitialTermSize(300, 300),
)
keyPress(tm, "down")
keyPress(tm, "esc")
fm := tm.FinalModel(t)
m, ok := fm.(model_cm)
if !ok {
t.Errorf("Expected model_cm, got %T", fm)
}
if m.textarea.Value() != "" {
t.Errorf("Expected empty textarea, got %s", m.textarea.Value())
}
}
// tui_commit_message TESTS END // tui_commit_message TESTS END
// tui_list TESTS BEGIN // tui_list TESTS BEGIN
@@ -363,7 +592,7 @@ func Test_GroupSelection(t *testing.T) {
keyPress(tm, "enter") keyPress(tm, "enter")
fm := tm.FinalModel(t) fm := tm.FinalModel(t)
m, ok := fm.(model) _, ok := fm.(model)
if !ok { if !ok {
t.Errorf("Expected model, got %T", fm) t.Errorf("Expected model, got %T", fm)
} }
@@ -377,7 +606,19 @@ func Test_pagination(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
m := mainModel{} // Add 20 authors to the test data
for i := 0; i < 20; i++ {
utils.Users[fmt.Sprintf("author%d", i)] = utils.User{
Shortname: fmt.Sprintf("a%d", i),
Longname: fmt.Sprintf("Author %d", i),
Username: fmt.Sprintf("AuthorUser%d", i),
Email: fmt.Sprintf("author%d@test.com", i),
Ex: false,
Groups: []string{},
}
}
m := listModel()
tm := teatest.NewTestModel( tm := teatest.NewTestModel(
t, m, teatest.WithInitialTermSize(25, 25), t, m, teatest.WithInitialTermSize(25, 25),
@@ -387,13 +628,13 @@ func Test_pagination(t *testing.T) {
tm.Quit() tm.Quit()
fm := tm.FinalModel(t) fm := tm.FinalModel(t)
m, ok := fm.(mainModel) m, ok := fm.(model)
if !ok { if !ok {
t.Errorf("Expected model, got %T", fm) t.Errorf("Expected model, got %T", fm)
} }
if m.paginator.Page != 1 { if m.list.Paginator.Page != 1 {
t.Errorf("Expected page 1, got %d", m.paginator.Page) t.Errorf("Expected page 1, got %d", m.list.Paginator.Page)
} }
} }
+32 -30
View File
@@ -3,6 +3,7 @@ package utils
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"os" "os"
"strings" "strings"
) )
@@ -13,64 +14,65 @@ import (
// An example of the author file can be found in the examples folder of the repo // An example of the author file can be found in the examples folder of the repo
func Find_authorfile() string { func Find_authorfile() string {
if os.Getenv("author_file") == "" { if os.Getenv("author_file") == "" {
authors, err := os.UserConfigDir() dirs, err := os.UserConfigDir()
if err != nil { if err != nil {
fmt.Println("Error getting user config directory") panic(fmt.Sprintf("Error getting user config directory: %v", err))
os.Exit(2)
} }
return (authors + "/cocommit/authors.json") return (dirs + "/cocommit/authors.json")
} else { } else {
return os.Getenv("author_file") return os.Getenv("author_file")
} }
} }
func CheckAuthorFile() string { func CheckAuthorFile(input io.Reader, output io.Writer) (string,error) {
var cocommit_folder string var cocommit_folder string
authorfile := Find_authorfile() authorfile := Find_authorfile()
if _, err := os.Stat(authorfile); os.IsNotExist(err) { if _, err := os.Stat(authorfile); os.IsNotExist(err) {
println("Author file not found at: ", authorfile) fmt.Fprintf(output, "Author file not found at: %s\n", authorfile)
println("Would you like to create one? (y/n)") fmt.Fprintf(output, "Would you like to create one? (y/n)\n")
var response string var response string
_, err := fmt.Scanln(&response) _, err := fmt.Fscanln(input, &response)
if err != nil { if err != nil {
println("Error reading response") fmt.Fprintln(output, "Error reading response")
} }
if response == "y" { if response == "y" {
parts := strings.Split(authorfile, "/") parts := strings.Split(authorfile, "/")
if len(parts) > 1 {
// remove the last part of the path
cocommit_folder = strings.Join(parts[:len(parts)-1], "/") cocommit_folder = strings.Join(parts[:len(parts)-1], "/")
} else {
cocommit_folder = "."
}
// create the author file // create the author file
if _, dirErr := os.Stat(cocommit_folder); os.IsNotExist(dirErr) { if _, dirErr := os.Stat(cocommit_folder); os.IsNotExist(dirErr) {
err := os.Mkdir(cocommit_folder, 0766) err := os.Mkdir(cocommit_folder, 0766)
if err != nil { if err != nil {
fmt.Println("Error creating directory: ", err, cocommit_folder) return "", fmt.Errorf("error creating directory: %v %s", err, cocommit_folder)
os.Exit(1)
} }
} }
file, err := os.Create(authorfile)
if err != nil {
fmt.Println("Error creating file: ", err)
os.Exit(1)
}
file, err := os.Create(authorfile)
if err != nil {
return "", fmt.Errorf("error creating file: %v", err)
}
defer file.Close() defer file.Close()
// write the header to the file // write the header to the file
json_string := json_string := `{
`{
"Authors": { "Authors": {
} }
}` }`
file.Write([]byte(json_string)) file.Write([]byte(json_string))
fmt.Fprintln(output, "Author file created. To add authors please launch the TUI with -a and press 'C'")
fmt.Println("Author file created. To add authors please launch the TUI with -a and press 'C'")
} else { } else {
os.Exit(1) os.Exit(0)
} }
} }
// This string output is mostly for convenience can mostly be ignored return authorfile, nil
return authorfile
} }
func CreateAuthor(user User) { func CreateAuthor(user User) {
@@ -106,6 +108,12 @@ func CreateAuthor(user User) {
} }
func DeleteOneAuthor(author string) { func DeleteOneAuthor(author string) {
// check that users aren't empty
if len(Users) < 1 {
fmt.Println("No users to remove")
return
}
author_file := Find_authorfile() author_file := Find_authorfile()
if _, exists := Users[author]; !exists { if _, exists := Users[author]; !exists {
@@ -121,12 +129,6 @@ func DeleteOneAuthor(author string) {
} }
defer file.Close() defer file.Close()
// check that users aren't empty
if len(Users) < 1 {
fmt.Println("No users to remove")
return
}
usr := Users[author] usr := Users[author]
// Remove the user from the Author struct (try both short and long name) // Remove the user from the Author struct (try both short and long name)
+26 -24
View File
@@ -10,33 +10,31 @@ import (
// This util file is used to create a commit message using a string builder // This util file is used to create a commit message using a string builder
// string builder for the commit message
var sb strings.Builder
// list of excluded authors based on the author file
var excludeMode = []string{}
// Regex pattern used to create temp users to add to the commit message // Regex pattern used to create temp users to add to the commit message
var reg, _ = regexp.Compile("([^:]+):([^:]+)") var reg, _ = regexp.Compile("([^:]+):([^:]+)")
func Commit(message string, authors []string) string { func Commit(message string, authors []string) string {
// string builder for the commit message
var sb strings.Builder
excludeMode := []string{}
// write the commit message to the string builder // write the commit message to the string builder
sb.WriteString(message + "\n") sb.WriteString(message + "\n")
fst := authors[0] fst := authors[0]
if fst == "all" || fst == "All" { if fst == "all" || fst == "All" {
add_x_users(excludeMode) add_x_users(excludeMode, &sb)
goto skip_loop return sb.String()
} else if Groups[fst] != nil { } else if Groups[fst] != nil {
excludeMode = group_selection(Groups[fst], excludeMode) excludeMode = group_selection(Groups[fst], excludeMode)
add_x_users(excludeMode) add_x_users(excludeMode, &sb)
goto skip_loop return sb.String()
} }
// Loop that adds users // Loop that adds users
for _, committer := range authors { for _, committer := range authors {
if _, ok := Users[committer]; ok { if _, ok := Users[committer]; ok {
sb_author(committer) sb_author(committer, &sb)
} else if match := reg.MatchString(committer); match { } else if match := reg.MatchString(committer); match {
str := strings.Split(committer, ":") str := strings.Split(committer, ":")
@@ -52,16 +50,15 @@ func Commit(message string, authors []string) string {
println(committer, " was unknown. User either not defined or name typed wrong") println(committer, " was unknown. User either not defined or name typed wrong")
} }
} }
if len(excludeMode) > 0 {
add_x_users(excludeMode)
}
// Skip label for edge cases at top of function // Add excluded users after processing all authors
skip_loop: if len(excludeMode) > 0 {
add_x_users(excludeMode, &sb)
}
return sb.String() return sb.String()
} }
func GitWrapper(commit string, flags []string) { func GitWrapper(commit string, flags []string) error {
// commit shell command // commit shell command
// specify git command // specify git command
input := []string{"commit"} input := []string{"commit"}
@@ -76,26 +73,31 @@ func GitWrapper(commit string, flags []string) {
cmd_output, err := cmd.CombinedOutput() cmd_output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
println(fmt.Sprint(err) + " : " + string(cmd_output)) return fmt.Errorf("error: %s : %s", err, string(cmd_output))
} else { } else {
println(string(cmd_output)) println(string(cmd_output))
} }
return nil
} }
func GitPush() { func GitPush(flags []string) error {
cmd := exec.Command("git", "push")
input := []string{"push"}
input = append(input, flags...)
cmd := exec.Command("git", input...)
cmd_output, err := cmd.CombinedOutput() cmd_output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
println(fmt.Sprint(err) + " : " + string(cmd_output)) return fmt.Errorf("error: %s : %s", err, string(cmd_output))
} else { } else {
println(string(cmd_output)) println(string(cmd_output))
} }
return nil
} }
// helper function to add an author to the commit message // helper function to add an author to the commit message
func sb_author(committer string) { func sb_author(committer string, sb *strings.Builder) {
sb.WriteString("\nCo-authored-by: ") sb.WriteString("\nCo-authored-by: ")
sb.WriteString(Users[committer].Username) sb.WriteString(Users[committer].Username)
sb.WriteString(" <") sb.WriteString(" <")
@@ -104,13 +106,13 @@ func sb_author(committer string) {
} }
// helper function to add x amount of users to the commit message // helper function to add x amount of users to the commit message
func add_x_users(excludeMode []string) { func add_x_users(excludeMode []string, sb *strings.Builder) {
if len(DefExclude) > 0 { if len(DefExclude) > 0 {
excludeMode = append(excludeMode, DefExclude...) excludeMode = append(excludeMode, DefExclude...)
} }
for key, user := range Users { for key, user := range Users {
if !slices.Contains(excludeMode, user.Username) { if !slices.Contains(excludeMode, user.Username) {
sb_author(key) sb_author(key, sb)
excludeMode = append(excludeMode, user.Username) excludeMode = append(excludeMode, user.Username)
} }
} }
+56 -2
View File
@@ -4,17 +4,63 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os/exec"
"strings" "strings"
) )
type GithubProfile struct { type GithubProfile struct {
Login string `json:"login"` Login string `json:"login"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"`
}
func checkGHCLI() bool {
// Check if the gh command line tool is installed
cmd := exec.Command("gh", "auth", "status")
out ,err := cmd.CombinedOutput()
if err == nil {
if strings.Contains(string(out), "Logged in to") {
return true
}
} else {
return false
}
return false
}
func useGHCLI(username string) []byte {
cmd := exec.Command("gh", "api", fmt.Sprintf("/users/%s", username))
out, err := cmd.CombinedOutput()
if err != nil {
panic(fmt.Sprint("Error fetching github profile",err))
}
return out
} }
func FetchGithubProfile(username string) User { func FetchGithubProfile(username string) User {
// Fetch the github profile and create a user with everything except the email // Fetch the github profile and create a user with everything except the email
var profile GithubProfile
check := checkGHCLI()
var err error
if check {
// If the gh command line tool is installed, use it to fetch the github profile
fmt.Println("Using gh-cli to fetch github profile")
data := useGHCLI(username)
err = json.Unmarshal(data, &profile)
} else {
fmt.Println("Using http request to fetch github profile")
// If the gh command line tool is not installed, use the http request
url := fmt.Sprintf("https://api.github.com/users/%s", username) url := fmt.Sprintf("https://api.github.com/users/%s", username)
resp, err := http.Get(url) resp, err := http.Get(url)
@@ -24,12 +70,20 @@ func FetchGithubProfile(username string) User {
defer resp.Body.Close() defer resp.Body.Close()
// Parse the response and create a user // Parse the response and create a user
var profile GithubProfile if err = json.NewDecoder(resp.Body).Decode(&profile); err != nil {
err = json.NewDecoder(resp.Body).Decode(&profile) panic(fmt.Sprint("Error decoding github profile: ", err))
}
}
// Check error
if err != nil { if err != nil {
panic(fmt.Sprint("Error parsing github profile: ", err)) panic(fmt.Sprint("Error parsing github profile: ", err))
} }
// Check if the profile has a name
if profile.Name == "" {
panic(fmt.Sprint("Error: No name found in github profile something went wrong whilst fetching the profile: ", err))
}
// Create a user with the github profile // Create a user with the github profile
return User{ return User{
Shortname: strings.ToLower(profile.Name[:2]), Shortname: strings.ToLower(profile.Name[:2]),
+2 -4
View File
@@ -58,13 +58,11 @@ func Define_users(author_file string) {
data, err := os.ReadFile(author_file) data, err := os.ReadFile(author_file)
if err != nil { if err != nil {
fmt.Println("Error reading author file: ", err) panic(fmt.Sprintf("Error reading author file: %v", err))
os.Exit(2)
} }
err = json.Unmarshal(data, &auth) err = json.Unmarshal(data, &auth)
if err != nil { if err != nil {
fmt.Println("Error unmarshalling json: ", err) panic(fmt.Sprintf("Error unmarshalling json: %v", err))
os.Exit(2)
} }
Authors = auth Authors = auth
+503 -1
View File
@@ -1,8 +1,13 @@
package utils_test package utils_test
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt"
"io"
"net/http"
"os" "os"
"strings"
"testing" "testing"
"github.com/Slug-Boi/cocommit/src/cmd/utils" "github.com/Slug-Boi/cocommit/src/cmd/utils"
@@ -117,6 +122,192 @@ func Test_CreateAuthor(t *testing.T) {
} }
} }
func Test_FindAuthorFilePanic(t *testing.T) {
// Save original environment variables
originalAuthorFile := os.Getenv("author_file")
originalHome := os.Getenv("HOME")
orignalXDG := os.Getenv("XDG_CONFIG_HOME")
// Test Find_authorfile panic
defer func() {
// Reset environment variables
os.Setenv("author_file", originalAuthorFile)
os.Setenv("HOME", originalHome)
os.Setenv("XDG_CONFIG_HOME", orignalXDG)
if r := recover(); r == nil {
t.Errorf("Find_authorfile() did not panic")
}
}()
// Set environment variables to empty strings
// to trigger the panic
os.Setenv("author_file", "")
os.Setenv("HOME", "")
os.Setenv("XDG_CONFIG_HOME", "")
utils.Find_authorfile()
}
func Test_FindAuthorFileEnv(t *testing.T) {
// Test Find_authorfile with env var
setup()
defer teardown()
os.Setenv("author_file", "")
authorfile := utils.Find_authorfile()
configdir, err := os.UserConfigDir()
if err != nil {
t.Fatalf("Failed to get user config directory: %v", err)
}
if authorfile != configdir+"/cocommit/authors.json" {
t.Errorf("Find_authorfile() = %v; want %v", authorfile, configdir+"/cocommit/authors.json")
}
}
func Test_CreateAuthorPanicOnFileError(t *testing.T) {
setup()
defer teardown()
// Set an invalid author file path to trigger file open error
os.Setenv("author_file", "/invalid/path/author_file_test")
defer func() {
if r := recover(); r == nil {
t.Errorf("CreateAuthor() did not panic on file open error")
}
}()
validUser := utils.User{
Shortname: "valid",
Longname: "ValidUser",
Username: "ValidUser",
Email: "valid@test.io",
Ex: false,
Groups: []string{},
}
utils.CreateAuthor(validUser)
}
func Test_DeleteOneAuthorPrints(t *testing.T) {
setup()
defer teardown()
// Redirect stdout to capture fmt.Println outputs
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Test case: User not found
utils.Define_users("author_file_test")
utils.DeleteOneAuthor("nonexistent_user")
w.Close()
out, _ := io.ReadAll(r)
os.Stdout = oldStdout
if !strings.Contains(string(out), "User not found") {
t.Errorf("Expected 'User not found' message, got: %s", string(out))
}
// Test case: Error opening file
// Test case: No users to remove
setup()
defer teardown()
utils.Define_users("author_file_test")
utils.Users = make(map[string]utils.User) // Clear users
r, w, _ = os.Pipe()
os.Stdout = w
utils.DeleteOneAuthor("te")
w.Close()
out, _ = io.ReadAll(r)
os.Stdout = oldStdout
if !strings.Contains(string(out), "No users to remove") {
t.Errorf("Expected 'No users to remove' message, got: %s", string(out))
}
}
func Test_CheckAuthorFile_FileExists(t *testing.T) {
setup()
defer teardown()
// Ensure the author file exists
authorfile := utils.Find_authorfile()
if _, err := os.Stat(authorfile); os.IsNotExist(err) {
t.Fatalf("Author file does not exist: %v", authorfile)
}
// Mock user input to simulate "y" response
input := strings.NewReader("y\n")
output := new(bytes.Buffer) // capture output
// Test CheckAuthorFile when the file exists
result, err := utils.CheckAuthorFile(input, output)
if err != nil {
t.Fatalf("CheckAuthorFile() returned error: %v", err)
}
if result != authorfile {
t.Errorf("CheckAuthorFile() = %v; want %v", result, authorfile)
}
}
func Test_CheckAuthorFile_FileNotExists_CreateFile(t *testing.T) {
setup()
defer teardown()
originalEnv := os.Getenv("author_file")
defer os.Setenv("author_file", originalEnv)
os.Setenv("author_file", "author_file_test")
// Remove the author file to simulate non-existence
authorfile := utils.Find_authorfile()
os.Remove(authorfile)
// Mock user input to simulate "y" response
input := strings.NewReader("y\n")
output := new(bytes.Buffer) // capture output
// Test CheckAuthorFile when the file does not exist and user agrees to create it
result, err := utils.CheckAuthorFile(input, output)
if err != nil {
t.Fatalf("CheckAuthorFile() returned error: %v", err)
}
if result != authorfile {
panic(fmt.Sprintf("CheckAuthorFile() = %v; want %v", result, authorfile))
}
}
func Test_CheckAuthorFile_FileNotExists_DeclineCreate(t *testing.T) {
setup()
defer teardown()
// Remove the author file to simulate non-existence
authorfile := utils.Find_authorfile()
os.Remove(authorfile)
// Mock user input to simulate "n" response
input := strings.NewReader("n\n")
output := new(bytes.Buffer) // capture output
// Test CheckAuthorFile when the file does not exist and user declines to create it
defer func() {
if r := recover(); r == nil {
t.Errorf("CheckAuthorFile() did not exit when user declined to create the file")
}
}()
utils.CheckAuthorFile(input, output)
// Check if the output contains the expected message
if !strings.Contains(output.String(), "") {
t.Errorf("Expected no message found output: %s", output.String())
}
}
// Author tests END // Author tests END
@@ -131,6 +322,56 @@ func Test_DefineUsers(t *testing.T) {
} }
} }
func Test_DefineUsersMultipleGroups(t *testing.T) {
setup()
defer teardown()
utils.Define_users("author_file_test")
utils.CreateAuthor(utils.User{
Shortname: "epic",
Longname: "Test",
Username: "TestUser",
Email: "dontcare",
Ex: false,
Groups: []string{"gr1"},
})
if len(utils.Users) != 6 {
t.Errorf("Define_users() = %v; want 6", len(utils.Users))
}
if len(utils.Groups["gr1"]) != 2 {
t.Errorf("Define_users() = %v; want 2", len(utils.Groups["gr1"]))
}
}
func Test_DefineUsersPanicOnMissingFile(t *testing.T) {
// Test Define_users panic on missing file
defer func() {
if r := recover(); r == nil {
t.Errorf("Define_users() did not panic on missing file")
}
}()
utils.Define_users("non_existent_file")
}
func Test_DefineUsersPanicOnInvalidJSON(t *testing.T) {
setup()
defer teardown()
// Create a file with invalid JSON
invalidJSON := `{"Authors": { "invalid": "data"`
os.WriteFile("invalid_author_file_test", []byte(invalidJSON), 0644)
defer os.Remove("invalid_author_file_test")
// Test Define_users panic on invalid JSON
defer func() {
if r := recover(); r == nil {
t.Errorf("Define_users() did not panic on invalid JSON")
}
}()
utils.Define_users("invalid_author_file_test")
}
func Test_RemoveUser(t *testing.T) { func Test_RemoveUser(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@@ -164,6 +405,42 @@ func Test_TempAddUser(t *testing.T) {
} }
} }
func Test_ContainsUser(t *testing.T) {
setup()
defer teardown()
// Test ContainsUser
utils.Define_users("author_file_test")
user := utils.Users["te"]
userList := make([]utils.User, 0, len(utils.Users))
for _, u := range utils.Users {
userList = append(userList, u)
}
if !utils.ContainsUser(userList, user) {
t.Errorf("ContainsUser() = %v; want true", false)
}
if utils.ContainsUser(userList, utils.User{}) {
t.Errorf("ContainsUser() = %v; want false", true)
}
}
func Test_CheckUserFields(t *testing.T) {
setup()
defer teardown()
// Test CheckUserFields
utils.Define_users("author_file_test")
user := utils.Users["te"]
if !utils.CheckUserFields(user) {
t.Errorf("CheckUserFields() = %v; want true", false)
}
emptyUser := utils.User{}
if utils.CheckUserFields(emptyUser) {
t.Errorf("CheckUserFields() = %v; want false", true)
}
}
// User tests END // User tests END
// Commit tests BEGIN // Commit tests BEGIN
@@ -180,6 +457,133 @@ 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)
} }
} }
func Test_CommitWithAllAuthors(t *testing.T) {
setup()
defer teardown()
utils.Define_users("author_file_test")
// Test Commit with "all" authors
authors := []string{"all"}
message := "Test commit message with all authors"
commit := utils.Commit(message, authors)
// Verify that all authors are included in the commit message
for _, user := range utils.Users {
coAuthorLine := fmt.Sprintf("Co-authored-by: %s <%s>", user.Username, user.Email)
if !strings.Contains(commit, coAuthorLine) {
t.Errorf("Commit() missing co-author line: %v", coAuthorLine)
}
}
}
func Test_CommitWithGroupAuthors(t *testing.T) {
setup()
defer teardown()
utils.Define_users("author_file_test")
// Test Commit with a group of authors
authors := []string{"gr1"}
message := "Test commit message with group authors"
commit := utils.Commit(message, authors)
// Verify that all group members are included in the commit message
for _, user := range utils.Groups["gr1"] {
coAuthorLine := fmt.Sprintf("Co-authored-by: %s <%s>", user.Username, user.Email)
if !strings.Contains(commit, coAuthorLine) {
t.Errorf("Commit() missing co-author line for group member: %v", coAuthorLine)
}
}
}
func Test_CommitWithInvalidGroup(t *testing.T) {
setup()
defer teardown()
// Reset utils.Users and utils.Groups to avoid interference from other tests
utils.Users = make(map[string]utils.User)
utils.Groups = make(map[string][]utils.User)
utils.Define_users("author_file_test")
// Test Commit with an invalid group
authors := []string{"invalid_group"}
message := "Test commit message with invalid group"
commit := utils.Commit(message, authors)
// Verify that no co-author lines are added for the invalid group
if strings.Contains(commit, "Co-authored-by:") {
t.Errorf("Commit() should not include co-author lines for an invalid group msg: %s ", commit)
}
}
func Test_CommitWithInlineAdd(t *testing.T) {
setup()
defer teardown()
utils.Define_users("author_file_test")
// Test Commit with inline addition of authors
authors := []string{"te:testtest"}
message := "Test commit message with inline addition"
commit := utils.Commit(message, authors)
// Verify that the commit message includes the inline addition
splitAuthors := strings.Split(authors[0], ":")
if !strings.Contains(commit, fmt.Sprintf("Co-authored-by: %s <%s>", splitAuthors[0], splitAuthors[1])) {
t.Errorf("Commit() missing co-author line for inline addition: %v:%v\n%s", splitAuthors[0],splitAuthors[1] ,commit)
}
}
func Test_CommitWithNegation(t *testing.T) {
setup()
defer teardown()
utils.Define_users("author_file_test")
// Test Commit with negation
authors := []string{"^testtest"}
message := "Test commit message with negation"
commit := utils.Commit(message, authors)
// Verify that the commit message does not include the negated author
if strings.Contains(commit, "Co-authored-by: testtest") {
t.Errorf("Commit() should not include co-author line for negated author")
}
}
func Test_GitWrapper(t *testing.T) {
setup()
defer teardown()
utils.Define_users("author_file_test")
// Test GitWrapper with --dry-run flag
authors := []string{"te"}
message := "Test commit message for GitWrapper"
commit := utils.Commit(message, authors)
flags := []string{"-a","--dry-run"}
err := utils.GitWrapper(commit, flags)
if err != nil {
t.Errorf("GitWrapper() returned error: %v", err)
}
}
func Test_GitPush(t *testing.T) {
setup()
defer teardown()
utils.Define_users("author_file_test")
// Test GitPush with --dry-run flag
flags := []string{"--all","--dry-run"}
err := utils.GitPush(flags)
if err != nil {
t.Errorf("GitPush() returned error: %v", err)
}
}
// Commit tests END // Commit tests END
// Github tests BEGIN // Github tests BEGIN
@@ -207,5 +611,103 @@ func Test_FetchGHProfile(t *testing.T) {
t.Errorf("FetchGithubProfile() = %v; want 0", len(profile.Groups)) t.Errorf("FetchGithubProfile() = %v; want 0", len(profile.Groups))
} }
} }
// Github tests END
func Test_FetchGHProfilePanicOnRequestError(t *testing.T) {
// Test FetchGithubProfile panic on HTTP request error
defer func() {
if r := recover(); r == nil {
t.Errorf("FetchGithubProfile() did not panic on HTTP request error")
}
}()
// Simulate an invalid URL by using an invalid username
utils.FetchGithubProfile("invalid_username_with_special_characters_@#$")
}
func Test_FetchGHProfilePanicOnInvalidJSON(t *testing.T) {
// Test FetchGithubProfile panic on invalid JSON response
defer func() {
if r := recover(); r == nil {
t.Errorf("FetchGithubProfile() did not panic on invalid JSON response")
}
}()
// Mock the HTTP response to return invalid JSON
http.DefaultClient = &http.Client{
Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"invalid_json":`)),
}, nil
}),
}
utils.FetchGithubProfile("valid_username")
}
func Test_FetchGHProfilePanicOnHTTPGetError(t *testing.T) {
// Test FetchGithubProfile panic on HTTP GET error
defer func() {
if r := recover(); r == nil {
t.Errorf("FetchGithubProfile() did not panic on HTTP GET error")
}
}()
// Mock the HTTP client to simulate an error during the GET request
http.DefaultClient = &http.Client{
Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("simulated HTTP GET error")
}),
}
utils.FetchGithubProfile("any_username")
}
type roundTripperFunc func(req *http.Request) (*http.Response, error)
func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
func Test_FetchGHProfileHTTP(t *testing.T) {
setup()
defer teardown()
// Mock the HTTP client to simulate a successful response
mockResponse := `{
"login": "Slug-Boi",
"name": "Theis",
"email": "",
"bio": "Test bio"
}`
http.DefaultClient = &http.Client{
Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
if req.URL.String() != "https://api.github.com/users/Slug-Boi" {
t.Errorf("Unexpected URL: %v", req.URL.String())
}
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(mockResponse)),
}, nil
}),
}
// Alias the `gh` command to an error to ensure the GitHub CLI is not used
os.Setenv("PATH", "/nonexistent")
// Test FetchGithubProfile using HTTP request
profile := utils.FetchGithubProfile("Slug-Boi")
if profile.Username != "Slug-Boi" {
t.Errorf("FetchGithubProfile() = %v; want Slug-Boi", profile.Username)
}
if profile.Longname != "Theis" {
t.Errorf("FetchGithubProfile() = %v; want Theis", profile.Longname)
}
if profile.Email != "" {
t.Errorf("FetchGithubProfile() = %v; want empty email", profile.Email)
}
}
// Github tests END