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