This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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