+6
-45
appview/commitverify/verify.go
+6
-45
appview/commitverify/verify.go
···
3
import (
4
"log"
5
6
-
"github.com/go-git/go-git/v5/plumbing/object"
7
"tangled.org/core/appview/db"
8
"tangled.org/core/appview/models"
9
"tangled.org/core/crypto"
···
35
return ""
36
}
37
38
-
func GetVerifiedObjectCommits(e db.Execer, emailToDid map[string]string, commits []*object.Commit) (VerifiedCommits, error) {
39
-
ndCommits := []types.NiceDiff{}
40
-
for _, commit := range commits {
41
-
ndCommits = append(ndCommits, ObjectCommitToNiceDiff(commit))
42
-
}
43
-
return GetVerifiedCommits(e, emailToDid, ndCommits)
44
-
}
45
-
46
-
func GetVerifiedCommits(e db.Execer, emailToDid map[string]string, ndCommits []types.NiceDiff) (VerifiedCommits, error) {
47
vcs := VerifiedCommits{}
48
49
didPubkeyCache := make(map[string][]models.PublicKey)
50
51
for _, commit := range ndCommits {
52
-
c := commit.Commit
53
-
54
-
committerEmail := c.Committer.Email
55
if did, exists := emailToDid[committerEmail]; exists {
56
// check if we've already fetched public keys for this did
57
pubKeys, ok := didPubkeyCache[did]
···
67
}
68
69
// try to verify with any associated pubkeys
70
for _, pk := range pubKeys {
71
-
if _, ok := crypto.VerifyCommitSignature(pk.Key, commit); ok {
72
73
fp, err := crypto.SSHFingerprint(pk.Key)
74
if err != nil {
75
log.Println("error computing ssh fingerprint:", err)
76
}
77
78
-
vc := verifiedCommit{fingerprint: fp, hash: c.This}
79
vcs[vc] = struct{}{}
80
break
81
}
···
86
87
return vcs, nil
88
}
89
-
90
-
// ObjectCommitToNiceDiff is a compatibility function to convert a
91
-
// commit object into a NiceDiff structure.
92
-
func ObjectCommitToNiceDiff(c *object.Commit) types.NiceDiff {
93
-
var niceDiff types.NiceDiff
94
-
95
-
// set commit information
96
-
niceDiff.Commit.Message = c.Message
97
-
niceDiff.Commit.Author = c.Author
98
-
niceDiff.Commit.This = c.Hash.String()
99
-
niceDiff.Commit.Committer = c.Committer
100
-
niceDiff.Commit.Tree = c.TreeHash.String()
101
-
niceDiff.Commit.PGPSignature = c.PGPSignature
102
-
103
-
changeId, ok := c.ExtraHeaders["change-id"]
104
-
if ok {
105
-
niceDiff.Commit.ChangedId = string(changeId)
106
-
}
107
-
108
-
// set parent hash if available
109
-
if len(c.ParentHashes) > 0 {
110
-
niceDiff.Commit.Parent = c.ParentHashes[0].String()
111
-
}
112
-
113
-
// XXX: Stats and Diff fields are typically populated
114
-
// after fetching the actual diff information, which isn't
115
-
// directly available in the commit object itself.
116
-
117
-
return niceDiff
118
-
}
···
3
import (
4
"log"
5
6
"tangled.org/core/appview/db"
7
"tangled.org/core/appview/models"
8
"tangled.org/core/crypto"
···
34
return ""
35
}
36
37
+
func GetVerifiedCommits(e db.Execer, emailToDid map[string]string, ndCommits []types.Commit) (VerifiedCommits, error) {
38
vcs := VerifiedCommits{}
39
40
didPubkeyCache := make(map[string][]models.PublicKey)
41
42
for _, commit := range ndCommits {
43
+
committerEmail := commit.Committer.Email
44
if did, exists := emailToDid[committerEmail]; exists {
45
// check if we've already fetched public keys for this did
46
pubKeys, ok := didPubkeyCache[did]
···
56
}
57
58
// try to verify with any associated pubkeys
59
+
payload := commit.Payload()
60
+
signature := commit.PGPSignature
61
for _, pk := range pubKeys {
62
+
if _, ok := crypto.VerifySignature([]byte(pk.Key), []byte(signature), []byte(payload)); ok {
63
64
fp, err := crypto.SSHFingerprint(pk.Key)
65
if err != nil {
66
log.Println("error computing ssh fingerprint:", err)
67
}
68
69
+
vc := verifiedCommit{fingerprint: fp, hash: commit.This}
70
vcs[vc] = struct{}{}
71
break
72
}
···
77
78
return vcs, nil
79
}
+3
-2
appview/db/artifact.go
+3
-2
appview/db/artifact.go
···
8
"github.com/go-git/go-git/v5/plumbing"
9
"github.com/ipfs/go-cid"
10
"tangled.org/core/appview/models"
11
)
12
13
func AddArtifact(e Execer, artifact models.Artifact) error {
···
37
return err
38
}
39
40
-
func GetArtifact(e Execer, filters ...filter) ([]models.Artifact, error) {
41
var artifacts []models.Artifact
42
43
var conditions []string
···
109
return artifacts, nil
110
}
111
112
-
func DeleteArtifact(e Execer, filters ...filter) error {
113
var conditions []string
114
var args []any
115
for _, filter := range filters {
···
8
"github.com/go-git/go-git/v5/plumbing"
9
"github.com/ipfs/go-cid"
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
)
13
14
func AddArtifact(e Execer, artifact models.Artifact) error {
···
38
return err
39
}
40
41
+
func GetArtifact(e Execer, filters ...orm.Filter) ([]models.Artifact, error) {
42
var artifacts []models.Artifact
43
44
var conditions []string
···
110
return artifacts, nil
111
}
112
113
+
func DeleteArtifact(e Execer, filters ...orm.Filter) error {
114
var conditions []string
115
var args []any
116
for _, filter := range filters {
+4
-3
appview/db/collaborators.go
+4
-3
appview/db/collaborators.go
···
6
"time"
7
8
"tangled.org/core/appview/models"
9
)
10
11
func AddCollaborator(e Execer, c models.Collaborator) error {
···
16
return err
17
}
18
19
-
func DeleteCollaborator(e Execer, filters ...filter) error {
20
var conditions []string
21
var args []any
22
for _, filter := range filters {
···
58
return nil, nil
59
}
60
61
-
return GetRepos(e, 0, FilterIn("at_uri", repoAts))
62
}
63
64
-
func GetCollaborators(e Execer, filters ...filter) ([]models.Collaborator, error) {
65
var collaborators []models.Collaborator
66
var conditions []string
67
var args []any
···
6
"time"
7
8
"tangled.org/core/appview/models"
9
+
"tangled.org/core/orm"
10
)
11
12
func AddCollaborator(e Execer, c models.Collaborator) error {
···
17
return err
18
}
19
20
+
func DeleteCollaborator(e Execer, filters ...orm.Filter) error {
21
var conditions []string
22
var args []any
23
for _, filter := range filters {
···
59
return nil, nil
60
}
61
62
+
return GetRepos(e, 0, orm.FilterIn("at_uri", repoAts))
63
}
64
65
+
func GetCollaborators(e Execer, filters ...orm.Filter) ([]models.Collaborator, error) {
66
var collaborators []models.Collaborator
67
var conditions []string
68
var args []any
+24
-137
appview/db/db.go
+24
-137
appview/db/db.go
···
3
import (
4
"context"
5
"database/sql"
6
-
"fmt"
7
"log/slog"
8
-
"reflect"
9
"strings"
10
11
_ "github.com/mattn/go-sqlite3"
12
"tangled.org/core/log"
13
)
14
15
type DB struct {
···
584
}
585
586
// run migrations
587
-
runMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
588
tx.Exec(`
589
alter table repos add column description text check (length(description) <= 200);
590
`)
591
return nil
592
})
593
594
-
runMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
595
// add unconstrained column
596
_, err := tx.Exec(`
597
alter table public_keys
···
614
return nil
615
})
616
617
-
runMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
618
_, err := tx.Exec(`
619
alter table comments drop column comment_at;
620
alter table comments add column rkey text;
···
622
return err
623
})
624
625
-
runMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
626
_, err := tx.Exec(`
627
alter table comments add column deleted text; -- timestamp
628
alter table comments add column edited text; -- timestamp
···
630
return err
631
})
632
633
-
runMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
634
_, err := tx.Exec(`
635
alter table pulls add column source_branch text;
636
alter table pulls add column source_repo_at text;
···
639
return err
640
})
641
642
-
runMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
643
_, err := tx.Exec(`
644
alter table repos add column source text;
645
`)
···
651
//
652
// [0]: https://sqlite.org/pragma.html#pragma_foreign_keys
653
conn.ExecContext(ctx, "pragma foreign_keys = off;")
654
-
runMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
655
_, err := tx.Exec(`
656
create table pulls_new (
657
-- identifiers
···
708
})
709
conn.ExecContext(ctx, "pragma foreign_keys = on;")
710
711
-
runMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
712
tx.Exec(`
713
alter table repos add column spindle text;
714
`)
···
718
// drop all knot secrets, add unique constraint to knots
719
//
720
// knots will henceforth use service auth for signed requests
721
-
runMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
722
_, err := tx.Exec(`
723
create table registrations_new (
724
id integer primary key autoincrement,
···
741
})
742
743
// recreate and add rkey + created columns with default constraint
744
-
runMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
745
// create new table
746
// - repo_at instead of repo integer
747
// - rkey field
···
795
return err
796
})
797
798
-
runMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
799
_, err := tx.Exec(`
800
alter table issues add column rkey text not null default '';
801
···
807
})
808
809
// repurpose the read-only column to "needs-upgrade"
810
-
runMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
811
_, err := tx.Exec(`
812
alter table registrations rename column read_only to needs_upgrade;
813
`)
···
815
})
816
817
// require all knots to upgrade after the release of total xrpc
818
-
runMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
819
_, err := tx.Exec(`
820
update registrations set needs_upgrade = 1;
821
`)
···
823
})
824
825
// require all knots to upgrade after the release of total xrpc
826
-
runMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
827
_, err := tx.Exec(`
828
alter table spindles add column needs_upgrade integer not null default 0;
829
`)
···
841
//
842
// disable foreign-keys for the next migration
843
conn.ExecContext(ctx, "pragma foreign_keys = off;")
844
-
runMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
845
_, err := tx.Exec(`
846
create table if not exists issues_new (
847
-- identifiers
···
911
// - new columns
912
// * column "reply_to" which can be any other comment
913
// * column "at-uri" which is a generated column
914
-
runMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
915
_, err := tx.Exec(`
916
create table if not exists issue_comments (
917
-- identifiers
···
971
//
972
// disable foreign-keys for the next migration
973
conn.ExecContext(ctx, "pragma foreign_keys = off;")
974
-
runMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
975
_, err := tx.Exec(`
976
create table if not exists pulls_new (
977
-- identifiers
···
1052
//
1053
// disable foreign-keys for the next migration
1054
conn.ExecContext(ctx, "pragma foreign_keys = off;")
1055
-
runMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
1056
_, err := tx.Exec(`
1057
create table if not exists pull_submissions_new (
1058
-- identifiers
···
1106
1107
// knots may report the combined patch for a comparison, we can store that on the appview side
1108
// (but not on the pds record), because calculating the combined patch requires a git index
1109
-
runMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
1110
_, err := tx.Exec(`
1111
alter table pull_submissions add column combined text;
1112
`)
1113
return err
1114
})
1115
1116
-
runMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1117
_, err := tx.Exec(`
1118
alter table profile add column pronouns text;
1119
`)
1120
return err
1121
})
1122
1123
-
runMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
1124
_, err := tx.Exec(`
1125
alter table repos add column website text;
1126
alter table repos add column topics text;
···
1128
return err
1129
})
1130
1131
-
runMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
1132
_, err := tx.Exec(`
1133
alter table notification_preferences add column user_mentioned integer not null default 1;
1134
`)
···
1136
})
1137
1138
// remove the foreign key constraints from stars.
1139
-
runMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
1140
_, err := tx.Exec(`
1141
create table stars_new (
1142
id integer primary key autoincrement,
···
1180
}, nil
1181
}
1182
1183
-
type migrationFn = func(*sql.Tx) error
1184
-
1185
-
func runMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
1186
-
logger = logger.With("migration", name)
1187
-
1188
-
tx, err := c.BeginTx(context.Background(), nil)
1189
-
if err != nil {
1190
-
return err
1191
-
}
1192
-
defer tx.Rollback()
1193
-
1194
-
var exists bool
1195
-
err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
1196
-
if err != nil {
1197
-
return err
1198
-
}
1199
-
1200
-
if !exists {
1201
-
// run migration
1202
-
err = migrationFn(tx)
1203
-
if err != nil {
1204
-
logger.Error("failed to run migration", "err", err)
1205
-
return err
1206
-
}
1207
-
1208
-
// mark migration as complete
1209
-
_, err = tx.Exec("insert into migrations (name) values (?)", name)
1210
-
if err != nil {
1211
-
logger.Error("failed to mark migration as complete", "err", err)
1212
-
return err
1213
-
}
1214
-
1215
-
// commit the transaction
1216
-
if err := tx.Commit(); err != nil {
1217
-
return err
1218
-
}
1219
-
1220
-
logger.Info("migration applied successfully")
1221
-
} else {
1222
-
logger.Warn("skipped migration, already applied")
1223
-
}
1224
-
1225
-
return nil
1226
-
}
1227
-
1228
func (d *DB) Close() error {
1229
return d.DB.Close()
1230
}
1231
-
1232
-
type filter struct {
1233
-
key string
1234
-
arg any
1235
-
cmp string
1236
-
}
1237
-
1238
-
func newFilter(key, cmp string, arg any) filter {
1239
-
return filter{
1240
-
key: key,
1241
-
arg: arg,
1242
-
cmp: cmp,
1243
-
}
1244
-
}
1245
-
1246
-
func FilterEq(key string, arg any) filter { return newFilter(key, "=", arg) }
1247
-
func FilterNotEq(key string, arg any) filter { return newFilter(key, "<>", arg) }
1248
-
func FilterGte(key string, arg any) filter { return newFilter(key, ">=", arg) }
1249
-
func FilterLte(key string, arg any) filter { return newFilter(key, "<=", arg) }
1250
-
func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) }
1251
-
func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) }
1252
-
func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) }
1253
-
func FilterLike(key string, arg any) filter { return newFilter(key, "like", arg) }
1254
-
func FilterNotLike(key string, arg any) filter { return newFilter(key, "not like", arg) }
1255
-
func FilterContains(key string, arg any) filter {
1256
-
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
1257
-
}
1258
-
1259
-
func (f filter) Condition() string {
1260
-
rv := reflect.ValueOf(f.arg)
1261
-
kind := rv.Kind()
1262
-
1263
-
// if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
1264
-
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
1265
-
if rv.Len() == 0 {
1266
-
// always false
1267
-
return "1 = 0"
1268
-
}
1269
-
1270
-
placeholders := make([]string, rv.Len())
1271
-
for i := range placeholders {
1272
-
placeholders[i] = "?"
1273
-
}
1274
-
1275
-
return fmt.Sprintf("%s %s (%s)", f.key, f.cmp, strings.Join(placeholders, ", "))
1276
-
}
1277
-
1278
-
return fmt.Sprintf("%s %s ?", f.key, f.cmp)
1279
-
}
1280
-
1281
-
func (f filter) Arg() []any {
1282
-
rv := reflect.ValueOf(f.arg)
1283
-
kind := rv.Kind()
1284
-
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
1285
-
if rv.Len() == 0 {
1286
-
return nil
1287
-
}
1288
-
1289
-
out := make([]any, rv.Len())
1290
-
for i := range rv.Len() {
1291
-
out[i] = rv.Index(i).Interface()
1292
-
}
1293
-
return out
1294
-
}
1295
-
1296
-
return []any{f.arg}
1297
-
}
···
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
+
"tangled.org/core/orm"
12
)
13
14
type DB struct {
···
583
}
584
585
// run migrations
586
+
orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
587
tx.Exec(`
588
alter table repos add column description text check (length(description) <= 200);
589
`)
590
return nil
591
})
592
593
+
orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
594
// add unconstrained column
595
_, err := tx.Exec(`
596
alter table public_keys
···
613
return nil
614
})
615
616
+
orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
617
_, err := tx.Exec(`
618
alter table comments drop column comment_at;
619
alter table comments add column rkey text;
···
621
return err
622
})
623
624
+
orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
625
_, err := tx.Exec(`
626
alter table comments add column deleted text; -- timestamp
627
alter table comments add column edited text; -- timestamp
···
629
return err
630
})
631
632
+
orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
633
_, err := tx.Exec(`
634
alter table pulls add column source_branch text;
635
alter table pulls add column source_repo_at text;
···
638
return err
639
})
640
641
+
orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
642
_, err := tx.Exec(`
643
alter table repos add column source text;
644
`)
···
650
//
651
// [0]: https://sqlite.org/pragma.html#pragma_foreign_keys
652
conn.ExecContext(ctx, "pragma foreign_keys = off;")
653
+
orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
654
_, err := tx.Exec(`
655
create table pulls_new (
656
-- identifiers
···
707
})
708
conn.ExecContext(ctx, "pragma foreign_keys = on;")
709
710
+
orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
711
tx.Exec(`
712
alter table repos add column spindle text;
713
`)
···
717
// drop all knot secrets, add unique constraint to knots
718
//
719
// knots will henceforth use service auth for signed requests
720
+
orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
721
_, err := tx.Exec(`
722
create table registrations_new (
723
id integer primary key autoincrement,
···
740
})
741
742
// recreate and add rkey + created columns with default constraint
743
+
orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
744
// create new table
745
// - repo_at instead of repo integer
746
// - rkey field
···
794
return err
795
})
796
797
+
orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
798
_, err := tx.Exec(`
799
alter table issues add column rkey text not null default '';
800
···
806
})
807
808
// repurpose the read-only column to "needs-upgrade"
809
+
orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
810
_, err := tx.Exec(`
811
alter table registrations rename column read_only to needs_upgrade;
812
`)
···
814
})
815
816
// require all knots to upgrade after the release of total xrpc
817
+
orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
818
_, err := tx.Exec(`
819
update registrations set needs_upgrade = 1;
820
`)
···
822
})
823
824
// require all knots to upgrade after the release of total xrpc
825
+
orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
826
_, err := tx.Exec(`
827
alter table spindles add column needs_upgrade integer not null default 0;
828
`)
···
840
//
841
// disable foreign-keys for the next migration
842
conn.ExecContext(ctx, "pragma foreign_keys = off;")
843
+
orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
844
_, err := tx.Exec(`
845
create table if not exists issues_new (
846
-- identifiers
···
910
// - new columns
911
// * column "reply_to" which can be any other comment
912
// * column "at-uri" which is a generated column
913
+
orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
914
_, err := tx.Exec(`
915
create table if not exists issue_comments (
916
-- identifiers
···
970
//
971
// disable foreign-keys for the next migration
972
conn.ExecContext(ctx, "pragma foreign_keys = off;")
973
+
orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
974
_, err := tx.Exec(`
975
create table if not exists pulls_new (
976
-- identifiers
···
1051
//
1052
// disable foreign-keys for the next migration
1053
conn.ExecContext(ctx, "pragma foreign_keys = off;")
1054
+
orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
1055
_, err := tx.Exec(`
1056
create table if not exists pull_submissions_new (
1057
-- identifiers
···
1105
1106
// knots may report the combined patch for a comparison, we can store that on the appview side
1107
// (but not on the pds record), because calculating the combined patch requires a git index
1108
+
orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
1109
_, err := tx.Exec(`
1110
alter table pull_submissions add column combined text;
1111
`)
1112
return err
1113
})
1114
1115
+
orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1116
_, err := tx.Exec(`
1117
alter table profile add column pronouns text;
1118
`)
1119
return err
1120
})
1121
1122
+
orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
1123
_, err := tx.Exec(`
1124
alter table repos add column website text;
1125
alter table repos add column topics text;
···
1127
return err
1128
})
1129
1130
+
orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
1131
_, err := tx.Exec(`
1132
alter table notification_preferences add column user_mentioned integer not null default 1;
1133
`)
···
1135
})
1136
1137
// remove the foreign key constraints from stars.
1138
+
orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
1139
_, err := tx.Exec(`
1140
create table stars_new (
1141
id integer primary key autoincrement,
···
1179
}, nil
1180
}
1181
1182
func (d *DB) Close() error {
1183
return d.DB.Close()
1184
}
+6
-3
appview/db/follow.go
+6
-3
appview/db/follow.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
func AddFollow(e Execer, follow *models.Follow) error {
···
134
return result, nil
135
}
136
137
-
func GetFollows(e Execer, limit int, filters ...filter) ([]models.Follow, error) {
138
var follows []models.Follow
139
140
var conditions []string
···
166
if err != nil {
167
return nil, err
168
}
169
for rows.Next() {
170
var follow models.Follow
171
var followedAt string
···
191
}
192
193
func GetFollowers(e Execer, did string) ([]models.Follow, error) {
194
-
return GetFollows(e, 0, FilterEq("subject_did", did))
195
}
196
197
func GetFollowing(e Execer, did string) ([]models.Follow, error) {
198
-
return GetFollows(e, 0, FilterEq("user_did", did))
199
}
200
201
func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
func AddFollow(e Execer, follow *models.Follow) error {
···
135
return result, nil
136
}
137
138
+
func GetFollows(e Execer, limit int, filters ...orm.Filter) ([]models.Follow, error) {
139
var follows []models.Follow
140
141
var conditions []string
···
167
if err != nil {
168
return nil, err
169
}
170
+
defer rows.Close()
171
+
172
for rows.Next() {
173
var follow models.Follow
174
var followedAt string
···
194
}
195
196
func GetFollowers(e Execer, did string) ([]models.Follow, error) {
197
+
return GetFollows(e, 0, orm.FilterEq("subject_did", did))
198
}
199
200
func GetFollowing(e Execer, did string) ([]models.Follow, error) {
201
+
return GetFollows(e, 0, orm.FilterEq("user_did", did))
202
}
203
204
func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
+22
-20
appview/db/issues.go
+22
-20
appview/db/issues.go
···
13
"tangled.org/core/api/tangled"
14
"tangled.org/core/appview/models"
15
"tangled.org/core/appview/pagination"
16
)
17
18
func PutIssue(tx *sql.Tx, issue *models.Issue) error {
···
27
28
issues, err := GetIssues(
29
tx,
30
-
FilterEq("did", issue.Did),
31
-
FilterEq("rkey", issue.Rkey),
32
)
33
switch {
34
case err != nil:
···
98
return nil
99
}
100
101
-
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) {
102
issueMap := make(map[string]*models.Issue) // at-uri -> issue
103
104
var conditions []string
···
114
whereClause = " where " + strings.Join(conditions, " and ")
115
}
116
117
-
pLower := FilterGte("row_num", page.Offset+1)
118
-
pUpper := FilterLte("row_num", page.Offset+page.Limit)
119
120
pageClause := ""
121
if page.Limit > 0 {
···
205
repoAts = append(repoAts, string(issue.RepoAt))
206
}
207
208
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoAts))
209
if err != nil {
210
return nil, fmt.Errorf("failed to build repo mappings: %w", err)
211
}
···
228
// collect comments
229
issueAts := slices.Collect(maps.Keys(issueMap))
230
231
-
comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts))
232
if err != nil {
233
return nil, fmt.Errorf("failed to query comments: %w", err)
234
}
···
240
}
241
242
// collect allLabels for each issue
243
-
allLabels, err := GetLabels(e, FilterIn("subject", issueAts))
244
if err != nil {
245
return nil, fmt.Errorf("failed to query labels: %w", err)
246
}
···
251
}
252
253
// collect references for each issue
254
-
allReferencs, err := GetReferencesAll(e, FilterIn("from_at", issueAts))
255
if err != nil {
256
return nil, fmt.Errorf("failed to query reference_links: %w", err)
257
}
···
277
issues, err := GetIssuesPaginated(
278
e,
279
pagination.Page{},
280
-
FilterEq("repo_at", repoAt),
281
-
FilterEq("issue_id", issueId),
282
)
283
if err != nil {
284
return nil, err
···
290
return &issues[0], nil
291
}
292
293
-
func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) {
294
return GetIssuesPaginated(e, pagination.Page{}, filters...)
295
}
296
···
298
func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) {
299
var ids []int64
300
301
-
var filters []filter
302
openValue := 0
303
if opts.IsOpen {
304
openValue = 1
305
}
306
-
filters = append(filters, FilterEq("open", openValue))
307
if opts.RepoAt != "" {
308
-
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
309
}
310
311
var conditions []string
···
397
return id, nil
398
}
399
400
-
func DeleteIssueComments(e Execer, filters ...filter) error {
401
var conditions []string
402
var args []any
403
for _, filter := range filters {
···
416
return err
417
}
418
419
-
func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) {
420
commentMap := make(map[string]*models.IssueComment)
421
422
var conditions []string
···
451
if err != nil {
452
return nil, err
453
}
454
455
for rows.Next() {
456
var comment models.IssueComment
···
506
507
// collect references for each comments
508
commentAts := slices.Collect(maps.Keys(commentMap))
509
-
allReferencs, err := GetReferencesAll(e, FilterIn("from_at", commentAts))
510
if err != nil {
511
return nil, fmt.Errorf("failed to query reference_links: %w", err)
512
}
···
548
return nil
549
}
550
551
-
func CloseIssues(e Execer, filters ...filter) error {
552
var conditions []string
553
var args []any
554
for _, filter := range filters {
···
566
return err
567
}
568
569
-
func ReopenIssues(e Execer, filters ...filter) error {
570
var conditions []string
571
var args []any
572
for _, filter := range filters {
···
13
"tangled.org/core/api/tangled"
14
"tangled.org/core/appview/models"
15
"tangled.org/core/appview/pagination"
16
+
"tangled.org/core/orm"
17
)
18
19
func PutIssue(tx *sql.Tx, issue *models.Issue) error {
···
28
29
issues, err := GetIssues(
30
tx,
31
+
orm.FilterEq("did", issue.Did),
32
+
orm.FilterEq("rkey", issue.Rkey),
33
)
34
switch {
35
case err != nil:
···
99
return nil
100
}
101
102
+
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) {
103
issueMap := make(map[string]*models.Issue) // at-uri -> issue
104
105
var conditions []string
···
115
whereClause = " where " + strings.Join(conditions, " and ")
116
}
117
118
+
pLower := orm.FilterGte("row_num", page.Offset+1)
119
+
pUpper := orm.FilterLte("row_num", page.Offset+page.Limit)
120
121
pageClause := ""
122
if page.Limit > 0 {
···
206
repoAts = append(repoAts, string(issue.RepoAt))
207
}
208
209
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoAts))
210
if err != nil {
211
return nil, fmt.Errorf("failed to build repo mappings: %w", err)
212
}
···
229
// collect comments
230
issueAts := slices.Collect(maps.Keys(issueMap))
231
232
+
comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts))
233
if err != nil {
234
return nil, fmt.Errorf("failed to query comments: %w", err)
235
}
···
241
}
242
243
// collect allLabels for each issue
244
+
allLabels, err := GetLabels(e, orm.FilterIn("subject", issueAts))
245
if err != nil {
246
return nil, fmt.Errorf("failed to query labels: %w", err)
247
}
···
252
}
253
254
// collect references for each issue
255
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", issueAts))
256
if err != nil {
257
return nil, fmt.Errorf("failed to query reference_links: %w", err)
258
}
···
278
issues, err := GetIssuesPaginated(
279
e,
280
pagination.Page{},
281
+
orm.FilterEq("repo_at", repoAt),
282
+
orm.FilterEq("issue_id", issueId),
283
)
284
if err != nil {
285
return nil, err
···
291
return &issues[0], nil
292
}
293
294
+
func GetIssues(e Execer, filters ...orm.Filter) ([]models.Issue, error) {
295
return GetIssuesPaginated(e, pagination.Page{}, filters...)
296
}
297
···
299
func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) {
300
var ids []int64
301
302
+
var filters []orm.Filter
303
openValue := 0
304
if opts.IsOpen {
305
openValue = 1
306
}
307
+
filters = append(filters, orm.FilterEq("open", openValue))
308
if opts.RepoAt != "" {
309
+
filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
310
}
311
312
var conditions []string
···
398
return id, nil
399
}
400
401
+
func DeleteIssueComments(e Execer, filters ...orm.Filter) error {
402
var conditions []string
403
var args []any
404
for _, filter := range filters {
···
417
return err
418
}
419
420
+
func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) {
421
commentMap := make(map[string]*models.IssueComment)
422
423
var conditions []string
···
452
if err != nil {
453
return nil, err
454
}
455
+
defer rows.Close()
456
457
for rows.Next() {
458
var comment models.IssueComment
···
508
509
// collect references for each comments
510
commentAts := slices.Collect(maps.Keys(commentMap))
511
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
512
if err != nil {
513
return nil, fmt.Errorf("failed to query reference_links: %w", err)
514
}
···
550
return nil
551
}
552
553
+
func CloseIssues(e Execer, filters ...orm.Filter) error {
554
var conditions []string
555
var args []any
556
for _, filter := range filters {
···
568
return err
569
}
570
571
+
func ReopenIssues(e Execer, filters ...orm.Filter) error {
572
var conditions []string
573
var args []any
574
for _, filter := range filters {
+8
-7
appview/db/label.go
+8
-7
appview/db/label.go
···
10
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
)
14
15
// no updating type for now
···
59
return id, nil
60
}
61
62
-
func DeleteLabelDefinition(e Execer, filters ...filter) error {
63
var conditions []string
64
var args []any
65
for _, filter := range filters {
···
75
return err
76
}
77
78
-
func GetLabelDefinitions(e Execer, filters ...filter) ([]models.LabelDefinition, error) {
79
var labelDefinitions []models.LabelDefinition
80
var conditions []string
81
var args []any
···
167
}
168
169
// helper to get exactly one label def
170
-
func GetLabelDefinition(e Execer, filters ...filter) (*models.LabelDefinition, error) {
171
labels, err := GetLabelDefinitions(e, filters...)
172
if err != nil {
173
return nil, err
···
227
return id, nil
228
}
229
230
-
func GetLabelOps(e Execer, filters ...filter) ([]models.LabelOp, error) {
231
var labelOps []models.LabelOp
232
var conditions []string
233
var args []any
···
302
}
303
304
// get labels for a given list of subject URIs
305
-
func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]models.LabelState, error) {
306
ops, err := GetLabelOps(e, filters...)
307
if err != nil {
308
return nil, err
···
322
}
323
labelAts := slices.Collect(maps.Keys(labelAtSet))
324
325
-
actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts))
326
if err != nil {
327
return nil, err
328
}
···
338
return results, nil
339
}
340
341
-
func NewLabelApplicationCtx(e Execer, filters ...filter) (*models.LabelApplicationCtx, error) {
342
labels, err := GetLabelDefinitions(e, filters...)
343
if err != nil {
344
return nil, err
···
10
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
+
"tangled.org/core/orm"
14
)
15
16
// no updating type for now
···
60
return id, nil
61
}
62
63
+
func DeleteLabelDefinition(e Execer, filters ...orm.Filter) error {
64
var conditions []string
65
var args []any
66
for _, filter := range filters {
···
76
return err
77
}
78
79
+
func GetLabelDefinitions(e Execer, filters ...orm.Filter) ([]models.LabelDefinition, error) {
80
var labelDefinitions []models.LabelDefinition
81
var conditions []string
82
var args []any
···
168
}
169
170
// helper to get exactly one label def
171
+
func GetLabelDefinition(e Execer, filters ...orm.Filter) (*models.LabelDefinition, error) {
172
labels, err := GetLabelDefinitions(e, filters...)
173
if err != nil {
174
return nil, err
···
228
return id, nil
229
}
230
231
+
func GetLabelOps(e Execer, filters ...orm.Filter) ([]models.LabelOp, error) {
232
var labelOps []models.LabelOp
233
var conditions []string
234
var args []any
···
303
}
304
305
// get labels for a given list of subject URIs
306
+
func GetLabels(e Execer, filters ...orm.Filter) (map[syntax.ATURI]models.LabelState, error) {
307
ops, err := GetLabelOps(e, filters...)
308
if err != nil {
309
return nil, err
···
323
}
324
labelAts := slices.Collect(maps.Keys(labelAtSet))
325
326
+
actx, err := NewLabelApplicationCtx(e, orm.FilterIn("at_uri", labelAts))
327
if err != nil {
328
return nil, err
329
}
···
339
return results, nil
340
}
341
342
+
func NewLabelApplicationCtx(e Execer, filters ...orm.Filter) (*models.LabelApplicationCtx, error) {
343
labels, err := GetLabelDefinitions(e, filters...)
344
if err != nil {
345
return nil, err
+6
-5
appview/db/language.go
+6
-5
appview/db/language.go
···
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetRepoLanguages(e Execer, filters ...filter) ([]models.RepoLanguage, error) {
13
var conditions []string
14
var args []any
15
for _, filter := range filters {
···
27
whereClause,
28
)
29
rows, err := e.Query(query, args...)
30
-
31
if err != nil {
32
return nil, fmt.Errorf("failed to execute query: %w ", err)
33
}
34
35
var langs []models.RepoLanguage
36
for rows.Next() {
···
85
return nil
86
}
87
88
-
func DeleteRepoLanguages(e Execer, filters ...filter) error {
89
var conditions []string
90
var args []any
91
for _, filter := range filters {
···
107
func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error {
108
err := DeleteRepoLanguages(
109
tx,
110
-
FilterEq("repo_at", repoAt),
111
-
FilterEq("ref", ref),
112
)
113
if err != nil {
114
return fmt.Errorf("failed to delete existing languages: %w", err)
···
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
+
func GetRepoLanguages(e Execer, filters ...orm.Filter) ([]models.RepoLanguage, error) {
14
var conditions []string
15
var args []any
16
for _, filter := range filters {
···
28
whereClause,
29
)
30
rows, err := e.Query(query, args...)
31
if err != nil {
32
return nil, fmt.Errorf("failed to execute query: %w ", err)
33
}
34
+
defer rows.Close()
35
36
var langs []models.RepoLanguage
37
for rows.Next() {
···
86
return nil
87
}
88
89
+
func DeleteRepoLanguages(e Execer, filters ...orm.Filter) error {
90
var conditions []string
91
var args []any
92
for _, filter := range filters {
···
108
func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error {
109
err := DeleteRepoLanguages(
110
tx,
111
+
orm.FilterEq("repo_at", repoAt),
112
+
orm.FilterEq("ref", ref),
113
)
114
if err != nil {
115
return fmt.Errorf("failed to delete existing languages: %w", err)
+14
-13
appview/db/notifications.go
+14
-13
appview/db/notifications.go
···
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
)
15
16
func CreateNotification(e Execer, notification *models.Notification) error {
···
44
}
45
46
// GetNotificationsPaginated retrieves notifications with filters and pagination
47
-
func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...filter) ([]*models.Notification, error) {
48
var conditions []string
49
var args []any
50
···
113
}
114
115
// GetNotificationsWithEntities retrieves notifications with their related entities
116
-
func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...filter) ([]*models.NotificationWithEntity, error) {
117
var conditions []string
118
var args []any
119
···
256
}
257
258
// GetNotifications retrieves notifications with filters
259
-
func GetNotifications(e Execer, filters ...filter) ([]*models.Notification, error) {
260
return GetNotificationsPaginated(e, pagination.FirstPage(), filters...)
261
}
262
263
-
func CountNotifications(e Execer, filters ...filter) (int64, error) {
264
var conditions []string
265
var args []any
266
for _, filter := range filters {
···
285
}
286
287
func MarkNotificationRead(e Execer, notificationID int64, userDID string) error {
288
-
idFilter := FilterEq("id", notificationID)
289
-
recipientFilter := FilterEq("recipient_did", userDID)
290
291
query := fmt.Sprintf(`
292
UPDATE notifications
···
314
}
315
316
func MarkAllNotificationsRead(e Execer, userDID string) error {
317
-
recipientFilter := FilterEq("recipient_did", userDID)
318
-
readFilter := FilterEq("read", 0)
319
320
query := fmt.Sprintf(`
321
UPDATE notifications
···
334
}
335
336
func DeleteNotification(e Execer, notificationID int64, userDID string) error {
337
-
idFilter := FilterEq("id", notificationID)
338
-
recipientFilter := FilterEq("recipient_did", userDID)
339
340
query := fmt.Sprintf(`
341
DELETE FROM notifications
···
362
}
363
364
func GetNotificationPreference(e Execer, userDid string) (*models.NotificationPreferences, error) {
365
-
prefs, err := GetNotificationPreferences(e, FilterEq("user_did", userDid))
366
if err != nil {
367
return nil, err
368
}
···
375
return p, nil
376
}
377
378
-
func GetNotificationPreferences(e Execer, filters ...filter) (map[syntax.DID]*models.NotificationPreferences, error) {
379
prefsMap := make(map[syntax.DID]*models.NotificationPreferences)
380
381
var conditions []string
···
483
484
func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error {
485
cutoff := time.Now().Add(-olderThan)
486
-
createdFilter := FilterLte("created", cutoff)
487
488
query := fmt.Sprintf(`
489
DELETE FROM notifications
···
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
+
"tangled.org/core/orm"
15
)
16
17
func CreateNotification(e Execer, notification *models.Notification) error {
···
45
}
46
47
// GetNotificationsPaginated retrieves notifications with filters and pagination
48
+
func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.Notification, error) {
49
var conditions []string
50
var args []any
51
···
114
}
115
116
// GetNotificationsWithEntities retrieves notifications with their related entities
117
+
func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.NotificationWithEntity, error) {
118
var conditions []string
119
var args []any
120
···
257
}
258
259
// GetNotifications retrieves notifications with filters
260
+
func GetNotifications(e Execer, filters ...orm.Filter) ([]*models.Notification, error) {
261
return GetNotificationsPaginated(e, pagination.FirstPage(), filters...)
262
}
263
264
+
func CountNotifications(e Execer, filters ...orm.Filter) (int64, error) {
265
var conditions []string
266
var args []any
267
for _, filter := range filters {
···
286
}
287
288
func MarkNotificationRead(e Execer, notificationID int64, userDID string) error {
289
+
idFilter := orm.FilterEq("id", notificationID)
290
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
291
292
query := fmt.Sprintf(`
293
UPDATE notifications
···
315
}
316
317
func MarkAllNotificationsRead(e Execer, userDID string) error {
318
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
319
+
readFilter := orm.FilterEq("read", 0)
320
321
query := fmt.Sprintf(`
322
UPDATE notifications
···
335
}
336
337
func DeleteNotification(e Execer, notificationID int64, userDID string) error {
338
+
idFilter := orm.FilterEq("id", notificationID)
339
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
340
341
query := fmt.Sprintf(`
342
DELETE FROM notifications
···
363
}
364
365
func GetNotificationPreference(e Execer, userDid string) (*models.NotificationPreferences, error) {
366
+
prefs, err := GetNotificationPreferences(e, orm.FilterEq("user_did", userDid))
367
if err != nil {
368
return nil, err
369
}
···
376
return p, nil
377
}
378
379
+
func GetNotificationPreferences(e Execer, filters ...orm.Filter) (map[syntax.DID]*models.NotificationPreferences, error) {
380
prefsMap := make(map[syntax.DID]*models.NotificationPreferences)
381
382
var conditions []string
···
484
485
func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error {
486
cutoff := time.Now().Add(-olderThan)
487
+
createdFilter := orm.FilterLte("created", cutoff)
488
489
query := fmt.Sprintf(`
490
DELETE FROM notifications
+6
-5
appview/db/pipeline.go
+6
-5
appview/db/pipeline.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetPipelines(e Execer, filters ...filter) ([]models.Pipeline, error) {
13
var pipelines []models.Pipeline
14
15
var conditions []string
···
168
169
// this is a mega query, but the most useful one:
170
// get N pipelines, for each one get the latest status of its N workflows
171
-
func GetPipelineStatuses(e Execer, limit int, filters ...filter) ([]models.Pipeline, error) {
172
var conditions []string
173
var args []any
174
for _, filter := range filters {
175
-
filter.key = "p." + filter.key // the table is aliased in the query to `p`
176
conditions = append(conditions, filter.Condition())
177
args = append(args, filter.Arg()...)
178
}
···
264
conditions = nil
265
args = nil
266
for _, p := range pipelines {
267
-
knotFilter := FilterEq("pipeline_knot", p.Knot)
268
-
rkeyFilter := FilterEq("pipeline_rkey", p.Rkey)
269
conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
270
args = append(args, p.Knot)
271
args = append(args, p.Rkey)
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
+
func GetPipelines(e Execer, filters ...orm.Filter) ([]models.Pipeline, error) {
14
var pipelines []models.Pipeline
15
16
var conditions []string
···
169
170
// this is a mega query, but the most useful one:
171
// get N pipelines, for each one get the latest status of its N workflows
172
+
func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.Pipeline, error) {
173
var conditions []string
174
var args []any
175
for _, filter := range filters {
176
+
filter.Key = "p." + filter.Key // the table is aliased in the query to `p`
177
conditions = append(conditions, filter.Condition())
178
args = append(args, filter.Arg()...)
179
}
···
265
conditions = nil
266
args = nil
267
for _, p := range pipelines {
268
+
knotFilter := orm.FilterEq("pipeline_knot", p.Knot)
269
+
rkeyFilter := orm.FilterEq("pipeline_rkey", p.Rkey)
270
conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
271
args = append(args, p.Knot)
272
args = append(args, p.Rkey)
+11
-5
appview/db/profile.go
+11
-5
appview/db/profile.go
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
)
15
16
const TimeframeMonths = 7
···
44
45
issues, err := GetIssues(
46
e,
47
-
FilterEq("did", forDid),
48
-
FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
49
)
50
if err != nil {
51
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···
65
*items = append(*items, &issue)
66
}
67
68
-
repos, err := GetRepos(e, 0, FilterEq("did", forDid))
69
if err != nil {
70
return nil, fmt.Errorf("error getting all repos by did: %w", err)
71
}
···
199
return tx.Commit()
200
}
201
202
-
func GetProfiles(e Execer, filters ...filter) (map[string]*models.Profile, error) {
203
var conditions []string
204
var args []any
205
for _, filter := range filters {
···
229
if err != nil {
230
return nil, err
231
}
232
233
profileMap := make(map[string]*models.Profile)
234
for rows.Next() {
···
269
if err != nil {
270
return nil, err
271
}
272
idxs := make(map[string]int)
273
for did := range profileMap {
274
idxs[did] = 0
···
289
if err != nil {
290
return nil, err
291
}
292
idxs = make(map[string]int)
293
for did := range profileMap {
294
idxs[did] = 0
···
441
}
442
443
// ensure all pinned repos are either own repos or collaborating repos
444
-
repos, err := GetRepos(e, 0, FilterEq("did", profile.Did))
445
if err != nil {
446
log.Printf("getting repos for %s: %s", profile.Did, err)
447
}
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
15
)
16
17
const TimeframeMonths = 7
···
45
46
issues, err := GetIssues(
47
e,
48
+
orm.FilterEq("did", forDid),
49
+
orm.FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
50
)
51
if err != nil {
52
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···
66
*items = append(*items, &issue)
67
}
68
69
+
repos, err := GetRepos(e, 0, orm.FilterEq("did", forDid))
70
if err != nil {
71
return nil, fmt.Errorf("error getting all repos by did: %w", err)
72
}
···
200
return tx.Commit()
201
}
202
203
+
func GetProfiles(e Execer, filters ...orm.Filter) (map[string]*models.Profile, error) {
204
var conditions []string
205
var args []any
206
for _, filter := range filters {
···
230
if err != nil {
231
return nil, err
232
}
233
+
defer rows.Close()
234
235
profileMap := make(map[string]*models.Profile)
236
for rows.Next() {
···
271
if err != nil {
272
return nil, err
273
}
274
+
defer rows.Close()
275
+
276
idxs := make(map[string]int)
277
for did := range profileMap {
278
idxs[did] = 0
···
293
if err != nil {
294
return nil, err
295
}
296
+
defer rows.Close()
297
+
298
idxs = make(map[string]int)
299
for did := range profileMap {
300
idxs[did] = 0
···
447
}
448
449
// ensure all pinned repos are either own repos or collaborating repos
450
+
repos, err := GetRepos(e, 0, orm.FilterEq("did", profile.Did))
451
if err != nil {
452
log.Printf("getting repos for %s: %s", profile.Did, err)
453
}
+21
-20
appview/db/pulls.go
+21
-20
appview/db/pulls.go
···
13
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
"tangled.org/core/appview/models"
16
)
17
18
func NewPull(tx *sql.Tx, pull *models.Pull) error {
···
118
return pullId - 1, err
119
}
120
121
-
func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*models.Pull, error) {
122
pulls := make(map[syntax.ATURI]*models.Pull)
123
124
var conditions []string
···
229
for _, p := range pulls {
230
pullAts = append(pullAts, p.AtUri())
231
}
232
-
submissionsMap, err := GetPullSubmissions(e, FilterIn("pull_at", pullAts))
233
if err != nil {
234
return nil, fmt.Errorf("failed to get submissions: %w", err)
235
}
···
241
}
242
243
// collect allLabels for each issue
244
-
allLabels, err := GetLabels(e, FilterIn("subject", pullAts))
245
if err != nil {
246
return nil, fmt.Errorf("failed to query labels: %w", err)
247
}
···
258
sourceAts = append(sourceAts, *p.PullSource.RepoAt)
259
}
260
}
261
-
sourceRepos, err := GetRepos(e, 0, FilterIn("at_uri", sourceAts))
262
if err != nil && !errors.Is(err, sql.ErrNoRows) {
263
return nil, fmt.Errorf("failed to get source repos: %w", err)
264
}
···
274
}
275
}
276
277
-
allReferences, err := GetReferencesAll(e, FilterIn("from_at", pullAts))
278
if err != nil {
279
return nil, fmt.Errorf("failed to query reference_links: %w", err)
280
}
···
295
return orderedByPullId, nil
296
}
297
298
-
func GetPulls(e Execer, filters ...filter) ([]*models.Pull, error) {
299
return GetPullsWithLimit(e, 0, filters...)
300
}
301
302
func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
303
var ids []int64
304
305
-
var filters []filter
306
-
filters = append(filters, FilterEq("state", opts.State))
307
if opts.RepoAt != "" {
308
-
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
309
}
310
311
var conditions []string
···
361
}
362
363
func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
364
-
pulls, err := GetPullsWithLimit(e, 1, FilterEq("repo_at", repoAt), FilterEq("pull_id", pullId))
365
if err != nil {
366
return nil, err
367
}
···
373
}
374
375
// mapping from pull -> pull submissions
376
-
func GetPullSubmissions(e Execer, filters ...filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
377
var conditions []string
378
var args []any
379
for _, filter := range filters {
···
448
449
// Get comments for all submissions using GetPullComments
450
submissionIds := slices.Collect(maps.Keys(submissionMap))
451
-
comments, err := GetPullComments(e, FilterIn("submission_id", submissionIds))
452
if err != nil {
453
return nil, fmt.Errorf("failed to get pull comments: %w", err)
454
}
···
474
return m, nil
475
}
476
477
-
func GetPullComments(e Execer, filters ...filter) ([]models.PullComment, error) {
478
var conditions []string
479
var args []any
480
for _, filter := range filters {
···
542
543
// collect references for each comments
544
commentAts := slices.Collect(maps.Keys(commentMap))
545
-
allReferencs, err := GetReferencesAll(e, FilterIn("from_at", commentAts))
546
if err != nil {
547
return nil, fmt.Errorf("failed to query reference_links: %w", err)
548
}
···
708
return err
709
}
710
711
-
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...filter) error {
712
var conditions []string
713
var args []any
714
···
732
733
// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
734
// otherwise submissions are immutable
735
-
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...filter) error {
736
var conditions []string
737
var args []any
738
···
790
func GetStack(e Execer, stackId string) (models.Stack, error) {
791
unorderedPulls, err := GetPulls(
792
e,
793
-
FilterEq("stack_id", stackId),
794
-
FilterNotEq("state", models.PullDeleted),
795
)
796
if err != nil {
797
return nil, err
···
835
func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
836
pulls, err := GetPulls(
837
e,
838
-
FilterEq("stack_id", stackId),
839
-
FilterEq("state", models.PullDeleted),
840
)
841
if err != nil {
842
return nil, err
···
13
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
"tangled.org/core/appview/models"
16
+
"tangled.org/core/orm"
17
)
18
19
func NewPull(tx *sql.Tx, pull *models.Pull) error {
···
119
return pullId - 1, err
120
}
121
122
+
func GetPullsWithLimit(e Execer, limit int, filters ...orm.Filter) ([]*models.Pull, error) {
123
pulls := make(map[syntax.ATURI]*models.Pull)
124
125
var conditions []string
···
230
for _, p := range pulls {
231
pullAts = append(pullAts, p.AtUri())
232
}
233
+
submissionsMap, err := GetPullSubmissions(e, orm.FilterIn("pull_at", pullAts))
234
if err != nil {
235
return nil, fmt.Errorf("failed to get submissions: %w", err)
236
}
···
242
}
243
244
// collect allLabels for each issue
245
+
allLabels, err := GetLabels(e, orm.FilterIn("subject", pullAts))
246
if err != nil {
247
return nil, fmt.Errorf("failed to query labels: %w", err)
248
}
···
259
sourceAts = append(sourceAts, *p.PullSource.RepoAt)
260
}
261
}
262
+
sourceRepos, err := GetRepos(e, 0, orm.FilterIn("at_uri", sourceAts))
263
if err != nil && !errors.Is(err, sql.ErrNoRows) {
264
return nil, fmt.Errorf("failed to get source repos: %w", err)
265
}
···
275
}
276
}
277
278
+
allReferences, err := GetReferencesAll(e, orm.FilterIn("from_at", pullAts))
279
if err != nil {
280
return nil, fmt.Errorf("failed to query reference_links: %w", err)
281
}
···
296
return orderedByPullId, nil
297
}
298
299
+
func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) {
300
return GetPullsWithLimit(e, 0, filters...)
301
}
302
303
func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
304
var ids []int64
305
306
+
var filters []orm.Filter
307
+
filters = append(filters, orm.FilterEq("state", opts.State))
308
if opts.RepoAt != "" {
309
+
filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
310
}
311
312
var conditions []string
···
362
}
363
364
func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
365
+
pulls, err := GetPullsWithLimit(e, 1, orm.FilterEq("repo_at", repoAt), orm.FilterEq("pull_id", pullId))
366
if err != nil {
367
return nil, err
368
}
···
374
}
375
376
// mapping from pull -> pull submissions
377
+
func GetPullSubmissions(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
378
var conditions []string
379
var args []any
380
for _, filter := range filters {
···
449
450
// Get comments for all submissions using GetPullComments
451
submissionIds := slices.Collect(maps.Keys(submissionMap))
452
+
comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds))
453
if err != nil {
454
return nil, fmt.Errorf("failed to get pull comments: %w", err)
455
}
···
475
return m, nil
476
}
477
478
+
func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) {
479
var conditions []string
480
var args []any
481
for _, filter := range filters {
···
543
544
// collect references for each comments
545
commentAts := slices.Collect(maps.Keys(commentMap))
546
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
547
if err != nil {
548
return nil, fmt.Errorf("failed to query reference_links: %w", err)
549
}
···
709
return err
710
}
711
712
+
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...orm.Filter) error {
713
var conditions []string
714
var args []any
715
···
733
734
// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
735
// otherwise submissions are immutable
736
+
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...orm.Filter) error {
737
var conditions []string
738
var args []any
739
···
791
func GetStack(e Execer, stackId string) (models.Stack, error) {
792
unorderedPulls, err := GetPulls(
793
e,
794
+
orm.FilterEq("stack_id", stackId),
795
+
orm.FilterNotEq("state", models.PullDeleted),
796
)
797
if err != nil {
798
return nil, err
···
836
func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
837
pulls, err := GetPulls(
838
e,
839
+
orm.FilterEq("stack_id", stackId),
840
+
orm.FilterEq("state", models.PullDeleted),
841
)
842
if err != nil {
843
return nil, err
+2
-1
appview/db/punchcard.go
+2
-1
appview/db/punchcard.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
// this adds to the existing count
···
20
return err
21
}
22
23
-
func MakePunchcard(e Execer, filters ...filter) (*models.Punchcard, error) {
24
punchcard := &models.Punchcard{}
25
now := time.Now()
26
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
// this adds to the existing count
···
21
return err
22
}
23
24
+
func MakePunchcard(e Execer, filters ...orm.Filter) (*models.Punchcard, error) {
25
punchcard := &models.Punchcard{}
26
now := time.Now()
27
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
+4
-3
appview/db/reference.go
+4
-3
appview/db/reference.go
···
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"tangled.org/core/api/tangled"
10
"tangled.org/core/appview/models"
11
)
12
13
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
···
205
return err
206
}
207
208
-
func GetReferencesAll(e Execer, filters ...filter) (map[syntax.ATURI][]syntax.ATURI, error) {
209
var (
210
conditions []string
211
args []any
···
347
if len(aturis) == 0 {
348
return nil, nil
349
}
350
-
filter := FilterIn("c.at_uri", aturis)
351
rows, err := e.Query(
352
fmt.Sprintf(
353
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
···
427
if len(aturis) == 0 {
428
return nil, nil
429
}
430
-
filter := FilterIn("c.comment_at", aturis)
431
rows, err := e.Query(
432
fmt.Sprintf(
433
`select r.did, r.name, p.pull_id, c.id, p.title, p.state
···
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"tangled.org/core/api/tangled"
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
)
13
14
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
···
206
return err
207
}
208
209
+
func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) {
210
var (
211
conditions []string
212
args []any
···
348
if len(aturis) == 0 {
349
return nil, nil
350
}
351
+
filter := orm.FilterIn("c.at_uri", aturis)
352
rows, err := e.Query(
353
fmt.Sprintf(
354
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
···
428
if len(aturis) == 0 {
429
return nil, nil
430
}
431
+
filter := orm.FilterIn("c.comment_at", aturis)
432
rows, err := e.Query(
433
fmt.Sprintf(
434
`select r.did, r.name, p.pull_id, c.id, p.title, p.state
+5
-3
appview/db/registration.go
+5
-3
appview/db/registration.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetRegistrations(e Execer, filters ...filter) ([]models.Registration, error) {
13
var registrations []models.Registration
14
15
var conditions []string
···
37
if err != nil {
38
return nil, err
39
}
40
41
for rows.Next() {
42
var createdAt string
···
69
return registrations, nil
70
}
71
72
-
func MarkRegistered(e Execer, filters ...filter) error {
73
var conditions []string
74
var args []any
75
for _, filter := range filters {
···
94
return err
95
}
96
97
-
func DeleteKnot(e Execer, filters ...filter) error {
98
var conditions []string
99
var args []any
100
for _, filter := range filters {
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
+
func GetRegistrations(e Execer, filters ...orm.Filter) ([]models.Registration, error) {
14
var registrations []models.Registration
15
16
var conditions []string
···
38
if err != nil {
39
return nil, err
40
}
41
+
defer rows.Close()
42
43
for rows.Next() {
44
var createdAt string
···
71
return registrations, nil
72
}
73
74
+
func MarkRegistered(e Execer, filters ...orm.Filter) error {
75
var conditions []string
76
var args []any
77
for _, filter := range filters {
···
96
return err
97
}
98
99
+
func DeleteKnot(e Execer, filters ...orm.Filter) error {
100
var conditions []string
101
var args []any
102
for _, filter := range filters {
+17
-6
appview/db/repos.go
+17
-6
appview/db/repos.go
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
)
15
16
-
func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) {
17
repoMap := make(map[syntax.ATURI]*models.Repo)
18
19
var conditions []string
···
55
limitClause,
56
)
57
rows, err := e.Query(repoQuery, args...)
58
-
59
if err != nil {
60
return nil, fmt.Errorf("failed to execute repo query: %w ", err)
61
}
62
63
for rows.Next() {
64
var repo models.Repo
···
127
if err != nil {
128
return nil, fmt.Errorf("failed to execute labels query: %w ", err)
129
}
130
for rows.Next() {
131
var repoat, labelat string
132
if err := rows.Scan(&repoat, &labelat); err != nil {
···
164
if err != nil {
165
return nil, fmt.Errorf("failed to execute lang query: %w ", err)
166
}
167
for rows.Next() {
168
var repoat, lang string
169
if err := rows.Scan(&repoat, &lang); err != nil {
···
190
if err != nil {
191
return nil, fmt.Errorf("failed to execute star-count query: %w ", err)
192
}
193
for rows.Next() {
194
var repoat string
195
var count int
···
219
if err != nil {
220
return nil, fmt.Errorf("failed to execute issue-count query: %w ", err)
221
}
222
for rows.Next() {
223
var repoat string
224
var open, closed int
···
260
if err != nil {
261
return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err)
262
}
263
for rows.Next() {
264
var repoat string
265
var open, merged, closed, deleted int
···
294
}
295
296
// helper to get exactly one repo
297
-
func GetRepo(e Execer, filters ...filter) (*models.Repo, error) {
298
repos, err := GetRepos(e, 0, filters...)
299
if err != nil {
300
return nil, err
···
311
return &repos[0], nil
312
}
313
314
-
func CountRepos(e Execer, filters ...filter) (int64, error) {
315
var conditions []string
316
var args []any
317
for _, filter := range filters {
···
542
return err
543
}
544
545
-
func UnsubscribeLabel(e Execer, filters ...filter) error {
546
var conditions []string
547
var args []any
548
for _, filter := range filters {
···
560
return err
561
}
562
563
-
func GetRepoLabels(e Execer, filters ...filter) ([]models.RepoLabel, error) {
564
var conditions []string
565
var args []any
566
for _, filter := range filters {
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
15
)
16
17
+
func GetRepos(e Execer, limit int, filters ...orm.Filter) ([]models.Repo, error) {
18
repoMap := make(map[syntax.ATURI]*models.Repo)
19
20
var conditions []string
···
56
limitClause,
57
)
58
rows, err := e.Query(repoQuery, args...)
59
if err != nil {
60
return nil, fmt.Errorf("failed to execute repo query: %w ", err)
61
}
62
+
defer rows.Close()
63
64
for rows.Next() {
65
var repo models.Repo
···
128
if err != nil {
129
return nil, fmt.Errorf("failed to execute labels query: %w ", err)
130
}
131
+
defer rows.Close()
132
+
133
for rows.Next() {
134
var repoat, labelat string
135
if err := rows.Scan(&repoat, &labelat); err != nil {
···
167
if err != nil {
168
return nil, fmt.Errorf("failed to execute lang query: %w ", err)
169
}
170
+
defer rows.Close()
171
+
172
for rows.Next() {
173
var repoat, lang string
174
if err := rows.Scan(&repoat, &lang); err != nil {
···
195
if err != nil {
196
return nil, fmt.Errorf("failed to execute star-count query: %w ", err)
197
}
198
+
defer rows.Close()
199
+
200
for rows.Next() {
201
var repoat string
202
var count int
···
226
if err != nil {
227
return nil, fmt.Errorf("failed to execute issue-count query: %w ", err)
228
}
229
+
defer rows.Close()
230
+
231
for rows.Next() {
232
var repoat string
233
var open, closed int
···
269
if err != nil {
270
return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err)
271
}
272
+
defer rows.Close()
273
+
274
for rows.Next() {
275
var repoat string
276
var open, merged, closed, deleted int
···
305
}
306
307
// helper to get exactly one repo
308
+
func GetRepo(e Execer, filters ...orm.Filter) (*models.Repo, error) {
309
repos, err := GetRepos(e, 0, filters...)
310
if err != nil {
311
return nil, err
···
322
return &repos[0], nil
323
}
324
325
+
func CountRepos(e Execer, filters ...orm.Filter) (int64, error) {
326
var conditions []string
327
var args []any
328
for _, filter := range filters {
···
553
return err
554
}
555
556
+
func UnsubscribeLabel(e Execer, filters ...orm.Filter) error {
557
var conditions []string
558
var args []any
559
for _, filter := range filters {
···
571
return err
572
}
573
574
+
func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) {
575
var conditions []string
576
var args []any
577
for _, filter := range filters {
+6
-5
appview/db/spindle.go
+6
-5
appview/db/spindle.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetSpindles(e Execer, filters ...filter) ([]models.Spindle, error) {
13
var spindles []models.Spindle
14
15
var conditions []string
···
91
return err
92
}
93
94
-
func VerifySpindle(e Execer, filters ...filter) (int64, error) {
95
var conditions []string
96
var args []any
97
for _, filter := range filters {
···
114
return res.RowsAffected()
115
}
116
117
-
func DeleteSpindle(e Execer, filters ...filter) error {
118
var conditions []string
119
var args []any
120
for _, filter := range filters {
···
144
return err
145
}
146
147
-
func RemoveSpindleMember(e Execer, filters ...filter) error {
148
var conditions []string
149
var args []any
150
for _, filter := range filters {
···
163
return err
164
}
165
166
-
func GetSpindleMembers(e Execer, filters ...filter) ([]models.SpindleMember, error) {
167
var members []models.SpindleMember
168
169
var conditions []string
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
+
func GetSpindles(e Execer, filters ...orm.Filter) ([]models.Spindle, error) {
14
var spindles []models.Spindle
15
16
var conditions []string
···
92
return err
93
}
94
95
+
func VerifySpindle(e Execer, filters ...orm.Filter) (int64, error) {
96
var conditions []string
97
var args []any
98
for _, filter := range filters {
···
115
return res.RowsAffected()
116
}
117
118
+
func DeleteSpindle(e Execer, filters ...orm.Filter) error {
119
var conditions []string
120
var args []any
121
for _, filter := range filters {
···
145
return err
146
}
147
148
+
func RemoveSpindleMember(e Execer, filters ...orm.Filter) error {
149
var conditions []string
150
var args []any
151
for _, filter := range filters {
···
164
return err
165
}
166
167
+
func GetSpindleMembers(e Execer, filters ...orm.Filter) ([]models.SpindleMember, error) {
168
var members []models.SpindleMember
169
170
var conditions []string
+6
-4
appview/db/star.go
+6
-4
appview/db/star.go
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
)
15
16
func AddStar(e Execer, star *models.Star) error {
···
133
134
// GetRepoStars return a list of stars each holding target repository.
135
// If there isn't known repo with starred at-uri, those stars will be ignored.
136
-
func GetRepoStars(e Execer, limit int, filters ...filter) ([]models.RepoStar, error) {
137
var conditions []string
138
var args []any
139
for _, filter := range filters {
···
164
if err != nil {
165
return nil, err
166
}
167
168
starMap := make(map[string][]models.Star)
169
for rows.Next() {
···
195
return nil, nil
196
}
197
198
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", args))
199
if err != nil {
200
return nil, err
201
}
···
225
return repoStars, nil
226
}
227
228
-
func CountStars(e Execer, filters ...filter) (int64, error) {
229
var conditions []string
230
var args []any
231
for _, filter := range filters {
···
298
}
299
300
// get full repo data
301
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris))
302
if err != nil {
303
return nil, err
304
}
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
15
)
16
17
func AddStar(e Execer, star *models.Star) error {
···
134
135
// GetRepoStars return a list of stars each holding target repository.
136
// If there isn't known repo with starred at-uri, those stars will be ignored.
137
+
func GetRepoStars(e Execer, limit int, filters ...orm.Filter) ([]models.RepoStar, error) {
138
var conditions []string
139
var args []any
140
for _, filter := range filters {
···
165
if err != nil {
166
return nil, err
167
}
168
+
defer rows.Close()
169
170
starMap := make(map[string][]models.Star)
171
for rows.Next() {
···
197
return nil, nil
198
}
199
200
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", args))
201
if err != nil {
202
return nil, err
203
}
···
227
return repoStars, nil
228
}
229
230
+
func CountStars(e Execer, filters ...orm.Filter) (int64, error) {
231
var conditions []string
232
var args []any
233
for _, filter := range filters {
···
300
}
301
302
// get full repo data
303
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoUris))
304
if err != nil {
305
return nil, err
306
}
+4
-3
appview/db/strings.go
+4
-3
appview/db/strings.go
···
8
"time"
9
10
"tangled.org/core/appview/models"
11
)
12
13
func AddString(e Execer, s models.String) error {
···
44
return err
45
}
46
47
-
func GetStrings(e Execer, limit int, filters ...filter) ([]models.String, error) {
48
var all []models.String
49
50
var conditions []string
···
127
return all, nil
128
}
129
130
-
func CountStrings(e Execer, filters ...filter) (int64, error) {
131
var conditions []string
132
var args []any
133
for _, filter := range filters {
···
151
return count, nil
152
}
153
154
-
func DeleteString(e Execer, filters ...filter) error {
155
var conditions []string
156
var args []any
157
for _, filter := range filters {
···
8
"time"
9
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
)
13
14
func AddString(e Execer, s models.String) error {
···
45
return err
46
}
47
48
+
func GetStrings(e Execer, limit int, filters ...orm.Filter) ([]models.String, error) {
49
var all []models.String
50
51
var conditions []string
···
128
return all, nil
129
}
130
131
+
func CountStrings(e Execer, filters ...orm.Filter) (int64, error) {
132
var conditions []string
133
var args []any
134
for _, filter := range filters {
···
152
return count, nil
153
}
154
155
+
func DeleteString(e Execer, filters ...orm.Filter) error {
156
var conditions []string
157
var args []any
158
for _, filter := range filters {
+9
-8
appview/db/timeline.go
+9
-8
appview/db/timeline.go
···
5
6
"github.com/bluesky-social/indigo/atproto/syntax"
7
"tangled.org/core/appview/models"
8
)
9
10
// TODO: this gathers heterogenous events from different sources and aggregates
···
84
}
85
86
func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
87
-
filters := make([]filter, 0)
88
if userIsFollowing != nil {
89
-
filters = append(filters, FilterIn("did", userIsFollowing))
90
}
91
92
repos, err := GetRepos(e, limit, filters...)
···
104
105
var origRepos []models.Repo
106
if args != nil {
107
-
origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args))
108
}
109
if err != nil {
110
return nil, err
···
144
}
145
146
func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
147
-
filters := make([]filter, 0)
148
if userIsFollowing != nil {
149
-
filters = append(filters, FilterIn("did", userIsFollowing))
150
}
151
152
stars, err := GetRepoStars(e, limit, filters...)
···
180
}
181
182
func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
183
-
filters := make([]filter, 0)
184
if userIsFollowing != nil {
185
-
filters = append(filters, FilterIn("user_did", userIsFollowing))
186
}
187
188
follows, err := GetFollows(e, limit, filters...)
···
199
return nil, nil
200
}
201
202
-
profiles, err := GetProfiles(e, FilterIn("did", subjects))
203
if err != nil {
204
return nil, err
205
}
···
5
6
"github.com/bluesky-social/indigo/atproto/syntax"
7
"tangled.org/core/appview/models"
8
+
"tangled.org/core/orm"
9
)
10
11
// TODO: this gathers heterogenous events from different sources and aggregates
···
85
}
86
87
func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
88
+
filters := make([]orm.Filter, 0)
89
if userIsFollowing != nil {
90
+
filters = append(filters, orm.FilterIn("did", userIsFollowing))
91
}
92
93
repos, err := GetRepos(e, limit, filters...)
···
105
106
var origRepos []models.Repo
107
if args != nil {
108
+
origRepos, err = GetRepos(e, 0, orm.FilterIn("at_uri", args))
109
}
110
if err != nil {
111
return nil, err
···
145
}
146
147
func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
148
+
filters := make([]orm.Filter, 0)
149
if userIsFollowing != nil {
150
+
filters = append(filters, orm.FilterIn("did", userIsFollowing))
151
}
152
153
stars, err := GetRepoStars(e, limit, filters...)
···
181
}
182
183
func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
184
+
filters := make([]orm.Filter, 0)
185
if userIsFollowing != nil {
186
+
filters = append(filters, orm.FilterIn("user_did", userIsFollowing))
187
}
188
189
follows, err := GetFollows(e, limit, filters...)
···
200
return nil, nil
201
}
202
203
+
profiles, err := GetProfiles(e, orm.FilterIn("did", subjects))
204
if err != nil {
205
return nil, err
206
}
+25
-24
appview/ingester.go
+25
-24
appview/ingester.go
···
21
"tangled.org/core/appview/serververify"
22
"tangled.org/core/appview/validator"
23
"tangled.org/core/idresolver"
24
"tangled.org/core/rbac"
25
)
26
···
253
254
err = db.AddArtifact(i.Db, artifact)
255
case jmodels.CommitOperationDelete:
256
-
err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
257
}
258
259
if err != nil {
···
350
351
err = db.UpsertProfile(tx, &profile)
352
case jmodels.CommitOperationDelete:
353
-
err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
354
}
355
356
if err != nil {
···
424
// get record from db first
425
members, err := db.GetSpindleMembers(
426
ddb,
427
-
db.FilterEq("did", did),
428
-
db.FilterEq("rkey", rkey),
429
)
430
if err != nil || len(members) != 1 {
431
return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members))
···
440
// remove record by rkey && update enforcer
441
if err = db.RemoveSpindleMember(
442
tx,
443
-
db.FilterEq("did", did),
444
-
db.FilterEq("rkey", rkey),
445
); err != nil {
446
return fmt.Errorf("failed to remove from db: %w", err)
447
}
···
523
// get record from db first
524
spindles, err := db.GetSpindles(
525
ddb,
526
-
db.FilterEq("owner", did),
527
-
db.FilterEq("instance", instance),
528
)
529
if err != nil || len(spindles) != 1 {
530
return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles))
···
543
// remove spindle members first
544
err = db.RemoveSpindleMember(
545
tx,
546
-
db.FilterEq("owner", did),
547
-
db.FilterEq("instance", instance),
548
)
549
if err != nil {
550
return err
···
552
553
err = db.DeleteSpindle(
554
tx,
555
-
db.FilterEq("owner", did),
556
-
db.FilterEq("instance", instance),
557
)
558
if err != nil {
559
return err
···
621
case jmodels.CommitOperationDelete:
622
if err := db.DeleteString(
623
ddb,
624
-
db.FilterEq("did", did),
625
-
db.FilterEq("rkey", rkey),
626
); err != nil {
627
l.Error("failed to delete", "err", err)
628
return fmt.Errorf("failed to delete string record: %w", err)
···
740
// get record from db first
741
registrations, err := db.GetRegistrations(
742
ddb,
743
-
db.FilterEq("domain", domain),
744
-
db.FilterEq("did", did),
745
)
746
if err != nil {
747
return fmt.Errorf("failed to get registration: %w", err)
···
762
763
err = db.DeleteKnot(
764
tx,
765
-
db.FilterEq("did", did),
766
-
db.FilterEq("domain", domain),
767
)
768
if err != nil {
769
return err
···
915
case jmodels.CommitOperationDelete:
916
if err := db.DeleteIssueComments(
917
ddb,
918
-
db.FilterEq("did", did),
919
-
db.FilterEq("rkey", rkey),
920
); err != nil {
921
return fmt.Errorf("failed to delete issue comment record: %w", err)
922
}
···
969
case jmodels.CommitOperationDelete:
970
if err := db.DeleteLabelDefinition(
971
ddb,
972
-
db.FilterEq("did", did),
973
-
db.FilterEq("rkey", rkey),
974
); err != nil {
975
return fmt.Errorf("failed to delete labeldef record: %w", err)
976
}
···
1010
var repo *models.Repo
1011
switch collection {
1012
case tangled.RepoIssueNSID:
1013
-
i, err := db.GetIssues(ddb, db.FilterEq("at_uri", subject))
1014
if err != nil || len(i) != 1 {
1015
return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i))
1016
}
···
1019
return fmt.Errorf("unsupport label subject: %s", collection)
1020
}
1021
1022
-
actx, err := db.NewLabelApplicationCtx(ddb, db.FilterIn("at_uri", repo.Labels))
1023
if err != nil {
1024
return fmt.Errorf("failed to build label application ctx: %w", err)
1025
}
···
21
"tangled.org/core/appview/serververify"
22
"tangled.org/core/appview/validator"
23
"tangled.org/core/idresolver"
24
+
"tangled.org/core/orm"
25
"tangled.org/core/rbac"
26
)
27
···
254
255
err = db.AddArtifact(i.Db, artifact)
256
case jmodels.CommitOperationDelete:
257
+
err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey))
258
}
259
260
if err != nil {
···
351
352
err = db.UpsertProfile(tx, &profile)
353
case jmodels.CommitOperationDelete:
354
+
err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey))
355
}
356
357
if err != nil {
···
425
// get record from db first
426
members, err := db.GetSpindleMembers(
427
ddb,
428
+
orm.FilterEq("did", did),
429
+
orm.FilterEq("rkey", rkey),
430
)
431
if err != nil || len(members) != 1 {
432
return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members))
···
441
// remove record by rkey && update enforcer
442
if err = db.RemoveSpindleMember(
443
tx,
444
+
orm.FilterEq("did", did),
445
+
orm.FilterEq("rkey", rkey),
446
); err != nil {
447
return fmt.Errorf("failed to remove from db: %w", err)
448
}
···
524
// get record from db first
525
spindles, err := db.GetSpindles(
526
ddb,
527
+
orm.FilterEq("owner", did),
528
+
orm.FilterEq("instance", instance),
529
)
530
if err != nil || len(spindles) != 1 {
531
return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles))
···
544
// remove spindle members first
545
err = db.RemoveSpindleMember(
546
tx,
547
+
orm.FilterEq("owner", did),
548
+
orm.FilterEq("instance", instance),
549
)
550
if err != nil {
551
return err
···
553
554
err = db.DeleteSpindle(
555
tx,
556
+
orm.FilterEq("owner", did),
557
+
orm.FilterEq("instance", instance),
558
)
559
if err != nil {
560
return err
···
622
case jmodels.CommitOperationDelete:
623
if err := db.DeleteString(
624
ddb,
625
+
orm.FilterEq("did", did),
626
+
orm.FilterEq("rkey", rkey),
627
); err != nil {
628
l.Error("failed to delete", "err", err)
629
return fmt.Errorf("failed to delete string record: %w", err)
···
741
// get record from db first
742
registrations, err := db.GetRegistrations(
743
ddb,
744
+
orm.FilterEq("domain", domain),
745
+
orm.FilterEq("did", did),
746
)
747
if err != nil {
748
return fmt.Errorf("failed to get registration: %w", err)
···
763
764
err = db.DeleteKnot(
765
tx,
766
+
orm.FilterEq("did", did),
767
+
orm.FilterEq("domain", domain),
768
)
769
if err != nil {
770
return err
···
916
case jmodels.CommitOperationDelete:
917
if err := db.DeleteIssueComments(
918
ddb,
919
+
orm.FilterEq("did", did),
920
+
orm.FilterEq("rkey", rkey),
921
); err != nil {
922
return fmt.Errorf("failed to delete issue comment record: %w", err)
923
}
···
970
case jmodels.CommitOperationDelete:
971
if err := db.DeleteLabelDefinition(
972
ddb,
973
+
orm.FilterEq("did", did),
974
+
orm.FilterEq("rkey", rkey),
975
); err != nil {
976
return fmt.Errorf("failed to delete labeldef record: %w", err)
977
}
···
1011
var repo *models.Repo
1012
switch collection {
1013
case tangled.RepoIssueNSID:
1014
+
i, err := db.GetIssues(ddb, orm.FilterEq("at_uri", subject))
1015
if err != nil || len(i) != 1 {
1016
return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i))
1017
}
···
1020
return fmt.Errorf("unsupport label subject: %s", collection)
1021
}
1022
1023
+
actx, err := db.NewLabelApplicationCtx(ddb, orm.FilterIn("at_uri", repo.Labels))
1024
if err != nil {
1025
return fmt.Errorf("failed to build label application ctx: %w", err)
1026
}
+46
-45
appview/issues/issues.go
+46
-45
appview/issues/issues.go
···
19
"tangled.org/core/appview/config"
20
"tangled.org/core/appview/db"
21
issues_indexer "tangled.org/core/appview/indexer/issues"
22
"tangled.org/core/appview/models"
23
"tangled.org/core/appview/notify"
24
"tangled.org/core/appview/oauth"
25
"tangled.org/core/appview/pages"
26
"tangled.org/core/appview/pages/repoinfo"
27
"tangled.org/core/appview/pagination"
28
-
"tangled.org/core/appview/refresolver"
29
"tangled.org/core/appview/reporesolver"
30
"tangled.org/core/appview/validator"
31
"tangled.org/core/idresolver"
32
"tangled.org/core/rbac"
33
"tangled.org/core/tid"
34
)
35
36
type Issues struct {
37
-
oauth *oauth.OAuth
38
-
repoResolver *reporesolver.RepoResolver
39
-
enforcer *rbac.Enforcer
40
-
pages *pages.Pages
41
-
idResolver *idresolver.Resolver
42
-
refResolver *refresolver.Resolver
43
-
db *db.DB
44
-
config *config.Config
45
-
notifier notify.Notifier
46
-
logger *slog.Logger
47
-
validator *validator.Validator
48
-
indexer *issues_indexer.Indexer
49
}
50
51
func New(
···
54
enforcer *rbac.Enforcer,
55
pages *pages.Pages,
56
idResolver *idresolver.Resolver,
57
-
refResolver *refresolver.Resolver,
58
db *db.DB,
59
config *config.Config,
60
notifier notify.Notifier,
···
63
logger *slog.Logger,
64
) *Issues {
65
return &Issues{
66
-
oauth: oauth,
67
-
repoResolver: repoResolver,
68
-
enforcer: enforcer,
69
-
pages: pages,
70
-
idResolver: idResolver,
71
-
refResolver: refResolver,
72
-
db: db,
73
-
config: config,
74
-
notifier: notifier,
75
-
logger: logger,
76
-
validator: validator,
77
-
indexer: indexer,
78
}
79
}
80
···
113
114
labelDefs, err := db.GetLabelDefinitions(
115
rp.db,
116
-
db.FilterIn("at_uri", f.Labels),
117
-
db.FilterContains("scope", tangled.RepoIssueNSID),
118
)
119
if err != nil {
120
l.Error("failed to fetch labels", "err", err)
···
163
newIssue := issue
164
newIssue.Title = r.FormValue("title")
165
newIssue.Body = r.FormValue("body")
166
-
newIssue.Mentions, newIssue.References = rp.refResolver.Resolve(r.Context(), newIssue.Body)
167
168
if err := rp.validator.ValidateIssue(newIssue); err != nil {
169
l.Error("validation error", "err", err)
···
314
if isIssueOwner || isRepoOwner || isCollaborator {
315
err = db.CloseIssues(
316
rp.db,
317
-
db.FilterEq("id", issue.Id),
318
)
319
if err != nil {
320
l.Error("failed to close issue", "err", err)
···
361
if isCollaborator || isRepoOwner || isIssueOwner {
362
err := db.ReopenIssues(
363
rp.db,
364
-
db.FilterEq("id", issue.Id),
365
)
366
if err != nil {
367
l.Error("failed to reopen issue", "err", err)
···
412
replyTo = &replyToUri
413
}
414
415
-
mentions, references := rp.refResolver.Resolve(r.Context(), body)
416
417
comment := models.IssueComment{
418
Did: user.Did,
···
506
commentId := chi.URLParam(r, "commentId")
507
comments, err := db.GetIssueComments(
508
rp.db,
509
-
db.FilterEq("id", commentId),
510
)
511
if err != nil {
512
l.Error("failed to fetch comment", "id", commentId)
···
542
commentId := chi.URLParam(r, "commentId")
543
comments, err := db.GetIssueComments(
544
rp.db,
545
-
db.FilterEq("id", commentId),
546
)
547
if err != nil {
548
l.Error("failed to fetch comment", "id", commentId)
···
584
newComment := comment
585
newComment.Body = newBody
586
newComment.Edited = &now
587
-
newComment.Mentions, newComment.References = rp.refResolver.Resolve(r.Context(), newBody)
588
589
record := newComment.AsRecord()
590
···
652
commentId := chi.URLParam(r, "commentId")
653
comments, err := db.GetIssueComments(
654
rp.db,
655
-
db.FilterEq("id", commentId),
656
)
657
if err != nil {
658
l.Error("failed to fetch comment", "id", commentId)
···
688
commentId := chi.URLParam(r, "commentId")
689
comments, err := db.GetIssueComments(
690
rp.db,
691
-
db.FilterEq("id", commentId),
692
)
693
if err != nil {
694
l.Error("failed to fetch comment", "id", commentId)
···
724
commentId := chi.URLParam(r, "commentId")
725
comments, err := db.GetIssueComments(
726
rp.db,
727
-
db.FilterEq("id", commentId),
728
)
729
if err != nil {
730
l.Error("failed to fetch comment", "id", commentId)
···
751
752
// optimistic deletion
753
deleted := time.Now()
754
-
err = db.DeleteIssueComments(rp.db, db.FilterEq("id", comment.Id))
755
if err != nil {
756
l.Error("failed to delete comment", "err", err)
757
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
840
841
issues, err = db.GetIssues(
842
rp.db,
843
-
db.FilterIn("id", res.Hits),
844
)
845
if err != nil {
846
l.Error("failed to get issues", "err", err)
···
856
issues, err = db.GetIssuesPaginated(
857
rp.db,
858
page,
859
-
db.FilterEq("repo_at", f.RepoAt()),
860
-
db.FilterEq("open", openInt),
861
)
862
if err != nil {
863
l.Error("failed to get issues", "err", err)
···
868
869
labelDefs, err := db.GetLabelDefinitions(
870
rp.db,
871
-
db.FilterIn("at_uri", f.Labels),
872
-
db.FilterContains("scope", tangled.RepoIssueNSID),
873
)
874
if err != nil {
875
l.Error("failed to fetch labels", "err", err)
···
912
})
913
case http.MethodPost:
914
body := r.FormValue("body")
915
-
mentions, references := rp.refResolver.Resolve(r.Context(), body)
916
917
issue := &models.Issue{
918
RepoAt: f.RepoAt(),
···
19
"tangled.org/core/appview/config"
20
"tangled.org/core/appview/db"
21
issues_indexer "tangled.org/core/appview/indexer/issues"
22
+
"tangled.org/core/appview/mentions"
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/notify"
25
"tangled.org/core/appview/oauth"
26
"tangled.org/core/appview/pages"
27
"tangled.org/core/appview/pages/repoinfo"
28
"tangled.org/core/appview/pagination"
29
"tangled.org/core/appview/reporesolver"
30
"tangled.org/core/appview/validator"
31
"tangled.org/core/idresolver"
32
+
"tangled.org/core/orm"
33
"tangled.org/core/rbac"
34
"tangled.org/core/tid"
35
)
36
37
type Issues struct {
38
+
oauth *oauth.OAuth
39
+
repoResolver *reporesolver.RepoResolver
40
+
enforcer *rbac.Enforcer
41
+
pages *pages.Pages
42
+
idResolver *idresolver.Resolver
43
+
mentionsResolver *mentions.Resolver
44
+
db *db.DB
45
+
config *config.Config
46
+
notifier notify.Notifier
47
+
logger *slog.Logger
48
+
validator *validator.Validator
49
+
indexer *issues_indexer.Indexer
50
}
51
52
func New(
···
55
enforcer *rbac.Enforcer,
56
pages *pages.Pages,
57
idResolver *idresolver.Resolver,
58
+
mentionsResolver *mentions.Resolver,
59
db *db.DB,
60
config *config.Config,
61
notifier notify.Notifier,
···
64
logger *slog.Logger,
65
) *Issues {
66
return &Issues{
67
+
oauth: oauth,
68
+
repoResolver: repoResolver,
69
+
enforcer: enforcer,
70
+
pages: pages,
71
+
idResolver: idResolver,
72
+
mentionsResolver: mentionsResolver,
73
+
db: db,
74
+
config: config,
75
+
notifier: notifier,
76
+
logger: logger,
77
+
validator: validator,
78
+
indexer: indexer,
79
}
80
}
81
···
114
115
labelDefs, err := db.GetLabelDefinitions(
116
rp.db,
117
+
orm.FilterIn("at_uri", f.Labels),
118
+
orm.FilterContains("scope", tangled.RepoIssueNSID),
119
)
120
if err != nil {
121
l.Error("failed to fetch labels", "err", err)
···
164
newIssue := issue
165
newIssue.Title = r.FormValue("title")
166
newIssue.Body = r.FormValue("body")
167
+
newIssue.Mentions, newIssue.References = rp.mentionsResolver.Resolve(r.Context(), newIssue.Body)
168
169
if err := rp.validator.ValidateIssue(newIssue); err != nil {
170
l.Error("validation error", "err", err)
···
315
if isIssueOwner || isRepoOwner || isCollaborator {
316
err = db.CloseIssues(
317
rp.db,
318
+
orm.FilterEq("id", issue.Id),
319
)
320
if err != nil {
321
l.Error("failed to close issue", "err", err)
···
362
if isCollaborator || isRepoOwner || isIssueOwner {
363
err := db.ReopenIssues(
364
rp.db,
365
+
orm.FilterEq("id", issue.Id),
366
)
367
if err != nil {
368
l.Error("failed to reopen issue", "err", err)
···
413
replyTo = &replyToUri
414
}
415
416
+
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
417
418
comment := models.IssueComment{
419
Did: user.Did,
···
507
commentId := chi.URLParam(r, "commentId")
508
comments, err := db.GetIssueComments(
509
rp.db,
510
+
orm.FilterEq("id", commentId),
511
)
512
if err != nil {
513
l.Error("failed to fetch comment", "id", commentId)
···
543
commentId := chi.URLParam(r, "commentId")
544
comments, err := db.GetIssueComments(
545
rp.db,
546
+
orm.FilterEq("id", commentId),
547
)
548
if err != nil {
549
l.Error("failed to fetch comment", "id", commentId)
···
585
newComment := comment
586
newComment.Body = newBody
587
newComment.Edited = &now
588
+
newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody)
589
590
record := newComment.AsRecord()
591
···
653
commentId := chi.URLParam(r, "commentId")
654
comments, err := db.GetIssueComments(
655
rp.db,
656
+
orm.FilterEq("id", commentId),
657
)
658
if err != nil {
659
l.Error("failed to fetch comment", "id", commentId)
···
689
commentId := chi.URLParam(r, "commentId")
690
comments, err := db.GetIssueComments(
691
rp.db,
692
+
orm.FilterEq("id", commentId),
693
)
694
if err != nil {
695
l.Error("failed to fetch comment", "id", commentId)
···
725
commentId := chi.URLParam(r, "commentId")
726
comments, err := db.GetIssueComments(
727
rp.db,
728
+
orm.FilterEq("id", commentId),
729
)
730
if err != nil {
731
l.Error("failed to fetch comment", "id", commentId)
···
752
753
// optimistic deletion
754
deleted := time.Now()
755
+
err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id))
756
if err != nil {
757
l.Error("failed to delete comment", "err", err)
758
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
841
842
issues, err = db.GetIssues(
843
rp.db,
844
+
orm.FilterIn("id", res.Hits),
845
)
846
if err != nil {
847
l.Error("failed to get issues", "err", err)
···
857
issues, err = db.GetIssuesPaginated(
858
rp.db,
859
page,
860
+
orm.FilterEq("repo_at", f.RepoAt()),
861
+
orm.FilterEq("open", openInt),
862
)
863
if err != nil {
864
l.Error("failed to get issues", "err", err)
···
869
870
labelDefs, err := db.GetLabelDefinitions(
871
rp.db,
872
+
orm.FilterIn("at_uri", f.Labels),
873
+
orm.FilterContains("scope", tangled.RepoIssueNSID),
874
)
875
if err != nil {
876
l.Error("failed to fetch labels", "err", err)
···
913
})
914
case http.MethodPost:
915
body := r.FormValue("body")
916
+
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
917
918
issue := &models.Issue{
919
RepoAt: f.RepoAt(),
+19
-18
appview/knots/knots.go
+19
-18
appview/knots/knots.go
···
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/eventconsumer"
23
"tangled.org/core/idresolver"
24
"tangled.org/core/rbac"
25
"tangled.org/core/tid"
26
···
72
user := k.OAuth.GetUser(r)
73
registrations, err := db.GetRegistrations(
74
k.Db,
75
-
db.FilterEq("did", user.Did),
76
)
77
if err != nil {
78
k.Logger.Error("failed to fetch knot registrations", "err", err)
···
102
103
registrations, err := db.GetRegistrations(
104
k.Db,
105
-
db.FilterEq("did", user.Did),
106
-
db.FilterEq("domain", domain),
107
)
108
if err != nil {
109
l.Error("failed to get registrations", "err", err)
···
127
repos, err := db.GetRepos(
128
k.Db,
129
0,
130
-
db.FilterEq("knot", domain),
131
)
132
if err != nil {
133
l.Error("failed to get knot repos", "err", err)
···
293
// get record from db first
294
registrations, err := db.GetRegistrations(
295
k.Db,
296
-
db.FilterEq("did", user.Did),
297
-
db.FilterEq("domain", domain),
298
)
299
if err != nil {
300
l.Error("failed to get registration", "err", err)
···
321
322
err = db.DeleteKnot(
323
tx,
324
-
db.FilterEq("did", user.Did),
325
-
db.FilterEq("domain", domain),
326
)
327
if err != nil {
328
l.Error("failed to delete registration", "err", err)
···
402
// get record from db first
403
registrations, err := db.GetRegistrations(
404
k.Db,
405
-
db.FilterEq("did", user.Did),
406
-
db.FilterEq("domain", domain),
407
)
408
if err != nil {
409
l.Error("failed to get registration", "err", err)
···
493
// Get updated registration to show
494
registrations, err = db.GetRegistrations(
495
k.Db,
496
-
db.FilterEq("did", user.Did),
497
-
db.FilterEq("domain", domain),
498
)
499
if err != nil {
500
l.Error("failed to get registration", "err", err)
···
529
530
registrations, err := db.GetRegistrations(
531
k.Db,
532
-
db.FilterEq("did", user.Did),
533
-
db.FilterEq("domain", domain),
534
-
db.FilterIsNot("registered", "null"),
535
)
536
if err != nil {
537
l.Error("failed to get registration", "err", err)
···
637
638
registrations, err := db.GetRegistrations(
639
k.Db,
640
-
db.FilterEq("did", user.Did),
641
-
db.FilterEq("domain", domain),
642
-
db.FilterIsNot("registered", "null"),
643
)
644
if err != nil {
645
l.Error("failed to get registration", "err", err)
···
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/eventconsumer"
23
"tangled.org/core/idresolver"
24
+
"tangled.org/core/orm"
25
"tangled.org/core/rbac"
26
"tangled.org/core/tid"
27
···
73
user := k.OAuth.GetUser(r)
74
registrations, err := db.GetRegistrations(
75
k.Db,
76
+
orm.FilterEq("did", user.Did),
77
)
78
if err != nil {
79
k.Logger.Error("failed to fetch knot registrations", "err", err)
···
103
104
registrations, err := db.GetRegistrations(
105
k.Db,
106
+
orm.FilterEq("did", user.Did),
107
+
orm.FilterEq("domain", domain),
108
)
109
if err != nil {
110
l.Error("failed to get registrations", "err", err)
···
128
repos, err := db.GetRepos(
129
k.Db,
130
0,
131
+
orm.FilterEq("knot", domain),
132
)
133
if err != nil {
134
l.Error("failed to get knot repos", "err", err)
···
294
// get record from db first
295
registrations, err := db.GetRegistrations(
296
k.Db,
297
+
orm.FilterEq("did", user.Did),
298
+
orm.FilterEq("domain", domain),
299
)
300
if err != nil {
301
l.Error("failed to get registration", "err", err)
···
322
323
err = db.DeleteKnot(
324
tx,
325
+
orm.FilterEq("did", user.Did),
326
+
orm.FilterEq("domain", domain),
327
)
328
if err != nil {
329
l.Error("failed to delete registration", "err", err)
···
403
// get record from db first
404
registrations, err := db.GetRegistrations(
405
k.Db,
406
+
orm.FilterEq("did", user.Did),
407
+
orm.FilterEq("domain", domain),
408
)
409
if err != nil {
410
l.Error("failed to get registration", "err", err)
···
494
// Get updated registration to show
495
registrations, err = db.GetRegistrations(
496
k.Db,
497
+
orm.FilterEq("did", user.Did),
498
+
orm.FilterEq("domain", domain),
499
)
500
if err != nil {
501
l.Error("failed to get registration", "err", err)
···
530
531
registrations, err := db.GetRegistrations(
532
k.Db,
533
+
orm.FilterEq("did", user.Did),
534
+
orm.FilterEq("domain", domain),
535
+
orm.FilterIsNot("registered", "null"),
536
)
537
if err != nil {
538
l.Error("failed to get registration", "err", err)
···
638
639
registrations, err := db.GetRegistrations(
640
k.Db,
641
+
orm.FilterEq("did", user.Did),
642
+
orm.FilterEq("domain", domain),
643
+
orm.FilterIsNot("registered", "null"),
644
)
645
if err != nil {
646
l.Error("failed to get registration", "err", err)
+5
-4
appview/labels/labels.go
+5
-4
appview/labels/labels.go
···
16
"tangled.org/core/appview/oauth"
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/validator"
19
"tangled.org/core/rbac"
20
"tangled.org/core/tid"
21
···
88
repoAt := r.Form.Get("repo")
89
subjectUri := r.Form.Get("subject")
90
91
-
repo, err := db.GetRepo(l.db, db.FilterEq("at_uri", repoAt))
92
if err != nil {
93
fail("Failed to get repository.", err)
94
return
95
}
96
97
// find all the labels that this repo subscribes to
98
-
repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt))
99
if err != nil {
100
fail("Failed to get labels for this repository.", err)
101
return
···
106
labelAts = append(labelAts, rl.LabelAt.String())
107
}
108
109
-
actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts))
110
if err != nil {
111
fail("Invalid form data.", err)
112
return
113
}
114
115
// calculate the start state by applying already known labels
116
-
existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri))
117
if err != nil {
118
fail("Invalid form data.", err)
119
return
···
16
"tangled.org/core/appview/oauth"
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/validator"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/rbac"
21
"tangled.org/core/tid"
22
···
89
repoAt := r.Form.Get("repo")
90
subjectUri := r.Form.Get("subject")
91
92
+
repo, err := db.GetRepo(l.db, orm.FilterEq("at_uri", repoAt))
93
if err != nil {
94
fail("Failed to get repository.", err)
95
return
96
}
97
98
// find all the labels that this repo subscribes to
99
+
repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_at", repoAt))
100
if err != nil {
101
fail("Failed to get labels for this repository.", err)
102
return
···
107
labelAts = append(labelAts, rl.LabelAt.String())
108
}
109
110
+
actx, err := db.NewLabelApplicationCtx(l.db, orm.FilterIn("at_uri", labelAts))
111
if err != nil {
112
fail("Invalid form data.", err)
113
return
114
}
115
116
// calculate the start state by applying already known labels
117
+
existingOps, err := db.GetLabelOps(l.db, orm.FilterEq("subject", subjectUri))
118
if err != nil {
119
fail("Invalid form data.", err)
120
return
+67
appview/mentions/resolver.go
+67
appview/mentions/resolver.go
···
···
1
+
package mentions
2
+
3
+
import (
4
+
"context"
5
+
"log/slog"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
"tangled.org/core/appview/config"
9
+
"tangled.org/core/appview/db"
10
+
"tangled.org/core/appview/models"
11
+
"tangled.org/core/appview/pages/markup"
12
+
"tangled.org/core/idresolver"
13
+
)
14
+
15
+
type Resolver struct {
16
+
config *config.Config
17
+
idResolver *idresolver.Resolver
18
+
execer db.Execer
19
+
logger *slog.Logger
20
+
}
21
+
22
+
func New(
23
+
config *config.Config,
24
+
idResolver *idresolver.Resolver,
25
+
execer db.Execer,
26
+
logger *slog.Logger,
27
+
) *Resolver {
28
+
return &Resolver{
29
+
config,
30
+
idResolver,
31
+
execer,
32
+
logger,
33
+
}
34
+
}
35
+
36
+
func (r *Resolver) Resolve(ctx context.Context, source string) ([]syntax.DID, []syntax.ATURI) {
37
+
l := r.logger.With("method", "Resolve")
38
+
39
+
rawMentions, rawRefs := markup.FindReferences(r.config.Core.AppviewHost, source)
40
+
l.Debug("found possible references", "mentions", rawMentions, "refs", rawRefs)
41
+
42
+
idents := r.idResolver.ResolveIdents(ctx, rawMentions)
43
+
var mentions []syntax.DID
44
+
for _, ident := range idents {
45
+
if ident != nil && !ident.Handle.IsInvalidHandle() {
46
+
mentions = append(mentions, ident.DID)
47
+
}
48
+
}
49
+
l.Debug("found mentions", "mentions", mentions)
50
+
51
+
var resolvedRefs []models.ReferenceLink
52
+
for _, rawRef := range rawRefs {
53
+
ident, err := r.idResolver.ResolveIdent(ctx, rawRef.Handle)
54
+
if err != nil || ident == nil || ident.Handle.IsInvalidHandle() {
55
+
continue
56
+
}
57
+
rawRef.Handle = string(ident.DID)
58
+
resolvedRefs = append(resolvedRefs, rawRef)
59
+
}
60
+
aturiRefs, err := db.ValidateReferenceLinks(r.execer, resolvedRefs)
61
+
if err != nil {
62
+
l.Error("failed running query", "err", err)
63
+
}
64
+
l.Debug("found references", "refs", aturiRefs)
65
+
66
+
return mentions, aturiRefs
67
+
}
+3
-2
appview/middleware/middleware.go
+3
-2
appview/middleware/middleware.go
···
18
"tangled.org/core/appview/pagination"
19
"tangled.org/core/appview/reporesolver"
20
"tangled.org/core/idresolver"
21
"tangled.org/core/rbac"
22
)
23
···
217
218
repo, err := db.GetRepo(
219
mw.db,
220
-
db.FilterEq("did", id.DID.String()),
221
-
db.FilterEq("name", repoName),
222
)
223
if err != nil {
224
log.Println("failed to resolve repo", "err", err)
···
18
"tangled.org/core/appview/pagination"
19
"tangled.org/core/appview/reporesolver"
20
"tangled.org/core/idresolver"
21
+
"tangled.org/core/orm"
22
"tangled.org/core/rbac"
23
)
24
···
218
219
repo, err := db.GetRepo(
220
mw.db,
221
+
orm.FilterEq("did", id.DID.String()),
222
+
orm.FilterEq("name", repoName),
223
)
224
if err != nil {
225
log.Println("failed to resolve repo", "err", err)
+5
-4
appview/notifications/notifications.go
+5
-4
appview/notifications/notifications.go
···
11
"tangled.org/core/appview/oauth"
12
"tangled.org/core/appview/pages"
13
"tangled.org/core/appview/pagination"
14
)
15
16
type Notifications struct {
···
53
54
total, err := db.CountNotifications(
55
n.db,
56
-
db.FilterEq("recipient_did", user.Did),
57
)
58
if err != nil {
59
l.Error("failed to get total notifications", "err", err)
···
64
notifications, err := db.GetNotificationsWithEntities(
65
n.db,
66
page,
67
-
db.FilterEq("recipient_did", user.Did),
68
)
69
if err != nil {
70
l.Error("failed to get notifications", "err", err)
···
96
97
count, err := db.CountNotifications(
98
n.db,
99
-
db.FilterEq("recipient_did", user.Did),
100
-
db.FilterEq("read", 0),
101
)
102
if err != nil {
103
http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
···
11
"tangled.org/core/appview/oauth"
12
"tangled.org/core/appview/pages"
13
"tangled.org/core/appview/pagination"
14
+
"tangled.org/core/orm"
15
)
16
17
type Notifications struct {
···
54
55
total, err := db.CountNotifications(
56
n.db,
57
+
orm.FilterEq("recipient_did", user.Did),
58
)
59
if err != nil {
60
l.Error("failed to get total notifications", "err", err)
···
65
notifications, err := db.GetNotificationsWithEntities(
66
n.db,
67
page,
68
+
orm.FilterEq("recipient_did", user.Did),
69
)
70
if err != nil {
71
l.Error("failed to get notifications", "err", err)
···
97
98
count, err := db.CountNotifications(
99
n.db,
100
+
orm.FilterEq("recipient_did", user.Did),
101
+
orm.FilterEq("read", 0),
102
)
103
if err != nil {
104
http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
+77
-66
appview/notify/db/db.go
+77
-66
appview/notify/db/db.go
···
3
import (
4
"context"
5
"log"
6
-
"maps"
7
"slices"
8
9
"github.com/bluesky-social/indigo/atproto/syntax"
···
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/notify"
14
"tangled.org/core/idresolver"
15
)
16
17
const (
18
-
maxMentions = 5
19
)
20
21
type databaseNotifier struct {
···
42
return
43
}
44
var err error
45
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(star.RepoAt)))
46
if err != nil {
47
log.Printf("NewStar: failed to get repos: %v", err)
48
return
49
}
50
51
actorDid := syntax.DID(star.Did)
52
-
recipients := []syntax.DID{syntax.DID(repo.Did)}
53
eventType := models.NotificationTypeRepoStarred
54
entityType := "repo"
55
entityId := star.RepoAt.String()
···
74
}
75
76
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {
77
-
78
-
// build the recipients list
79
-
// - owner of the repo
80
-
// - collaborators in the repo
81
-
var recipients []syntax.DID
82
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
83
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
84
if err != nil {
85
log.Printf("failed to fetch collaborators: %v", err)
86
return
87
}
88
for _, c := range collaborators {
89
-
recipients = append(recipients, c.SubjectDid)
90
}
91
92
actorDid := syntax.DID(issue.Did)
···
108
)
109
n.notifyEvent(
110
actorDid,
111
-
mentions,
112
models.NotificationTypeUserMentioned,
113
entityType,
114
entityId,
···
119
}
120
121
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
122
-
issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt))
123
if err != nil {
124
log.Printf("NewIssueComment: failed to get issues: %v", err)
125
return
···
130
}
131
issue := issues[0]
132
133
-
var recipients []syntax.DID
134
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
135
136
if comment.IsReply() {
137
// if this comment is a reply, then notify everybody in that thread
138
parentAtUri := *comment.ReplyTo
139
-
allThreads := issue.CommentList()
140
141
// find the parent thread, and add all DIDs from here to the recipient list
142
-
for _, t := range allThreads {
143
if t.Self.AtUri().String() == parentAtUri {
144
-
recipients = append(recipients, t.Participants()...)
145
}
146
}
147
} else {
148
// not a reply, notify just the issue author
149
-
recipients = append(recipients, syntax.DID(issue.Did))
150
}
151
152
actorDid := syntax.DID(comment.Did)
···
168
)
169
n.notifyEvent(
170
actorDid,
171
-
mentions,
172
models.NotificationTypeUserMentioned,
173
entityType,
174
entityId,
···
184
185
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
186
actorDid := syntax.DID(follow.UserDid)
187
-
recipients := []syntax.DID{syntax.DID(follow.SubjectDid)}
188
eventType := models.NotificationTypeFollowed
189
entityType := "follow"
190
entityId := follow.UserDid
···
207
}
208
209
func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {
210
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
211
if err != nil {
212
log.Printf("NewPull: failed to get repos: %v", err)
213
return
214
}
215
-
216
-
// build the recipients list
217
-
// - owner of the repo
218
-
// - collaborators in the repo
219
-
var recipients []syntax.DID
220
-
recipients = append(recipients, syntax.DID(repo.Did))
221
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
222
if err != nil {
223
log.Printf("failed to fetch collaborators: %v", err)
224
return
225
}
226
for _, c := range collaborators {
227
-
recipients = append(recipients, c.SubjectDid)
228
}
229
230
actorDid := syntax.DID(pull.OwnerDid)
···
258
return
259
}
260
261
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt))
262
if err != nil {
263
log.Printf("NewPullComment: failed to get repos: %v", err)
264
return
···
267
// build up the recipients list:
268
// - repo owner
269
// - all pull participants
270
-
var recipients []syntax.DID
271
-
recipients = append(recipients, syntax.DID(repo.Did))
272
for _, p := range pull.Participants() {
273
-
recipients = append(recipients, syntax.DID(p))
274
}
275
276
actorDid := syntax.DID(comment.OwnerDid)
···
294
)
295
n.notifyEvent(
296
actorDid,
297
-
mentions,
298
models.NotificationTypeUserMentioned,
299
entityType,
300
entityId,
···
321
}
322
323
func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
324
-
// build up the recipients list:
325
-
// - repo owner
326
-
// - repo collaborators
327
-
// - all issue participants
328
-
var recipients []syntax.DID
329
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
330
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
331
if err != nil {
332
log.Printf("failed to fetch collaborators: %v", err)
333
return
334
}
335
for _, c := range collaborators {
336
-
recipients = append(recipients, c.SubjectDid)
337
}
338
for _, p := range issue.Participants() {
339
-
recipients = append(recipients, syntax.DID(p))
340
}
341
342
entityType := "pull"
···
366
367
func (n *databaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
368
// Get repo details
369
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
370
if err != nil {
371
log.Printf("NewPullState: failed to get repos: %v", err)
372
return
373
}
374
375
-
// build up the recipients list:
376
-
// - repo owner
377
-
// - all pull participants
378
-
var recipients []syntax.DID
379
-
recipients = append(recipients, syntax.DID(repo.Did))
380
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
381
if err != nil {
382
log.Printf("failed to fetch collaborators: %v", err)
383
return
384
}
385
for _, c := range collaborators {
386
-
recipients = append(recipients, c.SubjectDid)
387
}
388
for _, p := range pull.Participants() {
389
-
recipients = append(recipients, syntax.DID(p))
390
}
391
392
entityType := "pull"
···
422
423
func (n *databaseNotifier) notifyEvent(
424
actorDid syntax.DID,
425
-
recipients []syntax.DID,
426
eventType models.NotificationType,
427
entityType string,
428
entityId string,
···
430
issueId *int64,
431
pullId *int64,
432
) {
433
-
if eventType == models.NotificationTypeUserMentioned && len(recipients) > maxMentions {
434
-
recipients = recipients[:maxMentions]
435
}
436
-
recipientSet := make(map[syntax.DID]struct{})
437
-
for _, did := range recipients {
438
-
// everybody except actor themselves
439
-
if did != actorDid {
440
-
recipientSet[did] = struct{}{}
441
-
}
442
-
}
443
444
prefMap, err := db.GetNotificationPreferences(
445
n.db,
446
-
db.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))),
447
)
448
if err != nil {
449
// failed to get prefs for users
···
459
defer tx.Rollback()
460
461
// filter based on preferences
462
-
for recipientDid := range recipientSet {
463
prefs, ok := prefMap[recipientDid]
464
if !ok {
465
prefs = models.DefaultNotificationPreferences(recipientDid)
···
3
import (
4
"context"
5
"log"
6
"slices"
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
···
11
"tangled.org/core/appview/models"
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 {
···
43
return
44
}
45
var err error
46
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(star.RepoAt)))
47
if err != nil {
48
log.Printf("NewStar: failed to get repos: %v", err)
49
return
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,
···
123
}
124
125
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
126
+
issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt))
127
if err != nil {
128
log.Printf("NewIssueComment: failed to get issues: %v", err)
129
return
···
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
···
220
}
221
222
func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {
223
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt)))
224
if err != nil {
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)
···
270
return
271
}
272
273
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt))
274
if err != nil {
275
log.Printf("NewPullComment: failed to get repos: %v", err)
276
return
···
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"
···
381
382
func (n *databaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
383
// Get repo details
384
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt)))
385
if err != nil {
386
log.Printf("NewPullState: failed to get repos: %v", err)
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)
-1
appview/notify/merged_notifier.go
-1
appview/notify/merged_notifier.go
+3
-2
appview/oauth/handler.go
+3
-2
appview/oauth/handler.go
···
16
"tangled.org/core/api/tangled"
17
"tangled.org/core/appview/db"
18
"tangled.org/core/consts"
19
"tangled.org/core/tid"
20
)
21
···
97
// and create an sh.tangled.spindle.member record with that
98
spindleMembers, err := db.GetSpindleMembers(
99
o.Db,
100
-
db.FilterEq("instance", "spindle.tangled.sh"),
101
-
db.FilterEq("subject", did),
102
)
103
if err != nil {
104
l.Error("failed to get spindle members", "err", err)
···
16
"tangled.org/core/api/tangled"
17
"tangled.org/core/appview/db"
18
"tangled.org/core/consts"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/tid"
21
)
22
···
98
// and create an sh.tangled.spindle.member record with that
99
spindleMembers, err := db.GetSpindleMembers(
100
o.Db,
101
+
orm.FilterEq("instance", "spindle.tangled.sh"),
102
+
orm.FilterEq("subject", did),
103
)
104
if err != nil {
105
l.Error("failed to get spindle members", "err", err)
+7
-2
appview/pages/funcmap.go
+7
-2
appview/pages/funcmap.go
···
25
"github.com/dustin/go-humanize"
26
"github.com/go-enry/go-enry/v2"
27
"github.com/yuin/goldmark"
28
"tangled.org/core/appview/filetree"
29
"tangled.org/core/appview/models"
30
"tangled.org/core/appview/pages/markup"
···
162
}
163
return pairs, nil
164
},
165
-
"append": func(s []string, values ...string) []string {
166
s = append(s, values...)
167
return s
168
},
···
261
},
262
"description": func(text string) template.HTML {
263
p.rctx.RendererType = markup.RendererTypeDefault
264
-
htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New())
265
sanitized := p.rctx.SanitizeDescription(htmlString)
266
return template.HTML(sanitized)
267
},
···
25
"github.com/dustin/go-humanize"
26
"github.com/go-enry/go-enry/v2"
27
"github.com/yuin/goldmark"
28
+
emoji "github.com/yuin/goldmark-emoji"
29
"tangled.org/core/appview/filetree"
30
"tangled.org/core/appview/models"
31
"tangled.org/core/appview/pages/markup"
···
163
}
164
return pairs, nil
165
},
166
+
"append": func(s []any, values ...any) []any {
167
s = append(s, values...)
168
return s
169
},
···
262
},
263
"description": func(text string) template.HTML {
264
p.rctx.RendererType = markup.RendererTypeDefault
265
+
htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New(
266
+
goldmark.WithExtensions(
267
+
emoji.Emoji,
268
+
),
269
+
))
270
sanitized := p.rctx.SanitizeDescription(htmlString)
271
return template.HTML(sanitized)
272
},
+2
-2
appview/pages/markup/markdown.go
+2
-2
appview/pages/markup/markdown.go
···
12
13
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
14
"github.com/alecthomas/chroma/v2/styles"
15
-
treeblood "github.com/wyatt915/goldmark-treeblood"
16
"github.com/yuin/goldmark"
17
highlighting "github.com/yuin/goldmark-highlighting/v2"
18
"github.com/yuin/goldmark/ast"
19
"github.com/yuin/goldmark/extension"
···
65
extension.NewFootnote(
66
extension.WithFootnoteIDPrefix([]byte("footnote")),
67
),
68
-
treeblood.MathML(),
69
callout.CalloutExtention,
70
textension.AtExt,
71
),
72
goldmark.WithParserOptions(
73
parser.WithAutoHeadingID(),
···
12
13
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
14
"github.com/alecthomas/chroma/v2/styles"
15
"github.com/yuin/goldmark"
16
+
"github.com/yuin/goldmark-emoji"
17
highlighting "github.com/yuin/goldmark-highlighting/v2"
18
"github.com/yuin/goldmark/ast"
19
"github.com/yuin/goldmark/extension"
···
65
extension.NewFootnote(
66
extension.WithFootnoteIDPrefix([]byte("footnote")),
67
),
68
callout.CalloutExtention,
69
textension.AtExt,
70
+
emoji.Emoji,
71
),
72
goldmark.WithParserOptions(
73
parser.WithAutoHeadingID(),
+2
-3
appview/pages/pages.go
+2
-3
appview/pages/pages.go
···
31
"github.com/bluesky-social/indigo/atproto/identity"
32
"github.com/bluesky-social/indigo/atproto/syntax"
33
"github.com/go-git/go-git/v5/plumbing"
34
-
"github.com/go-git/go-git/v5/plumbing/object"
35
)
36
37
//go:embed templates/* static legal
···
641
}
642
643
func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error {
644
-
return p.executePlain("fragments/starBtn", w, params)
645
}
646
647
type RepoIndexParams struct {
···
649
RepoInfo repoinfo.RepoInfo
650
Active string
651
TagMap map[string][]string
652
-
CommitsTrunc []*object.Commit
653
TagsTrunc []*types.TagReference
654
BranchesTrunc []types.Branch
655
// ForkInfo *types.ForkInfo
···
31
"github.com/bluesky-social/indigo/atproto/identity"
32
"github.com/bluesky-social/indigo/atproto/syntax"
33
"github.com/go-git/go-git/v5/plumbing"
34
)
35
36
//go:embed templates/* static legal
···
640
}
641
642
func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error {
643
+
return p.executePlain("fragments/starBtn-oob", w, params)
644
}
645
646
type RepoIndexParams struct {
···
648
RepoInfo repoinfo.RepoInfo
649
Active string
650
TagMap map[string][]string
651
+
CommitsTrunc []types.Commit
652
TagsTrunc []*types.TagReference
653
BranchesTrunc []types.Branch
654
// ForkInfo *types.ForkInfo
-46
appview/pages/repoinfo/repoinfo.go
-46
appview/pages/repoinfo/repoinfo.go
···
1
package repoinfo
2
3
import (
4
-
"encoding/json"
5
"fmt"
6
"path"
7
"slices"
···
118
func (r RolesInRepo) IsPushAllowed() bool {
119
return slices.Contains(r.Roles, "repo:push")
120
}
121
-
122
-
// PrimaryLanguage returns the first (most used) language from a list, or empty string if none
123
-
func PrimaryLanguage(languages []interface{}) string {
124
-
if len(languages) == 0 {
125
-
return ""
126
-
}
127
-
128
-
// Languages are already sorted by percentage in descending order
129
-
// Just get the first one
130
-
if firstLang, ok := languages[0].(map[string]interface{}); ok {
131
-
if name, ok := firstLang["Name"].(string); ok {
132
-
return name
133
-
}
134
-
}
135
-
136
-
return ""
137
-
}
138
-
139
-
// StructuredData generates Schema.org JSON-LD structured data for the repository
140
-
func (r RepoInfo) StructuredData(primaryLanguage string) string {
141
-
data := map[string]interface{}{
142
-
"@context": "https://schema.org",
143
-
"@type": "SoftwareSourceCode",
144
-
"name": r.Name,
145
-
"description": r.Description,
146
-
"codeRepository": "https://tangled.org/" + r.FullName(),
147
-
"url": "https://tangled.org/" + r.FullName(),
148
-
"author": map[string]interface{}{
149
-
"@type": "Person",
150
-
"name": r.owner(),
151
-
"url": "https://tangled.org/" + r.owner(),
152
-
},
153
-
}
154
-
155
-
// Add programming language if available
156
-
if primaryLanguage != "" {
157
-
data["programmingLanguage"] = primaryLanguage
158
-
}
159
-
160
-
jsonBytes, err := json.Marshal(data)
161
-
if err != nil {
162
-
return "{}"
163
-
}
164
-
return string(jsonBytes)
165
-
}
-44
appview/pages/templates/fragments/dolly/logo.svg
-44
appview/pages/templates/fragments/dolly/logo.svg
···
1
-
<svg
2
-
version="1.1"
3
-
id="svg1"
4
-
width="25"
5
-
height="25"
6
-
viewBox="0 0 25 25"
7
-
sodipodi:docname="tangled_dolly_face_only.png"
8
-
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
9
-
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
10
-
xmlns:xlink="http://www.w3.org/1999/xlink"
11
-
xmlns="http://www.w3.org/2000/svg"
12
-
xmlns:svg="http://www.w3.org/2000/svg">
13
-
<title>Dolly</title>
14
-
<defs
15
-
id="defs1" />
16
-
<sodipodi:namedview
17
-
id="namedview1"
18
-
pagecolor="#ffffff"
19
-
bordercolor="#000000"
20
-
borderopacity="0.25"
21
-
inkscape:showpageshadow="2"
22
-
inkscape:pageopacity="0.0"
23
-
inkscape:pagecheckerboard="true"
24
-
inkscape:deskcolor="#d5d5d5">
25
-
<inkscape:page
26
-
x="0"
27
-
y="0"
28
-
width="25"
29
-
height="25"
30
-
id="page2"
31
-
margin="0"
32
-
bleed="0" />
33
-
</sodipodi:namedview>
34
-
<g
35
-
inkscape:groupmode="layer"
36
-
inkscape:label="Image"
37
-
id="g1">
38
-
<path
39
-
fill="currentColor"
40
-
style="stroke-width:0.111183"
41
-
d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z"
42
-
id="path4" />
43
-
</g>
44
-
</svg>
···
+5
appview/pages/templates/fragments/starBtn-oob.html
+5
appview/pages/templates/fragments/starBtn-oob.html
+1
-3
appview/pages/templates/fragments/starBtn.html
+1
-3
appview/pages/templates/fragments/starBtn.html
···
1
{{ define "fragments/starBtn" }}
2
<button
3
id="starBtn"
4
class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group"
···
10
{{ end }}
11
12
hx-trigger="click"
13
-
hx-target="this"
14
-
hx-swap="outerHTML"
15
-
hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'
16
hx-disabled-elt="#starBtn"
17
>
18
{{ if .IsStarred }}
···
1
{{ define "fragments/starBtn" }}
2
+
{{/* NOTE: this fragment is always replaced with hx-swap-oob */}}
3
<button
4
id="starBtn"
5
class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group"
···
11
{{ end }}
12
13
hx-trigger="click"
14
hx-disabled-elt="#starBtn"
15
>
16
{{ if .IsStarred }}
+22
appview/pages/templates/fragments/tinyAvatarList.html
+22
appview/pages/templates/fragments/tinyAvatarList.html
···
···
1
+
{{ define "fragments/tinyAvatarList" }}
2
+
{{ $all := .all }}
3
+
{{ $classes := .classes }}
4
+
{{ $ps := take $all 5 }}
5
+
<div class="inline-flex items-center -space-x-3">
6
+
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
7
+
{{ range $i, $p := $ps }}
8
+
<img
9
+
src="{{ tinyAvatar . }}"
10
+
alt=""
11
+
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0 {{ $classes }}"
12
+
/>
13
+
{{ end }}
14
+
15
+
{{ if gt (len $all) 5 }}
16
+
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
17
+
+{{ sub (len $all) 5 }}
18
+
</span>
19
+
{{ end }}
20
+
</div>
21
+
{{ end }}
22
+
+2
-27
appview/pages/templates/goodfirstissues/index.html
+2
-27
appview/pages/templates/goodfirstissues/index.html
···
1
{{ define "title" }}good first issues{{ end }}
2
3
{{ define "extrameta" }}
4
-
<meta name="description" content="Discover beginner-friendly good first issues across open source projects on Tangled. Perfect for new contributors looking to get started with open source development." />
5
-
<meta name="keywords" content="good first issues, beginner issues, open source contribution, first time contributor, beginner friendly, open source projects" />
6
-
7
<meta property="og:title" content="good first issues ยท tangled" />
8
-
<meta property="og:type" content="website" />
9
<meta property="og:url" content="https://tangled.org/goodfirstissues" />
10
-
<meta property="og:description" content="Find beginner-friendly issues across all repositories to get started with open source contributions on Tangled." />
11
-
12
-
<meta name="twitter:card" content="summary" />
13
-
<meta name="twitter:title" content="good first issues ยท tangled" />
14
-
<meta name="twitter:description" content="Find beginner-friendly issues to get started with open source contributions." />
15
-
16
-
<!-- structured data for good first issues page -->
17
-
<script type="application/ld+json">
18
-
{
19
-
"@context": "https://schema.org",
20
-
"@type": "CollectionPage",
21
-
"name": "Good First Issues",
22
-
"description": "A curated collection of beginner-friendly issues across open source projects",
23
-
"url": "https://tangled.org/goodfirstissues",
24
-
"isPartOf": {
25
-
"@type": "WebSite",
26
-
"name": "Tangled",
27
-
"url": "https://tangled.org"
28
-
}
29
-
}
30
-
</script>
31
{{ end }}
32
-
33
-
{{ define "canonical" }}https://tangled.org/goodfirstissues{{ end }}
34
35
{{ define "content" }}
36
<div class="grid grid-cols-10">
···
1
{{ define "title" }}good first issues{{ end }}
2
3
{{ define "extrameta" }}
4
<meta property="og:title" content="good first issues ยท tangled" />
5
+
<meta property="og:type" content="object" />
6
<meta property="og:url" content="https://tangled.org/goodfirstissues" />
7
+
<meta property="og:description" content="Find good first issues to contribute to open source projects" />
8
{{ end }}
9
10
{{ define "content" }}
11
<div class="grid grid-cols-10">
+1
-1
appview/pages/templates/knots/index.html
+1
-1
appview/pages/templates/knots/index.html
+2
-26
appview/pages/templates/layouts/base.html
+2
-26
appview/pages/templates/layouts/base.html
···
4
<head>
5
<meta charset="UTF-8" />
6
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
7
-
<meta name="description" content="tightly-knit social coding"/>
8
-
<meta name="keywords" content="git hosting, social coding, version control, pull requests, CI/CD, code collaboration, open source, decentralized"/>
9
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
10
-
<meta name="author" content="Tangled"/>
11
-
12
-
<!-- Canonical URL -->
13
-
<link rel="canonical" href="{{ block "canonical" . }}https://tangled.org{{ .Request.URL.Path }}{{ end }}" />
14
15
<script defer src="/static/htmx.min.js"></script>
16
<script defer src="/static/htmx-ext-ws.min.js"></script>
···
20
<link rel="preconnect" href="https://avatar.tangled.sh" />
21
<link rel="preconnect" href="https://camo.tangled.sh" />
22
23
-
<!-- RSS Feed Discovery -->
24
-
{{ block "rss" . }}{{ end }}
25
-
26
<!-- pwa manifest -->
27
<link rel="manifest" href="/pwa-manifest.json" />
28
···
30
<link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin />
31
32
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
33
-
<title>{{ block "title" . }}{{ end }}</title>
34
-
35
-
<!-- Structured Data -->
36
-
{{ block "structuredData" . }}
37
-
<script type="application/ld+json">
38
-
{
39
-
"@context": "https://schema.org",
40
-
"@type": "Organization",
41
-
"name": "Tangled",
42
-
"url": "https://tangled.org",
43
-
"logo": "https://tangled.org/favicon.svg",
44
-
"description": "tightly-knit social coding",
45
-
"sameAs": []
46
-
}
47
-
</script>
48
-
{{ end }}
49
-
50
{{ block "extrameta" . }}{{ end }}
51
</head>
52
<body class="min-h-screen flex flex-col gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200">
···
4
<head>
5
<meta charset="UTF-8" />
6
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
7
+
<meta name="description" content="Social coding, but for real this time!"/>
8
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
9
10
<script defer src="/static/htmx.min.js"></script>
11
<script defer src="/static/htmx-ext-ws.min.js"></script>
···
15
<link rel="preconnect" href="https://avatar.tangled.sh" />
16
<link rel="preconnect" href="https://camo.tangled.sh" />
17
18
<!-- pwa manifest -->
19
<link rel="manifest" href="/pwa-manifest.json" />
20
···
22
<link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin />
23
24
<link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" />
25
+
<title>{{ block "title" . }}{{ end }} ยท tangled</title>
26
{{ block "extrameta" . }}{{ end }}
27
</head>
28
<body class="min-h-screen flex flex-col gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200">
+1
-20
appview/pages/templates/layouts/profilebase.html
+1
-20
appview/pages/templates/layouts/profilebase.html
···
10
<meta property="og:image" content="{{ $avatarUrl }}" />
11
<meta property="og:image:width" content="512" />
12
<meta property="og:image:height" content="512" />
13
-
14
<meta name="twitter:card" content="summary" />
15
<meta name="twitter:title" content="{{ $handle }}" />
16
<meta name="twitter:description" content="{{ or .Card.Profile.Description $handle }}" />
17
<meta name="twitter:image" content="{{ $avatarUrl }}" />
18
-
19
-
<!-- structured data for user profile -->
20
-
<script type="application/ld+json">
21
-
{
22
-
"@context": "https://schema.org",
23
-
"@type": "Person",
24
-
"name": "{{ or .Card.Profile.DisplayName .Card.UserHandle .Card.UserDid }}",
25
-
"url": "https://tangled.org/{{ or .Card.UserHandle .Card.UserDid }}",
26
-
"image": "{{ $avatarUrl }}",
27
-
"description": "{{ .Card.Profile.Description }}"{{ if .Card.UserHandle }},
28
-
"identifier": "{{ .Card.UserHandle }}"{{ end }}
29
-
}
30
-
</script>
31
-
{{ end }}
32
-
33
-
{{ define "canonical" }}https://tangled.org/{{ or .Card.UserHandle .Card.UserDid }}{{ end }}
34
-
35
-
{{ define "rss" }}
36
-
<link rel="alternate" type="application/atom+xml" title="{{ or .Card.UserHandle .Card.UserDid }} Activity Feed" href="https://tangled.org/{{ or .Card.UserHandle .Card.UserDid }}/feed.atom" />
37
{{ end }}
38
39
{{ define "content" }}
···
10
<meta property="og:image" content="{{ $avatarUrl }}" />
11
<meta property="og:image:width" content="512" />
12
<meta property="og:image:height" content="512" />
13
+
14
<meta name="twitter:card" content="summary" />
15
<meta name="twitter:title" content="{{ $handle }}" />
16
<meta name="twitter:description" content="{{ or .Card.Profile.Description $handle }}" />
17
<meta name="twitter:image" content="{{ $avatarUrl }}" />
18
{{ end }}
19
20
{{ define "content" }}
+34
-9
appview/pages/templates/repo/commit.html
+34
-9
appview/pages/templates/repo/commit.html
···
25
</div>
26
27
<div class="flex flex-wrap items-center space-x-2">
28
-
<p class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-300">
29
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
30
-
31
-
{{ if $did }}
32
-
{{ template "user/fragments/picHandleLink" $did }}
33
-
{{ else }}
34
-
<a href="mailto:{{ $commit.Author.Email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $commit.Author.Name }}</a>
35
-
{{ end }}
36
37
<span class="px-1 select-none before:content-['\00B7']"></span>
38
-
{{ template "repo/fragments/time" $commit.Author.When }}
39
<span class="px-1 select-none before:content-['\00B7']"></span>
40
41
<a href="/{{ $repo }}/commit/{{ $commit.This }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ slice $commit.This 0 8 }}</a>
···
78
79
</section>
80
{{end}}
81
82
{{ define "topbarLayout" }}
83
<header class="col-span-full" style="z-index: 20;">
···
25
</div>
26
27
<div class="flex flex-wrap items-center space-x-2">
28
+
<p class="flex flex-wrap items-center gap-1 text-sm text-gray-500 dark:text-gray-300">
29
+
{{ template "attribution" . }}
30
31
<span class="px-1 select-none before:content-['\00B7']"></span>
32
+
{{ template "repo/fragments/time" $commit.Committer.When }}
33
<span class="px-1 select-none before:content-['\00B7']"></span>
34
35
<a href="/{{ $repo }}/commit/{{ $commit.This }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ slice $commit.This 0 8 }}</a>
···
72
73
</section>
74
{{end}}
75
+
76
+
{{ define "attribution" }}
77
+
{{ $commit := .Diff.Commit }}
78
+
{{ $showCommitter := true }}
79
+
{{ if eq $commit.Author.Email $commit.Committer.Email }}
80
+
{{ $showCommitter = false }}
81
+
{{ end }}
82
+
83
+
{{ if $showCommitter }}
84
+
authored by {{ template "attributedUser" (list $commit.Author.Email $commit.Author.Name $.EmailToDid) }}
85
+
{{ range $commit.CoAuthors }}
86
+
{{ template "attributedUser" (list .Email .Name $.EmailToDid) }}
87
+
{{ end }}
88
+
and committed by {{ template "attributedUser" (list $commit.Committer.Email $commit.Committer.Name $.EmailToDid) }}
89
+
{{ else }}
90
+
{{ template "attributedUser" (list $commit.Author.Email $commit.Author.Name $.EmailToDid )}}
91
+
{{ end }}
92
+
{{ end }}
93
+
94
+
{{ define "attributedUser" }}
95
+
{{ $email := index . 0 }}
96
+
{{ $name := index . 1 }}
97
+
{{ $map := index . 2 }}
98
+
{{ $did := index $map $email }}
99
+
100
+
{{ if $did }}
101
+
{{ template "user/fragments/picHandleLink" $did }}
102
+
{{ else }}
103
+
<a href="mailto:{{ $email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $name }}</a>
104
+
{{ end }}
105
+
{{ end }}
106
107
{{ define "topbarLayout" }}
108
<header class="col-span-full" style="z-index: 20;">
+6
-6
appview/pages/templates/repo/fragments/backlinks.html
+6
-6
appview/pages/templates/repo/fragments/backlinks.html
···
14
<div class="flex gap-2 items-center">
15
{{ if .State.IsClosed }}
16
<span class="text-gray-500 dark:text-gray-400">
17
-
{{ i "ban" "w-4 h-4" }}
18
</span>
19
{{ else if eq .Kind.String "issues" }}
20
<span class="text-green-600 dark:text-green-500">
21
-
{{ i "circle-dot" "w-4 h-4" }}
22
</span>
23
{{ else if .State.IsOpen }}
24
<span class="text-green-600 dark:text-green-500">
25
-
{{ i "git-pull-request" "w-4 h-4" }}
26
</span>
27
{{ else if .State.IsMerged }}
28
<span class="text-purple-600 dark:text-purple-500">
29
-
{{ i "git-merge" "w-4 h-4" }}
30
</span>
31
{{ else }}
32
<span class="text-gray-600 dark:text-gray-300">
33
-
{{ i "git-pull-request-closed" "w-4 h-4" }}
34
</span>
35
{{ end }}
36
-
<a href="{{ . }}"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a>
37
</div>
38
{{ if not (eq $.RepoInfo.FullName $repoUrl) }}
39
<div>
···
14
<div class="flex gap-2 items-center">
15
{{ if .State.IsClosed }}
16
<span class="text-gray-500 dark:text-gray-400">
17
+
{{ i "ban" "size-3" }}
18
</span>
19
{{ else if eq .Kind.String "issues" }}
20
<span class="text-green-600 dark:text-green-500">
21
+
{{ i "circle-dot" "size-3" }}
22
</span>
23
{{ else if .State.IsOpen }}
24
<span class="text-green-600 dark:text-green-500">
25
+
{{ i "git-pull-request" "size-3" }}
26
</span>
27
{{ else if .State.IsMerged }}
28
<span class="text-purple-600 dark:text-purple-500">
29
+
{{ i "git-merge" "size-3" }}
30
</span>
31
{{ else }}
32
<span class="text-gray-600 dark:text-gray-300">
33
+
{{ i "git-pull-request-closed" "size-3" }}
34
</span>
35
{{ end }}
36
+
<a href="{{ . }}" class="line-clamp-1 text-sm"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a>
37
</div>
38
{{ if not (eq $.RepoInfo.FullName $repoUrl) }}
39
<div>
+1
-16
appview/pages/templates/repo/fragments/participants.html
+1
-16
appview/pages/templates/repo/fragments/participants.html
···
6
<span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span>
7
<span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span>
8
</div>
9
-
<div class="flex items-center -space-x-3 mt-2">
10
-
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
11
-
{{ range $i, $p := $ps }}
12
-
<img
13
-
src="{{ tinyAvatar . }}"
14
-
alt=""
15
-
class="rounded-full h-8 w-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0"
16
-
/>
17
-
{{ end }}
18
-
19
-
{{ if gt (len $all) 5 }}
20
-
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
21
-
+{{ sub (len $all) 5 }}
22
-
</span>
23
-
{{ end }}
24
-
</div>
25
</div>
26
{{ end }}
···
6
<span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span>
7
<span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span>
8
</div>
9
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "w-8 h-8") }}
10
</div>
11
{{ end }}
+30
-44
appview/pages/templates/repo/index.html
+30
-44
appview/pages/templates/repo/index.html
···
5
{{ template "repo/fragments/meta" . }}
6
7
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo) }}
8
-
9
-
<!-- Structured Data for Repository -->
10
-
<script type="application/ld+json">
11
-
{
12
-
"@context": "https://schema.org",
13
-
"@type": "SoftwareSourceCode",
14
-
"name": "{{ .RepoInfo.Name }}",
15
-
"description": "{{ .RepoInfo.Description }}",
16
-
"codeRepository": "https://tangled.org/{{ .RepoInfo.FullName }}",
17
-
"programmingLanguage": {{ if .Languages }}{{ range $idx, $lang := .Languages }}{{ if eq $idx 0 }}"{{ $lang.Name }}"{{ end }}{{ end }}{{ else }}"Unknown"{{ end }},
18
-
"url": "https://tangled.org/{{ .RepoInfo.FullName }}",
19
-
"author": {
20
-
"@type": "Person",
21
-
"name": "{{ .RepoInfo.OwnerWithAt }}",
22
-
"url": "https://tangled.org/{{ .RepoInfo.OwnerWithAt }}"
23
-
}{{ if .RepoInfo.Source }},
24
-
"isBasedOn": {
25
-
"@type": "SoftwareSourceCode",
26
-
"name": "{{ .RepoInfo.Source.Name }}",
27
-
"url": "https://tangled.org/{{ didOrHandle .RepoInfo.Source.Did .RepoInfo.SourceHandle }}/{{ .RepoInfo.Source.Name }}"
28
-
}{{ end }}
29
-
}
30
-
</script>
31
-
32
-
<!-- Breadcrumb Navigation -->
33
-
{{ template "fragments/breadcrumb" (list
34
-
(list "Home" "https://tangled.org")
35
-
(list .RepoInfo.OwnerWithAt (printf "https://tangled.org/%s" .RepoInfo.OwnerWithAt))
36
-
(list .RepoInfo.Name (printf "https://tangled.org/%s" .RepoInfo.FullName))
37
-
) }}
38
-
{{ end }}
39
-
40
-
{{ define "canonical" }}https://tangled.org/{{ .RepoInfo.FullName }}{{ end }}
41
-
42
-
{{ define "rss" }}
43
-
<link rel="alternate" type="application/atom+xml" title="{{ .RepoInfo.FullName }} Activity Feed" href="https://tangled.org/{{ .RepoInfo.FullName }}/feed.atom" />
44
{{ end }}
45
46
{{ define "repoContent" }}
···
50
{{ end }}
51
<div class="flex items-center justify-between pb-5">
52
{{ block "branchSelector" . }}{{ end }}
53
-
<div class="flex md:hidden items-center gap-2">
54
<a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold">
55
{{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }}
56
</a>
···
102
103
{{ define "branchSelector" }}
104
<div class="flex gap-2 items-center justify-between w-full">
105
-
<div class="flex gap-2 items-center">
106
<select
107
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)"
108
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
···
264
<span
265
class="mx-1 before:content-['ยท'] before:select-none"
266
></span>
267
-
<span>
268
-
{{ $did := index $.EmailToDid .Author.Email }}
269
-
<a href="{{ if $did }}/{{ resolve $did }}{{ else }}mailto:{{ .Author.Email }}{{ end }}"
270
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
271
-
>{{ if $did }}{{ template "user/fragments/picHandleLink" $did }}{{ else }}{{ .Author.Name }}{{ end }}</a>
272
-
</span>
273
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
274
{{ template "repo/fragments/time" .Committer.When }}
275
···
295
{{ end }}
296
</div>
297
</div>
298
{{ end }}
299
300
{{ define "branchList" }}
···
5
{{ template "repo/fragments/meta" . }}
6
7
{{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo) }}
8
{{ end }}
9
10
{{ define "repoContent" }}
···
14
{{ end }}
15
<div class="flex items-center justify-between pb-5">
16
{{ block "branchSelector" . }}{{ end }}
17
+
<div class="flex md:hidden items-center gap-3">
18
<a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold">
19
{{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }}
20
</a>
···
66
67
{{ define "branchSelector" }}
68
<div class="flex gap-2 items-center justify-between w-full">
69
+
<div class="flex gap-2 items-stretch">
70
<select
71
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)"
72
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
···
228
<span
229
class="mx-1 before:content-['ยท'] before:select-none"
230
></span>
231
+
{{ template "attribution" (list . $.EmailToDid) }}
232
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
233
{{ template "repo/fragments/time" .Committer.When }}
234
···
254
{{ end }}
255
</div>
256
</div>
257
+
{{ end }}
258
+
259
+
{{ define "attribution" }}
260
+
{{ $commit := index . 0 }}
261
+
{{ $map := index . 1 }}
262
+
<span class="flex items-center">
263
+
{{ $author := index $map $commit.Author.Email }}
264
+
{{ $coauthors := $commit.CoAuthors }}
265
+
{{ $all := list }}
266
+
267
+
{{ if $author }}
268
+
{{ $all = append $all $author }}
269
+
{{ end }}
270
+
{{ range $coauthors }}
271
+
{{ $co := index $map .Email }}
272
+
{{ if $co }}
273
+
{{ $all = append $all $co }}
274
+
{{ end }}
275
+
{{ end }}
276
+
277
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }}
278
+
<a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
279
+
class="no-underline hover:underline">
280
+
{{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
281
+
{{ if $coauthors }} +{{ length $coauthors }}{{ end }}
282
+
</a>
283
+
</span>
284
{{ end }}
285
286
{{ define "branchList" }}
+19
appview/pages/templates/repo/issues/fragments/og.html
+19
appview/pages/templates/repo/issues/fragments/og.html
···
···
1
+
{{ define "repo/issues/fragments/og" }}
2
+
{{ $title := printf "%s #%d" .Issue.Title .Issue.IssueId }}
3
+
{{ $description := or .Issue.Body .RepoInfo.Description }}
4
+
{{ $url := printf "https://tangled.org/%s/issues/%d" .RepoInfo.FullName .Issue.IssueId }}
5
+
{{ $imageUrl := printf "https://tangled.org/%s/issues/%d/opengraph" .RepoInfo.FullName .Issue.IssueId }}
6
+
7
+
<meta property="og:title" content="{{ unescapeHtml $title }}" />
8
+
<meta property="og:type" content="object" />
9
+
<meta property="og:url" content="{{ $url }}" />
10
+
<meta property="og:description" content="{{ $description }}" />
11
+
<meta property="og:image" content="{{ $imageUrl }}" />
12
+
<meta property="og:image:width" content="1200" />
13
+
<meta property="og:image:height" content="600" />
14
+
15
+
<meta name="twitter:card" content="summary_large_image" />
16
+
<meta name="twitter:title" content="{{ unescapeHtml $title }}" />
17
+
<meta name="twitter:description" content="{{ $description }}" />
18
+
<meta name="twitter:image" content="{{ $imageUrl }}" />
19
+
{{ end }}
+40
-23
appview/pages/templates/repo/log.html
+40
-23
appview/pages/templates/repo/log.html
···
17
<div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700">
18
{{ $grid := "grid grid-cols-14 gap-4" }}
19
<div class="{{ $grid }}">
20
-
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2">Author</div>
21
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Commit</div>
22
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-6">Message</div>
23
-
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-1"></div>
24
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2 justify-self-end">Date</div>
25
</div>
26
{{ range $index, $commit := .Commits }}
27
{{ $messageParts := splitN $commit.Message "\n\n" 2 }}
28
<div class="{{ $grid }} py-3">
29
-
<div class="align-top truncate col-span-2">
30
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
31
-
{{ if $did }}
32
-
{{ template "user/fragments/picHandleLink" $did }}
33
-
{{ else }}
34
-
<a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a>
35
-
{{ end }}
36
</div>
37
<div class="align-top font-mono flex items-start col-span-3">
38
{{ $verified := $.VerifiedCommits.IsVerified $commit.Hash.String }}
···
61
<div class="align-top col-span-6">
62
<div>
63
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a>
64
{{ if gt (len $messageParts) 1 }}
65
<button class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600 rounded" hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')">{{ i "ellipsis" "w-3 h-3" }}</button>
66
{{ end }}
···
72
</span>
73
{{ end }}
74
{{ end }}
75
</div>
76
77
{{ if gt (len $messageParts) 1 }}
78
<p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p>
79
{{ end }}
80
-
</div>
81
-
<div class="align-top col-span-1">
82
-
<!-- ci status -->
83
-
{{ $pipeline := index $.Pipelines .Hash.String }}
84
-
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
85
-
{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }}
86
-
{{ end }}
87
</div>
88
<div class="align-top justify-self-end text-gray-500 dark:text-gray-400 col-span-2">{{ template "repo/fragments/shortTimeAgo" $commit.Committer.When }}</div>
89
</div>
···
152
</a>
153
</span>
154
<span class="mx-2 before:content-['ยท'] before:select-none"></span>
155
-
<span>
156
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
157
-
<a href="{{ if $did }}/{{ $did }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
158
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline">
159
-
{{ if $did }}{{ template "user/fragments/picHandleLink" $did }}{{ else }}{{ $commit.Author.Name }}{{ end }}
160
-
</a>
161
-
</span>
162
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
163
<span>{{ template "repo/fragments/shortTime" $commit.Committer.When }}</span>
164
···
176
</div>
177
</section>
178
179
{{ end }}
180
181
{{ define "repoAfter" }}
···
17
<div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700">
18
{{ $grid := "grid grid-cols-14 gap-4" }}
19
<div class="{{ $grid }}">
20
+
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Author</div>
21
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Commit</div>
22
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-6">Message</div>
23
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2 justify-self-end">Date</div>
24
</div>
25
{{ range $index, $commit := .Commits }}
26
{{ $messageParts := splitN $commit.Message "\n\n" 2 }}
27
<div class="{{ $grid }} py-3">
28
+
<div class="align-top col-span-3">
29
+
{{ template "attribution" (list $commit $.EmailToDid) }}
30
</div>
31
<div class="align-top font-mono flex items-start col-span-3">
32
{{ $verified := $.VerifiedCommits.IsVerified $commit.Hash.String }}
···
55
<div class="align-top col-span-6">
56
<div>
57
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a>
58
+
59
{{ if gt (len $messageParts) 1 }}
60
<button class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600 rounded" hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')">{{ i "ellipsis" "w-3 h-3" }}</button>
61
{{ end }}
···
67
</span>
68
{{ end }}
69
{{ end }}
70
+
71
+
<!-- ci status -->
72
+
<span class="text-xs">
73
+
{{ $pipeline := index $.Pipelines .Hash.String }}
74
+
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
75
+
{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }}
76
+
{{ end }}
77
+
</span>
78
</div>
79
80
{{ if gt (len $messageParts) 1 }}
81
<p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p>
82
{{ end }}
83
</div>
84
<div class="align-top justify-self-end text-gray-500 dark:text-gray-400 col-span-2">{{ template "repo/fragments/shortTimeAgo" $commit.Committer.When }}</div>
85
</div>
···
148
</a>
149
</span>
150
<span class="mx-2 before:content-['ยท'] before:select-none"></span>
151
+
{{ template "attribution" (list $commit $.EmailToDid) }}
152
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
153
<span>{{ template "repo/fragments/shortTime" $commit.Committer.When }}</span>
154
···
166
</div>
167
</section>
168
169
+
{{ end }}
170
+
171
+
{{ define "attribution" }}
172
+
{{ $commit := index . 0 }}
173
+
{{ $map := index . 1 }}
174
+
<span class="flex items-center gap-1">
175
+
{{ $author := index $map $commit.Author.Email }}
176
+
{{ $coauthors := $commit.CoAuthors }}
177
+
{{ $all := list }}
178
+
179
+
{{ if $author }}
180
+
{{ $all = append $all $author }}
181
+
{{ end }}
182
+
{{ range $coauthors }}
183
+
{{ $co := index $map .Email }}
184
+
{{ if $co }}
185
+
{{ $all = append $all $co }}
186
+
{{ end }}
187
+
{{ end }}
188
+
189
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }}
190
+
<a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
191
+
class="no-underline hover:underline">
192
+
{{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
193
+
{{ if $coauthors }} +{{ length $coauthors }}{{ end }}
194
+
</a>
195
+
</span>
196
{{ end }}
197
198
{{ define "repoAfter" }}
+16
-16
appview/pages/templates/repo/pulls/fragments/og.html
+16
-16
appview/pages/templates/repo/pulls/fragments/og.html
···
1
-
{{ define "pulls/fragments/og" }}
2
-
{{ $title := printf "%s #%d" .Pull.Title .Pull.PullId }}
3
-
{{ $description := or .Pull.Body .RepoInfo.Description }}
4
-
{{ $url := printf "https://tangled.org/%s/pulls/%d" .RepoInfo.FullName .Pull.PullId }}
5
-
{{ $imageUrl := printf "https://tangled.org/%s/pulls/%d/opengraph" .RepoInfo.FullName .Pull.PullId }}
6
7
-
<meta property="og:title" content="{{ unescapeHtml $title }}" />
8
-
<meta property="og:type" content="object" />
9
-
<meta property="og:url" content="{{ $url }}" />
10
-
<meta property="og:description" content="{{ $description }}" />
11
-
<meta property="og:image" content="{{ $imageUrl }}" />
12
-
<meta property="og:image:width" content="1200" />
13
-
<meta property="og:image:height" content="600" />
14
15
-
<meta name="twitter:card" content="summary_large_image" />
16
-
<meta name="twitter:title" content="{{ unescapeHtml $title }}" />
17
-
<meta name="twitter:description" content="{{ $description }}" />
18
-
<meta name="twitter:image" content="{{ $imageUrl }}" />
19
{{ end }}
···
1
+
{{ define "repo/pulls/fragments/og" }}
2
+
{{ $title := printf "%s #%d" .Pull.Title .Pull.PullId }}
3
+
{{ $description := or .Pull.Body .RepoInfo.Description }}
4
+
{{ $url := printf "https://tangled.org/%s/pulls/%d" .RepoInfo.FullName .Pull.PullId }}
5
+
{{ $imageUrl := printf "https://tangled.org/%s/pulls/%d/opengraph" .RepoInfo.FullName .Pull.PullId }}
6
7
+
<meta property="og:title" content="{{ unescapeHtml $title }}" />
8
+
<meta property="og:type" content="object" />
9
+
<meta property="og:url" content="{{ $url }}" />
10
+
<meta property="og:description" content="{{ $description }}" />
11
+
<meta property="og:image" content="{{ $imageUrl }}" />
12
+
<meta property="og:image:width" content="1200" />
13
+
<meta property="og:image:height" content="600" />
14
15
+
<meta name="twitter:card" content="summary_large_image" />
16
+
<meta name="twitter:title" content="{{ unescapeHtml $title }}" />
17
+
<meta name="twitter:description" content="{{ $description }}" />
18
+
<meta name="twitter:image" content="{{ $imageUrl }}" />
19
{{ end }}
+1
-1
appview/pages/templates/strings/string.html
+1
-1
appview/pages/templates/strings/string.html
···
17
<span class="select-none">/</span>
18
<a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a>
19
</div>
20
-
<div class="flex gap-2 text-base">
21
{{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }}
22
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
23
hx-boost="true"
···
17
<span class="select-none">/</span>
18
<a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a>
19
</div>
20
+
<div class="flex gap-2 items-stretch text-base">
21
{{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }}
22
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
23
hx-boost="true"
+3
-49
appview/pages/templates/timeline/home.html
+3
-49
appview/pages/templates/timeline/home.html
···
1
{{ define "title" }}tangled · tightly-knit social coding{{ end }}
2
3
{{ define "extrameta" }}
4
-
{{ $desc := "Collaborate on code with decentralized git hosting, modern contribution and review workflows, and lightweight CI/CD pipelines." }}
5
-
{{ $title = "tangled ยท tightly-knit social coding" }}
6
-
7
-
<meta name="description" content="{{ $desc }}" />
8
-
<meta property="og:title" content="{{ $title }}" />
9
-
<meta property="og:type" content="website" />
10
<meta property="og:url" content="https://tangled.org" />
11
-
<meta property="og:description" content="Decentralized git hosting with improved pull requests and lightweight CI/CD. Host repositories on your own infrastructure." />
12
-
<meta property="og:image" content="https://assets.tangled.network/tangled_og.png" />
13
-
<meta property="og:image:width" content="1200" />
14
-
<meta property="og:image:height" content="630" />
15
-
16
-
<meta name="twitter:card" content="summary_large_image" />
17
-
<meta name="twitter:title" content="{{ $title }}" />
18
-
<meta name="twitter:description" content="{{ $desc }}" />
19
-
<meta name="twitter:image" content="https://assets.tangled.network/tangled_og.png" />
20
-
21
-
<!-- Enhanced Structured Data for Homepage -->
22
-
<script type="application/ld+json">
23
-
{
24
-
"@context": "https://schema.org",
25
-
"@type": "WebSite",
26
-
"name": "Tangled",
27
-
"alternateName": "Tangled",
28
-
"url": "https://tangled.org",
29
-
"description": "{{ $desc }}",
30
-
"potentialAction": {
31
-
"@type": "SearchAction",
32
-
"target": "https://tangled.org/?q={search_term_string}",
33
-
"query-input": "required name=search_term_string"
34
-
}
35
-
}
36
-
</script>
37
-
<script type="application/ld+json">
38
-
{
39
-
"@context": "https://schema.org",
40
-
"@type": "SoftwareApplication",
41
-
"name": "Tangled",
42
-
"applicationCategory": "DeveloperTool",
43
-
"offers": {
44
-
"@type": "Offer",
45
-
"price": "0",
46
-
"priceCurrency": "USD"
47
-
},
48
-
"operatingSystem": "Web",
49
-
"description": "{{ $desc }}"
50
-
}
51
-
</script>
52
{{ end }}
53
-
54
-
{{ define "canonical" }}https://tangled.org{{ end }}
55
56
57
{{ define "content" }}
···
1
{{ define "title" }}tangled · tightly-knit social coding{{ end }}
2
3
{{ define "extrameta" }}
4
+
<meta property="og:title" content="timeline ยท tangled" />
5
+
<meta property="og:type" content="object" />
6
<meta property="og:url" content="https://tangled.org" />
7
+
<meta property="og:description" content="tightly-knit social coding" />
8
{{ end }}
9
10
11
{{ define "content" }}
+9
-6
appview/pages/templates/user/signup.html
+9
-6
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 }}"></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>
···
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>
+12
-11
appview/pipelines/pipelines.go
+12
-11
appview/pipelines/pipelines.go
···
16
"tangled.org/core/appview/reporesolver"
17
"tangled.org/core/eventconsumer"
18
"tangled.org/core/idresolver"
19
"tangled.org/core/rbac"
20
spindlemodel "tangled.org/core/spindle/models"
21
···
81
ps, err := db.GetPipelineStatuses(
82
p.db,
83
30,
84
-
db.FilterEq("repo_owner", f.Did),
85
-
db.FilterEq("repo_name", f.Name),
86
-
db.FilterEq("knot", f.Knot),
87
)
88
if err != nil {
89
l.Error("failed to query db", "err", err)
···
122
ps, err := db.GetPipelineStatuses(
123
p.db,
124
1,
125
-
db.FilterEq("repo_owner", f.Did),
126
-
db.FilterEq("repo_name", f.Name),
127
-
db.FilterEq("knot", f.Knot),
128
-
db.FilterEq("id", pipelineId),
129
)
130
if err != nil {
131
l.Error("failed to query db", "err", err)
···
189
ps, err := db.GetPipelineStatuses(
190
p.db,
191
1,
192
-
db.FilterEq("repo_owner", f.Did),
193
-
db.FilterEq("repo_name", f.Name),
194
-
db.FilterEq("knot", f.Knot),
195
-
db.FilterEq("id", pipelineId),
196
)
197
if err != nil || len(ps) != 1 {
198
l.Error("pipeline query failed", "err", err, "count", len(ps))
···
16
"tangled.org/core/appview/reporesolver"
17
"tangled.org/core/eventconsumer"
18
"tangled.org/core/idresolver"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/rbac"
21
spindlemodel "tangled.org/core/spindle/models"
22
···
82
ps, err := db.GetPipelineStatuses(
83
p.db,
84
30,
85
+
orm.FilterEq("repo_owner", f.Did),
86
+
orm.FilterEq("repo_name", f.Name),
87
+
orm.FilterEq("knot", f.Knot),
88
)
89
if err != nil {
90
l.Error("failed to query db", "err", err)
···
123
ps, err := db.GetPipelineStatuses(
124
p.db,
125
1,
126
+
orm.FilterEq("repo_owner", f.Did),
127
+
orm.FilterEq("repo_name", f.Name),
128
+
orm.FilterEq("knot", f.Knot),
129
+
orm.FilterEq("id", pipelineId),
130
)
131
if err != nil {
132
l.Error("failed to query db", "err", err)
···
190
ps, err := db.GetPipelineStatuses(
191
p.db,
192
1,
193
+
orm.FilterEq("repo_owner", f.Did),
194
+
orm.FilterEq("repo_name", f.Name),
195
+
orm.FilterEq("knot", f.Knot),
196
+
orm.FilterEq("id", pipelineId),
197
)
198
if err != nil || len(ps) != 1 {
199
l.Error("pipeline query failed", "err", err, "count", len(ps))
+2
-1
appview/pulls/opengraph.go
+2
-1
appview/pulls/opengraph.go
···
13
"tangled.org/core/appview/db"
14
"tangled.org/core/appview/models"
15
"tangled.org/core/appview/ogcard"
16
"tangled.org/core/patchutil"
17
"tangled.org/core/types"
18
)
···
276
}
277
278
// Get comment count from database
279
-
comments, err := db.GetPullComments(s.db, db.FilterEq("pull_id", pull.ID))
280
if err != nil {
281
log.Printf("failed to get pull comments: %v", err)
282
}
···
13
"tangled.org/core/appview/db"
14
"tangled.org/core/appview/models"
15
"tangled.org/core/appview/ogcard"
16
+
"tangled.org/core/orm"
17
"tangled.org/core/patchutil"
18
"tangled.org/core/types"
19
)
···
277
}
278
279
// Get comment count from database
280
+
comments, err := db.GetPullComments(s.db, orm.FilterEq("pull_id", pull.ID))
281
if err != nil {
282
log.Printf("failed to get pull comments: %v", err)
283
}
+56
-47
appview/pulls/pulls.go
+56
-47
appview/pulls/pulls.go
···
19
"tangled.org/core/appview/config"
20
"tangled.org/core/appview/db"
21
pulls_indexer "tangled.org/core/appview/indexer/pulls"
22
"tangled.org/core/appview/models"
23
"tangled.org/core/appview/notify"
24
"tangled.org/core/appview/oauth"
25
"tangled.org/core/appview/pages"
26
"tangled.org/core/appview/pages/markup"
27
"tangled.org/core/appview/pages/repoinfo"
28
-
"tangled.org/core/appview/refresolver"
29
"tangled.org/core/appview/reporesolver"
30
"tangled.org/core/appview/validator"
31
"tangled.org/core/appview/xrpcclient"
32
"tangled.org/core/idresolver"
33
"tangled.org/core/patchutil"
34
"tangled.org/core/rbac"
35
"tangled.org/core/tid"
···
44
)
45
46
type Pulls struct {
47
-
oauth *oauth.OAuth
48
-
repoResolver *reporesolver.RepoResolver
49
-
pages *pages.Pages
50
-
idResolver *idresolver.Resolver
51
-
refResolver *refresolver.Resolver
52
-
db *db.DB
53
-
config *config.Config
54
-
notifier notify.Notifier
55
-
enforcer *rbac.Enforcer
56
-
logger *slog.Logger
57
-
validator *validator.Validator
58
-
indexer *pulls_indexer.Indexer
59
}
60
61
func New(
···
63
repoResolver *reporesolver.RepoResolver,
64
pages *pages.Pages,
65
resolver *idresolver.Resolver,
66
-
refResolver *refresolver.Resolver,
67
db *db.DB,
68
config *config.Config,
69
notifier notify.Notifier,
···
73
logger *slog.Logger,
74
) *Pulls {
75
return &Pulls{
76
-
oauth: oauth,
77
-
repoResolver: repoResolver,
78
-
pages: pages,
79
-
idResolver: resolver,
80
-
refResolver: refResolver,
81
-
db: db,
82
-
config: config,
83
-
notifier: notifier,
84
-
enforcer: enforcer,
85
-
logger: logger,
86
-
validator: validator,
87
-
indexer: indexer,
88
}
89
}
90
···
190
ps, err := db.GetPipelineStatuses(
191
s.db,
192
len(shas),
193
-
db.FilterEq("repo_owner", f.Did),
194
-
db.FilterEq("repo_name", f.Name),
195
-
db.FilterEq("knot", f.Knot),
196
-
db.FilterIn("sha", shas),
197
)
198
if err != nil {
199
log.Printf("failed to fetch pipeline statuses: %s", err)
···
217
218
labelDefs, err := db.GetLabelDefinitions(
219
s.db,
220
-
db.FilterIn("at_uri", f.Labels),
221
-
db.FilterContains("scope", tangled.RepoPullNSID),
222
)
223
if err != nil {
224
log.Println("failed to fetch labels", err)
···
597
598
pulls, err := db.GetPulls(
599
s.db,
600
-
db.FilterIn("id", ids),
601
)
602
if err != nil {
603
log.Println("failed to get pulls", err)
···
648
ps, err := db.GetPipelineStatuses(
649
s.db,
650
len(shas),
651
-
db.FilterEq("repo_owner", f.Did),
652
-
db.FilterEq("repo_name", f.Name),
653
-
db.FilterEq("knot", f.Knot),
654
-
db.FilterIn("sha", shas),
655
)
656
if err != nil {
657
log.Printf("failed to fetch pipeline statuses: %s", err)
···
664
665
labelDefs, err := db.GetLabelDefinitions(
666
s.db,
667
-
db.FilterIn("at_uri", f.Labels),
668
-
db.FilterContains("scope", tangled.RepoPullNSID),
669
)
670
if err != nil {
671
log.Println("failed to fetch labels", err)
···
729
return
730
}
731
732
-
mentions, references := s.refResolver.Resolve(r.Context(), body)
733
734
// Start a transaction
735
tx, err := s.db.BeginTx(r.Context(), nil)
···
1205
}
1206
}
1207
1208
-
mentions, references := s.refResolver.Resolve(r.Context(), body)
1209
1210
rkey := tid.TID()
1211
initialSubmission := models.PullSubmission{
···
1365
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1366
return
1367
}
1368
}
1369
1370
if err = tx.Commit(); err != nil {
1371
log.Println("failed to create pull request", err)
1372
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1373
return
1374
}
1375
1376
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
···
1498
// fork repo
1499
repo, err := db.GetRepo(
1500
s.db,
1501
-
db.FilterEq("did", forkOwnerDid),
1502
-
db.FilterEq("name", forkName),
1503
)
1504
if err != nil {
1505
log.Println("failed to get repo", "did", forkOwnerDid, "name", forkName, "err", err)
···
2066
tx,
2067
p.ParentChangeId,
2068
// these should be enough filters to be unique per-stack
2069
-
db.FilterEq("repo_at", p.RepoAt.String()),
2070
-
db.FilterEq("owner_did", p.OwnerDid),
2071
-
db.FilterEq("change_id", p.ChangeId),
2072
)
2073
2074
if err != nil {
···
2397
body := fp.Body
2398
rkey := tid.TID()
2399
2400
-
mentions, references := s.refResolver.Resolve(ctx, body)
2401
2402
initialSubmission := models.PullSubmission{
2403
Patch: fp.Raw,
···
19
"tangled.org/core/appview/config"
20
"tangled.org/core/appview/db"
21
pulls_indexer "tangled.org/core/appview/indexer/pulls"
22
+
"tangled.org/core/appview/mentions"
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/notify"
25
"tangled.org/core/appview/oauth"
26
"tangled.org/core/appview/pages"
27
"tangled.org/core/appview/pages/markup"
28
"tangled.org/core/appview/pages/repoinfo"
29
"tangled.org/core/appview/reporesolver"
30
"tangled.org/core/appview/validator"
31
"tangled.org/core/appview/xrpcclient"
32
"tangled.org/core/idresolver"
33
+
"tangled.org/core/orm"
34
"tangled.org/core/patchutil"
35
"tangled.org/core/rbac"
36
"tangled.org/core/tid"
···
45
)
46
47
type Pulls struct {
48
+
oauth *oauth.OAuth
49
+
repoResolver *reporesolver.RepoResolver
50
+
pages *pages.Pages
51
+
idResolver *idresolver.Resolver
52
+
mentionsResolver *mentions.Resolver
53
+
db *db.DB
54
+
config *config.Config
55
+
notifier notify.Notifier
56
+
enforcer *rbac.Enforcer
57
+
logger *slog.Logger
58
+
validator *validator.Validator
59
+
indexer *pulls_indexer.Indexer
60
}
61
62
func New(
···
64
repoResolver *reporesolver.RepoResolver,
65
pages *pages.Pages,
66
resolver *idresolver.Resolver,
67
+
mentionsResolver *mentions.Resolver,
68
db *db.DB,
69
config *config.Config,
70
notifier notify.Notifier,
···
74
logger *slog.Logger,
75
) *Pulls {
76
return &Pulls{
77
+
oauth: oauth,
78
+
repoResolver: repoResolver,
79
+
pages: pages,
80
+
idResolver: resolver,
81
+
mentionsResolver: mentionsResolver,
82
+
db: db,
83
+
config: config,
84
+
notifier: notifier,
85
+
enforcer: enforcer,
86
+
logger: logger,
87
+
validator: validator,
88
+
indexer: indexer,
89
}
90
}
91
···
191
ps, err := db.GetPipelineStatuses(
192
s.db,
193
len(shas),
194
+
orm.FilterEq("repo_owner", f.Did),
195
+
orm.FilterEq("repo_name", f.Name),
196
+
orm.FilterEq("knot", f.Knot),
197
+
orm.FilterIn("sha", shas),
198
)
199
if err != nil {
200
log.Printf("failed to fetch pipeline statuses: %s", err)
···
218
219
labelDefs, err := db.GetLabelDefinitions(
220
s.db,
221
+
orm.FilterIn("at_uri", f.Labels),
222
+
orm.FilterContains("scope", tangled.RepoPullNSID),
223
)
224
if err != nil {
225
log.Println("failed to fetch labels", err)
···
598
599
pulls, err := db.GetPulls(
600
s.db,
601
+
orm.FilterIn("id", ids),
602
)
603
if err != nil {
604
log.Println("failed to get pulls", err)
···
649
ps, err := db.GetPipelineStatuses(
650
s.db,
651
len(shas),
652
+
orm.FilterEq("repo_owner", f.Did),
653
+
orm.FilterEq("repo_name", f.Name),
654
+
orm.FilterEq("knot", f.Knot),
655
+
orm.FilterIn("sha", shas),
656
)
657
if err != nil {
658
log.Printf("failed to fetch pipeline statuses: %s", err)
···
665
666
labelDefs, err := db.GetLabelDefinitions(
667
s.db,
668
+
orm.FilterIn("at_uri", f.Labels),
669
+
orm.FilterContains("scope", tangled.RepoPullNSID),
670
)
671
if err != nil {
672
log.Println("failed to fetch labels", err)
···
730
return
731
}
732
733
+
mentions, references := s.mentionsResolver.Resolve(r.Context(), body)
734
735
// Start a transaction
736
tx, err := s.db.BeginTx(r.Context(), nil)
···
1206
}
1207
}
1208
1209
+
mentions, references := s.mentionsResolver.Resolve(r.Context(), body)
1210
1211
rkey := tid.TID()
1212
initialSubmission := models.PullSubmission{
···
1366
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1367
return
1368
}
1369
+
1370
}
1371
1372
if err = tx.Commit(); err != nil {
1373
log.Println("failed to create pull request", err)
1374
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1375
return
1376
+
}
1377
+
1378
+
// notify about each pull
1379
+
//
1380
+
// this is performed after tx.Commit, because it could result in a locked DB otherwise
1381
+
for _, p := range stack {
1382
+
s.notifier.NewPull(r.Context(), p)
1383
}
1384
1385
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
···
1507
// fork repo
1508
repo, err := db.GetRepo(
1509
s.db,
1510
+
orm.FilterEq("did", forkOwnerDid),
1511
+
orm.FilterEq("name", forkName),
1512
)
1513
if err != nil {
1514
log.Println("failed to get repo", "did", forkOwnerDid, "name", forkName, "err", err)
···
2075
tx,
2076
p.ParentChangeId,
2077
// these should be enough filters to be unique per-stack
2078
+
orm.FilterEq("repo_at", p.RepoAt.String()),
2079
+
orm.FilterEq("owner_did", p.OwnerDid),
2080
+
orm.FilterEq("change_id", p.ChangeId),
2081
)
2082
2083
if err != nil {
···
2406
body := fp.Body
2407
rkey := tid.TID()
2408
2409
+
mentions, references := s.mentionsResolver.Resolve(ctx, body)
2410
2411
initialSubmission := models.PullSubmission{
2412
Patch: fp.Raw,
-65
appview/refresolver/resolver.go
-65
appview/refresolver/resolver.go
···
1
-
package refresolver
2
-
3
-
import (
4
-
"context"
5
-
"log/slog"
6
-
7
-
"github.com/bluesky-social/indigo/atproto/syntax"
8
-
"tangled.org/core/appview/config"
9
-
"tangled.org/core/appview/db"
10
-
"tangled.org/core/appview/models"
11
-
"tangled.org/core/appview/pages/markup"
12
-
"tangled.org/core/idresolver"
13
-
)
14
-
15
-
type Resolver struct {
16
-
config *config.Config
17
-
idResolver *idresolver.Resolver
18
-
execer db.Execer
19
-
logger *slog.Logger
20
-
}
21
-
22
-
func New(
23
-
config *config.Config,
24
-
idResolver *idresolver.Resolver,
25
-
execer db.Execer,
26
-
logger *slog.Logger,
27
-
) *Resolver {
28
-
return &Resolver{
29
-
config,
30
-
idResolver,
31
-
execer,
32
-
logger,
33
-
}
34
-
}
35
-
36
-
func (r *Resolver) Resolve(ctx context.Context, source string) ([]syntax.DID, []syntax.ATURI) {
37
-
l := r.logger.With("method", "Resolve")
38
-
rawMentions, rawRefs := markup.FindReferences(r.config.Core.AppviewHost, source)
39
-
l.Debug("found possible references", "mentions", rawMentions, "refs", rawRefs)
40
-
idents := r.idResolver.ResolveIdents(ctx, rawMentions)
41
-
var mentions []syntax.DID
42
-
for _, ident := range idents {
43
-
if ident != nil && !ident.Handle.IsInvalidHandle() {
44
-
mentions = append(mentions, ident.DID)
45
-
}
46
-
}
47
-
l.Debug("found mentions", "mentions", mentions)
48
-
49
-
var resolvedRefs []models.ReferenceLink
50
-
for _, rawRef := range rawRefs {
51
-
ident, err := r.idResolver.ResolveIdent(ctx, rawRef.Handle)
52
-
if err != nil || ident == nil || ident.Handle.IsInvalidHandle() {
53
-
continue
54
-
}
55
-
rawRef.Handle = string(ident.DID)
56
-
resolvedRefs = append(resolvedRefs, rawRef)
57
-
}
58
-
aturiRefs, err := db.ValidateReferenceLinks(r.execer, resolvedRefs)
59
-
if err != nil {
60
-
l.Error("failed running query", "err", err)
61
-
}
62
-
l.Debug("found references", "refs", aturiRefs)
63
-
64
-
return mentions, aturiRefs
65
-
}
···
+10
-9
appview/repo/artifact.go
+10
-9
appview/repo/artifact.go
···
15
"tangled.org/core/appview/models"
16
"tangled.org/core/appview/pages"
17
"tangled.org/core/appview/xrpcclient"
18
"tangled.org/core/tid"
19
"tangled.org/core/types"
20
···
155
156
artifacts, err := db.GetArtifact(
157
rp.db,
158
-
db.FilterEq("repo_at", f.RepoAt()),
159
-
db.FilterEq("tag", tag.Tag.Hash[:]),
160
-
db.FilterEq("name", filename),
161
)
162
if err != nil {
163
log.Println("failed to get artifacts", err)
···
234
235
artifacts, err := db.GetArtifact(
236
rp.db,
237
-
db.FilterEq("repo_at", f.RepoAt()),
238
-
db.FilterEq("tag", tag[:]),
239
-
db.FilterEq("name", filename),
240
)
241
if err != nil {
242
log.Println("failed to get artifacts", err)
···
276
defer tx.Rollback()
277
278
err = db.DeleteArtifact(tx,
279
-
db.FilterEq("repo_at", f.RepoAt()),
280
-
db.FilterEq("tag", artifact.Tag[:]),
281
-
db.FilterEq("name", filename),
282
)
283
if err != nil {
284
log.Println("failed to remove artifact record from db", err)
···
15
"tangled.org/core/appview/models"
16
"tangled.org/core/appview/pages"
17
"tangled.org/core/appview/xrpcclient"
18
+
"tangled.org/core/orm"
19
"tangled.org/core/tid"
20
"tangled.org/core/types"
21
···
156
157
artifacts, err := db.GetArtifact(
158
rp.db,
159
+
orm.FilterEq("repo_at", f.RepoAt()),
160
+
orm.FilterEq("tag", tag.Tag.Hash[:]),
161
+
orm.FilterEq("name", filename),
162
)
163
if err != nil {
164
log.Println("failed to get artifacts", err)
···
235
236
artifacts, err := db.GetArtifact(
237
rp.db,
238
+
orm.FilterEq("repo_at", f.RepoAt()),
239
+
orm.FilterEq("tag", tag[:]),
240
+
orm.FilterEq("name", filename),
241
)
242
if err != nil {
243
log.Println("failed to get artifacts", err)
···
277
defer tx.Rollback()
278
279
err = db.DeleteArtifact(tx,
280
+
orm.FilterEq("repo_at", f.RepoAt()),
281
+
orm.FilterEq("tag", artifact.Tag[:]),
282
+
orm.FilterEq("name", filename),
283
)
284
if err != nil {
285
log.Println("failed to remove artifact record from db", err)
+3
-2
appview/repo/feed.go
+3
-2
appview/repo/feed.go
···
11
"tangled.org/core/appview/db"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
15
"github.com/bluesky-social/indigo/atproto/identity"
16
"github.com/bluesky-social/indigo/atproto/syntax"
···
20
func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) {
21
const feedLimitPerType = 100
22
23
-
pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", repo.RepoAt()))
24
if err != nil {
25
return nil, err
26
}
···
28
issues, err := db.GetIssuesPaginated(
29
rp.db,
30
pagination.Page{Limit: feedLimitPerType},
31
-
db.FilterEq("repo_at", repo.RepoAt()),
32
)
33
if err != nil {
34
return nil, err
···
11
"tangled.org/core/appview/db"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
+
"tangled.org/core/orm"
15
16
"github.com/bluesky-social/indigo/atproto/identity"
17
"github.com/bluesky-social/indigo/atproto/syntax"
···
21
func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) {
22
const feedLimitPerType = 100
23
24
+
pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, orm.FilterEq("repo_at", repo.RepoAt()))
25
if err != nil {
26
return nil, err
27
}
···
29
issues, err := db.GetIssuesPaginated(
30
rp.db,
31
pagination.Page{Limit: feedLimitPerType},
32
+
orm.FilterEq("repo_at", repo.RepoAt()),
33
)
34
if err != nil {
35
return nil, err
+4
-3
appview/repo/index.go
+4
-3
appview/repo/index.go
···
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/pages"
25
"tangled.org/core/appview/xrpcclient"
26
"tangled.org/core/types"
27
28
"github.com/go-chi/chi/v5"
···
122
l.Error("failed to get email to did map", "err", err)
123
}
124
125
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc)
126
if err != nil {
127
l.Error("failed to GetVerifiedObjectCommits", "err", err)
128
}
···
171
// first attempt to fetch from db
172
langs, err := db.GetRepoLanguages(
173
rp.db,
174
-
db.FilterEq("repo_at", repo.RepoAt()),
175
-
db.FilterEq("ref", currentRef),
176
)
177
178
if err != nil || langs == nil {
···
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/pages"
25
"tangled.org/core/appview/xrpcclient"
26
+
"tangled.org/core/orm"
27
"tangled.org/core/types"
28
29
"github.com/go-chi/chi/v5"
···
123
l.Error("failed to get email to did map", "err", err)
124
}
125
126
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, commitsTrunc)
127
if err != nil {
128
l.Error("failed to GetVerifiedObjectCommits", "err", err)
129
}
···
172
// first attempt to fetch from db
173
langs, err := db.GetRepoLanguages(
174
rp.db,
175
+
orm.FilterEq("repo_at", repo.RepoAt()),
176
+
orm.FilterEq("ref", currentRef),
177
)
178
179
if err != nil || langs == nil {
+2
-2
appview/repo/log.go
+2
-2
appview/repo/log.go
···
116
l.Error("failed to fetch email to did mapping", "err", err)
117
}
118
119
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits)
120
if err != nil {
121
l.Error("failed to GetVerifiedObjectCommits", "err", err)
122
}
···
192
l.Error("failed to get email to did mapping", "err", err)
193
}
194
195
-
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff})
196
if err != nil {
197
l.Error("failed to GetVerifiedCommits", "err", err)
198
}
···
116
l.Error("failed to fetch email to did mapping", "err", err)
117
}
118
119
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, xrpcResp.Commits)
120
if err != nil {
121
l.Error("failed to GetVerifiedObjectCommits", "err", err)
122
}
···
192
l.Error("failed to get email to did mapping", "err", err)
193
}
194
195
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.Commit{result.Diff.Commit})
196
if err != nil {
197
l.Error("failed to GetVerifiedCommits", "err", err)
198
}
+3
-2
appview/repo/opengraph.go
+3
-2
appview/repo/opengraph.go
···
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/models"
18
"tangled.org/core/appview/ogcard"
19
"tangled.org/core/types"
20
)
21
···
338
var languageStats []types.RepoLanguageDetails
339
langs, err := db.GetRepoLanguages(
340
rp.db,
341
-
db.FilterEq("repo_at", f.RepoAt()),
342
-
db.FilterEq("is_default_ref", 1),
343
)
344
if err != nil {
345
log.Printf("failed to get language stats from db: %v", err)
···
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/models"
18
"tangled.org/core/appview/ogcard"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/types"
21
)
22
···
339
var languageStats []types.RepoLanguageDetails
340
langs, err := db.GetRepoLanguages(
341
rp.db,
342
+
orm.FilterEq("repo_at", f.RepoAt()),
343
+
orm.FilterEq("is_default_ref", 1),
344
)
345
if err != nil {
346
log.Printf("failed to get language stats from db: %v", err)
+17
-16
appview/repo/repo.go
+17
-16
appview/repo/repo.go
···
24
xrpcclient "tangled.org/core/appview/xrpcclient"
25
"tangled.org/core/eventconsumer"
26
"tangled.org/core/idresolver"
27
"tangled.org/core/rbac"
28
"tangled.org/core/tid"
29
"tangled.org/core/xrpc/serviceauth"
···
345
// get form values
346
labelId := r.FormValue("label-id")
347
348
-
label, err := db.GetLabelDefinition(rp.db, db.FilterEq("id", labelId))
349
if err != nil {
350
fail("Failed to find label definition.", err)
351
return
···
409
410
err = db.UnsubscribeLabel(
411
tx,
412
-
db.FilterEq("repo_at", f.RepoAt()),
413
-
db.FilterEq("label_at", removedAt),
414
)
415
if err != nil {
416
fail("Failed to unsubscribe label.", err)
417
return
418
}
419
420
-
err = db.DeleteLabelDefinition(tx, db.FilterEq("id", label.Id))
421
if err != nil {
422
fail("Failed to delete label definition.", err)
423
return
···
456
}
457
458
labelAts := r.Form["label"]
459
-
_, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts))
460
if err != nil {
461
fail("Failed to subscribe to label.", err)
462
return
···
542
}
543
544
labelAts := r.Form["label"]
545
-
_, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts))
546
if err != nil {
547
fail("Failed to unsubscribe to label.", err)
548
return
···
582
583
err = db.UnsubscribeLabel(
584
rp.db,
585
-
db.FilterEq("repo_at", f.RepoAt()),
586
-
db.FilterIn("label_at", labelAts),
587
)
588
if err != nil {
589
fail("Failed to unsubscribe label.", err)
···
612
613
labelDefs, err := db.GetLabelDefinitions(
614
rp.db,
615
-
db.FilterIn("at_uri", f.Labels),
616
-
db.FilterContains("scope", subject.Collection().String()),
617
)
618
if err != nil {
619
l.Error("failed to fetch label defs", "err", err)
···
625
defs[l.AtUri().String()] = &l
626
}
627
628
-
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
629
if err != nil {
630
l.Error("failed to build label state", "err", err)
631
return
···
660
661
labelDefs, err := db.GetLabelDefinitions(
662
rp.db,
663
-
db.FilterIn("at_uri", f.Labels),
664
-
db.FilterContains("scope", subject.Collection().String()),
665
)
666
if err != nil {
667
l.Error("failed to fetch labels", "err", err)
···
673
defs[l.AtUri().String()] = &l
674
}
675
676
-
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
677
if err != nil {
678
l.Error("failed to build label state", "err", err)
679
return
···
1036
// in the user's account.
1037
existingRepo, err := db.GetRepo(
1038
rp.db,
1039
-
db.FilterEq("did", user.Did),
1040
-
db.FilterEq("name", forkName),
1041
)
1042
if err != nil {
1043
if !errors.Is(err, sql.ErrNoRows) {
···
24
xrpcclient "tangled.org/core/appview/xrpcclient"
25
"tangled.org/core/eventconsumer"
26
"tangled.org/core/idresolver"
27
+
"tangled.org/core/orm"
28
"tangled.org/core/rbac"
29
"tangled.org/core/tid"
30
"tangled.org/core/xrpc/serviceauth"
···
346
// get form values
347
labelId := r.FormValue("label-id")
348
349
+
label, err := db.GetLabelDefinition(rp.db, orm.FilterEq("id", labelId))
350
if err != nil {
351
fail("Failed to find label definition.", err)
352
return
···
410
411
err = db.UnsubscribeLabel(
412
tx,
413
+
orm.FilterEq("repo_at", f.RepoAt()),
414
+
orm.FilterEq("label_at", removedAt),
415
)
416
if err != nil {
417
fail("Failed to unsubscribe label.", err)
418
return
419
}
420
421
+
err = db.DeleteLabelDefinition(tx, orm.FilterEq("id", label.Id))
422
if err != nil {
423
fail("Failed to delete label definition.", err)
424
return
···
457
}
458
459
labelAts := r.Form["label"]
460
+
_, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts))
461
if err != nil {
462
fail("Failed to subscribe to label.", err)
463
return
···
543
}
544
545
labelAts := r.Form["label"]
546
+
_, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts))
547
if err != nil {
548
fail("Failed to unsubscribe to label.", err)
549
return
···
583
584
err = db.UnsubscribeLabel(
585
rp.db,
586
+
orm.FilterEq("repo_at", f.RepoAt()),
587
+
orm.FilterIn("label_at", labelAts),
588
)
589
if err != nil {
590
fail("Failed to unsubscribe label.", err)
···
613
614
labelDefs, err := db.GetLabelDefinitions(
615
rp.db,
616
+
orm.FilterIn("at_uri", f.Labels),
617
+
orm.FilterContains("scope", subject.Collection().String()),
618
)
619
if err != nil {
620
l.Error("failed to fetch label defs", "err", err)
···
626
defs[l.AtUri().String()] = &l
627
}
628
629
+
states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject))
630
if err != nil {
631
l.Error("failed to build label state", "err", err)
632
return
···
661
662
labelDefs, err := db.GetLabelDefinitions(
663
rp.db,
664
+
orm.FilterIn("at_uri", f.Labels),
665
+
orm.FilterContains("scope", subject.Collection().String()),
666
)
667
if err != nil {
668
l.Error("failed to fetch labels", "err", err)
···
674
defs[l.AtUri().String()] = &l
675
}
676
677
+
states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject))
678
if err != nil {
679
l.Error("failed to build label state", "err", err)
680
return
···
1037
// in the user's account.
1038
existingRepo, err := db.GetRepo(
1039
rp.db,
1040
+
orm.FilterEq("did", user.Did),
1041
+
orm.FilterEq("name", forkName),
1042
)
1043
if err != nil {
1044
if !errors.Is(err, sql.ErrNoRows) {
+16
-17
appview/repo/repo_util.go
+16
-17
appview/repo/repo_util.go
···
1
package repo
2
3
import (
4
"slices"
5
"sort"
6
"strings"
7
8
"tangled.org/core/appview/db"
9
"tangled.org/core/appview/models"
10
"tangled.org/core/types"
11
-
12
-
"github.com/go-git/go-git/v5/plumbing/object"
13
)
14
15
func sortFiles(files []types.NiceTree) {
···
42
})
43
}
44
45
-
func uniqueEmails(commits []*object.Commit) []string {
46
emails := make(map[string]struct{})
47
for _, commit := range commits {
48
-
if commit.Author.Email != "" {
49
-
emails[commit.Author.Email] = struct{}{}
50
-
}
51
-
if commit.Committer.Email != "" {
52
-
emails[commit.Committer.Email] = struct{}{}
53
}
54
}
55
-
var uniqueEmails []string
56
-
for email := range emails {
57
-
uniqueEmails = append(uniqueEmails, email)
58
-
}
59
-
return uniqueEmails
60
}
61
62
func balanceIndexItems(commitCount, branchCount, tagCount, fileCount int) (commitsTrunc int, branchesTrunc int, tagsTrunc int) {
···
104
ps, err := db.GetPipelineStatuses(
105
d,
106
len(shas),
107
-
db.FilterEq("repo_owner", repo.Did),
108
-
db.FilterEq("repo_name", repo.Name),
109
-
db.FilterEq("knot", repo.Knot),
110
-
db.FilterIn("sha", shas),
111
)
112
if err != nil {
113
return nil, err
···
1
package repo
2
3
import (
4
+
"maps"
5
"slices"
6
"sort"
7
"strings"
8
9
"tangled.org/core/appview/db"
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
"tangled.org/core/types"
13
)
14
15
func sortFiles(files []types.NiceTree) {
···
42
})
43
}
44
45
+
func uniqueEmails(commits []types.Commit) []string {
46
emails := make(map[string]struct{})
47
for _, commit := range commits {
48
+
emails[commit.Author.Email] = struct{}{}
49
+
emails[commit.Committer.Email] = struct{}{}
50
+
for _, c := range commit.CoAuthors() {
51
+
emails[c.Email] = struct{}{}
52
}
53
}
54
+
55
+
// delete empty emails if any, from the set
56
+
delete(emails, "")
57
+
58
+
return slices.Collect(maps.Keys(emails))
59
}
60
61
func balanceIndexItems(commitCount, branchCount, tagCount, fileCount int) (commitsTrunc int, branchesTrunc int, tagsTrunc int) {
···
103
ps, err := db.GetPipelineStatuses(
104
d,
105
len(shas),
106
+
orm.FilterEq("repo_owner", repo.Did),
107
+
orm.FilterEq("repo_name", repo.Name),
108
+
orm.FilterEq("knot", repo.Knot),
109
+
orm.FilterIn("sha", shas),
110
)
111
if err != nil {
112
return nil, err
+3
-2
appview/repo/settings.go
+3
-2
appview/repo/settings.go
···
14
"tangled.org/core/appview/oauth"
15
"tangled.org/core/appview/pages"
16
xrpcclient "tangled.org/core/appview/xrpcclient"
17
"tangled.org/core/types"
18
19
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
210
return
211
}
212
213
-
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
214
if err != nil {
215
l.Error("failed to fetch labels", "err", err)
216
rp.pages.Error503(w)
217
return
218
}
219
220
-
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Labels))
221
if err != nil {
222
l.Error("failed to fetch labels", "err", err)
223
rp.pages.Error503(w)
···
14
"tangled.org/core/appview/oauth"
15
"tangled.org/core/appview/pages"
16
xrpcclient "tangled.org/core/appview/xrpcclient"
17
+
"tangled.org/core/orm"
18
"tangled.org/core/types"
19
20
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
211
return
212
}
213
214
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
215
if err != nil {
216
l.Error("failed to fetch labels", "err", err)
217
rp.pages.Error503(w)
218
return
219
}
220
221
+
labels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", f.Labels))
222
if err != nil {
223
l.Error("failed to fetch labels", "err", err)
224
rp.pages.Error503(w)
+5
-4
appview/serververify/verify.go
+5
-4
appview/serververify/verify.go
···
9
"tangled.org/core/api/tangled"
10
"tangled.org/core/appview/db"
11
"tangled.org/core/appview/xrpcclient"
12
"tangled.org/core/rbac"
13
)
14
···
76
// mark this spindle as verified in the db
77
rowId, err := db.VerifySpindle(
78
tx,
79
-
db.FilterEq("owner", owner),
80
-
db.FilterEq("instance", instance),
81
)
82
if err != nil {
83
return 0, fmt.Errorf("failed to write to DB: %w", err)
···
115
// mark as registered
116
err = db.MarkRegistered(
117
tx,
118
-
db.FilterEq("did", owner),
119
-
db.FilterEq("domain", domain),
120
)
121
if err != nil {
122
return fmt.Errorf("failed to register domain: %w", err)
···
9
"tangled.org/core/api/tangled"
10
"tangled.org/core/appview/db"
11
"tangled.org/core/appview/xrpcclient"
12
+
"tangled.org/core/orm"
13
"tangled.org/core/rbac"
14
)
15
···
77
// mark this spindle as verified in the db
78
rowId, err := db.VerifySpindle(
79
tx,
80
+
orm.FilterEq("owner", owner),
81
+
orm.FilterEq("instance", instance),
82
)
83
if err != nil {
84
return 0, fmt.Errorf("failed to write to DB: %w", err)
···
116
// mark as registered
117
err = db.MarkRegistered(
118
tx,
119
+
orm.FilterEq("did", owner),
120
+
orm.FilterEq("domain", domain),
121
)
122
if err != nil {
123
return fmt.Errorf("failed to register domain: %w", err)
+25
-24
appview/spindles/spindles.go
+25
-24
appview/spindles/spindles.go
···
20
"tangled.org/core/appview/serververify"
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/idresolver"
23
"tangled.org/core/rbac"
24
"tangled.org/core/tid"
25
···
71
user := s.OAuth.GetUser(r)
72
all, err := db.GetSpindles(
73
s.Db,
74
-
db.FilterEq("owner", user.Did),
75
)
76
if err != nil {
77
s.Logger.Error("failed to fetch spindles", "err", err)
···
101
102
spindles, err := db.GetSpindles(
103
s.Db,
104
-
db.FilterEq("instance", instance),
105
-
db.FilterEq("owner", user.Did),
106
-
db.FilterIsNot("verified", "null"),
107
)
108
if err != nil || len(spindles) != 1 {
109
l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles))
···
123
repos, err := db.GetRepos(
124
s.Db,
125
0,
126
-
db.FilterEq("spindle", instance),
127
)
128
if err != nil {
129
l.Error("failed to get spindle repos", "err", err)
···
290
291
spindles, err := db.GetSpindles(
292
s.Db,
293
-
db.FilterEq("owner", user.Did),
294
-
db.FilterEq("instance", instance),
295
)
296
if err != nil || len(spindles) != 1 {
297
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
319
// remove spindle members first
320
err = db.RemoveSpindleMember(
321
tx,
322
-
db.FilterEq("did", user.Did),
323
-
db.FilterEq("instance", instance),
324
)
325
if err != nil {
326
l.Error("failed to remove spindle members", "err", err)
···
330
331
err = db.DeleteSpindle(
332
tx,
333
-
db.FilterEq("owner", user.Did),
334
-
db.FilterEq("instance", instance),
335
)
336
if err != nil {
337
l.Error("failed to delete spindle", "err", err)
···
410
411
spindles, err := db.GetSpindles(
412
s.Db,
413
-
db.FilterEq("owner", user.Did),
414
-
db.FilterEq("instance", instance),
415
)
416
if err != nil || len(spindles) != 1 {
417
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
453
454
verifiedSpindle, err := db.GetSpindles(
455
s.Db,
456
-
db.FilterEq("id", rowId),
457
)
458
if err != nil || len(verifiedSpindle) != 1 {
459
l.Error("failed get new spindle", "err", err)
···
486
487
spindles, err := db.GetSpindles(
488
s.Db,
489
-
db.FilterEq("owner", user.Did),
490
-
db.FilterEq("instance", instance),
491
)
492
if err != nil || len(spindles) != 1 {
493
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
622
623
spindles, err := db.GetSpindles(
624
s.Db,
625
-
db.FilterEq("owner", user.Did),
626
-
db.FilterEq("instance", instance),
627
)
628
if err != nil || len(spindles) != 1 {
629
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
672
// get the record from the DB first:
673
members, err := db.GetSpindleMembers(
674
s.Db,
675
-
db.FilterEq("did", user.Did),
676
-
db.FilterEq("instance", instance),
677
-
db.FilterEq("subject", memberId.DID),
678
)
679
if err != nil || len(members) != 1 {
680
l.Error("failed to get member", "err", err)
···
685
// remove from db
686
if err = db.RemoveSpindleMember(
687
tx,
688
-
db.FilterEq("did", user.Did),
689
-
db.FilterEq("instance", instance),
690
-
db.FilterEq("subject", memberId.DID),
691
); err != nil {
692
l.Error("failed to remove spindle member", "err", err)
693
fail()
···
20
"tangled.org/core/appview/serververify"
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/idresolver"
23
+
"tangled.org/core/orm"
24
"tangled.org/core/rbac"
25
"tangled.org/core/tid"
26
···
72
user := s.OAuth.GetUser(r)
73
all, err := db.GetSpindles(
74
s.Db,
75
+
orm.FilterEq("owner", user.Did),
76
)
77
if err != nil {
78
s.Logger.Error("failed to fetch spindles", "err", err)
···
102
103
spindles, err := db.GetSpindles(
104
s.Db,
105
+
orm.FilterEq("instance", instance),
106
+
orm.FilterEq("owner", user.Did),
107
+
orm.FilterIsNot("verified", "null"),
108
)
109
if err != nil || len(spindles) != 1 {
110
l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles))
···
124
repos, err := db.GetRepos(
125
s.Db,
126
0,
127
+
orm.FilterEq("spindle", instance),
128
)
129
if err != nil {
130
l.Error("failed to get spindle repos", "err", err)
···
291
292
spindles, err := db.GetSpindles(
293
s.Db,
294
+
orm.FilterEq("owner", user.Did),
295
+
orm.FilterEq("instance", instance),
296
)
297
if err != nil || len(spindles) != 1 {
298
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
320
// remove spindle members first
321
err = db.RemoveSpindleMember(
322
tx,
323
+
orm.FilterEq("did", user.Did),
324
+
orm.FilterEq("instance", instance),
325
)
326
if err != nil {
327
l.Error("failed to remove spindle members", "err", err)
···
331
332
err = db.DeleteSpindle(
333
tx,
334
+
orm.FilterEq("owner", user.Did),
335
+
orm.FilterEq("instance", instance),
336
)
337
if err != nil {
338
l.Error("failed to delete spindle", "err", err)
···
411
412
spindles, err := db.GetSpindles(
413
s.Db,
414
+
orm.FilterEq("owner", user.Did),
415
+
orm.FilterEq("instance", instance),
416
)
417
if err != nil || len(spindles) != 1 {
418
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
454
455
verifiedSpindle, err := db.GetSpindles(
456
s.Db,
457
+
orm.FilterEq("id", rowId),
458
)
459
if err != nil || len(verifiedSpindle) != 1 {
460
l.Error("failed get new spindle", "err", err)
···
487
488
spindles, err := db.GetSpindles(
489
s.Db,
490
+
orm.FilterEq("owner", user.Did),
491
+
orm.FilterEq("instance", instance),
492
)
493
if err != nil || len(spindles) != 1 {
494
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
623
624
spindles, err := db.GetSpindles(
625
s.Db,
626
+
orm.FilterEq("owner", user.Did),
627
+
orm.FilterEq("instance", instance),
628
)
629
if err != nil || len(spindles) != 1 {
630
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
673
// get the record from the DB first:
674
members, err := db.GetSpindleMembers(
675
s.Db,
676
+
orm.FilterEq("did", user.Did),
677
+
orm.FilterEq("instance", instance),
678
+
orm.FilterEq("subject", memberId.DID),
679
)
680
if err != nil || len(members) != 1 {
681
l.Error("failed to get member", "err", err)
···
686
// remove from db
687
if err = db.RemoveSpindleMember(
688
tx,
689
+
orm.FilterEq("did", user.Did),
690
+
orm.FilterEq("instance", instance),
691
+
orm.FilterEq("subject", memberId.DID),
692
); err != nil {
693
l.Error("failed to remove spindle member", "err", err)
694
fail()
+6
-5
appview/state/gfi.go
+6
-5
appview/state/gfi.go
···
11
"tangled.org/core/appview/pages"
12
"tangled.org/core/appview/pagination"
13
"tangled.org/core/consts"
14
)
15
16
func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
···
20
21
goodFirstIssueLabel := s.config.Label.GoodFirstIssue
22
23
-
gfiLabelDef, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", goodFirstIssueLabel))
24
if err != nil {
25
log.Println("failed to get gfi label def", err)
26
s.pages.Error500(w)
27
return
28
}
29
30
-
repoLabels, err := db.GetRepoLabels(s.db, db.FilterEq("label_at", goodFirstIssueLabel))
31
if err != nil {
32
log.Println("failed to get repo labels", err)
33
s.pages.Error503(w)
···
55
pagination.Page{
56
Limit: 500,
57
},
58
-
db.FilterIn("repo_at", repoUris),
59
-
db.FilterEq("open", 1),
60
)
61
if err != nil {
62
log.Println("failed to get issues", err)
···
132
}
133
134
if len(uriList) > 0 {
135
-
allLabelDefs, err = db.GetLabelDefinitions(s.db, db.FilterIn("at_uri", uriList))
136
if err != nil {
137
log.Println("failed to fetch labels", err)
138
}
···
11
"tangled.org/core/appview/pages"
12
"tangled.org/core/appview/pagination"
13
"tangled.org/core/consts"
14
+
"tangled.org/core/orm"
15
)
16
17
func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
···
21
22
goodFirstIssueLabel := s.config.Label.GoodFirstIssue
23
24
+
gfiLabelDef, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", goodFirstIssueLabel))
25
if err != nil {
26
log.Println("failed to get gfi label def", err)
27
s.pages.Error500(w)
28
return
29
}
30
31
+
repoLabels, err := db.GetRepoLabels(s.db, orm.FilterEq("label_at", goodFirstIssueLabel))
32
if err != nil {
33
log.Println("failed to get repo labels", err)
34
s.pages.Error503(w)
···
56
pagination.Page{
57
Limit: 500,
58
},
59
+
orm.FilterIn("repo_at", repoUris),
60
+
orm.FilterEq("open", 1),
61
)
62
if err != nil {
63
log.Println("failed to get issues", err)
···
133
}
134
135
if len(uriList) > 0 {
136
+
allLabelDefs, err = db.GetLabelDefinitions(s.db, orm.FilterIn("at_uri", uriList))
137
if err != nil {
138
log.Println("failed to fetch labels", err)
139
}
+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 {
+6
-5
appview/state/knotstream.go
+6
-5
appview/state/knotstream.go
···
16
ec "tangled.org/core/eventconsumer"
17
"tangled.org/core/eventconsumer/cursor"
18
"tangled.org/core/log"
19
"tangled.org/core/rbac"
20
"tangled.org/core/workflow"
21
···
30
31
knots, err := db.GetRegistrations(
32
d,
33
-
db.FilterIsNot("registered", "null"),
34
)
35
if err != nil {
36
return nil, err
···
143
repos, err := db.GetRepos(
144
d,
145
0,
146
-
db.FilterEq("did", record.RepoDid),
147
-
db.FilterEq("name", record.RepoName),
148
)
149
if err != nil {
150
return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err)
···
209
repos, err := db.GetRepos(
210
d,
211
0,
212
-
db.FilterEq("did", record.TriggerMetadata.Repo.Did),
213
-
db.FilterEq("name", record.TriggerMetadata.Repo.Repo),
214
)
215
if err != nil {
216
return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err)
···
16
ec "tangled.org/core/eventconsumer"
17
"tangled.org/core/eventconsumer/cursor"
18
"tangled.org/core/log"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/rbac"
21
"tangled.org/core/workflow"
22
···
31
32
knots, err := db.GetRegistrations(
33
d,
34
+
orm.FilterIsNot("registered", "null"),
35
)
36
if err != nil {
37
return nil, err
···
144
repos, err := db.GetRepos(
145
d,
146
0,
147
+
orm.FilterEq("did", record.RepoDid),
148
+
orm.FilterEq("name", record.RepoName),
149
)
150
if err != nil {
151
return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err)
···
210
repos, err := db.GetRepos(
211
d,
212
0,
213
+
orm.FilterEq("did", record.TriggerMetadata.Repo.Did),
214
+
orm.FilterEq("name", record.TriggerMetadata.Repo.Repo),
215
)
216
if err != nil {
217
return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err)
+13
-12
appview/state/profile.go
+13
-12
appview/state/profile.go
···
19
"tangled.org/core/appview/db"
20
"tangled.org/core/appview/models"
21
"tangled.org/core/appview/pages"
22
)
23
24
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
···
56
return nil, fmt.Errorf("failed to get profile: %w", err)
57
}
58
59
-
repoCount, err := db.CountRepos(s.db, db.FilterEq("did", did))
60
if err != nil {
61
return nil, fmt.Errorf("failed to get repo count: %w", err)
62
}
63
64
-
stringCount, err := db.CountStrings(s.db, db.FilterEq("did", did))
65
if err != nil {
66
return nil, fmt.Errorf("failed to get string count: %w", err)
67
}
68
69
-
starredCount, err := db.CountStars(s.db, db.FilterEq("did", did))
70
if err != nil {
71
return nil, fmt.Errorf("failed to get starred repo count: %w", err)
72
}
···
86
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
87
punchcard, err := db.MakePunchcard(
88
s.db,
89
-
db.FilterEq("did", did),
90
-
db.FilterGte("date", startOfYear.Format(time.DateOnly)),
91
-
db.FilterLte("date", now.Format(time.DateOnly)),
92
)
93
if err != nil {
94
return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err)
···
123
repos, err := db.GetRepos(
124
s.db,
125
0,
126
-
db.FilterEq("did", profile.UserDid),
127
)
128
if err != nil {
129
l.Error("failed to fetch repos", "err", err)
···
193
repos, err := db.GetRepos(
194
s.db,
195
0,
196
-
db.FilterEq("did", profile.UserDid),
197
)
198
if err != nil {
199
l.Error("failed to get repos", "err", err)
···
219
}
220
l = l.With("profileDid", profile.UserDid)
221
222
-
stars, err := db.GetRepoStars(s.db, 0, db.FilterEq("did", profile.UserDid))
223
if err != nil {
224
l.Error("failed to get stars", "err", err)
225
s.pages.Error500(w)
···
248
}
249
l = l.With("profileDid", profile.UserDid)
250
251
-
strings, err := db.GetStrings(s.db, 0, db.FilterEq("did", profile.UserDid))
252
if err != nil {
253
l.Error("failed to get strings", "err", err)
254
s.pages.Error500(w)
···
300
followDids = append(followDids, extractDid(follow))
301
}
302
303
-
profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids))
304
if err != nil {
305
l.Error("failed to get profiles", "followDids", followDids, "err", err)
306
return ¶ms, err
···
703
log.Printf("getting profile data for %s: %s", user.Did, err)
704
}
705
706
-
repos, err := db.GetRepos(s.db, 0, db.FilterEq("did", user.Did))
707
if err != nil {
708
log.Printf("getting repos for %s: %s", user.Did, err)
709
}
···
19
"tangled.org/core/appview/db"
20
"tangled.org/core/appview/models"
21
"tangled.org/core/appview/pages"
22
+
"tangled.org/core/orm"
23
)
24
25
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
···
57
return nil, fmt.Errorf("failed to get profile: %w", err)
58
}
59
60
+
repoCount, err := db.CountRepos(s.db, orm.FilterEq("did", did))
61
if err != nil {
62
return nil, fmt.Errorf("failed to get repo count: %w", err)
63
}
64
65
+
stringCount, err := db.CountStrings(s.db, orm.FilterEq("did", did))
66
if err != nil {
67
return nil, fmt.Errorf("failed to get string count: %w", err)
68
}
69
70
+
starredCount, err := db.CountStars(s.db, orm.FilterEq("did", did))
71
if err != nil {
72
return nil, fmt.Errorf("failed to get starred repo count: %w", err)
73
}
···
87
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
88
punchcard, err := db.MakePunchcard(
89
s.db,
90
+
orm.FilterEq("did", did),
91
+
orm.FilterGte("date", startOfYear.Format(time.DateOnly)),
92
+
orm.FilterLte("date", now.Format(time.DateOnly)),
93
)
94
if err != nil {
95
return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err)
···
124
repos, err := db.GetRepos(
125
s.db,
126
0,
127
+
orm.FilterEq("did", profile.UserDid),
128
)
129
if err != nil {
130
l.Error("failed to fetch repos", "err", err)
···
194
repos, err := db.GetRepos(
195
s.db,
196
0,
197
+
orm.FilterEq("did", profile.UserDid),
198
)
199
if err != nil {
200
l.Error("failed to get repos", "err", err)
···
220
}
221
l = l.With("profileDid", profile.UserDid)
222
223
+
stars, err := db.GetRepoStars(s.db, 0, orm.FilterEq("did", profile.UserDid))
224
if err != nil {
225
l.Error("failed to get stars", "err", err)
226
s.pages.Error500(w)
···
249
}
250
l = l.With("profileDid", profile.UserDid)
251
252
+
strings, err := db.GetStrings(s.db, 0, orm.FilterEq("did", profile.UserDid))
253
if err != nil {
254
l.Error("failed to get strings", "err", err)
255
s.pages.Error500(w)
···
301
followDids = append(followDids, extractDid(follow))
302
}
303
304
+
profiles, err := db.GetProfiles(s.db, orm.FilterIn("did", followDids))
305
if err != nil {
306
l.Error("failed to get profiles", "followDids", followDids, "err", err)
307
return ¶ms, err
···
704
log.Printf("getting profile data for %s: %s", user.Did, err)
705
}
706
707
+
repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did))
708
if err != nil {
709
log.Printf("getting repos for %s: %s", user.Did, err)
710
}
+3
-3
appview/state/router.go
+3
-3
appview/state/router.go
···
36
router.Get("/favicon.ico", s.Favicon)
37
router.Get("/pwa-manifest.json", s.PWAManifest)
38
router.Get("/robots.txt", s.RobotsTxt)
39
-
router.Get("/sitemap.xml", s.Sitemap)
40
41
userRouter := s.UserRouter(&middleware)
42
standardRouter := s.StandardRouter(&middleware)
···
102
103
// These routes get proxied to the knot
104
r.Get("/info/refs", s.InfoRefs)
105
r.Post("/git-upload-pack", s.UploadPack)
106
r.Post("/git-receive-pack", s.ReceivePack)
107
···
267
s.enforcer,
268
s.pages,
269
s.idResolver,
270
-
s.refResolver,
271
s.db,
272
s.config,
273
s.notifier,
···
284
s.repoResolver,
285
s.pages,
286
s.idResolver,
287
-
s.refResolver,
288
s.db,
289
s.config,
290
s.notifier,
···
36
router.Get("/favicon.ico", s.Favicon)
37
router.Get("/pwa-manifest.json", s.PWAManifest)
38
router.Get("/robots.txt", s.RobotsTxt)
39
40
userRouter := s.UserRouter(&middleware)
41
standardRouter := s.StandardRouter(&middleware)
···
101
102
// These routes get proxied to the knot
103
r.Get("/info/refs", s.InfoRefs)
104
+
r.Post("/git-upload-archive", s.UploadArchive)
105
r.Post("/git-upload-pack", s.UploadPack)
106
r.Post("/git-receive-pack", s.ReceivePack)
107
···
267
s.enforcer,
268
s.pages,
269
s.idResolver,
270
+
s.mentionsResolver,
271
s.db,
272
s.config,
273
s.notifier,
···
284
s.repoResolver,
285
s.pages,
286
s.idResolver,
287
+
s.mentionsResolver,
288
s.db,
289
s.config,
290
s.notifier,
+2
-1
appview/state/spindlestream.go
+2
-1
appview/state/spindlestream.go
···
17
ec "tangled.org/core/eventconsumer"
18
"tangled.org/core/eventconsumer/cursor"
19
"tangled.org/core/log"
20
"tangled.org/core/rbac"
21
spindle "tangled.org/core/spindle/models"
22
)
···
27
28
spindles, err := db.GetSpindles(
29
d,
30
-
db.FilterIsNot("verified", "null"),
31
)
32
if err != nil {
33
return nil, err
···
17
ec "tangled.org/core/eventconsumer"
18
"tangled.org/core/eventconsumer/cursor"
19
"tangled.org/core/log"
20
+
"tangled.org/core/orm"
21
"tangled.org/core/rbac"
22
spindle "tangled.org/core/spindle/models"
23
)
···
28
29
spindles, err := db.GetSpindles(
30
d,
31
+
orm.FilterIsNot("verified", "null"),
32
)
33
if err != nil {
34
return nil, err
+28
-80
appview/state/state.go
+28
-80
appview/state/state.go
···
15
"tangled.org/core/appview/config"
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/indexer"
18
"tangled.org/core/appview/models"
19
"tangled.org/core/appview/notify"
20
dbnotify "tangled.org/core/appview/notify/db"
21
phnotify "tangled.org/core/appview/notify/posthog"
22
"tangled.org/core/appview/oauth"
23
"tangled.org/core/appview/pages"
24
-
"tangled.org/core/appview/refresolver"
25
"tangled.org/core/appview/reporesolver"
26
"tangled.org/core/appview/validator"
27
xrpcclient "tangled.org/core/appview/xrpcclient"
···
30
"tangled.org/core/jetstream"
31
"tangled.org/core/log"
32
tlog "tangled.org/core/log"
33
"tangled.org/core/rbac"
34
"tangled.org/core/tid"
35
···
43
)
44
45
type State struct {
46
-
db *db.DB
47
-
notifier notify.Notifier
48
-
indexer *indexer.Indexer
49
-
oauth *oauth.OAuth
50
-
enforcer *rbac.Enforcer
51
-
pages *pages.Pages
52
-
idResolver *idresolver.Resolver
53
-
refResolver *refresolver.Resolver
54
-
posthog posthog.Client
55
-
jc *jetstream.JetstreamClient
56
-
config *config.Config
57
-
repoResolver *reporesolver.RepoResolver
58
-
knotstream *eventconsumer.Consumer
59
-
spindlestream *eventconsumer.Consumer
60
-
logger *slog.Logger
61
-
validator *validator.Validator
62
}
63
64
func Make(ctx context.Context, config *config.Config) (*State, error) {
···
100
101
repoResolver := reporesolver.New(config, enforcer, d)
102
103
-
refResolver := refresolver.New(config, res, d, log.SubLogger(logger, "refResolver"))
104
105
wrapper := db.DbWrapper{Execer: d}
106
jc, err := jetstream.NewJetstreamClient(
···
182
enforcer,
183
pages,
184
res,
185
-
refResolver,
186
posthog,
187
jc,
188
config,
···
220
221
robotsTxt := `User-agent: *
222
Allow: /
223
-
Disallow: /settings
224
-
Disallow: /notifications
225
-
Disallow: /login
226
-
Disallow: /logout
227
-
Disallow: /signup
228
-
Disallow: /oauth
229
-
Disallow: */settings$
230
-
Disallow: */settings/*
231
-
232
-
Crawl-delay: 1
233
-
234
-
Sitemap: https://tangled.org/sitemap.xml
235
`
236
w.Write([]byte(robotsTxt))
237
}
238
239
-
func (s *State) Sitemap(w http.ResponseWriter, r *http.Request) {
240
-
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
241
-
w.Header().Set("Cache-Control", "public, max-age=3600")
242
-
243
-
// basic sitemap with static pages
244
-
sitemap := `<?xml version="1.0" encoding="UTF-8"?>
245
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
246
-
<url>
247
-
<loc>https://tangled.org</loc>
248
-
<changefreq>daily</changefreq>
249
-
<priority>1.0</priority>
250
-
</url>
251
-
<url>
252
-
<loc>https://tangled.org/timeline</loc>
253
-
<changefreq>hourly</changefreq>
254
-
<priority>0.9</priority>
255
-
</url>
256
-
<url>
257
-
<loc>https://tangled.org/goodfirstissues</loc>
258
-
<changefreq>daily</changefreq>
259
-
<priority>0.8</priority>
260
-
</url>
261
-
<url>
262
-
<loc>https://tangled.org/terms</loc>
263
-
<changefreq>monthly</changefreq>
264
-
<priority>0.3</priority>
265
-
</url>
266
-
<url>
267
-
<loc>https://tangled.org/privacy</loc>
268
-
<changefreq>monthly</changefreq>
269
-
<priority>0.3</priority>
270
-
</url>
271
-
<url>
272
-
<loc>https://tangled.org/brand</loc>
273
-
<changefreq>monthly</changefreq>
274
-
<priority>0.5</priority>
275
-
</url>
276
-
</urlset>`
277
-
w.Write([]byte(sitemap))
278
-
}
279
-
280
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest
281
const manifestJson = `{
282
"name": "tangled",
···
352
return
353
}
354
355
-
gfiLabel, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
356
if err != nil {
357
// non-fatal
358
}
···
376
377
regs, err := db.GetRegistrations(
378
s.db,
379
-
db.FilterEq("did", user.Did),
380
-
db.FilterEq("needs_upgrade", 1),
381
)
382
if err != nil {
383
l.Error("non-fatal: failed to get registrations", "err", err)
···
385
386
spindles, err := db.GetSpindles(
387
s.db,
388
-
db.FilterEq("owner", user.Did),
389
-
db.FilterEq("needs_upgrade", 1),
390
)
391
if err != nil {
392
l.Error("non-fatal: failed to get spindles", "err", err)
···
557
// Check for existing repos
558
existingRepo, err := db.GetRepo(
559
s.db,
560
-
db.FilterEq("did", user.Did),
561
-
db.FilterEq("name", repoName),
562
)
563
if err == nil && existingRepo != nil {
564
l.Info("repo exists")
···
718
}
719
720
func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver, defaults []string) error {
721
-
defaultLabels, err := db.GetLabelDefinitions(e, db.FilterIn("at_uri", defaults))
722
if err != nil {
723
return err
724
}
···
15
"tangled.org/core/appview/config"
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/indexer"
18
+
"tangled.org/core/appview/mentions"
19
"tangled.org/core/appview/models"
20
"tangled.org/core/appview/notify"
21
dbnotify "tangled.org/core/appview/notify/db"
22
phnotify "tangled.org/core/appview/notify/posthog"
23
"tangled.org/core/appview/oauth"
24
"tangled.org/core/appview/pages"
25
"tangled.org/core/appview/reporesolver"
26
"tangled.org/core/appview/validator"
27
xrpcclient "tangled.org/core/appview/xrpcclient"
···
30
"tangled.org/core/jetstream"
31
"tangled.org/core/log"
32
tlog "tangled.org/core/log"
33
+
"tangled.org/core/orm"
34
"tangled.org/core/rbac"
35
"tangled.org/core/tid"
36
···
44
)
45
46
type State struct {
47
+
db *db.DB
48
+
notifier notify.Notifier
49
+
indexer *indexer.Indexer
50
+
oauth *oauth.OAuth
51
+
enforcer *rbac.Enforcer
52
+
pages *pages.Pages
53
+
idResolver *idresolver.Resolver
54
+
mentionsResolver *mentions.Resolver
55
+
posthog posthog.Client
56
+
jc *jetstream.JetstreamClient
57
+
config *config.Config
58
+
repoResolver *reporesolver.RepoResolver
59
+
knotstream *eventconsumer.Consumer
60
+
spindlestream *eventconsumer.Consumer
61
+
logger *slog.Logger
62
+
validator *validator.Validator
63
}
64
65
func Make(ctx context.Context, config *config.Config) (*State, error) {
···
101
102
repoResolver := reporesolver.New(config, enforcer, d)
103
104
+
mentionsResolver := mentions.New(config, res, d, log.SubLogger(logger, "mentionsResolver"))
105
106
wrapper := db.DbWrapper{Execer: d}
107
jc, err := jetstream.NewJetstreamClient(
···
183
enforcer,
184
pages,
185
res,
186
+
mentionsResolver,
187
posthog,
188
jc,
189
config,
···
221
222
robotsTxt := `User-agent: *
223
Allow: /
224
`
225
w.Write([]byte(robotsTxt))
226
}
227
228
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest
229
const manifestJson = `{
230
"name": "tangled",
···
300
return
301
}
302
303
+
gfiLabel, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
304
if err != nil {
305
// non-fatal
306
}
···
324
325
regs, err := db.GetRegistrations(
326
s.db,
327
+
orm.FilterEq("did", user.Did),
328
+
orm.FilterEq("needs_upgrade", 1),
329
)
330
if err != nil {
331
l.Error("non-fatal: failed to get registrations", "err", err)
···
333
334
spindles, err := db.GetSpindles(
335
s.db,
336
+
orm.FilterEq("owner", user.Did),
337
+
orm.FilterEq("needs_upgrade", 1),
338
)
339
if err != nil {
340
l.Error("non-fatal: failed to get spindles", "err", err)
···
505
// Check for existing repos
506
existingRepo, err := db.GetRepo(
507
s.db,
508
+
orm.FilterEq("did", user.Did),
509
+
orm.FilterEq("name", repoName),
510
)
511
if err == nil && existingRepo != nil {
512
l.Info("repo exists")
···
666
}
667
668
func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver, defaults []string) error {
669
+
defaultLabels, err := db.GetLabelDefinitions(e, orm.FilterIn("at_uri", defaults))
670
if err != nil {
671
return err
672
}
+7
-6
appview/strings/strings.go
+7
-6
appview/strings/strings.go
···
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/pages/markup"
19
"tangled.org/core/idresolver"
20
"tangled.org/core/tid"
21
22
"github.com/bluesky-social/indigo/api/atproto"
···
108
strings, err := db.GetStrings(
109
s.Db,
110
0,
111
-
db.FilterEq("did", id.DID),
112
-
db.FilterEq("rkey", rkey),
113
)
114
if err != nil {
115
l.Error("failed to fetch string", "err", err)
···
199
all, err := db.GetStrings(
200
s.Db,
201
0,
202
-
db.FilterEq("did", id.DID),
203
-
db.FilterEq("rkey", rkey),
204
)
205
if err != nil {
206
l.Error("failed to fetch string", "err", err)
···
408
409
if err := db.DeleteString(
410
s.Db,
411
-
db.FilterEq("did", user.Did),
412
-
db.FilterEq("rkey", rkey),
413
); err != nil {
414
fail("Failed to delete string.", err)
415
return
···
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/pages/markup"
19
"tangled.org/core/idresolver"
20
+
"tangled.org/core/orm"
21
"tangled.org/core/tid"
22
23
"github.com/bluesky-social/indigo/api/atproto"
···
109
strings, err := db.GetStrings(
110
s.Db,
111
0,
112
+
orm.FilterEq("did", id.DID),
113
+
orm.FilterEq("rkey", rkey),
114
)
115
if err != nil {
116
l.Error("failed to fetch string", "err", err)
···
200
all, err := db.GetStrings(
201
s.Db,
202
0,
203
+
orm.FilterEq("did", id.DID),
204
+
orm.FilterEq("rkey", rkey),
205
)
206
if err != nil {
207
l.Error("failed to fetch string", "err", err)
···
409
410
if err := db.DeleteString(
411
s.Db,
412
+
orm.FilterEq("did", user.Did),
413
+
orm.FilterEq("rkey", rkey),
414
); err != nil {
415
fail("Failed to delete string.", err)
416
return
+2
-1
appview/validator/issue.go
+2
-1
appview/validator/issue.go
···
6
7
"tangled.org/core/appview/db"
8
"tangled.org/core/appview/models"
9
)
10
11
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
12
// if comments have parents, only ingest ones that are 1 level deep
13
if comment.ReplyTo != nil {
14
-
parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo))
15
if err != nil {
16
return fmt.Errorf("failed to fetch parent comment: %w", err)
17
}
···
6
7
"tangled.org/core/appview/db"
8
"tangled.org/core/appview/models"
9
+
"tangled.org/core/orm"
10
)
11
12
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
13
// if comments have parents, only ingest ones that are 1 level deep
14
if comment.ReplyTo != nil {
15
+
parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo))
16
if err != nil {
17
return fmt.Errorf("failed to fetch parent comment: %w", err)
18
}
+1
-34
crypto/verify.go
+1
-34
crypto/verify.go
···
5
"crypto/sha256"
6
"encoding/base64"
7
"fmt"
8
-
"strings"
9
10
"github.com/hiddeco/sshsig"
11
"golang.org/x/crypto/ssh"
12
-
"tangled.org/core/types"
13
)
14
15
func VerifySignature(pubKey, signature, payload []byte) (error, bool) {
···
28
// multiple algorithms but sha-512 is most secure, and git's ssh signing defaults
29
// to sha-512 for all key types anyway.
30
err = sshsig.Verify(buf, sig, pub, sshsig.HashSHA512, "git")
31
-
return err, err == nil
32
-
}
33
34
-
// VerifyCommitSignature reconstructs the payload used to sign a commit. This is
35
-
// essentially the git cat-file output but without the gpgsig header.
36
-
//
37
-
// Caveats: signature verification will fail on commits with more than one parent,
38
-
// i.e. merge commits, because types.NiceDiff doesn't carry more than one Parent field
39
-
// and we are unable to reconstruct the payload correctly.
40
-
//
41
-
// Ideally this should directly operate on an *object.Commit.
42
-
func VerifyCommitSignature(pubKey string, commit types.NiceDiff) (error, bool) {
43
-
signature := commit.Commit.PGPSignature
44
-
45
-
author := bytes.NewBuffer([]byte{})
46
-
committer := bytes.NewBuffer([]byte{})
47
-
commit.Commit.Author.Encode(author)
48
-
commit.Commit.Committer.Encode(committer)
49
-
50
-
payload := strings.Builder{}
51
-
52
-
fmt.Fprintf(&payload, "tree %s\n", commit.Commit.Tree)
53
-
if commit.Commit.Parent != "" {
54
-
fmt.Fprintf(&payload, "parent %s\n", commit.Commit.Parent)
55
-
}
56
-
fmt.Fprintf(&payload, "author %s\n", author.String())
57
-
fmt.Fprintf(&payload, "committer %s\n", committer.String())
58
-
if commit.Commit.ChangedId != "" {
59
-
fmt.Fprintf(&payload, "change-id %s\n", commit.Commit.ChangedId)
60
-
}
61
-
fmt.Fprintf(&payload, "\n%s", commit.Commit.Message)
62
-
63
-
return VerifySignature([]byte(pubKey), []byte(signature), []byte(payload.String()))
64
}
65
66
// SSHFingerprint computes the fingerprint of the supplied ssh pubkey.
···
5
"crypto/sha256"
6
"encoding/base64"
7
"fmt"
8
9
"github.com/hiddeco/sshsig"
10
"golang.org/x/crypto/ssh"
11
)
12
13
func VerifySignature(pubKey, signature, payload []byte) (error, bool) {
···
26
// multiple algorithms but sha-512 is most secure, and git's ssh signing defaults
27
// to sha-512 for all key types anyway.
28
err = sshsig.Verify(buf, sig, pub, sshsig.HashSHA512, "git")
29
30
+
return err, err == nil
31
}
32
33
// SSHFingerprint computes the fingerprint of the supplied ssh pubkey.
+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 (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
···
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
+2
-3
go.mod
+2
-3
go.mod
···
1
module tangled.org/core
2
3
-
go 1.24.4
4
5
require (
6
github.com/Blank-Xu/sql-adapter v1.1.1
···
44
github.com/stretchr/testify v1.10.0
45
github.com/urfave/cli/v3 v3.3.3
46
github.com/whyrusleeping/cbor-gen v0.3.1
47
-
github.com/wyatt915/goldmark-treeblood v0.0.1
48
github.com/yuin/goldmark v1.7.13
49
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
50
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab
···
190
github.com/vmihailenco/go-tinylfu v0.2.2 // indirect
191
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
192
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
193
-
github.com/wyatt915/treeblood v0.1.16 // indirect
194
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
195
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
196
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
197
go.etcd.io/bbolt v1.4.0 // indirect
···
1
module tangled.org/core
2
3
+
go 1.25.0
4
5
require (
6
github.com/Blank-Xu/sql-adapter v1.1.1
···
44
github.com/stretchr/testify v1.10.0
45
github.com/urfave/cli/v3 v3.3.3
46
github.com/whyrusleeping/cbor-gen v0.3.1
47
github.com/yuin/goldmark v1.7.13
48
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
49
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab
···
189
github.com/vmihailenco/go-tinylfu v0.2.2 // indirect
190
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
191
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
192
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
193
+
github.com/yuin/goldmark-emoji v1.0.6 // indirect
194
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
195
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
196
go.etcd.io/bbolt v1.4.0 // indirect
+2
-4
go.sum
+2
-4
go.sum
···
495
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
496
github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0=
497
github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
498
-
github.com/wyatt915/goldmark-treeblood v0.0.1 h1:6vLJcjFrHgE4ASu2ga4hqIQmbvQLU37v53jlHZ3pqDs=
499
-
github.com/wyatt915/goldmark-treeblood v0.0.1/go.mod h1:SmcJp5EBaV17rroNlgNQFydYwy0+fv85CUr/ZaCz208=
500
-
github.com/wyatt915/treeblood v0.1.16 h1:byxNbWZhnPDxdTp7W5kQhCeaY8RBVmojTFz1tEHgg8Y=
501
-
github.com/wyatt915/treeblood v0.1.16/go.mod h1:i7+yhhmzdDP17/97pIsOSffw74EK/xk+qJ0029cSXUY=
502
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
503
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
504
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
···
509
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
510
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
511
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
512
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
513
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
514
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A=
···
495
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
496
github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0=
497
github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
498
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
499
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
500
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
···
505
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
506
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
507
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
508
+
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
509
+
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
510
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
511
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
512
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A=
+4
-4
hook/hook.go
+4
-4
hook/hook.go
···
48
},
49
Commands: []*cli.Command{
50
{
51
-
Name: "post-recieve",
52
-
Usage: "sends a post-recieve hook to the knot (waits for stdin)",
53
-
Action: postRecieve,
54
},
55
},
56
}
57
}
58
59
-
func postRecieve(ctx context.Context, cmd *cli.Command) error {
60
gitDir := cmd.String("git-dir")
61
userDid := cmd.String("user-did")
62
userHandle := cmd.String("user-handle")
···
48
},
49
Commands: []*cli.Command{
50
{
51
+
Name: "post-receive",
52
+
Usage: "sends a post-receive hook to the knot (waits for stdin)",
53
+
Action: postReceive,
54
},
55
},
56
}
57
}
58
59
+
func postReceive(ctx context.Context, cmd *cli.Command) error {
60
gitDir := cmd.String("git-dir")
61
userDid := cmd.String("user-did")
62
userHandle := cmd.String("user-handle")
+1
-1
hook/setup.go
+1
-1
hook/setup.go
···
138
option_var="GIT_PUSH_OPTION_$i"
139
push_options+=(-push-option "${!option_var}")
140
done
141
-
%s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-recieve
142
`, executablePath, config.internalApi)
143
144
return os.WriteFile(hookPath, []byte(hookContent), 0755)
···
138
option_var="GIT_PUSH_OPTION_$i"
139
push_options+=(-push-option "${!option_var}")
140
done
141
+
%s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-receive
142
`, executablePath, config.internalApi)
143
144
return os.WriteFile(hookPath, []byte(hookContent), 0755)
+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
-
}
···
+1
-17
knotserver/git/diff.go
+1
-17
knotserver/git/diff.go
···
77
nd.Diff = append(nd.Diff, ndiff)
78
}
79
80
-
nd.Stat.FilesChanged = len(diffs)
81
-
nd.Commit.This = c.Hash.String()
82
-
nd.Commit.PGPSignature = c.PGPSignature
83
-
nd.Commit.Committer = c.Committer
84
-
nd.Commit.Tree = c.TreeHash.String()
85
-
86
-
if parent.Hash.IsZero() {
87
-
nd.Commit.Parent = ""
88
-
} else {
89
-
nd.Commit.Parent = parent.Hash.String()
90
-
}
91
-
nd.Commit.Author = c.Author
92
-
nd.Commit.Message = c.Message
93
-
94
-
if v, ok := c.ExtraHeaders["change-id"]; ok {
95
-
nd.Commit.ChangedId = string(v)
96
-
}
97
98
return &nd, nil
99
}
+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
+6
-1
knotserver/xrpc/repo_log.go
+6
-1
knotserver/xrpc/repo_log.go
···
62
return
63
}
64
65
+
tcommits := make([]types.Commit, len(commits))
66
+
for i, c := range commits {
67
+
tcommits[i].FromGoGitCommit(c)
68
+
}
69
+
70
// Create response using existing types.RepoLogResponse
71
response := types.RepoLogResponse{
72
+
Commits: tcommits,
73
Ref: ref,
74
Page: (offset / limit) + 1,
75
PerPage: limit,
-30
nix/gomod2nix.toml
-30
nix/gomod2nix.toml
···
165
[mod."github.com/davecgh/go-spew"]
166
version = "v1.1.2-0.20180830191138-d8f796af33cc"
167
hash = "sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc="
168
-
[mod."github.com/decred/dcrd/dcrec/secp256k1/v4"]
169
-
version = "v4.4.0"
170
-
hash = "sha256-qrhEIwhDll3cxoVpMbm1NQ9/HTI42S7ms8Buzlo5HCg="
171
[mod."github.com/dgraph-io/ristretto"]
172
version = "v0.2.0"
173
hash = "sha256-bnpxX+oO/Qf7IJevA0gsbloVoqRx+5bh7RQ9d9eLNYw="
···
373
[mod."github.com/klauspost/cpuid/v2"]
374
version = "v2.3.0"
375
hash = "sha256-50JhbQyT67BK38HIdJihPtjV7orYp96HknI2VP7A9Yc="
376
-
[mod."github.com/lestrrat-go/blackmagic"]
377
-
version = "v1.0.4"
378
-
hash = "sha256-HmWOpwoPDNMwLdOi7onNn3Sb+ZsAa3Ai3gVBbXmQ0e8="
379
-
[mod."github.com/lestrrat-go/httpcc"]
380
-
version = "v1.0.1"
381
-
hash = "sha256-SMRSwJpqDIs/xL0l2e8vP0W65qtCHX2wigcOeqPJmos="
382
-
[mod."github.com/lestrrat-go/httprc"]
383
-
version = "v1.0.6"
384
-
hash = "sha256-mfZzePEhrmyyu/avEBd2MsDXyto8dq5+fyu5lA8GUWM="
385
-
[mod."github.com/lestrrat-go/iter"]
386
-
version = "v1.0.2"
387
-
hash = "sha256-30tErRf7Qu/NOAt1YURXY/XJSA6sCr6hYQfO8QqHrtw="
388
-
[mod."github.com/lestrrat-go/jwx/v2"]
389
-
version = "v2.1.6"
390
-
hash = "sha256-0LszXRZIba+X8AOrs3T4uanAUafBdlVB8/MpUNEFpbc="
391
-
[mod."github.com/lestrrat-go/option"]
392
-
version = "v1.0.1"
393
-
hash = "sha256-jVcIYYVsxElIS/l2akEw32vdEPR8+anR6oeT1FoYULI="
394
[mod."github.com/lucasb-eyer/go-colorful"]
395
version = "v1.2.0"
396
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
···
511
[mod."github.com/ryanuber/go-glob"]
512
version = "v1.0.0"
513
hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY="
514
-
[mod."github.com/segmentio/asm"]
515
-
version = "v1.2.0"
516
-
hash = "sha256-zbNuKxNrUDUc6IlmRQNuJQzVe5Ol/mqp7srDg9IMMqs="
517
[mod."github.com/sergi/go-diff"]
518
version = "v1.1.0"
519
hash = "sha256-8NJMabldpf40uwQN20T6QXx5KORDibCBJL02KD661xY="
···
548
[mod."github.com/whyrusleeping/cbor-gen"]
549
version = "v0.3.1"
550
hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc="
551
-
[mod."github.com/wyatt915/goldmark-treeblood"]
552
-
version = "v0.0.1"
553
-
hash = "sha256-hAVFaktO02MiiqZFffr8ZlvFEfwxw4Y84OZ2t7e5G7g="
554
-
[mod."github.com/wyatt915/treeblood"]
555
-
version = "v0.1.16"
556
-
hash = "sha256-T68sa+iVx0qY7dDjXEAJvRWQEGXYIpUsf9tcWwO1tIw="
557
[mod."github.com/xo/terminfo"]
558
version = "v0.0.0-20220910002029-abceb7e1c41e"
559
hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU="
···
165
[mod."github.com/davecgh/go-spew"]
166
version = "v1.1.2-0.20180830191138-d8f796af33cc"
167
hash = "sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc="
168
[mod."github.com/dgraph-io/ristretto"]
169
version = "v0.2.0"
170
hash = "sha256-bnpxX+oO/Qf7IJevA0gsbloVoqRx+5bh7RQ9d9eLNYw="
···
370
[mod."github.com/klauspost/cpuid/v2"]
371
version = "v2.3.0"
372
hash = "sha256-50JhbQyT67BK38HIdJihPtjV7orYp96HknI2VP7A9Yc="
373
[mod."github.com/lucasb-eyer/go-colorful"]
374
version = "v1.2.0"
375
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
···
490
[mod."github.com/ryanuber/go-glob"]
491
version = "v1.0.0"
492
hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY="
493
[mod."github.com/sergi/go-diff"]
494
version = "v1.1.0"
495
hash = "sha256-8NJMabldpf40uwQN20T6QXx5KORDibCBJL02KD661xY="
···
524
[mod."github.com/whyrusleeping/cbor-gen"]
525
version = "v0.3.1"
526
hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc="
527
[mod."github.com/xo/terminfo"]
528
version = "v0.0.0-20220910002029-abceb7e1c41e"
529
hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU="
+7
-5
nix/pkgs/sqlite-lib.nix
+7
-5
nix/pkgs/sqlite-lib.nix
···
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
···
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
+122
orm/orm.go
+122
orm/orm.go
···
···
1
+
package orm
2
+
3
+
import (
4
+
"context"
5
+
"database/sql"
6
+
"fmt"
7
+
"log/slog"
8
+
"reflect"
9
+
"strings"
10
+
)
11
+
12
+
type migrationFn = func(*sql.Tx) error
13
+
14
+
func RunMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
15
+
logger = logger.With("migration", name)
16
+
17
+
tx, err := c.BeginTx(context.Background(), nil)
18
+
if err != nil {
19
+
return err
20
+
}
21
+
defer tx.Rollback()
22
+
23
+
var exists bool
24
+
err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
25
+
if err != nil {
26
+
return err
27
+
}
28
+
29
+
if !exists {
30
+
// run migration
31
+
err = migrationFn(tx)
32
+
if err != nil {
33
+
logger.Error("failed to run migration", "err", err)
34
+
return err
35
+
}
36
+
37
+
// mark migration as complete
38
+
_, err = tx.Exec("insert into migrations (name) values (?)", name)
39
+
if err != nil {
40
+
logger.Error("failed to mark migration as complete", "err", err)
41
+
return err
42
+
}
43
+
44
+
// commit the transaction
45
+
if err := tx.Commit(); err != nil {
46
+
return err
47
+
}
48
+
49
+
logger.Info("migration applied successfully")
50
+
} else {
51
+
logger.Warn("skipped migration, already applied")
52
+
}
53
+
54
+
return nil
55
+
}
56
+
57
+
type Filter struct {
58
+
Key string
59
+
arg any
60
+
Cmp string
61
+
}
62
+
63
+
func newFilter(key, cmp string, arg any) Filter {
64
+
return Filter{
65
+
Key: key,
66
+
arg: arg,
67
+
Cmp: cmp,
68
+
}
69
+
}
70
+
71
+
func FilterEq(key string, arg any) Filter { return newFilter(key, "=", arg) }
72
+
func FilterNotEq(key string, arg any) Filter { return newFilter(key, "<>", arg) }
73
+
func FilterGte(key string, arg any) Filter { return newFilter(key, ">=", arg) }
74
+
func FilterLte(key string, arg any) Filter { return newFilter(key, "<=", arg) }
75
+
func FilterIs(key string, arg any) Filter { return newFilter(key, "is", arg) }
76
+
func FilterIsNot(key string, arg any) Filter { return newFilter(key, "is not", arg) }
77
+
func FilterIn(key string, arg any) Filter { return newFilter(key, "in", arg) }
78
+
func FilterLike(key string, arg any) Filter { return newFilter(key, "like", arg) }
79
+
func FilterNotLike(key string, arg any) Filter { return newFilter(key, "not like", arg) }
80
+
func FilterContains(key string, arg any) Filter {
81
+
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
82
+
}
83
+
84
+
func (f Filter) Condition() string {
85
+
rv := reflect.ValueOf(f.arg)
86
+
kind := rv.Kind()
87
+
88
+
// if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
89
+
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
90
+
if rv.Len() == 0 {
91
+
// always false
92
+
return "1 = 0"
93
+
}
94
+
95
+
placeholders := make([]string, rv.Len())
96
+
for i := range placeholders {
97
+
placeholders[i] = "?"
98
+
}
99
+
100
+
return fmt.Sprintf("%s %s (%s)", f.Key, f.Cmp, strings.Join(placeholders, ", "))
101
+
}
102
+
103
+
return fmt.Sprintf("%s %s ?", f.Key, f.Cmp)
104
+
}
105
+
106
+
func (f Filter) Arg() []any {
107
+
rv := reflect.ValueOf(f.arg)
108
+
kind := rv.Kind()
109
+
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
110
+
if rv.Len() == 0 {
111
+
return nil
112
+
}
113
+
114
+
out := make([]any, rv.Len())
115
+
for i := range rv.Len() {
116
+
out[i] = rv.Index(i).Interface()
117
+
}
118
+
return out
119
+
}
120
+
121
+
return []any{f.arg}
122
+
}
-1
patchutil/patchutil.go
-1
patchutil/patchutil.go
+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
spindle/db/repos.go
+1
spindle/db/repos.go
+17
-20
spindle/engine/engine.go
+17
-20
spindle/engine/engine.go
···
3
import (
4
"context"
5
"errors"
6
-
"fmt"
7
"log/slog"
8
9
securejoin "github.com/cyphar/filepath-securejoin"
10
-
"golang.org/x/sync/errgroup"
11
"tangled.org/core/notifier"
12
"tangled.org/core/spindle/config"
13
"tangled.org/core/spindle/db"
···
31
}
32
}
33
34
-
eg, ctx := errgroup.WithContext(ctx)
35
for eng, wfs := range pipeline.Workflows {
36
workflowTimeout := eng.WorkflowTimeout()
37
l.Info("using workflow timeout", "timeout", workflowTimeout)
38
39
for _, w := range wfs {
40
-
eg.Go(func() error {
41
wid := models.WorkflowId{
42
PipelineId: pipelineId,
43
Name: w.Name,
···
45
46
err := db.StatusRunning(wid, n)
47
if err != nil {
48
-
return err
49
}
50
51
err = eng.SetupWorkflow(ctx, wid, &w)
···
61
62
dbErr := db.StatusFailed(wid, err.Error(), -1, n)
63
if dbErr != nil {
64
-
return dbErr
65
}
66
-
return err
67
}
68
defer eng.DestroyWorkflow(ctx, wid)
69
···
99
if errors.Is(err, ErrTimedOut) {
100
dbErr := db.StatusTimeout(wid, n)
101
if dbErr != nil {
102
-
return dbErr
103
}
104
} else {
105
dbErr := db.StatusFailed(wid, err.Error(), -1, n)
106
if dbErr != nil {
107
-
return dbErr
108
}
109
}
110
-
111
-
return fmt.Errorf("starting steps image: %w", err)
112
}
113
}
114
115
err = db.StatusSuccess(wid, n)
116
if err != nil {
117
-
return err
118
}
119
-
120
-
return nil
121
-
})
122
}
123
}
124
125
-
if err := eg.Wait(); err != nil {
126
-
l.Error("failed to run one or more workflows", "err", err)
127
-
} else {
128
-
l.Info("successfully ran full pipeline")
129
-
}
130
}
···
3
import (
4
"context"
5
"errors"
6
"log/slog"
7
+
"sync"
8
9
securejoin "github.com/cyphar/filepath-securejoin"
10
"tangled.org/core/notifier"
11
"tangled.org/core/spindle/config"
12
"tangled.org/core/spindle/db"
···
30
}
31
}
32
33
+
var wg sync.WaitGroup
34
for eng, wfs := range pipeline.Workflows {
35
workflowTimeout := eng.WorkflowTimeout()
36
l.Info("using workflow timeout", "timeout", workflowTimeout)
37
38
for _, w := range wfs {
39
+
wg.Add(1)
40
+
go func() {
41
+
defer wg.Done()
42
+
43
wid := models.WorkflowId{
44
PipelineId: pipelineId,
45
Name: w.Name,
···
47
48
err := db.StatusRunning(wid, n)
49
if err != nil {
50
+
l.Error("failed to set workflow status to running", "wid", wid, "err", err)
51
+
return
52
}
53
54
err = eng.SetupWorkflow(ctx, wid, &w)
···
64
65
dbErr := db.StatusFailed(wid, err.Error(), -1, n)
66
if dbErr != nil {
67
+
l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr)
68
}
69
+
return
70
}
71
defer eng.DestroyWorkflow(ctx, wid)
72
···
102
if errors.Is(err, ErrTimedOut) {
103
dbErr := db.StatusTimeout(wid, n)
104
if dbErr != nil {
105
+
l.Error("failed to set workflow status to timeout", "wid", wid, "err", dbErr)
106
}
107
} else {
108
dbErr := db.StatusFailed(wid, err.Error(), -1, n)
109
if dbErr != nil {
110
+
l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr)
111
}
112
}
113
+
return
114
}
115
}
116
117
err = db.StatusSuccess(wid, n)
118
if err != nil {
119
+
l.Error("failed to set workflow status to success", "wid", wid, "err", err)
120
}
121
+
}()
122
}
123
}
124
125
+
wg.Wait()
126
+
l.Info("all workflows completed")
127
}
+5
-3
spindle/engines/nixery/engine.go
+5
-3
spindle/engines/nixery/engine.go
···
294
workflowEnvs.AddEnv(s.Key, s.Value)
295
}
296
297
-
step := w.Steps[idx].(Step)
298
299
select {
300
case <-ctx.Done():
···
303
}
304
305
envs := append(EnvVars(nil), workflowEnvs...)
306
-
for k, v := range step.environment {
307
-
envs.AddEnv(k, v)
308
}
309
envs.AddEnv("HOME", homeDir)
310
···
294
workflowEnvs.AddEnv(s.Key, s.Value)
295
}
296
297
+
step := w.Steps[idx]
298
299
select {
300
case <-ctx.Done():
···
303
}
304
305
envs := append(EnvVars(nil), workflowEnvs...)
306
+
if nixStep, ok := step.(Step); ok {
307
+
for k, v := range nixStep.environment {
308
+
envs.AddEnv(k, v)
309
+
}
310
}
311
envs.AddEnv("HOME", homeDir)
312
+199
types/commit.go
+199
types/commit.go
···
···
1
+
package types
2
+
3
+
import (
4
+
"bytes"
5
+
"encoding/json"
6
+
"fmt"
7
+
"maps"
8
+
"regexp"
9
+
"strings"
10
+
11
+
"github.com/go-git/go-git/v5/plumbing"
12
+
"github.com/go-git/go-git/v5/plumbing/object"
13
+
)
14
+
15
+
type Commit struct {
16
+
// hash of the commit object.
17
+
Hash plumbing.Hash `json:"hash,omitempty"`
18
+
19
+
// author is the original author of the commit.
20
+
Author object.Signature `json:"author"`
21
+
22
+
// committer is the one performing the commit, might be different from author.
23
+
Committer object.Signature `json:"committer"`
24
+
25
+
// message is the commit message, contains arbitrary text.
26
+
Message string `json:"message"`
27
+
28
+
// treehash is the hash of the root tree of the commit.
29
+
Tree string `json:"tree"`
30
+
31
+
// parents are the hashes of the parent commits of the commit.
32
+
ParentHashes []plumbing.Hash `json:"parent_hashes,omitempty"`
33
+
34
+
// pgpsignature is the pgp signature of the commit.
35
+
PGPSignature string `json:"pgp_signature,omitempty"`
36
+
37
+
// mergetag is the embedded tag object when a merge commit is created by
38
+
// merging a signed tag.
39
+
MergeTag string `json:"merge_tag,omitempty"`
40
+
41
+
// changeid is a unique identifier for the change (e.g., gerrit change-id).
42
+
ChangeId string `json:"change_id,omitempty"`
43
+
44
+
// extraheaders contains additional headers not captured by other fields.
45
+
ExtraHeaders map[string][]byte `json:"extra_headers,omitempty"`
46
+
47
+
// deprecated: kept for backwards compatibility with old json format.
48
+
This string `json:"this,omitempty"`
49
+
50
+
// deprecated: kept for backwards compatibility with old json format.
51
+
Parent string `json:"parent,omitempty"`
52
+
}
53
+
54
+
// types.Commit is an unify two commit structs:
55
+
// - git.object.Commit from
56
+
// - types.NiceDiff.commit
57
+
//
58
+
// to do this in backwards compatible fashion, we define the base struct
59
+
// to use the same fields as NiceDiff.Commit, and then we also unmarshal
60
+
// the struct fields from go-git structs, this custom unmarshal makes sense
61
+
// of both representations and unifies them to have maximal data in either
62
+
// form.
63
+
func (c *Commit) UnmarshalJSON(data []byte) error {
64
+
type Alias Commit
65
+
66
+
aux := &struct {
67
+
*object.Commit
68
+
*Alias
69
+
}{
70
+
Alias: (*Alias)(c),
71
+
}
72
+
73
+
if err := json.Unmarshal(data, aux); err != nil {
74
+
return err
75
+
}
76
+
77
+
c.FromGoGitCommit(aux.Commit)
78
+
79
+
return nil
80
+
}
81
+
82
+
// fill in as much of Commit as possible from the given go-git commit
83
+
func (c *Commit) FromGoGitCommit(gc *object.Commit) {
84
+
if gc == nil {
85
+
return
86
+
}
87
+
88
+
if c.Hash.IsZero() {
89
+
c.Hash = gc.Hash
90
+
}
91
+
if c.This == "" {
92
+
c.This = gc.Hash.String()
93
+
}
94
+
if isEmptySignature(c.Author) {
95
+
c.Author = gc.Author
96
+
}
97
+
if isEmptySignature(c.Committer) {
98
+
c.Committer = gc.Committer
99
+
}
100
+
if c.Message == "" {
101
+
c.Message = gc.Message
102
+
}
103
+
if c.Tree == "" {
104
+
c.Tree = gc.TreeHash.String()
105
+
}
106
+
if c.PGPSignature == "" {
107
+
c.PGPSignature = gc.PGPSignature
108
+
}
109
+
if c.MergeTag == "" {
110
+
c.MergeTag = gc.MergeTag
111
+
}
112
+
113
+
if len(c.ParentHashes) == 0 {
114
+
c.ParentHashes = gc.ParentHashes
115
+
}
116
+
if c.Parent == "" && len(gc.ParentHashes) > 0 {
117
+
c.Parent = gc.ParentHashes[0].String()
118
+
}
119
+
120
+
if len(c.ExtraHeaders) == 0 {
121
+
c.ExtraHeaders = make(map[string][]byte)
122
+
maps.Copy(c.ExtraHeaders, gc.ExtraHeaders)
123
+
}
124
+
125
+
if c.ChangeId == "" {
126
+
if v, ok := gc.ExtraHeaders["change-id"]; ok {
127
+
c.ChangeId = string(v)
128
+
}
129
+
}
130
+
}
131
+
132
+
func isEmptySignature(s object.Signature) bool {
133
+
return s.Email == "" && s.Name == "" && s.When.IsZero()
134
+
}
135
+
136
+
// produce a verifiable payload from this commit's metadata
137
+
func (c *Commit) Payload() string {
138
+
author := bytes.NewBuffer([]byte{})
139
+
c.Author.Encode(author)
140
+
141
+
committer := bytes.NewBuffer([]byte{})
142
+
c.Committer.Encode(committer)
143
+
144
+
payload := strings.Builder{}
145
+
146
+
fmt.Fprintf(&payload, "tree %s\n", c.Tree)
147
+
148
+
if len(c.ParentHashes) > 0 {
149
+
for _, p := range c.ParentHashes {
150
+
fmt.Fprintf(&payload, "parent %s\n", p.String())
151
+
}
152
+
} else {
153
+
// present for backwards compatibility
154
+
fmt.Fprintf(&payload, "parent %s\n", c.Parent)
155
+
}
156
+
157
+
fmt.Fprintf(&payload, "author %s\n", author.String())
158
+
fmt.Fprintf(&payload, "committer %s\n", committer.String())
159
+
160
+
if c.ChangeId != "" {
161
+
fmt.Fprintf(&payload, "change-id %s\n", c.ChangeId)
162
+
} else if v, ok := c.ExtraHeaders["change-id"]; ok {
163
+
fmt.Fprintf(&payload, "change-id %s\n", string(v))
164
+
}
165
+
166
+
fmt.Fprintf(&payload, "\n%s", c.Message)
167
+
168
+
return payload.String()
169
+
}
170
+
171
+
var (
172
+
coAuthorRegex = regexp.MustCompile(`(?im)^Co-authored-by:\s*(.+?)\s*<([^>]+)>`)
173
+
)
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,
192
+
Email: email,
193
+
When: commit.Committer.When,
194
+
})
195
+
}
196
+
}
197
+
198
+
return coAuthors
199
+
}
+2
-12
types/diff.go
+2
-12
types/diff.go
···
2
3
import (
4
"github.com/bluekeyes/go-gitdiff/gitdiff"
5
-
"github.com/go-git/go-git/v5/plumbing/object"
6
)
7
8
type DiffOpts struct {
···
43
44
// A nicer git diff representation.
45
type NiceDiff struct {
46
-
Commit struct {
47
-
Message string `json:"message"`
48
-
Author object.Signature `json:"author"`
49
-
This string `json:"this"`
50
-
Parent string `json:"parent"`
51
-
PGPSignature string `json:"pgp_signature"`
52
-
Committer object.Signature `json:"committer"`
53
-
Tree string `json:"tree"`
54
-
ChangedId string `json:"change_id"`
55
-
} `json:"commit"`
56
-
Stat struct {
57
FilesChanged int `json:"files_changed"`
58
Insertions int `json:"insertions"`
59
Deletions int `json:"deletions"`
···
2
3
import (
4
"github.com/bluekeyes/go-gitdiff/gitdiff"
5
)
6
7
type DiffOpts struct {
···
42
43
// A nicer git diff representation.
44
type NiceDiff struct {
45
+
Commit Commit `json:"commit"`
46
+
Stat struct {
47
FilesChanged int `json:"files_changed"`
48
Insertions int `json:"insertions"`
49
Deletions int `json:"deletions"`
+17
-17
types/repo.go
+17
-17
types/repo.go
···
8
)
9
10
type RepoIndexResponse struct {
11
-
IsEmpty bool `json:"is_empty"`
12
-
Ref string `json:"ref,omitempty"`
13
-
Readme string `json:"readme,omitempty"`
14
-
ReadmeFileName string `json:"readme_file_name,omitempty"`
15
-
Commits []*object.Commit `json:"commits,omitempty"`
16
-
Description string `json:"description,omitempty"`
17
-
Files []NiceTree `json:"files,omitempty"`
18
-
Branches []Branch `json:"branches,omitempty"`
19
-
Tags []*TagReference `json:"tags,omitempty"`
20
-
TotalCommits int `json:"total_commits,omitempty"`
21
}
22
23
type RepoLogResponse struct {
24
-
Commits []*object.Commit `json:"commits,omitempty"`
25
-
Ref string `json:"ref,omitempty"`
26
-
Description string `json:"description,omitempty"`
27
-
Log bool `json:"log,omitempty"`
28
-
Total int `json:"total,omitempty"`
29
-
Page int `json:"page,omitempty"`
30
-
PerPage int `json:"per_page,omitempty"`
31
}
32
33
type RepoCommitResponse struct {
···
8
)
9
10
type RepoIndexResponse struct {
11
+
IsEmpty bool `json:"is_empty"`
12
+
Ref string `json:"ref,omitempty"`
13
+
Readme string `json:"readme,omitempty"`
14
+
ReadmeFileName string `json:"readme_file_name,omitempty"`
15
+
Commits []Commit `json:"commits,omitempty"`
16
+
Description string `json:"description,omitempty"`
17
+
Files []NiceTree `json:"files,omitempty"`
18
+
Branches []Branch `json:"branches,omitempty"`
19
+
Tags []*TagReference `json:"tags,omitempty"`
20
+
TotalCommits int `json:"total_commits,omitempty"`
21
}
22
23
type RepoLogResponse struct {
24
+
Commits []Commit `json:"commits,omitempty"`
25
+
Ref string `json:"ref,omitempty"`
26
+
Description string `json:"description,omitempty"`
27
+
Log bool `json:"log,omitempty"`
28
+
Total int `json:"total,omitempty"`
29
+
Page int `json:"page,omitempty"`
30
+
PerPage int `json:"per_page,omitempty"`
31
}
32
33
type RepoCommitResponse struct {