+57
-67
appview/notify/db/db.go
+57
-67
appview/notify/db/db.go
···
3
import (
4
"context"
5
"log"
6
"slices"
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
···
12
"tangled.org/core/appview/notify"
13
"tangled.org/core/idresolver"
14
"tangled.org/core/orm"
15
-
"tangled.org/core/sets"
16
)
17
18
const (
19
-
maxMentions = 8
20
)
21
22
type databaseNotifier struct {
···
50
}
51
52
actorDid := syntax.DID(star.Did)
53
-
recipients := sets.Singleton(syntax.DID(repo.Did))
54
eventType := models.NotificationTypeRepoStarred
55
entityType := "repo"
56
entityId := star.RepoAt.String()
···
75
}
76
77
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {
78
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
79
if err != nil {
80
log.Printf("failed to fetch collaborators: %v", err)
81
return
82
}
83
-
84
-
// build the recipients list
85
-
// - owner of the repo
86
-
// - collaborators in the repo
87
-
// - remove users already mentioned
88
-
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
89
for _, c := range collaborators {
90
-
recipients.Insert(c.SubjectDid)
91
-
}
92
-
for _, m := range mentions {
93
-
recipients.Remove(m)
94
}
95
96
actorDid := syntax.DID(issue.Did)
···
112
)
113
n.notifyEvent(
114
actorDid,
115
-
sets.Collect(slices.Values(mentions)),
116
models.NotificationTypeUserMentioned,
117
entityType,
118
entityId,
···
134
}
135
issue := issues[0]
136
137
-
// built the recipients list:
138
-
// - the owner of the repo
139
-
// - | if the comment is a reply -> everybody on that thread
140
-
// | if the comment is a top level -> just the issue owner
141
-
// - remove mentioned users from the recipients list
142
-
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
143
144
if comment.IsReply() {
145
// if this comment is a reply, then notify everybody in that thread
146
parentAtUri := *comment.ReplyTo
147
148
// find the parent thread, and add all DIDs from here to the recipient list
149
-
for _, t := range issue.CommentList() {
150
if t.Self.AtUri().String() == parentAtUri {
151
-
for _, p := range t.Participants() {
152
-
recipients.Insert(p)
153
-
}
154
}
155
}
156
} else {
157
// not a reply, notify just the issue author
158
-
recipients.Insert(syntax.DID(issue.Did))
159
-
}
160
-
161
-
for _, m := range mentions {
162
-
recipients.Remove(m)
163
}
164
165
actorDid := syntax.DID(comment.Did)
···
181
)
182
n.notifyEvent(
183
actorDid,
184
-
sets.Collect(slices.Values(mentions)),
185
models.NotificationTypeUserMentioned,
186
entityType,
187
entityId,
···
197
198
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
199
actorDid := syntax.DID(follow.UserDid)
200
-
recipients := sets.Singleton(syntax.DID(follow.SubjectDid))
201
eventType := models.NotificationTypeFollowed
202
entityType := "follow"
203
entityId := follow.UserDid
···
225
log.Printf("NewPull: failed to get repos: %v", err)
226
return
227
}
228
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
229
if err != nil {
230
log.Printf("failed to fetch collaborators: %v", err)
231
return
232
}
233
-
234
-
// build the recipients list
235
-
// - owner of the repo
236
-
// - collaborators in the repo
237
-
recipients := sets.Singleton(syntax.DID(repo.Did))
238
for _, c := range collaborators {
239
-
recipients.Insert(c.SubjectDid)
240
}
241
242
actorDid := syntax.DID(pull.OwnerDid)
···
279
// build up the recipients list:
280
// - repo owner
281
// - all pull participants
282
-
// - remove those already mentioned
283
-
recipients := sets.Singleton(syntax.DID(repo.Did))
284
for _, p := range pull.Participants() {
285
-
recipients.Insert(syntax.DID(p))
286
-
}
287
-
for _, m := range mentions {
288
-
recipients.Remove(m)
289
}
290
291
actorDid := syntax.DID(comment.OwnerDid)
···
309
)
310
n.notifyEvent(
311
actorDid,
312
-
sets.Collect(slices.Values(mentions)),
313
models.NotificationTypeUserMentioned,
314
entityType,
315
entityId,
···
336
}
337
338
func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
339
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
340
if err != nil {
341
log.Printf("failed to fetch collaborators: %v", err)
342
return
343
}
344
-
345
-
// build up the recipients list:
346
-
// - repo owner
347
-
// - repo collaborators
348
-
// - all issue participants
349
-
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
350
for _, c := range collaborators {
351
-
recipients.Insert(c.SubjectDid)
352
}
353
for _, p := range issue.Participants() {
354
-
recipients.Insert(syntax.DID(p))
355
}
356
357
entityType := "pull"
···
387
return
388
}
389
390
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
391
if err != nil {
392
log.Printf("failed to fetch collaborators: %v", err)
393
return
394
}
395
-
396
-
// build up the recipients list:
397
-
// - repo owner
398
-
// - all pull participants
399
-
recipients := sets.Singleton(syntax.DID(repo.Did))
400
for _, c := range collaborators {
401
-
recipients.Insert(c.SubjectDid)
402
}
403
for _, p := range pull.Participants() {
404
-
recipients.Insert(syntax.DID(p))
405
}
406
407
entityType := "pull"
···
437
438
func (n *databaseNotifier) notifyEvent(
439
actorDid syntax.DID,
440
-
recipients sets.Set[syntax.DID],
441
eventType models.NotificationType,
442
entityType string,
443
entityId string,
···
445
issueId *int64,
446
pullId *int64,
447
) {
448
-
// if the user is attempting to mention >maxMentions users, this is probably spam, do not mention anybody
449
-
if eventType == models.NotificationTypeUserMentioned && recipients.Len() > maxMentions {
450
-
return
451
}
452
453
-
recipients.Remove(actorDid)
454
-
455
prefMap, err := db.GetNotificationPreferences(
456
n.db,
457
-
orm.FilterIn("user_did", slices.Collect(recipients.All())),
458
)
459
if err != nil {
460
// failed to get prefs for users
···
470
defer tx.Rollback()
471
472
// filter based on preferences
473
-
for recipientDid := range recipients.All() {
474
prefs, ok := prefMap[recipientDid]
475
if !ok {
476
prefs = models.DefaultNotificationPreferences(recipientDid)
···
3
import (
4
"context"
5
"log"
6
+
"maps"
7
"slices"
8
9
"github.com/bluesky-social/indigo/atproto/syntax"
···
13
"tangled.org/core/appview/notify"
14
"tangled.org/core/idresolver"
15
"tangled.org/core/orm"
16
)
17
18
const (
19
+
maxMentions = 5
20
)
21
22
type databaseNotifier struct {
···
50
}
51
52
actorDid := syntax.DID(star.Did)
53
+
recipients := []syntax.DID{syntax.DID(repo.Did)}
54
eventType := models.NotificationTypeRepoStarred
55
entityType := "repo"
56
entityId := star.RepoAt.String()
···
75
}
76
77
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {
78
+
79
+
// build the recipients list
80
+
// - owner of the repo
81
+
// - collaborators in the repo
82
+
var recipients []syntax.DID
83
+
recipients = append(recipients, syntax.DID(issue.Repo.Did))
84
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
85
if err != nil {
86
log.Printf("failed to fetch collaborators: %v", err)
87
return
88
}
89
for _, c := range collaborators {
90
+
recipients = append(recipients, c.SubjectDid)
91
}
92
93
actorDid := syntax.DID(issue.Did)
···
109
)
110
n.notifyEvent(
111
actorDid,
112
+
mentions,
113
models.NotificationTypeUserMentioned,
114
entityType,
115
entityId,
···
131
}
132
issue := issues[0]
133
134
+
var recipients []syntax.DID
135
+
recipients = append(recipients, syntax.DID(issue.Repo.Did))
136
137
if comment.IsReply() {
138
// if this comment is a reply, then notify everybody in that thread
139
parentAtUri := *comment.ReplyTo
140
+
allThreads := issue.CommentList()
141
142
// find the parent thread, and add all DIDs from here to the recipient list
143
+
for _, t := range allThreads {
144
if t.Self.AtUri().String() == parentAtUri {
145
+
recipients = append(recipients, t.Participants()...)
146
}
147
}
148
} else {
149
// not a reply, notify just the issue author
150
+
recipients = append(recipients, syntax.DID(issue.Did))
151
}
152
153
actorDid := syntax.DID(comment.Did)
···
169
)
170
n.notifyEvent(
171
actorDid,
172
+
mentions,
173
models.NotificationTypeUserMentioned,
174
entityType,
175
entityId,
···
185
186
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
187
actorDid := syntax.DID(follow.UserDid)
188
+
recipients := []syntax.DID{syntax.DID(follow.SubjectDid)}
189
eventType := models.NotificationTypeFollowed
190
entityType := "follow"
191
entityId := follow.UserDid
···
213
log.Printf("NewPull: failed to get repos: %v", err)
214
return
215
}
216
+
217
+
// build the recipients list
218
+
// - owner of the repo
219
+
// - collaborators in the repo
220
+
var recipients []syntax.DID
221
+
recipients = append(recipients, syntax.DID(repo.Did))
222
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
223
if err != nil {
224
log.Printf("failed to fetch collaborators: %v", err)
225
return
226
}
227
for _, c := range collaborators {
228
+
recipients = append(recipients, c.SubjectDid)
229
}
230
231
actorDid := syntax.DID(pull.OwnerDid)
···
268
// build up the recipients list:
269
// - repo owner
270
// - all pull participants
271
+
var recipients []syntax.DID
272
+
recipients = append(recipients, syntax.DID(repo.Did))
273
for _, p := range pull.Participants() {
274
+
recipients = append(recipients, syntax.DID(p))
275
}
276
277
actorDid := syntax.DID(comment.OwnerDid)
···
295
)
296
n.notifyEvent(
297
actorDid,
298
+
mentions,
299
models.NotificationTypeUserMentioned,
300
entityType,
301
entityId,
···
322
}
323
324
func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
325
+
// build up the recipients list:
326
+
// - repo owner
327
+
// - repo collaborators
328
+
// - all issue participants
329
+
var recipients []syntax.DID
330
+
recipients = append(recipients, syntax.DID(issue.Repo.Did))
331
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
332
if err != nil {
333
log.Printf("failed to fetch collaborators: %v", err)
334
return
335
}
336
for _, c := range collaborators {
337
+
recipients = append(recipients, c.SubjectDid)
338
}
339
for _, p := range issue.Participants() {
340
+
recipients = append(recipients, syntax.DID(p))
341
}
342
343
entityType := "pull"
···
373
return
374
}
375
376
+
// build up the recipients list:
377
+
// - repo owner
378
+
// - all pull participants
379
+
var recipients []syntax.DID
380
+
recipients = append(recipients, syntax.DID(repo.Did))
381
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
382
if err != nil {
383
log.Printf("failed to fetch collaborators: %v", err)
384
return
385
}
386
for _, c := range collaborators {
387
+
recipients = append(recipients, c.SubjectDid)
388
}
389
for _, p := range pull.Participants() {
390
+
recipients = append(recipients, syntax.DID(p))
391
}
392
393
entityType := "pull"
···
423
424
func (n *databaseNotifier) notifyEvent(
425
actorDid syntax.DID,
426
+
recipients []syntax.DID,
427
eventType models.NotificationType,
428
entityType string,
429
entityId string,
···
431
issueId *int64,
432
pullId *int64,
433
) {
434
+
if eventType == models.NotificationTypeUserMentioned && len(recipients) > maxMentions {
435
+
recipients = recipients[:maxMentions]
436
+
}
437
+
recipientSet := make(map[syntax.DID]struct{})
438
+
for _, did := range recipients {
439
+
// everybody except actor themselves
440
+
if did != actorDid {
441
+
recipientSet[did] = struct{}{}
442
+
}
443
}
444
445
prefMap, err := db.GetNotificationPreferences(
446
n.db,
447
+
orm.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))),
448
)
449
if err != nil {
450
// failed to get prefs for users
···
460
defer tx.Rollback()
461
462
// filter based on preferences
463
+
for recipientDid := range recipientSet {
464
prefs, ok := prefMap[recipientDid]
465
if !ok {
466
prefs = models.DefaultNotificationPreferences(recipientDid)
+6
-9
appview/pages/templates/user/signup.html
+6
-9
appview/pages/templates/user/signup.html
···
43
page to complete your registration.
44
</span>
45
<div class="w-full mt-4 text-center">
46
-
<div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div>
47
</div>
48
<button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" >
49
<span>join now</span>
50
</button>
51
-
<p class="text-sm text-gray-500">
52
-
Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
53
-
</p>
54
55
-
<p id="signup-msg" class="error w-full"></p>
56
-
<p class="text-sm text-gray-500 pt-4">
57
-
By signing up, you agree to our <a href="/terms" class="underline">Terms of Service</a> and <a href="/privacy" class="underline">Privacy Policy</a>.
58
-
</p>
59
-
</form>
60
</main>
61
</body>
62
</html>
···
43
page to complete your registration.
44
</span>
45
<div class="w-full mt-4 text-center">
46
+
<div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div>
47
</div>
48
<button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" >
49
<span>join now</span>
50
</button>
51
+
</form>
52
+
<p class="text-sm text-gray-500">
53
+
Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
54
+
</p>
55
56
+
<p id="signup-msg" class="error w-full"></p>
57
</main>
58
</body>
59
</html>
+17
appview/state/git_http.go
+17
appview/state/git_http.go
···
25
26
}
27
28
+
func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) {
29
+
user, ok := r.Context().Value("resolvedId").(identity.Identity)
30
+
if !ok {
31
+
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
32
+
return
33
+
}
34
+
repo := r.Context().Value("repo").(*models.Repo)
35
+
36
+
scheme := "https"
37
+
if s.config.Core.Dev {
38
+
scheme = "http"
39
+
}
40
+
41
+
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
42
+
s.proxyRequest(w, r, targetURL)
43
+
}
44
+
45
func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
46
user, ok := r.Context().Value("resolvedId").(identity.Identity)
47
if !ok {
+1
appview/state/router.go
+1
appview/state/router.go
+3
-3
flake.lock
+3
-3
flake.lock
+2
flake.nix
+2
flake.nix
···
80
}).buildGoApplication;
81
modules = ./nix/gomod2nix.toml;
82
sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix {
83
inherit sqlite-lib-src;
84
};
85
lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
···
155
nativeBuildInputs = [
156
pkgs.go
157
pkgs.air
158
pkgs.gopls
159
pkgs.httpie
160
pkgs.litecli
···
80
}).buildGoApplication;
81
modules = ./nix/gomod2nix.toml;
82
sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix {
83
+
inherit (pkgs) gcc;
84
inherit sqlite-lib-src;
85
};
86
lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
···
156
nativeBuildInputs = [
157
pkgs.go
158
pkgs.air
159
+
pkgs.tilt
160
pkgs.gopls
161
pkgs.httpie
162
pkgs.litecli
+1
-1
go.mod
+1
-1
go.mod
-81
knotserver/db/db.go
-81
knotserver/db/db.go
···
1
-
package db
2
-
3
-
import (
4
-
"context"
5
-
"database/sql"
6
-
"log/slog"
7
-
"strings"
8
-
9
-
_ "github.com/mattn/go-sqlite3"
10
-
"tangled.org/core/log"
11
-
)
12
-
13
-
type DB struct {
14
-
db *sql.DB
15
-
logger *slog.Logger
16
-
}
17
-
18
-
func Setup(ctx context.Context, dbPath string) (*DB, error) {
19
-
// https://github.com/mattn/go-sqlite3#connection-string
20
-
opts := []string{
21
-
"_foreign_keys=1",
22
-
"_journal_mode=WAL",
23
-
"_synchronous=NORMAL",
24
-
"_auto_vacuum=incremental",
25
-
}
26
-
27
-
logger := log.FromContext(ctx)
28
-
logger = log.SubLogger(logger, "db")
29
-
30
-
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
31
-
if err != nil {
32
-
return nil, err
33
-
}
34
-
35
-
conn, err := db.Conn(ctx)
36
-
if err != nil {
37
-
return nil, err
38
-
}
39
-
defer conn.Close()
40
-
41
-
_, err = conn.ExecContext(ctx, `
42
-
create table if not exists known_dids (
43
-
did text primary key
44
-
);
45
-
46
-
create table if not exists public_keys (
47
-
id integer primary key autoincrement,
48
-
did text not null,
49
-
key text not null,
50
-
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
51
-
unique(did, key),
52
-
foreign key (did) references known_dids(did) on delete cascade
53
-
);
54
-
55
-
create table if not exists _jetstream (
56
-
id integer primary key autoincrement,
57
-
last_time_us integer not null
58
-
);
59
-
60
-
create table if not exists events (
61
-
rkey text not null,
62
-
nsid text not null,
63
-
event text not null, -- json
64
-
created integer not null default (strftime('%s', 'now')),
65
-
primary key (rkey, nsid)
66
-
);
67
-
68
-
create table if not exists migrations (
69
-
id integer primary key autoincrement,
70
-
name text unique
71
-
);
72
-
`)
73
-
if err != nil {
74
-
return nil, err
75
-
}
76
-
77
-
return &DB{
78
-
db: db,
79
-
logger: logger,
80
-
}, nil
81
-
}
···
+64
knotserver/db/init.go
+64
knotserver/db/init.go
···
···
1
+
package db
2
+
3
+
import (
4
+
"database/sql"
5
+
"strings"
6
+
7
+
_ "github.com/mattn/go-sqlite3"
8
+
)
9
+
10
+
type DB struct {
11
+
db *sql.DB
12
+
}
13
+
14
+
func Setup(dbPath string) (*DB, error) {
15
+
// https://github.com/mattn/go-sqlite3#connection-string
16
+
opts := []string{
17
+
"_foreign_keys=1",
18
+
"_journal_mode=WAL",
19
+
"_synchronous=NORMAL",
20
+
"_auto_vacuum=incremental",
21
+
}
22
+
23
+
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
24
+
if err != nil {
25
+
return nil, err
26
+
}
27
+
28
+
// NOTE: If any other migration is added here, you MUST
29
+
// copy the pattern in appview: use a single sql.Conn
30
+
// for every migration.
31
+
32
+
_, err = db.Exec(`
33
+
create table if not exists known_dids (
34
+
did text primary key
35
+
);
36
+
37
+
create table if not exists public_keys (
38
+
id integer primary key autoincrement,
39
+
did text not null,
40
+
key text not null,
41
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
42
+
unique(did, key),
43
+
foreign key (did) references known_dids(did) on delete cascade
44
+
);
45
+
46
+
create table if not exists _jetstream (
47
+
id integer primary key autoincrement,
48
+
last_time_us integer not null
49
+
);
50
+
51
+
create table if not exists events (
52
+
rkey text not null,
53
+
nsid text not null,
54
+
event text not null, -- json
55
+
created integer not null default (strftime('%s', 'now')),
56
+
primary key (rkey, nsid)
57
+
);
58
+
`)
59
+
if err != nil {
60
+
return nil, err
61
+
}
62
+
63
+
return &DB{db: db}, nil
64
+
}
+13
-1
knotserver/git/service/service.go
+13
-1
knotserver/git/service/service.go
···
95
return c.RunService(cmd)
96
}
97
98
+
func (c *ServiceCommand) UploadArchive() error {
99
+
cmd := exec.Command("git", []string{
100
+
"upload-archive",
101
+
".",
102
+
}...)
103
+
104
+
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
105
+
cmd.Env = append(cmd.Env, fmt.Sprintf("GIT_PROTOCOL=%s", c.GitProtocol))
106
+
cmd.Dir = c.Dir
107
+
108
+
return c.RunService(cmd)
109
+
}
110
+
111
func (c *ServiceCommand) UploadPack() error {
112
cmd := exec.Command("git", []string{
113
"upload-pack",
114
"--stateless-rpc",
115
".",
+47
knotserver/git.go
+47
knotserver/git.go
···
56
}
57
}
58
59
+
func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) {
60
+
did := chi.URLParam(r, "did")
61
+
name := chi.URLParam(r, "name")
62
+
repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name))
63
+
if err != nil {
64
+
gitError(w, err.Error(), http.StatusInternalServerError)
65
+
h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err)
66
+
return
67
+
}
68
+
69
+
const expectedContentType = "application/x-git-upload-archive-request"
70
+
contentType := r.Header.Get("Content-Type")
71
+
if contentType != expectedContentType {
72
+
gitError(w, fmt.Sprintf("Expected Content-Type: '%s', but received '%s'.", expectedContentType, contentType), http.StatusUnsupportedMediaType)
73
+
}
74
+
75
+
var bodyReader io.ReadCloser = r.Body
76
+
if r.Header.Get("Content-Encoding") == "gzip" {
77
+
gzipReader, err := gzip.NewReader(r.Body)
78
+
if err != nil {
79
+
gitError(w, err.Error(), http.StatusInternalServerError)
80
+
h.l.Error("git: failed to create gzip reader", "handler", "UploadArchive", "error", err)
81
+
return
82
+
}
83
+
defer gzipReader.Close()
84
+
bodyReader = gzipReader
85
+
}
86
+
87
+
w.Header().Set("Content-Type", "application/x-git-upload-archive-result")
88
+
89
+
h.l.Info("git: executing git-upload-archive", "handler", "UploadArchive", "repo", repo)
90
+
91
+
cmd := service.ServiceCommand{
92
+
GitProtocol: r.Header.Get("Git-Protocol"),
93
+
Dir: repo,
94
+
Stdout: w,
95
+
Stdin: bodyReader,
96
+
}
97
+
98
+
w.WriteHeader(http.StatusOK)
99
+
100
+
if err := cmd.UploadArchive(); err != nil {
101
+
h.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err)
102
+
return
103
+
}
104
+
}
105
+
106
func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) {
107
did := chi.URLParam(r, "did")
108
name := chi.URLParam(r, "name")
+1
knotserver/router.go
+1
knotserver/router.go
+1
-1
knotserver/server.go
+1
-1
knotserver/server.go
+5
-7
nix/pkgs/sqlite-lib.nix
+5
-7
nix/pkgs/sqlite-lib.nix
···
1
{
2
stdenv,
3
sqlite-lib-src,
4
}:
5
stdenv.mkDerivation {
6
name = "sqlite-lib";
7
src = sqlite-lib-src;
8
-
9
buildPhase = ''
10
-
$CC -c sqlite3.c
11
-
$AR rcs libsqlite3.a sqlite3.o
12
-
$RANLIB libsqlite3.a
13
-
'';
14
-
15
-
installPhase = ''
16
mkdir -p $out/include $out/lib
17
cp *.h $out/include
18
cp libsqlite3.a $out/lib
···
1
{
2
+
gcc,
3
stdenv,
4
sqlite-lib-src,
5
}:
6
stdenv.mkDerivation {
7
name = "sqlite-lib";
8
src = sqlite-lib-src;
9
+
nativeBuildInputs = [gcc];
10
buildPhase = ''
11
+
gcc -c sqlite3.c
12
+
ar rcs libsqlite3.a sqlite3.o
13
+
ranlib libsqlite3.a
14
mkdir -p $out/include $out/lib
15
cp *.h $out/include
16
cp libsqlite3.a $out/lib
-31
sets/gen.go
-31
sets/gen.go
···
1
-
package sets
2
-
3
-
import (
4
-
"math/rand"
5
-
"reflect"
6
-
"testing/quick"
7
-
)
8
-
9
-
func (_ Set[T]) Generate(rand *rand.Rand, size int) reflect.Value {
10
-
s := New[T]()
11
-
12
-
var zero T
13
-
itemType := reflect.TypeOf(zero)
14
-
15
-
for {
16
-
if s.Len() >= size {
17
-
break
18
-
}
19
-
20
-
item, ok := quick.Value(itemType, rand)
21
-
if !ok {
22
-
continue
23
-
}
24
-
25
-
if val, ok := item.Interface().(T); ok {
26
-
s.Insert(val)
27
-
}
28
-
}
29
-
30
-
return reflect.ValueOf(s)
31
-
}
···
-35
sets/readme.txt
-35
sets/readme.txt
···
1
-
sets
2
-
----
3
-
set datastructure for go with generics and iterators. the
4
-
api is supposed to mimic rust's std::collections::HashSet api.
5
-
6
-
s1 := sets.Collect(slices.Values([]int{1, 2, 3, 4}))
7
-
s2 := sets.Collect(slices.Values([]int{1, 2, 3, 4, 5, 6}))
8
-
9
-
union := sets.Collect(s1.Union(s2))
10
-
intersect := sets.Collect(s1.Intersection(s2))
11
-
diff := sets.Collect(s1.Difference(s2))
12
-
symdiff := sets.Collect(s1.SymmetricDifference(s2))
13
-
14
-
s1.Len() // 4
15
-
s1.Contains(1) // true
16
-
s1.IsEmpty() // false
17
-
s1.IsSubset(s2) // true
18
-
s1.IsSuperset(s2) // false
19
-
s1.IsDisjoint(s2) // false
20
-
21
-
if exists := s1.Insert(1); exists {
22
-
// already existed in set
23
-
}
24
-
25
-
if existed := s1.Remove(1); existed {
26
-
// existed in set, now removed
27
-
}
28
-
29
-
30
-
testing
31
-
-------
32
-
includes property-based tests using the wonderful
33
-
testing/quick module!
34
-
35
-
go test -v
···
-174
sets/set.go
-174
sets/set.go
···
1
-
package sets
2
-
3
-
import (
4
-
"iter"
5
-
"maps"
6
-
)
7
-
8
-
type Set[T comparable] struct {
9
-
data map[T]struct{}
10
-
}
11
-
12
-
func New[T comparable]() Set[T] {
13
-
return Set[T]{
14
-
data: make(map[T]struct{}),
15
-
}
16
-
}
17
-
18
-
func (s *Set[T]) Insert(item T) bool {
19
-
_, exists := s.data[item]
20
-
s.data[item] = struct{}{}
21
-
return !exists
22
-
}
23
-
24
-
func Singleton[T comparable](item T) Set[T] {
25
-
n := New[T]()
26
-
_ = n.Insert(item)
27
-
return n
28
-
}
29
-
30
-
func (s *Set[T]) Remove(item T) bool {
31
-
_, exists := s.data[item]
32
-
if exists {
33
-
delete(s.data, item)
34
-
}
35
-
return exists
36
-
}
37
-
38
-
func (s Set[T]) Contains(item T) bool {
39
-
_, exists := s.data[item]
40
-
return exists
41
-
}
42
-
43
-
func (s Set[T]) Len() int {
44
-
return len(s.data)
45
-
}
46
-
47
-
func (s Set[T]) IsEmpty() bool {
48
-
return len(s.data) == 0
49
-
}
50
-
51
-
func (s *Set[T]) Clear() {
52
-
s.data = make(map[T]struct{})
53
-
}
54
-
55
-
func (s Set[T]) All() iter.Seq[T] {
56
-
return func(yield func(T) bool) {
57
-
for item := range s.data {
58
-
if !yield(item) {
59
-
return
60
-
}
61
-
}
62
-
}
63
-
}
64
-
65
-
func (s Set[T]) Clone() Set[T] {
66
-
return Set[T]{
67
-
data: maps.Clone(s.data),
68
-
}
69
-
}
70
-
71
-
func (s Set[T]) Union(other Set[T]) iter.Seq[T] {
72
-
if s.Len() >= other.Len() {
73
-
return chain(s.All(), other.Difference(s))
74
-
} else {
75
-
return chain(other.All(), s.Difference(other))
76
-
}
77
-
}
78
-
79
-
func chain[T any](seqs ...iter.Seq[T]) iter.Seq[T] {
80
-
return func(yield func(T) bool) {
81
-
for _, seq := range seqs {
82
-
for item := range seq {
83
-
if !yield(item) {
84
-
return
85
-
}
86
-
}
87
-
}
88
-
}
89
-
}
90
-
91
-
func (s Set[T]) Intersection(other Set[T]) iter.Seq[T] {
92
-
return func(yield func(T) bool) {
93
-
for item := range s.data {
94
-
if other.Contains(item) {
95
-
if !yield(item) {
96
-
return
97
-
}
98
-
}
99
-
}
100
-
}
101
-
}
102
-
103
-
func (s Set[T]) Difference(other Set[T]) iter.Seq[T] {
104
-
return func(yield func(T) bool) {
105
-
for item := range s.data {
106
-
if !other.Contains(item) {
107
-
if !yield(item) {
108
-
return
109
-
}
110
-
}
111
-
}
112
-
}
113
-
}
114
-
115
-
func (s Set[T]) SymmetricDifference(other Set[T]) iter.Seq[T] {
116
-
return func(yield func(T) bool) {
117
-
for item := range s.data {
118
-
if !other.Contains(item) {
119
-
if !yield(item) {
120
-
return
121
-
}
122
-
}
123
-
}
124
-
for item := range other.data {
125
-
if !s.Contains(item) {
126
-
if !yield(item) {
127
-
return
128
-
}
129
-
}
130
-
}
131
-
}
132
-
}
133
-
134
-
func (s Set[T]) IsSubset(other Set[T]) bool {
135
-
for item := range s.data {
136
-
if !other.Contains(item) {
137
-
return false
138
-
}
139
-
}
140
-
return true
141
-
}
142
-
143
-
func (s Set[T]) IsSuperset(other Set[T]) bool {
144
-
return other.IsSubset(s)
145
-
}
146
-
147
-
func (s Set[T]) IsDisjoint(other Set[T]) bool {
148
-
for item := range s.data {
149
-
if other.Contains(item) {
150
-
return false
151
-
}
152
-
}
153
-
return true
154
-
}
155
-
156
-
func (s Set[T]) Equal(other Set[T]) bool {
157
-
if s.Len() != other.Len() {
158
-
return false
159
-
}
160
-
for item := range s.data {
161
-
if !other.Contains(item) {
162
-
return false
163
-
}
164
-
}
165
-
return true
166
-
}
167
-
168
-
func Collect[T comparable](seq iter.Seq[T]) Set[T] {
169
-
result := New[T]()
170
-
for item := range seq {
171
-
result.Insert(item)
172
-
}
173
-
return result
174
-
}
···
-411
sets/set_test.go
-411
sets/set_test.go
···
1
-
package sets
2
-
3
-
import (
4
-
"slices"
5
-
"testing"
6
-
"testing/quick"
7
-
)
8
-
9
-
func TestNew(t *testing.T) {
10
-
s := New[int]()
11
-
if s.Len() != 0 {
12
-
t.Errorf("New set should be empty, got length %d", s.Len())
13
-
}
14
-
if !s.IsEmpty() {
15
-
t.Error("New set should be empty")
16
-
}
17
-
}
18
-
19
-
func TestFromSlice(t *testing.T) {
20
-
s := Collect(slices.Values([]int{1, 2, 3, 2, 1}))
21
-
if s.Len() != 3 {
22
-
t.Errorf("Expected length 3, got %d", s.Len())
23
-
}
24
-
if !s.Contains(1) || !s.Contains(2) || !s.Contains(3) {
25
-
t.Error("Set should contain all unique elements from slice")
26
-
}
27
-
}
28
-
29
-
func TestInsert(t *testing.T) {
30
-
s := New[string]()
31
-
32
-
if !s.Insert("hello") {
33
-
t.Error("First insert should return true")
34
-
}
35
-
if s.Insert("hello") {
36
-
t.Error("Duplicate insert should return false")
37
-
}
38
-
if s.Len() != 1 {
39
-
t.Errorf("Expected length 1, got %d", s.Len())
40
-
}
41
-
}
42
-
43
-
func TestRemove(t *testing.T) {
44
-
s := Collect(slices.Values([]int{1, 2, 3}))
45
-
46
-
if !s.Remove(2) {
47
-
t.Error("Remove existing element should return true")
48
-
}
49
-
if s.Remove(2) {
50
-
t.Error("Remove non-existing element should return false")
51
-
}
52
-
if s.Contains(2) {
53
-
t.Error("Element should be removed")
54
-
}
55
-
if s.Len() != 2 {
56
-
t.Errorf("Expected length 2, got %d", s.Len())
57
-
}
58
-
}
59
-
60
-
func TestContains(t *testing.T) {
61
-
s := Collect(slices.Values([]int{1, 2, 3}))
62
-
63
-
if !s.Contains(1) {
64
-
t.Error("Should contain 1")
65
-
}
66
-
if s.Contains(4) {
67
-
t.Error("Should not contain 4")
68
-
}
69
-
}
70
-
71
-
func TestClear(t *testing.T) {
72
-
s := Collect(slices.Values([]int{1, 2, 3}))
73
-
s.Clear()
74
-
75
-
if !s.IsEmpty() {
76
-
t.Error("Set should be empty after clear")
77
-
}
78
-
if s.Len() != 0 {
79
-
t.Errorf("Expected length 0, got %d", s.Len())
80
-
}
81
-
}
82
-
83
-
func TestIterator(t *testing.T) {
84
-
s := Collect(slices.Values([]int{1, 2, 3}))
85
-
var items []int
86
-
87
-
for item := range s.All() {
88
-
items = append(items, item)
89
-
}
90
-
91
-
slices.Sort(items)
92
-
expected := []int{1, 2, 3}
93
-
if !slices.Equal(items, expected) {
94
-
t.Errorf("Expected %v, got %v", expected, items)
95
-
}
96
-
}
97
-
98
-
func TestClone(t *testing.T) {
99
-
s1 := Collect(slices.Values([]int{1, 2, 3}))
100
-
s2 := s1.Clone()
101
-
102
-
if !s1.Equal(s2) {
103
-
t.Error("Cloned set should be equal to original")
104
-
}
105
-
106
-
s2.Insert(4)
107
-
if s1.Contains(4) {
108
-
t.Error("Modifying clone should not affect original")
109
-
}
110
-
}
111
-
112
-
func TestUnion(t *testing.T) {
113
-
s1 := Collect(slices.Values([]int{1, 2}))
114
-
s2 := Collect(slices.Values([]int{2, 3}))
115
-
116
-
result := Collect(s1.Union(s2))
117
-
expected := Collect(slices.Values([]int{1, 2, 3}))
118
-
119
-
if !result.Equal(expected) {
120
-
t.Errorf("Expected %v, got %v", expected, result)
121
-
}
122
-
}
123
-
124
-
func TestIntersection(t *testing.T) {
125
-
s1 := Collect(slices.Values([]int{1, 2, 3}))
126
-
s2 := Collect(slices.Values([]int{2, 3, 4}))
127
-
128
-
expected := Collect(slices.Values([]int{2, 3}))
129
-
result := Collect(s1.Intersection(s2))
130
-
131
-
if !result.Equal(expected) {
132
-
t.Errorf("Expected %v, got %v", expected, result)
133
-
}
134
-
}
135
-
136
-
func TestDifference(t *testing.T) {
137
-
s1 := Collect(slices.Values([]int{1, 2, 3}))
138
-
s2 := Collect(slices.Values([]int{2, 3, 4}))
139
-
140
-
expected := Collect(slices.Values([]int{1}))
141
-
result := Collect(s1.Difference(s2))
142
-
143
-
if !result.Equal(expected) {
144
-
t.Errorf("Expected %v, got %v", expected, result)
145
-
}
146
-
}
147
-
148
-
func TestSymmetricDifference(t *testing.T) {
149
-
s1 := Collect(slices.Values([]int{1, 2, 3}))
150
-
s2 := Collect(slices.Values([]int{2, 3, 4}))
151
-
152
-
expected := Collect(slices.Values([]int{1, 4}))
153
-
result := Collect(s1.SymmetricDifference(s2))
154
-
155
-
if !result.Equal(expected) {
156
-
t.Errorf("Expected %v, got %v", expected, result)
157
-
}
158
-
}
159
-
160
-
func TestSymmetricDifferenceCommutativeProperty(t *testing.T) {
161
-
s1 := Collect(slices.Values([]int{1, 2, 3}))
162
-
s2 := Collect(slices.Values([]int{2, 3, 4}))
163
-
164
-
result1 := Collect(s1.SymmetricDifference(s2))
165
-
result2 := Collect(s2.SymmetricDifference(s1))
166
-
167
-
if !result1.Equal(result2) {
168
-
t.Errorf("Expected %v, got %v", result1, result2)
169
-
}
170
-
}
171
-
172
-
func TestIsSubset(t *testing.T) {
173
-
s1 := Collect(slices.Values([]int{1, 2}))
174
-
s2 := Collect(slices.Values([]int{1, 2, 3}))
175
-
176
-
if !s1.IsSubset(s2) {
177
-
t.Error("s1 should be subset of s2")
178
-
}
179
-
if s2.IsSubset(s1) {
180
-
t.Error("s2 should not be subset of s1")
181
-
}
182
-
}
183
-
184
-
func TestIsSuperset(t *testing.T) {
185
-
s1 := Collect(slices.Values([]int{1, 2, 3}))
186
-
s2 := Collect(slices.Values([]int{1, 2}))
187
-
188
-
if !s1.IsSuperset(s2) {
189
-
t.Error("s1 should be superset of s2")
190
-
}
191
-
if s2.IsSuperset(s1) {
192
-
t.Error("s2 should not be superset of s1")
193
-
}
194
-
}
195
-
196
-
func TestIsDisjoint(t *testing.T) {
197
-
s1 := Collect(slices.Values([]int{1, 2}))
198
-
s2 := Collect(slices.Values([]int{3, 4}))
199
-
s3 := Collect(slices.Values([]int{2, 3}))
200
-
201
-
if !s1.IsDisjoint(s2) {
202
-
t.Error("s1 and s2 should be disjoint")
203
-
}
204
-
if s1.IsDisjoint(s3) {
205
-
t.Error("s1 and s3 should not be disjoint")
206
-
}
207
-
}
208
-
209
-
func TestEqual(t *testing.T) {
210
-
s1 := Collect(slices.Values([]int{1, 2, 3}))
211
-
s2 := Collect(slices.Values([]int{3, 2, 1}))
212
-
s3 := Collect(slices.Values([]int{1, 2}))
213
-
214
-
if !s1.Equal(s2) {
215
-
t.Error("s1 and s2 should be equal")
216
-
}
217
-
if s1.Equal(s3) {
218
-
t.Error("s1 and s3 should not be equal")
219
-
}
220
-
}
221
-
222
-
func TestCollect(t *testing.T) {
223
-
s1 := Collect(slices.Values([]int{1, 2}))
224
-
s2 := Collect(slices.Values([]int{2, 3}))
225
-
226
-
unionSet := Collect(s1.Union(s2))
227
-
if unionSet.Len() != 3 {
228
-
t.Errorf("Expected union set length 3, got %d", unionSet.Len())
229
-
}
230
-
if !unionSet.Contains(1) || !unionSet.Contains(2) || !unionSet.Contains(3) {
231
-
t.Error("Union set should contain 1, 2, and 3")
232
-
}
233
-
234
-
diffSet := Collect(s1.Difference(s2))
235
-
if diffSet.Len() != 1 {
236
-
t.Errorf("Expected difference set length 1, got %d", diffSet.Len())
237
-
}
238
-
if !diffSet.Contains(1) {
239
-
t.Error("Difference set should contain 1")
240
-
}
241
-
}
242
-
243
-
func TestPropertySingleonLen(t *testing.T) {
244
-
f := func(item int) bool {
245
-
single := Singleton(item)
246
-
return single.Len() == 1
247
-
}
248
-
249
-
if err := quick.Check(f, nil); err != nil {
250
-
t.Error(err)
251
-
}
252
-
}
253
-
254
-
func TestPropertyInsertIdempotent(t *testing.T) {
255
-
f := func(s Set[int], item int) bool {
256
-
clone := s.Clone()
257
-
258
-
clone.Insert(item)
259
-
firstLen := clone.Len()
260
-
261
-
clone.Insert(item)
262
-
secondLen := clone.Len()
263
-
264
-
return firstLen == secondLen
265
-
}
266
-
267
-
if err := quick.Check(f, nil); err != nil {
268
-
t.Error(err)
269
-
}
270
-
}
271
-
272
-
func TestPropertyUnionCommutative(t *testing.T) {
273
-
f := func(s1 Set[int], s2 Set[int]) bool {
274
-
union1 := Collect(s1.Union(s2))
275
-
union2 := Collect(s2.Union(s1))
276
-
return union1.Equal(union2)
277
-
}
278
-
279
-
if err := quick.Check(f, nil); err != nil {
280
-
t.Error(err)
281
-
}
282
-
}
283
-
284
-
func TestPropertyIntersectionCommutative(t *testing.T) {
285
-
f := func(s1 Set[int], s2 Set[int]) bool {
286
-
inter1 := Collect(s1.Intersection(s2))
287
-
inter2 := Collect(s2.Intersection(s1))
288
-
return inter1.Equal(inter2)
289
-
}
290
-
291
-
if err := quick.Check(f, nil); err != nil {
292
-
t.Error(err)
293
-
}
294
-
}
295
-
296
-
func TestPropertyCloneEquals(t *testing.T) {
297
-
f := func(s Set[int]) bool {
298
-
clone := s.Clone()
299
-
return s.Equal(clone)
300
-
}
301
-
302
-
if err := quick.Check(f, nil); err != nil {
303
-
t.Error(err)
304
-
}
305
-
}
306
-
307
-
func TestPropertyIntersectionIsSubset(t *testing.T) {
308
-
f := func(s1 Set[int], s2 Set[int]) bool {
309
-
inter := Collect(s1.Intersection(s2))
310
-
return inter.IsSubset(s1) && inter.IsSubset(s2)
311
-
}
312
-
313
-
if err := quick.Check(f, nil); err != nil {
314
-
t.Error(err)
315
-
}
316
-
}
317
-
318
-
func TestPropertyUnionIsSuperset(t *testing.T) {
319
-
f := func(s1 Set[int], s2 Set[int]) bool {
320
-
union := Collect(s1.Union(s2))
321
-
return union.IsSuperset(s1) && union.IsSuperset(s2)
322
-
}
323
-
324
-
if err := quick.Check(f, nil); err != nil {
325
-
t.Error(err)
326
-
}
327
-
}
328
-
329
-
func TestPropertyDifferenceDisjoint(t *testing.T) {
330
-
f := func(s1 Set[int], s2 Set[int]) bool {
331
-
diff := Collect(s1.Difference(s2))
332
-
return diff.IsDisjoint(s2)
333
-
}
334
-
335
-
if err := quick.Check(f, nil); err != nil {
336
-
t.Error(err)
337
-
}
338
-
}
339
-
340
-
func TestPropertySymmetricDifferenceCommutative(t *testing.T) {
341
-
f := func(s1 Set[int], s2 Set[int]) bool {
342
-
symDiff1 := Collect(s1.SymmetricDifference(s2))
343
-
symDiff2 := Collect(s2.SymmetricDifference(s1))
344
-
return symDiff1.Equal(symDiff2)
345
-
}
346
-
347
-
if err := quick.Check(f, nil); err != nil {
348
-
t.Error(err)
349
-
}
350
-
}
351
-
352
-
func TestPropertyRemoveWorks(t *testing.T) {
353
-
f := func(s Set[int], item int) bool {
354
-
clone := s.Clone()
355
-
clone.Insert(item)
356
-
clone.Remove(item)
357
-
return !clone.Contains(item)
358
-
}
359
-
360
-
if err := quick.Check(f, nil); err != nil {
361
-
t.Error(err)
362
-
}
363
-
}
364
-
365
-
func TestPropertyClearEmpty(t *testing.T) {
366
-
f := func(s Set[int]) bool {
367
-
s.Clear()
368
-
return s.IsEmpty() && s.Len() == 0
369
-
}
370
-
371
-
if err := quick.Check(f, nil); err != nil {
372
-
t.Error(err)
373
-
}
374
-
}
375
-
376
-
func TestPropertyIsSubsetReflexive(t *testing.T) {
377
-
f := func(s Set[int]) bool {
378
-
return s.IsSubset(s)
379
-
}
380
-
381
-
if err := quick.Check(f, nil); err != nil {
382
-
t.Error(err)
383
-
}
384
-
}
385
-
386
-
func TestPropertyDeMorganUnion(t *testing.T) {
387
-
f := func(s1 Set[int], s2 Set[int], universe Set[int]) bool {
388
-
// create a universe that contains both sets
389
-
u := universe.Clone()
390
-
for item := range s1.All() {
391
-
u.Insert(item)
392
-
}
393
-
for item := range s2.All() {
394
-
u.Insert(item)
395
-
}
396
-
397
-
// (A u B)' = A' n B'
398
-
union := Collect(s1.Union(s2))
399
-
complementUnion := Collect(u.Difference(union))
400
-
401
-
complementS1 := Collect(u.Difference(s1))
402
-
complementS2 := Collect(u.Difference(s2))
403
-
intersectionComplements := Collect(complementS1.Intersection(complementS2))
404
-
405
-
return complementUnion.Equal(intersectionComplements)
406
-
}
407
-
408
-
if err := quick.Check(f, nil); err != nil {
409
-
t.Error(err)
410
-
}
411
-
}
···
+1
-6
types/commit.go
+1
-6
types/commit.go
···
174
175
func (commit Commit) CoAuthors() []object.Signature {
176
var coAuthors []object.Signature
177
-
seen := make(map[string]bool)
178
matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1)
179
180
for _, match := range matches {
181
if len(match) >= 3 {
182
name := strings.TrimSpace(match[1])
183
email := strings.TrimSpace(match[2])
184
-
185
-
if seen[email] {
186
-
continue
187
-
}
188
-
seen[email] = true
189
190
coAuthors = append(coAuthors, object.Signature{
191
Name: name,
···
174
175
func (commit Commit) CoAuthors() []object.Signature {
176
var coAuthors []object.Signature
177
+
178
matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1)
179
180
for _, match := range matches {
181
if len(match) >= 3 {
182
name := strings.TrimSpace(match[1])
183
email := strings.TrimSpace(match[2])
184
185
coAuthors = append(coAuthors, object.Signature{
186
Name: name,