Monorepo for Tangled tangled.org

lexicons,appview/pulls: upload patches as blobs

By using blobs we massively increase our maximum patch size. When stored
directly on the record patches can be at most 2 MiB. By moving it to a
blob, we get a minimum of 50 MiB (smaller if the pds has set it but 50
is the default).

legacy field name `patch` in `repo.pull` lexicon is preserved (but not
used) to support some level of backward compatibility

Signed-off-by: Samuel Shuert <me@thecoded.prof>
Signed-off-by: Seongmin Lee <git@boltless.me>

Changed files
+148 -68
api
appview
models
pulls
lexicons
pulls
+79 -20
api/tangled/cbor_gen.go
··· 7934 7934 } 7935 7935 7936 7936 cw := cbg.NewCborWriter(w) 7937 - fieldCount := 9 7937 + fieldCount := 10 7938 7938 7939 7939 if t.Body == nil { 7940 7940 fieldCount-- 7941 7941 } 7942 7942 7943 7943 if t.Mentions == nil { 7944 + fieldCount-- 7945 + } 7946 + 7947 + if t.Patch == nil { 7944 7948 fieldCount-- 7945 7949 } 7946 7950 ··· 8008 8012 } 8009 8013 8010 8014 // t.Patch (string) (string) 8011 - if len("patch") > 1000000 { 8012 - return xerrors.Errorf("Value in field \"patch\" was too long") 8013 - } 8015 + if t.Patch != nil { 8014 8016 8015 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil { 8016 - return err 8017 - } 8018 - if _, err := cw.WriteString(string("patch")); err != nil { 8019 - return err 8020 - } 8017 + if len("patch") > 1000000 { 8018 + return xerrors.Errorf("Value in field \"patch\" was too long") 8019 + } 8021 8020 8022 - if len(t.Patch) > 1000000 { 8023 - return xerrors.Errorf("Value in field t.Patch was too long") 8024 - } 8021 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil { 8022 + return err 8023 + } 8024 + if _, err := cw.WriteString(string("patch")); err != nil { 8025 + return err 8026 + } 8027 + 8028 + if t.Patch == nil { 8029 + if _, err := cw.Write(cbg.CborNull); err != nil { 8030 + return err 8031 + } 8032 + } else { 8033 + if len(*t.Patch) > 1000000 { 8034 + return xerrors.Errorf("Value in field t.Patch was too long") 8035 + } 8025 8036 8026 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Patch))); err != nil { 8027 - return err 8028 - } 8029 - if _, err := cw.WriteString(string(t.Patch)); err != nil { 8030 - return err 8037 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Patch))); err != nil { 8038 + return err 8039 + } 8040 + if _, err := cw.WriteString(string(*t.Patch)); err != nil { 8041 + return err 8042 + } 8043 + } 8031 8044 } 8032 8045 8033 8046 // t.Title (string) (string) ··· 8147 8160 return err 8148 8161 } 8149 8162 8163 + // t.PatchBlob (util.LexBlob) (struct) 8164 + if len("patchBlob") > 1000000 { 8165 + return xerrors.Errorf("Value in field \"patchBlob\" was too long") 8166 + } 8167 + 8168 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patchBlob"))); err != nil { 8169 + return err 8170 + } 8171 + if _, err := cw.WriteString(string("patchBlob")); err != nil { 8172 + return err 8173 + } 8174 + 8175 + if err := t.PatchBlob.MarshalCBOR(cw); err != nil { 8176 + return err 8177 + } 8178 + 8150 8179 // t.References ([]string) (slice) 8151 8180 if t.References != nil { 8152 8181 ··· 8262 8291 case "patch": 8263 8292 8264 8293 { 8265 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8294 + b, err := cr.ReadByte() 8266 8295 if err != nil { 8267 8296 return err 8268 8297 } 8298 + if b != cbg.CborNull[0] { 8299 + if err := cr.UnreadByte(); err != nil { 8300 + return err 8301 + } 8269 8302 8270 - t.Patch = string(sval) 8303 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 8304 + if err != nil { 8305 + return err 8306 + } 8307 + 8308 + t.Patch = (*string)(&sval) 8309 + } 8271 8310 } 8272 8311 // t.Title (string) (string) 8273 8312 case "title": ··· 8370 8409 } 8371 8410 8372 8411 t.CreatedAt = string(sval) 8412 + } 8413 + // t.PatchBlob (util.LexBlob) (struct) 8414 + case "patchBlob": 8415 + 8416 + { 8417 + 8418 + b, err := cr.ReadByte() 8419 + if err != nil { 8420 + return err 8421 + } 8422 + if b != cbg.CborNull[0] { 8423 + if err := cr.UnreadByte(); err != nil { 8424 + return err 8425 + } 8426 + t.PatchBlob = new(util.LexBlob) 8427 + if err := t.PatchBlob.UnmarshalCBOR(cr); err != nil { 8428 + return xerrors.Errorf("unmarshaling t.PatchBlob pointer: %w", err) 8429 + } 8430 + } 8431 + 8373 8432 } 8374 8433 // t.References ([]string) (slice) 8375 8434 case "references":
+12 -9
api/tangled/repopull.go
··· 17 17 } // 18 18 // RECORDTYPE: RepoPull 19 19 type RepoPull struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 - Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 - Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 - Patch string `json:"patch" cborgen:"patch"` 25 - References []string `json:"references,omitempty" cborgen:"references,omitempty"` 26 - Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 27 - Target *RepoPull_Target `json:"target" cborgen:"target"` 28 - Title string `json:"title" cborgen:"title"` 20 + LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 + Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 + Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 + // patch: (deprecated) use patchBlob instead 25 + Patch *string `json:"patch,omitempty" cborgen:"patch,omitempty"` 26 + // patchBlob: patch content 27 + PatchBlob *util.LexBlob `json:"patchBlob" cborgen:"patchBlob"` 28 + References []string `json:"references,omitempty" cborgen:"references,omitempty"` 29 + Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 30 + Target *RepoPull_Target `json:"target" cborgen:"target"` 31 + Title string `json:"title" cborgen:"title"` 29 32 } 30 33 31 34 // RepoPull_Source is a "source" in the sh.tangled.repo.pull schema.
+1 -1
appview/models/pull.go
··· 83 83 Repo *Repo 84 84 } 85 85 86 + // NOTE: This method does not include patch blob in returned atproto record 86 87 func (p Pull) AsRecord() tangled.RepoPull { 87 88 var source *tangled.RepoPull_Source 88 89 if p.PullSource != nil { ··· 113 114 Repo: p.RepoAt.String(), 114 115 Branch: p.TargetBranch, 115 116 }, 116 - Patch: p.LatestPatch(), 117 117 Source: source, 118 118 } 119 119 return record
+48 -36
appview/pulls/pulls.go
··· 1241 1241 return 1242 1242 } 1243 1243 1244 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1245 + if err != nil { 1246 + log.Println("failed to upload patch", err) 1247 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1248 + return 1249 + } 1250 + 1244 1251 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1245 1252 Collection: tangled.RepoPullNSID, 1246 1253 Repo: user.Did, ··· 1252 1259 Repo: string(repo.RepoAt()), 1253 1260 Branch: targetBranch, 1254 1261 }, 1255 - Patch: patch, 1262 + PatchBlob: blob.Blob, 1256 1263 Source: recordPullSource, 1257 1264 CreatedAt: time.Now().Format(time.RFC3339), 1258 1265 }, ··· 1328 1335 // apply all record creations at once 1329 1336 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1330 1337 for _, p := range stack { 1338 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(p.LatestPatch())) 1339 + if err != nil { 1340 + log.Println("failed to upload patch blob", err) 1341 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1342 + return 1343 + } 1344 + 1331 1345 record := p.AsRecord() 1332 - write := comatproto.RepoApplyWrites_Input_Writes_Elem{ 1346 + record.PatchBlob = blob.Blob 1347 + writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1333 1348 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1334 1349 Collection: tangled.RepoPullNSID, 1335 1350 Rkey: &p.Rkey, ··· 1337 1352 Val: &record, 1338 1353 }, 1339 1354 }, 1340 - } 1341 - writes = append(writes, &write) 1355 + }) 1342 1356 } 1343 1357 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 1344 1358 Repo: user.Did, ··· 1871 1885 return 1872 1886 } 1873 1887 1874 - var recordPullSource *tangled.RepoPull_Source 1875 - if pull.IsBranchBased() { 1876 - recordPullSource = &tangled.RepoPull_Source{ 1877 - Branch: pull.PullSource.Branch, 1878 - Sha: sourceRev, 1879 - } 1888 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1889 + if err != nil { 1890 + log.Println("failed to upload patch blob", err) 1891 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1892 + return 1880 1893 } 1881 - if pull.IsForkBased() { 1882 - repoAt := pull.PullSource.RepoAt.String() 1883 - recordPullSource = &tangled.RepoPull_Source{ 1884 - Branch: pull.PullSource.Branch, 1885 - Repo: &repoAt, 1886 - Sha: sourceRev, 1887 - } 1888 - } 1894 + record := pull.AsRecord() 1895 + record.PatchBlob = blob.Blob 1896 + record.CreatedAt = time.Now().Format(time.RFC3339) 1889 1897 1890 1898 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1891 1899 Collection: tangled.RepoPullNSID, ··· 1893 1901 Rkey: pull.Rkey, 1894 1902 SwapRecord: ex.Cid, 1895 1903 Record: &lexutil.LexiconTypeDecoder{ 1896 - Val: &tangled.RepoPull{ 1897 - Title: pull.Title, 1898 - Target: &tangled.RepoPull_Target{ 1899 - Repo: string(repo.RepoAt()), 1900 - Branch: pull.TargetBranch, 1901 - }, 1902 - Patch: patch, // new patch 1903 - Source: recordPullSource, 1904 - CreatedAt: time.Now().Format(time.RFC3339), 1905 - }, 1904 + Val: &record, 1906 1905 }, 1907 1906 }) 1908 1907 if err != nil { ··· 1988 1987 } 1989 1988 defer tx.Rollback() 1990 1989 1990 + client, err := s.oauth.AuthorizedClient(r) 1991 + if err != nil { 1992 + log.Println("failed to authorize client") 1993 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1994 + return 1995 + } 1996 + 1991 1997 // pds updates to make 1992 1998 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1993 1999 ··· 2021 2027 return 2022 2028 } 2023 2029 2030 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2031 + if err != nil { 2032 + log.Println("failed to upload patch blob", err) 2033 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2034 + return 2035 + } 2024 2036 record := p.AsRecord() 2037 + record.PatchBlob = blob.Blob 2025 2038 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2026 2039 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 2027 2040 Collection: tangled.RepoPullNSID, ··· 2056 2069 return 2057 2070 } 2058 2071 2072 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2073 + if err != nil { 2074 + log.Println("failed to upload patch blob", err) 2075 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2076 + return 2077 + } 2059 2078 record := np.AsRecord() 2060 - 2079 + record.PatchBlob = blob.Blob 2061 2080 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2062 2081 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 2063 2082 Collection: tangled.RepoPullNSID, ··· 2091 2110 if err != nil { 2092 2111 log.Println("failed to resubmit pull", err) 2093 2112 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2094 - return 2095 - } 2096 - 2097 - client, err := s.oauth.AuthorizedClient(r) 2098 - if err != nil { 2099 - log.Println("failed to authorize client") 2100 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2101 2113 return 2102 2114 } 2103 2115
+8 -2
lexicons/pulls/pull.json
··· 12 12 "required": [ 13 13 "target", 14 14 "title", 15 - "patch", 15 + "patchBlob", 16 16 "createdAt" 17 17 ], 18 18 "properties": { ··· 27 27 "type": "string" 28 28 }, 29 29 "patch": { 30 - "type": "string" 30 + "type": "string", 31 + "description": "(deprecated) use patchBlob instead" 32 + }, 33 + "patchBlob": { 34 + "type": "blob", 35 + "accept": "text/x-patch", 36 + "description": "patch content" 31 37 }, 32 38 "source": { 33 39 "type": "ref",