From ec2f36158350623493a570f2cd77ce2bc703c8ec Mon Sep 17 00:00:00 2001 From: Slug-Boi Date: Sat, 29 Mar 2025 22:12:52 +0100 Subject: [PATCH 01/10] feat: allow for json files to be opened instead of the weird self made csv format --- src/cmd/tui/tui_groups.go | 3 +- src/cmd/users.go | 5 +- src/cmd/utils/commit.go | 2 +- src/cmd/utils/user_util.go | 100 ++++++++++++++++--------------------- 4 files changed, 47 insertions(+), 63 deletions(-) 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/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/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..7a509ea 100644 --- a/src/cmd/utils/user_util.go +++ b/src/cmd/utils/user_util.go @@ -1,84 +1,75 @@ 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 } 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) + } + err = json.Unmarshal(data, &auth) + if err != nil { + fmt.Println("Error unmarshalling json: ", 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]) - } + 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 +77,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) { From 39781da299ffb91ab9ea4728078b218dbc01235d Mon Sep 17 00:00:00 2001 From: Slug-Boi Date: Wed, 2 Apr 2025 18:25:14 +0200 Subject: [PATCH 02/10] refactor(json): add ability to delete author using the new json author file --- src/cmd/utils/author_file_utils.go | 52 +++++++++++++----------------- src/cmd/utils/user_util.go | 9 ++++-- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/cmd/utils/author_file_utils.go b/src/cmd/utils/author_file_utils.go index 1b071d5..ea63fed 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" ) @@ -71,47 +69,43 @@ 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/user_util.go b/src/cmd/utils/user_util.go index 7a509ea..baeeb51 100644 --- a/src/cmd/utils/user_util.go +++ b/src/cmd/utils/user_util.go @@ -23,6 +23,9 @@ 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{} @@ -45,7 +48,7 @@ func Define_users(author_file string) { Groups = map[string][]User{} var auth Author - + data, err := os.ReadFile(author_file) if err != nil { fmt.Println("Error reading author file: ", err) @@ -56,7 +59,9 @@ func Define_users(author_file string) { fmt.Println("Error unmarshalling json: ", err) os.Exit(2) } - + + Authors = auth + for _, usr := range auth.Authors { Users[usr.Shortname] = usr Users[usr.Longname] = usr From 4a8572efdf975081bee2c4a8fb05cdbc049af5bd Mon Sep 17 00:00:00 2001 From: Slug-Boi Date: Wed, 2 Apr 2025 18:34:43 +0200 Subject: [PATCH 03/10] chore: defer close --- src/cmd/utils/author_file_utils.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cmd/utils/author_file_utils.go b/src/cmd/utils/author_file_utils.go index ea63fed..f019073 100644 --- a/src/cmd/utils/author_file_utils.go +++ b/src/cmd/utils/author_file_utils.go @@ -80,6 +80,7 @@ func DeleteOneAuthor(author string) { fmt.Println("Error opening file: ", err) return } + defer file.Close() // check that users aren't empty if len(Users) < 1 { From 5106e9ce1eb0a332250b82936112d2c925a196e6 Mon Sep 17 00:00:00 2001 From: Slug-Boi Date: Wed, 2 Apr 2025 18:35:10 +0200 Subject: [PATCH 04/10] refactor(json): create author now works on new json authorfile --- src/cmd/tui/tui_author.go | 42 +++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/cmd/tui/tui_author.go b/src/cmd/tui/tui_author.go index 6213ff1..d87079f 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" @@ -250,28 +251,35 @@ 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")) + // 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: strings.Split(m.inputs[4].Value(), "|"), } - if m.inputs[4].Value() != "" { - sb.WriteString(fmt.Sprintf(";;%s", m.inputs[4].Value())) + 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)) + } - //sb.WriteRune('\n') + // write the data to the file + f.Truncate(0) + f.Seek(0, 0) + f.Write(data) + f.Close() - if _, err = f.WriteString(sb.String()); err != nil { - panic(err) - } + // redefine the users map for the tui to use utils.Define_users(utils.Find_authorfile()) author := m.inputs[0].Value() From 0f45cd9cd98d137be047aa688e7dea3989a8755d Mon Sep 17 00:00:00 2001 From: Slug-Boi Date: Wed, 2 Apr 2025 18:42:41 +0200 Subject: [PATCH 05/10] docs(json): update the readme with the new json authorfile format --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 73773f5..e9fcb2c 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) From 375d7f98e559b4c2065f765fe5dcd08370c7e776 Mon Sep 17 00:00:00 2001 From: Slug-Boi Date: Wed, 2 Apr 2025 19:27:03 +0200 Subject: [PATCH 06/10] fix(json): change groups default from empty string to empty string slice --- src/cmd/tui/tui_author.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/cmd/tui/tui_author.go b/src/cmd/tui/tui_author.go index d87079f..7ec54d3 100644 --- a/src/cmd/tui/tui_author.go +++ b/src/cmd/tui/tui_author.go @@ -250,6 +250,15 @@ func (m *model_ca) AddAuthor() { } defer f.Close() + var groups []string + if m.inputs[4].Value() == "" { + groups = []string{} + } else { + groups = strings.Split(m.inputs[4].Value(), "|") + } + + + // create and add the user to the users map usr := utils.User{ @@ -258,7 +267,7 @@ func (m *model_ca) AddAuthor() { Username: m.inputs[2].Value(), Email: m.inputs[3].Value(), Ex: m.exclude, - Groups: strings.Split(m.inputs[4].Value(), "|"), + Groups: groups, } utils.Users[m.inputs[0].Value()] = usr From bddcaa9ba205213685471a24096d5952aa185a3e Mon Sep 17 00:00:00 2001 From: Slug-Boi Date: Wed, 2 Apr 2025 19:27:32 +0200 Subject: [PATCH 07/10] refactor(json): change create authorfile to json file and update directory creator to not panic --- src/cmd/utils/author_file_utils.go | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/cmd/utils/author_file_utils.go b/src/cmd/utils/author_file_utils.go index f019073..f1a45f7 100644 --- a/src/cmd/utils/author_file_utils.go +++ b/src/cmd/utils/author_file_utils.go @@ -18,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") } @@ -40,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 os.Stat(cocommit_folder); os.IsNotExist(err) { + 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 { @@ -54,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) } From 36023787b4f3049f4b4bfea9c642a84d4d116dd7 Mon Sep 17 00:00:00 2001 From: Slug-Boi Date: Wed, 2 Apr 2025 19:27:45 +0200 Subject: [PATCH 08/10] test(json): change all test data to json --- src/cmd/cmd_test.go | 26 +++++++++++++++++++++++--- src/cmd/tui/tui_test.go | 28 ++++++++++++++++++++++++---- src/cmd/utils/util_test.go | 26 +++++++++++++++++++++++--- 3 files changed, 70 insertions(+), 10 deletions(-) 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_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/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") From 3690597b0a12f83384448e58ad3fa9f666de03b2 Mon Sep 17 00:00:00 2001 From: Theis <112340858+Slug-Boi@users.noreply.github.com> Date: Wed, 2 Apr 2025 19:30:43 +0200 Subject: [PATCH 09/10] Update src/cmd/utils/author_file_utils.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/cmd/utils/author_file_utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/utils/author_file_utils.go b/src/cmd/utils/author_file_utils.go index f1a45f7..bb1c6c4 100644 --- a/src/cmd/utils/author_file_utils.go +++ b/src/cmd/utils/author_file_utils.go @@ -40,7 +40,7 @@ func CheckAuthorFile() string { cocommit_folder = strings.Join(parts[:len(parts)-1], "/") // create the author file - if os.Stat(cocommit_folder); os.IsNotExist(err) { + 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) From 834ce162a42e8c4436dca37102cb587386e2037e Mon Sep 17 00:00:00 2001 From: Slug-Boi Date: Wed, 2 Apr 2025 19:31:33 +0200 Subject: [PATCH 10/10] docs: capital A --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9fcb2c..16a05f3 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ A sample lazygit config file can be found [here](https://github.com/Slug-Boi/coc 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":{ + "Authors":{ "Morgan Rivers":{ "shortname":"mr", "longname":"Morgan Rivers",