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