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