Lewis: May this revision serve well! lewis@tangled.org
+327
-24
Diff
round #1
+87
appview/compat113/compat.go
+87
appview/compat113/compat.go
···
1
+
package compat113
2
+
3
+
import (
4
+
"encoding/json"
5
+
"io"
6
+
7
+
lexutil "github.com/bluesky-social/indigo/lex/util"
8
+
"tangled.org/core/api/tangled"
9
+
)
10
+
11
+
func Collaborator(r *tangled.RepoCollaborator) *lexutil.LexiconTypeDecoder {
12
+
return &lexutil.LexiconTypeDecoder{Val: &collaboratorWrapper{inner: r}}
13
+
}
14
+
15
+
func Pull(r *tangled.RepoPull) *lexutil.LexiconTypeDecoder {
16
+
return &lexutil.LexiconTypeDecoder{Val: &pullWrapper{inner: r}}
17
+
}
18
+
19
+
type collaboratorWrapper struct {
20
+
LexiconTypeID string `cborgen:"$type,const=sh.tangled.repo.collaborator"`
21
+
inner *tangled.RepoCollaborator
22
+
}
23
+
24
+
func (c *collaboratorWrapper) MarshalJSON() ([]byte, error) {
25
+
c.inner.LexiconTypeID = "sh.tangled.repo.collaborator"
26
+
return marshalWithRepoDidShadow(c.inner, false)
27
+
}
28
+
29
+
func (c *collaboratorWrapper) MarshalCBOR(w io.Writer) error {
30
+
return c.inner.MarshalCBOR(w)
31
+
}
32
+
33
+
type pullWrapper struct {
34
+
LexiconTypeID string `cborgen:"$type,const=sh.tangled.repo.pull"`
35
+
inner *tangled.RepoPull
36
+
}
37
+
38
+
func (c *pullWrapper) MarshalJSON() ([]byte, error) {
39
+
c.inner.LexiconTypeID = "sh.tangled.repo.pull"
40
+
return marshalWithRepoDidShadow(c.inner, true)
41
+
}
42
+
43
+
func (c *pullWrapper) MarshalCBOR(w io.Writer) error {
44
+
return c.inner.MarshalCBOR(w)
45
+
}
46
+
47
+
func marshalWithRepoDidShadow(inner any, nestedTarget bool) ([]byte, error) {
48
+
raw, err := json.Marshal(inner)
49
+
if err != nil {
50
+
return nil, err
51
+
}
52
+
var top map[string]json.RawMessage
53
+
if err := json.Unmarshal(raw, &top); err != nil {
54
+
return raw, nil
55
+
}
56
+
if nestedTarget {
57
+
injectIntoNested(top, "target")
58
+
injectIntoNested(top, "source")
59
+
} else {
60
+
addRepoDidShadow(top)
61
+
}
62
+
return json.Marshal(top)
63
+
}
64
+
65
+
func injectIntoNested(parent map[string]json.RawMessage, key string) {
66
+
raw, ok := parent[key]
67
+
if !ok {
68
+
return
69
+
}
70
+
var nested map[string]json.RawMessage
71
+
if err := json.Unmarshal(raw, &nested); err != nil {
72
+
return
73
+
}
74
+
addRepoDidShadow(nested)
75
+
if reb, err := json.Marshal(nested); err == nil {
76
+
parent[key] = reb
77
+
}
78
+
}
79
+
80
+
func addRepoDidShadow(m map[string]json.RawMessage) {
81
+
if _, has := m["repoDid"]; has {
82
+
return
83
+
}
84
+
if v, ok := m["repo"]; ok {
85
+
m["repoDid"] = v
86
+
}
87
+
}
+113
appview/compat113/compat_test.go
+113
appview/compat113/compat_test.go
···
1
+
package compat113
2
+
3
+
import (
4
+
"encoding/json"
5
+
"testing"
6
+
7
+
"tangled.org/core/api/tangled"
8
+
)
9
+
10
+
func ptr[T any](v T) *T { return &v }
11
+
12
+
func TestCollaboratorShadowsRepoDid(t *testing.T) {
13
+
rec := &tangled.RepoCollaborator{
14
+
CreatedAt: "2026-05-08T00:00:00Z",
15
+
Repo: "did:plc:abalone",
16
+
Subject: "did:plc:limpet",
17
+
}
18
+
19
+
out, err := json.Marshal(Collaborator(rec))
20
+
if err != nil {
21
+
t.Fatalf("marshal: %v", err)
22
+
}
23
+
24
+
var got map[string]any
25
+
if err := json.Unmarshal(out, &got); err != nil {
26
+
t.Fatalf("unmarshal: %v", err)
27
+
}
28
+
29
+
if got["$type"] != "sh.tangled.repo.collaborator" {
30
+
t.Errorf("$type = %v, want sh.tangled.repo.collaborator", got["$type"])
31
+
}
32
+
if got["repo"] != "did:plc:abalone" {
33
+
t.Errorf("repo = %v, want did:plc:abalone", got["repo"])
34
+
}
35
+
if got["repoDid"] != "did:plc:abalone" {
36
+
t.Errorf("repoDid shadow missing or wrong: got %v", got["repoDid"])
37
+
}
38
+
}
39
+
40
+
func TestPullShadowsTargetRepoDid(t *testing.T) {
41
+
rec := &tangled.RepoPull{
42
+
CreatedAt: "2026-05-08T00:00:00Z",
43
+
Title: "rename whelk handler",
44
+
Target: &tangled.RepoPull_Target{
45
+
Branch: "main",
46
+
Repo: "did:plc:scallop",
47
+
},
48
+
Source: &tangled.RepoPull_Source{
49
+
Branch: "feature-1",
50
+
},
51
+
}
52
+
53
+
out, err := json.Marshal(Pull(rec))
54
+
if err != nil {
55
+
t.Fatalf("marshal: %v", err)
56
+
}
57
+
58
+
var got map[string]any
59
+
if err := json.Unmarshal(out, &got); err != nil {
60
+
t.Fatalf("unmarshal: %v", err)
61
+
}
62
+
63
+
target, ok := got["target"].(map[string]any)
64
+
if !ok {
65
+
t.Fatalf("target missing or wrong type: %v", got["target"])
66
+
}
67
+
if target["repo"] != "did:plc:scallop" {
68
+
t.Errorf("target.repo = %v", target["repo"])
69
+
}
70
+
if target["repoDid"] != "did:plc:scallop" {
71
+
t.Errorf("target.repoDid shadow missing: %v", target["repoDid"])
72
+
}
73
+
74
+
if _, has := got["repoDid"]; has {
75
+
t.Errorf("top-level repoDid should not be set on pull: %v", got["repoDid"])
76
+
}
77
+
}
78
+
79
+
func TestPullShadowsForkSourceRepoDid(t *testing.T) {
80
+
rec := &tangled.RepoPull{
81
+
CreatedAt: "2026-05-08T00:00:00Z",
82
+
Title: "fork-based PR",
83
+
Target: &tangled.RepoPull_Target{
84
+
Branch: "main",
85
+
Repo: "did:plc:scallop",
86
+
},
87
+
Source: &tangled.RepoPull_Source{
88
+
Branch: "feature-2",
89
+
Repo: ptr("did:plc:periwinkle"),
90
+
},
91
+
}
92
+
93
+
out, err := json.Marshal(Pull(rec))
94
+
if err != nil {
95
+
t.Fatalf("marshal: %v", err)
96
+
}
97
+
98
+
var got map[string]any
99
+
if err := json.Unmarshal(out, &got); err != nil {
100
+
t.Fatalf("unmarshal: %v", err)
101
+
}
102
+
103
+
source, ok := got["source"].(map[string]any)
104
+
if !ok {
105
+
t.Fatalf("source missing: %v", got["source"])
106
+
}
107
+
if source["repo"] != "did:plc:periwinkle" {
108
+
t.Errorf("source.repo = %v", source["repo"])
109
+
}
110
+
if source["repoDid"] != "did:plc:periwinkle" {
111
+
t.Errorf("source.repoDid shadow missing: %v", source["repoDid"])
112
+
}
113
+
}
+60
appview/compat113/version.go
+60
appview/compat113/version.go
···
1
+
package compat113
2
+
3
+
import (
4
+
"context"
5
+
"fmt"
6
+
"net/http"
7
+
"strconv"
8
+
"strings"
9
+
"time"
10
+
11
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
12
+
"tangled.org/core/api/tangled"
13
+
)
14
+
15
+
const versionProbeTimeout = 5 * time.Second
16
+
17
+
func KnotSupports114(ctx context.Context, host string, dev bool) bool {
18
+
scheme := "https"
19
+
if dev {
20
+
scheme = "http"
21
+
}
22
+
client := &indigoxrpc.Client{
23
+
Host: fmt.Sprintf("%s://%s", scheme, host),
24
+
Client: &http.Client{Timeout: versionProbeTimeout},
25
+
}
26
+
27
+
ctx, cancel := context.WithTimeout(ctx, versionProbeTimeout)
28
+
defer cancel()
29
+
30
+
resp, err := tangled.KnotVersion(ctx, client)
31
+
if err != nil || resp == nil {
32
+
return true
33
+
}
34
+
return atLeast114(resp.Version)
35
+
}
36
+
37
+
func atLeast114(v string) bool {
38
+
v = strings.TrimSpace(v)
39
+
v = strings.TrimPrefix(v, "v")
40
+
if strings.HasPrefix(v, "(devel)") {
41
+
return true
42
+
}
43
+
if v == "" {
44
+
return false
45
+
}
46
+
parts := strings.SplitN(v, ".", 3)
47
+
if len(parts) < 2 {
48
+
return false
49
+
}
50
+
major, err := strconv.Atoi(parts[0])
51
+
if err != nil {
52
+
return false
53
+
}
54
+
minorRaw := strings.SplitN(parts[1], "-", 2)[0]
55
+
minor, err := strconv.Atoi(minorRaw)
56
+
if err != nil {
57
+
return false
58
+
}
59
+
return major > 1 || (major == 1 && minor >= 14)
60
+
}
+35
appview/compat113/version_test.go
+35
appview/compat113/version_test.go
···
1
+
package compat113
2
+
3
+
import "testing"
4
+
5
+
func TestAtLeast114(t *testing.T) {
6
+
cases := []struct {
7
+
in string
8
+
want bool
9
+
}{
10
+
{"v1.14.0", true},
11
+
{"v1.14.0-alpha", true},
12
+
{"v1.14.5", true},
13
+
{"v1.13.0", false},
14
+
{"v1.13.0-alpha", false},
15
+
{"v1.0.0", false},
16
+
{"v2.0.0", true},
17
+
{"1.14.0", true},
18
+
{"1.13.99", false},
19
+
{"(devel)", true},
20
+
{"", false},
21
+
{"garbagio-furioso", false},
22
+
{"v1", false},
23
+
{"vX.Y.Z", false},
24
+
{"unknown", false},
25
+
{"unknown-abc1234", false},
26
+
{"unknown-abc1234-modified", false},
27
+
}
28
+
for _, c := range cases {
29
+
t.Run(c.in, func(t *testing.T) {
30
+
if got := atLeast114(c.in); got != c.want {
31
+
t.Errorf("atLeast114(%q) = %v, want %v", c.in, got, c.want)
32
+
}
33
+
})
34
+
}
35
+
}
+4
-1
appview/ingester_repo.go
+4
-1
appview/ingester_repo.go
···
371
371
if err != nil {
372
372
return false, fmt.Errorf("verify repo ownership: %w", err)
373
373
}
374
-
if result.OwnerDid.String() != eventDid {
374
+
if result.OwnerDid == "" {
375
+
l.Warn("knot lacks RepoDescribeRepo, skipping owner check; upgrade knot to 1.14+",
376
+
"repoDid", repoDid, "knot", result.KnotURL.String())
377
+
} else if result.OwnerDid.String() != eventDid {
375
378
l.Warn("rejecting repo event: owner mismatch",
376
379
"repoDid", repoDid,
377
380
"claimedOwner", eventDid,
+3
-6
appview/pulls/create.go
+3
-6
appview/pulls/create.go
···
11
11
"time"
12
12
13
13
"tangled.org/core/api/tangled"
14
+
"tangled.org/core/appview/compat113"
14
15
"tangled.org/core/appview/db"
15
16
"tangled.org/core/appview/models"
16
17
"tangled.org/core/appview/oauth"
···
301
302
Collection: tangled.RepoPullNSID,
302
303
Repo: userDid.String(),
303
304
Rkey: rkey,
304
-
Record: &lexutil.LexiconTypeDecoder{
305
-
Val: &record,
306
-
},
305
+
Record: compat113.Pull(&record),
307
306
})
308
307
if err != nil {
309
308
l.Error("failed to create pull request", "err", err)
···
403
402
RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
404
403
Collection: tangled.RepoPullNSID,
405
404
Rkey: &p.Rkey,
406
-
Value: &lexutil.LexiconTypeDecoder{
407
-
Val: &record,
408
-
},
405
+
Value: compat113.Pull(&record),
409
406
},
410
407
})
411
408
}
+4
-9
appview/pulls/resubmit.go
+4
-9
appview/pulls/resubmit.go
···
7
7
"time"
8
8
9
9
"tangled.org/core/api/tangled"
10
+
"tangled.org/core/appview/compat113"
10
11
"tangled.org/core/appview/db"
11
12
"tangled.org/core/appview/models"
12
13
"tangled.org/core/appview/oauth"
···
330
331
Repo: userDid.String(),
331
332
Rkey: pull.Rkey,
332
333
SwapRecord: ex.Cid,
333
-
Record: &lexutil.LexiconTypeDecoder{
334
-
Val: &record,
335
-
},
334
+
Record: compat113.Pull(&record),
336
335
})
337
336
if err != nil {
338
337
l.Error("failed to update record on PDS", "err", err, "rkey", pull.Rkey)
···
521
520
RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{
522
521
Collection: tangled.RepoPullNSID,
523
522
Rkey: &p.Rkey,
524
-
Value: &lexutil.LexiconTypeDecoder{
525
-
Val: &record,
526
-
},
523
+
Value: compat113.Pull(&record),
527
524
},
528
525
})
529
526
}
···
581
578
RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{
582
579
Collection: tangled.RepoPullNSID,
583
580
Rkey: op.Rkey,
584
-
Value: &lexutil.LexiconTypeDecoder{
585
-
Val: &record,
586
-
},
581
+
Value: compat113.Pull(&record),
587
582
},
588
583
})
589
584
}
+7
-3
appview/repo/repo.go
+7
-3
appview/repo/repo.go
···
15
15
"tangled.org/core/appview/cloudflare"
16
16
17
17
"tangled.org/core/api/tangled"
18
+
"tangled.org/core/appview/compat113"
18
19
"tangled.org/core/appview/config"
19
20
"tangled.org/core/appview/db"
20
21
"tangled.org/core/appview/models"
···
759
760
Collection: tangled.RepoCollaboratorNSID,
760
761
Repo: currentUser.Did,
761
762
Rkey: rkey,
762
-
Record: &lexutil.LexiconTypeDecoder{
763
-
Val: repoCollaboratorRecord(f, collaboratorIdent.DID.String(), createdAt),
764
-
},
763
+
Record: compat113.Collaborator(repoCollaboratorRecord(f, collaboratorIdent.DID.String(), createdAt)),
765
764
})
766
765
// invalid record
767
766
if err != nil {
···
850
849
return
851
850
}
852
851
852
+
if !compat113.KnotSupports114(r.Context(), f.Knot, rp.config.Core.Dev) {
853
+
rp.pages.Notice(w, noticeId, "This repository's knot is below v1.14 and does not yet support renames. Ask the knot operator to upgrade.")
854
+
return
855
+
}
856
+
853
857
newName, err := validateRenameInput(f.Name, f.Rkey, r.FormValue("name"))
854
858
if err != nil {
855
859
rp.pages.Notice(w, noticeId, err.Error())
+4
appview/repoverify/verify.go
+4
appview/repoverify/verify.go
···
2
2
3
3
import (
4
4
"context"
5
+
"errors"
5
6
"fmt"
6
7
"net"
7
8
"net/http"
···
109
110
client := &indigoxrpc.Client{Host: knot.String(), Client: httpClient}
110
111
out, err := tangled.RepoDescribeRepo(ctx, client, repoDid.String())
111
112
if xrpcErr := xrpcclient.HandleXrpcErr(err); xrpcErr != nil {
113
+
if errors.Is(xrpcErr, xrpcclient.ErrXrpcUnsupported) {
114
+
return Result{RepoDid: repoDid, KnotURL: knot}, nil
115
+
}
112
116
return Result{}, fmt.Errorf("describeRepo on %s: %w", knot, xrpcErr)
113
117
}
114
118
+10
-5
knotmirror/knotstream/slurper.go
+10
-5
knotmirror/knotstream/slurper.go
···
262
262
}
263
263
264
264
type legacyGitRefUpdate struct {
265
-
OwnerDid *string `json:"ownerDid,omitempty"`
266
-
RepoDid *string `json:"repo,omitempty"`
265
+
OwnerDid *string `json:"ownerDid,omitempty"`
266
+
Repo *string `json:"repo,omitempty"`
267
+
LegacyRepoDid *string `json:"repoDid,omitempty"`
267
268
}
268
269
269
270
type LegacyGitEvent struct {
···
288
289
// via the stable RepoDid join. Returns (nil, "", nil) when the event has no
289
290
// repoDid (unjoinable) and (nil, key, nil) on a clean miss.
290
291
func (s *KnotSlurper) lookupRepoForRefUpdate(ctx context.Context, evt *LegacyGitEvent) (*models.Repo, string, error) {
291
-
if evt.Event.RepoDid == nil || *evt.Event.RepoDid == "" {
292
+
raw := evt.Event.Repo
293
+
if raw == nil || *raw == "" {
294
+
raw = evt.Event.LegacyRepoDid
295
+
}
296
+
if raw == nil || *raw == "" {
292
297
return nil, "", nil
293
298
}
294
-
repoDid := syntax.DID(*evt.Event.RepoDid)
299
+
repoDid := syntax.DID(*raw)
295
300
curr, err := db.GetRepoByRepoDid(ctx, s.db, repoDid)
296
301
return curr, repoDid.String(), err
297
302
}
···
308
313
if curr == nil {
309
314
if lookupKey == "" {
310
315
l.Warn("skipping gitRefUpdate: event has no fields to join on",
311
-
"repo_did", evt.Event.RepoDid)
316
+
"repo", evt.Event.Repo, "legacy_repo_did", evt.Event.LegacyRepoDid)
312
317
} else {
313
318
// if repo doesn't exist in DB, just ignore the event. That repo is unknown.
314
319
// Hopefully crawler/tap will sync it later.
History
2 rounds
0 comments
oyster.cafe
submitted
#1
1 commit
expand
collapse
appview,knotmirror: tolerate 1.13 knots during 1.14 rollout
Lewis: May this revision serve well! <lewis@tangled.org>
merge conflicts detected
expand
collapse
expand
collapse
- api/tangled/cbor_gen.go:866
- api/tangled/feedstar.go:5
- api/tangled/gitrefUpdate.go:29
- api/tangled/repocollaborator.go:19
- api/tangled/repoissue.go:22
- api/tangled/repopull.go:39
- api/tangled/tangledrepo.go:24
- cmd/cborgen/cborgen.go:17
- knotserver/xrpc/merge.go:118
- lexicons/feed/star.json:10
- lexicons/git/refUpdate.json:11
- lexicons/issue/issue.json:9
- lexicons/pulls/pull.json:65
- lexicons/repo/collaborator.json:11
- lexicons/repo/repo.json:6
expand 0 comments
oyster.cafe
submitted
#0
1 commit
expand
collapse
appview,knotmirror: tolerate 1.13 knots during 1.14 rollout
Lewis: May this revision serve well! <lewis@tangled.org>