diff --git a/api/tangled/cbor_gen.go b/api/tangled/cbor_gen.go index 39193aae..b74dd068 100644 --- a/api/tangled/cbor_gen.go +++ b/api/tangled/cbor_gen.go @@ -5806,7 +5806,7 @@ func (t *Repo) MarshalCBOR(w io.Writer) error { } cw := cbg.NewCborWriter(w) - fieldCount := 8 + fieldCount := 10 if t.Description == nil { fieldCount-- @@ -5824,6 +5824,14 @@ func (t *Repo) MarshalCBOR(w io.Writer) error { fieldCount-- } + if t.Topics == nil { + fieldCount-- + } + + if t.Website == nil { + fieldCount-- + } + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { return err } @@ -5961,6 +5969,42 @@ func (t *Repo) MarshalCBOR(w io.Writer) error { } } + // t.Topics ([]string) (slice) + if t.Topics != nil { + + if len("topics") > 1000000 { + return xerrors.Errorf("Value in field \"topics\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("topics"))); err != nil { + return err + } + if _, err := cw.WriteString(string("topics")); err != nil { + return err + } + + if len(t.Topics) > 8192 { + return xerrors.Errorf("Slice value in field t.Topics was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Topics))); err != nil { + return err + } + for _, v := range t.Topics { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + // t.Spindle (string) (string) if t.Spindle != nil { @@ -5993,6 +6037,38 @@ func (t *Repo) MarshalCBOR(w io.Writer) error { } } + // t.Website (string) (string) + if t.Website != nil { + + if len("website") > 1000000 { + return xerrors.Errorf("Value in field \"website\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("website"))); err != nil { + return err + } + if _, err := cw.WriteString(string("website")); err != nil { + return err + } + + if t.Website == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Website) > 1000000 { + return xerrors.Errorf("Value in field t.Website was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Website))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Website)); err != nil { + return err + } + } + } + // t.CreatedAt (string) (string) if len("createdAt") > 1000000 { return xerrors.Errorf("Value in field \"createdAt\" was too long") @@ -6185,6 +6261,46 @@ func (t *Repo) UnmarshalCBOR(r io.Reader) (err error) { t.Source = (*string)(&sval) } } + // t.Topics ([]string) (slice) + case "topics": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Topics: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Topics = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Topics[i] = string(sval) + } + + } + } // t.Spindle (string) (string) case "spindle": @@ -6206,6 +6322,27 @@ func (t *Repo) UnmarshalCBOR(r io.Reader) (err error) { t.Spindle = (*string)(&sval) } } + // t.Website (string) (string) + case "website": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Website = (*string)(&sval) + } + } // t.CreatedAt (string) (string) case "createdAt": diff --git a/api/tangled/tangledrepo.go b/api/tangled/tangledrepo.go index 23532228..493e0cdf 100644 --- a/api/tangled/tangledrepo.go +++ b/api/tangled/tangledrepo.go @@ -30,4 +30,8 @@ type Repo struct { Source *string `json:"source,omitempty" cborgen:"source,omitempty"` // spindle: CI runner to send jobs to and receive results from Spindle *string `json:"spindle,omitempty" cborgen:"spindle,omitempty"` + // topics: Topics related to the repo + Topics []string `json:"topics,omitempty" cborgen:"topics,omitempty"` + // website: Any URI related to the repo + Website *string `json:"website,omitempty" cborgen:"website,omitempty"` } diff --git a/appview/db/db.go b/appview/db/db.go index f61490c7..9b331964 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -1113,6 +1113,14 @@ func Make(ctx context.Context, dbPath string) (*DB, error) { return err }) + runMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error { + _, err := tx.Exec(` + alter table repos add column website text; + alter table repos add column topics text; + `) + return err + }) + return &DB{ db, logger, diff --git a/appview/db/notifications.go b/appview/db/notifications.go index 10f18ce1..429436c9 100644 --- a/appview/db/notifications.go +++ b/appview/db/notifications.go @@ -134,7 +134,7 @@ func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...fil select n.id, n.recipient_did, n.actor_did, n.type, n.entity_type, n.entity_id, n.read, n.created, n.repo_id, n.issue_id, n.pull_id, - r.id as r_id, r.did as r_did, r.name as r_name, r.description as r_description, + r.id as r_id, r.did as r_did, r.name as r_name, r.description as r_description, r.website as r_website, r.topics as r_topics, i.id as i_id, i.did as i_did, i.issue_id as i_issue_id, i.title as i_title, i.open as i_open, p.id as p_id, p.owner_did as p_owner_did, p.pull_id as p_pull_id, p.title as p_title, p.state as p_state from notifications n @@ -163,7 +163,7 @@ func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...fil var issue models.Issue var pull models.Pull var rId, iId, pId sql.NullInt64 - var rDid, rName, rDescription sql.NullString + var rDid, rName, rDescription, rWebsite, rTopicStr sql.NullString var iDid sql.NullString var iIssueId sql.NullInt64 var iTitle sql.NullString @@ -176,7 +176,7 @@ func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...fil err := rows.Scan( &n.ID, &n.RecipientDid, &n.ActorDid, &typeStr, &n.EntityType, &n.EntityId, &n.Read, &createdStr, &n.RepoId, &n.IssueId, &n.PullId, - &rId, &rDid, &rName, &rDescription, + &rId, &rDid, &rName, &rDescription, &rWebsite, &rTopicStr, &iId, &iDid, &iIssueId, &iTitle, &iOpen, &pId, &pOwnerDid, &pPullId, &pTitle, &pState, ) @@ -204,6 +204,12 @@ func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...fil if rDescription.Valid { repo.Description = rDescription.String } + if rWebsite.Valid { + repo.Website = rWebsite.String + } + if rTopicStr.Valid { + repo.Topics = strings.Fields(rTopicStr.String) + } nwe.Repo = &repo } diff --git a/appview/db/repos.go b/appview/db/repos.go index 27a88b66..5b2bcdd0 100644 --- a/appview/db/repos.go +++ b/appview/db/repos.go @@ -70,6 +70,8 @@ func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) { rkey, created, description, + website, + topics, source, spindle from @@ -89,7 +91,7 @@ func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) { for rows.Next() { var repo models.Repo var createdAt string - var description, source, spindle sql.NullString + var description, website, topicStr, source, spindle sql.NullString err := rows.Scan( &repo.Id, @@ -99,6 +101,8 @@ func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) { &repo.Rkey, &createdAt, &description, + &website, + &topicStr, &source, &spindle, ) @@ -112,6 +116,12 @@ func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) { if description.Valid { repo.Description = description.String } + if website.Valid { + repo.Website = website.String + } + if topicStr.Valid { + repo.Topics = strings.Fields(topicStr.String) + } if source.Valid { repo.Source = source.String } @@ -356,11 +366,13 @@ func CountRepos(e Execer, filters ...filter) (int64, error) { func GetRepoByAtUri(e Execer, atUri string) (*models.Repo, error) { var repo models.Repo var nullableDescription sql.NullString + var nullableWebsite sql.NullString + var nullableTopicStr sql.NullString - row := e.QueryRow(`select id, did, name, knot, created, rkey, description from repos where at_uri = ?`, atUri) + row := e.QueryRow(`select id, did, name, knot, created, rkey, description, website, topics from repos where at_uri = ?`, atUri) var createdAt string - if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription); err != nil { + if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr); err != nil { return nil, err } createdAtTime, _ := time.Parse(time.RFC3339, createdAt) @@ -368,19 +380,34 @@ func GetRepoByAtUri(e Execer, atUri string) (*models.Repo, error) { if nullableDescription.Valid { repo.Description = nullableDescription.String - } else { - repo.Description = "" + } + if nullableWebsite.Valid { + repo.Website = nullableWebsite.String + } + if nullableTopicStr.Valid { + repo.Topics = strings.Fields(nullableTopicStr.String) } return &repo, nil } +func PutRepo(tx *sql.Tx, repo models.Repo) error { + _, err := tx.Exec( + `update repos + set knot = ?, description = ?, website = ?, topics = ? + where did = ? and rkey = ? + `, + repo.Knot, repo.Description, repo.Website, repo.TopicStr(), repo.Did, repo.Rkey, + ) + return err +} + func AddRepo(tx *sql.Tx, repo *models.Repo) error { _, err := tx.Exec( `insert into repos - (did, name, knot, rkey, at_uri, description, source) - values (?, ?, ?, ?, ?, ?, ?)`, - repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Source, + (did, name, knot, rkey, at_uri, description, website, topics source) + values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Website, repo.TopicStr(), repo.Source, ) if err != nil { return fmt.Errorf("failed to insert repo: %w", err) @@ -416,7 +443,7 @@ func GetForksByDid(e Execer, did string) ([]models.Repo, error) { var repos []models.Repo rows, err := e.Query( - `select distinct r.id, r.did, r.name, r.knot, r.rkey, r.description, r.created, r.source + `select distinct r.id, r.did, r.name, r.knot, r.rkey, r.description, r.website, r.created, r.source from repos r left join collaborators c on r.at_uri = c.repo_at where (r.did = ? or c.subject_did = ?) @@ -434,9 +461,10 @@ func GetForksByDid(e Execer, did string) ([]models.Repo, error) { var repo models.Repo var createdAt string var nullableDescription sql.NullString + var nullableWebsite sql.NullString var nullableSource sql.NullString - err := rows.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) + err := rows.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &createdAt, &nullableSource) if err != nil { return nil, err } @@ -470,16 +498,18 @@ func GetForkByDid(e Execer, did string, name string) (*models.Repo, error) { var repo models.Repo var createdAt string var nullableDescription sql.NullString + var nullableWebsite sql.NullString + var nullableTopicStr sql.NullString var nullableSource sql.NullString row := e.QueryRow( - `select id, did, name, knot, rkey, description, created, source + `select id, did, name, knot, rkey, description, website, topics, created, source from repos where did = ? and name = ? and source is not null and source != ''`, did, name, ) - err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) + err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &createdAt, &nullableSource) if err != nil { return nil, err } @@ -488,6 +518,14 @@ func GetForkByDid(e Execer, did string, name string) (*models.Repo, error) { repo.Description = nullableDescription.String } + if nullableWebsite.Valid { + repo.Website = nullableWebsite.String + } + + if nullableTopicStr.Valid { + repo.Topics = strings.Fields(nullableTopicStr.String) + } + if nullableSource.Valid { repo.Source = nullableSource.String } diff --git a/appview/models/repo.go b/appview/models/repo.go index ee54b4f1..60d5ad5c 100644 --- a/appview/models/repo.go +++ b/appview/models/repo.go @@ -2,6 +2,7 @@ package models import ( "fmt" + "strings" "time" "github.com/bluesky-social/indigo/atproto/syntax" @@ -17,6 +18,8 @@ type Repo struct { Rkey string Created time.Time Description string + Website string + Topics []string Spindle string Labels []string @@ -28,7 +31,7 @@ type Repo struct { } func (r *Repo) AsRecord() tangled.Repo { - var source, spindle, description *string + var source, spindle, description, website *string if r.Source != "" { source = &r.Source @@ -42,10 +45,16 @@ func (r *Repo) AsRecord() tangled.Repo { description = &r.Description } + if r.Website != "" { + website = &r.Website + } + return tangled.Repo{ Knot: r.Knot, Name: r.Name, Description: description, + Website: website, + Topics: r.Topics, CreatedAt: r.Created.Format(time.RFC3339), Source: source, Spindle: spindle, @@ -62,6 +71,10 @@ func (r Repo) DidSlashRepo() string { return p } +func (r Repo) TopicStr() string { + return strings.Join(r.Topics, " ") +} + type RepoStats struct { Language string StarCount int diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go index faaa68f7..336aea74 100644 --- a/appview/pages/funcmap.go +++ b/appview/pages/funcmap.go @@ -246,6 +246,11 @@ func (p *Pages) funcMap() template.FuncMap { sanitized := p.rctx.SanitizeDescription(htmlString) return template.HTML(sanitized) }, + "trimUriScheme": func(text string) string { + text = strings.TrimPrefix(text, "https://") + text = strings.TrimPrefix(text, "http://") + return text + }, "isNil": func(t any) bool { // returns false for other "zero" values return t == nil diff --git a/appview/pages/pages.go b/appview/pages/pages.go index c46691b0..7771121d 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -640,18 +640,6 @@ func (p *Pages) RepoStarFragment(w io.Writer, params RepoStarFragmentParams) err return p.executePlain("repo/fragments/repoStar", w, params) } -type RepoDescriptionParams struct { - RepoInfo repoinfo.RepoInfo -} - -func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { - return p.executePlain("repo/fragments/editRepoDescription", w, params) -} - -func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { - return p.executePlain("repo/fragments/repoDescription", w, params) -} - type RepoIndexParams struct { LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo diff --git a/appview/pages/repoinfo/repoinfo.go b/appview/pages/repoinfo/repoinfo.go index 2d4550ec..30e3d6fd 100644 --- a/appview/pages/repoinfo/repoinfo.go +++ b/appview/pages/repoinfo/repoinfo.go @@ -54,6 +54,8 @@ type RepoInfo struct { OwnerDid string OwnerHandle string Description string + Website string + Topics []string Knot string Spindle string RepoAt syntax.ATURI diff --git a/appview/pages/templates/layouts/repobase.html b/appview/pages/templates/layouts/repobase.html index 3299d60c..91c08fd1 100644 --- a/appview/pages/templates/layouts/repobase.html +++ b/appview/pages/templates/layouts/repobase.html @@ -17,6 +17,9 @@ {{ template "user/fragments/picHandleLink" .RepoInfo.OwnerDid }} / {{ .RepoInfo.Name }} + {{ range $topic := .RepoInfo.Topics }} + {{ $topic }} + {{ end }}