forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

+2
.tangled/workflows/build.yml
··· 2 2 - event: ["push", "pull_request"] 3 3 branch: ["master"] 4 4 5 + engine: nixery 6 + 5 7 dependencies: 6 8 nixpkgs: 7 9 - go
+2
.tangled/workflows/fmt.yml
··· 2 2 - event: ["push", "pull_request"] 3 3 branch: ["master"] 4 4 5 + engine: nixery 6 + 5 7 steps: 6 8 - name: "Check formatting" 7 9 command: |
+2
.tangled/workflows/test.yml
··· 2 2 - event: ["push", "pull_request"] 3 3 branch: ["master"] 4 4 5 + engine: nixery 6 + 5 7 dependencies: 6 8 nixpkgs: 7 9 - go
+2 -122
api/tangled/cbor_gen.go
··· 5512 5512 } 5513 5513 5514 5514 cw := cbg.NewCborWriter(w) 5515 - fieldCount := 7 5515 + fieldCount := 6 5516 5516 5517 5517 if t.Body == nil { 5518 5518 fieldCount-- ··· 5642 5642 return err 5643 5643 } 5644 5644 5645 - // t.IssueId (int64) (int64) 5646 - if len("issueId") > 1000000 { 5647 - return xerrors.Errorf("Value in field \"issueId\" was too long") 5648 - } 5649 - 5650 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("issueId"))); err != nil { 5651 - return err 5652 - } 5653 - if _, err := cw.WriteString(string("issueId")); err != nil { 5654 - return err 5655 - } 5656 - 5657 - if t.IssueId >= 0 { 5658 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.IssueId)); err != nil { 5659 - return err 5660 - } 5661 - } else { 5662 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.IssueId-1)); err != nil { 5663 - return err 5664 - } 5665 - } 5666 - 5667 5645 // t.CreatedAt (string) (string) 5668 5646 if len("createdAt") > 1000000 { 5669 5647 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 5795 5773 5796 5774 t.Title = string(sval) 5797 5775 } 5798 - // t.IssueId (int64) (int64) 5799 - case "issueId": 5800 - { 5801 - maj, extra, err := cr.ReadHeader() 5802 - if err != nil { 5803 - return err 5804 - } 5805 - var extraI int64 5806 - switch maj { 5807 - case cbg.MajUnsignedInt: 5808 - extraI = int64(extra) 5809 - if extraI < 0 { 5810 - return fmt.Errorf("int64 positive overflow") 5811 - } 5812 - case cbg.MajNegativeInt: 5813 - extraI = int64(extra) 5814 - if extraI < 0 { 5815 - return fmt.Errorf("int64 negative overflow") 5816 - } 5817 - extraI = -1 - extraI 5818 - default: 5819 - return fmt.Errorf("wrong type for int64 field: %d", maj) 5820 - } 5821 - 5822 - t.IssueId = int64(extraI) 5823 - } 5824 5776 // t.CreatedAt (string) (string) 5825 5777 case "createdAt": 5826 5778 ··· 5850 5802 } 5851 5803 5852 5804 cw := cbg.NewCborWriter(w) 5853 - fieldCount := 7 5854 - 5855 - if t.CommentId == nil { 5856 - fieldCount-- 5857 - } 5805 + fieldCount := 6 5858 5806 5859 5807 if t.Owner == nil { 5860 5808 fieldCount-- ··· 5997 5945 } 5998 5946 } 5999 5947 6000 - // t.CommentId (int64) (int64) 6001 - if t.CommentId != nil { 6002 - 6003 - if len("commentId") > 1000000 { 6004 - return xerrors.Errorf("Value in field \"commentId\" was too long") 6005 - } 6006 - 6007 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("commentId"))); err != nil { 6008 - return err 6009 - } 6010 - if _, err := cw.WriteString(string("commentId")); err != nil { 6011 - return err 6012 - } 6013 - 6014 - if t.CommentId == nil { 6015 - if _, err := cw.Write(cbg.CborNull); err != nil { 6016 - return err 6017 - } 6018 - } else { 6019 - if *t.CommentId >= 0 { 6020 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.CommentId)); err != nil { 6021 - return err 6022 - } 6023 - } else { 6024 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.CommentId-1)); err != nil { 6025 - return err 6026 - } 6027 - } 6028 - } 6029 - 6030 - } 6031 - 6032 5948 // t.CreatedAt (string) (string) 6033 5949 if len("createdAt") > 1000000 { 6034 5950 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 6168 6084 } 6169 6085 6170 6086 t.Owner = (*string)(&sval) 6171 - } 6172 - } 6173 - // t.CommentId (int64) (int64) 6174 - case "commentId": 6175 - { 6176 - 6177 - b, err := cr.ReadByte() 6178 - if err != nil { 6179 - return err 6180 - } 6181 - if b != cbg.CborNull[0] { 6182 - if err := cr.UnreadByte(); err != nil { 6183 - return err 6184 - } 6185 - maj, extra, err := cr.ReadHeader() 6186 - if err != nil { 6187 - return err 6188 - } 6189 - var extraI int64 6190 - switch maj { 6191 - case cbg.MajUnsignedInt: 6192 - extraI = int64(extra) 6193 - if extraI < 0 { 6194 - return fmt.Errorf("int64 positive overflow") 6195 - } 6196 - case cbg.MajNegativeInt: 6197 - extraI = int64(extra) 6198 - if extraI < 0 { 6199 - return fmt.Errorf("int64 negative overflow") 6200 - } 6201 - extraI = -1 - extraI 6202 - default: 6203 - return fmt.Errorf("wrong type for int64 field: %d", maj) 6204 - } 6205 - 6206 - t.CommentId = (*int64)(&extraI) 6207 6087 } 6208 6088 } 6209 6089 // t.CreatedAt (string) (string)
-1
api/tangled/issuecomment.go
··· 19 19 type RepoIssueComment struct { 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue.comment" cborgen:"$type,const=sh.tangled.repo.issue.comment"` 21 21 Body string `json:"body" cborgen:"body"` 22 - CommentId *int64 `json:"commentId,omitempty" cborgen:"commentId,omitempty"` 23 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 24 23 Issue string `json:"issue" cborgen:"issue"` 25 24 Owner *string `json:"owner,omitempty" cborgen:"owner,omitempty"`
-1
api/tangled/repoissue.go
··· 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue" cborgen:"$type,const=sh.tangled.repo.issue"` 21 21 Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 - IssueId int64 `json:"issueId" cborgen:"issueId"` 24 23 Owner string `json:"owner" cborgen:"owner"` 25 24 Repo string `json:"repo" cborgen:"repo"` 26 25 Title string `json:"title" cborgen:"title"`
+15
appview/db/db.go
··· 470 470 id integer primary key autoincrement, 471 471 name text unique 472 472 ); 473 + 474 + -- indexes for better star query performance 475 + create index if not exists idx_stars_created on stars(created); 476 + create index if not exists idx_stars_repo_at_created on stars(repo_at, created); 473 477 `) 474 478 if err != nil { 475 479 return nil, err ··· 660 664 661 665 // rename new table 662 666 _, err = tx.Exec(`alter table collaborators_new rename to collaborators`) 667 + return err 668 + }) 669 + 670 + runMigration(conn, "add-rkey-to-issues", func(tx *sql.Tx) error { 671 + _, err := tx.Exec(` 672 + alter table issues add column rkey text not null default ''; 673 + 674 + -- get last url section from issue_at and save to rkey column 675 + update issues 676 + set rkey = replace(issue_at, rtrim(issue_at, replace(issue_at, '/', '')), ''); 677 + `) 663 678 return err 664 679 }) 665 680
+208 -17
appview/db/issues.go
··· 2 2 3 3 import ( 4 4 "database/sql" 5 + "fmt" 6 + mathrand "math/rand/v2" 7 + "strings" 5 8 "time" 6 9 7 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 + "tangled.sh/tangled.sh/core/api/tangled" 8 12 "tangled.sh/tangled.sh/core/appview/pagination" 9 13 ) 10 14 ··· 13 17 RepoAt syntax.ATURI 14 18 OwnerDid string 15 19 IssueId int 16 - IssueAt string 20 + Rkey string 17 21 Created time.Time 18 22 Title string 19 23 Body string ··· 42 46 Edited *time.Time 43 47 } 44 48 49 + func (i *Issue) AtUri() syntax.ATURI { 50 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", i.OwnerDid, tangled.RepoIssueNSID, i.Rkey)) 51 + } 52 + 53 + func IssueFromRecord(did, rkey string, record tangled.RepoIssue) Issue { 54 + created, err := time.Parse(time.RFC3339, record.CreatedAt) 55 + if err != nil { 56 + created = time.Now() 57 + } 58 + 59 + body := "" 60 + if record.Body != nil { 61 + body = *record.Body 62 + } 63 + 64 + return Issue{ 65 + RepoAt: syntax.ATURI(record.Repo), 66 + OwnerDid: record.Owner, 67 + Rkey: rkey, 68 + Created: created, 69 + Title: record.Title, 70 + Body: body, 71 + Open: true, // new issues are open by default 72 + } 73 + } 74 + 75 + func ResolveIssueFromAtUri(e Execer, issueUri syntax.ATURI) (syntax.ATURI, int, error) { 76 + ownerDid := issueUri.Authority().String() 77 + issueRkey := issueUri.RecordKey().String() 78 + 79 + var repoAt string 80 + var issueId int 81 + 82 + query := `select repo_at, issue_id from issues where owner_did = ? and rkey = ?` 83 + err := e.QueryRow(query, ownerDid, issueRkey).Scan(&repoAt, &issueId) 84 + if err != nil { 85 + return "", 0, err 86 + } 87 + 88 + return syntax.ATURI(repoAt), issueId, nil 89 + } 90 + 91 + func IssueCommentFromRecord(e Execer, did, rkey string, record tangled.RepoIssueComment) (Comment, error) { 92 + created, err := time.Parse(time.RFC3339, record.CreatedAt) 93 + if err != nil { 94 + created = time.Now() 95 + } 96 + 97 + ownerDid := did 98 + if record.Owner != nil { 99 + ownerDid = *record.Owner 100 + } 101 + 102 + issueUri, err := syntax.ParseATURI(record.Issue) 103 + if err != nil { 104 + return Comment{}, err 105 + } 106 + 107 + repoAt, issueId, err := ResolveIssueFromAtUri(e, issueUri) 108 + if err != nil { 109 + return Comment{}, err 110 + } 111 + 112 + comment := Comment{ 113 + OwnerDid: ownerDid, 114 + RepoAt: repoAt, 115 + Rkey: rkey, 116 + Body: record.Body, 117 + Issue: issueId, 118 + CommentId: mathrand.IntN(1000000), 119 + Created: &created, 120 + } 121 + 122 + return comment, nil 123 + } 124 + 45 125 func NewIssue(tx *sql.Tx, issue *Issue) error { 46 126 defer tx.Rollback() 47 127 ··· 67 147 issue.IssueId = nextId 68 148 69 149 res, err := tx.Exec(` 70 - insert into issues (repo_at, owner_did, issue_id, title, body) 71 - values (?, ?, ?, ?, ?) 72 - `, issue.RepoAt, issue.OwnerDid, issue.IssueId, issue.Title, issue.Body) 150 + insert into issues (repo_at, owner_did, rkey, issue_at, issue_id, title, body) 151 + values (?, ?, ?, ?, ?, ?, ?) 152 + `, issue.RepoAt, issue.OwnerDid, issue.Rkey, issue.AtUri(), issue.IssueId, issue.Title, issue.Body) 73 153 if err != nil { 74 154 return err 75 155 } ··· 87 167 return nil 88 168 } 89 169 90 - func SetIssueAt(e Execer, repoAt syntax.ATURI, issueId int, issueAt string) error { 91 - _, err := e.Exec(`update issues set issue_at = ? where repo_at = ? and issue_id = ?`, issueAt, repoAt, issueId) 92 - return err 93 - } 94 - 95 170 func GetIssueAt(e Execer, repoAt syntax.ATURI, issueId int) (string, error) { 96 171 var issueAt string 97 172 err := e.QueryRow(`select issue_at from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&issueAt) ··· 104 179 return ownerDid, err 105 180 } 106 181 107 - func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 182 + func GetIssuesPaginated(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 108 183 var issues []Issue 109 184 openValue := 0 110 185 if isOpen { ··· 117 192 select 118 193 i.id, 119 194 i.owner_did, 195 + i.rkey, 120 196 i.issue_id, 121 197 i.created, 122 198 i.title, ··· 136 212 select 137 213 id, 138 214 owner_did, 215 + rkey, 139 216 issue_id, 140 217 created, 141 218 title, 142 219 body, 143 220 open, 144 221 comment_count 145 - from 222 + from 146 223 numbered_issue 147 - where 224 + where 148 225 row_num between ? and ?`, 149 226 repoAt, openValue, page.Offset+1, page.Offset+page.Limit) 150 227 if err != nil { ··· 156 233 var issue Issue 157 234 var createdAt string 158 235 var metadata IssueMetadata 159 - err := rows.Scan(&issue.ID, &issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount) 236 + err := rows.Scan(&issue.ID, &issue.OwnerDid, &issue.Rkey, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &metadata.CommentCount) 160 237 if err != nil { 161 238 return nil, err 162 239 } ··· 178 255 return issues, nil 179 256 } 180 257 258 + func GetIssuesWithLimit(e Execer, limit int, filters ...filter) ([]Issue, error) { 259 + issues := make([]Issue, 0, limit) 260 + 261 + var conditions []string 262 + var args []any 263 + for _, filter := range filters { 264 + conditions = append(conditions, filter.Condition()) 265 + args = append(args, filter.Arg()...) 266 + } 267 + 268 + whereClause := "" 269 + if conditions != nil { 270 + whereClause = " where " + strings.Join(conditions, " and ") 271 + } 272 + limitClause := "" 273 + if limit != 0 { 274 + limitClause = fmt.Sprintf(" limit %d ", limit) 275 + } 276 + 277 + query := fmt.Sprintf( 278 + `select 279 + i.id, 280 + i.owner_did, 281 + i.repo_at, 282 + i.issue_id, 283 + i.created, 284 + i.title, 285 + i.body, 286 + i.open 287 + from 288 + issues i 289 + %s 290 + order by 291 + i.created desc 292 + %s`, 293 + whereClause, limitClause) 294 + 295 + rows, err := e.Query(query, args...) 296 + if err != nil { 297 + return nil, err 298 + } 299 + defer rows.Close() 300 + 301 + for rows.Next() { 302 + var issue Issue 303 + var issueCreatedAt string 304 + err := rows.Scan( 305 + &issue.ID, 306 + &issue.OwnerDid, 307 + &issue.RepoAt, 308 + &issue.IssueId, 309 + &issueCreatedAt, 310 + &issue.Title, 311 + &issue.Body, 312 + &issue.Open, 313 + ) 314 + if err != nil { 315 + return nil, err 316 + } 317 + 318 + issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt) 319 + if err != nil { 320 + return nil, err 321 + } 322 + issue.Created = issueCreatedTime 323 + 324 + issues = append(issues, issue) 325 + } 326 + 327 + if err := rows.Err(); err != nil { 328 + return nil, err 329 + } 330 + 331 + return issues, nil 332 + } 333 + 334 + func GetIssues(e Execer, filters ...filter) ([]Issue, error) { 335 + return GetIssuesWithLimit(e, 0, filters...) 336 + } 337 + 181 338 // timeframe here is directly passed into the sql query filter, and any 182 339 // timeframe in the past should be negative; e.g.: "-3 months" 183 340 func GetIssuesByOwnerDid(e Execer, ownerDid string, timeframe string) ([]Issue, error) { ··· 187 344 `select 188 345 i.id, 189 346 i.owner_did, 347 + i.rkey, 190 348 i.repo_at, 191 349 i.issue_id, 192 350 i.created, ··· 219 377 err := rows.Scan( 220 378 &issue.ID, 221 379 &issue.OwnerDid, 380 + &issue.Rkey, 222 381 &issue.RepoAt, 223 382 &issue.IssueId, 224 383 &issueCreatedAt, ··· 262 421 } 263 422 264 423 func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) { 265 - query := `select id, owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?` 424 + query := `select id, owner_did, rkey, created, title, body, open from issues where repo_at = ? and issue_id = ?` 266 425 row := e.QueryRow(query, repoAt, issueId) 267 426 268 427 var issue Issue 269 428 var createdAt string 270 - err := row.Scan(&issue.ID, &issue.OwnerDid, &createdAt, &issue.Title, &issue.Body, &issue.Open) 429 + err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.Rkey, &createdAt, &issue.Title, &issue.Body, &issue.Open) 271 430 if err != nil { 272 431 return nil, err 273 432 } ··· 282 441 } 283 442 284 443 func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) { 285 - query := `select id, owner_did, issue_id, created, title, body, open, issue_at from issues where repo_at = ? and issue_id = ?` 444 + query := `select id, owner_did, rkey, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?` 286 445 row := e.QueryRow(query, repoAt, issueId) 287 446 288 447 var issue Issue 289 448 var createdAt string 290 - err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open, &issue.IssueAt) 449 + err := row.Scan(&issue.ID, &issue.OwnerDid, &issue.Rkey, &issue.IssueId, &createdAt, &issue.Title, &issue.Body, &issue.Open) 291 450 if err != nil { 292 451 return nil, nil, err 293 452 } ··· 464 623 deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 465 624 where repo_at = ? and issue_id = ? and comment_id = ? 466 625 `, repoAt, issueId, commentId) 626 + return err 627 + } 628 + 629 + func UpdateCommentByRkey(e Execer, ownerDid, rkey, newBody string) error { 630 + _, err := e.Exec( 631 + ` 632 + update comments 633 + set body = ?, 634 + edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 635 + where owner_did = ? and rkey = ? 636 + `, newBody, ownerDid, rkey) 637 + return err 638 + } 639 + 640 + func DeleteCommentByRkey(e Execer, ownerDid, rkey string) error { 641 + _, err := e.Exec( 642 + ` 643 + update comments 644 + set body = "", 645 + deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 646 + where owner_did = ? and rkey = ? 647 + `, ownerDid, rkey) 648 + return err 649 + } 650 + 651 + func UpdateIssueByRkey(e Execer, ownerDid, rkey, title, body string) error { 652 + _, err := e.Exec(`update issues set title = ?, body = ? where owner_did = ? and rkey = ?`, title, body, ownerDid, rkey) 653 + return err 654 + } 655 + 656 + func DeleteIssueByRkey(e Execer, ownerDid, rkey string) error { 657 + _, err := e.Exec(`delete from issues where owner_did = ? and rkey = ?`, ownerDid, rkey) 467 658 return err 468 659 } 469 660
+22 -3
appview/db/pulls.go
··· 310 310 return pullId - 1, err 311 311 } 312 312 313 - func GetPulls(e Execer, filters ...filter) ([]*Pull, error) { 313 + func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*Pull, error) { 314 314 pulls := make(map[int]*Pull) 315 315 316 316 var conditions []string ··· 323 323 whereClause := "" 324 324 if conditions != nil { 325 325 whereClause = " where " + strings.Join(conditions, " and ") 326 + } 327 + limitClause := "" 328 + if limit != 0 { 329 + limitClause = fmt.Sprintf(" limit %d ", limit) 326 330 } 327 331 328 332 query := fmt.Sprintf(` ··· 344 348 from 345 349 pulls 346 350 %s 347 - `, whereClause) 351 + order by 352 + created desc 353 + %s 354 + `, whereClause, limitClause) 348 355 349 356 rows, err := e.Query(query, args...) 350 357 if err != nil { ··· 412 419 inClause := strings.TrimSuffix(strings.Repeat("?, ", len(pulls)), ", ") 413 420 submissionsQuery := fmt.Sprintf(` 414 421 select 415 - id, pull_id, round_number, patch, source_rev 422 + id, pull_id, round_number, patch, created, source_rev 416 423 from 417 424 pull_submissions 418 425 where ··· 438 445 for submissionsRows.Next() { 439 446 var s PullSubmission 440 447 var sourceRev sql.NullString 448 + var createdAt string 441 449 err := submissionsRows.Scan( 442 450 &s.ID, 443 451 &s.PullId, 444 452 &s.RoundNumber, 445 453 &s.Patch, 454 + &createdAt, 446 455 &sourceRev, 447 456 ) 448 457 if err != nil { 449 458 return nil, err 450 459 } 460 + 461 + createdTime, err := time.Parse(time.RFC3339, createdAt) 462 + if err != nil { 463 + return nil, err 464 + } 465 + s.Created = createdTime 451 466 452 467 if sourceRev.Valid { 453 468 s.SourceRev = sourceRev.String ··· 511 526 }) 512 527 513 528 return orderedByPullId, nil 529 + } 530 + 531 + func GetPulls(e Execer, filters ...filter) ([]*Pull, error) { 532 + return GetPullsWithLimit(e, 0, filters...) 514 533 } 515 534 516 535 func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) {
+9 -10
appview/db/repos.go
··· 19 19 Knot string 20 20 Rkey string 21 21 Created time.Time 22 - AtUri string 23 22 Description string 24 23 Spindle string 25 24 ··· 391 390 var description, spindle sql.NullString 392 391 393 392 row := e.QueryRow(` 394 - select did, name, knot, created, at_uri, description, spindle 393 + select did, name, knot, created, description, spindle, rkey 395 394 from repos 396 395 where did = ? and name = ? 397 396 `, ··· 400 399 ) 401 400 402 401 var createdAt string 403 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &description, &spindle); err != nil { 402 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &description, &spindle, &repo.Rkey); err != nil { 404 403 return nil, err 405 404 } 406 405 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 421 420 var repo Repo 422 421 var nullableDescription sql.NullString 423 422 424 - row := e.QueryRow(`select did, name, knot, created, at_uri, description from repos where at_uri = ?`, atUri) 423 + row := e.QueryRow(`select did, name, knot, created, rkey, description from repos where at_uri = ?`, atUri) 425 424 426 425 var createdAt string 427 - if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri, &nullableDescription); err != nil { 426 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription); err != nil { 428 427 return nil, err 429 428 } 430 429 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 444 443 `insert into repos 445 444 (did, name, knot, rkey, at_uri, description, source) 446 445 values (?, ?, ?, ?, ?, ?, ?)`, 447 - repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri, repo.Description, repo.Source, 446 + repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Source, 448 447 ) 449 448 return err 450 449 } ··· 467 466 var repos []Repo 468 467 469 468 rows, err := e.Query( 470 - `select did, name, knot, rkey, description, created, at_uri, source 469 + `select did, name, knot, rkey, description, created, source 471 470 from repos 472 471 where did = ? and source is not null and source != '' 473 472 order by created desc`, ··· 484 483 var nullableDescription sql.NullString 485 484 var nullableSource sql.NullString 486 485 487 - err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 486 + err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) 488 487 if err != nil { 489 488 return nil, err 490 489 } ··· 521 520 var nullableSource sql.NullString 522 521 523 522 row := e.QueryRow( 524 - `select did, name, knot, rkey, description, created, at_uri, source 523 + `select did, name, knot, rkey, description, created, source 525 524 from repos 526 525 where did = ? and name = ? and source is not null and source != ''`, 527 526 did, name, 528 527 ) 529 528 530 - err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &repo.AtUri, &nullableSource) 529 + err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &createdAt, &nullableSource) 531 530 if err != nil { 532 531 return nil, err 533 532 }
+73 -6
appview/db/star.go
··· 47 47 // Get a star record 48 48 func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) { 49 49 query := ` 50 - select starred_by_did, repo_at, created, rkey 50 + select starred_by_did, repo_at, created, rkey 51 51 from stars 52 52 where starred_by_did = ? and repo_at = ?` 53 53 row := e.QueryRow(query, starredByDid, repoAt) ··· 119 119 } 120 120 121 121 repoQuery := fmt.Sprintf( 122 - `select starred_by_did, repo_at, created, rkey 122 + `select starred_by_did, repo_at, created, rkey 123 123 from stars 124 124 %s 125 125 order by created desc ··· 187 187 var stars []Star 188 188 189 189 rows, err := e.Query(` 190 - select 190 + select 191 191 s.starred_by_did, 192 192 s.repo_at, 193 193 s.rkey, ··· 196 196 r.name, 197 197 r.knot, 198 198 r.rkey, 199 - r.created, 200 - r.at_uri 199 + r.created 201 200 from stars s 202 201 join repos r on s.repo_at = r.at_uri 203 202 `) ··· 222 221 &repo.Knot, 223 222 &repo.Rkey, 224 223 &repoCreatedAt, 225 - &repo.AtUri, 226 224 ); err != nil { 227 225 return nil, err 228 226 } ··· 246 244 247 245 return stars, nil 248 246 } 247 + 248 + // GetTopStarredReposLastWeek returns the top 8 most starred repositories from the last week 249 + func GetTopStarredReposLastWeek(e Execer) ([]Repo, error) { 250 + // first, get the top repo URIs by star count from the last week 251 + query := ` 252 + with recent_starred_repos as ( 253 + select distinct repo_at 254 + from stars 255 + where created >= datetime('now', '-7 days') 256 + ), 257 + repo_star_counts as ( 258 + select 259 + s.repo_at, 260 + count(*) as star_count 261 + from stars s 262 + join recent_starred_repos rsr on s.repo_at = rsr.repo_at 263 + group by s.repo_at 264 + ) 265 + select rsc.repo_at 266 + from repo_star_counts rsc 267 + order by rsc.star_count desc 268 + limit 8 269 + ` 270 + 271 + rows, err := e.Query(query) 272 + if err != nil { 273 + return nil, err 274 + } 275 + defer rows.Close() 276 + 277 + var repoUris []string 278 + for rows.Next() { 279 + var repoUri string 280 + err := rows.Scan(&repoUri) 281 + if err != nil { 282 + return nil, err 283 + } 284 + repoUris = append(repoUris, repoUri) 285 + } 286 + 287 + if err := rows.Err(); err != nil { 288 + return nil, err 289 + } 290 + 291 + if len(repoUris) == 0 { 292 + return []Repo{}, nil 293 + } 294 + 295 + // get full repo data 296 + repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris)) 297 + if err != nil { 298 + return nil, err 299 + } 300 + 301 + // sort repos by the original trending order 302 + repoMap := make(map[string]Repo) 303 + for _, repo := range repos { 304 + repoMap[repo.RepoAt().String()] = repo 305 + } 306 + 307 + orderedRepos := make([]Repo, 0, len(repoUris)) 308 + for _, uri := range repoUris { 309 + if repo, exists := repoMap[uri]; exists { 310 + orderedRepos = append(orderedRepos, repo) 311 + } 312 + } 313 + 314 + return orderedRepos, nil 315 + }
+12 -11
appview/db/strings.go
··· 50 50 func (s String) Validate() error { 51 51 var err error 52 52 53 - if !strings.Contains(s.Filename, ".") { 54 - err = errors.Join(err, fmt.Errorf("missing filename extension")) 55 - } 56 - 57 - if strings.HasSuffix(s.Filename, ".") { 58 - err = errors.Join(err, fmt.Errorf("filename ends with `.`")) 59 - } 60 - 61 53 if utf8.RuneCountInString(s.Filename) > 140 { 62 54 err = errors.Join(err, fmt.Errorf("filename too long")) 63 55 } ··· 113 105 filename = excluded.filename, 114 106 description = excluded.description, 115 107 content = excluded.content, 116 - edited = case 108 + edited = case 117 109 when 118 110 strings.content != excluded.content 119 111 or strings.filename != excluded.filename ··· 131 123 return err 132 124 } 133 125 134 - func GetStrings(e Execer, filters ...filter) ([]String, error) { 126 + func GetStrings(e Execer, limit int, filters ...filter) ([]String, error) { 135 127 var all []String 136 128 137 129 var conditions []string ··· 146 138 whereClause = " where " + strings.Join(conditions, " and ") 147 139 } 148 140 141 + limitClause := "" 142 + if limit != 0 { 143 + limitClause = fmt.Sprintf(" limit %d ", limit) 144 + } 145 + 149 146 query := fmt.Sprintf(`select 150 147 did, 151 148 rkey, ··· 154 151 content, 155 152 created, 156 153 edited 157 - from strings %s`, 154 + from strings 155 + %s 156 + order by created desc 157 + %s`, 158 158 whereClause, 159 + limitClause, 159 160 ) 160 161 161 162 rows, err := e.Query(query, args...)
+179 -6
appview/ingester.go
··· 5 5 "encoding/json" 6 6 "fmt" 7 7 "log/slog" 8 + "strings" 8 9 "time" 9 10 10 11 "github.com/bluesky-social/indigo/atproto/syntax" ··· 14 15 "tangled.sh/tangled.sh/core/api/tangled" 15 16 "tangled.sh/tangled.sh/core/appview/config" 16 17 "tangled.sh/tangled.sh/core/appview/db" 18 + "tangled.sh/tangled.sh/core/appview/pages/markup" 17 19 "tangled.sh/tangled.sh/core/appview/spindleverify" 18 20 "tangled.sh/tangled.sh/core/idresolver" 19 21 "tangled.sh/tangled.sh/core/rbac" ··· 61 63 case tangled.ActorProfileNSID: 62 64 err = i.ingestProfile(e) 63 65 case tangled.SpindleMemberNSID: 64 - err = i.ingestSpindleMember(e) 66 + err = i.ingestSpindleMember(ctx, e) 65 67 case tangled.SpindleNSID: 66 - err = i.ingestSpindle(e) 68 + err = i.ingestSpindle(ctx, e) 67 69 case tangled.StringNSID: 68 70 err = i.ingestString(e) 71 + case tangled.RepoIssueNSID: 72 + err = i.ingestIssue(ctx, e) 73 + case tangled.RepoIssueCommentNSID: 74 + err = i.ingestIssueComment(e) 69 75 } 70 76 l = i.Logger.With("nsid", e.Commit.Collection) 71 77 } ··· 336 342 return nil 337 343 } 338 344 339 - func (i *Ingester) ingestSpindleMember(e *models.Event) error { 345 + func (i *Ingester) ingestSpindleMember(ctx context.Context, e *models.Event) error { 340 346 did := e.Did 341 347 var err error 342 348 ··· 359 365 return fmt.Errorf("failed to enforce permissions: %w", err) 360 366 } 361 367 362 - memberId, err := i.IdResolver.ResolveIdent(context.Background(), record.Subject) 368 + memberId, err := i.IdResolver.ResolveIdent(ctx, record.Subject) 363 369 if err != nil { 364 370 return err 365 371 } ··· 442 448 return nil 443 449 } 444 450 445 - func (i *Ingester) ingestSpindle(e *models.Event) error { 451 + func (i *Ingester) ingestSpindle(ctx context.Context, e *models.Event) error { 446 452 did := e.Did 447 453 var err error 448 454 ··· 475 481 return err 476 482 } 477 483 478 - err = spindleverify.RunVerification(context.Background(), instance, did, i.Config.Core.Dev) 484 + err = spindleverify.RunVerification(ctx, instance, did, i.Config.Core.Dev) 479 485 if err != nil { 480 486 l.Error("failed to add spindle to db", "err", err, "instance", instance) 481 487 return err ··· 609 615 610 616 return nil 611 617 } 618 + 619 + func (i *Ingester) ingestIssue(ctx context.Context, e *models.Event) error { 620 + did := e.Did 621 + rkey := e.Commit.RKey 622 + 623 + var err error 624 + 625 + l := i.Logger.With("handler", "ingestIssue", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 626 + l.Info("ingesting record") 627 + 628 + ddb, ok := i.Db.Execer.(*db.DB) 629 + if !ok { 630 + return fmt.Errorf("failed to index issue record, invalid db cast") 631 + } 632 + 633 + switch e.Commit.Operation { 634 + case models.CommitOperationCreate: 635 + raw := json.RawMessage(e.Commit.Record) 636 + record := tangled.RepoIssue{} 637 + err = json.Unmarshal(raw, &record) 638 + if err != nil { 639 + l.Error("invalid record", "err", err) 640 + return err 641 + } 642 + 643 + issue := db.IssueFromRecord(did, rkey, record) 644 + 645 + sanitizer := markup.NewSanitizer() 646 + if st := strings.TrimSpace(sanitizer.SanitizeDescription(issue.Title)); st == "" { 647 + return fmt.Errorf("title is empty after HTML sanitization") 648 + } 649 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(issue.Body)); sb == "" { 650 + return fmt.Errorf("body is empty after HTML sanitization") 651 + } 652 + 653 + tx, err := ddb.BeginTx(ctx, nil) 654 + if err != nil { 655 + l.Error("failed to begin transaction", "err", err) 656 + return err 657 + } 658 + 659 + err = db.NewIssue(tx, &issue) 660 + if err != nil { 661 + l.Error("failed to create issue", "err", err) 662 + return err 663 + } 664 + 665 + return nil 666 + 667 + case models.CommitOperationUpdate: 668 + raw := json.RawMessage(e.Commit.Record) 669 + record := tangled.RepoIssue{} 670 + err = json.Unmarshal(raw, &record) 671 + if err != nil { 672 + l.Error("invalid record", "err", err) 673 + return err 674 + } 675 + 676 + body := "" 677 + if record.Body != nil { 678 + body = *record.Body 679 + } 680 + 681 + sanitizer := markup.NewSanitizer() 682 + if st := strings.TrimSpace(sanitizer.SanitizeDescription(record.Title)); st == "" { 683 + return fmt.Errorf("title is empty after HTML sanitization") 684 + } 685 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(body)); sb == "" { 686 + return fmt.Errorf("body is empty after HTML sanitization") 687 + } 688 + 689 + err = db.UpdateIssueByRkey(ddb, did, rkey, record.Title, body) 690 + if err != nil { 691 + l.Error("failed to update issue", "err", err) 692 + return err 693 + } 694 + 695 + return nil 696 + 697 + case models.CommitOperationDelete: 698 + if err := db.DeleteIssueByRkey(ddb, did, rkey); err != nil { 699 + l.Error("failed to delete", "err", err) 700 + return fmt.Errorf("failed to delete issue record: %w", err) 701 + } 702 + 703 + return nil 704 + } 705 + 706 + return fmt.Errorf("unknown operation: %s", e.Commit.Operation) 707 + } 708 + 709 + func (i *Ingester) ingestIssueComment(e *models.Event) error { 710 + did := e.Did 711 + rkey := e.Commit.RKey 712 + 713 + var err error 714 + 715 + l := i.Logger.With("handler", "ingestIssueComment", "nsid", e.Commit.Collection, "did", did, "rkey", rkey) 716 + l.Info("ingesting record") 717 + 718 + ddb, ok := i.Db.Execer.(*db.DB) 719 + if !ok { 720 + return fmt.Errorf("failed to index issue comment record, invalid db cast") 721 + } 722 + 723 + switch e.Commit.Operation { 724 + case models.CommitOperationCreate: 725 + raw := json.RawMessage(e.Commit.Record) 726 + record := tangled.RepoIssueComment{} 727 + err = json.Unmarshal(raw, &record) 728 + if err != nil { 729 + l.Error("invalid record", "err", err) 730 + return err 731 + } 732 + 733 + comment, err := db.IssueCommentFromRecord(ddb, did, rkey, record) 734 + if err != nil { 735 + l.Error("failed to parse comment from record", "err", err) 736 + return err 737 + } 738 + 739 + sanitizer := markup.NewSanitizer() 740 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(comment.Body)); sb == "" { 741 + return fmt.Errorf("body is empty after HTML sanitization") 742 + } 743 + 744 + err = db.NewIssueComment(ddb, &comment) 745 + if err != nil { 746 + l.Error("failed to create issue comment", "err", err) 747 + return err 748 + } 749 + 750 + return nil 751 + 752 + case models.CommitOperationUpdate: 753 + raw := json.RawMessage(e.Commit.Record) 754 + record := tangled.RepoIssueComment{} 755 + err = json.Unmarshal(raw, &record) 756 + if err != nil { 757 + l.Error("invalid record", "err", err) 758 + return err 759 + } 760 + 761 + sanitizer := markup.NewSanitizer() 762 + if sb := strings.TrimSpace(sanitizer.SanitizeDefault(record.Body)); sb == "" { 763 + return fmt.Errorf("body is empty after HTML sanitization") 764 + } 765 + 766 + err = db.UpdateCommentByRkey(ddb, did, rkey, record.Body) 767 + if err != nil { 768 + l.Error("failed to update issue comment", "err", err) 769 + return err 770 + } 771 + 772 + return nil 773 + 774 + case models.CommitOperationDelete: 775 + if err := db.DeleteCommentByRkey(ddb, did, rkey); err != nil { 776 + l.Error("failed to delete", "err", err) 777 + return fmt.Errorf("failed to delete issue comment record: %w", err) 778 + } 779 + 780 + return nil 781 + } 782 + 783 + return fmt.Errorf("unknown operation: %s", e.Commit.Operation) 784 + }
+29 -41
appview/issues/issues.go
··· 12 12 13 13 comatproto "github.com/bluesky-social/indigo/api/atproto" 14 14 "github.com/bluesky-social/indigo/atproto/data" 15 - "github.com/bluesky-social/indigo/atproto/syntax" 16 15 lexutil "github.com/bluesky-social/indigo/lex/util" 17 16 "github.com/go-chi/chi/v5" 18 17 ··· 75 74 return 76 75 } 77 76 78 - issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt, issueIdInt) 77 + issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt(), issueIdInt) 79 78 if err != nil { 80 79 log.Println("failed to get issue and comments", err) 81 80 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 82 81 return 83 82 } 84 83 85 - reactionCountMap, err := db.GetReactionCountMap(rp.db, syntax.ATURI(issue.IssueAt)) 84 + reactionCountMap, err := db.GetReactionCountMap(rp.db, issue.AtUri()) 86 85 if err != nil { 87 86 log.Println("failed to get issue reactions") 88 87 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") ··· 90 89 91 90 userReactions := map[db.ReactionKind]bool{} 92 91 if user != nil { 93 - userReactions = db.GetReactionStatusMap(rp.db, user.Did, syntax.ATURI(issue.IssueAt)) 92 + userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) 94 93 } 95 94 96 95 issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid) ··· 101 100 rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 102 101 LoggedInUser: user, 103 102 RepoInfo: f.RepoInfo(user), 104 - Issue: *issue, 103 + Issue: issue, 105 104 Comments: comments, 106 105 107 106 IssueOwnerHandle: issueOwnerIdent.Handle.String(), ··· 129 128 return 130 129 } 131 130 132 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 131 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 133 132 if err != nil { 134 133 log.Println("failed to get issue", err) 135 134 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 161 160 Rkey: tid.TID(), 162 161 Record: &lexutil.LexiconTypeDecoder{ 163 162 Val: &tangled.RepoIssueState{ 164 - Issue: issue.IssueAt, 163 + Issue: issue.AtUri().String(), 165 164 State: closed, 166 165 }, 167 166 }, ··· 173 172 return 174 173 } 175 174 176 - err = db.CloseIssue(rp.db, f.RepoAt, issueIdInt) 175 + err = db.CloseIssue(rp.db, f.RepoAt(), issueIdInt) 177 176 if err != nil { 178 177 log.Println("failed to close issue", err) 179 178 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 205 204 return 206 205 } 207 206 208 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 207 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 209 208 if err != nil { 210 209 log.Println("failed to get issue", err) 211 210 rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 222 221 isIssueOwner := user.Did == issue.OwnerDid 223 222 224 223 if isCollaborator || isIssueOwner { 225 - err := db.ReopenIssue(rp.db, f.RepoAt, issueIdInt) 224 + err := db.ReopenIssue(rp.db, f.RepoAt(), issueIdInt) 226 225 if err != nil { 227 226 log.Println("failed to reopen issue", err) 228 227 rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") ··· 266 265 267 266 err := db.NewIssueComment(rp.db, &db.Comment{ 268 267 OwnerDid: user.Did, 269 - RepoAt: f.RepoAt, 268 + RepoAt: f.RepoAt(), 270 269 Issue: issueIdInt, 271 270 CommentId: commentId, 272 271 Body: body, ··· 279 278 } 280 279 281 280 createdAt := time.Now().Format(time.RFC3339) 282 - commentIdInt64 := int64(commentId) 283 281 ownerDid := user.Did 284 - issueAt, err := db.GetIssueAt(rp.db, f.RepoAt, issueIdInt) 282 + issueAt, err := db.GetIssueAt(rp.db, f.RepoAt(), issueIdInt) 285 283 if err != nil { 286 284 log.Println("failed to get issue at", err) 287 285 rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 288 286 return 289 287 } 290 288 291 - atUri := f.RepoAt.String() 289 + atUri := f.RepoAt().String() 292 290 client, err := rp.oauth.AuthorizedClient(r) 293 291 if err != nil { 294 292 log.Println("failed to get authorized client", err) ··· 303 301 Val: &tangled.RepoIssueComment{ 304 302 Repo: &atUri, 305 303 Issue: issueAt, 306 - CommentId: &commentIdInt64, 307 304 Owner: &ownerDid, 308 305 Body: body, 309 306 CreatedAt: createdAt, ··· 345 342 return 346 343 } 347 344 348 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 345 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 349 346 if err != nil { 350 347 log.Println("failed to get issue", err) 351 348 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 352 349 return 353 350 } 354 351 355 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 352 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 356 353 if err != nil { 357 354 http.Error(w, "bad comment id", http.StatusBadRequest) 358 355 return ··· 390 387 return 391 388 } 392 389 393 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 390 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 394 391 if err != nil { 395 392 log.Println("failed to get issue", err) 396 393 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 397 394 return 398 395 } 399 396 400 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 397 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 401 398 if err != nil { 402 399 http.Error(w, "bad comment id", http.StatusBadRequest) 403 400 return ··· 452 449 repoAt := record["repo"].(string) 453 450 issueAt := record["issue"].(string) 454 451 createdAt := record["createdAt"].(string) 455 - commentIdInt64 := int64(commentIdInt) 456 452 457 453 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 458 454 Collection: tangled.RepoIssueCommentNSID, ··· 463 459 Val: &tangled.RepoIssueComment{ 464 460 Repo: &repoAt, 465 461 Issue: issueAt, 466 - CommentId: &commentIdInt64, 467 462 Owner: &comment.OwnerDid, 468 463 Body: newBody, 469 464 CreatedAt: createdAt, ··· 508 503 return 509 504 } 510 505 511 - issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 506 + issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 512 507 if err != nil { 513 508 log.Println("failed to get issue", err) 514 509 rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") ··· 523 518 return 524 519 } 525 520 526 - comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 521 + comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 527 522 if err != nil { 528 523 http.Error(w, "bad comment id", http.StatusBadRequest) 529 524 return ··· 541 536 542 537 // optimistic deletion 543 538 deleted := time.Now() 544 - err = db.DeleteComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 539 + err = db.DeleteComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 545 540 if err != nil { 546 541 log.Println("failed to delete comment") 547 542 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") ··· 605 600 return 606 601 } 607 602 608 - issues, err := db.GetIssues(rp.db, f.RepoAt, isOpen, page) 603 + issues, err := db.GetIssuesPaginated(rp.db, f.RepoAt(), isOpen, page) 609 604 if err != nil { 610 605 log.Println("failed to get issues", err) 611 606 rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") ··· 662 657 } 663 658 664 659 issue := &db.Issue{ 665 - RepoAt: f.RepoAt, 660 + RepoAt: f.RepoAt(), 661 + Rkey: tid.TID(), 666 662 Title: title, 667 663 Body: body, 668 664 OwnerDid: user.Did, ··· 680 676 rp.pages.Notice(w, "issues", "Failed to create issue.") 681 677 return 682 678 } 683 - atUri := f.RepoAt.String() 684 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 679 + atUri := f.RepoAt().String() 680 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 685 681 Collection: tangled.RepoIssueNSID, 686 682 Repo: user.Did, 687 - Rkey: tid.TID(), 683 + Rkey: issue.Rkey, 688 684 Record: &lexutil.LexiconTypeDecoder{ 689 685 Val: &tangled.RepoIssue{ 690 - Repo: atUri, 691 - Title: title, 692 - Body: &body, 693 - Owner: user.Did, 694 - IssueId: int64(issue.IssueId), 686 + Repo: atUri, 687 + Title: title, 688 + Body: &body, 689 + Owner: user.Did, 695 690 }, 696 691 }, 697 692 }) 698 693 if err != nil { 699 694 log.Println("failed to create issue", err) 700 - rp.pages.Notice(w, "issues", "Failed to create issue.") 701 - return 702 - } 703 - 704 - err = db.SetIssueAt(rp.db, f.RepoAt, issue.IssueId, resp.Uri) 705 - if err != nil { 706 - log.Println("failed to set issue at", err) 707 695 rp.pages.Notice(w, "issues", "Failed to create issue.") 708 696 return 709 697 }
+3 -8
appview/middleware/middleware.go
··· 9 9 "slices" 10 10 "strconv" 11 11 "strings" 12 - "time" 13 12 14 13 "github.com/bluesky-social/indigo/atproto/identity" 15 14 "github.com/go-chi/chi/v5" ··· 222 221 return 223 222 } 224 223 225 - ctx := context.WithValue(req.Context(), "knot", repo.Knot) 226 - ctx = context.WithValue(ctx, "repoAt", repo.AtUri) 227 - ctx = context.WithValue(ctx, "repoDescription", repo.Description) 228 - ctx = context.WithValue(ctx, "repoSpindle", repo.Spindle) 229 - ctx = context.WithValue(ctx, "repoAddedAt", repo.Created.Format(time.RFC3339)) 224 + ctx := context.WithValue(req.Context(), "repo", repo) 230 225 next.ServeHTTP(w, req.WithContext(ctx)) 231 226 }) 232 227 } ··· 251 246 return 252 247 } 253 248 254 - pr, err := db.GetPull(mw.db, f.RepoAt, prIdInt) 249 + pr, err := db.GetPull(mw.db, f.RepoAt(), prIdInt) 255 250 if err != nil { 256 251 log.Println("failed to get pull and comments", err) 257 252 return ··· 292 287 return 293 288 } 294 289 295 - fullName := f.OwnerHandle() + "/" + f.RepoName 290 + fullName := f.OwnerHandle() + "/" + f.Name 296 291 297 292 if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 298 293 if r.URL.Query().Get("go-get") == "1" {
+34 -26
appview/pages/pages.go
··· 299 299 type TimelineParams struct { 300 300 LoggedInUser *oauth.User 301 301 Timeline []db.TimelineEvent 302 + Repos []db.Repo 302 303 } 303 304 304 305 func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 305 - return p.execute("timeline", w, params) 306 + return p.execute("timeline/timeline", w, params) 306 307 } 307 308 308 309 type SettingsParams struct { ··· 520 521 } 521 522 522 523 p.rctx.RepoInfo = params.RepoInfo 524 + p.rctx.RepoInfo.Ref = params.Ref 523 525 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 524 526 525 527 if params.ReadmeFileName != "" { ··· 673 675 } 674 676 } 675 677 676 - if params.Lines < 5000 { 677 - c := params.Contents 678 - formatter := chromahtml.New( 679 - chromahtml.InlineCode(false), 680 - chromahtml.WithLineNumbers(true), 681 - chromahtml.WithLinkableLineNumbers(true, "L"), 682 - chromahtml.Standalone(false), 683 - chromahtml.WithClasses(true), 684 - ) 685 - 686 - lexer := lexers.Get(filepath.Base(params.Path)) 687 - if lexer == nil { 688 - lexer = lexers.Fallback 689 - } 678 + c := params.Contents 679 + formatter := chromahtml.New( 680 + chromahtml.InlineCode(false), 681 + chromahtml.WithLineNumbers(true), 682 + chromahtml.WithLinkableLineNumbers(true, "L"), 683 + chromahtml.Standalone(false), 684 + chromahtml.WithClasses(true), 685 + ) 690 686 691 - iterator, err := lexer.Tokenise(nil, c) 692 - if err != nil { 693 - return fmt.Errorf("chroma tokenize: %w", err) 694 - } 687 + lexer := lexers.Get(filepath.Base(params.Path)) 688 + if lexer == nil { 689 + lexer = lexers.Fallback 690 + } 695 691 696 - var code bytes.Buffer 697 - err = formatter.Format(&code, style, iterator) 698 - if err != nil { 699 - return fmt.Errorf("chroma format: %w", err) 700 - } 692 + iterator, err := lexer.Tokenise(nil, c) 693 + if err != nil { 694 + return fmt.Errorf("chroma tokenize: %w", err) 695 + } 701 696 702 - params.Contents = code.String() 697 + var code bytes.Buffer 698 + err = formatter.Format(&code, style, iterator) 699 + if err != nil { 700 + return fmt.Errorf("chroma format: %w", err) 703 701 } 704 702 703 + params.Contents = code.String() 705 704 params.Active = "overview" 706 705 return p.executeRepo("repo/blob", w, params) 707 706 } ··· 793 792 LoggedInUser *oauth.User 794 793 RepoInfo repoinfo.RepoInfo 795 794 Active string 796 - Issue db.Issue 795 + Issue *db.Issue 797 796 Comments []db.Comment 798 797 IssueOwnerHandle string 799 798 ··· 1158 1157 1159 1158 func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1160 1159 return p.execute("strings/dashboard", w, params) 1160 + } 1161 + 1162 + type StringTimelineParams struct { 1163 + LoggedInUser *oauth.User 1164 + Strings []db.String 1165 + } 1166 + 1167 + func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1168 + return p.execute("strings/timeline", w, params) 1161 1169 } 1162 1170 1163 1171 type SingleStringParams struct {
-12
appview/pages/templates/layouts/base.html
··· 24 24 {{ block "mainLayout" . }} 25 25 <div class="px-1 col-span-1 md:col-start-3 md:col-span-8 flex flex-col gap-4"> 26 26 {{ block "contentLayout" . }} 27 - <div class="col-span-1 md:col-span-2"> 28 - {{ block "contentLeft" . }} {{ end }} 29 - </div> 30 27 <main class="col-span-1 md:col-span-8"> 31 28 {{ block "content" . }}{{ end }} 32 29 </main> 33 - <div class="col-span-1 md:col-span-2"> 34 - {{ block "contentRight" . }} {{ end }} 35 - </div> 36 30 {{ end }} 37 31 38 32 {{ block "contentAfterLayout" . }} 39 - <div class="col-span-1 md:col-span-2"> 40 - {{ block "contentAfterLeft" . }} {{ end }} 41 - </div> 42 33 <main class="col-span-1 md:col-span-8"> 43 34 {{ block "contentAfter" . }}{{ end }} 44 35 </main> 45 - <div class="col-span-1 md:col-span-2"> 46 - {{ block "contentAfterRight" . }} {{ end }} 47 - </div> 48 36 {{ end }} 49 37 </div> 50 38 {{ end }}
+15 -20
appview/pages/templates/layouts/repobase.html
··· 20 20 </div> 21 21 22 22 <div class="flex items-center gap-2 z-auto"> 23 + <a 24 + class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 25 + href="/{{ .RepoInfo.FullName }}/feed.atom" 26 + > 27 + {{ i "rss" "size-4" }} 28 + </a> 23 29 {{ template "repo/fragments/repoStar" .RepoInfo }} 24 - {{ if .RepoInfo.DisableFork }} 25 - <button 26 - class="btn text-sm no-underline hover:no-underline flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" 27 - disabled 28 - title="Empty repositories cannot be forked" 29 - > 30 - {{ i "git-fork" "w-4 h-4" }} 31 - fork 32 - </button> 33 - {{ else }} 34 - <a 35 - class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 36 - hx-boost="true" 37 - href="/{{ .RepoInfo.FullName }}/fork" 38 - > 39 - {{ i "git-fork" "w-4 h-4" }} 40 - fork 41 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 42 - </a> 43 - {{ end }} 30 + <a 31 + class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 32 + hx-boost="true" 33 + href="/{{ .RepoInfo.FullName }}/fork" 34 + > 35 + {{ i "git-fork" "w-4 h-4" }} 36 + fork 37 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 38 + </a> 44 39 </div> 45 40 </div> 46 41 {{ template "repo/fragments/repoDescription" . }}
+1 -1
appview/pages/templates/repo/commit.html
··· 118 118 <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 119 119 {{ template "repo/fragments/diffOpts" .DiffOpts }} 120 120 </div> 121 - <div class="sticky top-0 flex-grow max-h-screen"> 121 + <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 122 122 {{ template "repo/fragments/diffChangedFiles" .Diff }} 123 123 </div> 124 124 {{end}}
+1 -1
appview/pages/templates/repo/compare/compare.html
··· 49 49 <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 50 50 {{ template "repo/fragments/diffOpts" .DiffOpts }} 51 51 </div> 52 - <div class="sticky top-0 flex-grow max-h-screen"> 52 + <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 53 53 {{ template "repo/fragments/diffChangedFiles" .Diff }} 54 54 </div> 55 55 {{end}}
+1 -1
appview/pages/templates/repo/fragments/interdiffFiles.html
··· 1 1 {{ define "repo/fragments/interdiffFiles" }} 2 2 {{ $fileTree := fileTree .AffectedFiles }} 3 - <section class="mt-4 px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm min-h-full text-sm"> 3 + <section class="px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm min-h-full text-sm"> 4 4 <div class="diff-stat"> 5 5 <div class="flex gap-2 items-center"> 6 6 <strong class="text-sm uppercase dark:text-gray-200">files</strong>
+3 -3
appview/pages/templates/repo/index.html
··· 139 139 {{ $linkstyle := "no-underline hover:underline dark:text-white" }} 140 140 141 141 {{ range .Files }} 142 - <div class="grid grid-cols-2 gap-4 items-center py-1"> 143 - <div class="col-span-1"> 142 + <div class="grid grid-cols-3 gap-4 items-center py-1"> 143 + <div class="col-span-2"> 144 144 {{ $link := printf "/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (urlquery $.Ref) .Name }} 145 145 {{ $icon := "folder" }} 146 146 {{ $iconStyle := "size-4 fill-current" }} ··· 158 158 </a> 159 159 </div> 160 160 161 - <div class="text-xs col-span-1 text-right"> 161 + <div class="text-sm col-span-1 text-right"> 162 162 {{ with .LastCommit }} 163 163 <a href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash }}" class="text-gray-500 dark:text-gray-400">{{ template "repo/fragments/time" .When }}</a> 164 164 {{ end }}
+1 -1
appview/pages/templates/repo/issues/issue.html
··· 54 54 "Kind" $kind 55 55 "Count" (index $.Reactions $kind) 56 56 "IsReacted" (index $.UserReacted $kind) 57 - "ThreadAt" $.Issue.IssueAt) 57 + "ThreadAt" $.Issue.AtUri) 58 58 }} 59 59 {{ end }} 60 60 </div>
+1 -1
appview/pages/templates/repo/pulls/interdiff.html
··· 68 68 <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 69 69 {{ template "repo/fragments/diffOpts" .DiffOpts }} 70 70 </div> 71 - <div class="sticky top-0 flex-grow max-h-screen"> 71 + <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 72 72 {{ template "repo/fragments/interdiffFiles" .Interdiff }} 73 73 </div> 74 74 {{end}}
+1 -1
appview/pages/templates/repo/pulls/patch.html
··· 73 73 <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 74 74 {{ template "repo/fragments/diffOpts" .DiffOpts }} 75 75 </div> 76 - <div class="sticky top-0 flex-grow max-h-screen"> 76 + <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 77 77 {{ template "repo/fragments/diffChangedFiles" .Diff }} 78 78 </div> 79 79 {{end}}
+2 -2
appview/pages/templates/repo/tree.html
··· 54 54 55 55 {{ range .Files }} 56 56 <div class="grid grid-cols-12 gap-4 items-center py-1"> 57 - <div class="col-span-6 md:col-span-4"> 57 + <div class="col-span-8 md:col-span-4"> 58 58 {{ $link := printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (urlquery $.Ref) $.TreePath .Name }} 59 59 {{ $icon := "folder" }} 60 60 {{ $iconStyle := "size-4 fill-current" }} ··· 77 77 {{ end }} 78 78 </div> 79 79 80 - <div class="col-span-6 md:col-span-2 text-right"> 80 + <div class="col-span-4 md:col-span-2 text-sm text-right"> 81 81 {{ with .LastCommit }} 82 82 <a href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash }}" class="text-gray-500 dark:text-gray-400">{{ template "repo/fragments/time" .When }}</a> 83 83 {{ end }}
+3 -2
appview/pages/templates/strings/fragments/form.html
··· 13 13 type="text" 14 14 id="filename" 15 15 name="filename" 16 - placeholder="Filename with extension" 16 + placeholder="Filename" 17 17 required 18 18 value="{{ .String.Filename }}" 19 19 class="md:max-w-64 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded" ··· 31 31 name="content" 32 32 id="content-textarea" 33 33 wrap="off" 34 - class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400" 34 + class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 font-mono" 35 35 rows="20" 36 + spellcheck="false" 36 37 placeholder="Paste your string here!" 37 38 required>{{ .String.Contents }}</textarea> 38 39 <div class="flex justify-between items-center">
+2 -2
appview/pages/templates/strings/string.html
··· 35 35 title="Delete string" 36 36 hx-delete="/strings/{{ .String.Did }}/{{ .String.Rkey }}/" 37 37 hx-swap="none" 38 - hx-confirm="Are you sure you want to delete the gist `{{ .String.Filename }}`?" 38 + hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?" 39 39 > 40 40 {{ i "trash-2" "size-4" }} 41 41 <span class="hidden md:inline">delete</span> ··· 77 77 {{ end }} 78 78 </div> 79 79 </div> 80 - <div class="overflow-auto relative"> 80 + <div class="overflow-x-auto overflow-y-hidden relative"> 81 81 {{ if .ShowRendered }} 82 82 <div id="blob-contents" class="prose dark:prose-invert">{{ .RenderedContents }}</div> 83 83 {{ else }}
+65
appview/pages/templates/strings/timeline.html
··· 1 + {{ define "title" }} all strings {{ end }} 2 + 3 + {{ define "topbar" }} 4 + {{ template "layouts/topbar" $ }} 5 + {{ end }} 6 + 7 + {{ define "content" }} 8 + {{ block "timeline" $ }}{{ end }} 9 + {{ end }} 10 + 11 + {{ define "timeline" }} 12 + <div> 13 + <div class="p-6"> 14 + <p class="text-xl font-bold dark:text-white">All strings</p> 15 + </div> 16 + 17 + <div class="flex flex-col gap-4"> 18 + {{ range $i, $s := .Strings }} 19 + <div class="relative"> 20 + {{ if ne $i 0 }} 21 + <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-600"></div> 22 + {{ end }} 23 + <div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm"> 24 + {{ template "stringCard" $s }} 25 + </div> 26 + </div> 27 + {{ end }} 28 + </div> 29 + </div> 30 + {{ end }} 31 + 32 + {{ define "stringCard" }} 33 + <div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800"> 34 + <div class="font-medium dark:text-white flex gap-2 items-center"> 35 + <a href="/strings/{{ resolve .Did.String }}/{{ .Rkey }}">{{ .Filename }}</a> 36 + </div> 37 + {{ with .Description }} 38 + <div class="text-gray-600 dark:text-gray-300 text-sm"> 39 + {{ . }} 40 + </div> 41 + {{ end }} 42 + 43 + {{ template "stringCardInfo" . }} 44 + </div> 45 + {{ end }} 46 + 47 + {{ define "stringCardInfo" }} 48 + {{ $stat := .Stats }} 49 + {{ $resolved := resolve .Did.String }} 50 + <div class="text-gray-400 pt-4 text-sm font-mono inline-flex items-center gap-2 mt-auto"> 51 + <a href="/strings/{{ $resolved }}" class="flex items-center"> 52 + {{ template "user/fragments/picHandle" $resolved }} 53 + </a> 54 + <span class="select-none [&:before]:content-['ยท']"></span> 55 + <span>{{ $stat.LineCount }} line{{if ne $stat.LineCount 1}}s{{end}}</span> 56 + <span class="select-none [&:before]:content-['ยท']"></span> 57 + {{ with .Edited }} 58 + <span>edited {{ template "repo/fragments/shortTimeAgo" . }}</span> 59 + {{ else }} 60 + {{ template "repo/fragments/shortTimeAgo" .Created }} 61 + {{ end }} 62 + </div> 63 + {{ end }} 64 + 65 +
+183
appview/pages/templates/timeline/timeline.html
··· 1 + {{ define "title" }}timeline{{ end }} 2 + 3 + {{ define "extrameta" }} 4 + <meta property="og:title" content="timeline ยท tangled" /> 5 + <meta property="og:type" content="object" /> 6 + <meta property="og:url" content="https://tangled.sh" /> 7 + <meta property="og:description" content="tightly-knit social coding" /> 8 + {{ end }} 9 + 10 + {{ define "content" }} 11 + {{ if .LoggedInUser }} 12 + {{ else }} 13 + {{ block "hero" $ }}{{ end }} 14 + {{ end }} 15 + 16 + {{ block "trending" $ }}{{ end }} 17 + {{ block "timeline" $ }}{{ end }} 18 + {{ end }} 19 + 20 + {{ define "hero" }} 21 + <div class="flex flex-col text-black dark:text-white p-6 gap-6 max-w-xl"> 22 + <div class="font-bold text-4xl">tightly-knit<br>social coding.</div> 23 + 24 + <p class="text-lg"> 25 + tangled is new social-enabled git collaboration platform built on <a class="underline" href="https://atproto.com/">atproto</a>. 26 + </p> 27 + <p class="text-lg"> 28 + we envision a place where developers have complete ownership of their 29 + code, open source communities can freely self-govern and most 30 + importantly, coding can be social and fun again. 31 + </p> 32 + 33 + <div class="flex gap-6 items-center"> 34 + <a href="/signup" class="no-underline hover:no-underline "> 35 + <button class="btn-create flex gap-2 px-4 items-center"> 36 + join now {{ i "arrow-right" "size-4" }} 37 + </button> 38 + </a> 39 + </div> 40 + </div> 41 + {{ end }} 42 + 43 + {{ define "trending" }} 44 + <div class="w-full md:mx-0 py-4"> 45 + <div class="px-6 pb-4"> 46 + <h3 class="text-xl font-bold dark:text-white flex items-center gap-2"> 47 + Trending 48 + {{ i "trending-up" "size-4 flex-shrink-0" }} 49 + </h3> 50 + </div> 51 + <div class="flex gap-4 overflow-x-auto scrollbar-hide items-stretch"> 52 + {{ range $index, $repo := .Repos }} 53 + <div class="flex-none h-full border border-gray-200 dark:border-gray-700 rounded-sm w-96"> 54 + {{ template "user/fragments/repoCard" (list $ $repo true) }} 55 + </div> 56 + {{ else }} 57 + <div class="py-8 px-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-sm"> 58 + <div class="text-sm text-gray-500 dark:text-gray-400 text-center"> 59 + No trending repositories this week 60 + </div> 61 + </div> 62 + {{ end }} 63 + </div> 64 + </div> 65 + {{ end }} 66 + 67 + {{ define "timeline" }} 68 + <div class="py-4"> 69 + <div class="px-6 pb-4"> 70 + <p class="text-xl font-bold dark:text-white">Timeline</p> 71 + </div> 72 + 73 + <div class="flex flex-col gap-4"> 74 + {{ range $i, $e := .Timeline }} 75 + <div class="relative"> 76 + {{ if ne $i 0 }} 77 + <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-600"></div> 78 + {{ end }} 79 + {{ with $e }} 80 + <div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm"> 81 + {{ if .Repo }} 82 + {{ block "repoEvent" (list $ .Repo .Source) }} {{ end }} 83 + {{ else if .Star }} 84 + {{ block "starEvent" (list $ .Star) }} {{ end }} 85 + {{ else if .Follow }} 86 + {{ block "followEvent" (list $ .Follow .Profile .FollowStats) }} {{ end }} 87 + {{ end }} 88 + </div> 89 + {{ end }} 90 + </div> 91 + {{ end }} 92 + </div> 93 + </div> 94 + {{ end }} 95 + 96 + {{ define "repoEvent" }} 97 + {{ $root := index . 0 }} 98 + {{ $repo := index . 1 }} 99 + {{ $source := index . 2 }} 100 + {{ $userHandle := resolve $repo.Did }} 101 + <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 102 + {{ template "user/fragments/picHandleLink" $repo.Did }} 103 + {{ with $source }} 104 + {{ $sourceDid := resolve .Did }} 105 + forked 106 + <a href="/{{ $sourceDid }}/{{ .Name }}"class="no-underline hover:underline"> 107 + {{ $sourceDid }}/{{ .Name }} 108 + </a> 109 + to 110 + <a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline">{{ $repo.Name }}</a> 111 + {{ else }} 112 + created 113 + <a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline"> 114 + {{ $repo.Name }} 115 + </a> 116 + {{ end }} 117 + <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $repo.Created }}</span> 118 + </div> 119 + {{ with $repo }} 120 + {{ template "user/fragments/repoCard" (list $root . true) }} 121 + {{ end }} 122 + {{ end }} 123 + 124 + {{ define "starEvent" }} 125 + {{ $root := index . 0 }} 126 + {{ $star := index . 1 }} 127 + {{ with $star }} 128 + {{ $starrerHandle := resolve .StarredByDid }} 129 + {{ $repoOwnerHandle := resolve .Repo.Did }} 130 + <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 131 + {{ template "user/fragments/picHandleLink" $starrerHandle }} 132 + starred 133 + <a href="/{{ $repoOwnerHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline"> 134 + {{ $repoOwnerHandle | truncateAt30 }}/{{ .Repo.Name }} 135 + </a> 136 + <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" .Created }}</span> 137 + </div> 138 + {{ with .Repo }} 139 + {{ template "user/fragments/repoCard" (list $root . true) }} 140 + {{ end }} 141 + {{ end }} 142 + {{ end }} 143 + 144 + 145 + {{ define "followEvent" }} 146 + {{ $root := index . 0 }} 147 + {{ $follow := index . 1 }} 148 + {{ $profile := index . 2 }} 149 + {{ $stat := index . 3 }} 150 + 151 + {{ $userHandle := resolve $follow.UserDid }} 152 + {{ $subjectHandle := resolve $follow.SubjectDid }} 153 + <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 154 + {{ template "user/fragments/picHandleLink" $userHandle }} 155 + followed 156 + {{ template "user/fragments/picHandleLink" $subjectHandle }} 157 + <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $follow.FollowedAt }}</span> 158 + </div> 159 + <div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4"> 160 + <div class="flex-shrink-0 max-h-full w-24 h-24"> 161 + <img class="object-cover rounded-full p-2" src="{{ fullAvatar $subjectHandle }}" /> 162 + </div> 163 + 164 + <div class="flex-1 min-h-0 justify-around flex flex-col"> 165 + <a href="/{{ $subjectHandle }}"> 166 + <span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $subjectHandle | truncateAt30 }}</span> 167 + </a> 168 + {{ with $profile }} 169 + {{ with .Description }} 170 + <p class="text-sm pb-2 md:pb-2">{{.}}</p> 171 + {{ end }} 172 + {{ end }} 173 + {{ with $stat }} 174 + <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm"> 175 + <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 176 + <span id="followers">{{ .Followers }} followers</span> 177 + <span class="select-none after:content-['ยท']"></span> 178 + <span id="following">{{ .Following }} following</span> 179 + </div> 180 + {{ end }} 181 + </div> 182 + </div> 183 + {{ end }}
-162
appview/pages/templates/timeline.html
··· 1 - {{ define "title" }}timeline{{ end }} 2 - 3 - {{ define "extrameta" }} 4 - <meta property="og:title" content="timeline ยท tangled" /> 5 - <meta property="og:type" content="object" /> 6 - <meta property="og:url" content="https://tangled.sh" /> 7 - <meta property="og:description" content="see what's tangling" /> 8 - {{ end }} 9 - 10 - {{ define "topbar" }} 11 - {{ template "layouts/topbar" $ }} 12 - {{ end }} 13 - 14 - {{ define "content" }} 15 - {{ with .LoggedInUser }} 16 - {{ block "timeline" $ }}{{ end }} 17 - {{ else }} 18 - {{ block "hero" $ }}{{ end }} 19 - {{ block "timeline" $ }}{{ end }} 20 - {{ end }} 21 - {{ end }} 22 - 23 - {{ define "hero" }} 24 - <div class="flex flex-col text-black dark:text-white p-6 gap-6 max-w-xl"> 25 - <div class="font-bold text-4xl">tightly-knit<br>social coding.</div> 26 - 27 - <p class="text-lg"> 28 - tangled is new social-enabled git collaboration platform built on <a class="underline" href="https://atproto.com/">atproto</a>. 29 - </p> 30 - <p class="text-lg"> 31 - we envision a place where developers have complete ownership of their 32 - code, open source communities can freely self-govern and most 33 - importantly, coding can be social and fun again. 34 - </p> 35 - 36 - <div class="flex gap-6 items-center"> 37 - <a href="/signup" class="no-underline hover:no-underline "> 38 - <button class="btn-create flex gap-2 px-4 items-center"> 39 - join now {{ i "arrow-right" "size-4" }} 40 - </button> 41 - </a> 42 - </div> 43 - </div> 44 - {{ end }} 45 - 46 - {{ define "timeline" }} 47 - <div> 48 - <div class="p-6"> 49 - <p class="text-xl font-bold dark:text-white">Timeline</p> 50 - </div> 51 - 52 - <div class="flex flex-col gap-4"> 53 - {{ range $i, $e := .Timeline }} 54 - <div class="relative"> 55 - {{ if ne $i 0 }} 56 - <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-600"></div> 57 - {{ end }} 58 - {{ with $e }} 59 - <div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm"> 60 - {{ if .Repo }} 61 - {{ block "repoEvent" (list $ .Repo .Source) }} {{ end }} 62 - {{ else if .Star }} 63 - {{ block "starEvent" (list $ .Star) }} {{ end }} 64 - {{ else if .Follow }} 65 - {{ block "followEvent" (list $ .Follow .Profile .FollowStats) }} {{ end }} 66 - {{ end }} 67 - </div> 68 - {{ end }} 69 - </div> 70 - {{ end }} 71 - </div> 72 - </div> 73 - {{ end }} 74 - 75 - {{ define "repoEvent" }} 76 - {{ $root := index . 0 }} 77 - {{ $repo := index . 1 }} 78 - {{ $source := index . 2 }} 79 - {{ $userHandle := resolve $repo.Did }} 80 - <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 81 - {{ template "user/fragments/picHandleLink" $repo.Did }} 82 - {{ with $source }} 83 - {{ $sourceDid := resolve .Did }} 84 - forked 85 - <a href="/{{ $sourceDid }}/{{ .Name }}"class="no-underline hover:underline"> 86 - {{ $sourceDid }}/{{ .Name }} 87 - </a> 88 - to 89 - <a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline">{{ $repo.Name }}</a> 90 - {{ else }} 91 - created 92 - <a href="/{{ $userHandle }}/{{ $repo.Name }}" class="no-underline hover:underline"> 93 - {{ $repo.Name }} 94 - </a> 95 - {{ end }} 96 - <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $repo.Created }}</span> 97 - </div> 98 - {{ with $repo }} 99 - {{ template "user/fragments/repoCard" (list $root . true) }} 100 - {{ end }} 101 - {{ end }} 102 - 103 - {{ define "starEvent" }} 104 - {{ $root := index . 0 }} 105 - {{ $star := index . 1 }} 106 - {{ with $star }} 107 - {{ $starrerHandle := resolve .StarredByDid }} 108 - {{ $repoOwnerHandle := resolve .Repo.Did }} 109 - <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 110 - {{ template "user/fragments/picHandleLink" $starrerHandle }} 111 - starred 112 - <a href="/{{ $repoOwnerHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline"> 113 - {{ $repoOwnerHandle | truncateAt30 }}/{{ .Repo.Name }} 114 - </a> 115 - <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" .Created }}</span> 116 - </div> 117 - {{ with .Repo }} 118 - {{ template "user/fragments/repoCard" (list $root . true) }} 119 - {{ end }} 120 - {{ end }} 121 - {{ end }} 122 - 123 - 124 - {{ define "followEvent" }} 125 - {{ $root := index . 0 }} 126 - {{ $follow := index . 1 }} 127 - {{ $profile := index . 2 }} 128 - {{ $stat := index . 3 }} 129 - 130 - {{ $userHandle := resolve $follow.UserDid }} 131 - {{ $subjectHandle := resolve $follow.SubjectDid }} 132 - <div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm"> 133 - {{ template "user/fragments/picHandleLink" $userHandle }} 134 - followed 135 - {{ template "user/fragments/picHandleLink" $subjectHandle }} 136 - <span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $follow.FollowedAt }}</span> 137 - </div> 138 - <div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex items-center gap-4"> 139 - <div class="flex-shrink-0 max-h-full w-24 h-24"> 140 - <img class="object-cover rounded-full p-2" src="{{ fullAvatar $subjectHandle }}" /> 141 - </div> 142 - 143 - <div class="flex-1 min-h-0 justify-around flex flex-col"> 144 - <a href="/{{ $subjectHandle }}"> 145 - <span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $subjectHandle | truncateAt30 }}</span> 146 - </a> 147 - {{ with $profile }} 148 - {{ with .Description }} 149 - <p class="text-sm pb-2 md:pb-2">{{.}}</p> 150 - {{ end }} 151 - {{ end }} 152 - {{ with $stat }} 153 - <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm"> 154 - <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 155 - <span id="followers">{{ .Followers }} followers</span> 156 - <span class="select-none after:content-['ยท']"></span> 157 - <span id="following">{{ .Following }} following</span> 158 - </div> 159 - {{ end }} 160 - </div> 161 - </div> 162 - {{ end }}
+4 -4
appview/pages/templates/user/fragments/repoCard.html
··· 4 4 {{ $fullName := index . 2 }} 5 5 6 6 {{ with $repo }} 7 - <div class="py-4 px-6 gap-2 flex flex-col drop-shadow-sm rounded bg-white dark:bg-gray-800"> 7 + <div class="py-4 px-6 gap-1 flex flex-col drop-shadow-sm rounded bg-white dark:bg-gray-800 min-h-32"> 8 8 <div class="font-medium dark:text-white flex items-center"> 9 9 {{ if .Source }} 10 10 {{ i "git-fork" "w-4 h-4 mr-1.5 shrink-0" }} ··· 14 14 15 15 {{ $repoOwner := resolve .Did }} 16 16 {{- if $fullName -}} 17 - <a href="/{{ $repoOwner }}/{{ .Name }}">{{ $repoOwner }}/{{ .Name }}</a> 17 + <a href="/{{ $repoOwner }}/{{ .Name }}" class="truncate">{{ $repoOwner }}/{{ .Name }}</a> 18 18 {{- else -}} 19 - <a href="/{{ $repoOwner }}/{{ .Name }}">{{ .Name }}</a> 19 + <a href="/{{ $repoOwner }}/{{ .Name }}" class="truncate">{{ .Name }}</a> 20 20 {{- end -}} 21 21 </div> 22 22 {{ with .Description }} 23 - <div class="text-gray-600 dark:text-gray-300 text-sm"> 23 + <div class="text-gray-600 dark:text-gray-300 text-sm line-clamp-2"> 24 24 {{ . | description }} 25 25 </div> 26 26 {{ end }}
+24 -29
appview/pulls/pulls.go
··· 29 29 30 30 "github.com/bluekeyes/go-gitdiff/gitdiff" 31 31 comatproto "github.com/bluesky-social/indigo/api/atproto" 32 - "github.com/bluesky-social/indigo/atproto/syntax" 33 32 lexutil "github.com/bluesky-social/indigo/lex/util" 34 33 "github.com/go-chi/chi/v5" 35 34 "github.com/google/uuid" ··· 247 246 patch = mergeable.CombinedPatch() 248 247 } 249 248 250 - resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch) 249 + resp, err := ksClient.MergeCheck([]byte(patch), f.OwnerDid(), f.Name, pull.TargetBranch) 251 250 if err != nil { 252 251 log.Println("failed to check for mergeability:", err) 253 252 return types.MergeCheckResponse{ ··· 308 307 // pulls within the same repo 309 308 knot = f.Knot 310 309 ownerDid = f.OwnerDid() 311 - repoName = f.RepoName 310 + repoName = f.Name 312 311 } 313 312 314 313 us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) ··· 484 483 485 484 pulls, err := db.GetPulls( 486 485 s.db, 487 - db.FilterEq("repo_at", f.RepoAt), 486 + db.FilterEq("repo_at", f.RepoAt()), 488 487 db.FilterEq("state", state), 489 488 ) 490 489 if err != nil { ··· 611 610 createdAt := time.Now().Format(time.RFC3339) 612 611 ownerDid := user.Did 613 612 614 - pullAt, err := db.GetPullAt(s.db, f.RepoAt, pull.PullId) 613 + pullAt, err := db.GetPullAt(s.db, f.RepoAt(), pull.PullId) 615 614 if err != nil { 616 615 log.Println("failed to get pull at", err) 617 616 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 618 617 return 619 618 } 620 619 621 - atUri := f.RepoAt.String() 620 + atUri := f.RepoAt().String() 622 621 client, err := s.oauth.AuthorizedClient(r) 623 622 if err != nil { 624 623 log.Println("failed to get authorized client", err) ··· 647 646 648 647 comment := &db.PullComment{ 649 648 OwnerDid: user.Did, 650 - RepoAt: f.RepoAt.String(), 649 + RepoAt: f.RepoAt().String(), 651 650 PullId: pull.PullId, 652 651 Body: body, 653 652 CommentAt: atResp.Uri, ··· 693 692 return 694 693 } 695 694 696 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 695 + result, err := us.Branches(f.OwnerDid(), f.Name) 697 696 if err != nil { 698 697 log.Println("failed to fetch branches", err) 699 698 return ··· 822 821 return 823 822 } 824 823 825 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 824 + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, targetBranch, sourceBranch) 826 825 if err != nil { 827 826 log.Println("failed to compare", err) 828 827 s.pages.Notice(w, "pull", err.Error()) ··· 924 923 return 925 924 } 926 925 927 - forkAtUri, err := syntax.ParseATURI(fork.AtUri) 928 - if err != nil { 929 - log.Println("failed to parse fork AT URI", err) 930 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 931 - return 932 - } 926 + forkAtUri := fork.RepoAt() 927 + forkAtUriStr := forkAtUri.String() 933 928 934 929 pullSource := &db.PullSource{ 935 930 Branch: sourceBranch, ··· 937 932 } 938 933 recordPullSource := &tangled.RepoPull_Source{ 939 934 Branch: sourceBranch, 940 - Repo: &fork.AtUri, 935 + Repo: &forkAtUriStr, 941 936 Sha: sourceRev, 942 937 } 943 938 ··· 1013 1008 Body: body, 1014 1009 TargetBranch: targetBranch, 1015 1010 OwnerDid: user.Did, 1016 - RepoAt: f.RepoAt, 1011 + RepoAt: f.RepoAt(), 1017 1012 Rkey: rkey, 1018 1013 Submissions: []*db.PullSubmission{ 1019 1014 &initialSubmission, ··· 1026 1021 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1027 1022 return 1028 1023 } 1029 - pullId, err := db.NextPullId(tx, f.RepoAt) 1024 + pullId, err := db.NextPullId(tx, f.RepoAt()) 1030 1025 if err != nil { 1031 1026 log.Println("failed to get pull id", err) 1032 1027 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 1041 1036 Val: &tangled.RepoPull{ 1042 1037 Title: title, 1043 1038 PullId: int64(pullId), 1044 - TargetRepo: string(f.RepoAt), 1039 + TargetRepo: string(f.RepoAt()), 1045 1040 TargetBranch: targetBranch, 1046 1041 Patch: patch, 1047 1042 Source: recordPullSource, ··· 1219 1214 return 1220 1215 } 1221 1216 1222 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1217 + result, err := us.Branches(f.OwnerDid(), f.Name) 1223 1218 if err != nil { 1224 1219 log.Println("failed to reach knotserver", err) 1225 1220 return ··· 1303 1298 return 1304 1299 } 1305 1300 1306 - targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.RepoName) 1301 + targetResult, err := targetBranchesClient.Branches(f.OwnerDid(), f.Name) 1307 1302 if err != nil { 1308 1303 log.Println("failed to reach knotserver for target branches", err) 1309 1304 return ··· 1419 1414 return 1420 1415 } 1421 1416 1422 - comparison, err := ksClient.Compare(f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.PullSource.Branch) 1417 + comparison, err := ksClient.Compare(f.OwnerDid(), f.Name, pull.TargetBranch, pull.PullSource.Branch) 1423 1418 if err != nil { 1424 1419 log.Printf("compare request failed: %s", err) 1425 1420 s.pages.Notice(w, "resubmit-error", err.Error()) ··· 1603 1598 Val: &tangled.RepoPull{ 1604 1599 Title: pull.Title, 1605 1600 PullId: int64(pull.PullId), 1606 - TargetRepo: string(f.RepoAt), 1601 + TargetRepo: string(f.RepoAt()), 1607 1602 TargetBranch: pull.TargetBranch, 1608 1603 Patch: patch, // new patch 1609 1604 Source: recordPullSource, ··· 1940 1935 } 1941 1936 1942 1937 // Merge the pull request 1943 - resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.RepoName, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1938 + resp, err := ksClient.Merge([]byte(patch), f.OwnerDid(), f.Name, pull.TargetBranch, pull.Title, pull.Body, ident.Handle.String(), email.Address) 1944 1939 if err != nil { 1945 1940 log.Printf("failed to merge pull request: %s", err) 1946 1941 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1962 1957 defer tx.Rollback() 1963 1958 1964 1959 for _, p := range pullsToMerge { 1965 - err := db.MergePull(tx, f.RepoAt, p.PullId) 1960 + err := db.MergePull(tx, f.RepoAt(), p.PullId) 1966 1961 if err != nil { 1967 1962 log.Printf("failed to update pull request status in database: %s", err) 1968 1963 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1978 1973 return 1979 1974 } 1980 1975 1981 - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId)) 1976 + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.Name, pull.PullId)) 1982 1977 } 1983 1978 1984 1979 func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { ··· 2030 2025 2031 2026 for _, p := range pullsToClose { 2032 2027 // Close the pull in the database 2033 - err = db.ClosePull(tx, f.RepoAt, p.PullId) 2028 + err = db.ClosePull(tx, f.RepoAt(), p.PullId) 2034 2029 if err != nil { 2035 2030 log.Println("failed to close pull", err) 2036 2031 s.pages.Notice(w, "pull-close", "Failed to close pull.") ··· 2098 2093 2099 2094 for _, p := range pullsToReopen { 2100 2095 // Close the pull in the database 2101 - err = db.ReopenPull(tx, f.RepoAt, p.PullId) 2096 + err = db.ReopenPull(tx, f.RepoAt(), p.PullId) 2102 2097 if err != nil { 2103 2098 log.Println("failed to close pull", err) 2104 2099 s.pages.Notice(w, "pull-close", "Failed to close pull.") ··· 2150 2145 Body: body, 2151 2146 TargetBranch: targetBranch, 2152 2147 OwnerDid: user.Did, 2153 - RepoAt: f.RepoAt, 2148 + RepoAt: f.RepoAt(), 2154 2149 Rkey: rkey, 2155 2150 Submissions: []*db.PullSubmission{ 2156 2151 &initialSubmission,
+6 -6
appview/repo/artifact.go
··· 76 76 Artifact: uploadBlobResp.Blob, 77 77 CreatedAt: createdAt.Format(time.RFC3339), 78 78 Name: handler.Filename, 79 - Repo: f.RepoAt.String(), 79 + Repo: f.RepoAt().String(), 80 80 Tag: tag.Tag.Hash[:], 81 81 }, 82 82 }, ··· 100 100 artifact := db.Artifact{ 101 101 Did: user.Did, 102 102 Rkey: rkey, 103 - RepoAt: f.RepoAt, 103 + RepoAt: f.RepoAt(), 104 104 Tag: tag.Tag.Hash, 105 105 CreatedAt: createdAt, 106 106 BlobCid: cid.Cid(uploadBlobResp.Blob.Ref), ··· 155 155 156 156 artifacts, err := db.GetArtifact( 157 157 rp.db, 158 - db.FilterEq("repo_at", f.RepoAt), 158 + db.FilterEq("repo_at", f.RepoAt()), 159 159 db.FilterEq("tag", tag.Tag.Hash[:]), 160 160 db.FilterEq("name", filename), 161 161 ) ··· 197 197 198 198 artifacts, err := db.GetArtifact( 199 199 rp.db, 200 - db.FilterEq("repo_at", f.RepoAt), 200 + db.FilterEq("repo_at", f.RepoAt()), 201 201 db.FilterEq("tag", tag[:]), 202 202 db.FilterEq("name", filename), 203 203 ) ··· 239 239 defer tx.Rollback() 240 240 241 241 err = db.DeleteArtifact(tx, 242 - db.FilterEq("repo_at", f.RepoAt), 242 + db.FilterEq("repo_at", f.RepoAt()), 243 243 db.FilterEq("tag", artifact.Tag[:]), 244 244 db.FilterEq("name", filename), 245 245 ) ··· 270 270 return nil, err 271 271 } 272 272 273 - result, err := us.Tags(f.OwnerDid(), f.RepoName) 273 + result, err := us.Tags(f.OwnerDid(), f.Name) 274 274 if err != nil { 275 275 log.Println("failed to reach knotserver", err) 276 276 return nil, err
+165
appview/repo/feed.go
··· 1 + package repo 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log" 7 + "net/http" 8 + "slices" 9 + "time" 10 + 11 + "tangled.sh/tangled.sh/core/appview/db" 12 + "tangled.sh/tangled.sh/core/appview/reporesolver" 13 + 14 + "github.com/bluesky-social/indigo/atproto/syntax" 15 + "github.com/gorilla/feeds" 16 + ) 17 + 18 + func (rp *Repo) getRepoFeed(ctx context.Context, f *reporesolver.ResolvedRepo) (*feeds.Feed, error) { 19 + const feedLimitPerType = 100 20 + 21 + pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt())) 22 + if err != nil { 23 + return nil, err 24 + } 25 + 26 + issues, err := db.GetIssuesWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt())) 27 + if err != nil { 28 + return nil, err 29 + } 30 + 31 + feed := &feeds.Feed{ 32 + Title: fmt.Sprintf("activity feed for %s", f.OwnerSlashRepo()), 33 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, f.OwnerSlashRepo()), Type: "text/html", Rel: "alternate"}, 34 + Items: make([]*feeds.Item, 0), 35 + Updated: time.UnixMilli(0), 36 + } 37 + 38 + for _, pull := range pulls { 39 + items, err := rp.createPullItems(ctx, pull, f) 40 + if err != nil { 41 + return nil, err 42 + } 43 + feed.Items = append(feed.Items, items...) 44 + } 45 + 46 + for _, issue := range issues { 47 + item, err := rp.createIssueItem(ctx, issue, f) 48 + if err != nil { 49 + return nil, err 50 + } 51 + feed.Items = append(feed.Items, item) 52 + } 53 + 54 + slices.SortFunc(feed.Items, func(a, b *feeds.Item) int { 55 + if a.Created.After(b.Created) { 56 + return -1 57 + } 58 + return 1 59 + }) 60 + 61 + if len(feed.Items) > 0 { 62 + feed.Updated = feed.Items[0].Created 63 + } 64 + 65 + return feed, nil 66 + } 67 + 68 + func (rp *Repo) createPullItems(ctx context.Context, pull *db.Pull, f *reporesolver.ResolvedRepo) ([]*feeds.Item, error) { 69 + owner, err := rp.idResolver.ResolveIdent(ctx, pull.OwnerDid) 70 + if err != nil { 71 + return nil, err 72 + } 73 + 74 + var items []*feeds.Item 75 + 76 + state := rp.getPullState(pull) 77 + description := rp.buildPullDescription(owner.Handle, state, pull, f.OwnerSlashRepo()) 78 + 79 + mainItem := &feeds.Item{ 80 + Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title), 81 + Description: description, 82 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId)}, 83 + Created: pull.Created, 84 + Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 85 + } 86 + items = append(items, mainItem) 87 + 88 + for _, round := range pull.Submissions { 89 + if round == nil || round.RoundNumber == 0 { 90 + continue 91 + } 92 + 93 + roundItem := &feeds.Item{ 94 + Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber), 95 + Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in %s", owner.Handle, round.RoundNumber, pull.PullId, f.OwnerSlashRepo()), 96 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId, round.RoundNumber)}, 97 + Created: round.Created, 98 + Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 99 + } 100 + items = append(items, roundItem) 101 + } 102 + 103 + return items, nil 104 + } 105 + 106 + func (rp *Repo) createIssueItem(ctx context.Context, issue db.Issue, f *reporesolver.ResolvedRepo) (*feeds.Item, error) { 107 + owner, err := rp.idResolver.ResolveIdent(ctx, issue.OwnerDid) 108 + if err != nil { 109 + return nil, err 110 + } 111 + 112 + state := "closed" 113 + if issue.Open { 114 + state = "opened" 115 + } 116 + 117 + return &feeds.Item{ 118 + Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title), 119 + Description: fmt.Sprintf("@%s %s issue #%d in %s", owner.Handle, state, issue.IssueId, f.OwnerSlashRepo()), 120 + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), issue.IssueId)}, 121 + Created: issue.Created, 122 + Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)}, 123 + }, nil 124 + } 125 + 126 + func (rp *Repo) getPullState(pull *db.Pull) string { 127 + if pull.State == db.PullOpen { 128 + return "opened" 129 + } 130 + return pull.State.String() 131 + } 132 + 133 + func (rp *Repo) buildPullDescription(handle syntax.Handle, state string, pull *db.Pull, repoName string) string { 134 + base := fmt.Sprintf("@%s %s pull request #%d", handle, state, pull.PullId) 135 + 136 + if pull.State == db.PullMerged { 137 + return fmt.Sprintf("%s (on round #%d) in %s", base, pull.LastRoundNumber(), repoName) 138 + } 139 + 140 + return fmt.Sprintf("%s in %s", base, repoName) 141 + } 142 + 143 + func (rp *Repo) RepoAtomFeed(w http.ResponseWriter, r *http.Request) { 144 + f, err := rp.repoResolver.Resolve(r) 145 + if err != nil { 146 + log.Println("failed to fully resolve repo:", err) 147 + return 148 + } 149 + 150 + feed, err := rp.getRepoFeed(r.Context(), f) 151 + if err != nil { 152 + log.Println("failed to get repo feed:", err) 153 + rp.pages.Error500(w) 154 + return 155 + } 156 + 157 + atom, err := feed.ToAtom() 158 + if err != nil { 159 + rp.pages.Error500(w) 160 + return 161 + } 162 + 163 + w.Header().Set("content-type", "application/atom+xml") 164 + w.Write([]byte(atom)) 165 + }
+15 -12
appview/repo/index.go
··· 24 24 25 25 func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { 26 26 ref := chi.URLParam(r, "ref") 27 + 27 28 f, err := rp.repoResolver.Resolve(r) 28 29 if err != nil { 29 30 log.Println("failed to fully resolve repo", err) ··· 37 38 return 38 39 } 39 40 40 - result, err := us.Index(f.OwnerDid(), f.RepoName, ref) 41 + result, err := us.Index(f.OwnerDid(), f.Name, ref) 41 42 if err != nil { 42 43 rp.pages.Error503(w) 43 44 log.Println("failed to reach knotserver", err) ··· 118 119 119 120 var forkInfo *types.ForkInfo 120 121 if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) { 121 - forkInfo, err = getForkInfo(repoInfo, rp, f, user, signedClient) 122 + forkInfo, err = getForkInfo(repoInfo, rp, f, result.Ref, user, signedClient) 122 123 if err != nil { 123 124 log.Printf("Failed to fetch fork information: %v", err) 124 125 return ··· 126 127 } 127 128 128 129 // TODO: a bit dirty 129 - languageInfo, err := rp.getLanguageInfo(f, signedClient, chi.URLParam(r, "ref") == "") 130 + languageInfo, err := rp.getLanguageInfo(f, signedClient, result.Ref, ref == "") 130 131 if err != nil { 131 132 log.Printf("failed to compute language percentages: %s", err) 132 133 // non-fatal ··· 161 162 func (rp *Repo) getLanguageInfo( 162 163 f *reporesolver.ResolvedRepo, 163 164 signedClient *knotclient.SignedClient, 165 + currentRef string, 164 166 isDefaultRef bool, 165 167 ) ([]types.RepoLanguageDetails, error) { 166 168 // first attempt to fetch from db 167 169 langs, err := db.GetRepoLanguages( 168 170 rp.db, 169 - db.FilterEq("repo_at", f.RepoAt), 170 - db.FilterEq("ref", f.Ref), 171 + db.FilterEq("repo_at", f.RepoAt()), 172 + db.FilterEq("ref", currentRef), 171 173 ) 172 174 173 175 if err != nil || langs == nil { 174 176 // non-fatal, fetch langs from ks 175 - ls, err := signedClient.RepoLanguages(f.OwnerDid(), f.RepoName, f.Ref) 177 + ls, err := signedClient.RepoLanguages(f.OwnerDid(), f.Name, currentRef) 176 178 if err != nil { 177 179 return nil, err 178 180 } ··· 182 184 183 185 for l, s := range ls.Languages { 184 186 langs = append(langs, db.RepoLanguage{ 185 - RepoAt: f.RepoAt, 186 - Ref: f.Ref, 187 + RepoAt: f.RepoAt(), 188 + Ref: currentRef, 187 189 IsDefaultRef: isDefaultRef, 188 190 Language: l, 189 191 Bytes: s, ··· 234 236 repoInfo repoinfo.RepoInfo, 235 237 rp *Repo, 236 238 f *reporesolver.ResolvedRepo, 239 + currentRef string, 237 240 user *oauth.User, 238 241 signedClient *knotclient.SignedClient, 239 242 ) (*types.ForkInfo, error) { ··· 264 267 } 265 268 266 269 if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool { 267 - return branch.Name == f.Ref 270 + return branch.Name == currentRef 268 271 }) { 269 272 forkInfo.Status = types.MissingBranch 270 273 return &forkInfo, nil 271 274 } 272 275 273 - newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, f.Ref, f.Ref) 276 + newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, currentRef, currentRef) 274 277 if err != nil || newHiddenRefResp.StatusCode != http.StatusNoContent { 275 278 log.Printf("failed to update tracking branch: %s", err) 276 279 return nil, err 277 280 } 278 281 279 - hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref) 282 + hiddenRef := fmt.Sprintf("hidden/%s/%s", currentRef, currentRef) 280 283 281 284 var status types.AncestorCheckResponse 282 - forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt), repoInfo.Name, f.Ref, hiddenRef) 285 + forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt()), repoInfo.Name, currentRef, hiddenRef) 283 286 if err != nil { 284 287 log.Printf("failed to check if fork is ahead/behind: %s", err) 285 288 return nil, err
+70 -50
appview/repo/repo.go
··· 95 95 } else { 96 96 uri = "https" 97 97 } 98 - url := fmt.Sprintf("%s://%s/%s/%s/archive/%s.tar.gz", uri, f.Knot, f.OwnerDid(), f.RepoName, url.PathEscape(refParam)) 98 + url := fmt.Sprintf("%s://%s/%s/%s/archive/%s.tar.gz", uri, f.Knot, f.OwnerDid(), f.Name, url.PathEscape(refParam)) 99 99 100 100 http.Redirect(w, r, url, http.StatusFound) 101 101 } ··· 123 123 return 124 124 } 125 125 126 - repolog, err := us.Log(f.OwnerDid(), f.RepoName, ref, page) 126 + repolog, err := us.Log(f.OwnerDid(), f.Name, ref, page) 127 127 if err != nil { 128 128 log.Println("failed to reach knotserver", err) 129 129 return 130 130 } 131 131 132 - tagResult, err := us.Tags(f.OwnerDid(), f.RepoName) 132 + tagResult, err := us.Tags(f.OwnerDid(), f.Name) 133 133 if err != nil { 134 134 log.Println("failed to reach knotserver", err) 135 135 return ··· 144 144 tagMap[hash] = append(tagMap[hash], tag.Name) 145 145 } 146 146 147 - branchResult, err := us.Branches(f.OwnerDid(), f.RepoName) 147 + branchResult, err := us.Branches(f.OwnerDid(), f.Name) 148 148 if err != nil { 149 149 log.Println("failed to reach knotserver", err) 150 150 return ··· 212 212 return 213 213 } 214 214 215 - repoAt := f.RepoAt 215 + repoAt := f.RepoAt() 216 216 rkey := repoAt.RecordKey().String() 217 217 if rkey == "" { 218 218 log.Println("invalid aturi for repo", err) ··· 262 262 Record: &lexutil.LexiconTypeDecoder{ 263 263 Val: &tangled.Repo{ 264 264 Knot: f.Knot, 265 - Name: f.RepoName, 265 + Name: f.Name, 266 266 Owner: user.Did, 267 - CreatedAt: f.CreatedAt, 267 + CreatedAt: f.Created.Format(time.RFC3339), 268 268 Description: &newDescription, 269 269 Spindle: &f.Spindle, 270 270 }, ··· 310 310 return 311 311 } 312 312 313 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)) 313 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref)) 314 314 if err != nil { 315 315 log.Println("failed to reach knotserver", err) 316 316 return ··· 375 375 if !rp.config.Core.Dev { 376 376 protocol = "https" 377 377 } 378 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) 378 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, treePath)) 379 379 if err != nil { 380 380 log.Println("failed to reach knotserver", err) 381 381 return ··· 405 405 user := rp.oauth.GetUser(r) 406 406 407 407 var breadcrumbs [][]string 408 - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 408 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 409 409 if treePath != "" { 410 410 for idx, elem := range strings.Split(treePath, "/") { 411 411 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) ··· 436 436 return 437 437 } 438 438 439 - result, err := us.Tags(f.OwnerDid(), f.RepoName) 439 + result, err := us.Tags(f.OwnerDid(), f.Name) 440 440 if err != nil { 441 441 log.Println("failed to reach knotserver", err) 442 442 return 443 443 } 444 444 445 - artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt)) 445 + artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) 446 446 if err != nil { 447 447 log.Println("failed grab artifacts", err) 448 448 return ··· 493 493 return 494 494 } 495 495 496 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 496 + result, err := us.Branches(f.OwnerDid(), f.Name) 497 497 if err != nil { 498 498 log.Println("failed to reach knotserver", err) 499 499 return ··· 522 522 if !rp.config.Core.Dev { 523 523 protocol = "https" 524 524 } 525 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) 525 + resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)) 526 526 if err != nil { 527 527 log.Println("failed to reach knotserver", err) 528 528 return ··· 542 542 } 543 543 544 544 var breadcrumbs [][]string 545 - breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 545 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 546 546 if filePath != "" { 547 547 for idx, elem := range strings.Split(filePath, "/") { 548 548 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) ··· 575 575 576 576 // fetch the actual binary content like in RepoBlobRaw 577 577 578 - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 578 + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Name, ref, filePath) 579 579 contentSrc = blobURL 580 580 if !rp.config.Core.Dev { 581 581 contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL) ··· 612 612 if !rp.config.Core.Dev { 613 613 protocol = "https" 614 614 } 615 - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath) 616 - resp, err := http.Get(blobURL) 615 + 616 + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath) 617 + 618 + req, err := http.NewRequest("GET", blobURL, nil) 617 619 if err != nil { 618 - log.Println("failed to reach knotserver:", err) 620 + log.Println("failed to create request", err) 621 + return 622 + } 623 + 624 + // forward the If-None-Match header 625 + if clientETag := r.Header.Get("If-None-Match"); clientETag != "" { 626 + req.Header.Set("If-None-Match", clientETag) 627 + } 628 + 629 + client := &http.Client{} 630 + resp, err := client.Do(req) 631 + if err != nil { 632 + log.Println("failed to reach knotserver", err) 619 633 rp.pages.Error503(w) 620 634 return 621 635 } 622 636 defer resp.Body.Close() 637 + 638 + // forward 304 not modified 639 + if resp.StatusCode == http.StatusNotModified { 640 + w.WriteHeader(http.StatusNotModified) 641 + return 642 + } 623 643 624 644 if resp.StatusCode != http.StatusOK { 625 645 log.Printf("knotserver returned non-OK status for raw blob %s: %d", blobURL, resp.StatusCode) ··· 668 688 return 669 689 } 670 690 671 - repoAt := f.RepoAt 691 + repoAt := f.RepoAt() 672 692 rkey := repoAt.RecordKey().String() 673 693 if rkey == "" { 674 694 fail("Failed to resolve repo. Try again later", err) ··· 722 742 Record: &lexutil.LexiconTypeDecoder{ 723 743 Val: &tangled.Repo{ 724 744 Knot: f.Knot, 725 - Name: f.RepoName, 745 + Name: f.Name, 726 746 Owner: user.Did, 727 - CreatedAt: f.CreatedAt, 747 + CreatedAt: f.Created.Format(time.RFC3339), 728 748 Description: &f.Description, 729 749 Spindle: spindlePtr, 730 750 }, ··· 805 825 Record: &lexutil.LexiconTypeDecoder{ 806 826 Val: &tangled.RepoCollaborator{ 807 827 Subject: collaboratorIdent.DID.String(), 808 - Repo: string(f.RepoAt), 828 + Repo: string(f.RepoAt()), 809 829 CreatedAt: createdAt.Format(time.RFC3339), 810 830 }}, 811 831 }) ··· 830 850 return 831 851 } 832 852 833 - ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String()) 853 + ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.Name, collaboratorIdent.DID.String()) 834 854 if err != nil { 835 855 fail("Knot was unreachable.", err) 836 856 return ··· 864 884 Did: syntax.DID(currentUser.Did), 865 885 Rkey: rkey, 866 886 SubjectDid: collaboratorIdent.DID, 867 - RepoAt: f.RepoAt, 887 + RepoAt: f.RepoAt(), 868 888 Created: createdAt, 869 889 }) 870 890 if err != nil { ··· 902 922 log.Println("failed to get authorized client", err) 903 923 return 904 924 } 905 - repoRkey := f.RepoAt.RecordKey().String() 906 925 _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 907 926 Collection: tangled.RepoNSID, 908 927 Repo: user.Did, 909 - Rkey: repoRkey, 928 + Rkey: f.Rkey, 910 929 }) 911 930 if err != nil { 912 931 log.Printf("failed to delete record: %s", err) 913 932 rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 914 933 return 915 934 } 916 - log.Println("removed repo record ", f.RepoAt.String()) 935 + log.Println("removed repo record ", f.RepoAt().String()) 917 936 918 937 secret, err := db.GetRegistrationKey(rp.db, f.Knot) 919 938 if err != nil { ··· 927 946 return 928 947 } 929 948 930 - ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) 949 + ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.Name) 931 950 if err != nil { 932 951 log.Printf("failed to make request to %s: %s", f.Knot, err) 933 952 return ··· 973 992 } 974 993 975 994 // remove repo from db 976 - err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 995 + err = db.RemoveRepo(tx, f.OwnerDid(), f.Name) 977 996 if err != nil { 978 997 rp.pages.Notice(w, "settings-delete", "Failed to update appview") 979 998 return ··· 1022 1041 return 1023 1042 } 1024 1043 1025 - ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch) 1044 + ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.Name, branch) 1026 1045 if err != nil { 1027 1046 log.Printf("failed to make request to %s: %s", f.Knot, err) 1028 1047 return ··· 1090 1109 r.Context(), 1091 1110 spindleClient, 1092 1111 &tangled.RepoAddSecret_Input{ 1093 - Repo: f.RepoAt.String(), 1112 + Repo: f.RepoAt().String(), 1094 1113 Key: key, 1095 1114 Value: value, 1096 1115 }, ··· 1108 1127 r.Context(), 1109 1128 spindleClient, 1110 1129 &tangled.RepoRemoveSecret_Input{ 1111 - Repo: f.RepoAt.String(), 1130 + Repo: f.RepoAt().String(), 1112 1131 Key: key, 1113 1132 }, 1114 1133 ) ··· 1170 1189 // return 1171 1190 // } 1172 1191 1173 - // result, err := us.Branches(f.OwnerDid(), f.RepoName) 1192 + // result, err := us.Branches(f.OwnerDid(), f.Name) 1174 1193 // if err != nil { 1175 1194 // log.Println("failed to reach knotserver", err) 1176 1195 // return ··· 1192 1211 // oauth.WithDev(rp.config.Core.Dev), 1193 1212 // ); err != nil { 1194 1213 // log.Println("failed to create spindle client", err) 1195 - // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { 1214 + // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 1196 1215 // log.Println("failed to fetch secrets", err) 1197 1216 // } else { 1198 1217 // secrets = resp.Secrets ··· 1221 1240 return 1222 1241 } 1223 1242 1224 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1243 + result, err := us.Branches(f.OwnerDid(), f.Name) 1225 1244 if err != nil { 1226 1245 log.Println("failed to reach knotserver", err) 1227 1246 return ··· 1275 1294 oauth.WithDev(rp.config.Core.Dev), 1276 1295 ); err != nil { 1277 1296 log.Println("failed to create spindle client", err) 1278 - } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt.String()); err != nil { 1297 + } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 1279 1298 log.Println("failed to fetch secrets", err) 1280 1299 } else { 1281 1300 secrets = resp.Secrets ··· 1316 1335 } 1317 1336 1318 1337 func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1338 + ref := chi.URLParam(r, "ref") 1339 + 1319 1340 user := rp.oauth.GetUser(r) 1320 1341 f, err := rp.repoResolver.Resolve(r) 1321 1342 if err != nil { ··· 1343 1364 } else { 1344 1365 uri = "https" 1345 1366 } 1346 - forkName := fmt.Sprintf("%s", f.RepoName) 1347 - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1367 + forkName := fmt.Sprintf("%s", f.Name) 1368 + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name) 1348 1369 1349 - _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) 1370 + _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, ref) 1350 1371 if err != nil { 1351 1372 rp.pages.Notice(w, "repo", "Failed to sync repository fork.") 1352 1373 return ··· 1394 1415 return 1395 1416 } 1396 1417 1397 - forkName := fmt.Sprintf("%s", f.RepoName) 1418 + forkName := fmt.Sprintf("%s", f.Name) 1398 1419 1399 1420 // this check is *only* to see if the forked repo name already exists 1400 1421 // in the user's account. 1401 - existingRepo, err := db.GetRepo(rp.db, user.Did, f.RepoName) 1422 + existingRepo, err := db.GetRepo(rp.db, user.Did, f.Name) 1402 1423 if err != nil { 1403 1424 if errors.Is(err, sql.ErrNoRows) { 1404 1425 // no existing repo with this name found, we can use the name as is ··· 1429 1450 } else { 1430 1451 uri = "https" 1431 1452 } 1432 - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1433 - sourceAt := f.RepoAt.String() 1453 + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name) 1454 + sourceAt := f.RepoAt().String() 1434 1455 1435 1456 rkey := tid.TID() 1436 1457 repo := &db.Repo{ ··· 1499 1520 } 1500 1521 log.Println("created repo record: ", atresp.Uri) 1501 1522 1502 - repo.AtUri = atresp.Uri 1503 1523 err = db.AddRepo(tx, repo) 1504 1524 if err != nil { 1505 1525 log.Println(err) ··· 1550 1570 return 1551 1571 } 1552 1572 1553 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 1573 + result, err := us.Branches(f.OwnerDid(), f.Name) 1554 1574 if err != nil { 1555 1575 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1556 1576 log.Println("failed to reach knotserver", err) ··· 1580 1600 head = queryHead 1581 1601 } 1582 1602 1583 - tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1603 + tags, err := us.Tags(f.OwnerDid(), f.Name) 1584 1604 if err != nil { 1585 1605 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1586 1606 log.Println("failed to reach knotserver", err) ··· 1642 1662 return 1643 1663 } 1644 1664 1645 - branches, err := us.Branches(f.OwnerDid(), f.RepoName) 1665 + branches, err := us.Branches(f.OwnerDid(), f.Name) 1646 1666 if err != nil { 1647 1667 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1648 1668 log.Println("failed to reach knotserver", err) 1649 1669 return 1650 1670 } 1651 1671 1652 - tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1672 + tags, err := us.Tags(f.OwnerDid(), f.Name) 1653 1673 if err != nil { 1654 1674 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1655 1675 log.Println("failed to reach knotserver", err) 1656 1676 return 1657 1677 } 1658 1678 1659 - formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) 1679 + formatPatch, err := us.Compare(f.OwnerDid(), f.Name, base, head) 1660 1680 if err != nil { 1661 1681 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1662 1682 log.Println("failed to compare", err)
+1
appview/repo/router.go
··· 10 10 func (rp *Repo) Router(mw *middleware.Middleware) http.Handler { 11 11 r := chi.NewRouter() 12 12 r.Get("/", rp.RepoIndex) 13 + r.Get("/feed.atom", rp.RepoAtomFeed) 13 14 r.Get("/commits/{ref}", rp.RepoLog) 14 15 r.Route("/tree/{ref}", func(r chi.Router) { 15 16 r.Get("/", rp.RepoIndex)
+37 -104
appview/reporesolver/resolver.go
··· 7 7 "fmt" 8 8 "log" 9 9 "net/http" 10 - "net/url" 11 10 "path" 11 + "regexp" 12 12 "strings" 13 13 14 14 "github.com/bluesky-social/indigo/atproto/identity" 15 - "github.com/bluesky-social/indigo/atproto/syntax" 16 15 securejoin "github.com/cyphar/filepath-securejoin" 17 16 "github.com/go-chi/chi/v5" 18 17 "tangled.sh/tangled.sh/core/appview/config" ··· 21 20 "tangled.sh/tangled.sh/core/appview/pages" 22 21 "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 23 22 "tangled.sh/tangled.sh/core/idresolver" 24 - "tangled.sh/tangled.sh/core/knotclient" 25 23 "tangled.sh/tangled.sh/core/rbac" 26 24 ) 27 25 28 26 type ResolvedRepo struct { 29 - Knot string 30 - OwnerId identity.Identity 31 - RepoName string 32 - RepoAt syntax.ATURI 33 - Description string 34 - Spindle string 35 - CreatedAt string 36 - Ref string 37 - CurrentDir string 27 + db.Repo 28 + OwnerId identity.Identity 29 + CurrentDir string 30 + Ref string 38 31 39 32 rr *RepoResolver 40 33 } ··· 51 44 } 52 45 53 46 func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) { 54 - repoName := chi.URLParam(r, "repo") 55 - knot, ok := r.Context().Value("knot").(string) 47 + repo, ok := r.Context().Value("repo").(*db.Repo) 56 48 if !ok { 57 - log.Println("malformed middleware") 49 + log.Println("malformed middleware: `repo` not exist in context") 58 50 return nil, fmt.Errorf("malformed middleware") 59 51 } 60 52 id, ok := r.Context().Value("resolvedId").(identity.Identity) ··· 63 55 return nil, fmt.Errorf("malformed middleware") 64 56 } 65 57 66 - repoAt, ok := r.Context().Value("repoAt").(string) 67 - if !ok { 68 - log.Println("malformed middleware") 69 - return nil, fmt.Errorf("malformed middleware") 70 - } 71 - 72 - parsedRepoAt, err := syntax.ParseATURI(repoAt) 73 - if err != nil { 74 - log.Println("malformed repo at-uri") 75 - return nil, fmt.Errorf("malformed middleware") 76 - } 77 - 58 + currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath())) 78 59 ref := chi.URLParam(r, "ref") 79 60 80 - if ref == "" { 81 - us, err := knotclient.NewUnsignedClient(knot, rr.config.Core.Dev) 82 - if err != nil { 83 - return nil, err 84 - } 85 - 86 - defaultBranch, err := us.DefaultBranch(id.DID.String(), repoName) 87 - if err != nil { 88 - return nil, err 89 - } 90 - 91 - ref = defaultBranch.Branch 92 - } 93 - 94 - currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath(), ref)) 95 - 96 - // pass through values from the middleware 97 - description, ok := r.Context().Value("repoDescription").(string) 98 - addedAt, ok := r.Context().Value("repoAddedAt").(string) 99 - spindle, ok := r.Context().Value("repoSpindle").(string) 100 - 101 61 return &ResolvedRepo{ 102 - Knot: knot, 103 - OwnerId: id, 104 - RepoName: repoName, 105 - RepoAt: parsedRepoAt, 106 - Description: description, 107 - CreatedAt: addedAt, 108 - Ref: ref, 109 - CurrentDir: currentDir, 110 - Spindle: spindle, 62 + Repo: *repo, 63 + OwnerId: id, 64 + CurrentDir: currentDir, 65 + Ref: ref, 111 66 112 67 rr: rr, 113 68 }, nil ··· 126 81 127 82 var p string 128 83 if handle != "" && !handle.IsInvalidHandle() { 129 - p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.RepoName) 84 + p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.Name) 130 85 } else { 131 - p, _ = securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 86 + p, _ = securejoin.SecureJoin(f.OwnerDid(), f.Name) 132 87 } 133 88 134 - return p 135 - } 136 - 137 - func (f *ResolvedRepo) DidSlashRepo() string { 138 - p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 139 89 return p 140 90 } 141 91 ··· 187 137 // this function is a bit weird since it now returns RepoInfo from an entirely different 188 138 // package. we should refactor this or get rid of RepoInfo entirely. 189 139 func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo { 140 + repoAt := f.RepoAt() 190 141 isStarred := false 191 142 if user != nil { 192 - isStarred = db.GetStarStatus(f.rr.execer, user.Did, syntax.ATURI(f.RepoAt)) 143 + isStarred = db.GetStarStatus(f.rr.execer, user.Did, repoAt) 193 144 } 194 145 195 - starCount, err := db.GetStarCount(f.rr.execer, f.RepoAt) 146 + starCount, err := db.GetStarCount(f.rr.execer, repoAt) 196 147 if err != nil { 197 - log.Println("failed to get star count for ", f.RepoAt) 148 + log.Println("failed to get star count for ", repoAt) 198 149 } 199 - issueCount, err := db.GetIssueCount(f.rr.execer, f.RepoAt) 150 + issueCount, err := db.GetIssueCount(f.rr.execer, repoAt) 200 151 if err != nil { 201 - log.Println("failed to get issue count for ", f.RepoAt) 152 + log.Println("failed to get issue count for ", repoAt) 202 153 } 203 - pullCount, err := db.GetPullCount(f.rr.execer, f.RepoAt) 154 + pullCount, err := db.GetPullCount(f.rr.execer, repoAt) 204 155 if err != nil { 205 - log.Println("failed to get issue count for ", f.RepoAt) 156 + log.Println("failed to get issue count for ", repoAt) 206 157 } 207 - source, err := db.GetRepoSource(f.rr.execer, f.RepoAt) 158 + source, err := db.GetRepoSource(f.rr.execer, repoAt) 208 159 if errors.Is(err, sql.ErrNoRows) { 209 160 source = "" 210 161 } else if err != nil { 211 - log.Println("failed to get repo source for ", f.RepoAt, err) 162 + log.Println("failed to get repo source for ", repoAt, err) 212 163 } 213 164 214 165 var sourceRepo *db.Repo ··· 228 179 } 229 180 230 181 knot := f.Knot 231 - var disableFork bool 232 - us, err := knotclient.NewUnsignedClient(knot, f.rr.config.Core.Dev) 233 - if err != nil { 234 - log.Printf("failed to create unsigned client for %s: %v", knot, err) 235 - } else { 236 - result, err := us.Branches(f.OwnerDid(), f.RepoName) 237 - if err != nil { 238 - log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err) 239 - } 240 - 241 - if len(result.Branches) == 0 { 242 - disableFork = true 243 - } 244 - } 245 182 246 183 repoInfo := repoinfo.RepoInfo{ 247 184 OwnerDid: f.OwnerDid(), 248 185 OwnerHandle: f.OwnerHandle(), 249 - Name: f.RepoName, 250 - RepoAt: f.RepoAt, 186 + Name: f.Name, 187 + RepoAt: repoAt, 251 188 Description: f.Description, 252 - Ref: f.Ref, 253 189 IsStarred: isStarred, 254 190 Knot: knot, 255 191 Spindle: f.Spindle, ··· 259 195 IssueCount: issueCount, 260 196 PullCount: pullCount, 261 197 }, 262 - DisableFork: disableFork, 263 - CurrentDir: f.CurrentDir, 198 + CurrentDir: f.CurrentDir, 199 + Ref: f.Ref, 264 200 } 265 201 266 202 if sourceRepo != nil { ··· 284 220 // after the ref. for example: 285 221 // 286 222 // /@icyphox.sh/foorepo/blob/main/abc/xyz/ => abc/xyz/ 287 - func extractPathAfterRef(fullPath, ref string) string { 223 + func extractPathAfterRef(fullPath string) string { 288 224 fullPath = strings.TrimPrefix(fullPath, "/") 289 225 290 - ref = url.PathEscape(ref) 226 + // match blob/, tree/, or raw/ followed by any ref and then a slash 227 + // 228 + // captures everything after the final slash 229 + pattern := `(?:blob|tree|raw)/[^/]+/(.*)$` 291 230 292 - prefixes := []string{ 293 - fmt.Sprintf("blob/%s/", ref), 294 - fmt.Sprintf("tree/%s/", ref), 295 - fmt.Sprintf("raw/%s/", ref), 296 - } 231 + re := regexp.MustCompile(pattern) 232 + matches := re.FindStringSubmatch(fullPath) 297 233 298 - for _, prefix := range prefixes { 299 - idx := strings.Index(fullPath, prefix) 300 - if idx != -1 { 301 - return fullPath[idx+len(prefix):] 302 - } 234 + if len(matches) > 1 { 235 + return matches[1] 303 236 } 304 237 305 238 return ""
+9 -12
appview/state/git_http.go
··· 3 3 import ( 4 4 "fmt" 5 5 "io" 6 + "maps" 6 7 "net/http" 7 8 8 9 "github.com/bluesky-social/indigo/atproto/identity" 9 10 "github.com/go-chi/chi/v5" 11 + "tangled.sh/tangled.sh/core/appview/db" 10 12 ) 11 13 12 14 func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 13 15 user := r.Context().Value("resolvedId").(identity.Identity) 14 - knot := r.Context().Value("knot").(string) 15 - repo := chi.URLParam(r, "repo") 16 + repo := r.Context().Value("repo").(*db.Repo) 16 17 17 18 scheme := "https" 18 19 if s.config.Core.Dev { 19 20 scheme = "http" 20 21 } 21 22 22 - targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, knot, user.DID, repo, r.URL.RawQuery) 23 + targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 23 24 s.proxyRequest(w, r, targetURL) 24 25 25 26 } ··· 30 31 http.Error(w, "failed to resolve user", http.StatusInternalServerError) 31 32 return 32 33 } 33 - knot := r.Context().Value("knot").(string) 34 - repo := chi.URLParam(r, "repo") 34 + repo := r.Context().Value("repo").(*db.Repo) 35 35 36 36 scheme := "https" 37 37 if s.config.Core.Dev { 38 38 scheme = "http" 39 39 } 40 40 41 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, knot, user.DID, repo, r.URL.RawQuery) 41 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 42 42 s.proxyRequest(w, r, targetURL) 43 43 } 44 44 ··· 48 48 http.Error(w, "failed to resolve user", http.StatusInternalServerError) 49 49 return 50 50 } 51 - knot := r.Context().Value("knot").(string) 52 - repo := chi.URLParam(r, "repo") 51 + repo := r.Context().Value("repo").(*db.Repo) 53 52 54 53 scheme := "https" 55 54 if s.config.Core.Dev { 56 55 scheme = "http" 57 56 } 58 57 59 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, knot, user.DID, repo, r.URL.RawQuery) 58 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 60 59 s.proxyRequest(w, r, targetURL) 61 60 } 62 61 ··· 85 84 defer resp.Body.Close() 86 85 87 86 // Copy response headers 88 - for k, v := range resp.Header { 89 - w.Header()[k] = v 90 - } 87 + maps.Copy(w.Header(), resp.Header) 91 88 92 89 // Set response status code 93 90 w.WriteHeader(resp.StatusCode)
+95 -82
appview/state/profile.go
··· 89 89 log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err) 90 90 } 91 91 92 - var didsToResolve []string 93 - for _, r := range collaboratingRepos { 94 - didsToResolve = append(didsToResolve, r.Did) 95 - } 96 - for _, byMonth := range timeline.ByMonth { 97 - for _, pe := range byMonth.PullEvents.Items { 98 - didsToResolve = append(didsToResolve, pe.Repo.Did) 99 - } 100 - for _, ie := range byMonth.IssueEvents.Items { 101 - didsToResolve = append(didsToResolve, ie.Metadata.Repo.Did) 102 - } 103 - for _, re := range byMonth.RepoEvents { 104 - didsToResolve = append(didsToResolve, re.Repo.Did) 105 - if re.Source != nil { 106 - didsToResolve = append(didsToResolve, re.Source.Did) 107 - } 108 - } 109 - } 110 - 111 92 followers, following, err := db.GetFollowerFollowingCount(s.db, ident.DID.String()) 112 93 if err != nil { 113 94 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) ··· 194 175 }) 195 176 } 196 177 197 - func (s *State) feedFromRequest(w http.ResponseWriter, r *http.Request) *feeds.Feed { 178 + func (s *State) AtomFeedPage(w http.ResponseWriter, r *http.Request) { 198 179 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 199 180 if !ok { 200 181 s.pages.Error404(w) 201 - return nil 182 + return 202 183 } 203 184 204 - feed, err := s.GetProfileFeed(r.Context(), ident.Handle.String(), ident.DID.String()) 185 + feed, err := s.getProfileFeed(r.Context(), &ident) 205 186 if err != nil { 206 187 s.pages.Error500(w) 207 - return nil 188 + return 208 189 } 209 190 210 - return feed 211 - } 212 - 213 - func (s *State) AtomFeedPage(w http.ResponseWriter, r *http.Request) { 214 - feed := s.feedFromRequest(w, r) 215 191 if feed == nil { 216 192 return 217 193 } ··· 226 202 w.Write([]byte(atom)) 227 203 } 228 204 229 - func (s *State) GetProfileFeed(ctx context.Context, handle string, did string) (*feeds.Feed, error) { 230 - timeline, err := db.MakeProfileTimeline(s.db, did) 205 + func (s *State) getProfileFeed(ctx context.Context, id *identity.Identity) (*feeds.Feed, error) { 206 + timeline, err := db.MakeProfileTimeline(s.db, id.DID.String()) 231 207 if err != nil { 232 208 return nil, err 233 209 } 234 210 235 211 author := &feeds.Author{ 236 - Name: fmt.Sprintf("@%s", handle), 212 + Name: fmt.Sprintf("@%s", id.Handle), 237 213 } 238 - feed := &feeds.Feed{ 239 - Title: fmt.Sprintf("timeline feed for %s", author.Name), 240 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.AppviewHost, handle), Type: "text/html", Rel: "alternate"}, 214 + 215 + feed := feeds.Feed{ 216 + Title: fmt.Sprintf("%s's timeline", author.Name), 217 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.AppviewHost, id.Handle), Type: "text/html", Rel: "alternate"}, 241 218 Items: make([]*feeds.Item, 0), 242 219 Updated: time.UnixMilli(0), 243 220 Author: author, 244 221 } 222 + 245 223 for _, byMonth := range timeline.ByMonth { 246 - for _, pull := range byMonth.PullEvents.Items { 247 - owner, err := s.idResolver.ResolveIdent(ctx, pull.Repo.Did) 248 - if err != nil { 249 - return nil, err 250 - } 251 - feed.Items = append(feed.Items, &feeds.Item{ 252 - Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 253 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 254 - Created: pull.Created, 255 - Author: author, 256 - }) 257 - for _, submission := range pull.Submissions { 258 - feed.Items = append(feed.Items, &feeds.Item{ 259 - Title: fmt.Sprintf("%s submitted pull request '%s' (round #%d) in @%s/%s", author.Name, pull.Title, submission.RoundNumber, owner.Handle, pull.Repo.Name), 260 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 261 - Created: submission.Created, 262 - Author: author, 263 - }) 264 - } 224 + if err := s.addPullRequestItems(ctx, &feed, byMonth.PullEvents.Items, author); err != nil { 225 + return nil, err 265 226 } 266 - for _, issue := range byMonth.IssueEvents.Items { 267 - owner, err := s.idResolver.ResolveIdent(ctx, issue.Metadata.Repo.Did) 268 - if err != nil { 269 - return nil, err 270 - } 271 - feed.Items = append(feed.Items, &feeds.Item{ 272 - Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Metadata.Repo.Name), 273 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Metadata.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, 274 - Created: issue.Created, 275 - Author: author, 276 - }) 227 + if err := s.addIssueItems(ctx, &feed, byMonth.IssueEvents.Items, author); err != nil { 228 + return nil, err 277 229 } 278 - for _, repo := range byMonth.RepoEvents { 279 - var title string 280 - if repo.Source != nil { 281 - id, err := s.idResolver.ResolveIdent(ctx, repo.Source.Did) 282 - if err != nil { 283 - return nil, err 284 - } 285 - title = fmt.Sprintf("%s forked repository @%s/%s to '%s'", author.Name, id.Handle, repo.Source.Name, repo.Repo.Name) 286 - } else { 287 - title = fmt.Sprintf("%s created repository '%s'", author.Name, repo.Repo.Name) 288 - } 289 - feed.Items = append(feed.Items, &feeds.Item{ 290 - Title: title, 291 - Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.AppviewHost, handle, repo.Repo.Name), Type: "text/html", Rel: "alternate"}, 292 - Created: repo.Repo.Created, 293 - Author: author, 294 - }) 230 + if err := s.addRepoItems(ctx, &feed, byMonth.RepoEvents, author); err != nil { 231 + return nil, err 295 232 } 296 233 } 234 + 297 235 slices.SortFunc(feed.Items, func(a *feeds.Item, b *feeds.Item) int { 298 236 return int(b.Created.UnixMilli()) - int(a.Created.UnixMilli()) 299 237 }) 238 + 300 239 if len(feed.Items) > 0 { 301 240 feed.Updated = feed.Items[0].Created 302 241 } 303 242 304 - return feed, nil 243 + return &feed, nil 244 + } 245 + 246 + func (s *State) addPullRequestItems(ctx context.Context, feed *feeds.Feed, pulls []*db.Pull, author *feeds.Author) error { 247 + for _, pull := range pulls { 248 + owner, err := s.idResolver.ResolveIdent(ctx, pull.Repo.Did) 249 + if err != nil { 250 + return err 251 + } 252 + 253 + // Add pull request creation item 254 + feed.Items = append(feed.Items, s.createPullRequestItem(pull, owner, author)) 255 + } 256 + return nil 257 + } 258 + 259 + func (s *State) addIssueItems(ctx context.Context, feed *feeds.Feed, issues []*db.Issue, author *feeds.Author) error { 260 + for _, issue := range issues { 261 + owner, err := s.idResolver.ResolveIdent(ctx, issue.Metadata.Repo.Did) 262 + if err != nil { 263 + return err 264 + } 265 + 266 + feed.Items = append(feed.Items, s.createIssueItem(issue, owner, author)) 267 + } 268 + return nil 269 + } 270 + 271 + func (s *State) addRepoItems(ctx context.Context, feed *feeds.Feed, repos []db.RepoEvent, author *feeds.Author) error { 272 + for _, repo := range repos { 273 + item, err := s.createRepoItem(ctx, repo, author) 274 + if err != nil { 275 + return err 276 + } 277 + feed.Items = append(feed.Items, item) 278 + } 279 + return nil 280 + } 281 + 282 + func (s *State) createPullRequestItem(pull *db.Pull, owner *identity.Identity, author *feeds.Author) *feeds.Item { 283 + return &feeds.Item{ 284 + Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 285 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 286 + Created: pull.Created, 287 + Author: author, 288 + } 289 + } 290 + 291 + func (s *State) createIssueItem(issue *db.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item { 292 + return &feeds.Item{ 293 + Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Metadata.Repo.Name), 294 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Metadata.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, 295 + Created: issue.Created, 296 + Author: author, 297 + } 298 + } 299 + 300 + func (s *State) createRepoItem(ctx context.Context, repo db.RepoEvent, author *feeds.Author) (*feeds.Item, error) { 301 + var title string 302 + if repo.Source != nil { 303 + sourceOwner, err := s.idResolver.ResolveIdent(ctx, repo.Source.Did) 304 + if err != nil { 305 + return nil, err 306 + } 307 + title = fmt.Sprintf("%s forked repository @%s/%s to '%s'", author.Name, sourceOwner.Handle, repo.Source.Name, repo.Repo.Name) 308 + } else { 309 + title = fmt.Sprintf("%s created repository '%s'", author.Name, repo.Repo.Name) 310 + } 311 + 312 + return &feeds.Item{ 313 + Title: title, 314 + Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.AppviewHost, author.Name[1:], repo.Repo.Name), Type: "text/html", Rel: "alternate"}, // Remove @ prefix 315 + Created: repo.Repo.Created, 316 + Author: author, 317 + }, nil 305 318 } 306 319 307 320 func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) {
+11 -3
appview/state/router.go
··· 35 35 router.Get("/favicon.svg", s.Favicon) 36 36 router.Get("/favicon.ico", s.Favicon) 37 37 38 + userRouter := s.UserRouter(&middleware) 39 + standardRouter := s.StandardRouter(&middleware) 40 + 38 41 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 39 42 pat := chi.URLParam(r, "*") 40 43 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { 41 - s.UserRouter(&middleware).ServeHTTP(w, r) 44 + userRouter.ServeHTTP(w, r) 42 45 } else { 43 46 // Check if the first path element is a valid handle without '@' or a flattened DID 44 47 pathParts := strings.SplitN(pat, "/", 2) ··· 61 64 return 62 65 } 63 66 } 64 - s.StandardRouter(&middleware).ServeHTTP(w, r) 67 + standardRouter.ServeHTTP(w, r) 65 68 } 66 69 }) 67 70 ··· 75 78 r.Get("/", s.Profile) 76 79 r.Get("/feed.atom", s.AtomFeedPage) 77 80 81 + // redirect /@handle/repo.git -> /@handle/repo 82 + r.Get("/{repo}.git", func(w http.ResponseWriter, r *http.Request) { 83 + nonDotGitPath := strings.TrimSuffix(r.URL.Path, ".git") 84 + http.Redirect(w, r, nonDotGitPath, http.StatusMovedPermanently) 85 + }) 86 + 78 87 r.With(mw.ResolveRepo()).Route("/{repo}", func(r chi.Router) { 79 88 r.Use(mw.GoImport()) 80 - 81 89 r.Mount("/", s.RepoRouter(mw)) 82 90 r.Mount("/issues", s.IssuesRouter(mw)) 83 91 r.Mount("/pulls", s.PullsRouter(mw))
+16 -1
appview/state/state.go
··· 94 94 tangled.SpindleMemberNSID, 95 95 tangled.SpindleNSID, 96 96 tangled.StringNSID, 97 + tangled.RepoIssueNSID, 98 + tangled.RepoIssueCommentNSID, 97 99 }, 98 100 nil, 99 101 slog.Default(), ··· 191 193 if err != nil { 192 194 log.Println(err) 193 195 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 196 + } 197 + 198 + repos, err := db.GetTopStarredReposLastWeek(s.db) 199 + if err != nil { 200 + log.Println(err) 201 + s.pages.Notice(w, "topstarredrepos", "Unable to load.") 202 + return 194 203 } 195 204 196 205 s.pages.Timeline(w, pages.TimelineParams{ 197 206 LoggedInUser: user, 198 207 Timeline: timeline, 208 + Repos: repos, 199 209 }) 200 210 } 201 211 ··· 263 273 return nil 264 274 } 265 275 276 + func stripGitExt(name string) string { 277 + return strings.TrimSuffix(name, ".git") 278 + } 279 + 266 280 func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 267 281 switch r.Method { 268 282 case http.MethodGet: ··· 297 311 s.pages.Notice(w, "repo", err.Error()) 298 312 return 299 313 } 314 + 315 + repoName = stripGitExt(repoName) 300 316 301 317 defaultBranch := r.FormValue("branch") 302 318 if defaultBranch == "" { ··· 394 410 // continue 395 411 } 396 412 397 - repo.AtUri = atresp.Uri 398 413 err = db.AddRepo(tx, repo) 399 414 if err != nil { 400 415 log.Println(err)
+23 -12
appview/strings/strings.go
··· 7 7 "path" 8 8 "slices" 9 9 "strconv" 10 - "strings" 11 10 "time" 12 11 13 12 "tangled.sh/tangled.sh/core/api/tangled" ··· 44 43 r := chi.NewRouter() 45 44 46 45 r. 46 + Get("/", s.timeline) 47 + 48 + r. 47 49 With(mw.ResolveIdent()). 48 50 Route("/{user}", func(r chi.Router) { 49 51 r.Get("/", s.dashboard) ··· 70 72 return r 71 73 } 72 74 75 + func (s *Strings) timeline(w http.ResponseWriter, r *http.Request) { 76 + l := s.Logger.With("handler", "timeline") 77 + 78 + strings, err := db.GetStrings(s.Db, 50) 79 + if err != nil { 80 + l.Error("failed to fetch string", "err", err) 81 + w.WriteHeader(http.StatusInternalServerError) 82 + return 83 + } 84 + 85 + s.Pages.StringsTimeline(w, pages.StringTimelineParams{ 86 + LoggedInUser: s.OAuth.GetUser(r), 87 + Strings: strings, 88 + }) 89 + } 90 + 73 91 func (s *Strings) contents(w http.ResponseWriter, r *http.Request) { 74 92 l := s.Logger.With("handler", "contents") 75 93 ··· 91 109 92 110 strings, err := db.GetStrings( 93 111 s.Db, 112 + 0, 94 113 db.FilterEq("did", id.DID), 95 114 db.FilterEq("rkey", rkey), 96 115 ) ··· 154 173 155 174 all, err := db.GetStrings( 156 175 s.Db, 176 + 0, 157 177 db.FilterEq("did", id.DID), 158 178 ) 159 179 if err != nil { ··· 225 245 // get the string currently being edited 226 246 all, err := db.GetStrings( 227 247 s.Db, 248 + 0, 228 249 db.FilterEq("did", id.DID), 229 250 db.FilterEq("rkey", rkey), 230 251 ) ··· 266 287 fail("Empty filename.", nil) 267 288 return 268 289 } 269 - if !strings.Contains(filename, ".") { 270 - // TODO: make this a htmx form validation 271 - fail("No extension provided for filename.", nil) 272 - return 273 - } 274 290 275 291 content := r.FormValue("content") 276 292 if content == "" { ··· 353 369 fail("Empty filename.", nil) 354 370 return 355 371 } 356 - if !strings.Contains(filename, ".") { 357 - // TODO: make this a htmx form validation 358 - fail("No extension provided for filename.", nil) 359 - return 360 - } 361 372 362 373 content := r.FormValue("content") 363 374 if content == "" { ··· 434 445 } 435 446 436 447 if user.Did != id.DID.String() { 437 - fail("You cannot delete this gist", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String())) 448 + fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String())) 438 449 return 439 450 } 440 451
+1 -2
input.css
··· 78 78 @supports (font-variation-settings: normal) { 79 79 html { 80 80 font-feature-settings: 81 - "ss01" 1, 82 81 "kern" 1, 83 82 "liga" 1, 84 83 "cv05" 1, ··· 104 103 } 105 104 106 105 code { 107 - @apply font-mono rounded bg-gray-100 dark:bg-gray-700; 106 + @apply font-mono rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white; 108 107 } 109 108 } 110 109
+8 -10
knotserver/git/fork.go
··· 10 10 ) 11 11 12 12 func Fork(repoPath, source string) error { 13 - _, err := git.PlainClone(repoPath, true, &git.CloneOptions{ 14 - URL: source, 15 - SingleBranch: false, 16 - }) 17 - 18 - if err != nil { 13 + cloneCmd := exec.Command("git", "clone", "--bare", source, repoPath) 14 + if err := cloneCmd.Run(); err != nil { 19 15 return fmt.Errorf("failed to bare clone repository: %w", err) 20 16 } 21 17 22 - err = exec.Command("git", "-C", repoPath, "config", "receive.hideRefs", "refs/hidden").Run() 23 - if err != nil { 18 + configureCmd := exec.Command("git", "-C", repoPath, "config", "receive.hideRefs", "refs/hidden") 19 + if err := configureCmd.Run(); err != nil { 24 20 return fmt.Errorf("failed to configure hidden refs: %w", err) 25 21 } 26 22 27 23 return nil 28 24 } 29 25 30 - func (g *GitRepo) Sync(branch string) error { 26 + func (g *GitRepo) Sync() error { 27 + branch := g.h.String() 28 + 31 29 fetchOpts := &git.FetchOptions{ 32 30 RefSpecs: []config.RefSpec{ 33 - config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/heads/%s", branch, branch)), 31 + config.RefSpec("+" + branch + ":" + branch), // +refs/heads/master:refs/heads/master 34 32 }, 35 33 } 36 34
+1
knotserver/git.go
··· 129 129 // If the appview gave us the repository owner's handle we can attempt to 130 130 // construct the correct ssh url. 131 131 ownerHandle := r.Header.Get("x-tangled-repo-owner-handle") 132 + ownerHandle = strings.TrimPrefix(ownerHandle, "@") 132 133 if ownerHandle != "" && !strings.ContainsAny(ownerHandle, ":") { 133 134 hostname := d.c.Server.Hostname 134 135 if strings.Contains(hostname, ":") {
+2 -2
knotserver/handler.go
··· 142 142 r.Delete("/", h.RemoveRepo) 143 143 r.Route("/fork", func(r chi.Router) { 144 144 r.Post("/", h.RepoFork) 145 - r.Post("/sync/{branch}", h.RepoForkSync) 146 - r.Get("/sync/{branch}", h.RepoForkAheadBehind) 145 + r.Post("/sync/*", h.RepoForkSync) 146 + r.Get("/sync/*", h.RepoForkAheadBehind) 147 147 }) 148 148 }) 149 149
+16 -11
knotserver/routes.go
··· 286 286 mimeType = "image/svg+xml" 287 287 } 288 288 289 + contentHash := sha256.Sum256(contents) 290 + eTag := fmt.Sprintf("\"%x\"", contentHash) 291 + 289 292 // allow image, video, and text/plain files to be served directly 290 293 switch { 291 - case strings.HasPrefix(mimeType, "image/"): 292 - // allowed 293 - case strings.HasPrefix(mimeType, "video/"): 294 - // allowed 294 + case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 295 + if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 296 + w.WriteHeader(http.StatusNotModified) 297 + return 298 + } 299 + w.Header().Set("ETag", eTag) 300 + 295 301 case strings.HasPrefix(mimeType, "text/plain"): 296 - // allowed 302 + w.Header().Set("Cache-Control", "public, no-cache") 303 + 297 304 default: 298 305 l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 299 306 writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden) 300 307 return 301 308 } 302 309 303 - w.Header().Set("Cache-Control", "public, max-age=86400") // cache for 24 hours 304 - w.Header().Set("ETag", fmt.Sprintf("%x", sha256.Sum256(contents))) 305 310 w.Header().Set("Content-Type", mimeType) 306 311 w.Write(contents) 307 312 } ··· 710 715 } 711 716 712 717 func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) { 713 - l := h.l.With("handler", "RepoForkSync") 718 + l := h.l.With("handler", "RepoForkAheadBehind") 714 719 715 720 data := struct { 716 721 Did string `json:"did"` ··· 845 850 name = filepath.Base(source) 846 851 } 847 852 848 - branch := chi.URLParam(r, "branch") 853 + branch := chi.URLParam(r, "*") 849 854 branch, _ = url.PathUnescape(branch) 850 855 851 856 relativeRepoPath := filepath.Join(did, name) 852 857 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) 853 858 854 - gr, err := git.PlainOpen(repoPath) 859 + gr, err := git.Open(repoPath, branch) 855 860 if err != nil { 856 861 log.Println(err) 857 862 notFound(w) 858 863 return 859 864 } 860 865 861 - err = gr.Sync(branch) 866 + err = gr.Sync() 862 867 if err != nil { 863 868 l.Error("error syncing repo fork", "error", err.Error()) 864 869 writeError(w, err.Error(), http.StatusInternalServerError)
+1 -8
lexicons/issue/comment.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": [ 13 - "issue", 14 - "body", 15 - "createdAt" 16 - ], 12 + "required": ["issue", "body", "createdAt"], 17 13 "properties": { 18 14 "issue": { 19 15 "type": "string", ··· 22 18 "repo": { 23 19 "type": "string", 24 20 "format": "at-uri" 25 - }, 26 - "commentId": { 27 - "type": "integer" 28 21 }, 29 22 "owner": { 30 23 "type": "string",
+1 -10
lexicons/issue/issue.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": [ 13 - "repo", 14 - "issueId", 15 - "owner", 16 - "title", 17 - "createdAt" 18 - ], 12 + "required": ["repo", "owner", "title", "createdAt"], 19 13 "properties": { 20 14 "repo": { 21 15 "type": "string", 22 16 "format": "at-uri" 23 - }, 24 - "issueId": { 25 - "type": "integer" 26 17 }, 27 18 "owner": { 28 19 "type": "string",
+7 -6
spindle/engines/nixery/engine.go
··· 195 195 io.Copy(os.Stdout, reader) 196 196 197 197 resp, err := e.docker.ContainerCreate(ctx, &container.Config{ 198 - Image: addl.image, 199 - Cmd: []string{"cat"}, 200 - OpenStdin: true, // so cat stays alive :3 201 - Tty: false, 202 - Hostname: "spindle", 198 + Image: addl.image, 199 + Cmd: []string{"cat"}, 200 + OpenStdin: true, // so cat stays alive :3 201 + Tty: false, 202 + Hostname: "spindle", 203 + WorkingDir: workspaceDir, 203 204 // TODO(winter): investigate whether environment variables passed here 204 205 // get propagated to ContainerExec processes 205 206 }, &container.HostConfig{ ··· 304 305 envs.AddEnv(k, v) 305 306 } 306 307 envs.AddEnv("HOME", homeDir) 307 - e.l.Debug("envs for step", "step", step.Name, "envs", envs.Slice()) 308 308 309 309 mkExecResp, err := e.docker.ContainerExecCreate(ctx, addl.container, container.ExecOptions{ 310 310 Cmd: []string{"bash", "-c", step.command}, 311 311 AttachStdout: true, 312 312 AttachStderr: true, 313 + Env: envs, 313 314 }) 314 315 if err != nil { 315 316 return fmt.Errorf("creating exec: %w", err)