GitRoot

craft your forge, build your project, grow your community freely
  1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package user
  6
  7import (
  8	"bytes"
  9	"fmt"
 10	"slices"
 11
 12	"github.com/goccy/go-yaml"
 13	"github.com/goccy/go-yaml/parser"
 14	"github.com/samber/oops"
 15)
 16
 17type GitRootGroup struct {
 18	GroupName string
 19	Branches  []string
 20}
 21
 22type GitRootUser struct {
 23	Group     *GitRootGroup
 24	Email     string
 25	Pseudo    string
 26	AllGroups []*GitRootGroup
 27	PubKey    string
 28}
 29
 30type Group struct {
 31	Name     string `yaml:"-"`
 32	Branches []Branch
 33	Users    []User
 34}
 35
 36type Branch struct {
 37	Name string
 38	Not  []string `yaml:",omitempty"`
 39	Only []string `yaml:",omitempty"`
 40}
 41
 42type User struct {
 43	Pseudo string
 44	Avatar string
 45	Emails []string
 46	Ssh    []string
 47}
 48
 49func ParseGroups(fileContent []byte) ([]Group, error) {
 50	groups := make([]Group, 0)
 51	groupsByName := make(map[string]Group)
 52	if err := yaml.UnmarshalWithOptions(fileContent, &groupsByName, yaml.UseOrderedMap()); err != nil {
 53		return nil, oops.Wrapf(err, "can't parse yaml users")
 54	}
 55	for name, group := range groupsByName {
 56		group.Name = name
 57		groups = append(groups, group)
 58	}
 59	return groups, nil
 60}
 61
 62type SimpleUser struct {
 63	Pseudo string
 64	Email  string
 65	Ssh    string
 66}
 67
 68func CreateFileUser(defaultBranch string, users ...SimpleUser) ([]byte, error) {
 69	u := make([]User, len(users))
 70	for i, j := range users {
 71		u[i] = User{
 72			Pseudo: j.Pseudo,
 73			Avatar: "",
 74			Emails: []string{j.Email},
 75			Ssh:    []string{j.Ssh},
 76		}
 77	}
 78	g := Group{
 79		Name:     "owner",
 80		Branches: []Branch{{Name: defaultBranch}},
 81		Users:    u,
 82	}
 83	content, err := yaml.Marshal(map[string]Group{"owner": g})
 84	return content, oops.Wrapf(err, "can't marshal yaml users")
 85}
 86
 87func AppendUserToGroup(file []byte, groupName string, users ...SimpleUser) ([]byte, error) {
 88	urlPath, err := yaml.PathString(fmt.Sprintf("$.%s.users", groupName))
 89	if err != nil {
 90		return nil, oops.Wrapf(err, "invalid path yaml")
 91	}
 92	f, err := parser.ParseBytes(file, 0)
 93	if err != nil {
 94		return nil, oops.Wrapf(err, "can't parse file")
 95	}
 96	u := make([]User, 0)
 97	for _, j := range users {
 98		if !bytes.Contains(file, []byte(j.Ssh)) {
 99			u = append(u, User{
100				Pseudo: j.Pseudo,
101				Avatar: "",
102				Emails: []string{j.Email},
103				Ssh:    []string{j.Ssh},
104			})
105		}
106	}
107	content, err := yaml.Marshal(u)
108	if err != nil {
109		return nil, oops.Wrapf(err, "can't marshal u")
110	}
111	err = urlPath.MergeFromReader(f, bytes.NewReader(content))
112	return []byte(f.String()), oops.Wrapf(err, "can't merge yaml")
113}
114
115func AppendBranchToGroup(file []byte, groupName string, branches ...string) ([]byte, error) {
116	urlPath, err := yaml.PathString(fmt.Sprintf("$.%s.branches", groupName))
117	if err != nil {
118		return nil, oops.Wrapf(err, "invalid path yaml")
119	}
120	f, err := parser.ParseBytes(file, 0)
121	if err != nil {
122		return nil, oops.Wrapf(err, "can't parse file")
123	}
124	u := make([]Branch, len(branches))
125	for i, j := range branches {
126		u[i] = Branch{
127			Name: j,
128		}
129	}
130	content, err := yaml.Marshal(u)
131	if err != nil {
132		return nil, oops.Wrapf(err, "can't marshal u")
133	}
134	err = urlPath.MergeFromReader(f, bytes.NewReader(content))
135	return []byte(f.String()), oops.Wrapf(err, "can't merge yaml")
136}
137
138func AppendBranch(file []byte, groupName string, users ...SimpleUser) ([]byte, error) {
139	u := make([]User, len(users))
140	for i, j := range users {
141		u[i] = User{
142			Pseudo: j.Pseudo,
143			Avatar: "",
144			Emails: []string{j.Email},
145			Ssh:    []string{j.Ssh},
146		}
147	}
148	group := Group{Name: groupName, Branches: []Branch{{Name: groupName}}, Users: u}
149	content, err := yaml.Marshal(map[string]Group{groupName: group})
150	if err != nil {
151		return nil, oops.Wrapf(err, "can't marshal u")
152	}
153
154	return AppendBranchToGroup(append(file, content...), "owner", groupName)
155}
156
157func FindUser(file []byte, branch string, sshKey string) (*GitRootUser, error) {
158	groups, err := ParseGroups(file)
159	if err != nil {
160		return nil, oops.Wrapf(err, "can't read group")
161	}
162	found := false
163	var goodUser User
164	var goodGroup *GitRootGroup
165	allGroups := make([]*GitRootGroup, 0)
166	for _, g := range groups {
167		branches := make([]string, len(g.Branches))
168		for i, b := range g.Branches {
169			branches[i] = b.Name
170		}
171		currentGroup := &GitRootGroup{
172			GroupName: g.Name,
173			Branches:  branches,
174		}
175		allGroups = append(allGroups, currentGroup)
176		if slices.Contains(currentGroup.Branches, branch) {
177			for _, u := range g.Users {
178				for _, s := range u.Ssh {
179					if s == sshKey {
180						found = true
181						goodUser = u
182						goodGroup = currentGroup
183						break
184					}
185				}
186				if found {
187					break
188				}
189			}
190		}
191
192	}
193	if !found {
194		return &GitRootUser{
195			Group:     nil,
196			Email:     "",
197			Pseudo:    "",
198			AllGroups: allGroups,
199			PubKey:    sshKey,
200		}, nil
201	}
202	return &GitRootUser{
203		Group:     goodGroup,
204		Email:     goodUser.Emails[0],
205		Pseudo:    goodUser.Pseudo,
206		AllGroups: allGroups,
207		PubKey:    sshKey,
208	}, nil
209}
210
211func (u *GitRootUser) CanWrite(branch string) bool {
212	// user is in the group of the branch
213	if u.Group != nil { // new user
214		for _, b := range u.Group.Branches {
215			if b == branch || b == "*" {
216				return true
217			}
218		}
219	}
220	// other group have right for this branch
221	for _, g := range u.AllGroups {
222		if g.GroupName == "*" {
223			return false
224		}
225		if u.Group == nil || u.Group.GroupName != g.GroupName {
226			for _, b := range g.Branches {
227				if b == branch {
228					return false
229				}
230			}
231		}
232	}
233	// nobody take care of this branch
234	return true
235}