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