A vibe coded tangled fork which supports pijul.
at 2a2718a4a548236a0c704a596fd836f263e9740d 565 lines 12 kB view raw
1package models 2 3import ( 4 "bytes" 5 "compress/gzip" 6 "fmt" 7 "io" 8 "log" 9 "slices" 10 "strings" 11 "time" 12 13 "tangled.org/core/api/tangled" 14 "tangled.org/core/patchutil" 15 "tangled.org/core/types" 16 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 lexutil "github.com/bluesky-social/indigo/lex/util" 19) 20 21type PullState int 22 23const ( 24 PullClosed PullState = iota 25 PullOpen 26 PullMerged 27 PullAbandoned 28) 29 30func (p PullState) String() string { 31 switch p { 32 case PullOpen: 33 return "open" 34 case PullMerged: 35 return "merged" 36 case PullClosed: 37 return "closed" 38 case PullAbandoned: 39 return "abandoned" 40 default: 41 return "closed" 42 } 43} 44 45func (p PullState) IsOpen() bool { 46 return p == PullOpen 47} 48func (p PullState) IsMerged() bool { 49 return p == PullMerged 50} 51func (p PullState) IsClosed() bool { 52 return p == PullClosed 53} 54func (p PullState) IsAbandoned() bool { 55 return p == PullAbandoned 56} 57 58type Pull struct { 59 // ids 60 ID int 61 PullId int 62 63 // at ids 64 RepoAt syntax.ATURI 65 OwnerDid string 66 Rkey string 67 68 // content 69 Title string 70 Body string 71 TargetBranch string 72 State PullState 73 Submissions []*PullSubmission 74 Mentions []syntax.DID 75 References []syntax.ATURI 76 77 // stacking 78 DependentOn *syntax.ATURI 79 // StackId string // nullable string 80 // ChangeId string // nullable string 81 // ParentChangeId string // nullable string 82 83 // meta 84 Created time.Time 85 PullSource *PullSource 86 87 // optionally, populate this when querying for reverse mappings 88 Labels LabelState 89 Repo *Repo 90} 91 92// NOTE: This method does not include patch blob in returned atproto record 93func (p Pull) AsRecord() tangled.RepoPull { 94 var source *tangled.RepoPull_Source 95 if p.PullSource != nil { 96 source = &tangled.RepoPull_Source{} 97 source.Branch = p.PullSource.Branch 98 if p.PullSource.RepoAt != nil { 99 s := p.PullSource.RepoAt.String() 100 source.Repo = &s 101 } 102 } 103 mentions := make([]string, len(p.Mentions)) 104 for i, did := range p.Mentions { 105 mentions[i] = string(did) 106 } 107 references := make([]string, len(p.References)) 108 for i, uri := range p.References { 109 references[i] = string(uri) 110 } 111 112 rounds := make([]*tangled.RepoPull_Round, len(p.Submissions)) 113 for i, submission := range p.Submissions { 114 rounds[i] = submission.AsRecord() 115 } 116 117 var dependentOn *string 118 if p.DependentOn != nil { 119 x := p.DependentOn.String() 120 dependentOn = &x 121 } 122 123 record := tangled.RepoPull{ 124 Title: p.Title, 125 Body: &p.Body, 126 Mentions: mentions, 127 References: references, 128 CreatedAt: p.Created.Format(time.RFC3339), 129 Target: &tangled.RepoPull_Target{ 130 Repo: p.RepoAt.String(), 131 Branch: p.TargetBranch, 132 }, 133 Rounds: rounds, 134 Source: source, 135 DependentOn: dependentOn, 136 } 137 return record 138} 139 140func PullFromRecord(did, rkey string, record tangled.RepoPull, blobs []*io.ReadCloser) Pull { 141 created, err := time.Parse(time.RFC3339, record.CreatedAt) 142 if err != nil { 143 created = time.Now() 144 } 145 146 body := "" 147 if record.Body != nil { 148 body = *record.Body 149 } 150 151 var mentions []syntax.DID 152 for _, m := range record.Mentions { 153 if did, err := syntax.ParseDID(m); err == nil { 154 mentions = append(mentions, did) 155 } 156 } 157 158 var targetRepoAt syntax.ATURI 159 var targetBranch string 160 if record.Target != nil { 161 if uri, err := syntax.ParseATURI(record.Target.Repo); err == nil { 162 targetRepoAt = uri 163 } 164 targetBranch = record.Target.Branch 165 } 166 167 var pullSource *PullSource 168 if record.Source != nil { 169 pullSource = &PullSource{ 170 Branch: record.Source.Branch, 171 } 172 173 if record.Source.Repo != nil { 174 if uri, err := syntax.ParseATURI(*record.Source.Repo); err == nil { 175 pullSource.RepoAt = &uri 176 } 177 } 178 } 179 180 var dependentOn *syntax.ATURI 181 if record.DependentOn != nil { 182 if uri, err := syntax.ParseATURI(*record.DependentOn); err == nil { 183 dependentOn = &uri 184 } 185 } 186 187 var submissions []*PullSubmission 188 for i, s := range record.Rounds { 189 var blob *io.ReadCloser 190 if i < len(blobs) { 191 blob = blobs[i] 192 } 193 submission, err := PullSubmissionFromRecord(did, rkey, i, s, blob) 194 // TODO: log or bubble error here 195 if err != nil { 196 submissions = append(submissions, nil) 197 } else { 198 submissions = append(submissions, submission) 199 } 200 } 201 202 return Pull{ 203 RepoAt: targetRepoAt, 204 OwnerDid: did, 205 Rkey: rkey, 206 Title: record.Title, 207 Body: body, 208 TargetBranch: targetBranch, 209 PullSource: pullSource, 210 State: PullOpen, 211 Submissions: submissions, 212 Created: created, 213 DependentOn: dependentOn, 214 } 215} 216 217func PullSubmissionFromRecord(did, rkey string, roundNumber int, round *tangled.RepoPull_Round, blob *io.ReadCloser) (*PullSubmission, error) { 218 created, err := time.Parse(time.RFC3339, round.CreatedAt) 219 if err != nil { 220 created = time.Now() 221 } 222 223 var patch, sourceRev string 224 if blob != nil { 225 p, err := extractGzip(*blob) 226 if err != nil { 227 return nil, fmt.Errorf("failed to extract gzip: %w", err) 228 } 229 patch = p 230 if patchutil.IsFormatPatch(p) { 231 patches, err := patchutil.ExtractPatches(p) 232 if err != nil { 233 return nil, fmt.Errorf("failed to extract patches: %w", err) 234 } 235 236 for _, part := range patches { 237 sourceRev = part.SHA 238 } 239 } 240 } 241 242 log.Println("source sha", sourceRev) 243 244 return &PullSubmission{ 245 PullAt: syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", did, tangled.RepoPullNSID, rkey)), 246 RoundNumber: roundNumber, 247 Blob: *round.PatchBlob, 248 Created: created, 249 Patch: patch, 250 SourceRev: sourceRev, 251 }, nil 252} 253 254type PullSource struct { 255 Branch string 256 RepoAt *syntax.ATURI 257 258 // optionally populate this for reverse mappings 259 Repo *Repo 260} 261 262type PullSubmission struct { 263 // ids 264 ID int 265 266 // at ids 267 PullAt syntax.ATURI 268 269 // content 270 RoundNumber int 271 Blob lexutil.LexBlob 272 Patch string 273 Combined string 274 Comments []PullComment 275 SourceRev string // include the rev that was used to create this submission: only for branch/fork PRs 276 277 // meta 278 Created time.Time 279} 280 281type PullComment struct { 282 // ids 283 ID int 284 PullId int 285 SubmissionId int 286 287 // at ids 288 RepoAt string 289 OwnerDid string 290 CommentAt string 291 292 // content 293 Body string 294 295 // meta 296 Mentions []syntax.DID 297 References []syntax.ATURI 298 299 // meta 300 Created time.Time 301} 302 303func (p *PullComment) AtUri() syntax.ATURI { 304 return syntax.ATURI(p.CommentAt) 305} 306 307func (p *Pull) TotalComments() int { 308 total := 0 309 for _, s := range p.Submissions { 310 total += len(s.Comments) 311 } 312 return total 313} 314 315func (p *Pull) LastRoundNumber() int { 316 return len(p.Submissions) - 1 317} 318 319func (p *Pull) LatestSubmission() *PullSubmission { 320 return p.Submissions[p.LastRoundNumber()] 321} 322 323func (p *Pull) LatestPatch() string { 324 return p.LatestSubmission().Patch 325} 326 327func (p *Pull) LatestSha() string { 328 return p.LatestSubmission().SourceRev 329} 330 331func (p *Pull) AtUri() syntax.ATURI { 332 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.OwnerDid, tangled.RepoPullNSID, p.Rkey)) 333} 334 335func (p *Pull) IsPatchBased() bool { 336 return p.PullSource == nil 337} 338 339func (p *Pull) IsBranchBased() bool { 340 if p.PullSource != nil { 341 if p.PullSource.RepoAt != nil { 342 return p.PullSource.RepoAt == &p.RepoAt 343 } else { 344 // no repo specified 345 return true 346 } 347 } 348 return false 349} 350 351func (p *Pull) IsForkBased() bool { 352 if p.PullSource != nil { 353 if p.PullSource.RepoAt != nil { 354 // make sure repos are different 355 return p.PullSource.RepoAt != &p.RepoAt 356 } 357 } 358 return false 359} 360 361func (p *Pull) Participants() []string { 362 participantSet := make(map[string]struct{}) 363 participants := []string{} 364 365 addParticipant := func(did string) { 366 if _, exists := participantSet[did]; !exists { 367 participantSet[did] = struct{}{} 368 participants = append(participants, did) 369 } 370 } 371 372 addParticipant(p.OwnerDid) 373 374 for _, s := range p.Submissions { 375 for _, sp := range s.Participants() { 376 addParticipant(sp) 377 } 378 } 379 380 return participants 381} 382 383func (s PullSubmission) IsFormatPatch() bool { 384 return patchutil.IsFormatPatch(s.Patch) 385} 386 387func (s PullSubmission) AsFormatPatch() []types.FormatPatch { 388 patches, err := patchutil.ExtractPatches(s.Patch) 389 if err != nil { 390 log.Println("error extracting patches from submission:", err) 391 return []types.FormatPatch{} 392 } 393 394 return patches 395} 396 397// empty if invalid, not otherwise 398func (s PullSubmission) ChangeId() string { 399 patches := s.AsFormatPatch() 400 if len(patches) != 1 { 401 return "" 402 } 403 404 c, err := patches[0].ChangeId() 405 if err != nil { 406 return "" 407 } 408 409 return c 410} 411 412func (s *PullSubmission) Participants() []string { 413 participantSet := make(map[string]struct{}) 414 participants := []string{} 415 416 addParticipant := func(did string) { 417 if _, exists := participantSet[did]; !exists { 418 participantSet[did] = struct{}{} 419 participants = append(participants, did) 420 } 421 } 422 423 addParticipant(s.PullAt.Authority().String()) 424 425 for _, c := range s.Comments { 426 addParticipant(c.OwnerDid) 427 } 428 429 return participants 430} 431 432func (s PullSubmission) CombinedPatch() string { 433 if s.Combined == "" { 434 return s.Patch 435 } 436 437 return s.Combined 438} 439 440func (s *PullSubmission) GetBlob() *lexutil.LexBlob { 441 if !s.Blob.Ref.Defined() { 442 return nil 443 } 444 445 return &s.Blob 446} 447 448func (s *PullSubmission) AsRecord() *tangled.RepoPull_Round { 449 return &tangled.RepoPull_Round{ 450 CreatedAt: s.Created.Format(time.RFC3339), 451 PatchBlob: s.GetBlob(), 452 } 453} 454 455type Stack []*Pull 456 457// position of this pull in the stack 458func (stack Stack) Position(pull *Pull) int { 459 return slices.IndexFunc(stack, func(p *Pull) bool { 460 return p.AtUri() == pull.AtUri() 461 }) 462} 463 464// all pulls below this pull (including self) in this stack 465// 466// nil if this pull does not belong to this stack 467func (stack Stack) Below(pull *Pull) Stack { 468 position := stack.Position(pull) 469 470 if position < 0 { 471 return nil 472 } 473 474 return stack[position:] 475} 476 477// all pulls below this pull (excluding self) in this stack 478func (stack Stack) StrictlyBelow(pull *Pull) Stack { 479 below := stack.Below(pull) 480 481 if len(below) > 0 { 482 return below[1:] 483 } 484 485 return nil 486} 487 488// all pulls above this pull (including self) in this stack 489func (stack Stack) Above(pull *Pull) Stack { 490 position := stack.Position(pull) 491 492 if position < 0 { 493 return nil 494 } 495 496 return stack[:position+1] 497} 498 499// all pulls below this pull (excluding self) in this stack 500func (stack Stack) StrictlyAbove(pull *Pull) Stack { 501 above := stack.Above(pull) 502 503 if len(above) > 0 { 504 return above[:len(above)-1] 505 } 506 507 return nil 508} 509 510// the combined format-patches of all the newest submissions in this stack 511func (stack Stack) CombinedPatch() string { 512 // go in reverse order because the bottom of the stack is the last element in the slice 513 var combined strings.Builder 514 for idx := range stack { 515 pull := stack[len(stack)-1-idx] 516 combined.WriteString(pull.LatestPatch()) 517 combined.WriteString("\n") 518 } 519 return combined.String() 520} 521 522// filter out PRs that are "active" 523// 524// PRs that are still open are active 525func (stack Stack) Mergeable() Stack { 526 var mergeable Stack 527 528 for _, p := range stack { 529 // stop at the first merged PR 530 if p.State == PullMerged || p.State == PullClosed { 531 break 532 } 533 534 // skip over abandoned PRs 535 if p.State != PullAbandoned { 536 mergeable = append(mergeable, p) 537 } 538 } 539 540 return mergeable 541} 542 543type BranchDeleteStatus struct { 544 Repo *Repo 545 Branch string 546} 547 548func extractGzip(blob io.Reader) (string, error) { 549 var b bytes.Buffer 550 r, err := gzip.NewReader(blob) 551 if err != nil { 552 return "", err 553 } 554 defer r.Close() 555 556 const maxSize = 15 * 1024 * 1024 557 limitedReader := io.LimitReader(r, maxSize) 558 559 _, err = io.Copy(&b, limitedReader) 560 if err != nil { 561 return "", err 562 } 563 564 return b.String(), nil 565}