Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

knotserver,appview,lexicons: resolve merge/forkSync target by repodid

Lewis: May this revision serve well! <lewis@tangled.org>

Lewis (Jul 7, 2026, 4:08 PM +0300) 5c13c904 34096cef

+236 -83
+2
api/tangled/repoforkSync.go
··· 22 22 Did string `json:"did" cborgen:"did"` 23 23 // name: Name of the forked repository 24 24 Name string `json:"name" cborgen:"name"` 25 + // repo: DID of the repository 26 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 25 27 // source: AT-URI of the source repository 26 28 Source string `json:"source" cborgen:"source"` 27 29 }
+2
api/tangled/repomerge.go
··· 32 32 Name string `json:"name" cborgen:"name"` 33 33 // patch: Patch content to merge 34 34 Patch string `json:"patch" cborgen:"patch"` 35 + // repo: DID of the repository 36 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 35 37 } 36 38 37 39 // RepoMerge calls the XRPC method "sh.tangled.repo.merge".
+2
api/tangled/repomergeCheck.go
··· 32 32 Name string `json:"name" cborgen:"name"` 33 33 // patch: Patch or pull request to check for merge conflicts 34 34 Patch string `json:"patch" cborgen:"patch"` 35 + // repo: DID of the repository 36 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 35 37 } 36 38 37 39 // RepoMergeCheck_Output is the output of a sh.tangled.repo.mergeCheck call.
+2 -2
appview/ingester_repo.go
··· 16 16 "tangled.org/core/appview/db" 17 17 "tangled.org/core/appview/models" 18 18 "tangled.org/core/orm" 19 - "tangled.org/core/repoverify" 19 + "tangled.org/core/repoident" 20 20 ) 21 21 22 22 func (i *Ingester) ingestRepo(ctx context.Context, e *jmodels.Event, l *slog.Logger) error { ··· 422 422 if i.Verifier == nil { 423 423 return false, fmt.Errorf("ingester has no repo ownership verifier configured") 424 424 } 425 - rd, err := repoverify.NewRepoDid(repoDid) 425 + rd, err := repoident.NewRepoDid(repoDid) 426 426 if err != nil { 427 427 l.Warn("rejecting repo event: invalid repoDid on record", "repoDid", repoDid, "err", err) 428 428 return false, nil
+5 -4
appview/ingester_repo_test.go
··· 18 18 "tangled.org/core/appview/notify" 19 19 "tangled.org/core/orm" 20 20 "tangled.org/core/rbac" 21 + "tangled.org/core/repoident" 21 22 "tangled.org/core/repoverify" 22 23 ) 23 24 ··· 33 34 func acceptOwner(t *testing.T, e *jmodels.Event) repoverify.Verifier { 34 35 t.Helper() 35 36 knot := mustKnotURL(t, "https://knot.example") 36 - return func(_ context.Context, repoDid repoverify.RepoDid) (repoverify.Result, error) { 37 + return func(_ context.Context, repoDid repoident.RepoDid) (repoverify.Result, error) { 37 38 return repoverify.Result{ 38 39 RepoDid: repoDid, 39 - OwnerDid: repoverify.OwnerDid(e.Did), 40 + OwnerDid: repoident.OwnerDid(e.Did), 40 41 KnotURL: knot, 41 42 }, nil 42 43 } 43 44 } 44 45 45 46 func stubVerifier(result repoverify.Result, err error) repoverify.Verifier { 46 - return func(_ context.Context, _ repoverify.RepoDid) (repoverify.Result, error) { 47 + return func(_ context.Context, _ repoident.RepoDid) (repoverify.Result, error) { 47 48 return result, err 48 49 } 49 50 } ··· 691 692 }) 692 693 693 694 verifierCalled := false 694 - withVerifier(ing, func(_ context.Context, _ repoverify.RepoDid) (repoverify.Result, error) { 695 + withVerifier(ing, func(_ context.Context, _ repoident.RepoDid) (repoverify.Result, error) { 695 696 verifierCalled = true 696 697 return repoverify.Result{}, nil 697 698 })
+8 -6
appview/models/repo.go
··· 51 51 website = &r.Website 52 52 } 53 53 54 - var repoDid *string 55 - if r.RepoDid != "" { 56 - repoDid = &r.RepoDid 57 - } 58 - 59 54 return tangled.Repo{ 60 55 Knot: r.Knot, 61 56 Name: r.cosmeticName(), ··· 66 61 Source: source, 67 62 Spindle: spindle, 68 63 Labels: r.Labels, 69 - RepoDid: repoDid, 64 + RepoDid: r.RepoDidPtr(), 70 65 } 71 66 } 72 67 ··· 101 96 return r.RepoDid 102 97 } 103 98 return string(r.RepoAt()) 99 + } 100 + 101 + func (r Repo) RepoDidPtr() *string { 102 + if r.RepoDid == "" { 103 + return nil 104 + } 105 + return &r.RepoDid 104 106 } 105 107 106 108 func (r Repo) TopicStr() string {
+1
appview/pulls/compose.go
··· 467 467 resp, err := tangled.RepoMergeCheck(ctx, xrpcc, &tangled.RepoMergeCheck_Input{ 468 468 Did: repo.Did, 469 469 Name: repo.Name, 470 + Repo: repo.RepoDidPtr(), 470 471 Branch: targetBranch, 471 472 Patch: patch, 472 473 })
+1
appview/pulls/merge.go
··· 74 74 mergeInput := &tangled.RepoMerge_Input{ 75 75 Did: f.Did, 76 76 Name: f.Name, 77 + Repo: f.RepoDidPtr(), 77 78 Branch: pull.TargetBranch, 78 79 Patch: patch, 79 80 CommitMessage: &pull.Title,
+1
appview/pulls/single.go
··· 344 344 &tangled.RepoMergeCheck_Input{ 345 345 Did: f.Did, 346 346 Name: f.Name, 347 + Repo: f.RepoDidPtr(), 347 348 Branch: pull.TargetBranch, 348 349 Patch: patch, 349 350 },
+1
appview/repo/repo.go
··· 1383 1383 &tangled.RepoForkSync_Input{ 1384 1384 Did: user.Did, 1385 1385 Name: f.Name, 1386 + Repo: f.RepoDidPtr(), 1386 1387 Source: f.Source, 1387 1388 Branch: ref, 1388 1389 },
+4 -8
knotserver/xrpc/fork_sync.go
··· 40 40 return 41 41 } 42 42 43 - repoDid, err := x.Db.GetRepoDid(did, name) 44 - if err != nil { 45 - fail(xrpcerr.RepoNotFoundError) 46 - return 47 - } 48 - repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 43 + repoDid, repoPath, err := x.resolveRepoDID(data.Repo, did, name) 49 44 if err != nil { 45 + l.Error("failed to resolve repo", "err", err) 50 46 fail(xrpcerr.RepoNotFoundError) 51 47 return 52 48 } 53 49 54 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 55 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) 50 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid.String()); !ok || err != nil { 51 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid.String()) 56 52 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 57 53 return 58 54 }
+4 -8
knotserver/xrpc/merge.go
··· 42 42 return 43 43 } 44 44 45 - repoDid, err := x.Db.GetRepoDid(did, name) 46 - if err != nil { 47 - fail(xrpcerr.RepoNotFoundError) 48 - return 49 - } 50 - repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 45 + repoDid, repoPath, err := x.resolveRepoDID(data.Repo, did, name) 51 46 if err != nil { 47 + l.Error("failed to resolve repo", "err", err) 52 48 fail(xrpcerr.RepoNotFoundError) 53 49 return 54 50 } 55 51 56 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 57 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) 52 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid.String()); !ok || err != nil { 53 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid.String()) 58 54 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 59 55 return 60 56 }
+2 -6
knotserver/xrpc/merge_check.go
··· 33 33 return 34 34 } 35 35 36 - repoDid, err := x.Db.GetRepoDid(did, name) 37 - if err != nil { 38 - fail(xrpcerr.RepoNotFoundError) 39 - return 40 - } 41 - repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 36 + _, repoPath, err := x.resolveRepoDID(data.Repo, did, name) 42 37 if err != nil { 38 + l.Error("failed to resolve repo", "err", err) 43 39 fail(xrpcerr.RepoNotFoundError) 44 40 return 45 41 }
+83
knotserver/xrpc/resolve_repo_did_test.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "testing" 7 + ) 8 + 9 + const ( 10 + resolveOwnerDid = "did:plc:akshay" 11 + resolveRepoDid = "did:plc:squid" 12 + resolveStoredKey = "squidbot" 13 + ) 14 + 15 + func setupResolveRepo(t *testing.T) (*Xrpc, string) { 16 + t.Helper() 17 + x := newTestXrpc(t) 18 + scanPath := t.TempDir() 19 + x.Config.Repo.ScanPath = scanPath 20 + 21 + if err := x.Db.StoreRepoKey(resolveRepoDid, []byte("k256"), resolveOwnerDid, resolveStoredKey); err != nil { 22 + t.Fatalf("StoreRepoKey: %v", err) 23 + } 24 + if err := os.MkdirAll(filepath.Join(scanPath, resolveRepoDid), 0o755); err != nil { 25 + t.Fatalf("mkdir repo dir: %v", err) 26 + } 27 + return x, scanPath 28 + } 29 + 30 + func TestResolveRepoDID_PrefersRepoOverName(t *testing.T) { 31 + x, scanPath := setupResolveRepo(t) 32 + 33 + repo := resolveRepoDid 34 + gotDid, gotPath, err := x.resolveRepoDID(&repo, resolveOwnerDid, "SquidBot") 35 + if err != nil { 36 + t.Fatalf("resolveRepoDID with repo set: %v", err) 37 + } 38 + if gotDid != resolveRepoDid { 39 + t.Errorf("repoDid = %q, want %q", gotDid, resolveRepoDid) 40 + } 41 + if want := filepath.Join(scanPath, resolveRepoDid); gotPath != want { 42 + t.Errorf("repoPath = %q, want %q", gotPath, want) 43 + } 44 + } 45 + 46 + func TestResolveRepoDID_RejectsMalformedRepoDid(t *testing.T) { 47 + x, _ := setupResolveRepo(t) 48 + 49 + malformed := "not-a-did" 50 + if _, _, err := x.resolveRepoDID(&malformed, resolveOwnerDid, resolveStoredKey); err == nil { 51 + t.Fatal("resolveRepoDID with malformed repo DID: got nil error, want failure") 52 + } 53 + } 54 + 55 + func TestResolveRepoDID_UnknownRepoDidDoesNotFallBackToName(t *testing.T) { 56 + x, _ := setupResolveRepo(t) 57 + 58 + unknown := "did:plc:limpet" 59 + if _, _, err := x.resolveRepoDID(&unknown, resolveOwnerDid, resolveStoredKey); err == nil { 60 + t.Fatal("resolveRepoDID with unknown repo DID and resolvable name: got nil error, want failure") 61 + } 62 + } 63 + 64 + func TestResolveRepoDID_NameFallbackIsCaseSensitive(t *testing.T) { 65 + x, _ := setupResolveRepo(t) 66 + 67 + if _, _, err := x.resolveRepoDID(nil, resolveOwnerDid, "SquidBot"); err == nil { 68 + t.Fatal("resolveRepoDID with mismatched-case name: got nil error, want failure") 69 + } 70 + 71 + empty := "" 72 + if _, _, err := x.resolveRepoDID(&empty, resolveOwnerDid, "SquidBot"); err == nil { 73 + t.Fatal("resolveRepoDID with empty repo and mismatched-case name: got nil error, want failure") 74 + } 75 + 76 + gotDid, _, err := x.resolveRepoDID(nil, resolveOwnerDid, resolveStoredKey) 77 + if err != nil { 78 + t.Fatalf("resolveRepoDID with exact-case name: %v", err) 79 + } 80 + if gotDid != resolveRepoDid { 81 + t.Errorf("repoDid = %q, want %q", gotDid, resolveRepoDid) 82 + } 83 + }
+26
knotserver/xrpc/xrpc.go
··· 19 19 "tangled.org/core/knotserver/sandbox" 20 20 "tangled.org/core/notifier" 21 21 "tangled.org/core/rbac" 22 + "tangled.org/core/repoident" 22 23 xrpcerr "tangled.org/core/xrpc/errors" 23 24 "tangled.org/core/xrpc/serviceauth" 24 25 ) ··· 131 132 return "", xrpcerr.RepoNotFoundError 132 133 } 133 134 return repoPath, nil 135 + } 136 + 137 + func (x *Xrpc) resolveRepoDID(repo *string, ownerDid, name string) (repoident.RepoDid, string, error) { 138 + raw, err := x.selectRepoDID(repo, ownerDid, name) 139 + if err != nil { 140 + return "", "", err 141 + } 142 + 143 + repoDid, err := repoident.NewRepoDid(raw) 144 + if err != nil { 145 + return "", "", err 146 + } 147 + 148 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid.String()) 149 + if err != nil { 150 + return "", "", err 151 + } 152 + return repoDid, repoPath, nil 153 + } 154 + 155 + func (x *Xrpc) selectRepoDID(repo *string, ownerDid, name string) (string, error) { 156 + if repo != nil && *repo != "" { 157 + return *repo, nil 158 + } 159 + return x.Db.GetRepoDid(ownerDid, name) 134 160 } 135 161 136 162 func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
+5
lexicons/repo/forkSync.json
··· 30 30 "type": "string", 31 31 "description": "Name of the forked repository" 32 32 }, 33 + "repo": { 34 + "type": "string", 35 + "format": "did", 36 + "description": "DID of the repository" 37 + }, 33 38 "branch": { 34 39 "type": "string", 35 40 "description": "Branch to sync"
+5
lexicons/repo/merge.json
··· 20 20 "type": "string", 21 21 "description": "Name of the repository" 22 22 }, 23 + "repo": { 24 + "type": "string", 25 + "format": "did", 26 + "description": "DID of the repository" 27 + }, 23 28 "patch": { 24 29 "type": "string", 25 30 "description": "Patch content to merge"
+5
lexicons/repo/mergeCheck.json
··· 20 20 "type": "string", 21 21 "description": "Name of the repository" 22 22 }, 23 + "repo": { 24 + "type": "string", 25 + "format": "did", 26 + "description": "DID of the repository" 27 + }, 23 28 "patch": { 24 29 "type": "string", 25 30 "description": "Patch or pull request to check for merge conflicts"
+31
repoident/repoident.go
··· 1 + package repoident 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/bluesky-social/indigo/atproto/syntax" 7 + ) 8 + 9 + type RepoDid syntax.DID 10 + 11 + func (r RepoDid) String() string { return string(r) } 12 + 13 + func NewRepoDid(s string) (RepoDid, error) { 14 + did, err := syntax.ParseDID(s) 15 + if err != nil { 16 + return "", fmt.Errorf("invalid repoDid %q: %w", s, err) 17 + } 18 + return RepoDid(did), nil 19 + } 20 + 21 + type OwnerDid syntax.DID 22 + 23 + func (o OwnerDid) String() string { return string(o) } 24 + 25 + func NewOwnerDid(s string) (OwnerDid, error) { 26 + did, err := syntax.ParseDID(s) 27 + if err != nil { 28 + return "", fmt.Errorf("invalid ownerDid %q: %w", s, err) 29 + } 30 + return OwnerDid(did), nil 31 + }
+37
repoident/repoident_test.go
··· 1 + package repoident 2 + 3 + import "testing" 4 + 5 + func TestNewRepoDid_RejectsInvalid(t *testing.T) { 6 + if _, err := NewRepoDid(""); err == nil { 7 + t.Error("NewRepoDid(\"\") err = nil, want error") 8 + } 9 + } 10 + 11 + func TestNewRepoDid_AcceptsValid(t *testing.T) { 12 + raw := "did:plc:boltless" 13 + got, err := NewRepoDid(raw) 14 + if err != nil { 15 + t.Fatalf("NewRepoDid(%q): %v", raw, err) 16 + } 17 + if got.String() != raw { 18 + t.Errorf("got %q, want %q", got, raw) 19 + } 20 + } 21 + 22 + func TestNewOwnerDid_RejectsInvalid(t *testing.T) { 23 + if _, err := NewOwnerDid("not-a-did"); err == nil { 24 + t.Error("NewOwnerDid(\"not-a-did\") err = nil, want error") 25 + } 26 + } 27 + 28 + func TestNewOwnerDid_AcceptsValid(t *testing.T) { 29 + raw := "did:plc:akshay" 30 + got, err := NewOwnerDid(raw) 31 + if err != nil { 32 + t.Fatalf("NewOwnerDid(%q): %v", raw, err) 33 + } 34 + if got.String() != raw { 35 + t.Errorf("got %q, want %q", got, raw) 36 + } 37 + }
+7 -31
repoverify/verify.go
··· 10 10 "syscall" 11 11 "time" 12 12 13 - "github.com/bluesky-social/indigo/atproto/syntax" 14 13 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 15 14 "tangled.org/core/api/tangled" 16 15 "tangled.org/core/idresolver" 16 + "tangled.org/core/repoident" 17 17 "tangled.org/core/xrpc/xrpcclient" 18 18 ) 19 19 20 - type RepoDid syntax.DID 21 - 22 - func (r RepoDid) String() string { return string(r) } 23 - 24 - func NewRepoDid(s string) (RepoDid, error) { 25 - did, err := syntax.ParseDID(s) 26 - if err != nil { 27 - return "", fmt.Errorf("invalid repoDid %q: %w", s, err) 28 - } 29 - return RepoDid(did), nil 30 - } 31 - 32 - type OwnerDid syntax.DID 33 - 34 - func (o OwnerDid) String() string { return string(o) } 35 - 36 - func NewOwnerDid(s string) (OwnerDid, error) { 37 - did, err := syntax.ParseDID(s) 38 - if err != nil { 39 - return "", fmt.Errorf("invalid ownerDid %q: %w", s, err) 40 - } 41 - return OwnerDid(did), nil 42 - } 43 - 44 20 func ParseKnotEndpoint(raw string, dev bool) (*url.URL, error) { 45 21 if raw == "" { 46 22 return nil, fmt.Errorf("empty knot URL") ··· 65 41 } 66 42 67 43 type Result struct { 68 - RepoDid RepoDid 69 - OwnerDid OwnerDid 44 + RepoDid repoident.RepoDid 45 + OwnerDid repoident.OwnerDid 70 46 KnotURL *url.URL 71 47 // Rkey of the sh.tangled.repo record tracked by the knot; empty when the 72 48 // knot does not support describeRepo. 73 49 Rkey string 74 50 } 75 51 76 - type Verifier func(ctx context.Context, repoDid RepoDid) (Result, error) 52 + type Verifier func(ctx context.Context, repoDid repoident.RepoDid) (Result, error) 77 53 78 54 const verifyTimeout = 10 * time.Second 79 55 ··· 86 62 Transport: transport, 87 63 } 88 64 89 - return func(ctx context.Context, repoDid RepoDid) (Result, error) { 65 + return func(ctx context.Context, repoDid repoident.RepoDid) (Result, error) { 90 66 ctx, cancel := context.WithTimeout(ctx, verifyTimeout) 91 67 defer cancel() 92 68 return resolveAndDescribe(ctx, resolver, httpClient, repoDid, dev) ··· 97 73 ctx context.Context, 98 74 resolver *idresolver.Resolver, 99 75 httpClient *http.Client, 100 - repoDid RepoDid, 76 + repoDid repoident.RepoDid, 101 77 dev bool, 102 78 ) (Result, error) { 103 79 ident, err := resolver.ResolveIdent(ctx, repoDid.String()) ··· 123 99 return Result{}, fmt.Errorf("knot %s returned mismatched repoDid: got %q, want %q", knot, out.RepoDid, repoDid) 124 100 } 125 101 126 - ownerDid, err := NewOwnerDid(out.OwnerDid) 102 + ownerDid, err := repoident.NewOwnerDid(out.OwnerDid) 127 103 if err != nil { 128 104 return Result{}, fmt.Errorf("describeRepo on %s returned invalid ownerDid: %w", knot, err) 129 105 }
-17
repoverify/verify_test.go
··· 2 2 3 3 import "testing" 4 4 5 - func TestNewRepoDid_RejectsInvalid(t *testing.T) { 6 - if _, err := NewRepoDid(""); err == nil { 7 - t.Error("NewRepoDid(\"\") err = nil, want error") 8 - } 9 - } 10 - 11 - func TestNewRepoDid_AcceptsValid(t *testing.T) { 12 - raw := "did:plc:abc123abc123abc123abc123" 13 - got, err := NewRepoDid(raw) 14 - if err != nil { 15 - t.Fatalf("NewRepoDid(%q): %v", raw, err) 16 - } 17 - if got.String() != raw { 18 - t.Errorf("got %q, want %q", got, raw) 19 - } 20 - } 21 - 22 5 func TestParseKnotEndpoint_RejectsHttpInProd(t *testing.T) { 23 6 if _, err := ParseKnotEndpoint("http://knot.example", false); err == nil { 24 7 t.Error("http:// knot URL accepted in prod")
+2 -1
spindle/server.go
··· 28 28 "tangled.org/core/log" 29 29 "tangled.org/core/notifier" 30 30 "tangled.org/core/rbac" 31 + "tangled.org/core/repoident" 31 32 "tangled.org/core/repoverify" 32 33 "tangled.org/core/spindle/config" 33 34 "tangled.org/core/spindle/db" ··· 519 520 } 520 521 521 522 // verify repo, we don't want git sync to point to arbitrary endpoints 522 - res, err := s.verify(ctx, repoverify.RepoDid(repoDid)) 523 + res, err := s.verify(ctx, repoident.RepoDid(repoDid)) 523 524 if err != nil { 524 525 return nil, fmt.Errorf("verify sourceRepo %s: %w", repoDid, err) 525 526 }