forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
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 86func (p Pull) AsRecord() tangled.RepoPull { 87 var source *tangled.RepoPull_Source 88 if p.PullSource != nil { 89 source = &tangled.RepoPull_Source{} 90 source.Branch = p.PullSource.Branch 91 source.Sha = p.LatestSha() 92 if p.PullSource.RepoAt != nil { 93 s := p.PullSource.RepoAt.String() 94 source.Repo = &s 95 } 96 } 97 mentions := make([]string, len(p.Mentions)) 98 for i, did := range p.Mentions { 99 mentions[i] = string(did) 100 } 101 references := make([]string, len(p.References)) 102 for i, uri := range p.References { 103 references[i] = string(uri) 104 } 105 106 record := tangled.RepoPull{ 107 Title: p.Title, 108 Body: &p.Body, 109 Mentions: mentions, 110 References: references, 111 CreatedAt: p.Created.Format(time.RFC3339), 112 Target: &tangled.RepoPull_Target{ 113 Repo: p.RepoAt.String(), 114 Branch: p.TargetBranch, 115 }, 116 Patch: p.LatestPatch(), 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 174// func (p *PullComment) AsRecord() tangled.RepoPullComment { 175// mentions := make([]string, len(p.Mentions)) 176// for i, did := range p.Mentions { 177// mentions[i] = string(did) 178// } 179// references := make([]string, len(p.References)) 180// for i, uri := range p.References { 181// references[i] = string(uri) 182// } 183// return tangled.RepoPullComment{ 184// Pull: p.PullAt, 185// Body: p.Body, 186// Mentions: mentions, 187// References: references, 188// CreatedAt: p.Created.Format(time.RFC3339), 189// } 190// } 191 192func (p *Pull) LastRoundNumber() int { 193 return len(p.Submissions) - 1 194} 195 196func (p *Pull) LatestSubmission() *PullSubmission { 197 return p.Submissions[p.LastRoundNumber()] 198} 199 200func (p *Pull) LatestPatch() string { 201 return p.LatestSubmission().Patch 202} 203 204func (p *Pull) LatestSha() string { 205 return p.LatestSubmission().SourceRev 206} 207 208func (p *Pull) AtUri() syntax.ATURI { 209 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.OwnerDid, tangled.RepoPullNSID, p.Rkey)) 210} 211 212func (p *Pull) IsPatchBased() bool { 213 return p.PullSource == nil 214} 215 216func (p *Pull) IsBranchBased() bool { 217 if p.PullSource != nil { 218 if p.PullSource.RepoAt != nil { 219 return p.PullSource.RepoAt == &p.RepoAt 220 } else { 221 // no repo specified 222 return true 223 } 224 } 225 return false 226} 227 228func (p *Pull) IsForkBased() bool { 229 if p.PullSource != nil { 230 if p.PullSource.RepoAt != nil { 231 // make sure repos are different 232 return p.PullSource.RepoAt != &p.RepoAt 233 } 234 } 235 return false 236} 237 238func (p *Pull) IsStacked() bool { 239 return p.StackId != "" 240} 241 242func (p *Pull) Participants() []string { 243 participantSet := make(map[string]struct{}) 244 participants := []string{} 245 246 addParticipant := func(did string) { 247 if _, exists := participantSet[did]; !exists { 248 participantSet[did] = struct{}{} 249 participants = append(participants, did) 250 } 251 } 252 253 addParticipant(p.OwnerDid) 254 255 for _, s := range p.Submissions { 256 for _, sp := range s.Participants() { 257 addParticipant(sp) 258 } 259 } 260 261 return participants 262} 263 264func (s PullSubmission) IsFormatPatch() bool { 265 return patchutil.IsFormatPatch(s.Patch) 266} 267 268func (s PullSubmission) AsFormatPatch() []types.FormatPatch { 269 patches, err := patchutil.ExtractPatches(s.Patch) 270 if err != nil { 271 log.Println("error extracting patches from submission:", err) 272 return []types.FormatPatch{} 273 } 274 275 return patches 276} 277 278func (s *PullSubmission) Participants() []string { 279 participantSet := make(map[string]struct{}) 280 participants := []string{} 281 282 addParticipant := func(did string) { 283 if _, exists := participantSet[did]; !exists { 284 participantSet[did] = struct{}{} 285 participants = append(participants, did) 286 } 287 } 288 289 addParticipant(s.PullAt.Authority().String()) 290 291 for _, c := range s.Comments { 292 addParticipant(c.OwnerDid) 293 } 294 295 return participants 296} 297 298func (s PullSubmission) CombinedPatch() string { 299 if s.Combined == "" { 300 return s.Patch 301 } 302 303 return s.Combined 304} 305 306type Stack []*Pull 307 308// position of this pull in the stack 309func (stack Stack) Position(pull *Pull) int { 310 return slices.IndexFunc(stack, func(p *Pull) bool { 311 return p.ChangeId == pull.ChangeId 312 }) 313} 314 315// all pulls below this pull (including self) in this stack 316// 317// nil if this pull does not belong to this stack 318func (stack Stack) Below(pull *Pull) Stack { 319 position := stack.Position(pull) 320 321 if position < 0 { 322 return nil 323 } 324 325 return stack[position:] 326} 327 328// all pulls below this pull (excluding self) in this stack 329func (stack Stack) StrictlyBelow(pull *Pull) Stack { 330 below := stack.Below(pull) 331 332 if len(below) > 0 { 333 return below[1:] 334 } 335 336 return nil 337} 338 339// all pulls above this pull (including self) in this stack 340func (stack Stack) Above(pull *Pull) Stack { 341 position := stack.Position(pull) 342 343 if position < 0 { 344 return nil 345 } 346 347 return stack[:position+1] 348} 349 350// all pulls below this pull (excluding self) in this stack 351func (stack Stack) StrictlyAbove(pull *Pull) Stack { 352 above := stack.Above(pull) 353 354 if len(above) > 0 { 355 return above[:len(above)-1] 356 } 357 358 return nil 359} 360 361// the combined format-patches of all the newest submissions in this stack 362func (stack Stack) CombinedPatch() string { 363 // go in reverse order because the bottom of the stack is the last element in the slice 364 var combined strings.Builder 365 for idx := range stack { 366 pull := stack[len(stack)-1-idx] 367 combined.WriteString(pull.LatestPatch()) 368 combined.WriteString("\n") 369 } 370 return combined.String() 371} 372 373// filter out PRs that are "active" 374// 375// PRs that are still open are active 376func (stack Stack) Mergeable() Stack { 377 var mergeable Stack 378 379 for _, p := range stack { 380 // stop at the first merged PR 381 if p.State == PullMerged || p.State == PullClosed { 382 break 383 } 384 385 // skip over deleted PRs 386 if p.State != PullDeleted { 387 mergeable = append(mergeable, p) 388 } 389 } 390 391 return mergeable 392} 393 394type BranchDeleteStatus struct { 395 Repo *Repo 396 Branch string 397}