+8
-161
appview/db/issues.go
+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
+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
+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
+1
-1
appview/pages/markup/markdown.go
+8
appview/pages/templates/fragments/logotype.html
+8
appview/pages/templates/fragments/logotype.html
+3
-6
appview/pages/templates/knots/index.html
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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 ¶ms, err
294
}
295
296
if len(follows) == 0 {
297
+
return ¶ms, 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 ¶ms, 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 ¶ms, 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 ¶ms, nil
358
}
359
360
func (s *State) followersPage(w http.ResponseWriter, r *http.Request) {
-35
docs/migrations/knot-1.7.0.md
-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
+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
+1
knotserver/xrpc/repo_blob.go
+8
-6
knotserver/xrpc/repo_branches.go
+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
+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
+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
+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
+
}