close
An unofficial terminal client for Tangled, optimized for humans and agents. tgcli.wisp.place
cli atproto go tangled
164

Configure Feed

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

resolve fork sources by repository name and submit new pull request rounds #18

Merged opened by okami.mom targeting master from [deleted fork]: master

the changes was actually made by @niri.pet after trying to get her to use tg and running into issues, but her repos have been getting eaten in the void that prevented being able to make the PR on her side.

this adds two small tg fixes:

  • resolve fork source repositories through the existing name-aware resolver instead of treating the display name as an atproto rkey
  • add tg pr update <rkey> to submit a fresh pull-request round

tg pr update was tested here by her and it worked flawlessly: https://tangled.org/okami.mom/nixcfg/pulls/1/round/2

resolves #5

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:3rwz3xfw2crswgifqgc3g7zh/sh.tangled.repo.pull/3mrtl3ongl422
+124 -12
Diff #0
+39
internal/app/pulls.go
··· 162 162 return &PRCreateResult{URI: uri, Title: in.Title, Base: base, Head: head}, nil 163 163 } 164 164 165 + func (s *Service) UpdatePullRound(ctx context.Context, repoDir, rkey string) error { 166 + atClient, did, err := s.authenticatedPDS(ctx) 167 + if err != nil { 168 + return err 169 + } 170 + 171 + return updateRecord(ctx, atClient, did, pullCollection, rkey, func(value any) (tangledlex.RepoPull, error) { 172 + data, err := json.Marshal(value) 173 + if err != nil { 174 + return tangledlex.RepoPull{}, fmt.Errorf("encode existing pull request: %w", err) 175 + } 176 + var record tangledlex.RepoPull 177 + if err := json.Unmarshal(data, &record); err != nil { 178 + return tangledlex.RepoPull{}, fmt.Errorf("decode existing pull request: %w", err) 179 + } 180 + if record.Target == nil || record.Source == nil || record.Target.Branch == "" || record.Source.Branch == "" { 181 + return tangledlex.RepoPull{}, fmt.Errorf("pull request %q has no source and target branches", rkey) 182 + } 183 + 184 + patch, err := s.git.GeneratePatch(ctx, repoDir, record.Target.Branch, record.Source.Branch) 185 + if err != nil { 186 + return tangledlex.RepoPull{}, fmt.Errorf("generate pull request patch: %w", err) 187 + } 188 + blob, err := atClient.UploadBlob(ctx, patch, patchMimeType) 189 + if err != nil { 190 + return tangledlex.RepoPull{}, err 191 + } 192 + patchBlob, err := patchBlob(blob) 193 + if err != nil { 194 + return tangledlex.RepoPull{}, err 195 + } 196 + record.Rounds = append(record.Rounds, &tangledlex.RepoPull_Round{ 197 + CreatedAt: time.Now().UTC().Format(time.RFC3339), 198 + PatchBlob: &patchBlob, 199 + }) 200 + return record, nil 201 + }) 202 + } 203 + 165 204 func atURIPrefix(uri string) bool { return len(uri) >= 5 && uri[:5] == "at://" } 166 205 167 206 func createPullRecord(ctx context.Context, atClient pdsClient, did string, input pullRecordInput) (string, error) {
+48
internal/app/pulls_test.go
··· 1 1 package app 2 2 3 3 import ( 4 + "context" 4 5 "testing" 5 6 "time" 6 7 7 8 "github.com/alyraffauf/tg/atproto" 9 + "github.com/alyraffauf/tg/internal/tangledlex" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + lexutil "github.com/bluesky-social/indigo/lex/util" 12 + "github.com/ipfs/go-cid" 8 13 ) 9 14 10 15 func TestNewPullRecordUsesDistinctSourceAndTarget(t *testing.T) { ··· 25 30 } 26 31 if stringValue(record.Source.Repo) != "did:plc:fork" { 27 32 t.Fatalf("unexpected source: %+v", record.Source) 33 + } 34 + } 35 + 36 + func TestUpdatePullRoundAppendsWithCompareAndSwap(t *testing.T) { 37 + oldCID := cid.MustParse("bafybeigdyrzt5m6b5nkn55vsgzzfw5cfs2tidw6zqugycdkyybf2z7kz4q") 38 + newCID := cid.MustParse("bafybeibwzifrf5tfwmbtw6ewjqr5q6rh5y5b6gzzledmxce5ilrjzsozoa") 39 + oldBlob := lexutil.LexBlob{MimeType: "application/gzip", Ref: lexutil.LexLink(oldCID)} 40 + pds := &testPDS{ 41 + record: &atproto.GetRecordOutput{ 42 + CID: func() *syntax.CID { value := syntax.CID("bafyreicurrent"); return &value }(), 43 + Value: tangledlex.RepoPull{ 44 + LexiconTypeID: pullCollection, 45 + Title: "Example", 46 + CreatedAt: "2026-07-29T00:00:00Z", 47 + Target: &tangledlex.RepoPull_Target{Repo: "did:plc:target", Branch: "main"}, 48 + Source: &tangledlex.RepoPull_Source{Repo: optionalString("did:plc:source"), Branch: "feature"}, 49 + Rounds: []*tangledlex.RepoPull_Round{{CreatedAt: "2026-07-29T00:00:00Z", PatchBlob: &oldBlob}}, 50 + }, 51 + }, 52 + uploadBlob: &atproto.Blob{Type: "blob", MimeType: "application/gzip", Ref: lexutil.LexLink(newCID), Size: 42}, 53 + } 54 + git := &testGit{patch: []byte("diff --git a/a b/a\n")} 55 + service := testService(pds, git, &testKnot{}) 56 + 57 + if err := service.UpdatePullRound(context.Background(), "/tmp/repo", "pr-1"); err != nil { 58 + t.Fatalf("UpdatePullRound() error = %v", err) 59 + } 60 + if len(pds.puts) != 1 { 61 + t.Fatalf("record writes = %d, want 1", len(pds.puts)) 62 + } 63 + put := pds.puts[0] 64 + if put.SwapRecord == nil || put.SwapRecord.String() != "bafyreicurrent" { 65 + t.Fatalf("SwapRecord = %v, want bafyreicurrent", put.SwapRecord) 66 + } 67 + record, ok := put.Record.(tangledlex.RepoPull) 68 + if !ok { 69 + t.Fatalf("record type = %T, want RepoPull", put.Record) 70 + } 71 + if len(record.Rounds) != 2 { 72 + t.Fatalf("round count = %d, want 2", len(record.Rounds)) 73 + } 74 + if record.Rounds[0].PatchBlob.Ref.String() != oldCID.String() || record.Rounds[1].PatchBlob.Ref.String() != newCID.String() { 75 + t.Fatalf("rounds = %+v", record.Rounds) 28 76 } 29 77 }
+5 -11
internal/app/repos.go
··· 612 612 } 613 613 614 614 func (s *Service) getForkSource(ctx context.Context, t Target) (forkSource, error) { 615 - ident, err := s.resolver.ResolveHandle(ctx, t.Handle) 615 + // Repository names are not guaranteed to be their ATProto record rkeys. 616 + // Use the common resolver so forks work for repositories with generated rkeys. 617 + repo, err := s.resolveRepo(ctx, t) 616 618 if err != nil { 617 - return forkSource{}, fmt.Errorf("resolve handle %q: %w", t.Handle, err) 618 - } 619 - uri := fmt.Sprintf("at://%s/sh.tangled.repo/%s", ident.DID, t.Repo) 620 - repo, err := s.appview.GetRepo(ctx, uri) 621 - if err != nil { 622 - return forkSource{}, fmt.Errorf("get source repository %s: %w", t, err) 619 + return forkSource{}, fmt.Errorf("resolve source repository %s: %w", t, err) 623 620 } 624 621 if repo.Value.Knot == "" { 625 622 return forkSource{}, fmt.Errorf("source repository %s has no knot", t) ··· 627 624 if stringValue(repo.Value.RepoDid) == "" { 628 625 return forkSource{}, fmt.Errorf("source repository %s has no repo DID", t) 629 626 } 630 - if repo.URI != "" { 631 - uri = repo.URI 632 - } 633 - return forkSource{URI: uri, Knot: repo.Value.Knot, RepoDID: stringValue(repo.Value.RepoDid)}, nil 627 + return forkSource{URI: repo.URI, Knot: repo.Value.Knot, RepoDID: stringValue(repo.Value.RepoDid)}, nil 634 628 } 635 629 636 630 func (s *Service) deleteFork(ctx context.Context, atClient pdsClient, knotHost, did, name string) error {
+16
internal/app/service_test.go
··· 269 269 record *atproto.GetRecordOutput 270 270 records []atproto.RecordItem 271 271 putErr error 272 + uploadBlob *atproto.Blob 273 + uploadErr error 272 274 listErr error 273 275 listCalls int 274 276 listOptions []atproto.ListRecordsOpts ··· 295 297 } 296 298 297 299 func (p *testPDS) UploadBlob(context.Context, []byte, string) (*atproto.Blob, error) { 300 + if p.uploadErr != nil { 301 + return nil, p.uploadErr 302 + } 303 + if p.uploadBlob != nil { 304 + return p.uploadBlob, nil 305 + } 298 306 return nil, errors.New("not implemented") 299 307 } 300 308 ··· 333 341 334 342 type testGit struct { 335 343 branch string 344 + patch []byte 345 + patchErr error 336 346 clones []gitutil.CloneRepoParams 337 347 pushes []gitutil.PushNewRepoParams 338 348 repoCandidates []gitutil.RepoContext ··· 350 360 } 351 361 func (g *testGit) CheckoutPatch(context.Context, gitutil.CheckoutPatchParams) error { return nil } 352 362 func (g *testGit) GeneratePatch(context.Context, string, string, string) ([]byte, error) { 363 + if g.patchErr != nil { 364 + return nil, g.patchErr 365 + } 366 + if g.patch != nil { 367 + return g.patch, nil 368 + } 353 369 return nil, errors.New("not implemented") 354 370 } 355 371 func (g *testGit) CurrentBranch(context.Context, string) (string, error) { return g.branch, nil }
+15
internal/cli/pr_state.go
··· 7 7 "github.com/spf13/cobra" 8 8 ) 9 9 10 + func newPRUpdateCommand(service *app.Service) *cobra.Command { 11 + return &cobra.Command{ 12 + Use: "update <rkey>", 13 + Short: "Submit a new round for a pull request", 14 + Args: cobra.ExactArgs(1), 15 + RunE: func(cmd *cobra.Command, args []string) error { 16 + repoDir, err := getwd() 17 + if err != nil { 18 + return err 19 + } 20 + return service.UpdatePullRound(cmd.Context(), repoDir, args[0]) 21 + }, 22 + } 23 + } 24 + 10 25 func newPRCloseCommand(service *app.Service) *cobra.Command { 11 26 return newPRStateCommand(service, "close", "closed") 12 27 }
+1 -1
internal/cli/root.go
··· 41 41 rootCmd.AddCommand(issue) 42 42 43 43 pull := newPRCommand(service) 44 - pull.AddCommand(newPRListCommand(service), newPRViewCommand(service), newPRCreateCommand(service), newPRCommentCommand(service), newPRDiffCommand(service), newPRCheckoutCommand(service), newPRCloseCommand(service), newPRReopenCommand(service), newPREditCommand(service), newPRMergeCommand(service)) 44 + pull.AddCommand(newPRListCommand(service), newPRViewCommand(service), newPRCreateCommand(service), newPRCommentCommand(service), newPRDiffCommand(service), newPRCheckoutCommand(service), newPRCloseCommand(service), newPRReopenCommand(service), newPREditCommand(service), newPRUpdateCommand(service), newPRMergeCommand(service)) 45 45 rootCmd.AddCommand(pull) 46 46 47 47 repo := newRepoCommand(service)

History

1 round 1 comment
Sign up or Login to add to the discussion
okami.mom submitted #0
2 commits
Expand
93d41964
fix: resolve fork sources by repository name
4ea60e72
feat: submit new pull request rounds
Expand 1 comment

ty ana!

Pull request successfully merged