forked from tangled.org/core
Monorepo for Tangled

Compare changes

Choose any two refs to compare.

Changed files
+2068 -1370
appview
crypto
hook
knotserver
nix
orm
patchutil
sets
spindle
db
engine
engines
nixery
types
+6 -45
appview/commitverify/verify.go
··· 3 3 import ( 4 4 "log" 5 5 6 - "github.com/go-git/go-git/v5/plumbing/object" 7 6 "tangled.org/core/appview/db" 8 7 "tangled.org/core/appview/models" 9 8 "tangled.org/core/crypto" ··· 35 34 return "" 36 35 } 37 36 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) { 37 + func GetVerifiedCommits(e db.Execer, emailToDid map[string]string, ndCommits []types.Commit) (VerifiedCommits, error) { 47 38 vcs := VerifiedCommits{} 48 39 49 40 didPubkeyCache := make(map[string][]models.PublicKey) 50 41 51 42 for _, commit := range ndCommits { 52 - c := commit.Commit 53 - 54 - committerEmail := c.Committer.Email 43 + committerEmail := commit.Committer.Email 55 44 if did, exists := emailToDid[committerEmail]; exists { 56 45 // check if we've already fetched public keys for this did 57 46 pubKeys, ok := didPubkeyCache[did] ··· 67 56 } 68 57 69 58 // try to verify with any associated pubkeys 59 + payload := commit.Payload() 60 + signature := commit.PGPSignature 70 61 for _, pk := range pubKeys { 71 - if _, ok := crypto.VerifyCommitSignature(pk.Key, commit); ok { 62 + if _, ok := crypto.VerifySignature([]byte(pk.Key), []byte(signature), []byte(payload)); ok { 72 63 73 64 fp, err := crypto.SSHFingerprint(pk.Key) 74 65 if err != nil { 75 66 log.Println("error computing ssh fingerprint:", err) 76 67 } 77 68 78 - vc := verifiedCommit{fingerprint: fp, hash: c.This} 69 + vc := verifiedCommit{fingerprint: fp, hash: commit.This} 79 70 vcs[vc] = struct{}{} 80 71 break 81 72 } ··· 86 77 87 78 return vcs, nil 88 79 } 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 -2
appview/db/artifact.go
··· 8 8 "github.com/go-git/go-git/v5/plumbing" 9 9 "github.com/ipfs/go-cid" 10 10 "tangled.org/core/appview/models" 11 + "tangled.org/core/orm" 11 12 ) 12 13 13 14 func AddArtifact(e Execer, artifact models.Artifact) error { ··· 37 38 return err 38 39 } 39 40 40 - func GetArtifact(e Execer, filters ...filter) ([]models.Artifact, error) { 41 + func GetArtifact(e Execer, filters ...orm.Filter) ([]models.Artifact, error) { 41 42 var artifacts []models.Artifact 42 43 43 44 var conditions []string ··· 109 110 return artifacts, nil 110 111 } 111 112 112 - func DeleteArtifact(e Execer, filters ...filter) error { 113 + func DeleteArtifact(e Execer, filters ...orm.Filter) error { 113 114 var conditions []string 114 115 var args []any 115 116 for _, filter := range filters {
+4 -3
appview/db/collaborators.go
··· 6 6 "time" 7 7 8 8 "tangled.org/core/appview/models" 9 + "tangled.org/core/orm" 9 10 ) 10 11 11 12 func AddCollaborator(e Execer, c models.Collaborator) error { ··· 16 17 return err 17 18 } 18 19 19 - func DeleteCollaborator(e Execer, filters ...filter) error { 20 + func DeleteCollaborator(e Execer, filters ...orm.Filter) error { 20 21 var conditions []string 21 22 var args []any 22 23 for _, filter := range filters { ··· 58 59 return nil, nil 59 60 } 60 61 61 - return GetRepos(e, 0, FilterIn("at_uri", repoAts)) 62 + return GetRepos(e, 0, orm.FilterIn("at_uri", repoAts)) 62 63 } 63 64 64 - func GetCollaborators(e Execer, filters ...filter) ([]models.Collaborator, error) { 65 + func GetCollaborators(e Execer, filters ...orm.Filter) ([]models.Collaborator, error) { 65 66 var collaborators []models.Collaborator 66 67 var conditions []string 67 68 var args []any
+24 -137
appview/db/db.go
··· 3 3 import ( 4 4 "context" 5 5 "database/sql" 6 - "fmt" 7 6 "log/slog" 8 - "reflect" 9 7 "strings" 10 8 11 9 _ "github.com/mattn/go-sqlite3" 12 10 "tangled.org/core/log" 11 + "tangled.org/core/orm" 13 12 ) 14 13 15 14 type DB struct { ··· 584 583 } 585 584 586 585 // run migrations 587 - runMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error { 586 + orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error { 588 587 tx.Exec(` 589 588 alter table repos add column description text check (length(description) <= 200); 590 589 `) 591 590 return nil 592 591 }) 593 592 594 - runMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error { 593 + orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error { 595 594 // add unconstrained column 596 595 _, err := tx.Exec(` 597 596 alter table public_keys ··· 614 613 return nil 615 614 }) 616 615 617 - runMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error { 616 + orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error { 618 617 _, err := tx.Exec(` 619 618 alter table comments drop column comment_at; 620 619 alter table comments add column rkey text; ··· 622 621 return err 623 622 }) 624 623 625 - runMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 624 + orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 626 625 _, err := tx.Exec(` 627 626 alter table comments add column deleted text; -- timestamp 628 627 alter table comments add column edited text; -- timestamp ··· 630 629 return err 631 630 }) 632 631 633 - runMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 632 + orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 634 633 _, err := tx.Exec(` 635 634 alter table pulls add column source_branch text; 636 635 alter table pulls add column source_repo_at text; ··· 639 638 return err 640 639 }) 641 640 642 - runMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error { 641 + orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error { 643 642 _, err := tx.Exec(` 644 643 alter table repos add column source text; 645 644 `) ··· 651 650 // 652 651 // [0]: https://sqlite.org/pragma.html#pragma_foreign_keys 653 652 conn.ExecContext(ctx, "pragma foreign_keys = off;") 654 - runMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error { 653 + orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error { 655 654 _, err := tx.Exec(` 656 655 create table pulls_new ( 657 656 -- identifiers ··· 708 707 }) 709 708 conn.ExecContext(ctx, "pragma foreign_keys = on;") 710 709 711 - runMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error { 710 + orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error { 712 711 tx.Exec(` 713 712 alter table repos add column spindle text; 714 713 `) ··· 718 717 // drop all knot secrets, add unique constraint to knots 719 718 // 720 719 // knots will henceforth use service auth for signed requests 721 - runMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error { 720 + orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error { 722 721 _, err := tx.Exec(` 723 722 create table registrations_new ( 724 723 id integer primary key autoincrement, ··· 741 740 }) 742 741 743 742 // recreate and add rkey + created columns with default constraint 744 - runMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error { 743 + orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error { 745 744 // create new table 746 745 // - repo_at instead of repo integer 747 746 // - rkey field ··· 795 794 return err 796 795 }) 797 796 798 - runMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error { 797 + orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error { 799 798 _, err := tx.Exec(` 800 799 alter table issues add column rkey text not null default ''; 801 800 ··· 807 806 }) 808 807 809 808 // repurpose the read-only column to "needs-upgrade" 810 - runMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error { 809 + orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error { 811 810 _, err := tx.Exec(` 812 811 alter table registrations rename column read_only to needs_upgrade; 813 812 `) ··· 815 814 }) 816 815 817 816 // 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 { 817 + orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error { 819 818 _, err := tx.Exec(` 820 819 update registrations set needs_upgrade = 1; 821 820 `) ··· 823 822 }) 824 823 825 824 // 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 { 825 + orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error { 827 826 _, err := tx.Exec(` 828 827 alter table spindles add column needs_upgrade integer not null default 0; 829 828 `) ··· 841 840 // 842 841 // disable foreign-keys for the next migration 843 842 conn.ExecContext(ctx, "pragma foreign_keys = off;") 844 - runMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error { 843 + orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error { 845 844 _, err := tx.Exec(` 846 845 create table if not exists issues_new ( 847 846 -- identifiers ··· 911 910 // - new columns 912 911 // * column "reply_to" which can be any other comment 913 912 // * column "at-uri" which is a generated column 914 - runMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error { 913 + orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error { 915 914 _, err := tx.Exec(` 916 915 create table if not exists issue_comments ( 917 916 -- identifiers ··· 971 970 // 972 971 // disable foreign-keys for the next migration 973 972 conn.ExecContext(ctx, "pragma foreign_keys = off;") 974 - runMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error { 973 + orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error { 975 974 _, err := tx.Exec(` 976 975 create table if not exists pulls_new ( 977 976 -- identifiers ··· 1052 1051 // 1053 1052 // disable foreign-keys for the next migration 1054 1053 conn.ExecContext(ctx, "pragma foreign_keys = off;") 1055 - runMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error { 1054 + orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error { 1056 1055 _, err := tx.Exec(` 1057 1056 create table if not exists pull_submissions_new ( 1058 1057 -- identifiers ··· 1106 1105 1107 1106 // knots may report the combined patch for a comparison, we can store that on the appview side 1108 1107 // (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 { 1108 + orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error { 1110 1109 _, err := tx.Exec(` 1111 1110 alter table pull_submissions add column combined text; 1112 1111 `) 1113 1112 return err 1114 1113 }) 1115 1114 1116 - runMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error { 1115 + orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error { 1117 1116 _, err := tx.Exec(` 1118 1117 alter table profile add column pronouns text; 1119 1118 `) 1120 1119 return err 1121 1120 }) 1122 1121 1123 - runMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error { 1122 + orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error { 1124 1123 _, err := tx.Exec(` 1125 1124 alter table repos add column website text; 1126 1125 alter table repos add column topics text; ··· 1128 1127 return err 1129 1128 }) 1130 1129 1131 - runMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error { 1130 + orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error { 1132 1131 _, err := tx.Exec(` 1133 1132 alter table notification_preferences add column user_mentioned integer not null default 1; 1134 1133 `) ··· 1136 1135 }) 1137 1136 1138 1137 // remove the foreign key constraints from stars. 1139 - runMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error { 1138 + orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error { 1140 1139 _, err := tx.Exec(` 1141 1140 create table stars_new ( 1142 1141 id integer primary key autoincrement, ··· 1180 1179 }, nil 1181 1180 } 1182 1181 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 1182 func (d *DB) Close() error { 1229 1183 return d.DB.Close() 1230 1184 } 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 - }
+6 -3
appview/db/follow.go
··· 7 7 "time" 8 8 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 13 func AddFollow(e Execer, follow *models.Follow) error { ··· 134 135 return result, nil 135 136 } 136 137 137 - func GetFollows(e Execer, limit int, filters ...filter) ([]models.Follow, error) { 138 + func GetFollows(e Execer, limit int, filters ...orm.Filter) ([]models.Follow, error) { 138 139 var follows []models.Follow 139 140 140 141 var conditions []string ··· 166 167 if err != nil { 167 168 return nil, err 168 169 } 170 + defer rows.Close() 171 + 169 172 for rows.Next() { 170 173 var follow models.Follow 171 174 var followedAt string ··· 191 194 } 192 195 193 196 func GetFollowers(e Execer, did string) ([]models.Follow, error) { 194 - return GetFollows(e, 0, FilterEq("subject_did", did)) 197 + return GetFollows(e, 0, orm.FilterEq("subject_did", did)) 195 198 } 196 199 197 200 func GetFollowing(e Execer, did string) ([]models.Follow, error) { 198 - return GetFollows(e, 0, FilterEq("user_did", did)) 201 + return GetFollows(e, 0, orm.FilterEq("user_did", did)) 199 202 } 200 203 201 204 func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
+22 -20
appview/db/issues.go
··· 13 13 "tangled.org/core/api/tangled" 14 14 "tangled.org/core/appview/models" 15 15 "tangled.org/core/appview/pagination" 16 + "tangled.org/core/orm" 16 17 ) 17 18 18 19 func PutIssue(tx *sql.Tx, issue *models.Issue) error { ··· 27 28 28 29 issues, err := GetIssues( 29 30 tx, 30 - FilterEq("did", issue.Did), 31 - FilterEq("rkey", issue.Rkey), 31 + orm.FilterEq("did", issue.Did), 32 + orm.FilterEq("rkey", issue.Rkey), 32 33 ) 33 34 switch { 34 35 case err != nil: ··· 98 99 return nil 99 100 } 100 101 101 - func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) { 102 + func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) { 102 103 issueMap := make(map[string]*models.Issue) // at-uri -> issue 103 104 104 105 var conditions []string ··· 114 115 whereClause = " where " + strings.Join(conditions, " and ") 115 116 } 116 117 117 - pLower := FilterGte("row_num", page.Offset+1) 118 - pUpper := FilterLte("row_num", page.Offset+page.Limit) 118 + pLower := orm.FilterGte("row_num", page.Offset+1) 119 + pUpper := orm.FilterLte("row_num", page.Offset+page.Limit) 119 120 120 121 pageClause := "" 121 122 if page.Limit > 0 { ··· 205 206 repoAts = append(repoAts, string(issue.RepoAt)) 206 207 } 207 208 208 - repos, err := GetRepos(e, 0, FilterIn("at_uri", repoAts)) 209 + repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoAts)) 209 210 if err != nil { 210 211 return nil, fmt.Errorf("failed to build repo mappings: %w", err) 211 212 } ··· 228 229 // collect comments 229 230 issueAts := slices.Collect(maps.Keys(issueMap)) 230 231 231 - comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts)) 232 + comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts)) 232 233 if err != nil { 233 234 return nil, fmt.Errorf("failed to query comments: %w", err) 234 235 } ··· 240 241 } 241 242 242 243 // collect allLabels for each issue 243 - allLabels, err := GetLabels(e, FilterIn("subject", issueAts)) 244 + allLabels, err := GetLabels(e, orm.FilterIn("subject", issueAts)) 244 245 if err != nil { 245 246 return nil, fmt.Errorf("failed to query labels: %w", err) 246 247 } ··· 251 252 } 252 253 253 254 // collect references for each issue 254 - allReferencs, err := GetReferencesAll(e, FilterIn("from_at", issueAts)) 255 + allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", issueAts)) 255 256 if err != nil { 256 257 return nil, fmt.Errorf("failed to query reference_links: %w", err) 257 258 } ··· 277 278 issues, err := GetIssuesPaginated( 278 279 e, 279 280 pagination.Page{}, 280 - FilterEq("repo_at", repoAt), 281 - FilterEq("issue_id", issueId), 281 + orm.FilterEq("repo_at", repoAt), 282 + orm.FilterEq("issue_id", issueId), 282 283 ) 283 284 if err != nil { 284 285 return nil, err ··· 290 291 return &issues[0], nil 291 292 } 292 293 293 - func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) { 294 + func GetIssues(e Execer, filters ...orm.Filter) ([]models.Issue, error) { 294 295 return GetIssuesPaginated(e, pagination.Page{}, filters...) 295 296 } 296 297 ··· 298 299 func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) { 299 300 var ids []int64 300 301 301 - var filters []filter 302 + var filters []orm.Filter 302 303 openValue := 0 303 304 if opts.IsOpen { 304 305 openValue = 1 305 306 } 306 - filters = append(filters, FilterEq("open", openValue)) 307 + filters = append(filters, orm.FilterEq("open", openValue)) 307 308 if opts.RepoAt != "" { 308 - filters = append(filters, FilterEq("repo_at", opts.RepoAt)) 309 + filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt)) 309 310 } 310 311 311 312 var conditions []string ··· 397 398 return id, nil 398 399 } 399 400 400 - func DeleteIssueComments(e Execer, filters ...filter) error { 401 + func DeleteIssueComments(e Execer, filters ...orm.Filter) error { 401 402 var conditions []string 402 403 var args []any 403 404 for _, filter := range filters { ··· 416 417 return err 417 418 } 418 419 419 - func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) { 420 + func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) { 420 421 commentMap := make(map[string]*models.IssueComment) 421 422 422 423 var conditions []string ··· 451 452 if err != nil { 452 453 return nil, err 453 454 } 455 + defer rows.Close() 454 456 455 457 for rows.Next() { 456 458 var comment models.IssueComment ··· 506 508 507 509 // collect references for each comments 508 510 commentAts := slices.Collect(maps.Keys(commentMap)) 509 - allReferencs, err := GetReferencesAll(e, FilterIn("from_at", commentAts)) 511 + allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 510 512 if err != nil { 511 513 return nil, fmt.Errorf("failed to query reference_links: %w", err) 512 514 } ··· 548 550 return nil 549 551 } 550 552 551 - func CloseIssues(e Execer, filters ...filter) error { 553 + func CloseIssues(e Execer, filters ...orm.Filter) error { 552 554 var conditions []string 553 555 var args []any 554 556 for _, filter := range filters { ··· 566 568 return err 567 569 } 568 570 569 - func ReopenIssues(e Execer, filters ...filter) error { 571 + func ReopenIssues(e Execer, filters ...orm.Filter) error { 570 572 var conditions []string 571 573 var args []any 572 574 for _, filter := range filters {
+8 -7
appview/db/label.go
··· 10 10 11 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 12 "tangled.org/core/appview/models" 13 + "tangled.org/core/orm" 13 14 ) 14 15 15 16 // no updating type for now ··· 59 60 return id, nil 60 61 } 61 62 62 - func DeleteLabelDefinition(e Execer, filters ...filter) error { 63 + func DeleteLabelDefinition(e Execer, filters ...orm.Filter) error { 63 64 var conditions []string 64 65 var args []any 65 66 for _, filter := range filters { ··· 75 76 return err 76 77 } 77 78 78 - func GetLabelDefinitions(e Execer, filters ...filter) ([]models.LabelDefinition, error) { 79 + func GetLabelDefinitions(e Execer, filters ...orm.Filter) ([]models.LabelDefinition, error) { 79 80 var labelDefinitions []models.LabelDefinition 80 81 var conditions []string 81 82 var args []any ··· 167 168 } 168 169 169 170 // helper to get exactly one label def 170 - func GetLabelDefinition(e Execer, filters ...filter) (*models.LabelDefinition, error) { 171 + func GetLabelDefinition(e Execer, filters ...orm.Filter) (*models.LabelDefinition, error) { 171 172 labels, err := GetLabelDefinitions(e, filters...) 172 173 if err != nil { 173 174 return nil, err ··· 227 228 return id, nil 228 229 } 229 230 230 - func GetLabelOps(e Execer, filters ...filter) ([]models.LabelOp, error) { 231 + func GetLabelOps(e Execer, filters ...orm.Filter) ([]models.LabelOp, error) { 231 232 var labelOps []models.LabelOp 232 233 var conditions []string 233 234 var args []any ··· 302 303 } 303 304 304 305 // get labels for a given list of subject URIs 305 - func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]models.LabelState, error) { 306 + func GetLabels(e Execer, filters ...orm.Filter) (map[syntax.ATURI]models.LabelState, error) { 306 307 ops, err := GetLabelOps(e, filters...) 307 308 if err != nil { 308 309 return nil, err ··· 322 323 } 323 324 labelAts := slices.Collect(maps.Keys(labelAtSet)) 324 325 325 - actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts)) 326 + actx, err := NewLabelApplicationCtx(e, orm.FilterIn("at_uri", labelAts)) 326 327 if err != nil { 327 328 return nil, err 328 329 } ··· 338 339 return results, nil 339 340 } 340 341 341 - func NewLabelApplicationCtx(e Execer, filters ...filter) (*models.LabelApplicationCtx, error) { 342 + func NewLabelApplicationCtx(e Execer, filters ...orm.Filter) (*models.LabelApplicationCtx, error) { 342 343 labels, err := GetLabelDefinitions(e, filters...) 343 344 if err != nil { 344 345 return nil, err
+6 -5
appview/db/language.go
··· 7 7 8 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 - func GetRepoLanguages(e Execer, filters ...filter) ([]models.RepoLanguage, error) { 13 + func GetRepoLanguages(e Execer, filters ...orm.Filter) ([]models.RepoLanguage, error) { 13 14 var conditions []string 14 15 var args []any 15 16 for _, filter := range filters { ··· 27 28 whereClause, 28 29 ) 29 30 rows, err := e.Query(query, args...) 30 - 31 31 if err != nil { 32 32 return nil, fmt.Errorf("failed to execute query: %w ", err) 33 33 } 34 + defer rows.Close() 34 35 35 36 var langs []models.RepoLanguage 36 37 for rows.Next() { ··· 85 86 return nil 86 87 } 87 88 88 - func DeleteRepoLanguages(e Execer, filters ...filter) error { 89 + func DeleteRepoLanguages(e Execer, filters ...orm.Filter) error { 89 90 var conditions []string 90 91 var args []any 91 92 for _, filter := range filters { ··· 107 108 func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error { 108 109 err := DeleteRepoLanguages( 109 110 tx, 110 - FilterEq("repo_at", repoAt), 111 - FilterEq("ref", ref), 111 + orm.FilterEq("repo_at", repoAt), 112 + orm.FilterEq("ref", ref), 112 113 ) 113 114 if err != nil { 114 115 return fmt.Errorf("failed to delete existing languages: %w", err)
+14 -13
appview/db/notifications.go
··· 11 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 12 "tangled.org/core/appview/models" 13 13 "tangled.org/core/appview/pagination" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 17 func CreateNotification(e Execer, notification *models.Notification) error { ··· 44 45 } 45 46 46 47 // GetNotificationsPaginated retrieves notifications with filters and pagination 47 - func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...filter) ([]*models.Notification, error) { 48 + func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.Notification, error) { 48 49 var conditions []string 49 50 var args []any 50 51 ··· 113 114 } 114 115 115 116 // GetNotificationsWithEntities retrieves notifications with their related entities 116 - func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...filter) ([]*models.NotificationWithEntity, error) { 117 + func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.NotificationWithEntity, error) { 117 118 var conditions []string 118 119 var args []any 119 120 ··· 256 257 } 257 258 258 259 // GetNotifications retrieves notifications with filters 259 - func GetNotifications(e Execer, filters ...filter) ([]*models.Notification, error) { 260 + func GetNotifications(e Execer, filters ...orm.Filter) ([]*models.Notification, error) { 260 261 return GetNotificationsPaginated(e, pagination.FirstPage(), filters...) 261 262 } 262 263 263 - func CountNotifications(e Execer, filters ...filter) (int64, error) { 264 + func CountNotifications(e Execer, filters ...orm.Filter) (int64, error) { 264 265 var conditions []string 265 266 var args []any 266 267 for _, filter := range filters { ··· 285 286 } 286 287 287 288 func MarkNotificationRead(e Execer, notificationID int64, userDID string) error { 288 - idFilter := FilterEq("id", notificationID) 289 - recipientFilter := FilterEq("recipient_did", userDID) 289 + idFilter := orm.FilterEq("id", notificationID) 290 + recipientFilter := orm.FilterEq("recipient_did", userDID) 290 291 291 292 query := fmt.Sprintf(` 292 293 UPDATE notifications ··· 314 315 } 315 316 316 317 func MarkAllNotificationsRead(e Execer, userDID string) error { 317 - recipientFilter := FilterEq("recipient_did", userDID) 318 - readFilter := FilterEq("read", 0) 318 + recipientFilter := orm.FilterEq("recipient_did", userDID) 319 + readFilter := orm.FilterEq("read", 0) 319 320 320 321 query := fmt.Sprintf(` 321 322 UPDATE notifications ··· 334 335 } 335 336 336 337 func DeleteNotification(e Execer, notificationID int64, userDID string) error { 337 - idFilter := FilterEq("id", notificationID) 338 - recipientFilter := FilterEq("recipient_did", userDID) 338 + idFilter := orm.FilterEq("id", notificationID) 339 + recipientFilter := orm.FilterEq("recipient_did", userDID) 339 340 340 341 query := fmt.Sprintf(` 341 342 DELETE FROM notifications ··· 362 363 } 363 364 364 365 func GetNotificationPreference(e Execer, userDid string) (*models.NotificationPreferences, error) { 365 - prefs, err := GetNotificationPreferences(e, FilterEq("user_did", userDid)) 366 + prefs, err := GetNotificationPreferences(e, orm.FilterEq("user_did", userDid)) 366 367 if err != nil { 367 368 return nil, err 368 369 } ··· 375 376 return p, nil 376 377 } 377 378 378 - func GetNotificationPreferences(e Execer, filters ...filter) (map[syntax.DID]*models.NotificationPreferences, error) { 379 + func GetNotificationPreferences(e Execer, filters ...orm.Filter) (map[syntax.DID]*models.NotificationPreferences, error) { 379 380 prefsMap := make(map[syntax.DID]*models.NotificationPreferences) 380 381 381 382 var conditions []string ··· 483 484 484 485 func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error { 485 486 cutoff := time.Now().Add(-olderThan) 486 - createdFilter := FilterLte("created", cutoff) 487 + createdFilter := orm.FilterLte("created", cutoff) 487 488 488 489 query := fmt.Sprintf(` 489 490 DELETE FROM notifications
+6 -5
appview/db/pipeline.go
··· 7 7 "time" 8 8 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 - func GetPipelines(e Execer, filters ...filter) ([]models.Pipeline, error) { 13 + func GetPipelines(e Execer, filters ...orm.Filter) ([]models.Pipeline, error) { 13 14 var pipelines []models.Pipeline 14 15 15 16 var conditions []string ··· 168 169 169 170 // this is a mega query, but the most useful one: 170 171 // 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 + func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.Pipeline, error) { 172 173 var conditions []string 173 174 var args []any 174 175 for _, filter := range filters { 175 - filter.key = "p." + filter.key // the table is aliased in the query to `p` 176 + filter.Key = "p." + filter.Key // the table is aliased in the query to `p` 176 177 conditions = append(conditions, filter.Condition()) 177 178 args = append(args, filter.Arg()...) 178 179 } ··· 264 265 conditions = nil 265 266 args = nil 266 267 for _, p := range pipelines { 267 - knotFilter := FilterEq("pipeline_knot", p.Knot) 268 - rkeyFilter := FilterEq("pipeline_rkey", p.Rkey) 268 + knotFilter := orm.FilterEq("pipeline_knot", p.Knot) 269 + rkeyFilter := orm.FilterEq("pipeline_rkey", p.Rkey) 269 270 conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition())) 270 271 args = append(args, p.Knot) 271 272 args = append(args, p.Rkey)
+11 -5
appview/db/profile.go
··· 11 11 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 13 "tangled.org/core/appview/models" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 17 const TimeframeMonths = 7 ··· 44 45 45 46 issues, err := GetIssues( 46 47 e, 47 - FilterEq("did", forDid), 48 - FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)), 48 + orm.FilterEq("did", forDid), 49 + orm.FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)), 49 50 ) 50 51 if err != nil { 51 52 return nil, fmt.Errorf("error getting issues by owner did: %w", err) ··· 65 66 *items = append(*items, &issue) 66 67 } 67 68 68 - repos, err := GetRepos(e, 0, FilterEq("did", forDid)) 69 + repos, err := GetRepos(e, 0, orm.FilterEq("did", forDid)) 69 70 if err != nil { 70 71 return nil, fmt.Errorf("error getting all repos by did: %w", err) 71 72 } ··· 199 200 return tx.Commit() 200 201 } 201 202 202 - func GetProfiles(e Execer, filters ...filter) (map[string]*models.Profile, error) { 203 + func GetProfiles(e Execer, filters ...orm.Filter) (map[string]*models.Profile, error) { 203 204 var conditions []string 204 205 var args []any 205 206 for _, filter := range filters { ··· 229 230 if err != nil { 230 231 return nil, err 231 232 } 233 + defer rows.Close() 232 234 233 235 profileMap := make(map[string]*models.Profile) 234 236 for rows.Next() { ··· 269 271 if err != nil { 270 272 return nil, err 271 273 } 274 + defer rows.Close() 275 + 272 276 idxs := make(map[string]int) 273 277 for did := range profileMap { 274 278 idxs[did] = 0 ··· 289 293 if err != nil { 290 294 return nil, err 291 295 } 296 + defer rows.Close() 297 + 292 298 idxs = make(map[string]int) 293 299 for did := range profileMap { 294 300 idxs[did] = 0 ··· 441 447 } 442 448 443 449 // ensure all pinned repos are either own repos or collaborating repos 444 - repos, err := GetRepos(e, 0, FilterEq("did", profile.Did)) 450 + repos, err := GetRepos(e, 0, orm.FilterEq("did", profile.Did)) 445 451 if err != nil { 446 452 log.Printf("getting repos for %s: %s", profile.Did, err) 447 453 }
+21 -20
appview/db/pulls.go
··· 13 13 14 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 15 "tangled.org/core/appview/models" 16 + "tangled.org/core/orm" 16 17 ) 17 18 18 19 func NewPull(tx *sql.Tx, pull *models.Pull) error { ··· 118 119 return pullId - 1, err 119 120 } 120 121 121 - func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*models.Pull, error) { 122 + func GetPullsWithLimit(e Execer, limit int, filters ...orm.Filter) ([]*models.Pull, error) { 122 123 pulls := make(map[syntax.ATURI]*models.Pull) 123 124 124 125 var conditions []string ··· 229 230 for _, p := range pulls { 230 231 pullAts = append(pullAts, p.AtUri()) 231 232 } 232 - submissionsMap, err := GetPullSubmissions(e, FilterIn("pull_at", pullAts)) 233 + submissionsMap, err := GetPullSubmissions(e, orm.FilterIn("pull_at", pullAts)) 233 234 if err != nil { 234 235 return nil, fmt.Errorf("failed to get submissions: %w", err) 235 236 } ··· 241 242 } 242 243 243 244 // collect allLabels for each issue 244 - allLabels, err := GetLabels(e, FilterIn("subject", pullAts)) 245 + allLabels, err := GetLabels(e, orm.FilterIn("subject", pullAts)) 245 246 if err != nil { 246 247 return nil, fmt.Errorf("failed to query labels: %w", err) 247 248 } ··· 258 259 sourceAts = append(sourceAts, *p.PullSource.RepoAt) 259 260 } 260 261 } 261 - sourceRepos, err := GetRepos(e, 0, FilterIn("at_uri", sourceAts)) 262 + sourceRepos, err := GetRepos(e, 0, orm.FilterIn("at_uri", sourceAts)) 262 263 if err != nil && !errors.Is(err, sql.ErrNoRows) { 263 264 return nil, fmt.Errorf("failed to get source repos: %w", err) 264 265 } ··· 274 275 } 275 276 } 276 277 277 - allReferences, err := GetReferencesAll(e, FilterIn("from_at", pullAts)) 278 + allReferences, err := GetReferencesAll(e, orm.FilterIn("from_at", pullAts)) 278 279 if err != nil { 279 280 return nil, fmt.Errorf("failed to query reference_links: %w", err) 280 281 } ··· 295 296 return orderedByPullId, nil 296 297 } 297 298 298 - func GetPulls(e Execer, filters ...filter) ([]*models.Pull, error) { 299 + func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) { 299 300 return GetPullsWithLimit(e, 0, filters...) 300 301 } 301 302 302 303 func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) { 303 304 var ids []int64 304 305 305 - var filters []filter 306 - filters = append(filters, FilterEq("state", opts.State)) 306 + var filters []orm.Filter 307 + filters = append(filters, orm.FilterEq("state", opts.State)) 307 308 if opts.RepoAt != "" { 308 - filters = append(filters, FilterEq("repo_at", opts.RepoAt)) 309 + filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt)) 309 310 } 310 311 311 312 var conditions []string ··· 361 362 } 362 363 363 364 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 + pulls, err := GetPullsWithLimit(e, 1, orm.FilterEq("repo_at", repoAt), orm.FilterEq("pull_id", pullId)) 365 366 if err != nil { 366 367 return nil, err 367 368 } ··· 373 374 } 374 375 375 376 // mapping from pull -> pull submissions 376 - func GetPullSubmissions(e Execer, filters ...filter) (map[syntax.ATURI][]*models.PullSubmission, error) { 377 + func GetPullSubmissions(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]*models.PullSubmission, error) { 377 378 var conditions []string 378 379 var args []any 379 380 for _, filter := range filters { ··· 448 449 449 450 // Get comments for all submissions using GetPullComments 450 451 submissionIds := slices.Collect(maps.Keys(submissionMap)) 451 - comments, err := GetPullComments(e, FilterIn("submission_id", submissionIds)) 452 + comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds)) 452 453 if err != nil { 453 454 return nil, fmt.Errorf("failed to get pull comments: %w", err) 454 455 } ··· 474 475 return m, nil 475 476 } 476 477 477 - func GetPullComments(e Execer, filters ...filter) ([]models.PullComment, error) { 478 + func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) { 478 479 var conditions []string 479 480 var args []any 480 481 for _, filter := range filters { ··· 542 543 543 544 // collect references for each comments 544 545 commentAts := slices.Collect(maps.Keys(commentMap)) 545 - allReferencs, err := GetReferencesAll(e, FilterIn("from_at", commentAts)) 546 + allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts)) 546 547 if err != nil { 547 548 return nil, fmt.Errorf("failed to query reference_links: %w", err) 548 549 } ··· 708 709 return err 709 710 } 710 711 711 - func SetPullParentChangeId(e Execer, parentChangeId string, filters ...filter) error { 712 + func SetPullParentChangeId(e Execer, parentChangeId string, filters ...orm.Filter) error { 712 713 var conditions []string 713 714 var args []any 714 715 ··· 732 733 733 734 // Only used when stacking to update contents in the event of a rebase (the interdiff should be empty). 734 735 // otherwise submissions are immutable 735 - func UpdatePull(e Execer, newPatch, sourceRev string, filters ...filter) error { 736 + func UpdatePull(e Execer, newPatch, sourceRev string, filters ...orm.Filter) error { 736 737 var conditions []string 737 738 var args []any 738 739 ··· 790 791 func GetStack(e Execer, stackId string) (models.Stack, error) { 791 792 unorderedPulls, err := GetPulls( 792 793 e, 793 - FilterEq("stack_id", stackId), 794 - FilterNotEq("state", models.PullDeleted), 794 + orm.FilterEq("stack_id", stackId), 795 + orm.FilterNotEq("state", models.PullDeleted), 795 796 ) 796 797 if err != nil { 797 798 return nil, err ··· 835 836 func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) { 836 837 pulls, err := GetPulls( 837 838 e, 838 - FilterEq("stack_id", stackId), 839 - FilterEq("state", models.PullDeleted), 839 + orm.FilterEq("stack_id", stackId), 840 + orm.FilterEq("state", models.PullDeleted), 840 841 ) 841 842 if err != nil { 842 843 return nil, err
+2 -1
appview/db/punchcard.go
··· 7 7 "time" 8 8 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 13 // this adds to the existing count ··· 20 21 return err 21 22 } 22 23 23 - func MakePunchcard(e Execer, filters ...filter) (*models.Punchcard, error) { 24 + func MakePunchcard(e Execer, filters ...orm.Filter) (*models.Punchcard, error) { 24 25 punchcard := &models.Punchcard{} 25 26 now := time.Now() 26 27 startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
+4 -3
appview/db/reference.go
··· 8 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 9 "tangled.org/core/api/tangled" 10 10 "tangled.org/core/appview/models" 11 + "tangled.org/core/orm" 11 12 ) 12 13 13 14 // ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs. ··· 205 206 return err 206 207 } 207 208 208 - func GetReferencesAll(e Execer, filters ...filter) (map[syntax.ATURI][]syntax.ATURI, error) { 209 + func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) { 209 210 var ( 210 211 conditions []string 211 212 args []any ··· 347 348 if len(aturis) == 0 { 348 349 return nil, nil 349 350 } 350 - filter := FilterIn("c.at_uri", aturis) 351 + filter := orm.FilterIn("c.at_uri", aturis) 351 352 rows, err := e.Query( 352 353 fmt.Sprintf( 353 354 `select r.did, r.name, i.issue_id, c.id, i.title, i.open ··· 427 428 if len(aturis) == 0 { 428 429 return nil, nil 429 430 } 430 - filter := FilterIn("c.comment_at", aturis) 431 + filter := orm.FilterIn("c.comment_at", aturis) 431 432 rows, err := e.Query( 432 433 fmt.Sprintf( 433 434 `select r.did, r.name, p.pull_id, c.id, p.title, p.state
+5 -3
appview/db/registration.go
··· 7 7 "time" 8 8 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 - func GetRegistrations(e Execer, filters ...filter) ([]models.Registration, error) { 13 + func GetRegistrations(e Execer, filters ...orm.Filter) ([]models.Registration, error) { 13 14 var registrations []models.Registration 14 15 15 16 var conditions []string ··· 37 38 if err != nil { 38 39 return nil, err 39 40 } 41 + defer rows.Close() 40 42 41 43 for rows.Next() { 42 44 var createdAt string ··· 69 71 return registrations, nil 70 72 } 71 73 72 - func MarkRegistered(e Execer, filters ...filter) error { 74 + func MarkRegistered(e Execer, filters ...orm.Filter) error { 73 75 var conditions []string 74 76 var args []any 75 77 for _, filter := range filters { ··· 94 96 return err 95 97 } 96 98 97 - func DeleteKnot(e Execer, filters ...filter) error { 99 + func DeleteKnot(e Execer, filters ...orm.Filter) error { 98 100 var conditions []string 99 101 var args []any 100 102 for _, filter := range filters {
+17 -6
appview/db/repos.go
··· 11 11 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 13 "tangled.org/core/appview/models" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 - func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) { 17 + func GetRepos(e Execer, limit int, filters ...orm.Filter) ([]models.Repo, error) { 17 18 repoMap := make(map[syntax.ATURI]*models.Repo) 18 19 19 20 var conditions []string ··· 55 56 limitClause, 56 57 ) 57 58 rows, err := e.Query(repoQuery, args...) 58 - 59 59 if err != nil { 60 60 return nil, fmt.Errorf("failed to execute repo query: %w ", err) 61 61 } 62 + defer rows.Close() 62 63 63 64 for rows.Next() { 64 65 var repo models.Repo ··· 127 128 if err != nil { 128 129 return nil, fmt.Errorf("failed to execute labels query: %w ", err) 129 130 } 131 + defer rows.Close() 132 + 130 133 for rows.Next() { 131 134 var repoat, labelat string 132 135 if err := rows.Scan(&repoat, &labelat); err != nil { ··· 164 167 if err != nil { 165 168 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 166 169 } 170 + defer rows.Close() 171 + 167 172 for rows.Next() { 168 173 var repoat, lang string 169 174 if err := rows.Scan(&repoat, &lang); err != nil { ··· 190 195 if err != nil { 191 196 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 192 197 } 198 + defer rows.Close() 199 + 193 200 for rows.Next() { 194 201 var repoat string 195 202 var count int ··· 219 226 if err != nil { 220 227 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err) 221 228 } 229 + defer rows.Close() 230 + 222 231 for rows.Next() { 223 232 var repoat string 224 233 var open, closed int ··· 260 269 if err != nil { 261 270 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err) 262 271 } 272 + defer rows.Close() 273 + 263 274 for rows.Next() { 264 275 var repoat string 265 276 var open, merged, closed, deleted int ··· 294 305 } 295 306 296 307 // helper to get exactly one repo 297 - func GetRepo(e Execer, filters ...filter) (*models.Repo, error) { 308 + func GetRepo(e Execer, filters ...orm.Filter) (*models.Repo, error) { 298 309 repos, err := GetRepos(e, 0, filters...) 299 310 if err != nil { 300 311 return nil, err ··· 311 322 return &repos[0], nil 312 323 } 313 324 314 - func CountRepos(e Execer, filters ...filter) (int64, error) { 325 + func CountRepos(e Execer, filters ...orm.Filter) (int64, error) { 315 326 var conditions []string 316 327 var args []any 317 328 for _, filter := range filters { ··· 542 553 return err 543 554 } 544 555 545 - func UnsubscribeLabel(e Execer, filters ...filter) error { 556 + func UnsubscribeLabel(e Execer, filters ...orm.Filter) error { 546 557 var conditions []string 547 558 var args []any 548 559 for _, filter := range filters { ··· 560 571 return err 561 572 } 562 573 563 - func GetRepoLabels(e Execer, filters ...filter) ([]models.RepoLabel, error) { 574 + func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) { 564 575 var conditions []string 565 576 var args []any 566 577 for _, filter := range filters {
+6 -5
appview/db/spindle.go
··· 7 7 "time" 8 8 9 9 "tangled.org/core/appview/models" 10 + "tangled.org/core/orm" 10 11 ) 11 12 12 - func GetSpindles(e Execer, filters ...filter) ([]models.Spindle, error) { 13 + func GetSpindles(e Execer, filters ...orm.Filter) ([]models.Spindle, error) { 13 14 var spindles []models.Spindle 14 15 15 16 var conditions []string ··· 91 92 return err 92 93 } 93 94 94 - func VerifySpindle(e Execer, filters ...filter) (int64, error) { 95 + func VerifySpindle(e Execer, filters ...orm.Filter) (int64, error) { 95 96 var conditions []string 96 97 var args []any 97 98 for _, filter := range filters { ··· 114 115 return res.RowsAffected() 115 116 } 116 117 117 - func DeleteSpindle(e Execer, filters ...filter) error { 118 + func DeleteSpindle(e Execer, filters ...orm.Filter) error { 118 119 var conditions []string 119 120 var args []any 120 121 for _, filter := range filters { ··· 144 145 return err 145 146 } 146 147 147 - func RemoveSpindleMember(e Execer, filters ...filter) error { 148 + func RemoveSpindleMember(e Execer, filters ...orm.Filter) error { 148 149 var conditions []string 149 150 var args []any 150 151 for _, filter := range filters { ··· 163 164 return err 164 165 } 165 166 166 - func GetSpindleMembers(e Execer, filters ...filter) ([]models.SpindleMember, error) { 167 + func GetSpindleMembers(e Execer, filters ...orm.Filter) ([]models.SpindleMember, error) { 167 168 var members []models.SpindleMember 168 169 169 170 var conditions []string
+6 -4
appview/db/star.go
··· 11 11 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 13 "tangled.org/core/appview/models" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 17 func AddStar(e Execer, star *models.Star) error { ··· 133 134 134 135 // GetRepoStars return a list of stars each holding target repository. 135 136 // 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 + func GetRepoStars(e Execer, limit int, filters ...orm.Filter) ([]models.RepoStar, error) { 137 138 var conditions []string 138 139 var args []any 139 140 for _, filter := range filters { ··· 164 165 if err != nil { 165 166 return nil, err 166 167 } 168 + defer rows.Close() 167 169 168 170 starMap := make(map[string][]models.Star) 169 171 for rows.Next() { ··· 195 197 return nil, nil 196 198 } 197 199 198 - repos, err := GetRepos(e, 0, FilterIn("at_uri", args)) 200 + repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", args)) 199 201 if err != nil { 200 202 return nil, err 201 203 } ··· 225 227 return repoStars, nil 226 228 } 227 229 228 - func CountStars(e Execer, filters ...filter) (int64, error) { 230 + func CountStars(e Execer, filters ...orm.Filter) (int64, error) { 229 231 var conditions []string 230 232 var args []any 231 233 for _, filter := range filters { ··· 298 300 } 299 301 300 302 // get full repo data 301 - repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris)) 303 + repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoUris)) 302 304 if err != nil { 303 305 return nil, err 304 306 }
+4 -3
appview/db/strings.go
··· 8 8 "time" 9 9 10 10 "tangled.org/core/appview/models" 11 + "tangled.org/core/orm" 11 12 ) 12 13 13 14 func AddString(e Execer, s models.String) error { ··· 44 45 return err 45 46 } 46 47 47 - func GetStrings(e Execer, limit int, filters ...filter) ([]models.String, error) { 48 + func GetStrings(e Execer, limit int, filters ...orm.Filter) ([]models.String, error) { 48 49 var all []models.String 49 50 50 51 var conditions []string ··· 127 128 return all, nil 128 129 } 129 130 130 - func CountStrings(e Execer, filters ...filter) (int64, error) { 131 + func CountStrings(e Execer, filters ...orm.Filter) (int64, error) { 131 132 var conditions []string 132 133 var args []any 133 134 for _, filter := range filters { ··· 151 152 return count, nil 152 153 } 153 154 154 - func DeleteString(e Execer, filters ...filter) error { 155 + func DeleteString(e Execer, filters ...orm.Filter) error { 155 156 var conditions []string 156 157 var args []any 157 158 for _, filter := range filters {
+9 -8
appview/db/timeline.go
··· 5 5 6 6 "github.com/bluesky-social/indigo/atproto/syntax" 7 7 "tangled.org/core/appview/models" 8 + "tangled.org/core/orm" 8 9 ) 9 10 10 11 // TODO: this gathers heterogenous events from different sources and aggregates ··· 84 85 } 85 86 86 87 func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) { 87 - filters := make([]filter, 0) 88 + filters := make([]orm.Filter, 0) 88 89 if userIsFollowing != nil { 89 - filters = append(filters, FilterIn("did", userIsFollowing)) 90 + filters = append(filters, orm.FilterIn("did", userIsFollowing)) 90 91 } 91 92 92 93 repos, err := GetRepos(e, limit, filters...) ··· 104 105 105 106 var origRepos []models.Repo 106 107 if args != nil { 107 - origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args)) 108 + origRepos, err = GetRepos(e, 0, orm.FilterIn("at_uri", args)) 108 109 } 109 110 if err != nil { 110 111 return nil, err ··· 144 145 } 145 146 146 147 func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) { 147 - filters := make([]filter, 0) 148 + filters := make([]orm.Filter, 0) 148 149 if userIsFollowing != nil { 149 - filters = append(filters, FilterIn("did", userIsFollowing)) 150 + filters = append(filters, orm.FilterIn("did", userIsFollowing)) 150 151 } 151 152 152 153 stars, err := GetRepoStars(e, limit, filters...) ··· 180 181 } 181 182 182 183 func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) { 183 - filters := make([]filter, 0) 184 + filters := make([]orm.Filter, 0) 184 185 if userIsFollowing != nil { 185 - filters = append(filters, FilterIn("user_did", userIsFollowing)) 186 + filters = append(filters, orm.FilterIn("user_did", userIsFollowing)) 186 187 } 187 188 188 189 follows, err := GetFollows(e, limit, filters...) ··· 199 200 return nil, nil 200 201 } 201 202 202 - profiles, err := GetProfiles(e, FilterIn("did", subjects)) 203 + profiles, err := GetProfiles(e, orm.FilterIn("did", subjects)) 203 204 if err != nil { 204 205 return nil, err 205 206 }
+25 -24
appview/ingester.go
··· 21 21 "tangled.org/core/appview/serververify" 22 22 "tangled.org/core/appview/validator" 23 23 "tangled.org/core/idresolver" 24 + "tangled.org/core/orm" 24 25 "tangled.org/core/rbac" 25 26 ) 26 27 ··· 253 254 254 255 err = db.AddArtifact(i.Db, artifact) 255 256 case jmodels.CommitOperationDelete: 256 - err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 257 + err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey)) 257 258 } 258 259 259 260 if err != nil { ··· 350 351 351 352 err = db.UpsertProfile(tx, &profile) 352 353 case jmodels.CommitOperationDelete: 353 - err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey)) 354 + err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey)) 354 355 } 355 356 356 357 if err != nil { ··· 424 425 // get record from db first 425 426 members, err := db.GetSpindleMembers( 426 427 ddb, 427 - db.FilterEq("did", did), 428 - db.FilterEq("rkey", rkey), 428 + orm.FilterEq("did", did), 429 + orm.FilterEq("rkey", rkey), 429 430 ) 430 431 if err != nil || len(members) != 1 { 431 432 return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members)) ··· 440 441 // remove record by rkey && update enforcer 441 442 if err = db.RemoveSpindleMember( 442 443 tx, 443 - db.FilterEq("did", did), 444 - db.FilterEq("rkey", rkey), 444 + orm.FilterEq("did", did), 445 + orm.FilterEq("rkey", rkey), 445 446 ); err != nil { 446 447 return fmt.Errorf("failed to remove from db: %w", err) 447 448 } ··· 523 524 // get record from db first 524 525 spindles, err := db.GetSpindles( 525 526 ddb, 526 - db.FilterEq("owner", did), 527 - db.FilterEq("instance", instance), 527 + orm.FilterEq("owner", did), 528 + orm.FilterEq("instance", instance), 528 529 ) 529 530 if err != nil || len(spindles) != 1 { 530 531 return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles)) ··· 543 544 // remove spindle members first 544 545 err = db.RemoveSpindleMember( 545 546 tx, 546 - db.FilterEq("owner", did), 547 - db.FilterEq("instance", instance), 547 + orm.FilterEq("owner", did), 548 + orm.FilterEq("instance", instance), 548 549 ) 549 550 if err != nil { 550 551 return err ··· 552 553 553 554 err = db.DeleteSpindle( 554 555 tx, 555 - db.FilterEq("owner", did), 556 - db.FilterEq("instance", instance), 556 + orm.FilterEq("owner", did), 557 + orm.FilterEq("instance", instance), 557 558 ) 558 559 if err != nil { 559 560 return err ··· 621 622 case jmodels.CommitOperationDelete: 622 623 if err := db.DeleteString( 623 624 ddb, 624 - db.FilterEq("did", did), 625 - db.FilterEq("rkey", rkey), 625 + orm.FilterEq("did", did), 626 + orm.FilterEq("rkey", rkey), 626 627 ); err != nil { 627 628 l.Error("failed to delete", "err", err) 628 629 return fmt.Errorf("failed to delete string record: %w", err) ··· 740 741 // get record from db first 741 742 registrations, err := db.GetRegistrations( 742 743 ddb, 743 - db.FilterEq("domain", domain), 744 - db.FilterEq("did", did), 744 + orm.FilterEq("domain", domain), 745 + orm.FilterEq("did", did), 745 746 ) 746 747 if err != nil { 747 748 return fmt.Errorf("failed to get registration: %w", err) ··· 762 763 763 764 err = db.DeleteKnot( 764 765 tx, 765 - db.FilterEq("did", did), 766 - db.FilterEq("domain", domain), 766 + orm.FilterEq("did", did), 767 + orm.FilterEq("domain", domain), 767 768 ) 768 769 if err != nil { 769 770 return err ··· 915 916 case jmodels.CommitOperationDelete: 916 917 if err := db.DeleteIssueComments( 917 918 ddb, 918 - db.FilterEq("did", did), 919 - db.FilterEq("rkey", rkey), 919 + orm.FilterEq("did", did), 920 + orm.FilterEq("rkey", rkey), 920 921 ); err != nil { 921 922 return fmt.Errorf("failed to delete issue comment record: %w", err) 922 923 } ··· 969 970 case jmodels.CommitOperationDelete: 970 971 if err := db.DeleteLabelDefinition( 971 972 ddb, 972 - db.FilterEq("did", did), 973 - db.FilterEq("rkey", rkey), 973 + orm.FilterEq("did", did), 974 + orm.FilterEq("rkey", rkey), 974 975 ); err != nil { 975 976 return fmt.Errorf("failed to delete labeldef record: %w", err) 976 977 } ··· 1010 1011 var repo *models.Repo 1011 1012 switch collection { 1012 1013 case tangled.RepoIssueNSID: 1013 - i, err := db.GetIssues(ddb, db.FilterEq("at_uri", subject)) 1014 + i, err := db.GetIssues(ddb, orm.FilterEq("at_uri", subject)) 1014 1015 if err != nil || len(i) != 1 { 1015 1016 return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i)) 1016 1017 } ··· 1019 1020 return fmt.Errorf("unsupport label subject: %s", collection) 1020 1021 } 1021 1022 1022 - actx, err := db.NewLabelApplicationCtx(ddb, db.FilterIn("at_uri", repo.Labels)) 1023 + actx, err := db.NewLabelApplicationCtx(ddb, orm.FilterIn("at_uri", repo.Labels)) 1023 1024 if err != nil { 1024 1025 return fmt.Errorf("failed to build label application ctx: %w", err) 1025 1026 }
+46 -45
appview/issues/issues.go
··· 19 19 "tangled.org/core/appview/config" 20 20 "tangled.org/core/appview/db" 21 21 issues_indexer "tangled.org/core/appview/indexer/issues" 22 + "tangled.org/core/appview/mentions" 22 23 "tangled.org/core/appview/models" 23 24 "tangled.org/core/appview/notify" 24 25 "tangled.org/core/appview/oauth" 25 26 "tangled.org/core/appview/pages" 26 27 "tangled.org/core/appview/pages/repoinfo" 27 28 "tangled.org/core/appview/pagination" 28 - "tangled.org/core/appview/refresolver" 29 29 "tangled.org/core/appview/reporesolver" 30 30 "tangled.org/core/appview/validator" 31 31 "tangled.org/core/idresolver" 32 + "tangled.org/core/orm" 32 33 "tangled.org/core/rbac" 33 34 "tangled.org/core/tid" 34 35 ) 35 36 36 37 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 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 49 50 } 50 51 51 52 func New( ··· 54 55 enforcer *rbac.Enforcer, 55 56 pages *pages.Pages, 56 57 idResolver *idresolver.Resolver, 57 - refResolver *refresolver.Resolver, 58 + mentionsResolver *mentions.Resolver, 58 59 db *db.DB, 59 60 config *config.Config, 60 61 notifier notify.Notifier, ··· 63 64 logger *slog.Logger, 64 65 ) *Issues { 65 66 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, 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, 78 79 } 79 80 } 80 81 ··· 113 114 114 115 labelDefs, err := db.GetLabelDefinitions( 115 116 rp.db, 116 - db.FilterIn("at_uri", f.Labels), 117 - db.FilterContains("scope", tangled.RepoIssueNSID), 117 + orm.FilterIn("at_uri", f.Labels), 118 + orm.FilterContains("scope", tangled.RepoIssueNSID), 118 119 ) 119 120 if err != nil { 120 121 l.Error("failed to fetch labels", "err", err) ··· 163 164 newIssue := issue 164 165 newIssue.Title = r.FormValue("title") 165 166 newIssue.Body = r.FormValue("body") 166 - newIssue.Mentions, newIssue.References = rp.refResolver.Resolve(r.Context(), newIssue.Body) 167 + newIssue.Mentions, newIssue.References = rp.mentionsResolver.Resolve(r.Context(), newIssue.Body) 167 168 168 169 if err := rp.validator.ValidateIssue(newIssue); err != nil { 169 170 l.Error("validation error", "err", err) ··· 314 315 if isIssueOwner || isRepoOwner || isCollaborator { 315 316 err = db.CloseIssues( 316 317 rp.db, 317 - db.FilterEq("id", issue.Id), 318 + orm.FilterEq("id", issue.Id), 318 319 ) 319 320 if err != nil { 320 321 l.Error("failed to close issue", "err", err) ··· 361 362 if isCollaborator || isRepoOwner || isIssueOwner { 362 363 err := db.ReopenIssues( 363 364 rp.db, 364 - db.FilterEq("id", issue.Id), 365 + orm.FilterEq("id", issue.Id), 365 366 ) 366 367 if err != nil { 367 368 l.Error("failed to reopen issue", "err", err) ··· 412 413 replyTo = &replyToUri 413 414 } 414 415 415 - mentions, references := rp.refResolver.Resolve(r.Context(), body) 416 + mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 416 417 417 418 comment := models.IssueComment{ 418 419 Did: user.Did, ··· 506 507 commentId := chi.URLParam(r, "commentId") 507 508 comments, err := db.GetIssueComments( 508 509 rp.db, 509 - db.FilterEq("id", commentId), 510 + orm.FilterEq("id", commentId), 510 511 ) 511 512 if err != nil { 512 513 l.Error("failed to fetch comment", "id", commentId) ··· 542 543 commentId := chi.URLParam(r, "commentId") 543 544 comments, err := db.GetIssueComments( 544 545 rp.db, 545 - db.FilterEq("id", commentId), 546 + orm.FilterEq("id", commentId), 546 547 ) 547 548 if err != nil { 548 549 l.Error("failed to fetch comment", "id", commentId) ··· 584 585 newComment := comment 585 586 newComment.Body = newBody 586 587 newComment.Edited = &now 587 - newComment.Mentions, newComment.References = rp.refResolver.Resolve(r.Context(), newBody) 588 + newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody) 588 589 589 590 record := newComment.AsRecord() 590 591 ··· 652 653 commentId := chi.URLParam(r, "commentId") 653 654 comments, err := db.GetIssueComments( 654 655 rp.db, 655 - db.FilterEq("id", commentId), 656 + orm.FilterEq("id", commentId), 656 657 ) 657 658 if err != nil { 658 659 l.Error("failed to fetch comment", "id", commentId) ··· 688 689 commentId := chi.URLParam(r, "commentId") 689 690 comments, err := db.GetIssueComments( 690 691 rp.db, 691 - db.FilterEq("id", commentId), 692 + orm.FilterEq("id", commentId), 692 693 ) 693 694 if err != nil { 694 695 l.Error("failed to fetch comment", "id", commentId) ··· 724 725 commentId := chi.URLParam(r, "commentId") 725 726 comments, err := db.GetIssueComments( 726 727 rp.db, 727 - db.FilterEq("id", commentId), 728 + orm.FilterEq("id", commentId), 728 729 ) 729 730 if err != nil { 730 731 l.Error("failed to fetch comment", "id", commentId) ··· 751 752 752 753 // optimistic deletion 753 754 deleted := time.Now() 754 - err = db.DeleteIssueComments(rp.db, db.FilterEq("id", comment.Id)) 755 + err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id)) 755 756 if err != nil { 756 757 l.Error("failed to delete comment", "err", err) 757 758 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 840 841 841 842 issues, err = db.GetIssues( 842 843 rp.db, 843 - db.FilterIn("id", res.Hits), 844 + orm.FilterIn("id", res.Hits), 844 845 ) 845 846 if err != nil { 846 847 l.Error("failed to get issues", "err", err) ··· 856 857 issues, err = db.GetIssuesPaginated( 857 858 rp.db, 858 859 page, 859 - db.FilterEq("repo_at", f.RepoAt()), 860 - db.FilterEq("open", openInt), 860 + orm.FilterEq("repo_at", f.RepoAt()), 861 + orm.FilterEq("open", openInt), 861 862 ) 862 863 if err != nil { 863 864 l.Error("failed to get issues", "err", err) ··· 868 869 869 870 labelDefs, err := db.GetLabelDefinitions( 870 871 rp.db, 871 - db.FilterIn("at_uri", f.Labels), 872 - db.FilterContains("scope", tangled.RepoIssueNSID), 872 + orm.FilterIn("at_uri", f.Labels), 873 + orm.FilterContains("scope", tangled.RepoIssueNSID), 873 874 ) 874 875 if err != nil { 875 876 l.Error("failed to fetch labels", "err", err) ··· 912 913 }) 913 914 case http.MethodPost: 914 915 body := r.FormValue("body") 915 - mentions, references := rp.refResolver.Resolve(r.Context(), body) 916 + mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 916 917 917 918 issue := &models.Issue{ 918 919 RepoAt: f.RepoAt(),
+19 -18
appview/knots/knots.go
··· 21 21 "tangled.org/core/appview/xrpcclient" 22 22 "tangled.org/core/eventconsumer" 23 23 "tangled.org/core/idresolver" 24 + "tangled.org/core/orm" 24 25 "tangled.org/core/rbac" 25 26 "tangled.org/core/tid" 26 27 ··· 72 73 user := k.OAuth.GetUser(r) 73 74 registrations, err := db.GetRegistrations( 74 75 k.Db, 75 - db.FilterEq("did", user.Did), 76 + orm.FilterEq("did", user.Did), 76 77 ) 77 78 if err != nil { 78 79 k.Logger.Error("failed to fetch knot registrations", "err", err) ··· 102 103 103 104 registrations, err := db.GetRegistrations( 104 105 k.Db, 105 - db.FilterEq("did", user.Did), 106 - db.FilterEq("domain", domain), 106 + orm.FilterEq("did", user.Did), 107 + orm.FilterEq("domain", domain), 107 108 ) 108 109 if err != nil { 109 110 l.Error("failed to get registrations", "err", err) ··· 127 128 repos, err := db.GetRepos( 128 129 k.Db, 129 130 0, 130 - db.FilterEq("knot", domain), 131 + orm.FilterEq("knot", domain), 131 132 ) 132 133 if err != nil { 133 134 l.Error("failed to get knot repos", "err", err) ··· 293 294 // get record from db first 294 295 registrations, err := db.GetRegistrations( 295 296 k.Db, 296 - db.FilterEq("did", user.Did), 297 - db.FilterEq("domain", domain), 297 + orm.FilterEq("did", user.Did), 298 + orm.FilterEq("domain", domain), 298 299 ) 299 300 if err != nil { 300 301 l.Error("failed to get registration", "err", err) ··· 321 322 322 323 err = db.DeleteKnot( 323 324 tx, 324 - db.FilterEq("did", user.Did), 325 - db.FilterEq("domain", domain), 325 + orm.FilterEq("did", user.Did), 326 + orm.FilterEq("domain", domain), 326 327 ) 327 328 if err != nil { 328 329 l.Error("failed to delete registration", "err", err) ··· 402 403 // get record from db first 403 404 registrations, err := db.GetRegistrations( 404 405 k.Db, 405 - db.FilterEq("did", user.Did), 406 - db.FilterEq("domain", domain), 406 + orm.FilterEq("did", user.Did), 407 + orm.FilterEq("domain", domain), 407 408 ) 408 409 if err != nil { 409 410 l.Error("failed to get registration", "err", err) ··· 493 494 // Get updated registration to show 494 495 registrations, err = db.GetRegistrations( 495 496 k.Db, 496 - db.FilterEq("did", user.Did), 497 - db.FilterEq("domain", domain), 497 + orm.FilterEq("did", user.Did), 498 + orm.FilterEq("domain", domain), 498 499 ) 499 500 if err != nil { 500 501 l.Error("failed to get registration", "err", err) ··· 529 530 530 531 registrations, err := db.GetRegistrations( 531 532 k.Db, 532 - db.FilterEq("did", user.Did), 533 - db.FilterEq("domain", domain), 534 - db.FilterIsNot("registered", "null"), 533 + orm.FilterEq("did", user.Did), 534 + orm.FilterEq("domain", domain), 535 + orm.FilterIsNot("registered", "null"), 535 536 ) 536 537 if err != nil { 537 538 l.Error("failed to get registration", "err", err) ··· 637 638 638 639 registrations, err := db.GetRegistrations( 639 640 k.Db, 640 - db.FilterEq("did", user.Did), 641 - db.FilterEq("domain", domain), 642 - db.FilterIsNot("registered", "null"), 641 + orm.FilterEq("did", user.Did), 642 + orm.FilterEq("domain", domain), 643 + orm.FilterIsNot("registered", "null"), 643 644 ) 644 645 if err != nil { 645 646 l.Error("failed to get registration", "err", err)
+5 -4
appview/labels/labels.go
··· 16 16 "tangled.org/core/appview/oauth" 17 17 "tangled.org/core/appview/pages" 18 18 "tangled.org/core/appview/validator" 19 + "tangled.org/core/orm" 19 20 "tangled.org/core/rbac" 20 21 "tangled.org/core/tid" 21 22 ··· 88 89 repoAt := r.Form.Get("repo") 89 90 subjectUri := r.Form.Get("subject") 90 91 91 - repo, err := db.GetRepo(l.db, db.FilterEq("at_uri", repoAt)) 92 + repo, err := db.GetRepo(l.db, orm.FilterEq("at_uri", repoAt)) 92 93 if err != nil { 93 94 fail("Failed to get repository.", err) 94 95 return 95 96 } 96 97 97 98 // find all the labels that this repo subscribes to 98 - repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt)) 99 + repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_at", repoAt)) 99 100 if err != nil { 100 101 fail("Failed to get labels for this repository.", err) 101 102 return ··· 106 107 labelAts = append(labelAts, rl.LabelAt.String()) 107 108 } 108 109 109 - actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts)) 110 + actx, err := db.NewLabelApplicationCtx(l.db, orm.FilterIn("at_uri", labelAts)) 110 111 if err != nil { 111 112 fail("Invalid form data.", err) 112 113 return 113 114 } 114 115 115 116 // calculate the start state by applying already known labels 116 - existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri)) 117 + existingOps, err := db.GetLabelOps(l.db, orm.FilterEq("subject", subjectUri)) 117 118 if err != nil { 118 119 fail("Invalid form data.", err) 119 120 return
+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
··· 18 18 "tangled.org/core/appview/pagination" 19 19 "tangled.org/core/appview/reporesolver" 20 20 "tangled.org/core/idresolver" 21 + "tangled.org/core/orm" 21 22 "tangled.org/core/rbac" 22 23 ) 23 24 ··· 217 218 218 219 repo, err := db.GetRepo( 219 220 mw.db, 220 - db.FilterEq("did", id.DID.String()), 221 - db.FilterEq("name", repoName), 221 + orm.FilterEq("did", id.DID.String()), 222 + orm.FilterEq("name", repoName), 222 223 ) 223 224 if err != nil { 224 225 log.Println("failed to resolve repo", "err", err)
+5 -4
appview/notifications/notifications.go
··· 11 11 "tangled.org/core/appview/oauth" 12 12 "tangled.org/core/appview/pages" 13 13 "tangled.org/core/appview/pagination" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 17 type Notifications struct { ··· 53 54 54 55 total, err := db.CountNotifications( 55 56 n.db, 56 - db.FilterEq("recipient_did", user.Did), 57 + orm.FilterEq("recipient_did", user.Did), 57 58 ) 58 59 if err != nil { 59 60 l.Error("failed to get total notifications", "err", err) ··· 64 65 notifications, err := db.GetNotificationsWithEntities( 65 66 n.db, 66 67 page, 67 - db.FilterEq("recipient_did", user.Did), 68 + orm.FilterEq("recipient_did", user.Did), 68 69 ) 69 70 if err != nil { 70 71 l.Error("failed to get notifications", "err", err) ··· 96 97 97 98 count, err := db.CountNotifications( 98 99 n.db, 99 - db.FilterEq("recipient_did", user.Did), 100 - db.FilterEq("read", 0), 100 + orm.FilterEq("recipient_did", user.Did), 101 + orm.FilterEq("read", 0), 101 102 ) 102 103 if err != nil { 103 104 http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
+77 -66
appview/notify/db/db.go
··· 3 3 import ( 4 4 "context" 5 5 "log" 6 - "maps" 7 6 "slices" 8 7 9 8 "github.com/bluesky-social/indigo/atproto/syntax" ··· 12 11 "tangled.org/core/appview/models" 13 12 "tangled.org/core/appview/notify" 14 13 "tangled.org/core/idresolver" 14 + "tangled.org/core/orm" 15 + "tangled.org/core/sets" 15 16 ) 16 17 17 18 const ( 18 - maxMentions = 5 19 + maxMentions = 8 19 20 ) 20 21 21 22 type databaseNotifier struct { ··· 42 43 return 43 44 } 44 45 var err error 45 - repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(star.RepoAt))) 46 + repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(star.RepoAt))) 46 47 if err != nil { 47 48 log.Printf("NewStar: failed to get repos: %v", err) 48 49 return 49 50 } 50 51 51 52 actorDid := syntax.DID(star.Did) 52 - recipients := []syntax.DID{syntax.DID(repo.Did)} 53 + recipients := sets.Singleton(syntax.DID(repo.Did)) 53 54 eventType := models.NotificationTypeRepoStarred 54 55 entityType := "repo" 55 56 entityId := star.RepoAt.String() ··· 74 75 } 75 76 76 77 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())) 78 + collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 84 79 if err != nil { 85 80 log.Printf("failed to fetch collaborators: %v", err) 86 81 return 87 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)) 88 89 for _, c := range collaborators { 89 - recipients = append(recipients, c.SubjectDid) 90 + recipients.Insert(c.SubjectDid) 91 + } 92 + for _, m := range mentions { 93 + recipients.Remove(m) 90 94 } 91 95 92 96 actorDid := syntax.DID(issue.Did) ··· 108 112 ) 109 113 n.notifyEvent( 110 114 actorDid, 111 - mentions, 115 + sets.Collect(slices.Values(mentions)), 112 116 models.NotificationTypeUserMentioned, 113 117 entityType, 114 118 entityId, ··· 119 123 } 120 124 121 125 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)) 126 + issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt)) 123 127 if err != nil { 124 128 log.Printf("NewIssueComment: failed to get issues: %v", err) 125 129 return ··· 130 134 } 131 135 issue := issues[0] 132 136 133 - var recipients []syntax.DID 134 - recipients = append(recipients, syntax.DID(issue.Repo.Did)) 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)) 135 143 136 144 if comment.IsReply() { 137 145 // if this comment is a reply, then notify everybody in that thread 138 146 parentAtUri := *comment.ReplyTo 139 - allThreads := issue.CommentList() 140 147 141 148 // find the parent thread, and add all DIDs from here to the recipient list 142 - for _, t := range allThreads { 149 + for _, t := range issue.CommentList() { 143 150 if t.Self.AtUri().String() == parentAtUri { 144 - recipients = append(recipients, t.Participants()...) 151 + for _, p := range t.Participants() { 152 + recipients.Insert(p) 153 + } 145 154 } 146 155 } 147 156 } else { 148 157 // not a reply, notify just the issue author 149 - recipients = append(recipients, syntax.DID(issue.Did)) 158 + recipients.Insert(syntax.DID(issue.Did)) 159 + } 160 + 161 + for _, m := range mentions { 162 + recipients.Remove(m) 150 163 } 151 164 152 165 actorDid := syntax.DID(comment.Did) ··· 168 181 ) 169 182 n.notifyEvent( 170 183 actorDid, 171 - mentions, 184 + sets.Collect(slices.Values(mentions)), 172 185 models.NotificationTypeUserMentioned, 173 186 entityType, 174 187 entityId, ··· 184 197 185 198 func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) { 186 199 actorDid := syntax.DID(follow.UserDid) 187 - recipients := []syntax.DID{syntax.DID(follow.SubjectDid)} 200 + recipients := sets.Singleton(syntax.DID(follow.SubjectDid)) 188 201 eventType := models.NotificationTypeFollowed 189 202 entityType := "follow" 190 203 entityId := follow.UserDid ··· 207 220 } 208 221 209 222 func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) { 210 - repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt))) 223 + repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt))) 211 224 if err != nil { 212 225 log.Printf("NewPull: failed to get repos: %v", err) 213 226 return 214 227 } 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())) 228 + collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 222 229 if err != nil { 223 230 log.Printf("failed to fetch collaborators: %v", err) 224 231 return 225 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)) 226 238 for _, c := range collaborators { 227 - recipients = append(recipients, c.SubjectDid) 239 + recipients.Insert(c.SubjectDid) 228 240 } 229 241 230 242 actorDid := syntax.DID(pull.OwnerDid) ··· 258 270 return 259 271 } 260 272 261 - repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt)) 273 + repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt)) 262 274 if err != nil { 263 275 log.Printf("NewPullComment: failed to get repos: %v", err) 264 276 return ··· 267 279 // build up the recipients list: 268 280 // - repo owner 269 281 // - all pull participants 270 - var recipients []syntax.DID 271 - recipients = append(recipients, syntax.DID(repo.Did)) 282 + // - remove those already mentioned 283 + recipients := sets.Singleton(syntax.DID(repo.Did)) 272 284 for _, p := range pull.Participants() { 273 - recipients = append(recipients, syntax.DID(p)) 285 + recipients.Insert(syntax.DID(p)) 286 + } 287 + for _, m := range mentions { 288 + recipients.Remove(m) 274 289 } 275 290 276 291 actorDid := syntax.DID(comment.OwnerDid) ··· 294 309 ) 295 310 n.notifyEvent( 296 311 actorDid, 297 - mentions, 312 + sets.Collect(slices.Values(mentions)), 298 313 models.NotificationTypeUserMentioned, 299 314 entityType, 300 315 entityId, ··· 321 336 } 322 337 323 338 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())) 339 + collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 331 340 if err != nil { 332 341 log.Printf("failed to fetch collaborators: %v", err) 333 342 return 334 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)) 335 350 for _, c := range collaborators { 336 - recipients = append(recipients, c.SubjectDid) 351 + recipients.Insert(c.SubjectDid) 337 352 } 338 353 for _, p := range issue.Participants() { 339 - recipients = append(recipients, syntax.DID(p)) 354 + recipients.Insert(syntax.DID(p)) 340 355 } 341 356 342 357 entityType := "pull" ··· 366 381 367 382 func (n *databaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) { 368 383 // Get repo details 369 - repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt))) 384 + repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt))) 370 385 if err != nil { 371 386 log.Printf("NewPullState: failed to get repos: %v", err) 372 387 return 373 388 } 374 389 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())) 390 + collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 381 391 if err != nil { 382 392 log.Printf("failed to fetch collaborators: %v", err) 383 393 return 384 394 } 395 + 396 + // build up the recipients list: 397 + // - repo owner 398 + // - all pull participants 399 + recipients := sets.Singleton(syntax.DID(repo.Did)) 385 400 for _, c := range collaborators { 386 - recipients = append(recipients, c.SubjectDid) 401 + recipients.Insert(c.SubjectDid) 387 402 } 388 403 for _, p := range pull.Participants() { 389 - recipients = append(recipients, syntax.DID(p)) 404 + recipients.Insert(syntax.DID(p)) 390 405 } 391 406 392 407 entityType := "pull" ··· 422 437 423 438 func (n *databaseNotifier) notifyEvent( 424 439 actorDid syntax.DID, 425 - recipients []syntax.DID, 440 + recipients sets.Set[syntax.DID], 426 441 eventType models.NotificationType, 427 442 entityType string, 428 443 entityId string, ··· 430 445 issueId *int64, 431 446 pullId *int64, 432 447 ) { 433 - if eventType == models.NotificationTypeUserMentioned && len(recipients) > maxMentions { 434 - recipients = recipients[:maxMentions] 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 435 451 } 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 - } 452 + 453 + recipients.Remove(actorDid) 443 454 444 455 prefMap, err := db.GetNotificationPreferences( 445 456 n.db, 446 - db.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))), 457 + orm.FilterIn("user_did", slices.Collect(recipients.All())), 447 458 ) 448 459 if err != nil { 449 460 // failed to get prefs for users ··· 459 470 defer tx.Rollback() 460 471 461 472 // filter based on preferences 462 - for recipientDid := range recipientSet { 473 + for recipientDid := range recipients.All() { 463 474 prefs, ok := prefMap[recipientDid] 464 475 if !ok { 465 476 prefs = models.DefaultNotificationPreferences(recipientDid)
-1
appview/notify/merged_notifier.go
··· 39 39 v.Call(in) 40 40 }(n) 41 41 } 42 - wg.Wait() 43 42 } 44 43 45 44 func (m *mergedNotifier) NewRepo(ctx context.Context, repo *models.Repo) {
+3 -2
appview/oauth/handler.go
··· 16 16 "tangled.org/core/api/tangled" 17 17 "tangled.org/core/appview/db" 18 18 "tangled.org/core/consts" 19 + "tangled.org/core/orm" 19 20 "tangled.org/core/tid" 20 21 ) 21 22 ··· 97 98 // and create an sh.tangled.spindle.member record with that 98 99 spindleMembers, err := db.GetSpindleMembers( 99 100 o.Db, 100 - db.FilterEq("instance", "spindle.tangled.sh"), 101 - db.FilterEq("subject", did), 101 + orm.FilterEq("instance", "spindle.tangled.sh"), 102 + orm.FilterEq("subject", did), 102 103 ) 103 104 if err != nil { 104 105 l.Error("failed to get spindle members", "err", err)
+1 -1
appview/pages/funcmap.go
··· 162 162 } 163 163 return pairs, nil 164 164 }, 165 - "append": func(s []string, values ...string) []string { 165 + "append": func(s []any, values ...any) []any { 166 166 s = append(s, values...) 167 167 return s 168 168 },
+14
appview/pages/markup/extension/atlink.go
··· 36 36 } 37 37 38 38 var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)`) 39 + var markdownLinkRegexp = regexp.MustCompile(`(?ms)\[.*\]\(.*\)`) 39 40 40 41 type atParser struct{} 41 42 ··· 55 56 if m == nil { 56 57 return nil 57 58 } 59 + 60 + if !util.IsSpaceRune(block.PrecendingCharacter()) { 61 + return nil 62 + } 63 + 64 + // Check for all links in the markdown to see if the handle found is inside one 65 + linksIndexes := markdownLinkRegexp.FindAllIndex(block.Source(), -1) 66 + for _, linkMatch := range linksIndexes { 67 + if linkMatch[0] < segment.Start && segment.Start < linkMatch[1] { 68 + return nil 69 + } 70 + } 71 + 58 72 atSegment := text.NewSegment(segment.Start, segment.Start+m[1]) 59 73 block.Advance(m[1]) 60 74 node := &AtNode{}
-2
appview/pages/markup/markdown.go
··· 12 12 13 13 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 14 14 "github.com/alecthomas/chroma/v2/styles" 15 - treeblood "github.com/wyatt915/goldmark-treeblood" 16 15 "github.com/yuin/goldmark" 17 16 highlighting "github.com/yuin/goldmark-highlighting/v2" 18 17 "github.com/yuin/goldmark/ast" ··· 65 64 extension.NewFootnote( 66 65 extension.WithFootnoteIDPrefix([]byte("footnote")), 67 66 ), 68 - treeblood.MathML(), 69 67 callout.CalloutExtention, 70 68 textension.AtExt, 71 69 ),
+2 -3
appview/pages/pages.go
··· 31 31 "github.com/bluesky-social/indigo/atproto/identity" 32 32 "github.com/bluesky-social/indigo/atproto/syntax" 33 33 "github.com/go-git/go-git/v5/plumbing" 34 - "github.com/go-git/go-git/v5/plumbing/object" 35 34 ) 36 35 37 36 //go:embed templates/* static legal ··· 641 640 } 642 641 643 642 func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 644 - return p.executePlain("fragments/starBtn", w, params) 643 + return p.executePlain("fragments/starBtn-oob", w, params) 645 644 } 646 645 647 646 type RepoIndexParams struct { ··· 649 648 RepoInfo repoinfo.RepoInfo 650 649 Active string 651 650 TagMap map[string][]string 652 - CommitsTrunc []*object.Commit 651 + CommitsTrunc []types.Commit 653 652 TagsTrunc []*types.TagReference 654 653 BranchesTrunc []types.Branch 655 654 // ForkInfo *types.ForkInfo
-46
appview/pages/repoinfo/repoinfo.go
··· 1 1 package repoinfo 2 2 3 3 import ( 4 - "encoding/json" 5 4 "fmt" 6 5 "path" 7 6 "slices" ··· 118 117 func (r RolesInRepo) IsPushAllowed() bool { 119 118 return slices.Contains(r.Roles, "repo:push") 120 119 } 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 - }
-22
appview/pages/templates/fragments/breadcrumb.html
··· 1 - {{ define "fragments/breadcrumb" }} 2 - {{ $items := . }} 3 - {{ if gt (len $items) 0 }} 4 - <script type="application/ld+json"> 5 - { 6 - "@context": "https://schema.org", 7 - "@type": "BreadcrumbList", 8 - "itemListElement": [ 9 - {{ range $idx, $item := $items }} 10 - {{ if gt $idx 0 }},{{ end }} 11 - { 12 - "@type": "ListItem", 13 - "position": {{ add $idx 1 }}, 14 - "name": "{{ index $item 0 }}", 15 - "item": "{{ index $item 1 }}" 16 - } 17 - {{ end }} 18 - ] 19 - } 20 - </script> 21 - {{ end }} 22 - {{ end }}
-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
··· 1 + {{ define "fragments/starBtn-oob" }} 2 + <div hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'> 3 + {{ template "fragments/starBtn" . }} 4 + </div> 5 + {{ end }}
+1 -3
appview/pages/templates/fragments/starBtn.html
··· 1 1 {{ define "fragments/starBtn" }} 2 + {{/* NOTE: this fragment is always replaced with hx-swap-oob */}} 2 3 <button 3 4 id="starBtn" 4 5 class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group" ··· 10 11 {{ end }} 11 12 12 13 hx-trigger="click" 13 - hx-target="this" 14 - hx-swap="outerHTML" 15 - hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]' 16 14 hx-disabled-elt="#starBtn" 17 15 > 18 16 {{ if .IsStarred }}
+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
··· 1 1 {{ define "title" }}good first issues{{ end }} 2 2 3 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 4 <meta property="og:title" content="good first issues ยท tangled" /> 8 - <meta property="og:type" content="website" /> 5 + <meta property="og:type" content="object" /> 9 6 <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> 7 + <meta property="og:description" content="Find good first issues to contribute to open source projects" /> 31 8 {{ end }} 32 - 33 - {{ define "canonical" }}https://tangled.org/goodfirstissues{{ end }} 34 9 35 10 {{ define "content" }} 36 11 <div class="grid grid-cols-10">
+2 -26
appview/pages/templates/layouts/base.html
··· 4 4 <head> 5 5 <meta charset="UTF-8" /> 6 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"/> 7 + <meta name="description" content="Social coding, but for real this time!"/> 9 8 <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 9 15 10 <script defer src="/static/htmx.min.js"></script> 16 11 <script defer src="/static/htmx-ext-ws.min.js"></script> ··· 20 15 <link rel="preconnect" href="https://avatar.tangled.sh" /> 21 16 <link rel="preconnect" href="https://camo.tangled.sh" /> 22 17 23 - <!-- RSS Feed Discovery --> 24 - {{ block "rss" . }}{{ end }} 25 - 26 18 <!-- pwa manifest --> 27 19 <link rel="manifest" href="/pwa-manifest.json" /> 28 20 ··· 30 22 <link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin /> 31 23 32 24 <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 - 25 + <title>{{ block "title" . }}{{ end }} ยท tangled</title> 50 26 {{ block "extrameta" . }}{{ end }} 51 27 </head> 52 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
··· 10 10 <meta property="og:image" content="{{ $avatarUrl }}" /> 11 11 <meta property="og:image:width" content="512" /> 12 12 <meta property="og:image:height" content="512" /> 13 - 13 + 14 14 <meta name="twitter:card" content="summary" /> 15 15 <meta name="twitter:title" content="{{ $handle }}" /> 16 16 <meta name="twitter:description" content="{{ or .Card.Profile.Description $handle }}" /> 17 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 18 {{ end }} 38 19 39 20 {{ define "content" }}
+34 -9
appview/pages/templates/repo/commit.html
··· 25 25 </div> 26 26 27 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 }} 28 + <p class="flex flex-wrap items-center gap-1 text-sm text-gray-500 dark:text-gray-300"> 29 + {{ template "attribution" . }} 36 30 37 31 <span class="px-1 select-none before:content-['\00B7']"></span> 38 - {{ template "repo/fragments/time" $commit.Author.When }} 32 + {{ template "repo/fragments/time" $commit.Committer.When }} 39 33 <span class="px-1 select-none before:content-['\00B7']"></span> 40 34 41 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> ··· 78 72 79 73 </section> 80 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 }} 81 106 82 107 {{ define "topbarLayout" }} 83 108 <header class="col-span-full" style="z-index: 20;">
+6 -6
appview/pages/templates/repo/fragments/backlinks.html
··· 14 14 <div class="flex gap-2 items-center"> 15 15 {{ if .State.IsClosed }} 16 16 <span class="text-gray-500 dark:text-gray-400"> 17 - {{ i "ban" "w-4 h-4" }} 17 + {{ i "ban" "size-3" }} 18 18 </span> 19 19 {{ else if eq .Kind.String "issues" }} 20 20 <span class="text-green-600 dark:text-green-500"> 21 - {{ i "circle-dot" "w-4 h-4" }} 21 + {{ i "circle-dot" "size-3" }} 22 22 </span> 23 23 {{ else if .State.IsOpen }} 24 24 <span class="text-green-600 dark:text-green-500"> 25 - {{ i "git-pull-request" "w-4 h-4" }} 25 + {{ i "git-pull-request" "size-3" }} 26 26 </span> 27 27 {{ else if .State.IsMerged }} 28 28 <span class="text-purple-600 dark:text-purple-500"> 29 - {{ i "git-merge" "w-4 h-4" }} 29 + {{ i "git-merge" "size-3" }} 30 30 </span> 31 31 {{ else }} 32 32 <span class="text-gray-600 dark:text-gray-300"> 33 - {{ i "git-pull-request-closed" "w-4 h-4" }} 33 + {{ i "git-pull-request-closed" "size-3" }} 34 34 </span> 35 35 {{ end }} 36 - <a href="{{ . }}"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a> 36 + <a href="{{ . }}" class="line-clamp-1 text-sm"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a> 37 37 </div> 38 38 {{ if not (eq $.RepoInfo.FullName $repoUrl) }} 39 39 <div>
+1 -16
appview/pages/templates/repo/fragments/participants.html
··· 6 6 <span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span> 7 7 <span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span> 8 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> 9 + {{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "w-8 h-8") }} 25 10 </div> 26 11 {{ end }}
+30 -44
appview/pages/templates/repo/index.html
··· 5 5 {{ template "repo/fragments/meta" . }} 6 6 7 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 8 {{ end }} 45 9 46 10 {{ define "repoContent" }} ··· 50 14 {{ end }} 51 15 <div class="flex items-center justify-between pb-5"> 52 16 {{ block "branchSelector" . }}{{ end }} 53 - <div class="flex md:hidden items-center gap-2"> 17 + <div class="flex md:hidden items-center gap-3"> 54 18 <a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold"> 55 19 {{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }} 56 20 </a> ··· 102 66 103 67 {{ define "branchSelector" }} 104 68 <div class="flex gap-2 items-center justify-between w-full"> 105 - <div class="flex gap-2 items-center"> 69 + <div class="flex gap-2 items-stretch"> 106 70 <select 107 71 onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)" 108 72 class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700" ··· 264 228 <span 265 229 class="mx-1 before:content-['ยท'] before:select-none" 266 230 ></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> 231 + {{ template "attribution" (list . $.EmailToDid) }} 273 232 <div class="inline-block px-1 select-none after:content-['ยท']"></div> 274 233 {{ template "repo/fragments/time" .Committer.When }} 275 234 ··· 295 254 {{ end }} 296 255 </div> 297 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> 298 284 {{ end }} 299 285 300 286 {{ define "branchList" }}
+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
··· 17 17 <div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700"> 18 18 {{ $grid := "grid grid-cols-14 gap-4" }} 19 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> 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 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 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 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> 25 24 </div> 26 25 {{ range $index, $commit := .Commits }} 27 26 {{ $messageParts := splitN $commit.Message "\n\n" 2 }} 28 27 <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 }} 28 + <div class="align-top col-span-3"> 29 + {{ template "attribution" (list $commit $.EmailToDid) }} 36 30 </div> 37 31 <div class="align-top font-mono flex items-start col-span-3"> 38 32 {{ $verified := $.VerifiedCommits.IsVerified $commit.Hash.String }} ··· 61 55 <div class="align-top col-span-6"> 62 56 <div> 63 57 <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a> 58 + 64 59 {{ if gt (len $messageParts) 1 }} 65 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> 66 61 {{ end }} ··· 72 67 </span> 73 68 {{ end }} 74 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> 75 78 </div> 76 79 77 80 {{ if gt (len $messageParts) 1 }} 78 81 <p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p> 79 82 {{ 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 83 </div> 88 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> 89 85 </div> ··· 152 148 </a> 153 149 </span> 154 150 <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> 151 + {{ template "attribution" (list $commit $.EmailToDid) }} 162 152 <div class="inline-block px-1 select-none after:content-['ยท']"></div> 163 153 <span>{{ template "repo/fragments/shortTime" $commit.Committer.When }}</span> 164 154 ··· 176 166 </div> 177 167 </section> 178 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> 179 196 {{ end }} 180 197 181 198 {{ define "repoAfter" }}
+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 }} 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 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" /> 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 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 }}" /> 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 19 {{ end }}
+1 -1
appview/pages/templates/strings/string.html
··· 17 17 <span class="select-none">/</span> 18 18 <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 19 19 </div> 20 - <div class="flex gap-2 text-base"> 20 + <div class="flex gap-2 items-stretch text-base"> 21 21 {{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }} 22 22 <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 23 23 hx-boost="true"
+3 -49
appview/pages/templates/timeline/home.html
··· 1 1 {{ define "title" }}tangled &middot; tightly-knit social coding{{ end }} 2 2 3 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" /> 4 + <meta property="og:title" content="timeline ยท tangled" /> 5 + <meta property="og:type" content="object" /> 10 6 <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> 7 + <meta property="og:description" content="tightly-knit social coding" /> 52 8 {{ end }} 53 - 54 - {{ define "canonical" }}https://tangled.org{{ end }} 55 9 56 10 57 11 {{ define "content" }}
+9 -6
appview/pages/templates/user/signup.html
··· 43 43 page to complete your registration. 44 44 </span> 45 45 <div class="w-full mt-4 text-center"> 46 - <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div> 46 + <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div> 47 47 </div> 48 48 <button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" > 49 49 <span>join now</span> 50 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> 51 59 </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 60 </main> 58 61 </body> 59 62 </html>
+12 -11
appview/pipelines/pipelines.go
··· 16 16 "tangled.org/core/appview/reporesolver" 17 17 "tangled.org/core/eventconsumer" 18 18 "tangled.org/core/idresolver" 19 + "tangled.org/core/orm" 19 20 "tangled.org/core/rbac" 20 21 spindlemodel "tangled.org/core/spindle/models" 21 22 ··· 81 82 ps, err := db.GetPipelineStatuses( 82 83 p.db, 83 84 30, 84 - db.FilterEq("repo_owner", f.Did), 85 - db.FilterEq("repo_name", f.Name), 86 - db.FilterEq("knot", f.Knot), 85 + orm.FilterEq("repo_owner", f.Did), 86 + orm.FilterEq("repo_name", f.Name), 87 + orm.FilterEq("knot", f.Knot), 87 88 ) 88 89 if err != nil { 89 90 l.Error("failed to query db", "err", err) ··· 122 123 ps, err := db.GetPipelineStatuses( 123 124 p.db, 124 125 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), 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), 129 130 ) 130 131 if err != nil { 131 132 l.Error("failed to query db", "err", err) ··· 189 190 ps, err := db.GetPipelineStatuses( 190 191 p.db, 191 192 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), 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), 196 197 ) 197 198 if err != nil || len(ps) != 1 { 198 199 l.Error("pipeline query failed", "err", err, "count", len(ps))
+2 -1
appview/pulls/opengraph.go
··· 13 13 "tangled.org/core/appview/db" 14 14 "tangled.org/core/appview/models" 15 15 "tangled.org/core/appview/ogcard" 16 + "tangled.org/core/orm" 16 17 "tangled.org/core/patchutil" 17 18 "tangled.org/core/types" 18 19 ) ··· 276 277 } 277 278 278 279 // Get comment count from database 279 - comments, err := db.GetPullComments(s.db, db.FilterEq("pull_id", pull.ID)) 280 + comments, err := db.GetPullComments(s.db, orm.FilterEq("pull_id", pull.ID)) 280 281 if err != nil { 281 282 log.Printf("failed to get pull comments: %v", err) 282 283 }
+56 -47
appview/pulls/pulls.go
··· 19 19 "tangled.org/core/appview/config" 20 20 "tangled.org/core/appview/db" 21 21 pulls_indexer "tangled.org/core/appview/indexer/pulls" 22 + "tangled.org/core/appview/mentions" 22 23 "tangled.org/core/appview/models" 23 24 "tangled.org/core/appview/notify" 24 25 "tangled.org/core/appview/oauth" 25 26 "tangled.org/core/appview/pages" 26 27 "tangled.org/core/appview/pages/markup" 27 28 "tangled.org/core/appview/pages/repoinfo" 28 - "tangled.org/core/appview/refresolver" 29 29 "tangled.org/core/appview/reporesolver" 30 30 "tangled.org/core/appview/validator" 31 31 "tangled.org/core/appview/xrpcclient" 32 32 "tangled.org/core/idresolver" 33 + "tangled.org/core/orm" 33 34 "tangled.org/core/patchutil" 34 35 "tangled.org/core/rbac" 35 36 "tangled.org/core/tid" ··· 44 45 ) 45 46 46 47 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 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 59 60 } 60 61 61 62 func New( ··· 63 64 repoResolver *reporesolver.RepoResolver, 64 65 pages *pages.Pages, 65 66 resolver *idresolver.Resolver, 66 - refResolver *refresolver.Resolver, 67 + mentionsResolver *mentions.Resolver, 67 68 db *db.DB, 68 69 config *config.Config, 69 70 notifier notify.Notifier, ··· 73 74 logger *slog.Logger, 74 75 ) *Pulls { 75 76 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, 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, 88 89 } 89 90 } 90 91 ··· 190 191 ps, err := db.GetPipelineStatuses( 191 192 s.db, 192 193 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), 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), 197 198 ) 198 199 if err != nil { 199 200 log.Printf("failed to fetch pipeline statuses: %s", err) ··· 217 218 218 219 labelDefs, err := db.GetLabelDefinitions( 219 220 s.db, 220 - db.FilterIn("at_uri", f.Labels), 221 - db.FilterContains("scope", tangled.RepoPullNSID), 221 + orm.FilterIn("at_uri", f.Labels), 222 + orm.FilterContains("scope", tangled.RepoPullNSID), 222 223 ) 223 224 if err != nil { 224 225 log.Println("failed to fetch labels", err) ··· 597 598 598 599 pulls, err := db.GetPulls( 599 600 s.db, 600 - db.FilterIn("id", ids), 601 + orm.FilterIn("id", ids), 601 602 ) 602 603 if err != nil { 603 604 log.Println("failed to get pulls", err) ··· 648 649 ps, err := db.GetPipelineStatuses( 649 650 s.db, 650 651 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), 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), 655 656 ) 656 657 if err != nil { 657 658 log.Printf("failed to fetch pipeline statuses: %s", err) ··· 664 665 665 666 labelDefs, err := db.GetLabelDefinitions( 666 667 s.db, 667 - db.FilterIn("at_uri", f.Labels), 668 - db.FilterContains("scope", tangled.RepoPullNSID), 668 + orm.FilterIn("at_uri", f.Labels), 669 + orm.FilterContains("scope", tangled.RepoPullNSID), 669 670 ) 670 671 if err != nil { 671 672 log.Println("failed to fetch labels", err) ··· 729 730 return 730 731 } 731 732 732 - mentions, references := s.refResolver.Resolve(r.Context(), body) 733 + mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 733 734 734 735 // Start a transaction 735 736 tx, err := s.db.BeginTx(r.Context(), nil) ··· 1205 1206 } 1206 1207 } 1207 1208 1208 - mentions, references := s.refResolver.Resolve(r.Context(), body) 1209 + mentions, references := s.mentionsResolver.Resolve(r.Context(), body) 1209 1210 1210 1211 rkey := tid.TID() 1211 1212 initialSubmission := models.PullSubmission{ ··· 1365 1366 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1366 1367 return 1367 1368 } 1369 + 1368 1370 } 1369 1371 1370 1372 if err = tx.Commit(); err != nil { 1371 1373 log.Println("failed to create pull request", err) 1372 1374 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1373 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) 1374 1383 } 1375 1384 1376 1385 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) ··· 1498 1507 // fork repo 1499 1508 repo, err := db.GetRepo( 1500 1509 s.db, 1501 - db.FilterEq("did", forkOwnerDid), 1502 - db.FilterEq("name", forkName), 1510 + orm.FilterEq("did", forkOwnerDid), 1511 + orm.FilterEq("name", forkName), 1503 1512 ) 1504 1513 if err != nil { 1505 1514 log.Println("failed to get repo", "did", forkOwnerDid, "name", forkName, "err", err) ··· 2066 2075 tx, 2067 2076 p.ParentChangeId, 2068 2077 // 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), 2078 + orm.FilterEq("repo_at", p.RepoAt.String()), 2079 + orm.FilterEq("owner_did", p.OwnerDid), 2080 + orm.FilterEq("change_id", p.ChangeId), 2072 2081 ) 2073 2082 2074 2083 if err != nil { ··· 2397 2406 body := fp.Body 2398 2407 rkey := tid.TID() 2399 2408 2400 - mentions, references := s.refResolver.Resolve(ctx, body) 2409 + mentions, references := s.mentionsResolver.Resolve(ctx, body) 2401 2410 2402 2411 initialSubmission := models.PullSubmission{ 2403 2412 Patch: fp.Raw,
-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
··· 15 15 "tangled.org/core/appview/models" 16 16 "tangled.org/core/appview/pages" 17 17 "tangled.org/core/appview/xrpcclient" 18 + "tangled.org/core/orm" 18 19 "tangled.org/core/tid" 19 20 "tangled.org/core/types" 20 21 ··· 155 156 156 157 artifacts, err := db.GetArtifact( 157 158 rp.db, 158 - db.FilterEq("repo_at", f.RepoAt()), 159 - db.FilterEq("tag", tag.Tag.Hash[:]), 160 - db.FilterEq("name", filename), 159 + orm.FilterEq("repo_at", f.RepoAt()), 160 + orm.FilterEq("tag", tag.Tag.Hash[:]), 161 + orm.FilterEq("name", filename), 161 162 ) 162 163 if err != nil { 163 164 log.Println("failed to get artifacts", err) ··· 234 235 235 236 artifacts, err := db.GetArtifact( 236 237 rp.db, 237 - db.FilterEq("repo_at", f.RepoAt()), 238 - db.FilterEq("tag", tag[:]), 239 - db.FilterEq("name", filename), 238 + orm.FilterEq("repo_at", f.RepoAt()), 239 + orm.FilterEq("tag", tag[:]), 240 + orm.FilterEq("name", filename), 240 241 ) 241 242 if err != nil { 242 243 log.Println("failed to get artifacts", err) ··· 276 277 defer tx.Rollback() 277 278 278 279 err = db.DeleteArtifact(tx, 279 - db.FilterEq("repo_at", f.RepoAt()), 280 - db.FilterEq("tag", artifact.Tag[:]), 281 - db.FilterEq("name", filename), 280 + orm.FilterEq("repo_at", f.RepoAt()), 281 + orm.FilterEq("tag", artifact.Tag[:]), 282 + orm.FilterEq("name", filename), 282 283 ) 283 284 if err != nil { 284 285 log.Println("failed to remove artifact record from db", err)
+3 -2
appview/repo/feed.go
··· 11 11 "tangled.org/core/appview/db" 12 12 "tangled.org/core/appview/models" 13 13 "tangled.org/core/appview/pagination" 14 + "tangled.org/core/orm" 14 15 15 16 "github.com/bluesky-social/indigo/atproto/identity" 16 17 "github.com/bluesky-social/indigo/atproto/syntax" ··· 20 21 func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) { 21 22 const feedLimitPerType = 100 22 23 23 - pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", repo.RepoAt())) 24 + pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, orm.FilterEq("repo_at", repo.RepoAt())) 24 25 if err != nil { 25 26 return nil, err 26 27 } ··· 28 29 issues, err := db.GetIssuesPaginated( 29 30 rp.db, 30 31 pagination.Page{Limit: feedLimitPerType}, 31 - db.FilterEq("repo_at", repo.RepoAt()), 32 + orm.FilterEq("repo_at", repo.RepoAt()), 32 33 ) 33 34 if err != nil { 34 35 return nil, err
+4 -3
appview/repo/index.go
··· 23 23 "tangled.org/core/appview/models" 24 24 "tangled.org/core/appview/pages" 25 25 "tangled.org/core/appview/xrpcclient" 26 + "tangled.org/core/orm" 26 27 "tangled.org/core/types" 27 28 28 29 "github.com/go-chi/chi/v5" ··· 122 123 l.Error("failed to get email to did map", "err", err) 123 124 } 124 125 125 - vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc) 126 + vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, commitsTrunc) 126 127 if err != nil { 127 128 l.Error("failed to GetVerifiedObjectCommits", "err", err) 128 129 } ··· 171 172 // first attempt to fetch from db 172 173 langs, err := db.GetRepoLanguages( 173 174 rp.db, 174 - db.FilterEq("repo_at", repo.RepoAt()), 175 - db.FilterEq("ref", currentRef), 175 + orm.FilterEq("repo_at", repo.RepoAt()), 176 + orm.FilterEq("ref", currentRef), 176 177 ) 177 178 178 179 if err != nil || langs == nil {
+2 -2
appview/repo/log.go
··· 116 116 l.Error("failed to fetch email to did mapping", "err", err) 117 117 } 118 118 119 - vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits) 119 + vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, xrpcResp.Commits) 120 120 if err != nil { 121 121 l.Error("failed to GetVerifiedObjectCommits", "err", err) 122 122 } ··· 192 192 l.Error("failed to get email to did mapping", "err", err) 193 193 } 194 194 195 - vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff}) 195 + vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.Commit{result.Diff.Commit}) 196 196 if err != nil { 197 197 l.Error("failed to GetVerifiedCommits", "err", err) 198 198 }
+3 -2
appview/repo/opengraph.go
··· 16 16 "tangled.org/core/appview/db" 17 17 "tangled.org/core/appview/models" 18 18 "tangled.org/core/appview/ogcard" 19 + "tangled.org/core/orm" 19 20 "tangled.org/core/types" 20 21 ) 21 22 ··· 338 339 var languageStats []types.RepoLanguageDetails 339 340 langs, err := db.GetRepoLanguages( 340 341 rp.db, 341 - db.FilterEq("repo_at", f.RepoAt()), 342 - db.FilterEq("is_default_ref", 1), 342 + orm.FilterEq("repo_at", f.RepoAt()), 343 + orm.FilterEq("is_default_ref", 1), 343 344 ) 344 345 if err != nil { 345 346 log.Printf("failed to get language stats from db: %v", err)
+17 -16
appview/repo/repo.go
··· 24 24 xrpcclient "tangled.org/core/appview/xrpcclient" 25 25 "tangled.org/core/eventconsumer" 26 26 "tangled.org/core/idresolver" 27 + "tangled.org/core/orm" 27 28 "tangled.org/core/rbac" 28 29 "tangled.org/core/tid" 29 30 "tangled.org/core/xrpc/serviceauth" ··· 345 346 // get form values 346 347 labelId := r.FormValue("label-id") 347 348 348 - label, err := db.GetLabelDefinition(rp.db, db.FilterEq("id", labelId)) 349 + label, err := db.GetLabelDefinition(rp.db, orm.FilterEq("id", labelId)) 349 350 if err != nil { 350 351 fail("Failed to find label definition.", err) 351 352 return ··· 409 410 410 411 err = db.UnsubscribeLabel( 411 412 tx, 412 - db.FilterEq("repo_at", f.RepoAt()), 413 - db.FilterEq("label_at", removedAt), 413 + orm.FilterEq("repo_at", f.RepoAt()), 414 + orm.FilterEq("label_at", removedAt), 414 415 ) 415 416 if err != nil { 416 417 fail("Failed to unsubscribe label.", err) 417 418 return 418 419 } 419 420 420 - err = db.DeleteLabelDefinition(tx, db.FilterEq("id", label.Id)) 421 + err = db.DeleteLabelDefinition(tx, orm.FilterEq("id", label.Id)) 421 422 if err != nil { 422 423 fail("Failed to delete label definition.", err) 423 424 return ··· 456 457 } 457 458 458 459 labelAts := r.Form["label"] 459 - _, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts)) 460 + _, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts)) 460 461 if err != nil { 461 462 fail("Failed to subscribe to label.", err) 462 463 return ··· 542 543 } 543 544 544 545 labelAts := r.Form["label"] 545 - _, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts)) 546 + _, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts)) 546 547 if err != nil { 547 548 fail("Failed to unsubscribe to label.", err) 548 549 return ··· 582 583 583 584 err = db.UnsubscribeLabel( 584 585 rp.db, 585 - db.FilterEq("repo_at", f.RepoAt()), 586 - db.FilterIn("label_at", labelAts), 586 + orm.FilterEq("repo_at", f.RepoAt()), 587 + orm.FilterIn("label_at", labelAts), 587 588 ) 588 589 if err != nil { 589 590 fail("Failed to unsubscribe label.", err) ··· 612 613 613 614 labelDefs, err := db.GetLabelDefinitions( 614 615 rp.db, 615 - db.FilterIn("at_uri", f.Labels), 616 - db.FilterContains("scope", subject.Collection().String()), 616 + orm.FilterIn("at_uri", f.Labels), 617 + orm.FilterContains("scope", subject.Collection().String()), 617 618 ) 618 619 if err != nil { 619 620 l.Error("failed to fetch label defs", "err", err) ··· 625 626 defs[l.AtUri().String()] = &l 626 627 } 627 628 628 - states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject)) 629 + states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject)) 629 630 if err != nil { 630 631 l.Error("failed to build label state", "err", err) 631 632 return ··· 660 661 661 662 labelDefs, err := db.GetLabelDefinitions( 662 663 rp.db, 663 - db.FilterIn("at_uri", f.Labels), 664 - db.FilterContains("scope", subject.Collection().String()), 664 + orm.FilterIn("at_uri", f.Labels), 665 + orm.FilterContains("scope", subject.Collection().String()), 665 666 ) 666 667 if err != nil { 667 668 l.Error("failed to fetch labels", "err", err) ··· 673 674 defs[l.AtUri().String()] = &l 674 675 } 675 676 676 - states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject)) 677 + states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject)) 677 678 if err != nil { 678 679 l.Error("failed to build label state", "err", err) 679 680 return ··· 1036 1037 // in the user's account. 1037 1038 existingRepo, err := db.GetRepo( 1038 1039 rp.db, 1039 - db.FilterEq("did", user.Did), 1040 - db.FilterEq("name", forkName), 1040 + orm.FilterEq("did", user.Did), 1041 + orm.FilterEq("name", forkName), 1041 1042 ) 1042 1043 if err != nil { 1043 1044 if !errors.Is(err, sql.ErrNoRows) {
+16 -17
appview/repo/repo_util.go
··· 1 1 package repo 2 2 3 3 import ( 4 + "maps" 4 5 "slices" 5 6 "sort" 6 7 "strings" 7 8 8 9 "tangled.org/core/appview/db" 9 10 "tangled.org/core/appview/models" 11 + "tangled.org/core/orm" 10 12 "tangled.org/core/types" 11 - 12 - "github.com/go-git/go-git/v5/plumbing/object" 13 13 ) 14 14 15 15 func sortFiles(files []types.NiceTree) { ··· 42 42 }) 43 43 } 44 44 45 - func uniqueEmails(commits []*object.Commit) []string { 45 + func uniqueEmails(commits []types.Commit) []string { 46 46 emails := make(map[string]struct{}) 47 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{}{} 48 + emails[commit.Author.Email] = struct{}{} 49 + emails[commit.Committer.Email] = struct{}{} 50 + for _, c := range commit.CoAuthors() { 51 + emails[c.Email] = struct{}{} 53 52 } 54 53 } 55 - var uniqueEmails []string 56 - for email := range emails { 57 - uniqueEmails = append(uniqueEmails, email) 58 - } 59 - return uniqueEmails 54 + 55 + // delete empty emails if any, from the set 56 + delete(emails, "") 57 + 58 + return slices.Collect(maps.Keys(emails)) 60 59 } 61 60 62 61 func balanceIndexItems(commitCount, branchCount, tagCount, fileCount int) (commitsTrunc int, branchesTrunc int, tagsTrunc int) { ··· 104 103 ps, err := db.GetPipelineStatuses( 105 104 d, 106 105 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), 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), 111 110 ) 112 111 if err != nil { 113 112 return nil, err
+3 -2
appview/repo/settings.go
··· 14 14 "tangled.org/core/appview/oauth" 15 15 "tangled.org/core/appview/pages" 16 16 xrpcclient "tangled.org/core/appview/xrpcclient" 17 + "tangled.org/core/orm" 17 18 "tangled.org/core/types" 18 19 19 20 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 210 211 return 211 212 } 212 213 213 - defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs)) 214 + defaultLabels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs)) 214 215 if err != nil { 215 216 l.Error("failed to fetch labels", "err", err) 216 217 rp.pages.Error503(w) 217 218 return 218 219 } 219 220 220 - labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Labels)) 221 + labels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", f.Labels)) 221 222 if err != nil { 222 223 l.Error("failed to fetch labels", "err", err) 223 224 rp.pages.Error503(w)
+2 -1
appview/repo/tags.go
··· 10 10 "tangled.org/core/appview/models" 11 11 "tangled.org/core/appview/pages" 12 12 xrpcclient "tangled.org/core/appview/xrpcclient" 13 + "tangled.org/core/orm" 13 14 "tangled.org/core/types" 14 15 15 16 indigoxrpc "github.com/bluesky-social/indigo/xrpc" ··· 44 45 rp.pages.Error503(w) 45 46 return 46 47 } 47 - artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) 48 + artifacts, err := db.GetArtifact(rp.db, orm.FilterEq("repo_at", f.RepoAt())) 48 49 if err != nil { 49 50 l.Error("failed grab artifacts", "err", err) 50 51 return
+5 -4
appview/serververify/verify.go
··· 9 9 "tangled.org/core/api/tangled" 10 10 "tangled.org/core/appview/db" 11 11 "tangled.org/core/appview/xrpcclient" 12 + "tangled.org/core/orm" 12 13 "tangled.org/core/rbac" 13 14 ) 14 15 ··· 76 77 // mark this spindle as verified in the db 77 78 rowId, err := db.VerifySpindle( 78 79 tx, 79 - db.FilterEq("owner", owner), 80 - db.FilterEq("instance", instance), 80 + orm.FilterEq("owner", owner), 81 + orm.FilterEq("instance", instance), 81 82 ) 82 83 if err != nil { 83 84 return 0, fmt.Errorf("failed to write to DB: %w", err) ··· 115 116 // mark as registered 116 117 err = db.MarkRegistered( 117 118 tx, 118 - db.FilterEq("did", owner), 119 - db.FilterEq("domain", domain), 119 + orm.FilterEq("did", owner), 120 + orm.FilterEq("domain", domain), 120 121 ) 121 122 if err != nil { 122 123 return fmt.Errorf("failed to register domain: %w", err)
+25 -24
appview/spindles/spindles.go
··· 20 20 "tangled.org/core/appview/serververify" 21 21 "tangled.org/core/appview/xrpcclient" 22 22 "tangled.org/core/idresolver" 23 + "tangled.org/core/orm" 23 24 "tangled.org/core/rbac" 24 25 "tangled.org/core/tid" 25 26 ··· 71 72 user := s.OAuth.GetUser(r) 72 73 all, err := db.GetSpindles( 73 74 s.Db, 74 - db.FilterEq("owner", user.Did), 75 + orm.FilterEq("owner", user.Did), 75 76 ) 76 77 if err != nil { 77 78 s.Logger.Error("failed to fetch spindles", "err", err) ··· 101 102 102 103 spindles, err := db.GetSpindles( 103 104 s.Db, 104 - db.FilterEq("instance", instance), 105 - db.FilterEq("owner", user.Did), 106 - db.FilterIsNot("verified", "null"), 105 + orm.FilterEq("instance", instance), 106 + orm.FilterEq("owner", user.Did), 107 + orm.FilterIsNot("verified", "null"), 107 108 ) 108 109 if err != nil || len(spindles) != 1 { 109 110 l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles)) ··· 123 124 repos, err := db.GetRepos( 124 125 s.Db, 125 126 0, 126 - db.FilterEq("spindle", instance), 127 + orm.FilterEq("spindle", instance), 127 128 ) 128 129 if err != nil { 129 130 l.Error("failed to get spindle repos", "err", err) ··· 290 291 291 292 spindles, err := db.GetSpindles( 292 293 s.Db, 293 - db.FilterEq("owner", user.Did), 294 - db.FilterEq("instance", instance), 294 + orm.FilterEq("owner", user.Did), 295 + orm.FilterEq("instance", instance), 295 296 ) 296 297 if err != nil || len(spindles) != 1 { 297 298 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) ··· 319 320 // remove spindle members first 320 321 err = db.RemoveSpindleMember( 321 322 tx, 322 - db.FilterEq("did", user.Did), 323 - db.FilterEq("instance", instance), 323 + orm.FilterEq("did", user.Did), 324 + orm.FilterEq("instance", instance), 324 325 ) 325 326 if err != nil { 326 327 l.Error("failed to remove spindle members", "err", err) ··· 330 331 331 332 err = db.DeleteSpindle( 332 333 tx, 333 - db.FilterEq("owner", user.Did), 334 - db.FilterEq("instance", instance), 334 + orm.FilterEq("owner", user.Did), 335 + orm.FilterEq("instance", instance), 335 336 ) 336 337 if err != nil { 337 338 l.Error("failed to delete spindle", "err", err) ··· 410 411 411 412 spindles, err := db.GetSpindles( 412 413 s.Db, 413 - db.FilterEq("owner", user.Did), 414 - db.FilterEq("instance", instance), 414 + orm.FilterEq("owner", user.Did), 415 + orm.FilterEq("instance", instance), 415 416 ) 416 417 if err != nil || len(spindles) != 1 { 417 418 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) ··· 453 454 454 455 verifiedSpindle, err := db.GetSpindles( 455 456 s.Db, 456 - db.FilterEq("id", rowId), 457 + orm.FilterEq("id", rowId), 457 458 ) 458 459 if err != nil || len(verifiedSpindle) != 1 { 459 460 l.Error("failed get new spindle", "err", err) ··· 486 487 487 488 spindles, err := db.GetSpindles( 488 489 s.Db, 489 - db.FilterEq("owner", user.Did), 490 - db.FilterEq("instance", instance), 490 + orm.FilterEq("owner", user.Did), 491 + orm.FilterEq("instance", instance), 491 492 ) 492 493 if err != nil || len(spindles) != 1 { 493 494 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) ··· 622 623 623 624 spindles, err := db.GetSpindles( 624 625 s.Db, 625 - db.FilterEq("owner", user.Did), 626 - db.FilterEq("instance", instance), 626 + orm.FilterEq("owner", user.Did), 627 + orm.FilterEq("instance", instance), 627 628 ) 628 629 if err != nil || len(spindles) != 1 { 629 630 l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) ··· 672 673 // get the record from the DB first: 673 674 members, err := db.GetSpindleMembers( 674 675 s.Db, 675 - db.FilterEq("did", user.Did), 676 - db.FilterEq("instance", instance), 677 - db.FilterEq("subject", memberId.DID), 676 + orm.FilterEq("did", user.Did), 677 + orm.FilterEq("instance", instance), 678 + orm.FilterEq("subject", memberId.DID), 678 679 ) 679 680 if err != nil || len(members) != 1 { 680 681 l.Error("failed to get member", "err", err) ··· 685 686 // remove from db 686 687 if err = db.RemoveSpindleMember( 687 688 tx, 688 - db.FilterEq("did", user.Did), 689 - db.FilterEq("instance", instance), 690 - db.FilterEq("subject", memberId.DID), 689 + orm.FilterEq("did", user.Did), 690 + orm.FilterEq("instance", instance), 691 + orm.FilterEq("subject", memberId.DID), 691 692 ); err != nil { 692 693 l.Error("failed to remove spindle member", "err", err) 693 694 fail()
+6 -5
appview/state/gfi.go
··· 11 11 "tangled.org/core/appview/pages" 12 12 "tangled.org/core/appview/pagination" 13 13 "tangled.org/core/consts" 14 + "tangled.org/core/orm" 14 15 ) 15 16 16 17 func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) { ··· 20 21 21 22 goodFirstIssueLabel := s.config.Label.GoodFirstIssue 22 23 23 - gfiLabelDef, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", goodFirstIssueLabel)) 24 + gfiLabelDef, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", goodFirstIssueLabel)) 24 25 if err != nil { 25 26 log.Println("failed to get gfi label def", err) 26 27 s.pages.Error500(w) 27 28 return 28 29 } 29 30 30 - repoLabels, err := db.GetRepoLabels(s.db, db.FilterEq("label_at", goodFirstIssueLabel)) 31 + repoLabels, err := db.GetRepoLabels(s.db, orm.FilterEq("label_at", goodFirstIssueLabel)) 31 32 if err != nil { 32 33 log.Println("failed to get repo labels", err) 33 34 s.pages.Error503(w) ··· 55 56 pagination.Page{ 56 57 Limit: 500, 57 58 }, 58 - db.FilterIn("repo_at", repoUris), 59 - db.FilterEq("open", 1), 59 + orm.FilterIn("repo_at", repoUris), 60 + orm.FilterEq("open", 1), 60 61 ) 61 62 if err != nil { 62 63 log.Println("failed to get issues", err) ··· 132 133 } 133 134 134 135 if len(uriList) > 0 { 135 - allLabelDefs, err = db.GetLabelDefinitions(s.db, db.FilterIn("at_uri", uriList)) 136 + allLabelDefs, err = db.GetLabelDefinitions(s.db, orm.FilterIn("at_uri", uriList)) 136 137 if err != nil { 137 138 log.Println("failed to fetch labels", err) 138 139 }
+17
appview/state/git_http.go
··· 25 25 26 26 } 27 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 + 28 45 func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 29 46 user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 47 if !ok {
+6 -5
appview/state/knotstream.go
··· 16 16 ec "tangled.org/core/eventconsumer" 17 17 "tangled.org/core/eventconsumer/cursor" 18 18 "tangled.org/core/log" 19 + "tangled.org/core/orm" 19 20 "tangled.org/core/rbac" 20 21 "tangled.org/core/workflow" 21 22 ··· 30 31 31 32 knots, err := db.GetRegistrations( 32 33 d, 33 - db.FilterIsNot("registered", "null"), 34 + orm.FilterIsNot("registered", "null"), 34 35 ) 35 36 if err != nil { 36 37 return nil, err ··· 143 144 repos, err := db.GetRepos( 144 145 d, 145 146 0, 146 - db.FilterEq("did", record.RepoDid), 147 - db.FilterEq("name", record.RepoName), 147 + orm.FilterEq("did", record.RepoDid), 148 + orm.FilterEq("name", record.RepoName), 148 149 ) 149 150 if err != nil { 150 151 return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err) ··· 209 210 repos, err := db.GetRepos( 210 211 d, 211 212 0, 212 - db.FilterEq("did", record.TriggerMetadata.Repo.Did), 213 - db.FilterEq("name", record.TriggerMetadata.Repo.Repo), 213 + orm.FilterEq("did", record.TriggerMetadata.Repo.Did), 214 + orm.FilterEq("name", record.TriggerMetadata.Repo.Repo), 214 215 ) 215 216 if err != nil { 216 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
··· 19 19 "tangled.org/core/appview/db" 20 20 "tangled.org/core/appview/models" 21 21 "tangled.org/core/appview/pages" 22 + "tangled.org/core/orm" 22 23 ) 23 24 24 25 func (s *State) Profile(w http.ResponseWriter, r *http.Request) { ··· 56 57 return nil, fmt.Errorf("failed to get profile: %w", err) 57 58 } 58 59 59 - repoCount, err := db.CountRepos(s.db, db.FilterEq("did", did)) 60 + repoCount, err := db.CountRepos(s.db, orm.FilterEq("did", did)) 60 61 if err != nil { 61 62 return nil, fmt.Errorf("failed to get repo count: %w", err) 62 63 } 63 64 64 - stringCount, err := db.CountStrings(s.db, db.FilterEq("did", did)) 65 + stringCount, err := db.CountStrings(s.db, orm.FilterEq("did", did)) 65 66 if err != nil { 66 67 return nil, fmt.Errorf("failed to get string count: %w", err) 67 68 } 68 69 69 - starredCount, err := db.CountStars(s.db, db.FilterEq("did", did)) 70 + starredCount, err := db.CountStars(s.db, orm.FilterEq("did", did)) 70 71 if err != nil { 71 72 return nil, fmt.Errorf("failed to get starred repo count: %w", err) 72 73 } ··· 86 87 startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC) 87 88 punchcard, err := db.MakePunchcard( 88 89 s.db, 89 - db.FilterEq("did", did), 90 - db.FilterGte("date", startOfYear.Format(time.DateOnly)), 91 - db.FilterLte("date", now.Format(time.DateOnly)), 90 + orm.FilterEq("did", did), 91 + orm.FilterGte("date", startOfYear.Format(time.DateOnly)), 92 + orm.FilterLte("date", now.Format(time.DateOnly)), 92 93 ) 93 94 if err != nil { 94 95 return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err) ··· 123 124 repos, err := db.GetRepos( 124 125 s.db, 125 126 0, 126 - db.FilterEq("did", profile.UserDid), 127 + orm.FilterEq("did", profile.UserDid), 127 128 ) 128 129 if err != nil { 129 130 l.Error("failed to fetch repos", "err", err) ··· 193 194 repos, err := db.GetRepos( 194 195 s.db, 195 196 0, 196 - db.FilterEq("did", profile.UserDid), 197 + orm.FilterEq("did", profile.UserDid), 197 198 ) 198 199 if err != nil { 199 200 l.Error("failed to get repos", "err", err) ··· 219 220 } 220 221 l = l.With("profileDid", profile.UserDid) 221 222 222 - stars, err := db.GetRepoStars(s.db, 0, db.FilterEq("did", profile.UserDid)) 223 + stars, err := db.GetRepoStars(s.db, 0, orm.FilterEq("did", profile.UserDid)) 223 224 if err != nil { 224 225 l.Error("failed to get stars", "err", err) 225 226 s.pages.Error500(w) ··· 248 249 } 249 250 l = l.With("profileDid", profile.UserDid) 250 251 251 - strings, err := db.GetStrings(s.db, 0, db.FilterEq("did", profile.UserDid)) 252 + strings, err := db.GetStrings(s.db, 0, orm.FilterEq("did", profile.UserDid)) 252 253 if err != nil { 253 254 l.Error("failed to get strings", "err", err) 254 255 s.pages.Error500(w) ··· 300 301 followDids = append(followDids, extractDid(follow)) 301 302 } 302 303 303 - profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids)) 304 + profiles, err := db.GetProfiles(s.db, orm.FilterIn("did", followDids)) 304 305 if err != nil { 305 306 l.Error("failed to get profiles", "followDids", followDids, "err", err) 306 307 return &params, err ··· 703 704 log.Printf("getting profile data for %s: %s", user.Did, err) 704 705 } 705 706 706 - repos, err := db.GetRepos(s.db, 0, db.FilterEq("did", user.Did)) 707 + repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did)) 707 708 if err != nil { 708 709 log.Printf("getting repos for %s: %s", user.Did, err) 709 710 }
+3 -3
appview/state/router.go
··· 36 36 router.Get("/favicon.ico", s.Favicon) 37 37 router.Get("/pwa-manifest.json", s.PWAManifest) 38 38 router.Get("/robots.txt", s.RobotsTxt) 39 - router.Get("/sitemap.xml", s.Sitemap) 40 39 41 40 userRouter := s.UserRouter(&middleware) 42 41 standardRouter := s.StandardRouter(&middleware) ··· 102 101 103 102 // These routes get proxied to the knot 104 103 r.Get("/info/refs", s.InfoRefs) 104 + r.Post("/git-upload-archive", s.UploadArchive) 105 105 r.Post("/git-upload-pack", s.UploadPack) 106 106 r.Post("/git-receive-pack", s.ReceivePack) 107 107 ··· 267 267 s.enforcer, 268 268 s.pages, 269 269 s.idResolver, 270 - s.refResolver, 270 + s.mentionsResolver, 271 271 s.db, 272 272 s.config, 273 273 s.notifier, ··· 284 284 s.repoResolver, 285 285 s.pages, 286 286 s.idResolver, 287 - s.refResolver, 287 + s.mentionsResolver, 288 288 s.db, 289 289 s.config, 290 290 s.notifier,
+2 -1
appview/state/spindlestream.go
··· 17 17 ec "tangled.org/core/eventconsumer" 18 18 "tangled.org/core/eventconsumer/cursor" 19 19 "tangled.org/core/log" 20 + "tangled.org/core/orm" 20 21 "tangled.org/core/rbac" 21 22 spindle "tangled.org/core/spindle/models" 22 23 ) ··· 27 28 28 29 spindles, err := db.GetSpindles( 29 30 d, 30 - db.FilterIsNot("verified", "null"), 31 + orm.FilterIsNot("verified", "null"), 31 32 ) 32 33 if err != nil { 33 34 return nil, err
+28 -80
appview/state/state.go
··· 15 15 "tangled.org/core/appview/config" 16 16 "tangled.org/core/appview/db" 17 17 "tangled.org/core/appview/indexer" 18 + "tangled.org/core/appview/mentions" 18 19 "tangled.org/core/appview/models" 19 20 "tangled.org/core/appview/notify" 20 21 dbnotify "tangled.org/core/appview/notify/db" 21 22 phnotify "tangled.org/core/appview/notify/posthog" 22 23 "tangled.org/core/appview/oauth" 23 24 "tangled.org/core/appview/pages" 24 - "tangled.org/core/appview/refresolver" 25 25 "tangled.org/core/appview/reporesolver" 26 26 "tangled.org/core/appview/validator" 27 27 xrpcclient "tangled.org/core/appview/xrpcclient" ··· 30 30 "tangled.org/core/jetstream" 31 31 "tangled.org/core/log" 32 32 tlog "tangled.org/core/log" 33 + "tangled.org/core/orm" 33 34 "tangled.org/core/rbac" 34 35 "tangled.org/core/tid" 35 36 ··· 43 44 ) 44 45 45 46 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 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 62 63 } 63 64 64 65 func Make(ctx context.Context, config *config.Config) (*State, error) { ··· 100 101 101 102 repoResolver := reporesolver.New(config, enforcer, d) 102 103 103 - refResolver := refresolver.New(config, res, d, log.SubLogger(logger, "refResolver")) 104 + mentionsResolver := mentions.New(config, res, d, log.SubLogger(logger, "mentionsResolver")) 104 105 105 106 wrapper := db.DbWrapper{Execer: d} 106 107 jc, err := jetstream.NewJetstreamClient( ··· 182 183 enforcer, 183 184 pages, 184 185 res, 185 - refResolver, 186 + mentionsResolver, 186 187 posthog, 187 188 jc, 188 189 config, ··· 220 221 221 222 robotsTxt := `User-agent: * 222 223 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 224 ` 236 225 w.Write([]byte(robotsTxt)) 237 226 } 238 227 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 228 // https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest 281 229 const manifestJson = `{ 282 230 "name": "tangled", ··· 352 300 return 353 301 } 354 302 355 - gfiLabel, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", s.config.Label.GoodFirstIssue)) 303 + gfiLabel, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", s.config.Label.GoodFirstIssue)) 356 304 if err != nil { 357 305 // non-fatal 358 306 } ··· 376 324 377 325 regs, err := db.GetRegistrations( 378 326 s.db, 379 - db.FilterEq("did", user.Did), 380 - db.FilterEq("needs_upgrade", 1), 327 + orm.FilterEq("did", user.Did), 328 + orm.FilterEq("needs_upgrade", 1), 381 329 ) 382 330 if err != nil { 383 331 l.Error("non-fatal: failed to get registrations", "err", err) ··· 385 333 386 334 spindles, err := db.GetSpindles( 387 335 s.db, 388 - db.FilterEq("owner", user.Did), 389 - db.FilterEq("needs_upgrade", 1), 336 + orm.FilterEq("owner", user.Did), 337 + orm.FilterEq("needs_upgrade", 1), 390 338 ) 391 339 if err != nil { 392 340 l.Error("non-fatal: failed to get spindles", "err", err) ··· 557 505 // Check for existing repos 558 506 existingRepo, err := db.GetRepo( 559 507 s.db, 560 - db.FilterEq("did", user.Did), 561 - db.FilterEq("name", repoName), 508 + orm.FilterEq("did", user.Did), 509 + orm.FilterEq("name", repoName), 562 510 ) 563 511 if err == nil && existingRepo != nil { 564 512 l.Info("repo exists") ··· 718 666 } 719 667 720 668 func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver, defaults []string) error { 721 - defaultLabels, err := db.GetLabelDefinitions(e, db.FilterIn("at_uri", defaults)) 669 + defaultLabels, err := db.GetLabelDefinitions(e, orm.FilterIn("at_uri", defaults)) 722 670 if err != nil { 723 671 return err 724 672 }
+7 -6
appview/strings/strings.go
··· 17 17 "tangled.org/core/appview/pages" 18 18 "tangled.org/core/appview/pages/markup" 19 19 "tangled.org/core/idresolver" 20 + "tangled.org/core/orm" 20 21 "tangled.org/core/tid" 21 22 22 23 "github.com/bluesky-social/indigo/api/atproto" ··· 108 109 strings, err := db.GetStrings( 109 110 s.Db, 110 111 0, 111 - db.FilterEq("did", id.DID), 112 - db.FilterEq("rkey", rkey), 112 + orm.FilterEq("did", id.DID), 113 + orm.FilterEq("rkey", rkey), 113 114 ) 114 115 if err != nil { 115 116 l.Error("failed to fetch string", "err", err) ··· 199 200 all, err := db.GetStrings( 200 201 s.Db, 201 202 0, 202 - db.FilterEq("did", id.DID), 203 - db.FilterEq("rkey", rkey), 203 + orm.FilterEq("did", id.DID), 204 + orm.FilterEq("rkey", rkey), 204 205 ) 205 206 if err != nil { 206 207 l.Error("failed to fetch string", "err", err) ··· 408 409 409 410 if err := db.DeleteString( 410 411 s.Db, 411 - db.FilterEq("did", user.Did), 412 - db.FilterEq("rkey", rkey), 412 + orm.FilterEq("did", user.Did), 413 + orm.FilterEq("rkey", rkey), 413 414 ); err != nil { 414 415 fail("Failed to delete string.", err) 415 416 return
+2 -1
appview/validator/issue.go
··· 6 6 7 7 "tangled.org/core/appview/db" 8 8 "tangled.org/core/appview/models" 9 + "tangled.org/core/orm" 9 10 ) 10 11 11 12 func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error { 12 13 // if comments have parents, only ingest ones that are 1 level deep 13 14 if comment.ReplyTo != nil { 14 - parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo)) 15 + parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo)) 15 16 if err != nil { 16 17 return fmt.Errorf("failed to fetch parent comment: %w", err) 17 18 }
+1 -34
crypto/verify.go
··· 5 5 "crypto/sha256" 6 6 "encoding/base64" 7 7 "fmt" 8 - "strings" 9 8 10 9 "github.com/hiddeco/sshsig" 11 10 "golang.org/x/crypto/ssh" 12 - "tangled.org/core/types" 13 11 ) 14 12 15 13 func VerifySignature(pubKey, signature, payload []byte) (error, bool) { ··· 28 26 // multiple algorithms but sha-512 is most secure, and git's ssh signing defaults 29 27 // to sha-512 for all key types anyway. 30 28 err = sshsig.Verify(buf, sig, pub, sshsig.HashSHA512, "git") 31 - return err, err == nil 32 - } 33 29 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())) 30 + return err, err == nil 64 31 } 65 32 66 33 // SSHFingerprint computes the fingerprint of the supplied ssh pubkey.
+3 -3
flake.lock
··· 150 150 }, 151 151 "nixpkgs": { 152 152 "locked": { 153 - "lastModified": 1751984180, 154 - "narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=", 153 + "lastModified": 1765186076, 154 + "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", 155 155 "owner": "nixos", 156 156 "repo": "nixpkgs", 157 - "rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0", 157 + "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", 158 158 "type": "github" 159 159 }, 160 160 "original": {
-2
flake.nix
··· 80 80 }).buildGoApplication; 81 81 modules = ./nix/gomod2nix.toml; 82 82 sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix { 83 - inherit (pkgs) gcc; 84 83 inherit sqlite-lib-src; 85 84 }; 86 85 lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;}; ··· 156 155 nativeBuildInputs = [ 157 156 pkgs.go 158 157 pkgs.air 159 - pkgs.tilt 160 158 pkgs.gopls 161 159 pkgs.httpie 162 160 pkgs.litecli
+1 -3
go.mod
··· 1 1 module tangled.org/core 2 2 3 - go 1.24.4 3 + go 1.25.0 4 4 5 5 require ( 6 6 github.com/Blank-Xu/sql-adapter v1.1.1 ··· 44 44 github.com/stretchr/testify v1.10.0 45 45 github.com/urfave/cli/v3 v3.3.3 46 46 github.com/whyrusleeping/cbor-gen v0.3.1 47 - github.com/wyatt915/goldmark-treeblood v0.0.1 48 47 github.com/yuin/goldmark v1.7.13 49 48 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 50 49 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab ··· 190 189 github.com/vmihailenco/go-tinylfu v0.2.2 // indirect 191 190 github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 192 191 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 193 - github.com/wyatt915/treeblood v0.1.16 // indirect 194 192 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 195 193 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 196 194 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
-4
go.sum
··· 495 495 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 496 496 github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= 497 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 498 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 503 499 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 504 500 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+4 -4
hook/hook.go
··· 48 48 }, 49 49 Commands: []*cli.Command{ 50 50 { 51 - Name: "post-recieve", 52 - Usage: "sends a post-recieve hook to the knot (waits for stdin)", 53 - Action: postRecieve, 51 + Name: "post-receive", 52 + Usage: "sends a post-receive hook to the knot (waits for stdin)", 53 + Action: postReceive, 54 54 }, 55 55 }, 56 56 } 57 57 } 58 58 59 - func postRecieve(ctx context.Context, cmd *cli.Command) error { 59 + func postReceive(ctx context.Context, cmd *cli.Command) error { 60 60 gitDir := cmd.String("git-dir") 61 61 userDid := cmd.String("user-did") 62 62 userHandle := cmd.String("user-handle")
+1 -1
hook/setup.go
··· 138 138 option_var="GIT_PUSH_OPTION_$i" 139 139 push_options+=(-push-option "${!option_var}") 140 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 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 142 `, executablePath, config.internalApi) 143 143 144 144 return os.WriteFile(hookPath, []byte(hookContent), 0755)
+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
··· 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
··· 77 77 nd.Diff = append(nd.Diff, ndiff) 78 78 } 79 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 - } 80 + nd.Commit.FromGoGitCommit(c) 97 81 98 82 return &nd, nil 99 83 }
+13 -1
knotserver/git/service/service.go
··· 95 95 return c.RunService(cmd) 96 96 } 97 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 + 98 111 func (c *ServiceCommand) UploadPack() error { 99 112 cmd := exec.Command("git", []string{ 100 - "-c", "uploadpack.allowFilter=true", 101 113 "upload-pack", 102 114 "--stateless-rpc", 103 115 ".",
+47
knotserver/git.go
··· 56 56 } 57 57 } 58 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 + 59 106 func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { 60 107 did := chi.URLParam(r, "did") 61 108 name := chi.URLParam(r, "name")
+1
knotserver/router.go
··· 82 82 r.Route("/{name}", func(r chi.Router) { 83 83 // routes for git operations 84 84 r.Get("/info/refs", h.InfoRefs) 85 + r.Post("/git-upload-archive", h.UploadArchive) 85 86 r.Post("/git-upload-pack", h.UploadPack) 86 87 r.Post("/git-receive-pack", h.ReceivePack) 87 88 })
+1 -1
knotserver/server.go
··· 64 64 logger.Info("running in dev mode, signature verification is disabled") 65 65 } 66 66 67 - db, err := db.Setup(c.Server.DBPath) 67 + db, err := db.Setup(ctx, c.Server.DBPath) 68 68 if err != nil { 69 69 return fmt.Errorf("failed to load db: %w", err) 70 70 }
+6 -1
knotserver/xrpc/repo_log.go
··· 62 62 return 63 63 } 64 64 65 + tcommits := make([]types.Commit, len(commits)) 66 + for i, c := range commits { 67 + tcommits[i].FromGoGitCommit(c) 68 + } 69 + 65 70 // Create response using existing types.RepoLogResponse 66 71 response := types.RepoLogResponse{ 67 - Commits: commits, 72 + Commits: tcommits, 68 73 Ref: ref, 69 74 Page: (offset / limit) + 1, 70 75 PerPage: limit,
-30
nix/gomod2nix.toml
··· 165 165 [mod."github.com/davecgh/go-spew"] 166 166 version = "v1.1.2-0.20180830191138-d8f796af33cc" 167 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 168 [mod."github.com/dgraph-io/ristretto"] 172 169 version = "v0.2.0" 173 170 hash = "sha256-bnpxX+oO/Qf7IJevA0gsbloVoqRx+5bh7RQ9d9eLNYw=" ··· 373 370 [mod."github.com/klauspost/cpuid/v2"] 374 371 version = "v2.3.0" 375 372 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 373 [mod."github.com/lucasb-eyer/go-colorful"] 395 374 version = "v1.2.0" 396 375 hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE=" ··· 511 490 [mod."github.com/ryanuber/go-glob"] 512 491 version = "v1.0.0" 513 492 hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY=" 514 - [mod."github.com/segmentio/asm"] 515 - version = "v1.2.0" 516 - hash = "sha256-zbNuKxNrUDUc6IlmRQNuJQzVe5Ol/mqp7srDg9IMMqs=" 517 493 [mod."github.com/sergi/go-diff"] 518 494 version = "v1.1.0" 519 495 hash = "sha256-8NJMabldpf40uwQN20T6QXx5KORDibCBJL02KD661xY=" ··· 548 524 [mod."github.com/whyrusleeping/cbor-gen"] 549 525 version = "v0.3.1" 550 526 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 527 [mod."github.com/xo/terminfo"] 558 528 version = "v0.0.0-20220910002029-abceb7e1c41e" 559 529 hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU="
+7 -5
nix/pkgs/sqlite-lib.nix
··· 1 1 { 2 - gcc, 3 2 stdenv, 4 3 sqlite-lib-src, 5 4 }: 6 5 stdenv.mkDerivation { 7 6 name = "sqlite-lib"; 8 7 src = sqlite-lib-src; 9 - nativeBuildInputs = [gcc]; 8 + 10 9 buildPhase = '' 11 - gcc -c sqlite3.c 12 - ar rcs libsqlite3.a sqlite3.o 13 - ranlib libsqlite3.a 10 + $CC -c sqlite3.c 11 + $AR rcs libsqlite3.a sqlite3.o 12 + $RANLIB libsqlite3.a 13 + ''; 14 + 15 + installPhase = '' 14 16 mkdir -p $out/include $out/lib 15 17 cp *.h $out/include 16 18 cp libsqlite3.a $out/lib
+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
··· 296 296 } 297 297 298 298 nd := types.NiceDiff{} 299 - nd.Commit.Parent = targetBranch 300 299 301 300 for _, d := range diffs { 302 301 ndiff := types.Diff{}
+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
··· 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
··· 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
··· 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
··· 16 16 if err != nil { 17 17 return nil, err 18 18 } 19 + defer rows.Close() 19 20 20 21 var knots []string 21 22 for rows.Next() {
+17 -20
spindle/engine/engine.go
··· 3 3 import ( 4 4 "context" 5 5 "errors" 6 - "fmt" 7 6 "log/slog" 7 + "sync" 8 8 9 9 securejoin "github.com/cyphar/filepath-securejoin" 10 - "golang.org/x/sync/errgroup" 11 10 "tangled.org/core/notifier" 12 11 "tangled.org/core/spindle/config" 13 12 "tangled.org/core/spindle/db" ··· 31 30 } 32 31 } 33 32 34 - eg, ctx := errgroup.WithContext(ctx) 33 + var wg sync.WaitGroup 35 34 for eng, wfs := range pipeline.Workflows { 36 35 workflowTimeout := eng.WorkflowTimeout() 37 36 l.Info("using workflow timeout", "timeout", workflowTimeout) 38 37 39 38 for _, w := range wfs { 40 - eg.Go(func() error { 39 + wg.Add(1) 40 + go func() { 41 + defer wg.Done() 42 + 41 43 wid := models.WorkflowId{ 42 44 PipelineId: pipelineId, 43 45 Name: w.Name, ··· 45 47 46 48 err := db.StatusRunning(wid, n) 47 49 if err != nil { 48 - return err 50 + l.Error("failed to set workflow status to running", "wid", wid, "err", err) 51 + return 49 52 } 50 53 51 54 err = eng.SetupWorkflow(ctx, wid, &w) ··· 61 64 62 65 dbErr := db.StatusFailed(wid, err.Error(), -1, n) 63 66 if dbErr != nil { 64 - return dbErr 67 + l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr) 65 68 } 66 - return err 69 + return 67 70 } 68 71 defer eng.DestroyWorkflow(ctx, wid) 69 72 ··· 99 102 if errors.Is(err, ErrTimedOut) { 100 103 dbErr := db.StatusTimeout(wid, n) 101 104 if dbErr != nil { 102 - return dbErr 105 + l.Error("failed to set workflow status to timeout", "wid", wid, "err", dbErr) 103 106 } 104 107 } else { 105 108 dbErr := db.StatusFailed(wid, err.Error(), -1, n) 106 109 if dbErr != nil { 107 - return dbErr 110 + l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr) 108 111 } 109 112 } 110 - 111 - return fmt.Errorf("starting steps image: %w", err) 113 + return 112 114 } 113 115 } 114 116 115 117 err = db.StatusSuccess(wid, n) 116 118 if err != nil { 117 - return err 119 + l.Error("failed to set workflow status to success", "wid", wid, "err", err) 118 120 } 119 - 120 - return nil 121 - }) 121 + }() 122 122 } 123 123 } 124 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 - } 125 + wg.Wait() 126 + l.Info("all workflows completed") 130 127 }
+5 -3
spindle/engines/nixery/engine.go
··· 294 294 workflowEnvs.AddEnv(s.Key, s.Value) 295 295 } 296 296 297 - step := w.Steps[idx].(Step) 297 + step := w.Steps[idx] 298 298 299 299 select { 300 300 case <-ctx.Done(): ··· 303 303 } 304 304 305 305 envs := append(EnvVars(nil), workflowEnvs...) 306 - for k, v := range step.environment { 307 - envs.AddEnv(k, v) 306 + if nixStep, ok := step.(Step); ok { 307 + for k, v := range nixStep.environment { 308 + envs.AddEnv(k, v) 309 + } 308 310 } 309 311 envs.AddEnv("HOME", homeDir) 310 312
+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 2 3 3 import ( 4 4 "github.com/bluekeyes/go-gitdiff/gitdiff" 5 - "github.com/go-git/go-git/v5/plumbing/object" 6 5 ) 7 6 8 7 type DiffOpts struct { ··· 43 42 44 43 // A nicer git diff representation. 45 44 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 { 45 + Commit Commit `json:"commit"` 46 + Stat struct { 57 47 FilesChanged int `json:"files_changed"` 58 48 Insertions int `json:"insertions"` 59 49 Deletions int `json:"deletions"`
+17 -17
types/repo.go
··· 8 8 ) 9 9 10 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"` 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 21 } 22 22 23 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"` 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 31 } 32 32 33 33 type RepoCommitResponse struct {