diff --git a/internal/app/repo_create.go b/internal/app/repo_create.go index dc5c0d9..c8331b1 100644 --- a/internal/app/repo_create.go +++ b/internal/app/repo_create.go @@ -15,12 +15,20 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" ) -type ProvisionRepoInput struct { +type provisionRepoInput struct { KnotHost string Name string Description string } +type provisionedRepo struct { + URI string + RepoDID string + Handle string + KnotHost string + Warnings []string +} + // CreateRepoInput configures provisioning and optional local setup. type CreateRepoInput struct { KnotHost string @@ -52,17 +60,20 @@ func (s *Service) CreateRepo(ctx context.Context, in CreateRepoInput) (*RepoCrea } in.KnotHost = knotHost } - uri, handle, selectedKnot, warnings, err := s.provisionRepo(ctx, ProvisionRepoInput{ + provisioned, err := s.provisionRepo(ctx, provisionRepoInput{ KnotHost: in.KnotHost, Name: in.Name, Description: in.Description, }) if err != nil { return nil, err } - result := &RepoCreateResult{Handle: handle, Name: in.Name, URI: uri, Knot: selectedKnot, Warnings: warnings} + result := &RepoCreateResult{ + Handle: provisioned.Handle, Name: in.Name, URI: provisioned.URI, + Knot: provisioned.KnotHost, Warnings: provisioned.Warnings, + } if in.Clone { if _, err := s.cloneRepo(ctx, CloneRepoInput{ - KnotHost: selectedKnot, SSHPort: in.SSHPort, - Protocol: in.CloneProtocol, Handle: handle, Repo: in.Name, Destination: in.Name, + KnotHost: provisioned.KnotHost, SSHPort: in.SSHPort, + Protocol: in.CloneProtocol, Handle: provisioned.Handle, Repo: in.Name, Destination: in.Name, }); err != nil { return nil, fmt.Errorf("clone new repository: %w", err) } @@ -72,8 +83,8 @@ func (s *Service) CreateRepo(ctx context.Context, in CreateRepoInput) (*RepoCrea return result, nil } pushResult, err := s.pushNewRepo(ctx, PushNewRepoInput{ - KnotHost: selectedKnot, SSHPort: in.SSHPort, RepoURI: uri, Dir: in.PushPath, - Handle: handle, Repo: in.Name, RemoteName: in.RemoteName, + KnotHost: provisioned.KnotHost, SSHPort: in.SSHPort, RepoDID: provisioned.RepoDID, Dir: in.PushPath, + Handle: provisioned.Handle, Repo: in.Name, RemoteName: in.RemoteName, }) if pushResult.defaultBranchWarning != nil { result.Warnings = append(result.Warnings, fmt.Sprintf("could not set default branch: %v", pushResult.defaultBranchWarning)) @@ -86,45 +97,48 @@ func (s *Service) CreateRepo(ctx context.Context, in CreateRepoInput) (*RepoCrea return result, nil } -func (s *Service) provisionRepo(ctx context.Context, in ProvisionRepoInput) (uri, handle, knotHost string, warnings []string, err error) { +func (s *Service) provisionRepo(ctx context.Context, in provisionRepoInput) (*provisionedRepo, error) { atClient, did, err := s.authenticatedPDS(ctx) if err != nil { - return "", "", "", nil, err + return nil, err } - knotHost, warnings, err = s.selectCreationKnot(ctx, atClient, did, in.KnotHost) + knotHost, warnings, err := s.selectCreationKnot(ctx, atClient, did, in.KnotHost) if err != nil { - return "", "", "", nil, err + return nil, err } token, err := atClient.GetServiceAuth(ctx, "did:web:"+knotHost, "sh.tangled.repo.create") if err != nil { - return "", "", "", nil, err + return nil, err } - repoDid, err := s.knot.New(knotHost, token).CreateRepo(ctx, knot.CreateRepoInput{ + repoDID, err := s.knot.New(knotHost, token).CreateRepo(ctx, knot.CreateRepoInput{ Name: in.Name, Rkey: in.Name, }) if err != nil { - return "", "", "", nil, err + return nil, err } record := tangledlex.Repo{ LexiconTypeID: repoCollection, Knot: knotHost, CreatedAt: time.Now().UTC().Format(time.RFC3339), - RepoDid: optionalString(repoDid), + RepoDid: optionalString(repoDID), } if in.Description != "" { record.Description = optionalString(in.Description) } - uri, _, err = atClient.PutRecord(ctx, atproto.PutRecordInput{ + uri, _, err := atClient.PutRecord(ctx, atproto.PutRecordInput{ Repo: did, Collection: repoCollection, Rkey: in.Name, Record: record, }) if err != nil { - return "", "", "", nil, err + return nil, err } - return uri, s.ownerHandle(ctx, did), knotHost, warnings, nil + return &provisionedRepo{ + URI: uri, RepoDID: repoDID, Handle: s.ownerHandle(ctx, did), + KnotHost: knotHost, Warnings: warnings, + }, nil } func (s *Service) selectCreationKnot(ctx context.Context, atClient pdsClient, did, configured string) (string, []string, error) { @@ -195,7 +209,7 @@ func (s *Service) selectCreationKnot(ctx context.Context, atClient pdsClient, di return verified[0], warnings, nil } -func (s *Service) setDefaultBranchFromDir(ctx context.Context, knotHost, repoURI, dir string) (string, error) { +func (s *Service) setDefaultBranchFromDir(ctx context.Context, knotHost, repoDID, dir string) (string, error) { atClient, _, err := s.authenticatedPDS(ctx) if err != nil { return "", err @@ -204,7 +218,7 @@ func (s *Service) setDefaultBranchFromDir(ctx context.Context, knotHost, repoURI if err != nil { return "", err } - if err := s.setKnotDefaultBranch(ctx, atClient, knotHost, repoURI, branch); err != nil { + if err := s.setKnotDefaultBranch(ctx, atClient, knotHost, repoDID, branch); err != nil { return branch, err } return branch, nil @@ -219,7 +233,7 @@ type pushNewRepoResult struct { type PushNewRepoInput struct { KnotHost string SSHPort int - RepoURI string + RepoDID string Dir string Handle string Repo string @@ -227,7 +241,7 @@ type PushNewRepoInput struct { } func (s *Service) pushNewRepo(ctx context.Context, in PushNewRepoInput) (pushNewRepoResult, error) { - branch, defaultBranchErr := s.setDefaultBranchFromDir(ctx, in.KnotHost, in.RepoURI, in.Dir) + branch, defaultBranchErr := s.setDefaultBranchFromDir(ctx, in.KnotHost, in.RepoDID, in.Dir) result := pushNewRepoResult{defaultBranch: branch, defaultBranchWarning: defaultBranchErr} if err := s.git.PushNewRepo(ctx, gitutil.PushNewRepoParams{ Dir: in.Dir, KnotHost: in.KnotHost, SSHPort: in.SSHPort, diff --git a/internal/app/repo_set_default_branch.go b/internal/app/repo_set_default_branch.go index 5f685af..609f73b 100644 --- a/internal/app/repo_set_default_branch.go +++ b/internal/app/repo_set_default_branch.go @@ -20,19 +20,23 @@ func (s *Service) SetRepoDefaultBranch(ctx context.Context, t Target, branch str if repo.Value.Knot == "" { return nil, fmt.Errorf("repo %q has no knot", t.String()) } - if err := s.setKnotDefaultBranch(ctx, atClient, repo.Value.Knot, repo.URI, branch); err != nil { + repoDID := stringValue(repo.Value.RepoDid) + if repoDID == "" { + return nil, fmt.Errorf("repo %q has no repository DID", t.String()) + } + if err := s.setKnotDefaultBranch(ctx, atClient, repo.Value.Knot, repoDID, branch); err != nil { return nil, err } return &RepoDefaultBranchResult{URI: repo.URI, Branch: branch}, nil } -func (s *Service) setKnotDefaultBranch(ctx context.Context, atClient pdsClient, knotHost, repoURI, branch string) error { +func (s *Service) setKnotDefaultBranch(ctx context.Context, atClient pdsClient, knotHost, repoDID, branch string) error { token, err := atClient.GetServiceAuth(ctx, "did:web:"+knotHost, "sh.tangled.repo.setDefaultBranch") if err != nil { return fmt.Errorf("get knot authorization: %w", err) } return s.knot.New(knotHost, token).SetDefaultBranch(ctx, knot.SetDefaultBranchInput{ - Repo: repoURI, + Repo: repoDID, DefaultBranch: branch, }) } diff --git a/internal/app/service_test.go b/internal/app/service_test.go index 65f99ab..7bc12a1 100644 --- a/internal/app/service_test.go +++ b/internal/app/service_test.go @@ -63,6 +63,9 @@ func TestCreateRepoRecordsDefaultBranchOutcome(t *testing.T) { if len(git.pushes) != 1 { t.Fatalf("git pushes = %+v", git.pushes) } + if knotClient.setDefaultBranchInput.Repo != "did:plc:repo" { + t.Fatalf("SetDefaultBranch() repo = %q, want newly created repository DID", knotClient.setDefaultBranchInput.Repo) + } if git.pushes[0].KnotHost != "knot.example" || git.pushes[0].SSHPort != 2222 { t.Fatalf("git push destination = %+v", git.pushes[0]) } @@ -70,6 +73,27 @@ func TestCreateRepoRecordsDefaultBranchOutcome(t *testing.T) { } } +func TestSetRepoDefaultBranchUsesRepositoryDID(t *testing.T) { + pds := &testPDS{} + knotClient := &testKnot{} + service := testService(pds, &testGit{}, knotClient) + service.appview = testAppview{repo: &tangled.Repo{ + URI: "at://did:plc:owner/sh.tangled.repo/example", + Value: tangledlex.Repo{ + Knot: "knot.example", + RepoDid: optionalString("did:plc:repository"), + }, + }} + + _, err := service.SetRepoDefaultBranch(context.Background(), Target{Handle: "owner.test", Repo: "example"}, "main") + if err != nil { + t.Fatalf("SetRepoDefaultBranch() error = %v", err) + } + if knotClient.setDefaultBranchInput.Repo != "did:plc:repository" { + t.Fatalf("SetDefaultBranch() repo = %q, want repository DID", knotClient.setDefaultBranchInput.Repo) + } +} + func TestDeleteRepoRestoresRecordWhenKnotDeleteFails(t *testing.T) { pds := &testPDS{record: &atproto.GetRecordOutput{Value: map[string]any{"$type": repoCollection, "knot": "knot.example", "createdAt": "2026-07-25T12:00:00Z"}}} knotClient := &testKnot{deleteErr: errors.New("knot unavailable")} @@ -438,13 +462,14 @@ func (v *testKnotOwnershipVerifier) Verify(_ context.Context, host, _ string) er } type testKnot struct { - setDefaultBranchErr error - defaultBranch *knot.DefaultBranch - deleteErr error - deleteCalls int - mergeCalls int - mergeInput knot.MergeInput - createCalls int + setDefaultBranchErr error + setDefaultBranchInput knot.SetDefaultBranchInput + defaultBranch *knot.DefaultBranch + deleteErr error + deleteCalls int + mergeCalls int + mergeInput knot.MergeInput + createCalls int } func (k *testKnot) CreateRepo(context.Context, knot.CreateRepoInput) (string, error) { @@ -455,7 +480,8 @@ func (k *testKnot) DeleteRepo(context.Context, knot.DeleteRepoInput) error { k.deleteCalls++ return k.deleteErr } -func (k *testKnot) SetDefaultBranch(context.Context, knot.SetDefaultBranchInput) error { +func (k *testKnot) SetDefaultBranch(_ context.Context, input knot.SetDefaultBranchInput) error { + k.setDefaultBranchInput = input return k.setDefaultBranchErr } func (k *testKnot) GetDefaultBranch(context.Context, string) (*knot.DefaultBranch, error) { diff --git a/knot/repo_set_default_branch.go b/knot/repo_set_default_branch.go index b6b996d..389b876 100644 --- a/knot/repo_set_default_branch.go +++ b/knot/repo_set_default_branch.go @@ -9,7 +9,7 @@ import ( // SetDefaultBranchInput is the argument to sh.tangled.repo.setDefaultBranch. type SetDefaultBranchInput struct { - Repo string `json:"repo"` // at:// URI of the sh.tangled.repo record + Repo string `json:"repo"` // repository DID DefaultBranch string `json:"defaultBranch"` }