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