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