Monorepo for Tangled
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381package models
import ( "bytes" "compress/gzip" "encoding/hex" "fmt" "io" "maps" "slices" "strings" "time"
"tangled.org/core/api/tangled" "tangled.org/core/appview/pages/markup/sanitizer" "tangled.org/core/patchutil"
"github.com/bluesky-social/indigo/atproto/syntax")
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 { ID int64 // appview-local PR id. Used for quick referencing OwnerDid syntax.DID Rkey syntax.RecordKey Cid syntax.CID RepoDid syntax.DID PullId int64
Title string Body string TargetBranch string SourceRepo syntax.DID SourceBranch *string Versions []PullVersion Created time.Time
State PullState
// optionally, populate this when querying for reverse mappings Labels LabelState Repo *Repo}
func (p *Pull) AtUri() syntax.ATURI { return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.OwnerDid, tangled.RepoPullNSID, p.Rkey))}
func (p *Pull) AsRecord() tangled.RepoPull { sourceRepo := p.SourceRepo.String() var sourceBranch string if p.SourceBranch != nil { sourceBranch = *p.SourceBranch } versions := make([]*tangled.RepoPull_Version, len(p.Versions)) for i, v := range p.Versions { var base *string if v.Base != "" { base = &v.Base } versions[i] = &tangled.RepoPull_Version{ Base: base, Head: v.Head, CreatedAt: v.Created.Format(time.RFC3339), } } return tangled.RepoPull{ Title: p.Title, Body: &p.Body, Target: &tangled.RepoPull_Target{ Repo: p.RepoDid.String(), Branch: p.TargetBranch, }, Source: &tangled.RepoPull_Source{ Repo: &sourceRepo, Branch: sourceBranch, }, Versions: versions, CreatedAt: p.Created.Format(time.RFC3339), }}
func (p *Pull) Validate() error { if len(p.Versions) == 0 { return fmt.Errorf("pull must have at least one version") }
if p.Title == "" { return fmt.Errorf("pull title is empty (required for non-format-patch pulls)") } if st := strings.TrimSpace(sanitizer.SanitizeDescription(p.Title)); st == "" { return fmt.Errorf("title is empty after HTML sanitization") }
for i, version := range p.Versions { if err := version.Validate(); err != nil { return fmt.Errorf("versions[%d]: %w", i, err) } } return nil}
func (v *PullVersion) Validate() error { if v.Base != "" && !IsHash(v.Base) { return fmt.Errorf("invalid base commit id: %q", v.Base) } if !IsHash(v.Head) { return fmt.Errorf("invalid head commit id: %q", v.Head) } return nil}
func PullFromRecord(did syntax.DID, rkey syntax.RecordKey, cid syntax.CID, record tangled.RepoPull, versions []PullVersion) (*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 sourceRepo syntax.DID var sourceBranch *string if record.Source != nil { 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) } sourceRepo = did } if record.Source.Branch != "" { sourceBranch = new(string) *sourceBranch = record.Source.Branch } }
return &Pull{ ID: -1, // uninitialized OwnerDid: did, Rkey: rkey, Cid: cid, RepoDid: targetRepoDid, PullId: 0, // uninitialized
Title: record.Title, Body: body, TargetBranch: targetBranch, SourceRepo: sourceRepo, SourceBranch: sourceBranch, Versions: versions, Created: created, State: PullOpen, // default to open }, nil}
func PullVersionFromRecord(idx int, record *tangled.RepoPull_Version) (PullVersion, error) { created, err := time.Parse(time.RFC3339, record.CreatedAt) if err != nil { return PullVersion{}, fmt.Errorf("invalid createdAt: %w", err) }
var base string if record.Base != nil { base = *record.Base } return PullVersion{ ID: idx, Base: base, Head: record.Head, Created: created, }, nil}
func PullVersionFromLegacy(idx int, record *tangled.RepoPull_Round, reader io.Reader) (PullVersion, error) { created, err := time.Parse(time.RFC3339, record.CreatedAt) if err != nil { return PullVersion{}, fmt.Errorf("invalid createdAt: %w", err) }
patch, err := extractGzip(reader) if err != nil { return PullVersion{}, fmt.Errorf("failed to extract gzip: %w", err) } if !patchutil.IsFormatPatch(patch) { return PullVersion{}, fmt.Errorf("only format-patch patch is supported") }
var sourceRev string patches, err := patchutil.ExtractPatches(patch) if err != nil { return PullVersion{}, fmt.Errorf("failed to extract patches: %w", err) } for _, part := range patches { sourceRev = part.SHA } if sourceRev == "" { return PullVersion{}, fmt.Errorf("source rev is missing") } return PullVersion{ ID: idx, Base: "", Head: sourceRev, Created: created, }, 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 PullVersion struct { ID int Head string // head commit ID Base string // base commit ID (for combined interdiff) Created time.Time
// reverse mappings Comments []Comment}
func (p *Pull) TotalComments() int { total := 0 for _, s := range p.Versions { total += len(s.Comments) } return total}
func (p *Pull) GetVersion(id int) (PullVersion, bool) { for _, version := range p.Versions { if version.ID == id { return version, true } } return PullVersion{}, false}
func (p *Pull) LatestVersionNumber() int { return len(p.Versions) - 1}
func (p *Pull) LatestVersion() PullVersion { return p.Versions[p.LatestVersionNumber()]}
func (p *Pull) LatestSha() string { return p.LatestVersion().Head}
func (p *Pull) IsForkBased() bool { return p.RepoDid != p.SourceRepo}
func (p *Pull) Participants() []syntax.DID { participants := make(map[syntax.DID]struct{})
for _, v := range p.Versions { for _, sp := range v.Participants() { participants[sp] = struct{}{} } }
return slices.Collect(maps.Keys(participants))}
func (s *PullVersion) Participants() []syntax.DID { participants := make(map[syntax.DID]struct{})
for _, c := range s.Comments { participants[c.Did] = struct{}{} }
return slices.Collect(maps.Keys(participants))}
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}
func IsHash(s string) bool { switch len(s) { case 40: // SHA1 case 64: // SHA2 default: return false } _, err := hex.DecodeString(s) return err == nil}