forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
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
86func (p Pull) AsRecord() tangled.RepoPull {
87 var source *tangled.RepoPull_Source
88 if p.PullSource != nil {
89 source = &tangled.RepoPull_Source{}
90 source.Branch = p.PullSource.Branch
91 source.Sha = p.LatestSha()
92 if p.PullSource.RepoAt != nil {
93 s := p.PullSource.RepoAt.String()
94 source.Repo = &s
95 }
96 }
97 mentions := make([]string, len(p.Mentions))
98 for i, did := range p.Mentions {
99 mentions[i] = string(did)
100 }
101 references := make([]string, len(p.References))
102 for i, uri := range p.References {
103 references[i] = string(uri)
104 }
105
106 record := tangled.RepoPull{
107 Title: p.Title,
108 Body: &p.Body,
109 Mentions: mentions,
110 References: references,
111 CreatedAt: p.Created.Format(time.RFC3339),
112 Target: &tangled.RepoPull_Target{
113 Repo: p.RepoAt.String(),
114 Branch: p.TargetBranch,
115 },
116 Patch: p.LatestPatch(),
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
174// func (p *PullComment) AsRecord() tangled.RepoPullComment {
175// mentions := make([]string, len(p.Mentions))
176// for i, did := range p.Mentions {
177// mentions[i] = string(did)
178// }
179// references := make([]string, len(p.References))
180// for i, uri := range p.References {
181// references[i] = string(uri)
182// }
183// return tangled.RepoPullComment{
184// Pull: p.PullAt,
185// Body: p.Body,
186// Mentions: mentions,
187// References: references,
188// CreatedAt: p.Created.Format(time.RFC3339),
189// }
190// }
191
192func (p *Pull) LastRoundNumber() int {
193 return len(p.Submissions) - 1
194}
195
196func (p *Pull) LatestSubmission() *PullSubmission {
197 return p.Submissions[p.LastRoundNumber()]
198}
199
200func (p *Pull) LatestPatch() string {
201 return p.LatestSubmission().Patch
202}
203
204func (p *Pull) LatestSha() string {
205 return p.LatestSubmission().SourceRev
206}
207
208func (p *Pull) AtUri() syntax.ATURI {
209 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.OwnerDid, tangled.RepoPullNSID, p.Rkey))
210}
211
212func (p *Pull) IsPatchBased() bool {
213 return p.PullSource == nil
214}
215
216func (p *Pull) IsBranchBased() bool {
217 if p.PullSource != nil {
218 if p.PullSource.RepoAt != nil {
219 return p.PullSource.RepoAt == &p.RepoAt
220 } else {
221 // no repo specified
222 return true
223 }
224 }
225 return false
226}
227
228func (p *Pull) IsForkBased() bool {
229 if p.PullSource != nil {
230 if p.PullSource.RepoAt != nil {
231 // make sure repos are different
232 return p.PullSource.RepoAt != &p.RepoAt
233 }
234 }
235 return false
236}
237
238func (p *Pull) IsStacked() bool {
239 return p.StackId != ""
240}
241
242func (p *Pull) Participants() []string {
243 participantSet := make(map[string]struct{})
244 participants := []string{}
245
246 addParticipant := func(did string) {
247 if _, exists := participantSet[did]; !exists {
248 participantSet[did] = struct{}{}
249 participants = append(participants, did)
250 }
251 }
252
253 addParticipant(p.OwnerDid)
254
255 for _, s := range p.Submissions {
256 for _, sp := range s.Participants() {
257 addParticipant(sp)
258 }
259 }
260
261 return participants
262}
263
264func (s PullSubmission) IsFormatPatch() bool {
265 return patchutil.IsFormatPatch(s.Patch)
266}
267
268func (s PullSubmission) AsFormatPatch() []types.FormatPatch {
269 patches, err := patchutil.ExtractPatches(s.Patch)
270 if err != nil {
271 log.Println("error extracting patches from submission:", err)
272 return []types.FormatPatch{}
273 }
274
275 return patches
276}
277
278func (s *PullSubmission) Participants() []string {
279 participantSet := make(map[string]struct{})
280 participants := []string{}
281
282 addParticipant := func(did string) {
283 if _, exists := participantSet[did]; !exists {
284 participantSet[did] = struct{}{}
285 participants = append(participants, did)
286 }
287 }
288
289 addParticipant(s.PullAt.Authority().String())
290
291 for _, c := range s.Comments {
292 addParticipant(c.OwnerDid)
293 }
294
295 return participants
296}
297
298func (s PullSubmission) CombinedPatch() string {
299 if s.Combined == "" {
300 return s.Patch
301 }
302
303 return s.Combined
304}
305
306type Stack []*Pull
307
308// position of this pull in the stack
309func (stack Stack) Position(pull *Pull) int {
310 return slices.IndexFunc(stack, func(p *Pull) bool {
311 return p.ChangeId == pull.ChangeId
312 })
313}
314
315// all pulls below this pull (including self) in this stack
316//
317// nil if this pull does not belong to this stack
318func (stack Stack) Below(pull *Pull) Stack {
319 position := stack.Position(pull)
320
321 if position < 0 {
322 return nil
323 }
324
325 return stack[position:]
326}
327
328// all pulls below this pull (excluding self) in this stack
329func (stack Stack) StrictlyBelow(pull *Pull) Stack {
330 below := stack.Below(pull)
331
332 if len(below) > 0 {
333 return below[1:]
334 }
335
336 return nil
337}
338
339// all pulls above this pull (including self) in this stack
340func (stack Stack) Above(pull *Pull) Stack {
341 position := stack.Position(pull)
342
343 if position < 0 {
344 return nil
345 }
346
347 return stack[:position+1]
348}
349
350// all pulls below this pull (excluding self) in this stack
351func (stack Stack) StrictlyAbove(pull *Pull) Stack {
352 above := stack.Above(pull)
353
354 if len(above) > 0 {
355 return above[:len(above)-1]
356 }
357
358 return nil
359}
360
361// the combined format-patches of all the newest submissions in this stack
362func (stack Stack) CombinedPatch() string {
363 // go in reverse order because the bottom of the stack is the last element in the slice
364 var combined strings.Builder
365 for idx := range stack {
366 pull := stack[len(stack)-1-idx]
367 combined.WriteString(pull.LatestPatch())
368 combined.WriteString("\n")
369 }
370 return combined.String()
371}
372
373// filter out PRs that are "active"
374//
375// PRs that are still open are active
376func (stack Stack) Mergeable() Stack {
377 var mergeable Stack
378
379 for _, p := range stack {
380 // stop at the first merged PR
381 if p.State == PullMerged || p.State == PullClosed {
382 break
383 }
384
385 // skip over deleted PRs
386 if p.State != PullDeleted {
387 mergeable = append(mergeable, p)
388 }
389 }
390
391 return mergeable
392}
393
394type BranchDeleteStatus struct {
395 Repo *Repo
396 Branch string
397}