Monorepo for Tangled
1package models
2
3import (
4 "fmt"
5 "log"
6 "slices"
7 "strings"
8 "time"
9
10 "github.com/bluesky-social/indigo/atproto/syntax"
11 "tangled.org/core/api/tangled"
12 "tangled.org/core/patchutil"
13 "tangled.org/core/types"
14)
15
16type PullState int
17
18const (
19 PullClosed PullState = iota
20 PullOpen
21 PullMerged
22 PullDeleted
23)
24
25func (p PullState) String() string {
26 switch p {
27 case PullOpen:
28 return "open"
29 case PullMerged:
30 return "merged"
31 case PullClosed:
32 return "closed"
33 case PullDeleted:
34 return "deleted"
35 default:
36 return "closed"
37 }
38}
39
40func (p PullState) IsOpen() bool {
41 return p == PullOpen
42}
43func (p PullState) IsMerged() bool {
44 return p == PullMerged
45}
46func (p PullState) IsClosed() bool {
47 return p == PullClosed
48}
49func (p PullState) IsDeleted() bool {
50 return p == PullDeleted
51}
52
53type Pull struct {
54 // ids
55 ID int
56 PullId int
57
58 // at ids
59 RepoAt syntax.ATURI
60 OwnerDid string
61 Rkey string
62 RepoDid string
63
64 // content
65 Title string
66 Body string
67 TargetBranch string
68 State PullState
69 Submissions []*PullSubmission
70 Mentions []syntax.DID
71 References []syntax.ATURI
72
73 // stacking
74 StackId string // nullable string
75 ChangeId string // nullable string
76 ParentChangeId string // nullable string
77
78 // meta
79 Created time.Time
80 PullSource *PullSource
81
82 // optionally, populate this when querying for reverse mappings
83 Labels LabelState
84 Repo *Repo
85}
86
87// NOTE: This method does not include patch blob in returned atproto record
88func (p Pull) AsRecord() tangled.RepoPull {
89 var source *tangled.RepoPull_Source
90 if p.PullSource != nil {
91 source = &tangled.RepoPull_Source{}
92 source.Branch = p.PullSource.Branch
93 source.Sha = p.LatestSha()
94 if p.PullSource.RepoDid != "" {
95 source.RepoDid = &p.PullSource.RepoDid
96 } else if p.PullSource.RepoAt != nil {
97 s := p.PullSource.RepoAt.String()
98 source.Repo = &s
99 }
100 }
101 mentions := make([]string, len(p.Mentions))
102 for i, did := range p.Mentions {
103 mentions[i] = string(did)
104 }
105 references := make([]string, len(p.References))
106 for i, uri := range p.References {
107 references[i] = string(uri)
108 }
109
110 var targetRepo *string
111 var targetRepoDid *string
112 if p.RepoDid != "" {
113 targetRepoDid = &p.RepoDid
114 } else {
115 s := p.RepoAt.String()
116 targetRepo = &s
117 }
118 record := tangled.RepoPull{
119 Title: p.Title,
120 Body: &p.Body,
121 Mentions: mentions,
122 References: references,
123 CreatedAt: p.Created.Format(time.RFC3339),
124 Target: &tangled.RepoPull_Target{
125 Repo: targetRepo,
126 RepoDid: targetRepoDid,
127 Branch: p.TargetBranch,
128 },
129 Source: source,
130 }
131 return record
132}
133
134type PullSource struct {
135 Branch string
136 RepoAt *syntax.ATURI
137 RepoDid string
138
139 // optionally populate this for reverse mappings
140 Repo *Repo
141}
142
143type PullSubmission struct {
144 // ids
145 ID int
146
147 // at ids
148 PullAt syntax.ATURI
149
150 // content
151 RoundNumber int
152 Patch string
153 Combined string
154 Comments []PullComment
155 SourceRev string // include the rev that was used to create this submission: only for branch/fork PRs
156
157 // meta
158 Created time.Time
159}
160
161type PullComment struct {
162 // ids
163 ID int
164 PullId int
165 SubmissionId int
166
167 // at ids
168 RepoAt string
169 OwnerDid string
170 CommentAt string
171 RepoDid string
172
173 // content
174 Body string
175
176 // meta
177 Mentions []syntax.DID
178 References []syntax.ATURI
179
180 // meta
181 Created time.Time
182}
183
184func (p *PullComment) AtUri() syntax.ATURI {
185 return syntax.ATURI(p.CommentAt)
186}
187
188func (p *Pull) TotalComments() int {
189 total := 0
190 for _, s := range p.Submissions {
191 total += len(s.Comments)
192 }
193 return total
194}
195
196func (p *Pull) LastRoundNumber() int {
197 return len(p.Submissions) - 1
198}
199
200func (p *Pull) LatestSubmission() *PullSubmission {
201 return p.Submissions[p.LastRoundNumber()]
202}
203
204func (p *Pull) LatestPatch() string {
205 return p.LatestSubmission().Patch
206}
207
208func (p *Pull) LatestSha() string {
209 return p.LatestSubmission().SourceRev
210}
211
212func (p *Pull) AtUri() syntax.ATURI {
213 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.OwnerDid, tangled.RepoPullNSID, p.Rkey))
214}
215
216func (p *Pull) IsPatchBased() bool {
217 return p.PullSource == nil
218}
219
220func (p *Pull) IsBranchBased() bool {
221 if p.PullSource != nil {
222 if p.PullSource.RepoAt != nil {
223 return p.PullSource.RepoAt == &p.RepoAt
224 } else {
225 // no repo specified
226 return true
227 }
228 }
229 return false
230}
231
232func (p *Pull) IsForkBased() bool {
233 if p.PullSource != nil {
234 if p.PullSource.RepoAt != nil {
235 // make sure repos are different
236 return p.PullSource.RepoAt != &p.RepoAt
237 }
238 }
239 return false
240}
241
242func (p *Pull) IsStacked() bool {
243 return p.StackId != ""
244}
245
246func (p *Pull) Participants() []string {
247 participantSet := make(map[string]struct{})
248 participants := []string{}
249
250 addParticipant := func(did string) {
251 if _, exists := participantSet[did]; !exists {
252 participantSet[did] = struct{}{}
253 participants = append(participants, did)
254 }
255 }
256
257 addParticipant(p.OwnerDid)
258
259 for _, s := range p.Submissions {
260 for _, sp := range s.Participants() {
261 addParticipant(sp)
262 }
263 }
264
265 return participants
266}
267
268func (s PullSubmission) IsFormatPatch() bool {
269 return patchutil.IsFormatPatch(s.Patch)
270}
271
272func (s PullSubmission) AsFormatPatch() []types.FormatPatch {
273 patches, err := patchutil.ExtractPatches(s.Patch)
274 if err != nil {
275 log.Println("error extracting patches from submission:", err)
276 return []types.FormatPatch{}
277 }
278
279 return patches
280}
281
282func (s *PullSubmission) Participants() []string {
283 participantSet := make(map[string]struct{})
284 participants := []string{}
285
286 addParticipant := func(did string) {
287 if _, exists := participantSet[did]; !exists {
288 participantSet[did] = struct{}{}
289 participants = append(participants, did)
290 }
291 }
292
293 addParticipant(s.PullAt.Authority().String())
294
295 for _, c := range s.Comments {
296 addParticipant(c.OwnerDid)
297 }
298
299 return participants
300}
301
302func (s PullSubmission) CombinedPatch() string {
303 if s.Combined == "" {
304 return s.Patch
305 }
306
307 return s.Combined
308}
309
310type Stack []*Pull
311
312// position of this pull in the stack
313func (stack Stack) Position(pull *Pull) int {
314 return slices.IndexFunc(stack, func(p *Pull) bool {
315 return p.ChangeId == pull.ChangeId
316 })
317}
318
319// all pulls below this pull (including self) in this stack
320//
321// nil if this pull does not belong to this stack
322func (stack Stack) Below(pull *Pull) Stack {
323 position := stack.Position(pull)
324
325 if position < 0 {
326 return nil
327 }
328
329 return stack[position:]
330}
331
332// all pulls below this pull (excluding self) in this stack
333func (stack Stack) StrictlyBelow(pull *Pull) Stack {
334 below := stack.Below(pull)
335
336 if len(below) > 0 {
337 return below[1:]
338 }
339
340 return nil
341}
342
343// all pulls above this pull (including self) in this stack
344func (stack Stack) Above(pull *Pull) Stack {
345 position := stack.Position(pull)
346
347 if position < 0 {
348 return nil
349 }
350
351 return stack[:position+1]
352}
353
354// all pulls below this pull (excluding self) in this stack
355func (stack Stack) StrictlyAbove(pull *Pull) Stack {
356 above := stack.Above(pull)
357
358 if len(above) > 0 {
359 return above[:len(above)-1]
360 }
361
362 return nil
363}
364
365// the combined format-patches of all the newest submissions in this stack
366func (stack Stack) CombinedPatch() string {
367 // go in reverse order because the bottom of the stack is the last element in the slice
368 var combined strings.Builder
369 for idx := range stack {
370 pull := stack[len(stack)-1-idx]
371 combined.WriteString(pull.LatestPatch())
372 combined.WriteString("\n")
373 }
374 return combined.String()
375}
376
377// filter out PRs that are "active"
378//
379// PRs that are still open are active
380func (stack Stack) Mergeable() Stack {
381 var mergeable Stack
382
383 for _, p := range stack {
384 // stop at the first merged PR
385 if p.State == PullMerged || p.State == PullClosed {
386 break
387 }
388
389 // skip over deleted PRs
390 if p.State != PullDeleted {
391 mergeable = append(mergeable, p)
392 }
393 }
394
395 return mergeable
396}
397
398type BranchDeleteStatus struct {
399 Repo *Repo
400 Branch string
401}