···7 "time"
89 "tangled.org/core/appview/models"
010)
1112-func GetPipelines(e Execer, filters ...filter) ([]models.Pipeline, error) {
13 var pipelines []models.Pipeline
1415 var conditions []string
···168169// this is a mega query, but the most useful one:
170// get N pipelines, for each one get the latest status of its N workflows
171-func GetPipelineStatuses(e Execer, limit int, filters ...filter) ([]models.Pipeline, error) {
172 var conditions []string
173 var args []any
174 for _, filter := range filters {
175- filter.key = "p." + filter.key // the table is aliased in the query to `p`
176 conditions = append(conditions, filter.Condition())
177 args = append(args, filter.Arg()...)
178 }
···264 conditions = nil
265 args = nil
266 for _, p := range pipelines {
267- knotFilter := FilterEq("pipeline_knot", p.Knot)
268- rkeyFilter := FilterEq("pipeline_rkey", p.Rkey)
269 conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
270 args = append(args, p.Knot)
271 args = append(args, p.Rkey)
···7 "time"
89 "tangled.org/core/appview/models"
10+ "tangled.org/core/orm"
11)
1213+func GetPipelines(e Execer, filters ...orm.Filter) ([]models.Pipeline, error) {
14 var pipelines []models.Pipeline
1516 var conditions []string
···169170// this is a mega query, but the most useful one:
171// get N pipelines, for each one get the latest status of its N workflows
172+func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.Pipeline, error) {
173 var conditions []string
174 var args []any
175 for _, filter := range filters {
176+ filter.Key = "p." + filter.Key // the table is aliased in the query to `p`
177 conditions = append(conditions, filter.Condition())
178 args = append(args, filter.Arg()...)
179 }
···265 conditions = nil
266 args = nil
267 for _, p := range pipelines {
268+ knotFilter := orm.FilterEq("pipeline_knot", p.Knot)
269+ rkeyFilter := orm.FilterEq("pipeline_rkey", p.Rkey)
270 conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
271 args = append(args, p.Knot)
272 args = append(args, p.Rkey)
+11-5
appview/db/profile.go
···1112 "github.com/bluesky-social/indigo/atproto/syntax"
13 "tangled.org/core/appview/models"
014)
1516const TimeframeMonths = 7
···4445 issues, err := GetIssues(
46 e,
47- FilterEq("did", forDid),
48- FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
49 )
50 if err != nil {
51 return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···65 *items = append(*items, &issue)
66 }
6768- repos, err := GetRepos(e, 0, FilterEq("did", forDid))
69 if err != nil {
70 return nil, fmt.Errorf("error getting all repos by did: %w", err)
71 }
···199 return tx.Commit()
200}
201202-func GetProfiles(e Execer, filters ...filter) (map[string]*models.Profile, error) {
203 var conditions []string
204 var args []any
205 for _, filter := range filters {
···229 if err != nil {
230 return nil, err
231 }
0232233 profileMap := make(map[string]*models.Profile)
234 for rows.Next() {
···269 if err != nil {
270 return nil, err
271 }
00272 idxs := make(map[string]int)
273 for did := range profileMap {
274 idxs[did] = 0
···289 if err != nil {
290 return nil, err
291 }
00292 idxs = make(map[string]int)
293 for did := range profileMap {
294 idxs[did] = 0
···441 }
442443 // ensure all pinned repos are either own repos or collaborating repos
444- repos, err := GetRepos(e, 0, FilterEq("did", profile.Did))
445 if err != nil {
446 log.Printf("getting repos for %s: %s", profile.Did, err)
447 }
···1112 "github.com/bluesky-social/indigo/atproto/syntax"
13 "tangled.org/core/appview/models"
14+ "tangled.org/core/orm"
15)
1617const TimeframeMonths = 7
···4546 issues, err := GetIssues(
47 e,
48+ orm.FilterEq("did", forDid),
49+ orm.FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
50 )
51 if err != nil {
52 return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···66 *items = append(*items, &issue)
67 }
6869+ repos, err := GetRepos(e, 0, orm.FilterEq("did", forDid))
70 if err != nil {
71 return nil, fmt.Errorf("error getting all repos by did: %w", err)
72 }
···200 return tx.Commit()
201}
202203+func GetProfiles(e Execer, filters ...orm.Filter) (map[string]*models.Profile, error) {
204 var conditions []string
205 var args []any
206 for _, filter := range filters {
···230 if err != nil {
231 return nil, err
232 }
233+ defer rows.Close()
234235 profileMap := make(map[string]*models.Profile)
236 for rows.Next() {
···271 if err != nil {
272 return nil, err
273 }
274+ defer rows.Close()
275+276 idxs := make(map[string]int)
277 for did := range profileMap {
278 idxs[did] = 0
···293 if err != nil {
294 return nil, err
295 }
296+ defer rows.Close()
297+298 idxs = make(map[string]int)
299 for did := range profileMap {
300 idxs[did] = 0
···447 }
448449 // ensure all pinned repos are either own repos or collaborating repos
450+ repos, err := GetRepos(e, 0, orm.FilterEq("did", profile.Did))
451 if err != nil {
452 log.Printf("getting repos for %s: %s", profile.Did, err)
453 }
+21-20
appview/db/pulls.go
···1314 "github.com/bluesky-social/indigo/atproto/syntax"
15 "tangled.org/core/appview/models"
016)
1718func NewPull(tx *sql.Tx, pull *models.Pull) error {
···118 return pullId - 1, err
119}
120121-func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*models.Pull, error) {
122 pulls := make(map[syntax.ATURI]*models.Pull)
123124 var conditions []string
···229 for _, p := range pulls {
230 pullAts = append(pullAts, p.AtUri())
231 }
232- submissionsMap, err := GetPullSubmissions(e, FilterIn("pull_at", pullAts))
233 if err != nil {
234 return nil, fmt.Errorf("failed to get submissions: %w", err)
235 }
···241 }
242243 // collect allLabels for each issue
244- allLabels, err := GetLabels(e, FilterIn("subject", pullAts))
245 if err != nil {
246 return nil, fmt.Errorf("failed to query labels: %w", err)
247 }
···258 sourceAts = append(sourceAts, *p.PullSource.RepoAt)
259 }
260 }
261- sourceRepos, err := GetRepos(e, 0, FilterIn("at_uri", sourceAts))
262 if err != nil && !errors.Is(err, sql.ErrNoRows) {
263 return nil, fmt.Errorf("failed to get source repos: %w", err)
264 }
···274 }
275 }
276277- allReferences, err := GetReferencesAll(e, FilterIn("from_at", pullAts))
278 if err != nil {
279 return nil, fmt.Errorf("failed to query reference_links: %w", err)
280 }
···295 return orderedByPullId, nil
296}
297298-func GetPulls(e Execer, filters ...filter) ([]*models.Pull, error) {
299 return GetPullsWithLimit(e, 0, filters...)
300}
301302func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
303 var ids []int64
304305- var filters []filter
306- filters = append(filters, FilterEq("state", opts.State))
307 if opts.RepoAt != "" {
308- filters = append(filters, FilterEq("repo_at", opts.RepoAt))
309 }
310311 var conditions []string
···361}
362363func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
364- pulls, err := GetPullsWithLimit(e, 1, FilterEq("repo_at", repoAt), FilterEq("pull_id", pullId))
365 if err != nil {
366 return nil, err
367 }
···373}
374375// mapping from pull -> pull submissions
376-func GetPullSubmissions(e Execer, filters ...filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
377 var conditions []string
378 var args []any
379 for _, filter := range filters {
···448449 // Get comments for all submissions using GetPullComments
450 submissionIds := slices.Collect(maps.Keys(submissionMap))
451- comments, err := GetPullComments(e, FilterIn("submission_id", submissionIds))
452 if err != nil {
453 return nil, fmt.Errorf("failed to get pull comments: %w", err)
454 }
···474 return m, nil
475}
476477-func GetPullComments(e Execer, filters ...filter) ([]models.PullComment, error) {
478 var conditions []string
479 var args []any
480 for _, filter := range filters {
···542543 // collect references for each comments
544 commentAts := slices.Collect(maps.Keys(commentMap))
545- allReferencs, err := GetReferencesAll(e, FilterIn("from_at", commentAts))
546 if err != nil {
547 return nil, fmt.Errorf("failed to query reference_links: %w", err)
548 }
···708 return err
709}
710711-func SetPullParentChangeId(e Execer, parentChangeId string, filters ...filter) error {
712 var conditions []string
713 var args []any
714···732733// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
734// otherwise submissions are immutable
735-func UpdatePull(e Execer, newPatch, sourceRev string, filters ...filter) error {
736 var conditions []string
737 var args []any
738···790func GetStack(e Execer, stackId string) (models.Stack, error) {
791 unorderedPulls, err := GetPulls(
792 e,
793- FilterEq("stack_id", stackId),
794- FilterNotEq("state", models.PullDeleted),
795 )
796 if err != nil {
797 return nil, err
···835func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
836 pulls, err := GetPulls(
837 e,
838- FilterEq("stack_id", stackId),
839- FilterEq("state", models.PullDeleted),
840 )
841 if err != nil {
842 return nil, err
···1314 "github.com/bluesky-social/indigo/atproto/syntax"
15 "tangled.org/core/appview/models"
16+ "tangled.org/core/orm"
17)
1819func NewPull(tx *sql.Tx, pull *models.Pull) error {
···119 return pullId - 1, err
120}
121122+func GetPullsWithLimit(e Execer, limit int, filters ...orm.Filter) ([]*models.Pull, error) {
123 pulls := make(map[syntax.ATURI]*models.Pull)
124125 var conditions []string
···230 for _, p := range pulls {
231 pullAts = append(pullAts, p.AtUri())
232 }
233+ submissionsMap, err := GetPullSubmissions(e, orm.FilterIn("pull_at", pullAts))
234 if err != nil {
235 return nil, fmt.Errorf("failed to get submissions: %w", err)
236 }
···242 }
243244 // collect allLabels for each issue
245+ allLabels, err := GetLabels(e, orm.FilterIn("subject", pullAts))
246 if err != nil {
247 return nil, fmt.Errorf("failed to query labels: %w", err)
248 }
···259 sourceAts = append(sourceAts, *p.PullSource.RepoAt)
260 }
261 }
262+ sourceRepos, err := GetRepos(e, 0, orm.FilterIn("at_uri", sourceAts))
263 if err != nil && !errors.Is(err, sql.ErrNoRows) {
264 return nil, fmt.Errorf("failed to get source repos: %w", err)
265 }
···275 }
276 }
277278+ allReferences, err := GetReferencesAll(e, orm.FilterIn("from_at", pullAts))
279 if err != nil {
280 return nil, fmt.Errorf("failed to query reference_links: %w", err)
281 }
···296 return orderedByPullId, nil
297}
298299+func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) {
300 return GetPullsWithLimit(e, 0, filters...)
301}
302303func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
304 var ids []int64
305306+ var filters []orm.Filter
307+ filters = append(filters, orm.FilterEq("state", opts.State))
308 if opts.RepoAt != "" {
309+ filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
310 }
311312 var conditions []string
···362}
363364func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
365+ pulls, err := GetPullsWithLimit(e, 1, orm.FilterEq("repo_at", repoAt), orm.FilterEq("pull_id", pullId))
366 if err != nil {
367 return nil, err
368 }
···374}
375376// mapping from pull -> pull submissions
377+func GetPullSubmissions(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
378 var conditions []string
379 var args []any
380 for _, filter := range filters {
···449450 // Get comments for all submissions using GetPullComments
451 submissionIds := slices.Collect(maps.Keys(submissionMap))
452+ comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds))
453 if err != nil {
454 return nil, fmt.Errorf("failed to get pull comments: %w", err)
455 }
···475 return m, nil
476}
477478+func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) {
479 var conditions []string
480 var args []any
481 for _, filter := range filters {
···543544 // collect references for each comments
545 commentAts := slices.Collect(maps.Keys(commentMap))
546+ allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
547 if err != nil {
548 return nil, fmt.Errorf("failed to query reference_links: %w", err)
549 }
···709 return err
710}
711712+func SetPullParentChangeId(e Execer, parentChangeId string, filters ...orm.Filter) error {
713 var conditions []string
714 var args []any
715···733734// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
735// otherwise submissions are immutable
736+func UpdatePull(e Execer, newPatch, sourceRev string, filters ...orm.Filter) error {
737 var conditions []string
738 var args []any
739···791func GetStack(e Execer, stackId string) (models.Stack, error) {
792 unorderedPulls, err := GetPulls(
793 e,
794+ orm.FilterEq("stack_id", stackId),
795+ orm.FilterNotEq("state", models.PullDeleted),
796 )
797 if err != nil {
798 return nil, err
···836func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
837 pulls, err := GetPulls(
838 e,
839+ orm.FilterEq("stack_id", stackId),
840+ orm.FilterEq("state", models.PullDeleted),
841 )
842 if err != nil {
843 return nil, err
···1{{ define "title" }}good first issues{{ end }}
23{{ define "extrameta" }}
4- <meta name="description" content="Discover beginner-friendly good first issues across open source projects on Tangled. Perfect for new contributors looking to get started with open source development." />
5- <meta name="keywords" content="good first issues, beginner issues, open source contribution, first time contributor, beginner friendly, open source projects" />
6-7 <meta property="og:title" content="good first issues ยท tangled" />
8- <meta property="og:type" content="website" />
9 <meta property="og:url" content="https://tangled.org/goodfirstissues" />
10- <meta property="og:description" content="Find beginner-friendly issues across all repositories to get started with open source contributions on Tangled." />
11-12- <meta name="twitter:card" content="summary" />
13- <meta name="twitter:title" content="good first issues ยท tangled" />
14- <meta name="twitter:description" content="Find beginner-friendly issues to get started with open source contributions." />
15-16- <!-- structured data for good first issues page -->
17- <script type="application/ld+json">
18- {
19- "@context": "https://schema.org",
20- "@type": "CollectionPage",
21- "name": "Good First Issues",
22- "description": "A curated collection of beginner-friendly issues across open source projects",
23- "url": "https://tangled.org/goodfirstissues",
24- "isPartOf": {
25- "@type": "WebSite",
26- "name": "Tangled",
27- "url": "https://tangled.org"
28- }
29- }
30- </script>
31{{ end }}
32-33-{{ define "canonical" }}https://tangled.org/goodfirstissues{{ end }}
3435{{ define "content" }}
36<div class="grid grid-cols-10">
···1{{ define "title" }}good first issues{{ end }}
23{{ define "extrameta" }}
0004 <meta property="og:title" content="good first issues ยท tangled" />
5+ <meta property="og:type" content="object" />
6 <meta property="og:url" content="https://tangled.org/goodfirstissues" />
7+ <meta property="og:description" content="Find good first issues to contribute to open source projects" />
000000000000000000008{{ end }}
00910{{ define "content" }}
11<div class="grid grid-cols-10">
···11 "tangled.org/core/appview/pages"
12 "tangled.org/core/appview/pagination"
13 "tangled.org/core/consts"
014)
1516func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
···2021 goodFirstIssueLabel := s.config.Label.GoodFirstIssue
2223- gfiLabelDef, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", goodFirstIssueLabel))
24 if err != nil {
25 log.Println("failed to get gfi label def", err)
26 s.pages.Error500(w)
27 return
28 }
2930- repoLabels, err := db.GetRepoLabels(s.db, db.FilterEq("label_at", goodFirstIssueLabel))
31 if err != nil {
32 log.Println("failed to get repo labels", err)
33 s.pages.Error503(w)
···55 pagination.Page{
56 Limit: 500,
57 },
58- db.FilterIn("repo_at", repoUris),
59- db.FilterEq("open", 1),
60 )
61 if err != nil {
62 log.Println("failed to get issues", err)
···132 }
133134 if len(uriList) > 0 {
135- allLabelDefs, err = db.GetLabelDefinitions(s.db, db.FilterIn("at_uri", uriList))
136 if err != nil {
137 log.Println("failed to fetch labels", err)
138 }
···11 "tangled.org/core/appview/pages"
12 "tangled.org/core/appview/pagination"
13 "tangled.org/core/consts"
14+ "tangled.org/core/orm"
15)
1617func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
···2122 goodFirstIssueLabel := s.config.Label.GoodFirstIssue
2324+ gfiLabelDef, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", goodFirstIssueLabel))
25 if err != nil {
26 log.Println("failed to get gfi label def", err)
27 s.pages.Error500(w)
28 return
29 }
3031+ repoLabels, err := db.GetRepoLabels(s.db, orm.FilterEq("label_at", goodFirstIssueLabel))
32 if err != nil {
33 log.Println("failed to get repo labels", err)
34 s.pages.Error503(w)
···56 pagination.Page{
57 Limit: 500,
58 },
59+ orm.FilterIn("repo_at", repoUris),
60+ orm.FilterEq("open", 1),
61 )
62 if err != nil {
63 log.Println("failed to get issues", err)
···133 }
134135 if len(uriList) > 0 {
136+ allLabelDefs, err = db.GetLabelDefinitions(s.db, orm.FilterIn("at_uri", uriList))
137 if err != nil {
138 log.Println("failed to fetch labels", err)
139 }
+17
appview/state/git_http.go
···2526}
270000000000000000028func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
29 user, ok := r.Context().Value("resolvedId").(identity.Identity)
30 if !ok {
···2526}
2728+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+45func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
46 user, ok := r.Context().Value("resolvedId").(identity.Identity)
47 if !ok {
···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-}