diff --git a/README.md b/README.md index 73773f5..16a05f3 100644 --- a/README.md +++ b/README.md @@ -128,13 +128,49 @@ customCommands: A sample lazygit config file can be found [here](https://github.com/Slug-Boi/cocommit/blob/main/lazygit_config/config.yml) # Syntax for the author file -The syntax for the author file can be found at the top of the template file included in the repo. It should look like this (opt) is optional syntax: +The syntax for the author file is json below is a small example with fake information to show what it looks like. The author file can be edited safely from the tool so there is no real need to edit this manually. Whilst this format is a little heavier than the old custom CSV format it is much easier to work with and handle json so rest assured this is best way forward +```json +{ + "Authors":{ + "Morgan Rivers":{ + "shortname":"mr", + "longname":"Morgan Rivers", + "username":"morgan-rivers", + "email":"mrivers@example.com", + "ex":false, + "groups":[ + "dev", + "qa", + "design" + ] + }, + "Taylor Chen":{ + "shortname":"tc", + "longname":"Taylor Chen", + "username":"tchen", + "email":"tchen@example.org", + "ex":true, + "groups":[ + "dev", + "admin", + "support" + ] + }, + "Jordan Smithfield":{ + "shortname":"js", + "longname":"Jordan Smithfield", + "username":"jsmith", + "email":"j.smithfield@test.net", + "ex":false, + "groups":[ + "marketing", + "content", + "social" + ] + } + } +} ``` -name_short|Name|Username|email (opt: |ex) (opt: ;;group1 or ;;group1|group2|group3...) -``` -opt explained: -ex -> excludes the given author for all and negation selection commands -group -> groups an author which can then be called as an argument to add all people from that group. An author can be a part of multiple groups # Why? Co-authoring commits is a feature that is supported by github and gitlab and other git hosting services but creating the commits can be a bit of a pain. Co-authoring is extremely useful as teams can be much more transparent in who worked on what and it can be a great way to give credit to people who have helped on projects. This will make git-blame a lot more useful as you can quickly see who to contact or talk to about a specific part of the code. I strongly believe that this feature is underutilized and i attribute it mostly to the fact that is combersome to use. This tool aims to fix and streamline that process. (It even allows for automation of the process with the CLI mode) diff --git a/src/cmd/cmd_test.go b/src/cmd/cmd_test.go index d3678ea..3fb045d 100644 --- a/src/cmd/cmd_test.go +++ b/src/cmd/cmd_test.go @@ -10,9 +10,29 @@ import ( "github.com/Slug-Boi/cocommit/src/cmd/utils" ) -const author_data = `syntax for the test file -te|testing|TestUser|test@test.test|ex -ti|testtest|UserName2|testing@user.io;;gr1` +const author_data = ` +{ + "Authors": { + "testing": { + "shortname": "te", + "longname": "testing", + "username": "TestUser", + "email": "test@test.test", + "ex": true, + "groups": [] + }, + "testtest": { + "shortname": "ti", + "longname": "testtest", + "username": "UserName2", + "email": "testing@user.io", + "ex": false, + "groups": [ + "gr1" + ] + } + } +}` var envVar = utils.Find_authorfile() diff --git a/src/cmd/tui/tui_author.go b/src/cmd/tui/tui_author.go index 6213ff1..7ec54d3 100644 --- a/src/cmd/tui/tui_author.go +++ b/src/cmd/tui/tui_author.go @@ -4,6 +4,7 @@ package tui // from the Bubbles component library. import ( + "encoding/json" "fmt" "os" "strings" @@ -249,29 +250,45 @@ func (m *model_ca) AddAuthor() { } defer f.Close() - - sb := strings.Builder{} - sb.WriteRune('\n') - - sb.WriteString(fmt.Sprintf("%s|%s|%s|%s", - m.inputs[0].Value(), - m.inputs[1].Value(), - m.inputs[2].Value(), - m.inputs[3].Value())) - - if m.exclude { - sb.WriteString(fmt.Sprintf("|%s", "ex")) + var groups []string + if m.inputs[4].Value() == "" { + groups = []string{} + } else { + groups = strings.Split(m.inputs[4].Value(), "|") } - if m.inputs[4].Value() != "" { - sb.WriteString(fmt.Sprintf(";;%s", m.inputs[4].Value())) + + + + // create and add the user to the users map + usr := utils.User{ + Shortname: m.inputs[0].Value(), + Longname: m.inputs[1].Value(), + Username: m.inputs[2].Value(), + Email: m.inputs[3].Value(), + Ex: m.exclude, + Groups: groups, } - //sb.WriteRune('\n') + utils.Users[m.inputs[0].Value()] = usr + utils.Users[m.inputs[1].Value()] = usr + + + utils.Authors.Authors[m.inputs[1].Value()] = usr - if _, err = f.WriteString(sb.String()); err != nil { - panic(err) + 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() diff --git a/src/cmd/tui/tui_groups.go b/src/cmd/tui/tui_groups.go index 2293219..4fdb07e 100644 --- a/src/cmd/tui/tui_groups.go +++ b/src/cmd/tui/tui_groups.go @@ -125,8 +125,7 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { for k, v := range dupProtect { if _, ok := selected[k]; !ok { for _, user := range users { - split := strings.Split(user.Names, "/") - if split[0] == v || split[1] == v { + if user.Shortname == v || user.Longname == v { selectToggle(item(k)) } } diff --git a/src/cmd/tui/tui_test.go b/src/cmd/tui/tui_test.go index 526a0df..2889ff1 100644 --- a/src/cmd/tui/tui_test.go +++ b/src/cmd/tui/tui_test.go @@ -12,9 +12,29 @@ import ( "github.com/charmbracelet/x/exp/teatest" ) -const author_data = `syntax for the test file -te|testing|TestUser|test@test.test|ex;;gr0 -ti|testtest|UserName2|testing@user.io;;gr1` +const author_data = ` +{ + "Authors": { + "testing": { + "shortname": "te", + "longname": "testing", + "username": "TestUser", + "email": "test@test.test", + "ex": true, + "groups": [] + }, + "testtest": { + "shortname": "ti", + "longname": "testtest", + "username": "UserName2", + "email": "testing@user.io", + "ex": false, + "groups": [ + "gr1" + ] + } + } +}` var envVar string @@ -54,7 +74,7 @@ func TestShowUser(t *testing.T) { teatest.WithInitialTermSize(300, 300), ) teatest.WaitFor(t, tm.Output(), func(bts []byte) bool { - return bytes.Contains(bts, []byte("syntax for the test file")) + return bytes.Contains(bts, []byte("\"Authors\": {")) }, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2)) keyPress(tm, "q") diff --git a/src/cmd/users.go b/src/cmd/users.go index 591558e..012d280 100644 --- a/src/cmd/users.go +++ b/src/cmd/users.go @@ -3,7 +3,6 @@ package cmd import ( "os" "os/exec" - "slices" "sort" "strings" @@ -34,8 +33,8 @@ func UsersCmd() *cobra.Command { 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") + if !utils.ContainsUser(seen_users, usr) { + user_sb = append(user_sb, utils.Users[name].Shortname+"/"+utils.Users[name].Longname+" ->"+" Username: "+usr.Username+" Email: "+usr.Email+"\n") seen_users = append(seen_users, usr) } } diff --git a/src/cmd/utils/author_file_utils.go b/src/cmd/utils/author_file_utils.go index 1b071d5..bb1c6c4 100644 --- a/src/cmd/utils/author_file_utils.go +++ b/src/cmd/utils/author_file_utils.go @@ -1,11 +1,9 @@ package utils import ( - "bufio" - "bytes" + "encoding/json" "fmt" "os" - "regexp" "strings" ) @@ -20,7 +18,7 @@ func Find_authorfile() string { fmt.Println("Error getting user config directory") os.Exit(2) } - return (authors + "/cocommit/authors") + return (authors + "/cocommit/authors.json") } else { return os.Getenv("author_file") } @@ -42,10 +40,12 @@ func CheckAuthorFile() string { cocommit_folder = strings.Join(parts[:len(parts)-1], "/") // create the author file - err := os.Mkdir(cocommit_folder, 0766) - if err != nil { - fmt.Println("Error creating directory: ", err, cocommit_folder) - os.Exit(1) + if _, dirErr := os.Stat(cocommit_folder); os.IsNotExist(dirErr) { + err := os.Mkdir(cocommit_folder, 0766) + if err != nil { + fmt.Println("Error creating directory: ", err, cocommit_folder) + os.Exit(1) + } } file, err := os.Create(authorfile) if err != nil { @@ -56,10 +56,15 @@ func CheckAuthorFile() string { defer file.Close() // write the header to the file - file.WriteString("Syntax: name_short|Name|Username|email (opt: |ex) (opt: ;;group1|group2|group3...)\n") + json_string := + `{ + "Authors": { + } +}` + + file.Write([]byte(json_string)) fmt.Println("Author file created. To add authors please launch the TUI with -a and press 'C'") - } else { os.Exit(1) } @@ -71,47 +76,44 @@ func CheckAuthorFile() string { func DeleteOneAuthor(author string) { author_file := Find_authorfile() + if _, exists := Users[author]; !exists { + fmt.Println("User not found") + return + } + // open author_file - file, err := os.OpenFile(author_file, os.O_RDWR, 0644) + file, err := os.OpenFile(author_file, os.O_RDWR, 0666) if err != nil { fmt.Println("Error opening file: ", err) return } - defer file.Close() - // create regex to capture author line - regexp, err := regexp.Compile(fmt.Sprintf("^(.+\\|%s\\|.+|%s\\|.+\\|.+)$", author, author)) - if err != nil { - fmt.Println("Error compiling regex: ", err) + // check that users aren't empty + if len(Users) < 1 { + fmt.Println("No users to remove") return } - var b []byte - buf := bytes.NewBuffer(b) + usr := Users[author] - // create a scanner for the file - scanner := bufio.NewScanner(file) - - // write the header to the buffer - scanner.Scan() - buf.WriteString(scanner.Text() + "\n") - - // check if author matches the regex and skip - for scanner.Scan() { - line := scanner.Text() - if regexp.MatchString(line) { - continue - } - buf.WriteString(line + "\n") + // Remove the user from the Author struct (try both short and long name) + delete(Authors.Authors, usr.Shortname) + delete(Authors.Authors, usr.Longname) + // marshal the struct back to json + data, err := json.MarshalIndent(Authors, "", " ") + if err != nil { + fmt.Println("Error marshalling json: ", err) + return } - // remove the last newline character - buf.Truncate(buf.Len() - 1) + + // write the data to the file file.Truncate(0) file.Seek(0, 0) - buf.WriteTo(file) + file.Write(data) + file.Close() RemoveUser(author) } diff --git a/src/cmd/utils/commit.go b/src/cmd/utils/commit.go index a3ad371..06f5c6b 100644 --- a/src/cmd/utils/commit.go +++ b/src/cmd/utils/commit.go @@ -119,7 +119,7 @@ func add_x_users(excludeMode []string) { // helper function to select groups 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)) { + if !(ContainsUser(group, user)) { excludeMode = append(excludeMode, user.Username) } } diff --git a/src/cmd/utils/user_util.go b/src/cmd/utils/user_util.go index 033c189..baeeb51 100644 --- a/src/cmd/utils/user_util.go +++ b/src/cmd/utils/user_util.go @@ -1,84 +1,80 @@ package utils import ( - "bufio" "fmt" "os" - "strings" + "slices" + + "encoding/json" ) // This util file is used to handle users and their information + type User struct { - Username string - Email string - Names string + Shortname string `json:"shortname"` + Longname string `json:"longname"` + Username string `json:"username"` + Email string `json:"email"` + Ex bool `json:"ex"` + Groups []string `json:"groups"` } +type Author struct { + Authors map[string]User +} + +// purely used for editing the author file later +var Authors = Author{} + var Users = map[string]User{} var DefExclude = []string{} var Groups = map[string][]User{} +func ContainsUser(users []User, user User) bool { + return slices.ContainsFunc(users, func(u User) bool { + return u.Shortname == user.Shortname && + u.Longname == user.Longname && + u.Username == user.Username && + u.Email == user.Email && + u.Ex == user.Ex && + slices.Equal(u.Groups, user.Groups) + }) +} + func Define_users(author_file string) { // wipe the users map Users = map[string]User{} DefExclude = []string{} Groups = map[string][]User{} - file, err := os.Open(author_file) + var auth Author + + data, err := os.ReadFile(author_file) if err != nil { - print("File not found") + fmt.Println("Error reading author file: ", err) 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, "|") - if len(info) < 4 { - if len(info) > 0 { - if info[0] == "" { - info[0] = "(empty string)" - } - fmt.Println("Error: User", info[0], "is missing information") - } else { - fmt.Println("Error: Some user is missing information") - } - fmt.Println("Please check the author file for proper syntax") - if input_str == "" { - fmt.Println("empty line found in author file") - } else { - fmt.Println("author file input:", input_str) - } - os.Exit(1) - } - 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]) - } + err = json.Unmarshal(data, &auth) + if err != nil { + fmt.Println("Error unmarshalling json: ", err) + os.Exit(2) + } + + Authors = auth + + for _, usr := range auth.Authors { + Users[usr.Shortname] = usr + Users[usr.Longname] = usr + if usr.Ex { + DefExclude = append(DefExclude, usr.Shortname) } + + group_info := usr.Groups 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 @@ -86,17 +82,12 @@ func Define_users(author_file string) { } } } - - if err := scanner.Err(); err != nil { - os.Exit(2) - } } func RemoveUser(short string) { usr := Users[short] - split := strings.Split(usr.Names, "/") - delete(Users, split[0]) - delete(Users, split[1]) + delete(Users, usr.Shortname) + delete(Users, usr.Longname) } func TempAddUser(username, email string) { diff --git a/src/cmd/utils/util_test.go b/src/cmd/utils/util_test.go index 3a66d3f..58823b6 100644 --- a/src/cmd/utils/util_test.go +++ b/src/cmd/utils/util_test.go @@ -6,9 +6,29 @@ import ( "testing" ) -const author_data = `syntax for the test file -te|testing|TestUser|test@test.test|ex -ti|testtest|UserName2|testing@user.io;;gr1` +const author_data = ` +{ + "Authors": { + "testing": { + "shortname": "te", + "longname": "testing", + "username": "TestUser", + "email": "test@test.test", + "ex": true, + "groups": [] + }, + "testtest": { + "shortname": "ti", + "longname": "testtest", + "username": "UserName2", + "email": "testing@user.io", + "ex": false, + "groups": [ + "gr1" + ] + } + } +}` var envVar = os.Getenv("author_file")