forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
fork
Configure Feed
Select the types of activity you want to include in your feed.
1package db
2
3import (
4 "database/sql"
5 "fmt"
6 "log"
7 "strings"
8 "time"
9
10 "github.com/bluekeyes/go-gitdiff/gitdiff"
11 "github.com/bluesky-social/indigo/atproto/syntax"
12 "tangled.sh/tangled.sh/core/types"
13)
14
15type PullState int
16
17const (
18 PullClosed PullState = iota
19 PullOpen
20 PullMerged
21)
22
23func (p PullState) String() string {
24 switch p {
25 case PullOpen:
26 return "open"
27 case PullMerged:
28 return "merged"
29 case PullClosed:
30 return "closed"
31 default:
32 return "closed"
33 }
34}
35
36func (p PullState) IsOpen() bool {
37 return p == PullOpen
38}
39func (p PullState) IsMerged() bool {
40 return p == PullMerged
41}
42func (p PullState) IsClosed() bool {
43 return p == PullClosed
44}
45
46type Pull struct {
47 // ids
48 ID int
49 PullId int
50
51 // at ids
52 RepoAt syntax.ATURI
53 OwnerDid string
54 Rkey string
55 PullAt syntax.ATURI
56
57 // content
58 Title string
59 Body string
60 TargetBranch string
61 State PullState
62 Submissions []*PullSubmission
63
64 // meta
65 Created time.Time
66 PullSource *PullSource
67
68 // optionally, populate this when querying for reverse mappings
69 Repo *Repo
70}
71
72type PullSource struct {
73 Branch string
74 RepoAt *syntax.ATURI
75
76 // optionally populate this for reverse mappings
77 Repo *Repo
78}
79
80type PullSubmission struct {
81 // ids
82 ID int
83 PullId int
84
85 // at ids
86 RepoAt syntax.ATURI
87
88 // content
89 RoundNumber int
90 Patch string
91 Comments []PullComment
92 SourceRev string // include the rev that was used to create this submission: only for branch PRs
93
94 // meta
95 Created time.Time
96}
97
98type PullComment struct {
99 // ids
100 ID int
101 PullId int
102 SubmissionId int
103
104 // at ids
105 RepoAt string
106 OwnerDid string
107 CommentAt string
108
109 // content
110 Body string
111
112 // meta
113 Created time.Time
114}
115
116func (p *Pull) LatestPatch() string {
117 latestSubmission := p.Submissions[p.LastRoundNumber()]
118 return latestSubmission.Patch
119}
120
121func (p *Pull) LastRoundNumber() int {
122 return len(p.Submissions) - 1
123}
124
125func (p *Pull) IsSameRepoBranch() bool {
126 if p.PullSource != nil {
127 if p.PullSource.RepoAt != nil {
128 return p.PullSource.RepoAt == &p.RepoAt
129 } else {
130 // no repo specified
131 return true
132 }
133 }
134 return false
135}
136
137func (p *Pull) IsPatch() bool {
138 return p.PullSource == nil
139}
140
141func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff {
142 patch := s.Patch
143
144 diffs, _, err := gitdiff.Parse(strings.NewReader(patch))
145 if err != nil {
146 log.Println(err)
147 }
148
149 nd := types.NiceDiff{}
150 nd.Commit.Parent = targetBranch
151
152 for _, d := range diffs {
153 ndiff := types.Diff{}
154 ndiff.Name.New = d.NewName
155 ndiff.Name.Old = d.OldName
156 ndiff.IsBinary = d.IsBinary
157 ndiff.IsNew = d.IsNew
158 ndiff.IsDelete = d.IsDelete
159 ndiff.IsCopy = d.IsCopy
160 ndiff.IsRename = d.IsRename
161
162 for _, tf := range d.TextFragments {
163 ndiff.TextFragments = append(ndiff.TextFragments, *tf)
164 for _, l := range tf.Lines {
165 switch l.Op {
166 case gitdiff.OpAdd:
167 nd.Stat.Insertions += 1
168 case gitdiff.OpDelete:
169 nd.Stat.Deletions += 1
170 }
171 }
172 }
173
174 nd.Diff = append(nd.Diff, ndiff)
175 }
176
177 nd.Stat.FilesChanged = len(diffs)
178
179 return nd
180}
181
182func NewPull(tx *sql.Tx, pull *Pull) error {
183 defer tx.Rollback()
184
185 _, err := tx.Exec(`
186 insert or ignore into repo_pull_seqs (repo_at, next_pull_id)
187 values (?, 1)
188 `, pull.RepoAt)
189 if err != nil {
190 return err
191 }
192
193 var nextId int
194 err = tx.QueryRow(`
195 update repo_pull_seqs
196 set next_pull_id = next_pull_id + 1
197 where repo_at = ?
198 returning next_pull_id - 1
199 `, pull.RepoAt).Scan(&nextId)
200 if err != nil {
201 return err
202 }
203
204 pull.PullId = nextId
205 pull.State = PullOpen
206
207 var sourceBranch, sourceRepoAt *string
208 if pull.PullSource != nil {
209 sourceBranch = &pull.PullSource.Branch
210 if pull.PullSource.RepoAt != nil {
211 x := pull.PullSource.RepoAt.String()
212 sourceRepoAt = &x
213 }
214 }
215
216 _, err = tx.Exec(
217 `
218 insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state, source_branch, source_repo_at)
219 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
220 pull.RepoAt,
221 pull.OwnerDid,
222 pull.PullId,
223 pull.Title,
224 pull.TargetBranch,
225 pull.Body,
226 pull.Rkey,
227 pull.State,
228 sourceBranch,
229 sourceRepoAt,
230 )
231 if err != nil {
232 return err
233 }
234
235 _, err = tx.Exec(`
236 insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev)
237 values (?, ?, ?, ?, ?)
238 `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev)
239 if err != nil {
240 return err
241 }
242
243 if err := tx.Commit(); err != nil {
244 return err
245 }
246
247 return nil
248}
249
250func SetPullAt(e Execer, repoAt syntax.ATURI, pullId int, pullAt string) error {
251 _, err := e.Exec(`update pulls set pull_at = ? where repo_at = ? and pull_id = ?`, pullAt, repoAt, pullId)
252 return err
253}
254
255func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (string, error) {
256 var pullAt string
257 err := e.QueryRow(`select pull_at from pulls where repo_at = ? and pull_id = ?`, repoAt, pullId).Scan(&pullAt)
258 return pullAt, err
259}
260
261func NextPullId(e Execer, repoAt syntax.ATURI) (int, error) {
262 var pullId int
263 err := e.QueryRow(`select next_pull_id from repo_pull_seqs where repo_at = ?`, repoAt).Scan(&pullId)
264 return pullId - 1, err
265}
266
267func GetPulls(e Execer, repoAt syntax.ATURI, state PullState) ([]Pull, error) {
268 var pulls []Pull
269
270 rows, err := e.Query(`
271 select
272 owner_did,
273 pull_id,
274 created,
275 title,
276 state,
277 target_branch,
278 pull_at,
279 body,
280 rkey,
281 source_branch,
282 source_repo_at
283 from
284 pulls
285 where
286 repo_at = ? and state = ?
287 order by
288 created desc`, repoAt, state)
289 if err != nil {
290 return nil, err
291 }
292 defer rows.Close()
293
294 for rows.Next() {
295 var pull Pull
296 var createdAt string
297 var sourceBranch, sourceRepoAt sql.NullString
298 err := rows.Scan(
299 &pull.OwnerDid,
300 &pull.PullId,
301 &createdAt,
302 &pull.Title,
303 &pull.State,
304 &pull.TargetBranch,
305 &pull.PullAt,
306 &pull.Body,
307 &pull.Rkey,
308 &sourceBranch,
309 &sourceRepoAt,
310 )
311 if err != nil {
312 return nil, err
313 }
314
315 createdTime, err := time.Parse(time.RFC3339, createdAt)
316 if err != nil {
317 return nil, err
318 }
319 pull.Created = createdTime
320
321 if sourceBranch.Valid {
322 pull.PullSource = &PullSource{
323 Branch: sourceBranch.String,
324 }
325 if sourceRepoAt.Valid {
326 sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
327 if err != nil {
328 return nil, err
329 }
330 pull.PullSource.RepoAt = &sourceRepoAtParsed
331 }
332 }
333
334 pulls = append(pulls, pull)
335 }
336
337 if err := rows.Err(); err != nil {
338 return nil, err
339 }
340
341 return pulls, nil
342}
343
344func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) {
345 query := `
346 select
347 owner_did,
348 pull_id,
349 created,
350 title,
351 state,
352 target_branch,
353 pull_at,
354 repo_at,
355 body,
356 rkey,
357 source_branch,
358 source_repo_at
359 from
360 pulls
361 where
362 repo_at = ? and pull_id = ?
363 `
364 row := e.QueryRow(query, repoAt, pullId)
365
366 var pull Pull
367 var createdAt string
368 var sourceBranch, sourceRepoAt sql.NullString
369 err := row.Scan(
370 &pull.OwnerDid,
371 &pull.PullId,
372 &createdAt,
373 &pull.Title,
374 &pull.State,
375 &pull.TargetBranch,
376 &pull.PullAt,
377 &pull.RepoAt,
378 &pull.Body,
379 &pull.Rkey,
380 &sourceBranch,
381 &sourceRepoAt,
382 )
383 if err != nil {
384 return nil, err
385 }
386
387 createdTime, err := time.Parse(time.RFC3339, createdAt)
388 if err != nil {
389 return nil, err
390 }
391 pull.Created = createdTime
392
393 // populate source
394 if sourceBranch.Valid {
395 pull.PullSource = &PullSource{
396 Branch: sourceBranch.String,
397 }
398 if sourceRepoAt.Valid {
399 sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
400 if err != nil {
401 return nil, err
402 }
403 pull.PullSource.RepoAt = &sourceRepoAtParsed
404 }
405 }
406
407 submissionsQuery := `
408 select
409 id, pull_id, repo_at, round_number, patch, created, source_rev
410 from
411 pull_submissions
412 where
413 repo_at = ? and pull_id = ?
414 `
415 submissionsRows, err := e.Query(submissionsQuery, repoAt, pullId)
416 if err != nil {
417 return nil, err
418 }
419 defer submissionsRows.Close()
420
421 submissionsMap := make(map[int]*PullSubmission)
422
423 for submissionsRows.Next() {
424 var submission PullSubmission
425 var submissionCreatedStr string
426 var submissionSourceRev sql.NullString
427 err := submissionsRows.Scan(
428 &submission.ID,
429 &submission.PullId,
430 &submission.RepoAt,
431 &submission.RoundNumber,
432 &submission.Patch,
433 &submissionCreatedStr,
434 &submissionSourceRev,
435 )
436 if err != nil {
437 return nil, err
438 }
439
440 submissionCreatedTime, err := time.Parse(time.RFC3339, submissionCreatedStr)
441 if err != nil {
442 return nil, err
443 }
444 submission.Created = submissionCreatedTime
445
446 if submissionSourceRev.Valid {
447 submission.SourceRev = submissionSourceRev.String
448 }
449
450 submissionsMap[submission.ID] = &submission
451 }
452 if err = submissionsRows.Close(); err != nil {
453 return nil, err
454 }
455 if len(submissionsMap) == 0 {
456 return &pull, nil
457 }
458
459 var args []any
460 for k := range submissionsMap {
461 args = append(args, k)
462 }
463 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(submissionsMap)), ", ")
464 commentsQuery := fmt.Sprintf(`
465 select
466 id,
467 pull_id,
468 submission_id,
469 repo_at,
470 owner_did,
471 comment_at,
472 body,
473 created
474 from
475 pull_comments
476 where
477 submission_id IN (%s)
478 order by
479 created asc
480 `, inClause)
481 commentsRows, err := e.Query(commentsQuery, args...)
482 if err != nil {
483 return nil, err
484 }
485 defer commentsRows.Close()
486
487 for commentsRows.Next() {
488 var comment PullComment
489 var commentCreatedStr string
490 err := commentsRows.Scan(
491 &comment.ID,
492 &comment.PullId,
493 &comment.SubmissionId,
494 &comment.RepoAt,
495 &comment.OwnerDid,
496 &comment.CommentAt,
497 &comment.Body,
498 &commentCreatedStr,
499 )
500 if err != nil {
501 return nil, err
502 }
503
504 commentCreatedTime, err := time.Parse(time.RFC3339, commentCreatedStr)
505 if err != nil {
506 return nil, err
507 }
508 comment.Created = commentCreatedTime
509
510 // Add the comment to its submission
511 if submission, ok := submissionsMap[comment.SubmissionId]; ok {
512 submission.Comments = append(submission.Comments, comment)
513 }
514
515 }
516 if err = commentsRows.Err(); err != nil {
517 return nil, err
518 }
519
520 pull.Submissions = make([]*PullSubmission, len(submissionsMap))
521 for _, submission := range submissionsMap {
522 pull.Submissions[submission.RoundNumber] = submission
523 }
524
525 return &pull, nil
526}
527
528// timeframe here is directly passed into the sql query filter, and any
529// timeframe in the past should be negative; e.g.: "-3 months"
530func GetPullsByOwnerDid(e Execer, did, timeframe string) ([]Pull, error) {
531 var pulls []Pull
532
533 rows, err := e.Query(`
534 select
535 p.owner_did,
536 p.repo_at,
537 p.pull_id,
538 p.created,
539 p.title,
540 p.state,
541 r.did,
542 r.name,
543 r.knot,
544 r.rkey,
545 r.created
546 from
547 pulls p
548 join
549 repos r on p.repo_at = r.at_uri
550 where
551 p.owner_did = ? and p.created >= date ('now', ?)
552 order by
553 p.created desc`, did, timeframe)
554 if err != nil {
555 return nil, err
556 }
557 defer rows.Close()
558
559 for rows.Next() {
560 var pull Pull
561 var repo Repo
562 var pullCreatedAt, repoCreatedAt string
563 err := rows.Scan(
564 &pull.OwnerDid,
565 &pull.RepoAt,
566 &pull.PullId,
567 &pullCreatedAt,
568 &pull.Title,
569 &pull.State,
570 &repo.Did,
571 &repo.Name,
572 &repo.Knot,
573 &repo.Rkey,
574 &repoCreatedAt,
575 )
576 if err != nil {
577 return nil, err
578 }
579
580 pullCreatedTime, err := time.Parse(time.RFC3339, pullCreatedAt)
581 if err != nil {
582 return nil, err
583 }
584 pull.Created = pullCreatedTime
585
586 repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt)
587 if err != nil {
588 return nil, err
589 }
590 repo.Created = repoCreatedTime
591
592 pull.Repo = &repo
593
594 pulls = append(pulls, pull)
595 }
596
597 if err := rows.Err(); err != nil {
598 return nil, err
599 }
600
601 return pulls, nil
602}
603
604func NewPullComment(e Execer, comment *PullComment) (int64, error) {
605 query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)`
606 res, err := e.Exec(
607 query,
608 comment.OwnerDid,
609 comment.RepoAt,
610 comment.SubmissionId,
611 comment.CommentAt,
612 comment.PullId,
613 comment.Body,
614 )
615 if err != nil {
616 return 0, err
617 }
618
619 i, err := res.LastInsertId()
620 if err != nil {
621 return 0, err
622 }
623
624 return i, nil
625}
626
627func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState PullState) error {
628 _, err := e.Exec(`update pulls set state = ? where repo_at = ? and pull_id = ?`, pullState, repoAt, pullId)
629 return err
630}
631
632func ClosePull(e Execer, repoAt syntax.ATURI, pullId int) error {
633 err := SetPullState(e, repoAt, pullId, PullClosed)
634 return err
635}
636
637func ReopenPull(e Execer, repoAt syntax.ATURI, pullId int) error {
638 err := SetPullState(e, repoAt, pullId, PullOpen)
639 return err
640}
641
642func MergePull(e Execer, repoAt syntax.ATURI, pullId int) error {
643 err := SetPullState(e, repoAt, pullId, PullMerged)
644 return err
645}
646
647func ResubmitPull(e Execer, pull *Pull, newPatch, sourceRev string) error {
648 newRoundNumber := len(pull.Submissions)
649 _, err := e.Exec(`
650 insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev)
651 values (?, ?, ?, ?, ?)
652 `, pull.PullId, pull.RepoAt, newRoundNumber, newPatch, sourceRev)
653
654 return err
655}
656
657type PullCount struct {
658 Open int
659 Merged int
660 Closed int
661}
662
663func GetPullCount(e Execer, repoAt syntax.ATURI) (PullCount, error) {
664 row := e.QueryRow(`
665 select
666 count(case when state = ? then 1 end) as open_count,
667 count(case when state = ? then 1 end) as merged_count,
668 count(case when state = ? then 1 end) as closed_count
669 from pulls
670 where repo_at = ?`,
671 PullOpen,
672 PullMerged,
673 PullClosed,
674 repoAt,
675 )
676
677 var count PullCount
678 if err := row.Scan(&count.Open, &count.Merged, &count.Closed); err != nil {
679 return PullCount{0, 0, 0}, err
680 }
681
682 return count, nil
683}