Monorepo for Tangled
at master 401 lines 7.8 kB view raw
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 RepoDid string 63 64 // content 65 Title string 66 Body string 67 TargetBranch string 68 State PullState 69 Submissions []*PullSubmission 70 Mentions []syntax.DID 71 References []syntax.ATURI 72 73 // stacking 74 StackId string // nullable string 75 ChangeId string // nullable string 76 ParentChangeId string // nullable string 77 78 // meta 79 Created time.Time 80 PullSource *PullSource 81 82 // optionally, populate this when querying for reverse mappings 83 Labels LabelState 84 Repo *Repo 85} 86 87// NOTE: This method does not include patch blob in returned atproto record 88func (p Pull) AsRecord() tangled.RepoPull { 89 var source *tangled.RepoPull_Source 90 if p.PullSource != nil { 91 source = &tangled.RepoPull_Source{} 92 source.Branch = p.PullSource.Branch 93 source.Sha = p.LatestSha() 94 if p.PullSource.RepoDid != "" { 95 source.RepoDid = &p.PullSource.RepoDid 96 } else if p.PullSource.RepoAt != nil { 97 s := p.PullSource.RepoAt.String() 98 source.Repo = &s 99 } 100 } 101 mentions := make([]string, len(p.Mentions)) 102 for i, did := range p.Mentions { 103 mentions[i] = string(did) 104 } 105 references := make([]string, len(p.References)) 106 for i, uri := range p.References { 107 references[i] = string(uri) 108 } 109 110 var targetRepo *string 111 var targetRepoDid *string 112 if p.RepoDid != "" { 113 targetRepoDid = &p.RepoDid 114 } else { 115 s := p.RepoAt.String() 116 targetRepo = &s 117 } 118 record := tangled.RepoPull{ 119 Title: p.Title, 120 Body: &p.Body, 121 Mentions: mentions, 122 References: references, 123 CreatedAt: p.Created.Format(time.RFC3339), 124 Target: &tangled.RepoPull_Target{ 125 Repo: targetRepo, 126 RepoDid: targetRepoDid, 127 Branch: p.TargetBranch, 128 }, 129 Source: source, 130 } 131 return record 132} 133 134type PullSource struct { 135 Branch string 136 RepoAt *syntax.ATURI 137 RepoDid string 138 139 // optionally populate this for reverse mappings 140 Repo *Repo 141} 142 143type PullSubmission struct { 144 // ids 145 ID int 146 147 // at ids 148 PullAt syntax.ATURI 149 150 // content 151 RoundNumber int 152 Patch string 153 Combined string 154 Comments []PullComment 155 SourceRev string // include the rev that was used to create this submission: only for branch/fork PRs 156 157 // meta 158 Created time.Time 159} 160 161type PullComment struct { 162 // ids 163 ID int 164 PullId int 165 SubmissionId int 166 167 // at ids 168 RepoAt string 169 OwnerDid string 170 CommentAt string 171 RepoDid string 172 173 // content 174 Body string 175 176 // meta 177 Mentions []syntax.DID 178 References []syntax.ATURI 179 180 // meta 181 Created time.Time 182} 183 184func (p *PullComment) AtUri() syntax.ATURI { 185 return syntax.ATURI(p.CommentAt) 186} 187 188func (p *Pull) TotalComments() int { 189 total := 0 190 for _, s := range p.Submissions { 191 total += len(s.Comments) 192 } 193 return total 194} 195 196func (p *Pull) LastRoundNumber() int { 197 return len(p.Submissions) - 1 198} 199 200func (p *Pull) LatestSubmission() *PullSubmission { 201 return p.Submissions[p.LastRoundNumber()] 202} 203 204func (p *Pull) LatestPatch() string { 205 return p.LatestSubmission().Patch 206} 207 208func (p *Pull) LatestSha() string { 209 return p.LatestSubmission().SourceRev 210} 211 212func (p *Pull) AtUri() syntax.ATURI { 213 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.OwnerDid, tangled.RepoPullNSID, p.Rkey)) 214} 215 216func (p *Pull) IsPatchBased() bool { 217 return p.PullSource == nil 218} 219 220func (p *Pull) IsBranchBased() bool { 221 if p.PullSource != nil { 222 if p.PullSource.RepoAt != nil { 223 return p.PullSource.RepoAt == &p.RepoAt 224 } else { 225 // no repo specified 226 return true 227 } 228 } 229 return false 230} 231 232func (p *Pull) IsForkBased() bool { 233 if p.PullSource != nil { 234 if p.PullSource.RepoAt != nil { 235 // make sure repos are different 236 return p.PullSource.RepoAt != &p.RepoAt 237 } 238 } 239 return false 240} 241 242func (p *Pull) IsStacked() bool { 243 return p.StackId != "" 244} 245 246func (p *Pull) Participants() []string { 247 participantSet := make(map[string]struct{}) 248 participants := []string{} 249 250 addParticipant := func(did string) { 251 if _, exists := participantSet[did]; !exists { 252 participantSet[did] = struct{}{} 253 participants = append(participants, did) 254 } 255 } 256 257 addParticipant(p.OwnerDid) 258 259 for _, s := range p.Submissions { 260 for _, sp := range s.Participants() { 261 addParticipant(sp) 262 } 263 } 264 265 return participants 266} 267 268func (s PullSubmission) IsFormatPatch() bool { 269 return patchutil.IsFormatPatch(s.Patch) 270} 271 272func (s PullSubmission) AsFormatPatch() []types.FormatPatch { 273 patches, err := patchutil.ExtractPatches(s.Patch) 274 if err != nil { 275 log.Println("error extracting patches from submission:", err) 276 return []types.FormatPatch{} 277 } 278 279 return patches 280} 281 282func (s *PullSubmission) Participants() []string { 283 participantSet := make(map[string]struct{}) 284 participants := []string{} 285 286 addParticipant := func(did string) { 287 if _, exists := participantSet[did]; !exists { 288 participantSet[did] = struct{}{} 289 participants = append(participants, did) 290 } 291 } 292 293 addParticipant(s.PullAt.Authority().String()) 294 295 for _, c := range s.Comments { 296 addParticipant(c.OwnerDid) 297 } 298 299 return participants 300} 301 302func (s PullSubmission) CombinedPatch() string { 303 if s.Combined == "" { 304 return s.Patch 305 } 306 307 return s.Combined 308} 309 310type Stack []*Pull 311 312// position of this pull in the stack 313func (stack Stack) Position(pull *Pull) int { 314 return slices.IndexFunc(stack, func(p *Pull) bool { 315 return p.ChangeId == pull.ChangeId 316 }) 317} 318 319// all pulls below this pull (including self) in this stack 320// 321// nil if this pull does not belong to this stack 322func (stack Stack) Below(pull *Pull) Stack { 323 position := stack.Position(pull) 324 325 if position < 0 { 326 return nil 327 } 328 329 return stack[position:] 330} 331 332// all pulls below this pull (excluding self) in this stack 333func (stack Stack) StrictlyBelow(pull *Pull) Stack { 334 below := stack.Below(pull) 335 336 if len(below) > 0 { 337 return below[1:] 338 } 339 340 return nil 341} 342 343// all pulls above this pull (including self) in this stack 344func (stack Stack) Above(pull *Pull) Stack { 345 position := stack.Position(pull) 346 347 if position < 0 { 348 return nil 349 } 350 351 return stack[:position+1] 352} 353 354// all pulls below this pull (excluding self) in this stack 355func (stack Stack) StrictlyAbove(pull *Pull) Stack { 356 above := stack.Above(pull) 357 358 if len(above) > 0 { 359 return above[:len(above)-1] 360 } 361 362 return nil 363} 364 365// the combined format-patches of all the newest submissions in this stack 366func (stack Stack) CombinedPatch() string { 367 // go in reverse order because the bottom of the stack is the last element in the slice 368 var combined strings.Builder 369 for idx := range stack { 370 pull := stack[len(stack)-1-idx] 371 combined.WriteString(pull.LatestPatch()) 372 combined.WriteString("\n") 373 } 374 return combined.String() 375} 376 377// filter out PRs that are "active" 378// 379// PRs that are still open are active 380func (stack Stack) Mergeable() Stack { 381 var mergeable Stack 382 383 for _, p := range stack { 384 // stop at the first merged PR 385 if p.State == PullMerged || p.State == PullClosed { 386 break 387 } 388 389 // skip over deleted PRs 390 if p.State != PullDeleted { 391 mergeable = append(mergeable, p) 392 } 393 } 394 395 return mergeable 396} 397 398type BranchDeleteStatus struct { 399 Repo *Repo 400 Branch string 401}