From 6600c93cafd2cfea8e46c05f5f34d6ce98180def Mon Sep 17 00:00:00 2001 From: niri Date: Wed, 29 Jul 2026 16:30:20 -0400 Subject: [PATCH] feat: submit new pull request rounds --- internal/app/pulls.go | 39 +++++++++++++++++++++++++++++ internal/app/pulls_test.go | 48 ++++++++++++++++++++++++++++++++++++ internal/app/service_test.go | 16 ++++++++++++ internal/cli/pr_state.go | 15 +++++++++++ internal/cli/root.go | 2 +- 5 files changed, 119 insertions(+), 1 deletion(-) diff --git a/internal/app/pulls.go b/internal/app/pulls.go index dbf51a2..6e5b7e4 100644 --- a/internal/app/pulls.go +++ b/internal/app/pulls.go @@ -162,6 +162,45 @@ func (s *Service) CreatePull(ctx context.Context, in CreatePullInput) (*PRCreate return &PRCreateResult{URI: uri, Title: in.Title, Base: base, Head: head}, nil } +func (s *Service) UpdatePullRound(ctx context.Context, repoDir, rkey string) error { + atClient, did, err := s.authenticatedPDS(ctx) + if err != nil { + return err + } + + return updateRecord(ctx, atClient, did, pullCollection, rkey, func(value any) (tangledlex.RepoPull, error) { + data, err := json.Marshal(value) + if err != nil { + return tangledlex.RepoPull{}, fmt.Errorf("encode existing pull request: %w", err) + } + var record tangledlex.RepoPull + if err := json.Unmarshal(data, &record); err != nil { + return tangledlex.RepoPull{}, fmt.Errorf("decode existing pull request: %w", err) + } + if record.Target == nil || record.Source == nil || record.Target.Branch == "" || record.Source.Branch == "" { + return tangledlex.RepoPull{}, fmt.Errorf("pull request %q has no source and target branches", rkey) + } + + patch, err := s.git.GeneratePatch(ctx, repoDir, record.Target.Branch, record.Source.Branch) + if err != nil { + return tangledlex.RepoPull{}, fmt.Errorf("generate pull request patch: %w", err) + } + blob, err := atClient.UploadBlob(ctx, patch, patchMimeType) + if err != nil { + return tangledlex.RepoPull{}, err + } + patchBlob, err := patchBlob(blob) + if err != nil { + return tangledlex.RepoPull{}, err + } + record.Rounds = append(record.Rounds, &tangledlex.RepoPull_Round{ + CreatedAt: time.Now().UTC().Format(time.RFC3339), + PatchBlob: &patchBlob, + }) + return record, nil + }) +} + func atURIPrefix(uri string) bool { return len(uri) >= 5 && uri[:5] == "at://" } func createPullRecord(ctx context.Context, atClient pdsClient, did string, input pullRecordInput) (string, error) { diff --git a/internal/app/pulls_test.go b/internal/app/pulls_test.go index 576dc9b..d92f0c0 100644 --- a/internal/app/pulls_test.go +++ b/internal/app/pulls_test.go @@ -1,10 +1,15 @@ package app import ( + "context" "testing" "time" "github.com/alyraffauf/tg/atproto" + "github.com/alyraffauf/tg/internal/tangledlex" + "github.com/bluesky-social/indigo/atproto/syntax" + lexutil "github.com/bluesky-social/indigo/lex/util" + "github.com/ipfs/go-cid" ) func TestNewPullRecordUsesDistinctSourceAndTarget(t *testing.T) { @@ -27,3 +32,46 @@ func TestNewPullRecordUsesDistinctSourceAndTarget(t *testing.T) { t.Fatalf("unexpected source: %+v", record.Source) } } + +func TestUpdatePullRoundAppendsWithCompareAndSwap(t *testing.T) { + oldCID := cid.MustParse("bafybeigdyrzt5m6b5nkn55vsgzzfw5cfs2tidw6zqugycdkyybf2z7kz4q") + newCID := cid.MustParse("bafybeibwzifrf5tfwmbtw6ewjqr5q6rh5y5b6gzzledmxce5ilrjzsozoa") + oldBlob := lexutil.LexBlob{MimeType: "application/gzip", Ref: lexutil.LexLink(oldCID)} + pds := &testPDS{ + record: &atproto.GetRecordOutput{ + CID: func() *syntax.CID { value := syntax.CID("bafyreicurrent"); return &value }(), + Value: tangledlex.RepoPull{ + LexiconTypeID: pullCollection, + Title: "Example", + CreatedAt: "2026-07-29T00:00:00Z", + Target: &tangledlex.RepoPull_Target{Repo: "did:plc:target", Branch: "main"}, + Source: &tangledlex.RepoPull_Source{Repo: optionalString("did:plc:source"), Branch: "feature"}, + Rounds: []*tangledlex.RepoPull_Round{{CreatedAt: "2026-07-29T00:00:00Z", PatchBlob: &oldBlob}}, + }, + }, + uploadBlob: &atproto.Blob{Type: "blob", MimeType: "application/gzip", Ref: lexutil.LexLink(newCID), Size: 42}, + } + git := &testGit{patch: []byte("diff --git a/a b/a\n")} + service := testService(pds, git, &testKnot{}) + + if err := service.UpdatePullRound(context.Background(), "/tmp/repo", "pr-1"); err != nil { + t.Fatalf("UpdatePullRound() error = %v", err) + } + if len(pds.puts) != 1 { + t.Fatalf("record writes = %d, want 1", len(pds.puts)) + } + put := pds.puts[0] + if put.SwapRecord == nil || put.SwapRecord.String() != "bafyreicurrent" { + t.Fatalf("SwapRecord = %v, want bafyreicurrent", put.SwapRecord) + } + record, ok := put.Record.(tangledlex.RepoPull) + if !ok { + t.Fatalf("record type = %T, want RepoPull", put.Record) + } + if len(record.Rounds) != 2 { + t.Fatalf("round count = %d, want 2", len(record.Rounds)) + } + if record.Rounds[0].PatchBlob.Ref.String() != oldCID.String() || record.Rounds[1].PatchBlob.Ref.String() != newCID.String() { + t.Fatalf("rounds = %+v", record.Rounds) + } +} diff --git a/internal/app/service_test.go b/internal/app/service_test.go index 222b628..c2d73a3 100644 --- a/internal/app/service_test.go +++ b/internal/app/service_test.go @@ -269,6 +269,8 @@ type testPDS struct { record *atproto.GetRecordOutput records []atproto.RecordItem putErr error + uploadBlob *atproto.Blob + uploadErr error listErr error listCalls int listOptions []atproto.ListRecordsOpts @@ -295,6 +297,12 @@ func (p *testPDS) DeleteRecord(_ context.Context, input atproto.DeleteRecordInpu } func (p *testPDS) UploadBlob(context.Context, []byte, string) (*atproto.Blob, error) { + if p.uploadErr != nil { + return nil, p.uploadErr + } + if p.uploadBlob != nil { + return p.uploadBlob, nil + } return nil, errors.New("not implemented") } @@ -333,6 +341,8 @@ func (p *testPDS) GetServiceAuth(_ context.Context, audience, _ string) (string, type testGit struct { branch string + patch []byte + patchErr error clones []gitutil.CloneRepoParams pushes []gitutil.PushNewRepoParams repoCandidates []gitutil.RepoContext @@ -350,6 +360,12 @@ func (g *testGit) PushNewRepo(_ context.Context, input gitutil.PushNewRepoParams } func (g *testGit) CheckoutPatch(context.Context, gitutil.CheckoutPatchParams) error { return nil } func (g *testGit) GeneratePatch(context.Context, string, string, string) ([]byte, error) { + if g.patchErr != nil { + return nil, g.patchErr + } + if g.patch != nil { + return g.patch, nil + } return nil, errors.New("not implemented") } func (g *testGit) CurrentBranch(context.Context, string) (string, error) { return g.branch, nil } diff --git a/internal/cli/pr_state.go b/internal/cli/pr_state.go index 8371882..dfef50f 100644 --- a/internal/cli/pr_state.go +++ b/internal/cli/pr_state.go @@ -7,6 +7,21 @@ import ( "github.com/spf13/cobra" ) +func newPRUpdateCommand(service *app.Service) *cobra.Command { + return &cobra.Command{ + Use: "update ", + Short: "Submit a new round for a pull request", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + repoDir, err := getwd() + if err != nil { + return err + } + return service.UpdatePullRound(cmd.Context(), repoDir, args[0]) + }, + } +} + func newPRCloseCommand(service *app.Service) *cobra.Command { return newPRStateCommand(service, "close", "closed") } diff --git a/internal/cli/root.go b/internal/cli/root.go index 4a73189..e312655 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -41,7 +41,7 @@ func newRoot(service *app.Service, defaultKnot, defaultSSHPort string) *cobra.Co rootCmd.AddCommand(issue) pull := newPRCommand(service) - pull.AddCommand(newPRListCommand(service), newPRViewCommand(service), newPRCreateCommand(service), newPRCommentCommand(service), newPRDiffCommand(service), newPRCheckoutCommand(service), newPRCloseCommand(service), newPRReopenCommand(service), newPREditCommand(service), newPRMergeCommand(service)) + pull.AddCommand(newPRListCommand(service), newPRViewCommand(service), newPRCreateCommand(service), newPRCommentCommand(service), newPRDiffCommand(service), newPRCheckoutCommand(service), newPRCloseCommand(service), newPRReopenCommand(service), newPREditCommand(service), newPRUpdateCommand(service), newPRMergeCommand(service)) rootCmd.AddCommand(pull) repo := newRepoCommand(service) -- 2.51.2