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