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