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