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