Merge pull request #64 from Slug-Boi/refactor_authors_as_json

feat:  change defautl author format to json
This commit is contained in:
Theis
2025-04-02 19:34:10 +02:00
committed by GitHub
10 changed files with 236 additions and 132 deletions
+42 -6
View File
@@ -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) 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 # 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? # 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) 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)
+23 -3
View File
@@ -10,9 +10,29 @@ import (
"github.com/Slug-Boi/cocommit/src/cmd/utils" "github.com/Slug-Boi/cocommit/src/cmd/utils"
) )
const author_data = `syntax for the test file const author_data = `
te|testing|TestUser|test@test.test|ex {
ti|testtest|UserName2|testing@user.io;;gr1` "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() var envVar = utils.Find_authorfile()
+34 -17
View File
@@ -4,6 +4,7 @@ package tui
// from the Bubbles component library. // from the Bubbles component library.
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@@ -249,29 +250,45 @@ func (m *model_ca) AddAuthor() {
} }
defer f.Close() defer f.Close()
var groups []string
sb := strings.Builder{} if m.inputs[4].Value() == "" {
sb.WriteRune('\n') groups = []string{}
} else {
sb.WriteString(fmt.Sprintf("%s|%s|%s|%s", groups = strings.Split(m.inputs[4].Value(), "|")
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"))
} }
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 { data, err := json.MarshalIndent(utils.Authors, "", " ")
panic(err) if err != nil {
panic(fmt.Sprintf("Error marshalling json: %v", err))
} }
// write the data to the file
f.Truncate(0)
f.Seek(0, 0)
f.Write(data)
f.Close()
// redefine the users map for the tui to use
utils.Define_users(utils.Find_authorfile()) utils.Define_users(utils.Find_authorfile())
author := m.inputs[0].Value() author := m.inputs[0].Value()
+1 -2
View File
@@ -125,8 +125,7 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
for k, v := range dupProtect { for k, v := range dupProtect {
if _, ok := selected[k]; !ok { if _, ok := selected[k]; !ok {
for _, user := range users { for _, user := range users {
split := strings.Split(user.Names, "/") if user.Shortname == v || user.Longname == v {
if split[0] == v || split[1] == v {
selectToggle(item(k)) selectToggle(item(k))
} }
} }
+24 -4
View File
@@ -12,9 +12,29 @@ import (
"github.com/charmbracelet/x/exp/teatest" "github.com/charmbracelet/x/exp/teatest"
) )
const author_data = `syntax for the test file const author_data = `
te|testing|TestUser|test@test.test|ex;;gr0 {
ti|testtest|UserName2|testing@user.io;;gr1` "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 var envVar string
@@ -54,7 +74,7 @@ func TestShowUser(t *testing.T) {
teatest.WithInitialTermSize(300, 300), teatest.WithInitialTermSize(300, 300),
) )
teatest.WaitFor(t, tm.Output(), func(bts []byte) bool { 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)) }, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2))
keyPress(tm, "q") keyPress(tm, "q")
+2 -3
View File
@@ -3,7 +3,6 @@ package cmd
import ( import (
"os" "os"
"os/exec" "os/exec"
"slices"
"sort" "sort"
"strings" "strings"
@@ -34,8 +33,8 @@ func UsersCmd() *cobra.Command {
seen_users := []utils.User{} seen_users := []utils.User{}
user_sb := []string{} user_sb := []string{}
for name, usr := range utils.Users { for name, usr := range utils.Users {
if !slices.Contains(seen_users, usr) { if !utils.ContainsUser(seen_users, usr) {
user_sb = append(user_sb, utils.Users[name].Names+" ->"+" Username: "+usr.Username+" Email: "+usr.Email+"\n") 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) seen_users = append(seen_users, usr)
} }
} }
+37 -35
View File
@@ -1,11 +1,9 @@
package utils package utils
import ( import (
"bufio" "encoding/json"
"bytes"
"fmt" "fmt"
"os" "os"
"regexp"
"strings" "strings"
) )
@@ -20,7 +18,7 @@ func Find_authorfile() string {
fmt.Println("Error getting user config directory") fmt.Println("Error getting user config directory")
os.Exit(2) os.Exit(2)
} }
return (authors + "/cocommit/authors") return (authors + "/cocommit/authors.json")
} else { } else {
return os.Getenv("author_file") return os.Getenv("author_file")
} }
@@ -42,10 +40,12 @@ func CheckAuthorFile() string {
cocommit_folder = strings.Join(parts[:len(parts)-1], "/") cocommit_folder = strings.Join(parts[:len(parts)-1], "/")
// create the author file // create the author file
err := os.Mkdir(cocommit_folder, 0766) if _, dirErr := os.Stat(cocommit_folder); os.IsNotExist(dirErr) {
if err != nil { err := os.Mkdir(cocommit_folder, 0766)
fmt.Println("Error creating directory: ", err, cocommit_folder) if err != nil {
os.Exit(1) fmt.Println("Error creating directory: ", err, cocommit_folder)
os.Exit(1)
}
} }
file, err := os.Create(authorfile) file, err := os.Create(authorfile)
if err != nil { if err != nil {
@@ -56,10 +56,15 @@ func CheckAuthorFile() string {
defer file.Close() defer file.Close()
// write the header to the file // 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'") fmt.Println("Author file created. To add authors please launch the TUI with -a and press 'C'")
} else { } else {
os.Exit(1) os.Exit(1)
} }
@@ -71,47 +76,44 @@ func CheckAuthorFile() string {
func DeleteOneAuthor(author string) { func DeleteOneAuthor(author string) {
author_file := Find_authorfile() author_file := Find_authorfile()
if _, exists := Users[author]; !exists {
fmt.Println("User not found")
return
}
// open author_file // 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 { if err != nil {
fmt.Println("Error opening file: ", err) fmt.Println("Error opening file: ", err)
return return
} }
defer file.Close() defer file.Close()
// create regex to capture author line // check that users aren't empty
regexp, err := regexp.Compile(fmt.Sprintf("^(.+\\|%s\\|.+|%s\\|.+\\|.+)$", author, author)) if len(Users) < 1 {
if err != nil { fmt.Println("No users to remove")
fmt.Println("Error compiling regex: ", err)
return return
} }
var b []byte usr := Users[author]
buf := bytes.NewBuffer(b)
// create a scanner for the file // Remove the user from the Author struct (try both short and long name)
scanner := bufio.NewScanner(file) delete(Authors.Authors, usr.Shortname)
delete(Authors.Authors, usr.Longname)
// 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")
// 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.Truncate(0)
file.Seek(0, 0) file.Seek(0, 0)
buf.WriteTo(file) file.Write(data)
file.Close()
RemoveUser(author) RemoveUser(author)
} }
+1 -1
View File
@@ -119,7 +119,7 @@ func add_x_users(excludeMode []string) {
// helper function to select groups of users to exclude in the commit message // helper function to select groups of users to exclude in the commit message
func group_selection(group []User, excludeMode []string) []string { func group_selection(group []User, excludeMode []string) []string {
for _, user := range Users { for _, user := range Users {
if !(slices.Contains(group, user)) { if !(ContainsUser(group, user)) {
excludeMode = append(excludeMode, user.Username) excludeMode = append(excludeMode, user.Username)
} }
} }
+49 -58
View File
@@ -1,84 +1,80 @@
package utils package utils
import ( import (
"bufio"
"fmt" "fmt"
"os" "os"
"strings" "slices"
"encoding/json"
) )
// This util file is used to handle users and their information // This util file is used to handle users and their information
type User struct { type User struct {
Username string Shortname string `json:"shortname"`
Email string Longname string `json:"longname"`
Names string 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 Users = map[string]User{}
var DefExclude = []string{} var DefExclude = []string{}
var Groups = map[string][]User{} 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) { func Define_users(author_file string) {
// wipe the users map // wipe the users map
Users = map[string]User{} Users = map[string]User{}
DefExclude = []string{} DefExclude = []string{}
Groups = map[string][]User{} Groups = map[string][]User{}
file, err := os.Open(author_file) var auth Author
data, err := os.ReadFile(author_file)
if err != nil { if err != nil {
print("File not found") fmt.Println("Error reading author file: ", err)
os.Exit(2) os.Exit(2)
} }
defer file.Close() err = json.Unmarshal(data, &auth)
if err != nil {
scanner := bufio.NewScanner(file) fmt.Println("Error unmarshalling json: ", err)
os.Exit(2)
// eat a single input }
scanner.Scan()
Authors = auth
// reads the input of authors file and formats accordingly
for scanner.Scan() { for _, usr := range auth.Authors {
input_str := scanner.Text() Users[usr.Shortname] = usr
group_info := []string{} Users[usr.Longname] = usr
if strings.Contains(input_str, ";;") { if usr.Ex {
input := strings.Split(input_str, ";;") DefExclude = append(DefExclude, usr.Shortname)
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])
}
} }
group_info := usr.Groups
if len(group_info) > 0 { if len(group_info) > 0 {
// Group assignment
for _, group := range group_info { for _, group := range group_info {
if Groups[group] == nil { if Groups[group] == nil {
Groups[group] = []User{usr} Groups[group] = []User{usr}
} else { } else {
//TODO: Try and find a cleaner way of doing this
usr_lst := Groups[group] usr_lst := Groups[group]
usr_lst = append(usr_lst, usr) usr_lst = append(usr_lst, usr)
Groups[group] = usr_lst 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) { func RemoveUser(short string) {
usr := Users[short] usr := Users[short]
split := strings.Split(usr.Names, "/") delete(Users, usr.Shortname)
delete(Users, split[0]) delete(Users, usr.Longname)
delete(Users, split[1])
} }
func TempAddUser(username, email string) { func TempAddUser(username, email string) {
+23 -3
View File
@@ -6,9 +6,29 @@ import (
"testing" "testing"
) )
const author_data = `syntax for the test file const author_data = `
te|testing|TestUser|test@test.test|ex {
ti|testtest|UserName2|testing@user.io;;gr1` "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") var envVar = os.Getenv("author_file")