Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
Go
at sl/comment
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543package models
import ( "bytes" "compress/gzip" "fmt" "io" "log" "slices" "strings" "time"
"tangled.org/core/api/tangled" "tangled.org/core/patchutil" "tangled.org/core/types"
"github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util")
type PullState int
const ( PullClosed PullState = iota PullOpen PullMerged PullAbandoned)
func (p PullState) String() string { switch p { case PullOpen: return "open" case PullMerged: return "merged" case PullClosed: return "closed" case PullAbandoned: return "abandoned" default: return "closed" }}
func (p PullState) IsOpen() bool { return p == PullOpen}func (p PullState) IsMerged() bool { return p == PullMerged}func (p PullState) IsClosed() bool { return p == PullClosed}func (p PullState) IsAbandoned() bool { return p == PullAbandoned}
type Pull struct { // ids ID int PullId int
// at ids RepoDid syntax.DID OwnerDid string Rkey string
// content Title string Body string TargetBranch string State PullState Submissions []*PullSubmission Mentions []syntax.DID References []syntax.ATURI
// stacking DependentOn *syntax.ATURI
// meta Created time.Time PullSource *PullSource
// optionally, populate this when querying for reverse mappings Labels LabelState Repo *Repo}
// NOTE: This method does not include patch blob in returned atproto recordfunc (p Pull) AsRecord() tangled.RepoPull { mentions := make([]string, len(p.Mentions)) for i, did := range p.Mentions { mentions[i] = string(did) } references := make([]string, len(p.References)) for i, uri := range p.References { references[i] = string(uri) }
rounds := make([]*tangled.RepoPull_Round, len(p.Submissions)) for i, submission := range p.Submissions { rounds[i] = submission.AsRecord() }
var dependentOn *string if p.DependentOn != nil { x := p.DependentOn.String() dependentOn = &x }
return tangled.RepoPull{ Title: p.Title, Body: &p.Body, Mentions: mentions, References: references, CreatedAt: p.Created.Format(time.RFC3339), Target: &tangled.RepoPull_Target{ Repo: string(p.RepoDid), Branch: p.TargetBranch, }, Rounds: rounds, Source: p.PullSource.AsRecord(), DependentOn: dependentOn, }}
func PullFromRecord(did, rkey string, record tangled.RepoPull, blobs []*io.ReadCloser) (*Pull, error) { created, err := time.Parse(time.RFC3339, record.CreatedAt) if err != nil { return nil, fmt.Errorf("invalid createdAt: %w", err) }
body := "" if record.Body != nil { body = *record.Body }
var mentions []syntax.DID for _, m := range record.Mentions { if did, err := syntax.ParseDID(m); err == nil { mentions = append(mentions, did) } }
var targetRepoDid syntax.DID var targetBranch string if record.Target != nil { did, err := syntax.ParseDID(record.Target.Repo) if err != nil { return nil, fmt.Errorf("invalid target.repo did: %w", err) } targetRepoDid = did targetBranch = record.Target.Branch }
var pullSource *PullSource if record.Source != nil { pullSource = &PullSource{ Branch: record.Source.Branch, }
if record.Source.Repo != nil { did, err := syntax.ParseDID(*record.Source.Repo) if err != nil { return nil, fmt.Errorf("invalid source.repo did: %w", err) } pullSource.RepoDid = &did } }
var dependentOn *syntax.ATURI if record.DependentOn != nil { uri, err := syntax.ParseATURI(*record.DependentOn) if err != nil { return nil, fmt.Errorf("invalid dependentOn aturi: %w", err) } dependentOn = &uri }
var submissions []*PullSubmission for i, s := range record.Rounds { var blob *io.ReadCloser if i < len(blobs) { blob = blobs[i] } submission, err := PullSubmissionFromRecord(did, rkey, i, s, blob) if err != nil { return nil, fmt.Errorf("invalid pull round at index %d: %w", i, err) } submissions = append(submissions, submission) }
return &Pull{ RepoDid: targetRepoDid, OwnerDid: did, Rkey: rkey, Title: record.Title, Body: body, TargetBranch: targetBranch, PullSource: pullSource, State: PullOpen, Submissions: submissions, Created: created, DependentOn: dependentOn, }, nil}
func PullSubmissionFromRecord(did, rkey string, roundNumber int, round *tangled.RepoPull_Round, blob *io.ReadCloser) (*PullSubmission, error) { created, err := time.Parse(time.RFC3339, round.CreatedAt) if err != nil { return nil, fmt.Errorf("invalid createdAt: %w", err) }
var patch, sourceRev string if blob != nil { p, err := extractGzip(*blob) if err != nil { return nil, fmt.Errorf("failed to extract gzip: %w", err) } patch = p if patchutil.IsFormatPatch(p) { patches, err := patchutil.ExtractPatches(p) if err != nil { return nil, fmt.Errorf("failed to extract patches: %w", err) }
for _, part := range patches { sourceRev = part.SHA } } }
return &PullSubmission{ PullAt: syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", did, tangled.RepoPullNSID, rkey)), RoundNumber: roundNumber, Blob: *round.PatchBlob, Created: created, Patch: patch, SourceRev: sourceRev, }, nil}
type PullSource struct { Branch string RepoDid *syntax.DID
// optionally populate this for reverse mappings Repo *Repo}
func (s *PullSource) AsRecord() *tangled.RepoPull_Source { if s == nil { return nil } var repo *string if s.RepoDid != nil { r := s.RepoDid.String() repo = &r } return &tangled.RepoPull_Source{ Branch: s.Branch, Repo: repo, }}
type PullSubmission struct { // ids ID int
// at ids PullAt syntax.ATURI
// content RoundNumber int Blob lexutil.LexBlob Patch string Combined string Comments []Comment SourceRev string // include the rev that was used to create this submission: only for branch/fork PRs
// meta Created time.Time}
func (p *Pull) TotalComments() int { total := 0 for _, s := range p.Submissions { total += len(s.Comments) } return total}
func (p *Pull) LastRoundNumber() int { return len(p.Submissions) - 1}
func (p *Pull) LatestSubmission() *PullSubmission { return p.Submissions[p.LastRoundNumber()]}
func (p *Pull) LatestPatch() string { return p.LatestSubmission().Patch}
func (p *Pull) LatestSha() string { return p.LatestSubmission().SourceRev}
func (p *Pull) AtUri() syntax.ATURI { return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.OwnerDid, tangled.RepoPullNSID, p.Rkey))}
func (p *Pull) IsPatchBased() bool { return p.PullSource == nil}
func (p *Pull) IsBranchBased() bool { if p.PullSource != nil { if p.PullSource.RepoDid != nil { return *p.PullSource.RepoDid == p.RepoDid } // no repo specified return true } return false}
func (p *Pull) IsForkBased() bool { if p.PullSource != nil { if p.PullSource.RepoDid != nil { // make sure repos are different return *p.PullSource.RepoDid != p.RepoDid } } return false}
func (p *Pull) Participants() []syntax.DID { participantSet := make(map[syntax.DID]struct{}) participants := []syntax.DID{}
addParticipant := func(did syntax.DID) { if _, exists := participantSet[did]; !exists { participantSet[did] = struct{}{} participants = append(participants, did) } }
addParticipant(syntax.DID(p.OwnerDid))
for _, s := range p.Submissions { for _, sp := range s.Participants() { addParticipant(syntax.DID(sp)) } }
return participants}
func (s PullSubmission) IsFormatPatch() bool { return patchutil.IsFormatPatch(s.Patch)}
func (s PullSubmission) AsFormatPatch() []types.FormatPatch { patches, err := patchutil.ExtractPatches(s.Patch) if err != nil { log.Println("error extracting patches from submission:", err) return []types.FormatPatch{} }
return patches}
// empty if invalid, not otherwisefunc (s PullSubmission) ChangeId() string { patches := s.AsFormatPatch() if len(patches) != 1 { return "" }
c, err := patches[0].ChangeId() if err != nil { return "" }
return c}
func (s *PullSubmission) Participants() []string { participantSet := make(map[string]struct{}) participants := []string{}
addParticipant := func(did string) { if _, exists := participantSet[did]; !exists { participantSet[did] = struct{}{} participants = append(participants, did) } }
addParticipant(s.PullAt.Authority().String())
for _, c := range s.Comments { addParticipant(c.Did.String()) }
return participants}
func (s PullSubmission) CombinedPatch() string { if s.Combined == "" { return s.Patch }
return s.Combined}
func (s *PullSubmission) GetBlob() *lexutil.LexBlob { if !s.Blob.Ref.Defined() { return nil }
return &s.Blob}
func (s *PullSubmission) AsRecord() *tangled.RepoPull_Round { return &tangled.RepoPull_Round{ CreatedAt: s.Created.Format(time.RFC3339), PatchBlob: s.GetBlob(), }}
type Stack []*Pull
// position of this pull in the stackfunc (stack Stack) Position(pull *Pull) int { return slices.IndexFunc(stack, func(p *Pull) bool { return p.AtUri() == pull.AtUri() })}
// all pulls below this pull (including self) in this stack//// nil if this pull does not belong to this stackfunc (stack Stack) Below(pull *Pull) Stack { position := stack.Position(pull)
if position < 0 { return nil }
return stack[position:]}
// all pulls below this pull (excluding self) in this stackfunc (stack Stack) StrictlyBelow(pull *Pull) Stack { below := stack.Below(pull)
if len(below) > 0 { return below[1:] }
return nil}
// all pulls above this pull (including self) in this stackfunc (stack Stack) Above(pull *Pull) Stack { position := stack.Position(pull)
if position < 0 { return nil }
return stack[:position+1]}
// all pulls below this pull (excluding self) in this stackfunc (stack Stack) StrictlyAbove(pull *Pull) Stack { above := stack.Above(pull)
if len(above) > 0 { return above[:len(above)-1] }
return nil}
// the combined format-patches of all the newest submissions in this stackfunc (stack Stack) CombinedPatch() string { // go in reverse order because the bottom of the stack is the last element in the slice var combined strings.Builder for idx := range stack { pull := stack[len(stack)-1-idx] combined.WriteString(pull.LatestPatch()) combined.WriteString("\n") } return combined.String()}
// filter out PRs that are "active"//// PRs that are still open are activefunc (stack Stack) Mergeable() Stack { var mergeable Stack
for _, p := range stack { // stop at the first merged PR if p.State == PullMerged || p.State == PullClosed { break }
// skip over abandoned PRs if p.State != PullAbandoned { mergeable = append(mergeable, p) } }
return mergeable}
type BranchDeleteStatus struct { Repo *Repo Branch string}
func extractGzip(blob io.Reader) (string, error) { var b bytes.Buffer r, err := gzip.NewReader(blob) if err != nil { return "", err } defer r.Close()
const maxSize = 15 * 1024 * 1024 limitedReader := io.LimitReader(r, maxSize)
_, err = io.Copy(&b, limitedReader) if err != nil { return "", err }
return b.String(), nil}