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