+8
-4
appview/db/issues.go
+8
-4
appview/db/issues.go
···
359
359
repoMap[string(repos[i].RepoAt())] = &repos[i]
360
360
}
361
361
362
-
for issueAt := range issueMap {
363
-
i := issueMap[issueAt]
364
-
r := repoMap[string(i.RepoAt)]
365
-
i.Repo = r
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
+
}
366
370
}
367
371
368
372
// collect comments
+2
-2
appview/db/profile.go
+2
-2
appview/db/profile.go
···
553
553
query = `select count(id) from pulls where owner_did = ? and state = ?`
554
554
args = append(args, did, PullOpen)
555
555
case VanityStatOpenIssueCount:
556
-
query = `select count(id) from issues where owner_did = ? and open = 1`
556
+
query = `select count(id) from issues where did = ? and open = 1`
557
557
args = append(args, did)
558
558
case VanityStatClosedIssueCount:
559
-
query = `select count(id) from issues where owner_did = ? and open = 0`
559
+
query = `select count(id) from issues where did = ? and open = 0`
560
560
args = append(args, did)
561
561
case VanityStatRepositoryCount:
562
562
query = `select count(id) from repos where did = ?`
+1
-1
appview/pages/markup/markdown.go
+1
-1
appview/pages/markup/markdown.go
···
235
235
repoName := fmt.Sprintf("%s/%s", rctx.RepoInfo.OwnerDid, rctx.RepoInfo.Name)
236
236
237
237
query := fmt.Sprintf("repo=%s&ref=%s&path=%s&raw=true",
238
-
repoName, url.PathEscape(rctx.RepoInfo.Ref), actualPath)
238
+
url.PathEscape(repoName), url.PathEscape(rctx.RepoInfo.Ref), actualPath)
239
239
240
240
parsedURL := &url.URL{
241
241
Scheme: scheme,
+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
1
{{ define "title" }}knots{{ end }}
2
2
3
3
{{ define "content" }}
4
-
<div class="px-6 py-4 flex items-end justify-start gap-4 align-bottom">
4
+
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
5
5
<h1 class="text-xl font-bold dark:text-white">Knots</h1>
6
-
7
-
<span class="flex items-center gap-1 text-sm">
6
+
<span class="flex items-center gap-1">
8
7
{{ 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>
8
+
<a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/knot-hosting.md">docs</a>
12
9
</span>
13
10
</div>
14
11
+1
-3
appview/pages/templates/layouts/fragments/topbar.html
+1
-3
appview/pages/templates/layouts/fragments/topbar.html
···
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
3
<div class="flex justify-between p-0 items-center">
4
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>
5
+
<a href="/" hx-boost="true" class="text-lg">{{ template "fragments/logotype" }}</a>
8
6
</div>
9
7
10
8
<div id="right-items" class="flex items-center gap-2">
+3
-7
appview/pages/templates/spindles/index.html
+3
-7
appview/pages/templates/spindles/index.html
···
1
1
{{ define "title" }}spindles{{ end }}
2
2
3
3
{{ define "content" }}
4
-
<div class="px-6 py-4 flex items-end justify-start gap-4 align-bottom">
4
+
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
5
5
<h1 class="text-xl font-bold dark:text-white">Spindles</h1>
6
-
7
-
8
-
<span class="flex items-center gap-1 text-sm">
6
+
<span class="flex items-center gap-1">
9
7
{{ 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>
8
+
<a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md">docs</a>
13
9
</span>
14
10
</div>
15
11
+2
-4
appview/pages/templates/user/completeSignup.html
+2
-4
appview/pages/templates/user/completeSignup.html
···
29
29
</head>
30
30
<body class="flex items-center justify-center min-h-screen">
31
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
32
+
<h1 class="flex place-content-center text-2xl font-semibold italic dark:text-white" >
33
+
{{ template "fragments/logotype" }}
36
34
</h1>
37
35
<h2 class="text-center text-xl italic dark:text-white">
38
36
tightly-knit social coding.
+2
-2
appview/pages/templates/user/login.html
+2
-2
appview/pages/templates/user/login.html
···
13
13
</head>
14
14
<body class="flex items-center justify-center min-h-screen">
15
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
16
+
<h1 class="flex place-content-center text-2xl font-semibold italic dark:text-white" >
17
+
{{ template "fragments/logotype" }}
18
18
</h1>
19
19
<h2 class="text-center text-xl italic dark:text-white">
20
20
tightly-knit social coding.
+3
-1
appview/pages/templates/user/signup.html
+3
-1
appview/pages/templates/user/signup.html
···
13
13
</head>
14
14
<body class="flex items-center justify-center min-h-screen">
15
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>
16
+
<h1 class="flex place-content-center text-2xl font-semibold italic dark:text-white" >
17
+
{{ template "fragments/logotype" }}
18
+
</h1>
17
19
<h2 class="text-center text-xl italic dark:text-white">tightly-knit social coding.</h2>
18
20
<form
19
21
class="mt-4 max-w-sm mx-auto"
+1
-2
appview/repo/repo.go
+1
-2
appview/repo/repo.go
···
11
11
"log/slog"
12
12
"net/http"
13
13
"net/url"
14
-
"path"
15
14
"path/filepath"
16
15
"slices"
17
16
"strconv"
···
710
709
}
711
710
712
711
// fetch the raw binary content using sh.tangled.repo.blob xrpc
713
-
repoName := path.Join("%s/%s", f.OwnerDid(), f.Name)
712
+
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
714
713
blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true",
715
714
scheme, f.Knot, url.QueryEscape(repoName), url.QueryEscape(ref), url.QueryEscape(filePath))
716
715
+10
-9
appview/state/profile.go
+10
-9
appview/state/profile.go
···
17
17
"github.com/gorilla/feeds"
18
18
"tangled.sh/tangled.sh/core/api/tangled"
19
19
"tangled.sh/tangled.sh/core/appview/db"
20
-
// "tangled.sh/tangled.sh/core/appview/oauth"
21
20
"tangled.sh/tangled.sh/core/appview/pages"
22
21
)
23
22
···
284
283
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
285
284
286
285
loggedInUser := s.oauth.GetUser(r)
286
+
params := FollowsPageParams{
287
+
Card: profile,
288
+
}
287
289
288
290
follows, err := fetchFollows(s.db, profile.UserDid)
289
291
if err != nil {
290
292
l.Error("failed to fetch follows", "err", err)
291
-
return nil, err
293
+
return ¶ms, err
292
294
}
293
295
294
296
if len(follows) == 0 {
295
-
return nil, nil
297
+
return ¶ms, nil
296
298
}
297
299
298
300
followDids := make([]string, 0, len(follows))
···
303
305
profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids))
304
306
if err != nil {
305
307
l.Error("failed to get profiles", "followDids", followDids, "err", err)
306
-
return nil, err
308
+
return ¶ms, err
307
309
}
308
310
309
311
followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids)
···
316
318
following, err := db.GetFollowing(s.db, loggedInUser.Did)
317
319
if err != nil {
318
320
l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did)
319
-
return nil, err
321
+
return ¶ms, err
320
322
}
321
323
loggedInUserFollowing = make(map[string]struct{}, len(following))
322
324
for _, follow := range following {
···
350
352
}
351
353
}
352
354
353
-
return &FollowsPageParams{
354
-
Follows: followCards,
355
-
Card: profile,
356
-
}, nil
355
+
params.Follows = followCards
356
+
357
+
return ¶ms, nil
357
358
}
358
359
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
20
21
21
cursor := r.URL.Query().Get("cursor")
22
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
-
}
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
29
31
30
32
gr, err := git.PlainOpen(repoPath)
31
33
if err != nil {
+11
-1
knotserver/xrpc/repo_log.go
+11
-1
knotserver/xrpc/repo_log.go
···
73
73
return
74
74
}
75
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
+
76
86
// Create response using existing types.RepoLogResponse
77
87
response := types.RepoLogResponse{
78
88
Commits: commits,
79
89
Ref: ref,
80
90
Page: (offset / limit) + 1,
81
91
PerPage: limit,
82
-
Total: len(commits), // This is not accurate for pagination, but matches existing behavior
92
+
Total: total,
83
93
}
84
94
85
95
if path != "" {
+8
-2
nix/gomod2nix.toml
+8
-2
nix/gomod2nix.toml
···
425
425
[mod."github.com/whyrusleeping/cbor-gen"]
426
426
version = "v0.3.1"
427
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="
428
434
[mod."github.com/yuin/goldmark"]
429
-
version = "v1.4.15"
430
-
hash = "sha256-MvSOT6dwf5hVYkIg4MnqMpsy5ZtWZ7amAE7Zo9HkEa0="
435
+
version = "v1.7.12"
436
+
hash = "sha256-thLYBS4woL2X5qRdo7vP+xCvjlGRDU0jXtDCUt6vvWM="
431
437
[mod."github.com/yuin/goldmark-highlighting/v2"]
432
438
version = "v2.0.0-20230729083705-37449abec8cc"
433
439
hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg="
+15
-17
nix/pkgs/knot-unwrapped.nix
+15
-17
nix/pkgs/knot-unwrapped.nix
···
3
3
modules,
4
4
sqlite-lib,
5
5
src,
6
-
}:
7
-
let
8
-
version = "1.8.1-alpha";
6
+
}: let
7
+
version = "1.9.0-alpha";
9
8
in
10
-
buildGoApplication {
11
-
pname = "knot";
12
-
version = "1.8.1";
13
-
inherit src modules;
9
+
buildGoApplication {
10
+
pname = "knot";
11
+
inherit src version modules;
14
12
15
-
doCheck = false;
13
+
doCheck = false;
16
14
17
-
subPackages = ["cmd/knot"];
18
-
tags = ["libsqlite3"];
15
+
subPackages = ["cmd/knot"];
16
+
tags = ["libsqlite3"];
19
17
20
-
ldflags = [
21
-
"-X tangled.sh/tangled.sh/core/knotserver/xrpc.version=${version}"
22
-
];
18
+
ldflags = [
19
+
"-X tangled.sh/tangled.sh/core/knotserver/xrpc.version=${version}"
20
+
];
23
21
24
-
env.CGO_CFLAGS = "-I ${sqlite-lib}/include ";
25
-
env.CGO_LDFLAGS = "-L ${sqlite-lib}/lib";
26
-
CGO_ENABLED = 1;
27
-
}
22
+
env.CGO_CFLAGS = "-I ${sqlite-lib}/include ";
23
+
env.CGO_LDFLAGS = "-L ${sqlite-lib}/lib";
24
+
CGO_ENABLED = 1;
25
+
}