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 Repo *Repo
81}
82
83func (p Pull) AsRecord() tangled.RepoPull {
84 var source *tangled.RepoPull_Source
85 if p.PullSource != nil {
86 s := p.PullSource.AsRecord()
87 source = &s
88 source.Sha = p.LatestSha()
89 }
90
91 record := tangled.RepoPull{
92 Title: p.Title,
93 Body: &p.Body,
94 CreatedAt: p.Created.Format(time.RFC3339),
95 Target: &tangled.RepoPull_Target{
96 Repo: p.RepoAt.String(),
97 Branch: p.TargetBranch,
98 },
99 Patch: p.LatestPatch(),
100 Source: source,
101 }
102 return record
103}
104
105type PullSource struct {
106 Branch string
107 RepoAt *syntax.ATURI
108
109 // optionally populate this for reverse mappings
110 Repo *Repo
111}
112
113func (p PullSource) AsRecord() tangled.RepoPull_Source {
114 var repoAt *string
115 if p.RepoAt != nil {
116 s := p.RepoAt.String()
117 repoAt = &s
118 }
119 record := tangled.RepoPull_Source{
120 Branch: p.Branch,
121 Repo: repoAt,
122 }
123 return record
124}
125
126type PullSubmission struct {
127 // ids
128 ID int
129 PullId int
130
131 // at ids
132 RepoAt syntax.ATURI
133
134 // content
135 RoundNumber int
136 Patch string
137 Comments []PullComment
138 SourceRev string // include the rev that was used to create this submission: only for branch/fork PRs
139
140 // meta
141 Created time.Time
142}
143
144type PullComment struct {
145 // ids
146 ID int
147 PullId int
148 SubmissionId int
149
150 // at ids
151 RepoAt string
152 OwnerDid string
153 CommentAt string
154
155 // content
156 Body string
157
158 // meta
159 Created time.Time
160}
161
162func (p *Pull) LatestPatch() string {
163 latestSubmission := p.Submissions[p.LastRoundNumber()]
164 return latestSubmission.Patch
165}
166
167func (p *Pull) LatestSha() string {
168 latestSubmission := p.Submissions[p.LastRoundNumber()]
169 return latestSubmission.SourceRev
170}
171
172func (p *Pull) PullAt() syntax.ATURI {
173 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.OwnerDid, tangled.RepoPullNSID, p.Rkey))
174}
175
176func (p *Pull) LastRoundNumber() int {
177 return len(p.Submissions) - 1
178}
179
180func (p *Pull) IsPatchBased() bool {
181 return p.PullSource == nil
182}
183
184func (p *Pull) IsBranchBased() bool {
185 if p.PullSource != nil {
186 if p.PullSource.RepoAt != nil {
187 return p.PullSource.RepoAt == &p.RepoAt
188 } else {
189 // no repo specified
190 return true
191 }
192 }
193 return false
194}
195
196func (p *Pull) IsForkBased() bool {
197 if p.PullSource != nil {
198 if p.PullSource.RepoAt != nil {
199 // make sure repos are different
200 return p.PullSource.RepoAt != &p.RepoAt
201 }
202 }
203 return false
204}
205
206func (p *Pull) IsStacked() bool {
207 return p.StackId != ""
208}
209
210func (s PullSubmission) IsFormatPatch() bool {
211 return patchutil.IsFormatPatch(s.Patch)
212}
213
214func (s PullSubmission) AsFormatPatch() []types.FormatPatch {
215 patches, err := patchutil.ExtractPatches(s.Patch)
216 if err != nil {
217 log.Println("error extracting patches from submission:", err)
218 return []types.FormatPatch{}
219 }
220
221 return patches
222}
223
224type Stack []*Pull
225
226// position of this pull in the stack
227func (stack Stack) Position(pull *Pull) int {
228 return slices.IndexFunc(stack, func(p *Pull) bool {
229 return p.ChangeId == pull.ChangeId
230 })
231}
232
233// all pulls below this pull (including self) in this stack
234//
235// nil if this pull does not belong to this stack
236func (stack Stack) Below(pull *Pull) Stack {
237 position := stack.Position(pull)
238
239 if position < 0 {
240 return nil
241 }
242
243 return stack[position:]
244}
245
246// all pulls below this pull (excluding self) in this stack
247func (stack Stack) StrictlyBelow(pull *Pull) Stack {
248 below := stack.Below(pull)
249
250 if len(below) > 0 {
251 return below[1:]
252 }
253
254 return nil
255}
256
257// all pulls above this pull (including self) in this stack
258func (stack Stack) Above(pull *Pull) Stack {
259 position := stack.Position(pull)
260
261 if position < 0 {
262 return nil
263 }
264
265 return stack[:position+1]
266}
267
268// all pulls below this pull (excluding self) in this stack
269func (stack Stack) StrictlyAbove(pull *Pull) Stack {
270 above := stack.Above(pull)
271
272 if len(above) > 0 {
273 return above[:len(above)-1]
274 }
275
276 return nil
277}
278
279// the combined format-patches of all the newest submissions in this stack
280func (stack Stack) CombinedPatch() string {
281 // go in reverse order because the bottom of the stack is the last element in the slice
282 var combined strings.Builder
283 for idx := range stack {
284 pull := stack[len(stack)-1-idx]
285 combined.WriteString(pull.LatestPatch())
286 combined.WriteString("\n")
287 }
288 return combined.String()
289}
290
291// filter out PRs that are "active"
292//
293// PRs that are still open are active
294func (stack Stack) Mergeable() Stack {
295 var mergeable Stack
296
297 for _, p := range stack {
298 // stop at the first merged PR
299 if p.State == PullMerged || p.State == PullClosed {
300 break
301 }
302
303 // skip over deleted PRs
304 if p.State != PullDeleted {
305 mergeable = append(mergeable, p)
306 }
307 }
308
309 return mergeable
310}