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

Compare changes

Choose any two refs to compare.

Changed files
+207 -300
appview
db
issues
pages
markup
templates
repo
state
docs
knotserver
nix
+8 -161
appview/db/issues.go
··· 359 repoMap[string(repos[i].RepoAt())] = &repos[i] 360 } 361 362 - for issueAt := range issueMap { 363 - i := issueMap[issueAt] 364 - r := repoMap[string(i.RepoAt)] 365 - i.Repo = r 366 } 367 368 // collect comments ··· 391 return issues, nil 392 } 393 394 - func GetIssuesWithLimit(e Execer, limit int, filters ...filter) ([]Issue, error) { 395 - issues := make([]Issue, 0, limit) 396 - 397 - var conditions []string 398 - var args []any 399 - for _, filter := range filters { 400 - conditions = append(conditions, filter.Condition()) 401 - args = append(args, filter.Arg()...) 402 - } 403 - 404 - whereClause := "" 405 - if conditions != nil { 406 - whereClause = " where " + strings.Join(conditions, " and ") 407 - } 408 - limitClause := "" 409 - if limit != 0 { 410 - limitClause = fmt.Sprintf(" limit %d ", limit) 411 - } 412 - 413 - query := fmt.Sprintf( 414 - `select 415 - i.id, 416 - i.owner_did, 417 - i.repo_at, 418 - i.issue_id, 419 - i.created, 420 - i.title, 421 - i.body, 422 - i.open 423 - from 424 - issues i 425 - %s 426 - order by 427 - i.created desc 428 - %s`, 429 - whereClause, limitClause) 430 - 431 - rows, err := e.Query(query, args...) 432 - if err != nil { 433 - return nil, err 434 - } 435 - defer rows.Close() 436 - 437 - for rows.Next() { 438 - var issue Issue 439 - var issueCreatedAt string 440 - err := rows.Scan( 441 - &issue.Id, 442 - &issue.Did, 443 - &issue.RepoAt, 444 - &issue.IssueId, 445 - &issueCreatedAt, 446 - &issue.Title, 447 - &issue.Body, 448 - &issue.Open, 449 - ) 450 - if err != nil { 451 - return nil, err 452 - } 453 - 454 - issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt) 455 - if err != nil { 456 - return nil, err 457 - } 458 - issue.Created = issueCreatedTime 459 - 460 - issues = append(issues, issue) 461 - } 462 - 463 - if err := rows.Err(); err != nil { 464 - return nil, err 465 - } 466 - 467 - return issues, nil 468 - } 469 - 470 func GetIssues(e Execer, filters ...filter) ([]Issue, error) { 471 return GetIssuesPaginated(e, pagination.FirstPage(), filters...) 472 - } 473 - 474 - // timeframe here is directly passed into the sql query filter, and any 475 - // timeframe in the past should be negative; e.g.: "-3 months" 476 - func GetIssuesByOwnerDid(e Execer, ownerDid string, timeframe string) ([]Issue, error) { 477 - var issues []Issue 478 - 479 - rows, err := e.Query( 480 - `select 481 - i.id, 482 - i.owner_did, 483 - i.rkey, 484 - i.repo_at, 485 - i.issue_id, 486 - i.created, 487 - i.title, 488 - i.body, 489 - i.open, 490 - r.did, 491 - r.name, 492 - r.knot, 493 - r.rkey, 494 - r.created 495 - from 496 - issues i 497 - join 498 - repos r on i.repo_at = r.at_uri 499 - where 500 - i.owner_did = ? and i.created >= date ('now', ?) 501 - order by 502 - i.created desc`, 503 - ownerDid, timeframe) 504 - if err != nil { 505 - return nil, err 506 - } 507 - defer rows.Close() 508 - 509 - for rows.Next() { 510 - var issue Issue 511 - var issueCreatedAt, repoCreatedAt string 512 - var repo Repo 513 - err := rows.Scan( 514 - &issue.Id, 515 - &issue.Did, 516 - &issue.Rkey, 517 - &issue.RepoAt, 518 - &issue.IssueId, 519 - &issueCreatedAt, 520 - &issue.Title, 521 - &issue.Body, 522 - &issue.Open, 523 - &repo.Did, 524 - &repo.Name, 525 - &repo.Knot, 526 - &repo.Rkey, 527 - &repoCreatedAt, 528 - ) 529 - if err != nil { 530 - return nil, err 531 - } 532 - 533 - issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt) 534 - if err != nil { 535 - return nil, err 536 - } 537 - issue.Created = issueCreatedTime 538 - 539 - repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt) 540 - if err != nil { 541 - return nil, err 542 - } 543 - repo.Created = repoCreatedTime 544 - 545 - issues = append(issues, issue) 546 - } 547 - 548 - if err := rows.Err(); err != nil { 549 - return nil, err 550 - } 551 - 552 - return issues, nil 553 } 554 555 func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) {
··· 359 repoMap[string(repos[i].RepoAt())] = &repos[i] 360 } 361 362 + for issueAt, i := range issueMap { 363 + if r, ok := repoMap[string(i.RepoAt)]; ok { 364 + i.Repo = r 365 + } else { 366 + // do not show up the issue if the repo is deleted 367 + // TODO: foreign key where? 368 + delete(issueMap, issueAt) 369 + } 370 } 371 372 // collect comments ··· 395 return issues, nil 396 } 397 398 func GetIssues(e Execer, filters ...filter) ([]Issue, error) { 399 return GetIssuesPaginated(e, pagination.FirstPage(), filters...) 400 } 401 402 func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) {
+7 -3
appview/db/profile.go
··· 132 *items = append(*items, &pull) 133 } 134 135 - issues, err := GetIssuesByOwnerDid(e, forDid, timeframe) 136 if err != nil { 137 return nil, fmt.Errorf("error getting issues by owner did: %w", err) 138 } ··· 549 query = `select count(id) from pulls where owner_did = ? and state = ?` 550 args = append(args, did, PullOpen) 551 case VanityStatOpenIssueCount: 552 - query = `select count(id) from issues where owner_did = ? and open = 1` 553 args = append(args, did) 554 case VanityStatClosedIssueCount: 555 - query = `select count(id) from issues where owner_did = ? and open = 0` 556 args = append(args, did) 557 case VanityStatRepositoryCount: 558 query = `select count(id) from repos where did = ?`
··· 132 *items = append(*items, &pull) 133 } 134 135 + issues, err := GetIssues( 136 + e, 137 + FilterEq("did", forDid), 138 + FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)), 139 + ) 140 if err != nil { 141 return nil, fmt.Errorf("error getting issues by owner did: %w", err) 142 } ··· 553 query = `select count(id) from pulls where owner_did = ? and state = ?` 554 args = append(args, did, PullOpen) 555 case VanityStatOpenIssueCount: 556 + query = `select count(id) from issues where did = ? and open = 1` 557 args = append(args, did) 558 case VanityStatClosedIssueCount: 559 + query = `select count(id) from issues where did = ? and open = 0` 560 args = append(args, did) 561 case VanityStatRepositoryCount: 562 query = `select count(id) from repos where did = ?`
+35 -24
appview/issues/issues.go
··· 198 199 func (rp *Issues) DeleteIssue(w http.ResponseWriter, r *http.Request) { 200 l := rp.logger.With("handler", "DeleteIssue") 201 user := rp.oauth.GetUser(r) 202 f, err := rp.repoResolver.Resolve(r) 203 if err != nil { 204 - log.Println("failed to get repo and knot", err) 205 return 206 } 207 208 issue, ok := r.Context().Value("issue").(*db.Issue) 209 if !ok { 210 l.Error("failed to get issue") 211 - rp.pages.Error404(w) 212 return 213 } 214 215 - switch r.Method { 216 - case http.MethodGet: 217 - rp.pages.EditIssueFragment(w, pages.EditIssueParams{ 218 - LoggedInUser: user, 219 - RepoInfo: f.RepoInfo(user), 220 - Issue: issue, 221 - }) 222 - case http.MethodPost: 223 } 224 } 225 226 func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) { ··· 338 replyToUri := r.FormValue("reply-to") 339 var replyTo *string 340 if replyToUri != "" { 341 - uri, err := syntax.ParseATURI(replyToUri) 342 - if err != nil { 343 - l.Error("failed to get parse replyTo", "err", err, "replyTo", replyToUri) 344 - rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 345 - return 346 - } 347 - if uri.Collection() != tangled.RepoIssueCommentNSID { 348 - l.Error("invalid replyTo collection", "collection", uri.Collection()) 349 - rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 350 - return 351 - } 352 - u := uri.String() 353 - replyTo = &u 354 } 355 356 comment := db.IssueComment{ ··· 697 return 698 } 699 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 700 - Collection: tangled.GraphFollowNSID, 701 Repo: user.Did, 702 Rkey: comment.Rkey, 703 })
··· 198 199 func (rp *Issues) DeleteIssue(w http.ResponseWriter, r *http.Request) { 200 l := rp.logger.With("handler", "DeleteIssue") 201 + noticeId := "issue-actions-error" 202 + 203 user := rp.oauth.GetUser(r) 204 + 205 f, err := rp.repoResolver.Resolve(r) 206 if err != nil { 207 + l.Error("failed to get repo and knot", "err", err) 208 return 209 } 210 211 issue, ok := r.Context().Value("issue").(*db.Issue) 212 if !ok { 213 l.Error("failed to get issue") 214 + rp.pages.Notice(w, noticeId, "Failed to delete issue.") 215 + return 216 + } 217 + l = l.With("did", issue.Did, "rkey", issue.Rkey) 218 + 219 + // delete from PDS 220 + client, err := rp.oauth.AuthorizedClient(r) 221 + if err != nil { 222 + log.Println("failed to get authorized client", err) 223 + rp.pages.Notice(w, "issue-comment", "Failed to delete comment.") 224 + return 225 + } 226 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 227 + Collection: tangled.RepoIssueNSID, 228 + Repo: issue.Did, 229 + Rkey: issue.Rkey, 230 + }) 231 + if err != nil { 232 + // TODO: transact this better 233 + l.Error("failed to delete issue from PDS", "err", err) 234 + rp.pages.Notice(w, noticeId, "Failed to delete issue.") 235 return 236 } 237 238 + // delete from db 239 + if err := db.DeleteIssues(rp.db, db.FilterEq("id", issue.Id)); err != nil { 240 + l.Error("failed to delete issue", "err", err) 241 + rp.pages.Notice(w, noticeId, "Failed to delete issue.") 242 + return 243 } 244 + 245 + // return to all issues page 246 + rp.pages.HxRedirect(w, "/"+f.RepoInfo(user).FullName()+"/issues") 247 } 248 249 func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) { ··· 361 replyToUri := r.FormValue("reply-to") 362 var replyTo *string 363 if replyToUri != "" { 364 + replyTo = &replyToUri 365 } 366 367 comment := db.IssueComment{ ··· 708 return 709 } 710 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 711 + Collection: tangled.RepoIssueCommentNSID, 712 Repo: user.Did, 713 Rkey: comment.Rkey, 714 })
+1 -1
appview/pages/markup/markdown.go
··· 235 repoName := fmt.Sprintf("%s/%s", rctx.RepoInfo.OwnerDid, rctx.RepoInfo.Name) 236 237 query := fmt.Sprintf("repo=%s&ref=%s&path=%s&raw=true", 238 - repoName, url.PathEscape(rctx.RepoInfo.Ref), actualPath) 239 240 parsedURL := &url.URL{ 241 Scheme: scheme,
··· 235 repoName := fmt.Sprintf("%s/%s", rctx.RepoInfo.OwnerDid, rctx.RepoInfo.Name) 236 237 query := fmt.Sprintf("repo=%s&ref=%s&path=%s&raw=true", 238 + url.PathEscape(repoName), url.PathEscape(rctx.RepoInfo.Ref), actualPath) 239 240 parsedURL := &url.URL{ 241 Scheme: scheme,
+1 -1
appview/pages/templates/banner.html
··· 30 <div class="mx-6"> 31 These services may not be fully accessible until upgraded. 32 <a class="underline text-red-800 dark:text-red-200" 33 - href="https://tangled.sh/@tangled.sh/core/tree/master/docs/migrations/"> 34 Click to read the upgrade guide</a>. 35 </div> 36 </details>
··· 30 <div class="mx-6"> 31 These services may not be fully accessible until upgraded. 32 <a class="underline text-red-800 dark:text-red-200" 33 + href="https://tangled.sh/@tangled.sh/core/tree/master/docs/migrations.md"> 34 Click to read the upgrade guide</a>. 35 </div> 36 </details>
+8
appview/pages/templates/fragments/logotype.html
···
··· 1 + {{ define "fragments/logotype" }} 2 + <span class="flex items-center gap-2"> 3 + <span class="font-bold italic">tangled</span> 4 + <span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1"> 5 + alpha 6 + </span> 7 + <span> 8 + {{ end }}
+3 -6
appview/pages/templates/knots/index.html
··· 1 {{ define "title" }}knots{{ end }} 2 3 {{ define "content" }} 4 - <div class="px-6 py-4 flex items-end justify-start gap-4 align-bottom"> 5 <h1 class="text-xl font-bold dark:text-white">Knots</h1> 6 - 7 - <span class="flex items-center gap-1 text-sm"> 8 {{ i "book" "w-3 h-3" }} 9 - <a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/knot-hosting.md"> 10 - docs 11 - </a> 12 </span> 13 </div> 14
··· 1 {{ define "title" }}knots{{ end }} 2 3 {{ define "content" }} 4 + <div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom"> 5 <h1 class="text-xl font-bold dark:text-white">Knots</h1> 6 + <span class="flex items-center gap-1"> 7 {{ i "book" "w-3 h-3" }} 8 + <a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/knot-hosting.md">docs</a> 9 </span> 10 </div> 11
+4 -4
appview/pages/templates/layouts/base.html
··· 21 <title>{{ block "title" . }}{{ end }} ยท tangled</title> 22 {{ block "extrameta" . }}{{ end }} 23 </head> 24 - <body class="min-h-screen grid grid-cols-1 grid-rows-[min-content_auto_min-content] md:grid-cols-12 gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200"> 25 {{ block "topbarLayout" . }} 26 - <header class="px-1 col-span-1 md:col-start-3 md:col-span-8" style="z-index: 20;"> 27 28 {{ if .LoggedInUser }} 29 <div id="upgrade-banner" ··· 37 {{ end }} 38 39 {{ block "mainLayout" . }} 40 - <div class="px-1 col-span-1 md:col-start-3 md:col-span-8 flex flex-col gap-4"> 41 {{ block "contentLayout" . }} 42 <main class="col-span-1 md:col-span-8"> 43 {{ block "content" . }}{{ end }} ··· 53 {{ end }} 54 55 {{ block "footerLayout" . }} 56 - <footer class="px-1 col-span-1 md:col-start-3 md:col-span-8 mt-12"> 57 {{ template "layouts/fragments/footer" . }} 58 </footer> 59 {{ end }}
··· 21 <title>{{ block "title" . }}{{ end }} ยท tangled</title> 22 {{ block "extrameta" . }}{{ end }} 23 </head> 24 + <body class="min-h-screen grid grid-cols-1 grid-rows-[min-content_auto_min-content] md:grid-cols-10 lg:grid-cols-12 gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200"> 25 {{ block "topbarLayout" . }} 26 + <header class="px-1 col-span-1 md:col-start-2 md:col-span-8 lg:col-start-3" style="z-index: 20;"> 27 28 {{ if .LoggedInUser }} 29 <div id="upgrade-banner" ··· 37 {{ end }} 38 39 {{ block "mainLayout" . }} 40 + <div class="px-1 col-span-1 md:col-start-2 md:col-span-8 lg:col-start-3 flex flex-col gap-4"> 41 {{ block "contentLayout" . }} 42 <main class="col-span-1 md:col-span-8"> 43 {{ block "content" . }}{{ end }} ··· 53 {{ end }} 54 55 {{ block "footerLayout" . }} 56 + <footer class="px-1 col-span-1 md:col-start-2 md:col-span-8 lg:col-start-3 mt-12"> 57 {{ template "layouts/fragments/footer" . }} 58 </footer> 59 {{ end }}
+1 -3
appview/pages/templates/layouts/fragments/topbar.html
··· 2 <nav class="space-x-4 px-6 py-2 rounded bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm"> 3 <div class="flex justify-between p-0 items-center"> 4 <div id="left-items"> 5 - <a href="/" hx-boost="true" class="flex gap-2 font-bold italic"> 6 - tangled<sub>alpha</sub> 7 - </a> 8 </div> 9 10 <div id="right-items" class="flex items-center gap-2">
··· 2 <nav class="space-x-4 px-6 py-2 rounded bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm"> 3 <div class="flex justify-between p-0 items-center"> 4 <div id="left-items"> 5 + <a href="/" hx-boost="true" class="text-lg">{{ template "fragments/logotype" }}</a> 6 </div> 7 8 <div id="right-items" class="flex items-center gap-2">
+3 -4
appview/pages/templates/repo/issues/issue.html
··· 56 {{ template "issueActions" . }} 57 {{ end }} 58 </div> 59 {{ end }} 60 61 {{ define "issueActions" }} ··· 76 {{ define "deleteIssue" }} 77 <a 78 class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group" 79 - hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/delete" 80 hx-confirm="Are you sure you want to delete your issue?" 81 - hx-swap="innerHTML" 82 - hx-target="#comment-body-{{.Issue.IssueId}}" 83 - > 84 {{ i "trash-2" "size-3" }} 85 {{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }} 86 </a>
··· 56 {{ template "issueActions" . }} 57 {{ end }} 58 </div> 59 + <div id="issue-actions-error" class="error"></div> 60 {{ end }} 61 62 {{ define "issueActions" }} ··· 77 {{ define "deleteIssue" }} 78 <a 79 class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group" 80 + hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/" 81 hx-confirm="Are you sure you want to delete your issue?" 82 + hx-swap="none"> 83 {{ i "trash-2" "size-3" }} 84 {{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }} 85 </a>
+3 -7
appview/pages/templates/spindles/index.html
··· 1 {{ define "title" }}spindles{{ end }} 2 3 {{ define "content" }} 4 - <div class="px-6 py-4 flex items-end justify-start gap-4 align-bottom"> 5 <h1 class="text-xl font-bold dark:text-white">Spindles</h1> 6 - 7 - 8 - <span class="flex items-center gap-1 text-sm"> 9 {{ i "book" "w-3 h-3" }} 10 - <a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md"> 11 - docs 12 - </a> 13 </span> 14 </div> 15
··· 1 {{ define "title" }}spindles{{ end }} 2 3 {{ define "content" }} 4 + <div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom"> 5 <h1 class="text-xl font-bold dark:text-white">Spindles</h1> 6 + <span class="flex items-center gap-1"> 7 {{ i "book" "w-3 h-3" }} 8 + <a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md">docs</a> 9 </span> 10 </div> 11
+1 -1
appview/pages/templates/timeline/fragments/hero.html
··· 23 24 <figure class="w-full hidden md:block md:w-auto"> 25 <a href="https://tangled.sh/@tangled.sh/core" class="block"> 26 - <img src="https://assets.tangled.network/hero-repo.png" alt="Screenshot of the Tangled monorepo." class="max-w-md mx-auto md:max-w-none w-full md:w-[30vw] h-auto shadow-sm rounded hover:shadow-md transition-shadow" /> 27 </a> 28 <figcaption class="text-sm text-gray-600 dark:text-gray-400 mt-2 text-center"> 29 Monorepo for Tangled, built in the open with the community.
··· 23 24 <figure class="w-full hidden md:block md:w-auto"> 25 <a href="https://tangled.sh/@tangled.sh/core" class="block"> 26 + <img src="https://assets.tangled.network/hero-repo.png" alt="Screenshot of the Tangled monorepo." class="max-w-md mx-auto md:max-w-none w-full md:w-[30vw] h-auto shadow-sm rounded" /> 27 </a> 28 <figcaption class="text-sm text-gray-600 dark:text-gray-400 mt-2 text-center"> 29 Monorepo for Tangled, built in the open with the community.
+3 -3
appview/pages/templates/timeline/home.html
··· 27 {{ define "feature" }} 28 {{ $info := index . 0 }} 29 {{ $bullets := index . 1 }} 30 - <div class="flex flex-col items-top gap-6 md:flex-row md:gap-12"> 31 <div class="flex-1"> 32 <h2 class="text-2xl font-bold text-black dark:text-white mb-6">{{ $info.title }}</h2> 33 <ul class="leading-normal"> ··· 38 </div> 39 <div class="flex-shrink-0 w-96 md:w-1/3"> 40 <a href="{{ $info.image }}"> 41 - <img src="{{ $info.image }}" alt="{{ $info.alt }}" class="w-full h-auto rounded" /> 42 </a> 43 </div> 44 </div> 45 {{ end }} 46 47 {{ define "features" }} 48 - <div class="prose dark:text-gray-200 space-y-12 px-6 py-4"> 49 {{ template "feature" (list 50 (dict 51 "title" "lightweight git repo hosting"
··· 27 {{ define "feature" }} 28 {{ $info := index . 0 }} 29 {{ $bullets := index . 1 }} 30 + <div class="flex flex-col items-center gap-6 md:flex-row md:items-top"> 31 <div class="flex-1"> 32 <h2 class="text-2xl font-bold text-black dark:text-white mb-6">{{ $info.title }}</h2> 33 <ul class="leading-normal"> ··· 38 </div> 39 <div class="flex-shrink-0 w-96 md:w-1/3"> 40 <a href="{{ $info.image }}"> 41 + <img src="{{ $info.image }}" alt="{{ $info.alt }}" class="w-full h-auto rounded shadow-sm" /> 42 </a> 43 </div> 44 </div> 45 {{ end }} 46 47 {{ define "features" }} 48 + <div class="prose dark:text-gray-200 space-y-12 px-6 py-4 bg-white dark:bg-gray-800 rounded drop-shadow-sm"> 49 {{ template "feature" (list 50 (dict 51 "title" "lightweight git repo hosting"
+2 -4
appview/pages/templates/user/completeSignup.html
··· 29 </head> 30 <body class="flex items-center justify-center min-h-screen"> 31 <main class="max-w-md px-6 -mt-4"> 32 - <h1 33 - class="text-center text-2xl font-semibold italic dark:text-white" 34 - > 35 - tangled 36 </h1> 37 <h2 class="text-center text-xl italic dark:text-white"> 38 tightly-knit social coding.
··· 29 </head> 30 <body class="flex items-center justify-center min-h-screen"> 31 <main class="max-w-md px-6 -mt-4"> 32 + <h1 class="flex place-content-center text-2xl font-semibold italic dark:text-white" > 33 + {{ template "fragments/logotype" }} 34 </h1> 35 <h2 class="text-center text-xl italic dark:text-white"> 36 tightly-knit social coding.
+2 -2
appview/pages/templates/user/login.html
··· 13 </head> 14 <body class="flex items-center justify-center min-h-screen"> 15 <main class="max-w-md px-6 -mt-4"> 16 - <h1 class="text-center text-2xl font-semibold italic dark:text-white" > 17 - tangled 18 </h1> 19 <h2 class="text-center text-xl italic dark:text-white"> 20 tightly-knit social coding.
··· 13 </head> 14 <body class="flex items-center justify-center min-h-screen"> 15 <main class="max-w-md px-6 -mt-4"> 16 + <h1 class="flex place-content-center text-2xl font-semibold italic dark:text-white" > 17 + {{ template "fragments/logotype" }} 18 </h1> 19 <h2 class="text-center text-xl italic dark:text-white"> 20 tightly-knit social coding.
+2 -2
appview/pages/templates/user/overview.html
··· 115 </summary> 116 <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 117 {{ range $items }} 118 - {{ $repoOwner := resolve .Metadata.Repo.Did }} 119 - {{ $repoName := .Metadata.Repo.Name }} 120 {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 121 122 <div class="flex gap-2 text-gray-600 dark:text-gray-300">
··· 115 </summary> 116 <div class="py-2 text-sm flex flex-col gap-3 mb-2"> 117 {{ range $items }} 118 + {{ $repoOwner := resolve .Repo.Did }} 119 + {{ $repoName := .Repo.Name }} 120 {{ $repoUrl := printf "%s/%s" $repoOwner $repoName }} 121 122 <div class="flex gap-2 text-gray-600 dark:text-gray-300">
+3 -1
appview/pages/templates/user/signup.html
··· 13 </head> 14 <body class="flex items-center justify-center min-h-screen"> 15 <main class="max-w-md px-6 -mt-4"> 16 - <h1 class="text-center text-2xl font-semibold italic dark:text-white" >tangled</h1> 17 <h2 class="text-center text-xl italic dark:text-white">tightly-knit social coding.</h2> 18 <form 19 class="mt-4 max-w-sm mx-auto"
··· 13 </head> 14 <body class="flex items-center justify-center min-h-screen"> 15 <main class="max-w-md px-6 -mt-4"> 16 + <h1 class="flex place-content-center text-2xl font-semibold italic dark:text-white" > 17 + {{ template "fragments/logotype" }} 18 + </h1> 19 <h2 class="text-center text-xl italic dark:text-white">tightly-knit social coding.</h2> 20 <form 21 class="mt-4 max-w-sm mx-auto"
+6 -1
appview/repo/feed.go
··· 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" ··· 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 }
··· 9 "time" 10 11 "tangled.sh/tangled.sh/core/appview/db" 12 + "tangled.sh/tangled.sh/core/appview/pagination" 13 "tangled.sh/tangled.sh/core/appview/reporesolver" 14 15 "github.com/bluesky-social/indigo/atproto/syntax" ··· 24 return nil, err 25 } 26 27 + issues, err := db.GetIssuesPaginated( 28 + rp.db, 29 + pagination.Page{Limit: feedLimitPerType}, 30 + db.FilterEq("repo_at", f.RepoAt()), 31 + ) 32 if err != nil { 33 return nil, err 34 }
+1 -2
appview/repo/repo.go
··· 11 "log/slog" 12 "net/http" 13 "net/url" 14 - "path" 15 "path/filepath" 16 "slices" 17 "strconv" ··· 710 } 711 712 // fetch the raw binary content using sh.tangled.repo.blob xrpc 713 - repoName := path.Join("%s/%s", f.OwnerDid(), f.Name) 714 blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true", 715 scheme, f.Knot, url.QueryEscape(repoName), url.QueryEscape(ref), url.QueryEscape(filePath)) 716
··· 11 "log/slog" 12 "net/http" 13 "net/url" 14 "path/filepath" 15 "slices" 16 "strconv" ··· 709 } 710 711 // fetch the raw binary content using sh.tangled.repo.blob xrpc 712 + repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 713 blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true", 714 scheme, f.Knot, url.QueryEscape(repoName), url.QueryEscape(ref), url.QueryEscape(filePath)) 715
+10 -9
appview/state/profile.go
··· 17 "github.com/gorilla/feeds" 18 "tangled.sh/tangled.sh/core/api/tangled" 19 "tangled.sh/tangled.sh/core/appview/db" 20 - // "tangled.sh/tangled.sh/core/appview/oauth" 21 "tangled.sh/tangled.sh/core/appview/pages" 22 ) 23 ··· 284 l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) 285 286 loggedInUser := s.oauth.GetUser(r) 287 288 follows, err := fetchFollows(s.db, profile.UserDid) 289 if err != nil { 290 l.Error("failed to fetch follows", "err", err) 291 - return nil, err 292 } 293 294 if len(follows) == 0 { 295 - return nil, nil 296 } 297 298 followDids := make([]string, 0, len(follows)) ··· 303 profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids)) 304 if err != nil { 305 l.Error("failed to get profiles", "followDids", followDids, "err", err) 306 - return nil, err 307 } 308 309 followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids) ··· 316 following, err := db.GetFollowing(s.db, loggedInUser.Did) 317 if err != nil { 318 l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did) 319 - return nil, err 320 } 321 loggedInUserFollowing = make(map[string]struct{}, len(following)) 322 for _, follow := range following { ··· 350 } 351 } 352 353 - return &FollowsPageParams{ 354 - Follows: followCards, 355 - Card: profile, 356 - }, nil 357 } 358 359 func (s *State) followersPage(w http.ResponseWriter, r *http.Request) {
··· 17 "github.com/gorilla/feeds" 18 "tangled.sh/tangled.sh/core/api/tangled" 19 "tangled.sh/tangled.sh/core/appview/db" 20 "tangled.sh/tangled.sh/core/appview/pages" 21 ) 22 ··· 283 l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle) 284 285 loggedInUser := s.oauth.GetUser(r) 286 + params := FollowsPageParams{ 287 + Card: profile, 288 + } 289 290 follows, err := fetchFollows(s.db, profile.UserDid) 291 if err != nil { 292 l.Error("failed to fetch follows", "err", err) 293 + return &params, err 294 } 295 296 if len(follows) == 0 { 297 + return &params, nil 298 } 299 300 followDids := make([]string, 0, len(follows)) ··· 305 profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids)) 306 if err != nil { 307 l.Error("failed to get profiles", "followDids", followDids, "err", err) 308 + return &params, err 309 } 310 311 followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids) ··· 318 following, err := db.GetFollowing(s.db, loggedInUser.Did) 319 if err != nil { 320 l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did) 321 + return &params, err 322 } 323 loggedInUserFollowing = make(map[string]struct{}, len(following)) 324 for _, follow := range following { ··· 352 } 353 } 354 355 + params.Follows = followCards 356 + 357 + return &params, nil 358 } 359 360 func (s *State) followersPage(w http.ResponseWriter, r *http.Request) {
-35
docs/migrations/knot-1.7.0.md
··· 1 - # Upgrading from v1.7.0 2 - 3 - After v1.7.0, knot secrets have been deprecated. You no 4 - longer need a secret from the appview to run a knot. All 5 - authorized commands to knots are managed via [Inter-Service 6 - Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt). 7 - Knots will be read-only until upgraded. 8 - 9 - Upgrading is quite easy, in essence: 10 - 11 - - `KNOT_SERVER_SECRET` is no more, you can remove this 12 - environment variable entirely 13 - - `KNOT_SERVER_OWNER` is now required on boot, set this to 14 - your DID. You can find your DID in the 15 - [settings](https://tangled.sh/settings) page. 16 - - Restart your knot once you have replaced the environment 17 - variable 18 - - Head to the [knot dashboard](https://tangled.sh/knots) and 19 - hit the "retry" button to verify your knot. This simply 20 - writes a `sh.tangled.knot` record to your PDS. 21 - 22 - ## Nix 23 - 24 - If you use the nix module, simply bump the flake to the 25 - latest revision, and change your config block like so: 26 - 27 - ```diff 28 - services.tangled-knot = { 29 - enable = true; 30 - server = { 31 - - secretFile = /path/to/secret; 32 - + owner = "did:plc:foo"; 33 - }; 34 - }; 35 - ```
···
+60
docs/migrations.md
···
··· 1 + # Migrations 2 + 3 + This document is laid out in reverse-chronological order. 4 + Newer migration guides are listed first, and older guides 5 + are further down the page. 6 + 7 + ## Upgrading from v1.8.x 8 + 9 + After v1.8.2, the HTTP API for knot and spindles have been 10 + deprecated and replaced with XRPC. Repositories on outdated 11 + knots will not be viewable from the appview. Upgrading is 12 + straightforward however. 13 + 14 + For knots: 15 + 16 + - Upgrade to latest tag (v1.9.0 or above) 17 + - Head to the [knot dashboard](https://tangled.sh/knots) and 18 + hit the "retry" button to verify your knot 19 + 20 + For spindles: 21 + 22 + - Upgrade to latest tag (v1.9.0 or above) 23 + - Head to the [spindle 24 + dashboard](https://tangled.sh/spindles) and hit the 25 + "retry" button to verify your spindle 26 + 27 + ## Upgrading from v1.7.x 28 + 29 + After v1.7.0, knot secrets have been deprecated. You no 30 + longer need a secret from the appview to run a knot. All 31 + authorized commands to knots are managed via [Inter-Service 32 + Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt). 33 + Knots will be read-only until upgraded. 34 + 35 + Upgrading is quite easy, in essence: 36 + 37 + - `KNOT_SERVER_SECRET` is no more, you can remove this 38 + environment variable entirely 39 + - `KNOT_SERVER_OWNER` is now required on boot, set this to 40 + your DID. You can find your DID in the 41 + [settings](https://tangled.sh/settings) page. 42 + - Restart your knot once you have replaced the environment 43 + variable 44 + - Head to the [knot dashboard](https://tangled.sh/knots) and 45 + hit the "retry" button to verify your knot. This simply 46 + writes a `sh.tangled.knot` record to your PDS. 47 + 48 + If you use the nix module, simply bump the flake to the 49 + latest revision, and change your config block like so: 50 + 51 + ```diff 52 + services.tangled-knot = { 53 + enable = true; 54 + server = { 55 + - secretFile = /path/to/secret; 56 + + owner = "did:plc:foo"; 57 + }; 58 + }; 59 + ``` 60 +
+1
knotserver/xrpc/repo_blob.go
··· 69 return 70 } 71 w.Header().Set("ETag", eTag) 72 73 case strings.HasPrefix(mimeType, "text/"): 74 w.Header().Set("Cache-Control", "public, no-cache")
··· 69 return 70 } 71 w.Header().Set("ETag", eTag) 72 + w.Header().Set("Content-Type", mimeType) 73 74 case strings.HasPrefix(mimeType, "text/"): 75 w.Header().Set("Cache-Control", "public, no-cache")
+8 -6
knotserver/xrpc/repo_branches.go
··· 20 21 cursor := r.URL.Query().Get("cursor") 22 23 - limit := 50 // default 24 - if limitStr := r.URL.Query().Get("limit"); limitStr != "" { 25 - if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { 26 - limit = l 27 - } 28 - } 29 30 gr, err := git.PlainOpen(repoPath) 31 if err != nil {
··· 20 21 cursor := r.URL.Query().Get("cursor") 22 23 + // limit := 50 // default 24 + // if limitStr := r.URL.Query().Get("limit"); limitStr != "" { 25 + // if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { 26 + // limit = l 27 + // } 28 + // } 29 + 30 + limit := 500 31 32 gr, err := git.PlainOpen(repoPath) 33 if err != nil {
+11 -1
knotserver/xrpc/repo_log.go
··· 73 return 74 } 75 76 // Create response using existing types.RepoLogResponse 77 response := types.RepoLogResponse{ 78 Commits: commits, 79 Ref: ref, 80 Page: (offset / limit) + 1, 81 PerPage: limit, 82 - Total: len(commits), // This is not accurate for pagination, but matches existing behavior 83 } 84 85 if path != "" {
··· 73 return 74 } 75 76 + total, err := gr.TotalCommits() 77 + if err != nil { 78 + x.Logger.Error("fetching total commits", "error", err.Error()) 79 + writeError(w, xrpcerr.NewXrpcError( 80 + xrpcerr.WithTag("InternalServerError"), 81 + xrpcerr.WithMessage("failed to fetch total commits"), 82 + ), http.StatusNotFound) 83 + return 84 + } 85 + 86 // Create response using existing types.RepoLogResponse 87 response := types.RepoLogResponse{ 88 Commits: commits, 89 Ref: ref, 90 Page: (offset / limit) + 1, 91 PerPage: limit, 92 + Total: total, 93 } 94 95 if path != "" {
+8 -2
nix/gomod2nix.toml
··· 425 [mod."github.com/whyrusleeping/cbor-gen"] 426 version = "v0.3.1" 427 hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc=" 428 [mod."github.com/yuin/goldmark"] 429 - version = "v1.4.15" 430 - hash = "sha256-MvSOT6dwf5hVYkIg4MnqMpsy5ZtWZ7amAE7Zo9HkEa0=" 431 [mod."github.com/yuin/goldmark-highlighting/v2"] 432 version = "v2.0.0-20230729083705-37449abec8cc" 433 hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg="
··· 425 [mod."github.com/whyrusleeping/cbor-gen"] 426 version = "v0.3.1" 427 hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc=" 428 + [mod."github.com/wyatt915/goldmark-treeblood"] 429 + version = "v0.0.0-20250825231212-5dcbdb2f4b57" 430 + hash = "sha256-IZEsUXTBTsNgWoD7vqRUc9aFCCHNjzk1IUmI9O+NCnM=" 431 + [mod."github.com/wyatt915/treeblood"] 432 + version = "v0.1.15" 433 + hash = "sha256-hb99exdkoY2Qv8WdDxhwgPXGbEYimUr6wFtPXEvcO9g=" 434 [mod."github.com/yuin/goldmark"] 435 + version = "v1.7.12" 436 + hash = "sha256-thLYBS4woL2X5qRdo7vP+xCvjlGRDU0jXtDCUt6vvWM=" 437 [mod."github.com/yuin/goldmark-highlighting/v2"] 438 version = "v2.0.0-20230729083705-37449abec8cc" 439 hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg="
+15 -17
nix/pkgs/knot-unwrapped.nix
··· 3 modules, 4 sqlite-lib, 5 src, 6 - }: 7 - let 8 - version = "1.8.1-alpha"; 9 in 10 - buildGoApplication { 11 - pname = "knot"; 12 - version = "1.8.1"; 13 - inherit src modules; 14 15 - doCheck = false; 16 17 - subPackages = ["cmd/knot"]; 18 - tags = ["libsqlite3"]; 19 20 - ldflags = [ 21 - "-X tangled.sh/tangled.sh/core/knotserver/xrpc.version=${version}" 22 - ]; 23 24 - env.CGO_CFLAGS = "-I ${sqlite-lib}/include "; 25 - env.CGO_LDFLAGS = "-L ${sqlite-lib}/lib"; 26 - CGO_ENABLED = 1; 27 - }
··· 3 modules, 4 sqlite-lib, 5 src, 6 + }: let 7 + version = "1.9.0-alpha"; 8 in 9 + buildGoApplication { 10 + pname = "knot"; 11 + inherit src version modules; 12 13 + doCheck = false; 14 15 + subPackages = ["cmd/knot"]; 16 + tags = ["libsqlite3"]; 17 18 + ldflags = [ 19 + "-X tangled.sh/tangled.sh/core/knotserver/xrpc.version=${version}" 20 + ]; 21 22 + env.CGO_CFLAGS = "-I ${sqlite-lib}/include "; 23 + env.CGO_LDFLAGS = "-L ${sqlite-lib}/lib"; 24 + CGO_ENABLED = 1; 25 + }