Monorepo for Tangled tangled.org

appview: pulls: allow stacked PR creation

Changed files
+192 -2
appview
db
state
xrpcclient
patchutil
+18 -2
appview/db/pulls.go
··· 271 } 272 } 273 274 _, err = tx.Exec( 275 ` 276 - insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state, source_branch, source_repo_at) 277 - values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 278 pull.RepoAt, 279 pull.OwnerDid, 280 pull.PullId, ··· 285 pull.State, 286 sourceBranch, 287 sourceRepoAt, 288 ) 289 if err != nil { 290 return err
··· 271 } 272 } 273 274 + var stackId, changeId, parentChangeId *string 275 + if pull.StackId != "" { 276 + stackId = &pull.StackId 277 + } 278 + if pull.ChangeId != "" { 279 + changeId = &pull.ChangeId 280 + } 281 + if pull.ParentChangeId != "" { 282 + parentChangeId = &pull.ParentChangeId 283 + } 284 + 285 _, err = tx.Exec( 286 ` 287 + insert into pulls ( 288 + repo_at, owner_did, pull_id, title, target_branch, body, rkey, state, source_branch, source_repo_at, stack_id, change_id, parent_change_id 289 + ) 290 + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 291 pull.RepoAt, 292 pull.OwnerDid, 293 pull.PullId, ··· 298 pull.State, 299 sourceBranch, 300 sourceRepoAt, 301 + stackId, 302 + changeId, 303 + parentChangeId, 304 ) 305 if err != nil { 306 return err
+147
appview/state/pull.go
··· 25 "github.com/bluesky-social/indigo/atproto/syntax" 26 lexutil "github.com/bluesky-social/indigo/lex/util" 27 "github.com/go-chi/chi/v5" 28 ) 29 30 // htmx fragment ··· 843 isStacked bool, 844 ) { 845 if isStacked { 846 } 847 848 tx, err := s.db.BeginTx(r.Context(), nil) ··· 933 } 934 935 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 936 } 937 938 func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) {
··· 25 "github.com/bluesky-social/indigo/atproto/syntax" 26 lexutil "github.com/bluesky-social/indigo/lex/util" 27 "github.com/go-chi/chi/v5" 28 + "github.com/google/uuid" 29 ) 30 31 // htmx fragment ··· 844 isStacked bool, 845 ) { 846 if isStacked { 847 + // creates a series of PRs, each linking to the previous, identified by jj's change-id 848 + s.createStackedPulLRequest( 849 + w, 850 + r, 851 + f, 852 + user, 853 + title, body, targetBranch, 854 + patch, 855 + sourceRev, 856 + pullSource, 857 + recordPullSource, 858 + ) 859 + return 860 } 861 862 tx, err := s.db.BeginTx(r.Context(), nil) ··· 947 } 948 949 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId)) 950 + } 951 + 952 + func (s *State) createStackedPulLRequest( 953 + w http.ResponseWriter, 954 + r *http.Request, 955 + f *FullyResolvedRepo, 956 + user *oauth.User, 957 + title, body, targetBranch string, 958 + patch string, 959 + sourceRev string, 960 + pullSource *db.PullSource, 961 + recordPullSource *tangled.RepoPull_Source, 962 + ) { 963 + // run some necessary checks for stacked-prs first 964 + 965 + // must be branch or fork based 966 + if sourceRev == "" { 967 + log.Println("stacked PR from patch-based pull") 968 + s.pages.Notice(w, "pull", "Stacking is only supported on branch and fork based pull-requests.") 969 + return 970 + } 971 + 972 + formatPatches, err := patchutil.ExtractPatches(patch) 973 + if err != nil { 974 + s.pages.Notice(w, "pull", fmt.Sprintf("Failed to extract patches: %v", err)) 975 + return 976 + } 977 + 978 + // must have atleast 1 patch to begin with 979 + if len(formatPatches) == 0 { 980 + s.pages.Notice(w, "pull", "No patches found in the generated format-patch.") 981 + return 982 + } 983 + 984 + tx, err := s.db.BeginTx(r.Context(), nil) 985 + if err != nil { 986 + log.Println("failed to start tx") 987 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 988 + return 989 + } 990 + defer tx.Rollback() 991 + 992 + // create a series of pull requests, and write records from them at once 993 + var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 994 + 995 + // the stack is identified by a UUID 996 + stackId := uuid.New() 997 + parentChangeId := "" 998 + for _, fp := range formatPatches { 999 + // all patches must have a jj change-id 1000 + changeId, err := fp.ChangeId() 1001 + if err != nil { 1002 + s.pages.Notice(w, "pull", "Stacking is only supported if all patches contain a change-id commit header.") 1003 + return 1004 + } 1005 + 1006 + title = fp.Title 1007 + body = fp.Body 1008 + rkey := appview.TID() 1009 + 1010 + // TODO: can we just use a format-patch string here? 1011 + initialSubmission := db.PullSubmission{ 1012 + Patch: fp.Patch(), 1013 + SourceRev: sourceRev, 1014 + } 1015 + err = db.NewPull(tx, &db.Pull{ 1016 + Title: title, 1017 + Body: body, 1018 + TargetBranch: targetBranch, 1019 + OwnerDid: user.Did, 1020 + RepoAt: f.RepoAt, 1021 + Rkey: rkey, 1022 + Submissions: []*db.PullSubmission{ 1023 + &initialSubmission, 1024 + }, 1025 + PullSource: pullSource, 1026 + 1027 + StackId: stackId.String(), 1028 + ChangeId: changeId, 1029 + ParentChangeId: parentChangeId, 1030 + }) 1031 + if err != nil { 1032 + log.Println("failed to create pull request", err) 1033 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1034 + return 1035 + } 1036 + 1037 + record := tangled.RepoPull{ 1038 + Title: title, 1039 + TargetRepo: string(f.RepoAt), 1040 + TargetBranch: targetBranch, 1041 + Patch: fp.Patch(), 1042 + Source: recordPullSource, 1043 + } 1044 + writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1045 + RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1046 + Collection: tangled.RepoPullNSID, 1047 + Rkey: &rkey, 1048 + Value: &lexutil.LexiconTypeDecoder{ 1049 + Val: &record, 1050 + }, 1051 + }, 1052 + }) 1053 + 1054 + parentChangeId = changeId 1055 + } 1056 + 1057 + client, err := s.oauth.AuthorizedClient(r) 1058 + if err != nil { 1059 + log.Println("failed to get authorized client", err) 1060 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1061 + return 1062 + } 1063 + 1064 + // apply all record creations at once 1065 + _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{ 1066 + Repo: user.Did, 1067 + Writes: writes, 1068 + }) 1069 + if err != nil { 1070 + log.Println("failed to create stacked pull request", err) 1071 + s.pages.Notice(w, "pull", "Failed to create stacked pull request. Try again later.") 1072 + return 1073 + } 1074 + 1075 + // create all pulls at once 1076 + if err = tx.Commit(); err != nil { 1077 + log.Println("failed to create pull request", err) 1078 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1079 + return 1080 + } 1081 + 1082 + s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", f.OwnerSlashRepo())) 1083 } 1084 1085 func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) {
+9
appview/xrpcclient/xrpc.go
··· 31 return &out, nil 32 } 33 34 func (c *Client) RepoGetRecord(ctx context.Context, cid string, collection string, repo string, rkey string) (*atproto.RepoGetRecord_Output, error) { 35 var out atproto.RepoGetRecord_Output 36
··· 31 return &out, nil 32 } 33 34 + func (c *Client) RepoApplyWrites(ctx context.Context, input *atproto.RepoApplyWrites_Input) (*atproto.RepoApplyWrites_Output, error) { 35 + var out atproto.RepoApplyWrites_Output 36 + if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.applyWrites", nil, input, &out); err != nil { 37 + return nil, err 38 + } 39 + 40 + return &out, nil 41 + } 42 + 43 func (c *Client) RepoGetRecord(ctx context.Context, cid string, collection string, repo string, rkey string) (*atproto.RepoGetRecord_Output, error) { 44 var out atproto.RepoGetRecord_Output 45
+2
flake.nix
··· 156 pkgs.websocat 157 pkgs.tailwindcss 158 pkgs.nixos-shell 159 ]; 160 shellHook = '' 161 mkdir -p appview/pages/static/{fonts,icons}
··· 156 pkgs.websocat 157 pkgs.tailwindcss 158 pkgs.nixos-shell 159 + pkgs.nodePackages.localtunnel 160 + pkgs.python312Packages.pyngrok 161 ]; 162 shellHook = '' 163 mkdir -p appview/pages/static/{fonts,icons}
+16
patchutil/patchutil.go
··· 15 *gitdiff.PatchHeader 16 } 17 18 func ExtractPatches(formatPatch string) ([]FormatPatch, error) { 19 patches := splitFormatPatch(formatPatch) 20
··· 15 *gitdiff.PatchHeader 16 } 17 18 + // Extracts just the diff from this format-patch 19 + func (f FormatPatch) Patch() string { 20 + var b strings.Builder 21 + for _, p := range f.Files { 22 + b.WriteString(p.String()) 23 + } 24 + return b.String() 25 + } 26 + 27 + func (f FormatPatch) ChangeId() (string, error) { 28 + if vals, ok := f.RawHeaders["Change-Id"]; ok && len(vals) == 1 { 29 + return vals[0], nil 30 + } 31 + return "", fmt.Errorf("no change-id found") 32 + } 33 + 34 func ExtractPatches(formatPatch string) ([]FormatPatch, error) { 35 patches := splitFormatPatch(formatPatch) 36