diff --git a/go.mod b/go.mod index ffbdcf4..0954662 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,10 @@ require ( github.com/99designs/gqlgen v0.17.31 // indirect github.com/Khan/genqlient v0.6.0 // indirect github.com/adrg/xdg v0.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/vektah/gqlparser/v2 v2.5.6 // indirect golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect golang.org/x/sync v0.6.0 // indirect diff --git a/src_code/go_src/LICENSE b/src_code/go_src/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/src_code/go_src/cmd/root.go b/src_code/go_src/cmd/root.go new file mode 100644 index 0000000..f2c0e49 --- /dev/null +++ b/src_code/go_src/cmd/root.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "main/src_code/go_src/cmd/utils" + "os" + "fmt" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: `cocommit [co-author2] ... || + cocommit [co-author2:email] ... || + cocommit all || + cocommit ^ ^[co-author2] ... || + cocommit || + cocommit users ||`, + DisableFlagsInUseLine: true, + Short: "A cli tool to help you add co-authors to your git commits", + Long: `A cli tool to help you add co-authors to your git commits`, + //TODO: add bubble tea interface to this + Args: cobra.MinimumNArgs(0), + Run: func(cmd *cobra.Command, args []string) { + // check if the print flag is set + pflag, _ := cmd.Flags().GetBool("print") + // run execute commands again as root run will not call this part + // redundant check for now but will be useful later when we add tui + if len(args) == 1 { + utils.GitWrapper(args[0]) + if pflag { + fmt.Println(args[0]) + } + os.Exit(0) + } + // builds the commit message with the selected authors + message := utils.Commit(args[0], args[1:]) + // prints the commit message to the console if the print flag is set + if pflag { + fmt.Println(message) + } + // runs the git commit command + utils.GitWrapper(message) + }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + // author file check + author_file := utils.CheckAuthorFile() + // define users + utils.Define_users(author_file) + + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.Flags().BoolP("print", "p", false, "Prints the commit message to the console") + +} diff --git a/src_code/go_src/cmd/users.go b/src_code/go_src/cmd/users.go new file mode 100644 index 0000000..6fef0d3 --- /dev/null +++ b/src_code/go_src/cmd/users.go @@ -0,0 +1,39 @@ +package cmd + +import ( + "main/src_code/go_src/cmd/utils" + "os" + "slices" + "sort" + "strings" + + "github.com/spf13/cobra" +) + +var authorfile = utils.Find_authorfile() + +// usersCmd represents the users command +var usersCmd = &cobra.Command{ + Use: "users", + Short: "Displays all users from the author file located at: " + authorfile, + Long: `Displays all users from the author file located at: ` + authorfile, + Run: func(cmd *cobra.Command, args []string) { + //TODO: make this print a bit prettier (sort it and maybe use a table) + println("List of users:\nFormat: / -> Username: Email: ") + seen_users := []utils.User{} + user_sb := []string{} + for name, usr := range utils.Users { + if !slices.Contains(seen_users, usr) { + user_sb = append(user_sb, utils.Users[name].Names+" ->"+" Username: "+usr.Username+" Email: "+usr.Email+"\n") + seen_users = append(seen_users, usr) + } + } + sort.Strings(user_sb) + println(strings.Join(user_sb, "")) + os.Exit(1) + }, +} + +func init() { + rootCmd.AddCommand(usersCmd) +} diff --git a/src_code/go_src/cmd/utils/author_file_utils.go b/src_code/go_src/cmd/utils/author_file_utils.go new file mode 100644 index 0000000..a189f42 --- /dev/null +++ b/src_code/go_src/cmd/utils/author_file_utils.go @@ -0,0 +1,44 @@ +package utils + +import ( + "fmt" + "os" +) + +// Author file utils is a package that contains functions that are used to read +// check, and potentially write to the author file. The author file is a file +// that contains the names and emails of the users that are allowed to commit +// An example of the author file can be found in the examples folder of the repo +func Find_authorfile() string { + if os.Getenv("author_file") == "" { + authors, err := os.UserConfigDir() + if err != nil { + fmt.Println("Error getting user config directory") + os.Exit(2) + } + return (authors + "/cocommit/authors") + } else { + return os.Getenv("author_file") + } +} + +func CheckAuthorFile() string { + authorfile := Find_authorfile() + if _, err := os.Stat(authorfile); os.IsNotExist(err) { + println("Author file not found at: ", authorfile) + println("Would you like to create one? (y/n)") + var response string + _, err := fmt.Scanln(&response) + if err != nil { + println("Error reading response") + } + if response == "y" { + //TODO: Tui response to create author file + //createAuthorFile(authorfile) + } else { + os.Exit(1) + } + } + // This string output is mostly for convenience can mostly be ignored + return authorfile +} diff --git a/src_code/go_src/cmd/utils/commit.go b/src_code/go_src/cmd/utils/commit.go new file mode 100644 index 0000000..e8271b3 --- /dev/null +++ b/src_code/go_src/cmd/utils/commit.go @@ -0,0 +1,110 @@ +package utils + +import ( + "fmt" + "os/exec" + "regexp" + "slices" + "strings" +) + +// 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 +var reg, _ = regexp.Compile("([^:]+):([^:]+)") + +func Commit(message string, authors []string) string { + // write the commit message to the string builder + sb.WriteString(message + "\n") + fst := authors[0] + + if fst == "all" || fst == "All" { + add_x_users(excludeMode) + goto skip_loop + } else if Groups[fst] != nil { + excludeMode = group_selection(Groups[fst], excludeMode) + add_x_users(excludeMode) + goto skip_loop + } + + // Loop that adds users + for _, committer := range authors { + if _, ok := Users[committer]; ok { + sb_author(committer) + } else if match := reg.MatchString(committer); match { + str := strings.Split(committer, ":") + + sb.WriteString("\nCo-authored-by: ") + sb.WriteString(str[0]) + sb.WriteString(" <") + sb.WriteString(str[1]) + sb.WriteRune('>') + + } else if committer[0] == '^' { // Negations + excludeMode = append(excludeMode, Users[committer[1:]].Username) + } else { + 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 +skip_loop: + return sb.String() +} + +func GitWrapper(commit string) { + // commit shell command + cmd := exec.Command("git", "commit", "-m", commit) + + // https://stackoverflow.com/questions/18159704/how-to-debug-exit-status-1-error-when-running-exec-command-in-golang + + cmd_output, err := cmd.CombinedOutput() + + if err != nil { + println(fmt.Sprint(err) + " : " + string(cmd_output)) + } else { + println(string(cmd_output)) + } +} + +// helper function to add an author to the commit message +func sb_author(committer string) { + sb.WriteString("\nCo-authored-by: ") + sb.WriteString(Users[committer].Username) + sb.WriteString(" <") + sb.WriteString(Users[committer].Email) + sb.WriteRune('>') +} + +// helper function to add x amount of users to the commit message +func add_x_users(excludeMode []string) { + if len(DefExclude) > 0 { + excludeMode = append(excludeMode, DefExclude...) + } + for key, user := range Users { + if !slices.Contains(excludeMode, user.Username) { + sb_author(key) + excludeMode = append(excludeMode, user.Username) + } + } +} + +// helper function to select a group of users to exclude in the commit message +func group_selection(group []User, excludeMode []string) []string { + for _, user := range Users { + if !(slices.Contains(group, user)) { + excludeMode = append(excludeMode, user.Username) + } + } + + return excludeMode +} diff --git a/src_code/go_src/cmd/utils/user_util.go b/src_code/go_src/cmd/utils/user_util.go new file mode 100644 index 0000000..55bdaab --- /dev/null +++ b/src_code/go_src/cmd/utils/user_util.go @@ -0,0 +1,69 @@ +package utils + +import ( + "bufio" + "os" + "strings" +) + +// This util file is used to handle users and their information +type User struct { + Username string + Email string + Names string +} + +var Users = map[string]User{} +var DefExclude = []string{} +var Groups = map[string][]User{} + +func Define_users(author_file string) { + file, err := os.Open(author_file) + if err != nil { + print("File not found") + os.Exit(2) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + // eat a single input + scanner.Scan() + + // reads the input of authors file and formats accordingly + for scanner.Scan() { + input_str := scanner.Text() + group_info := []string{} + if strings.Contains(input_str, ";;") { + input := strings.Split(input_str, ";;") + input_str = input[0] + group_info = append(group_info, strings.Split(input[1], "|")...) + } + info := strings.Split(input_str, "|") + usr := User{Username: info[2], Email: info[3], Names: info[0] + "/" + info[1]} + Users[info[0]] = usr + Users[info[1]] = usr + // Adds users with the ex tag to the defExclude list + if len(info) == 5 { + if info[4] == "ex" { + DefExclude = append(DefExclude, info[2]) + } + } else if len(group_info) > 0 { + // Group assignment + for _, group := range group_info { + if Groups[group] == nil { + Groups[group] = []User{usr} + } else { + //TODO: Try and find a cleaner way of doing this + usr_lst := Groups[group] + usr_lst = append(usr_lst, usr) + Groups[group] = usr_lst + } + } + } + } + + if err := scanner.Err(); err != nil { + os.Exit(2) + } +} diff --git a/src_code/go_src/cocommit.go b/src_code/go_src/cocommit.go deleted file mode 100644 index ece86ec..0000000 --- a/src_code/go_src/cocommit.go +++ /dev/null @@ -1,153 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "os/exec" - "regexp" - "slices" - "strings" -) - -type user struct { - username string - email string -} - -var users = make(map[string]user) -var sb strings.Builder -var all_flag = false - -func main() { - - - // Reads a shell env variable :: author_file - authors := os.Getenv("author_file") - - file, err := os.Open(authors) - if err != nil { - print("File not found") - os.Exit(2) - } - defer file.Close() - - scanner := bufio.NewScanner(file) - - // eat a single input - scanner.Scan() - - // reads the input of authors file and formats accordingly - for scanner.Scan() { - info := strings.Split(scanner.Text(), "|") - usr := user{username: info[2], email: info[3]} - users[info[0]] = usr - users[info[1]] = usr - } - - if err := scanner.Err(); err != nil { - os.Exit(2) - } - - args := os.Args[1:] - - NoInput(args, users) - - excludeMode := []string{} - - // builds the commit message with the selected authors - sb.WriteString(string(args[0]) + "\n") - reg, _ := regexp.Compile("([^:]+):([^:]+)") - - if args[1] == "all" || args[1] == "All" { - all_flag = true - goto skip_loop - } - - - for _, committer := range args[1:] { - if _, ok := users[committer]; ok { - sb_author(committer) - } else if match := reg.MatchString(committer); match { - str := strings.Split(committer, ":") - - sb.WriteString("\nCo-authored-by: ") - sb.WriteString(str[0]) - sb.WriteString(" <") - sb.WriteString(str[1]) - sb.WriteRune('>') - - } else if committer[0] == '^' { - excludeMode = append(excludeMode, users[committer[1:]].username) - - } else { - println(committer, " was unknown. User either not defined or name typed wrong") - } - } - - skip_loop: - - if len(excludeMode) > 0 || all_flag { - add_x_users(excludeMode) - } - - - // commit msg built - commit := sb_build() - - //NOTE: Uncomment for testing - //print(commit) - - // commit shell command - cmd := exec.Command("git", "commit", "-m", commit) - - // https://stackoverflow.com/questions/18159704/how-to-debug-exit-status-1-error-when-running-exec-command-in-golang - - cmd_output, err := cmd.CombinedOutput() - - if err != nil { - println(fmt.Sprint(err) + " : " + string(cmd_output)) - } else { - println(string(cmd_output)) - } - -} - -func add_x_users(excludeMode []string) { - for key, user := range users { - if !slices.Contains(excludeMode, user.username) { - sb_author(key) - excludeMode = append(excludeMode, user.username) - } - } -} - -func sb_build() string { - return sb.String() -} - -func sb_author(committer string) { - sb.WriteString("\nCo-authored-by: ") - sb.WriteString(users[committer].username) - sb.WriteString(" <") - sb.WriteString(users[committer].email) - sb.WriteRune('>') -} - -// TODO: move half this into another function and call before building users to improve performance -func NoInput(args []string, users map[string]user) { - if len(args) < 2 { - // If you call binary with users prints users - if len(args) == 1 && args[0] == "users" { - println("List of users:") - for name, usr := range users { - println(name, " ->", " Username:", usr.username, " Email:", usr.email) - } - os.Exit(1) - } - // if calling binary with nothing or only string - print("Usage: cocommit [co-author2] [co-author3] || \n cocommit [co-author2:email] [co-author3:email] || Mixes of both") - - os.Exit(1) - } -} diff --git a/src_code/go_src/deprecated/cocommit.go b/src_code/go_src/deprecated/cocommit.go new file mode 100644 index 0000000..f0a4c7f --- /dev/null +++ b/src_code/go_src/deprecated/cocommit.go @@ -0,0 +1,309 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "regexp" + "slices" + "sort" + "strings" +) + +type user struct { + username string + email string + names string +} +//TODO: Remove later once everything is up and running with the new version + +// Map of all th users in the author file +var users = make(map[string]user) + +// String builder for building the commit message +var sb strings.Builder + +// Flag that can be toggled to include all users in a commit message (excluding defExclude) +var all_flag = false + +// DefaultExclude -> A list that contains users marked with ex meaning +// they should not be included in all and negations +var defExclude = []string{} + +// Group map for adding people as a group +var groups = make(map[string][]user) + +func main() { + + // Reads a shell env variable :: author_file + var authors string + envVar := os.Getenv("author_file") + if envVar == "" { + var err error + authors, err = os.UserConfigDir() + authors += "/cocommit/authors" + if err != nil { + println("Error: ", err) + os.Exit(1) + } + } else { + authors = envVar + } + + file, err := os.Open(authors) + if err != nil { + authors, _ = os.UserConfigDir() + authors += "/cocommit/authors" + println("Authors file cannot be found. Please check the path to the file. \nEither set the author_file env variable or place the file in the default location. \nDefault location: " + authors) + println("If you want to create a blank template file at the default location type y|yes or cancel with n|no") + var input string + fmt.Scanln(&input) + if input == "y" || input == "yes" { + create_author_file("yes") + os.Exit(1) + } else { + println("Cancelled") + os.Exit(1) + } + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + // eat a single input + scanner.Scan() + + // reads the input of authors file and formats accordingly + for scanner.Scan() { + input_str := scanner.Text() + group_info := []string{} + if strings.Contains(input_str, ";;") { + input := strings.Split(input_str, ";;") + input_str = input[0] + group_info = append(group_info, strings.Split(input[1], "|")...) + } + info := strings.Split(input_str, "|") + usr := user{username: info[2], email: info[3], names: info[0] + "/" + info[1]} + users[info[0]] = usr + users[info[1]] = usr + // Adds users with the ex tag to the defExclude list + if len(info) == 5 { + if info[4] == "ex" { + defExclude = append(defExclude, info[2]) + } + } else if len(group_info) > 0 { + // Group assignment + for _, group := range group_info { + if groups[group] == nil { + groups[group] = []user{usr} + } else { + //TODO: Try and find a cleaner way of doing this + usr_lst := groups[group] + usr_lst = append(usr_lst, usr) + groups[group] = usr_lst + } + } + } + } + + check_err(scanner.Err()) + // Removes the call command for the program + args := os.Args[1:] + + // Checks if the user called the program with any inputs or with non commit args + NoInput(args, users) + + // This list is used when doing negations and for removing duplicate users during string building + excludeMode := []string{} + + // builds the commit message with the selected authors + sb.WriteString(string(args[0]) + "\n") + + // Regex that catches one off authors + reg, _ := regexp.Compile("([^:]+):([^:]+)") + + if args[1] == "all" || args[1] == "All" { + all_flag = true + goto skip_loop + } else if groups[args[1]] != nil { + // Selects everybody that isn't the group members and adds them to the defExclude + excludeMode = group_selection(groups[args[1]], excludeMode) + goto skip_loop + } + + // Loop that adds users + for _, committer := range args[1:] { + if _, ok := users[committer]; ok { + sb_author(committer) + } else if match := reg.MatchString(committer); match { + str := strings.Split(committer, ":") + + sb.WriteString("\nCo-authored-by: ") + sb.WriteString(str[0]) + sb.WriteString(" <") + sb.WriteString(str[1]) + sb.WriteRune('>') + + } else if committer[0] == '^' { // Negations + excludeMode = append(excludeMode, users[committer[1:]].username) + + } else { + println(committer, " was unknown. User either not defined or name typed wrong") + } + } + + // Skip label for adding all +skip_loop: + + if len(excludeMode) > 0 || all_flag { + // adds all users not in the excludeMode list + add_x_users(excludeMode) + } + + // commit msg built + commit := sb_build() + + print(commit) + + //NOTE: Uncomment for testing + //print(commit) + + // commit shell command + cmd := exec.Command("git", "commit", "-m", commit) + + // https://stackoverflow.com/questions/18159704/how-to-debug-exit-status-1-error-when-running-exec-command-in-golang + + cmd_output, err := cmd.CombinedOutput() + + if err != nil { + println(fmt.Sprint(err) + " : " + string(cmd_output)) + } else { + println(string(cmd_output)) + } + +} + +func group_selection(group []user, excludeMode []string) []string { + for _, user := range users { + if !(slices.Contains(group, user)) { + excludeMode = append(excludeMode, user.username) + } + } + + return excludeMode +} + +func add_x_users(excludeMode []string) { + if len(defExclude) > 0 { + excludeMode = append(excludeMode, defExclude...) + } + for key, user := range users { + if !slices.Contains(excludeMode, user.username) { + sb_author(key) + excludeMode = append(excludeMode, user.username) + } + } +} + +func sb_build() string { + return sb.String() +} + +func sb_author(committer string) { + sb.WriteString("\nCo-authored-by: ") + sb.WriteString(users[committer].username) + sb.WriteString(" <") + sb.WriteString(users[committer].email) + sb.WriteRune('>') +} + +// TODO: move half this into another function and call before building users to improve performance +func NoInput(args []string, users map[string]user) { + if len(args) < 2 { + // If you call binary with users prints users + if len(args) == 1 && args[0] == "users" { + println("List of users:\nFormat: / -> Username: Email: ") + seen_users := []user{} + user_sb := []string{} + for name, usr := range users { + if !slices.Contains(seen_users, usr) { + user_sb = append(user_sb, users[name].names+" ->"+" Username: "+usr.username+" Email: "+usr.email+"\n") + seen_users = append(seen_users, usr) + } + } + sort.Strings(user_sb) + println(strings.Join(user_sb, "")) + os.Exit(1) + } else if len(args) == 1 && args[0] == "config" { + create_author_file() + } + // if calling binary with nothing or only string + command_options := []string{ + "cocommit [co-author2] [co-author3]", + "cocommit [co-author2:email] [co-author3:email]", + "cocommit all", + "cocommit ^ ^[co-author2]", + "cocommit ", + "cocommit users", + } + println("Usage:") + for _, v := range command_options { + print(v) + println(" ||") + } + println("Mixes of both") + + os.Exit(1) + } +} + +func create_author_file(param ...string) { + var input string + authors, err := os.UserConfigDir() + + if err != nil { + println("Error: ", err) + os.Exit(1) + } + if len(param) > 0 { + input = "yes" + goto skip + } + println("This command will create a blank template auhtor file in the default location. \nDefault location: " + authors + "\nConfirm by typing y|yes or cancel with n|no") + fmt.Scanln(&input) + if err != nil { + println("Error: ", err) + os.Exit(1) + } +skip: + if input == "y" || input == "yes" { + // create folder cocommit in .config + authors += "/cocommit" + err := os.MkdirAll(authors, 0755) + if err != nil { + println("Error in dir creation: ", err.Error()) + os.Exit(1) + } + authors += "/authors" + file, err := os.Create(authors) + if err != nil { + println("Error: ", err.Error()) + os.Exit(1) + } + defer file.Close() + file.WriteString("name_short|Name|Username|email (opt: |ex) (opt: ;;group1 or ;;group1|group2|group3...)\n") + println("File created successfully at: " + authors) + os.Exit(1) + } else { + println("Cancelled") + os.Exit(1) + } +} + +func check_err(e error) { + if e != nil { + fmt.Println(e.Error()) + os.Exit(2) + } +} \ No newline at end of file diff --git a/src_code/go_src/cocommit_test.go b/src_code/go_src/deprecated/cocommit_test.go similarity index 100% rename from src_code/go_src/cocommit_test.go rename to src_code/go_src/deprecated/cocommit_test.go diff --git a/src_code/go_src/main.go b/src_code/go_src/main.go new file mode 100644 index 0000000..54c4d10 --- /dev/null +++ b/src_code/go_src/main.go @@ -0,0 +1,11 @@ +/* +Copyright © 2024 NAME HERE + +*/ +package main + +import "main/src_code/go_src/cmd" + +func main() { + cmd.Execute() +}