Monorepo for Tangled

wip: pr rewrite

tl;dr
- drop the stacked PR feature. It will be reimplemented as shared stacks
later.
- atprotate the PR record. Ingester will track the PR resubmission using
CID.
- switch to service layer for future xrpc endpoints/unit tests

TODO: use `com.atproto.repo.strongRef`

Signed-off-by: Seongmin Lee <git@boltless.me>

boltless.me 1a0ca28e 5bdb6472

verified
+877 -10
+102
appview/db/db.go
··· 1262 return err 1263 }) 1264 1265 return &DB{ 1266 db, 1267 logger,
··· 1262 return err 1263 }) 1264 1265 + runMigration(conn, logger, "rewrite-pulls-table", func(tx *sql.Tx) error { 1266 + _, err := tx.Exec(` 1267 + create table pulls2 ( 1268 + -- identifiers 1269 + id integer primary key autoincrement, 1270 + pull_id integer not null, 1271 + did text not null, 1272 + rkey text not null, 1273 + at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) stored, 1274 + 1275 + -- content 1276 + state integer not null default 0 check (state in (0, 1, 2, 3)), -- closed, open, merged, deleted 1277 + 1278 + unique(did, rkey) 1279 + ); 1280 + 1281 + create table pull_rounds ( 1282 + -- identifiers 1283 + id integer primary key autoincrement, 1284 + did text not null, 1285 + rkey text not null, 1286 + cid text not null, 1287 + 1288 + target_repo_at text not null, 1289 + target_branch text not null, 1290 + source_repo_at text, 1291 + source_branch text, 1292 + patch text not null, 1293 + title text not null, 1294 + body text not null, 1295 + 1296 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1297 + 1298 + unique(target_repo_at, pull_id, cid), 1299 + unique(did, rkey, cid), 1300 + 1301 + foreign key (did, rkey) 1302 + referneces pulls2(did, rkey) 1303 + on delete cascade 1304 + ); 1305 + 1306 + create view pull_rounds_view as 1307 + select 1308 + pr.id, 1309 + pr.cid, 1310 + 1311 + p.pull_id, 1312 + p.did, 1313 + p.rkey, 1314 + p.at_uri, 1315 + p.state, 1316 + 1317 + row_number() over ( 1318 + partition by p.did, p.rkey 1319 + order by pr.id 1320 + ) -1 as round, 1321 + 1322 + pr.target_repo_at, 1323 + pr.target_branch, 1324 + pr.source_repo_at, 1325 + pr.source_branch, 1326 + pr.patch, 1327 + pr.title, 1328 + pr.body, 1329 + pr.created 1330 + from pull_rounds pr 1331 + join pulls p 1332 + on p.did = pr.did 1333 + and p.rkey = pr.rkey; 1334 + 1335 + create view pull_latests_view as 1336 + select * 1337 + from ( 1338 + select 1339 + *, 1340 + (count(*) over (partition by did, rkey)) - 1 as round, 1341 + row_number() over ( 1342 + partition by did, rkey 1343 + order by id desc 1344 + ) as rn 1345 + from pull_rounds_view 1346 + ) 1347 + where rn = 1; 1348 + 1349 + create table reference_links_new ( 1350 + id integer primary key autoincrement, 1351 + from_at text not null, 1352 + from_cid text, 1353 + to_at text not null, 1354 + unique (from_at, from_cid, to_at) 1355 + ); 1356 + 1357 + insert into reference_links_new 1358 + select * from reference_links; 1359 + 1360 + drop table reference_links; 1361 + 1362 + alter table reference_links_new to reference_links; 1363 + `) 1364 + return err 1365 + }) 1366 + 1367 return &DB{ 1368 db, 1369 logger,
+235
appview/db/pulls2.go
···
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + "strings" 7 + "time" 8 + 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + "tangled.org/core/appview/models" 11 + "tangled.org/core/appview/pagination" 12 + ) 13 + 14 + // NewPullRound creates new PR submission. 15 + // Set pullId to open a new PR. 16 + func NewPullRound(tx *sql.Tx, pullId int, round *models.PullRound) error { 17 + // Create new PR when pullId isn't set 18 + if pullId == 0 { 19 + // ensure sequence exists 20 + _, err := tx.Exec(` 21 + insert or ignore into repo_pull_seqs (repo_at, next_pull_id) 22 + values (?, 1) 23 + `, round.Target.RepoAt) 24 + if err != nil { 25 + return err 26 + } 27 + 28 + err = tx.QueryRow(` 29 + update repo_pull_seqs 30 + set next_pull_id = next_pull_id + 1 31 + where repo_at = ? 32 + returning next_pull_id - 1 33 + `, round.Target.RepoAt).Scan(&pullId) 34 + if err != nil { 35 + return err 36 + } 37 + 38 + _, err = tx.Exec( 39 + `insert into pulls2 (pull_id, did, rkey, state) 40 + values (?, ?, ?, ?)`, 41 + pullId, 42 + round.Did, 43 + round.Rkey, 44 + models.PullOpen, 45 + ) 46 + if err != nil { 47 + return fmt.Errorf("insert pull submission: %w", err) 48 + } 49 + } 50 + 51 + var sourceRepoAt, sourceBranch *string 52 + if round.Source != nil { 53 + x := round.Source.RepoAt.String() 54 + sourceRepoAt = &x 55 + sourceBranch = &round.Source.Branch 56 + } 57 + _, err := tx.Exec( 58 + `insert into pull_rounds ( 59 + pull_did, 60 + pull_rkey, 61 + cid, 62 + target_repo_at, 63 + target_branch, 64 + source_repo_at, 65 + source_branch, 66 + patch, 67 + title, 68 + body, 69 + created 70 + ) 71 + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 72 + round.Did, 73 + round.Rkey, 74 + round.Cid, 75 + round.Target.RepoAt, 76 + round.Target.Branch, 77 + sourceRepoAt, 78 + sourceBranch, 79 + round.Patch, 80 + round.Title, 81 + round.Body, 82 + round.Created.Format(time.RFC3339), 83 + ) 84 + if err != nil { 85 + return fmt.Errorf("insert pull submission: %w", err) 86 + } 87 + 88 + err = tx.QueryRow( 89 + `select id, round from pull_rounds_view 90 + where did = ? and rkey = ? and cid = ?`, 91 + round.Did, round.Rkey, round.Cid, 92 + ).Scan(&round.Id, &round.Round) 93 + if err != nil { 94 + return fmt.Errorf("get id and round number: %w", err) 95 + } 96 + 97 + if err := putReferences(tx, round.AtUri(), &round.Cid, round.References); err != nil { 98 + return fmt.Errorf("put reference_links: %w", err) 99 + } 100 + 101 + return nil 102 + } 103 + 104 + func SetPullState2(e Execer, pullAt syntax.ATURI, state models.PullState) error { 105 + _, err := e.Exec( 106 + `update pulls2 set state = ? where at_uri = ? and (state <> ? or state <> ?)`, 107 + state, 108 + pullAt, 109 + models.PullDeleted, // only update state of non-deleted pulls 110 + models.PullMerged, // only update state of non-merged pulls 111 + ) 112 + return err 113 + } 114 + 115 + func ClosePull2(e Execer, pullAt syntax.ATURI) error { 116 + return SetPullState2(e, pullAt, models.PullClosed) 117 + } 118 + 119 + func ReopenPull2(e Execer, pullAt syntax.ATURI) error { 120 + return SetPullState2(e, pullAt, models.PullOpen) 121 + } 122 + 123 + func MergePull2(e Execer, pullAt syntax.ATURI) error { 124 + return SetPullState2(e, pullAt, models.PullMerged) 125 + } 126 + 127 + func DeletePull2(e Execer, pullAt syntax.ATURI) error { 128 + return SetPullState2(e, pullAt, models.PullDeleted) 129 + } 130 + 131 + func GetPulls2(e Execer, filters ...filter) ([]*models.Pull2, error) { 132 + return GetPullsPaginated(e, pagination.Page{}, filters...) 133 + } 134 + 135 + func GetPullsPaginated(e Execer, page pagination.Page, filters ...filter) ([]*models.Pull2, error) { 136 + pullsMap := make(map[syntax.ATURI]*models.Pull2) 137 + 138 + var conditions []string 139 + var args []any 140 + for _, filter := range filters { 141 + conditions = append(conditions, filter.Condition()) 142 + args = append(args, filter.Arg()...) 143 + } 144 + 145 + whereClause := "" 146 + if conditions != nil { 147 + whereClause = " where " + strings.Join(conditions, " and ") 148 + } 149 + 150 + pLower := FilterGte("row_num", page.Offset+1) 151 + pUpper := FilterLte("row_num", page.Offset+page.Limit) 152 + 153 + pageClause := "" 154 + if page.Limit > 0 { 155 + args = append(args, pLower.Arg()...) 156 + args = append(args, pUpper.Arg()...) 157 + pageClause = " where " + pLower.Condition() + " and " + pUpper.Condition() 158 + } 159 + 160 + query := fmt.Sprintf( 161 + `select * from ( 162 + select 163 + id, 164 + did, 165 + rkey, 166 + cid, 167 + target_repo_at, 168 + target_branch, 169 + source_repo_at, 170 + source_branch, 171 + patch, 172 + title, 173 + body, 174 + created, 175 + row_number() over (order by id desc) as row_num 176 + from 177 + pull_rounds 178 + %s 179 + ) ranked_pull_rounds 180 + %s`, 181 + whereClause, 182 + pageClause, 183 + ) 184 + 185 + rows, err := e.Query(query, args...) 186 + if err != nil { 187 + return nil, fmt.Errorf("query pulls table: %w", err) 188 + } 189 + defer rows.Close() 190 + 191 + for rows.Next() { 192 + var round models.PullRound 193 + var sourceBranch, sourceRepoAt sql.NullString 194 + var created string 195 + err := rows.Scan( 196 + &round.Id, 197 + &round.Did, 198 + &round.Rkey, 199 + &round.Cid, 200 + &round.Target.RepoAt, 201 + &round.Target.Branch, 202 + &sourceBranch, 203 + &sourceRepoAt, 204 + &round.Patch, 205 + &round.Title, 206 + &round.Body, 207 + &created, 208 + ) 209 + if err != nil { 210 + return nil, fmt.Errorf("scan row: %w", err) 211 + } 212 + 213 + if sourceBranch.Valid && sourceRepoAt.Valid { 214 + round.Source = &models.PullSource2{} 215 + round.Source.Branch = sourceBranch.String 216 + round.Source.RepoAt = syntax.ATURI(sourceRepoAt.String) 217 + } 218 + createdAtTime, _ := time.Parse(time.RFC3339, created) 219 + round.Created = createdAtTime 220 + 221 + pull, ok := pullsMap[round.AtUri()] 222 + if !ok { 223 + pull = &models.Pull2{ 224 + Did: round.Did, 225 + Rkey: round.Rkey, 226 + } 227 + } 228 + pull.Submissions = append(pull.Submissions, &round) 229 + pullsMap[round.AtUri()] = pull 230 + } 231 + 232 + // TODO: fetch pulls (id, pull_id, state) from (did, rkey) 233 + 234 + panic("unimplemented") 235 + }
+13 -10
appview/db/reference.go
··· 161 } 162 163 func putReferences(tx *sql.Tx, fromAt syntax.ATURI, fromCid *syntax.CID, references []syntax.ATURI) error { 164 - err := deleteReferences(tx, fromAt) 165 - if err != nil { 166 - return fmt.Errorf("delete old reference_links: %w", err) 167 - } 168 - if len(references) == 0 { 169 - return nil 170 } 171 172 values := make([]string, 0, len(references)) 173 args := make([]any, 0, len(references)*2) 174 for _, ref := range references { 175 - values = append(values, "(?, ?)") 176 - args = append(args, fromAt, ref) 177 } 178 - _, err = tx.Exec( 179 fmt.Sprintf( 180 - `insert into reference_links (from_at, to_at) 181 values %s`, 182 strings.Join(values, ","), 183 ),
··· 161 } 162 163 func putReferences(tx *sql.Tx, fromAt syntax.ATURI, fromCid *syntax.CID, references []syntax.ATURI) error { 164 + // clear existing referneces when cid isn't provided 165 + if fromCid == nil { 166 + err := deleteReferences(tx, fromAt) 167 + if err != nil { 168 + return fmt.Errorf("delete old reference_links: %w", err) 169 + } 170 + if len(references) == 0 { 171 + return nil 172 + } 173 } 174 175 values := make([]string, 0, len(references)) 176 args := make([]any, 0, len(references)*2) 177 for _, ref := range references { 178 + values = append(values, "(?, ?, ?)") 179 + args = append(args, fromAt, fromCid, ref) 180 } 181 + _, err := tx.Exec( 182 fmt.Sprintf( 183 + `insert into reference_links (from_at, from_cid, to_at) 184 values %s`, 185 strings.Join(values, ","), 186 ),
+188
appview/models/pull2.go
···
··· 1 + package models 2 + 3 + import ( 4 + "fmt" 5 + "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "tangled.org/core/api/tangled" 9 + ) 10 + 11 + type Pull2 struct { 12 + // TODO: PullId is not atprotated yet 13 + PullId int 14 + Did syntax.DID 15 + Rkey syntax.RecordKey 16 + 17 + Submissions []*PullRound // PR submission history 18 + 19 + // backlinked records 20 + 21 + State PullState 22 + Labels LabelState 23 + Comments []Comment 24 + 25 + // optionally, populate these when querying for reverse mappings 26 + 27 + Repo *Repo 28 + } 29 + 30 + // PullRound represents snapshot of `sh.tangled.repo.pull` record 31 + // NOTE: Non-patch change can make new submission 32 + type PullRound struct { 33 + Id int64 34 + Round int 35 + Did syntax.DID 36 + Rkey syntax.RecordKey 37 + Cid syntax.CID 38 + 39 + // content 40 + 41 + Target PullTarget 42 + Source *PullSource2 // optional for patch based PR 43 + Patch string // list of commits encoded in patch object 44 + Title string 45 + Body string 46 + Mentions []syntax.DID 47 + References []syntax.ATURI 48 + Created time.Time 49 + } 50 + 51 + type PullTarget struct { 52 + RepoAt syntax.ATURI 53 + Branch string 54 + } 55 + 56 + // PullSource2 is not a source of truth but rather a metadata to help resubmitting 57 + type PullSource2 struct { 58 + RepoAt syntax.ATURI 59 + Branch string 60 + } 61 + 62 + func (p Pull2) AsRecord() tangled.RepoPull { 63 + return p.Latest().AsRecord() 64 + } 65 + 66 + func (p *Pull2) AtUri() syntax.ATURI { 67 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.Did, tangled.RepoPullNSID, p.Rkey)) 68 + } 69 + 70 + func (p *Pull2) Latest() *PullRound { 71 + return p.Submissions[len(p.Submissions)-1] 72 + } 73 + 74 + func (p *Pull2) CID() syntax.CID { 75 + return p.Latest().Cid 76 + } 77 + 78 + func (p *Pull2) Created() time.Time { 79 + return p.Submissions[0].Created 80 + } 81 + 82 + func (p *Pull2) Edited() time.Time { 83 + return p.Latest().Created 84 + } 85 + 86 + func (p *Pull2) RepoAt() syntax.ATURI { 87 + return p.Latest().Target.RepoAt 88 + } 89 + 90 + func (p *Pull2) Validate() error { 91 + if len(p.Submissions) == 0 { 92 + return fmt.Errorf("Pull should include at least one submission") 93 + } 94 + // we don't need to validate existing records 95 + if err := p.Latest().Validate(); err != nil { 96 + return fmt.Errorf("validate latest stack: %w", err) 97 + } 98 + return nil 99 + } 100 + 101 + func PullContentFromRecord(cid syntax.CID, record tangled.RepoPull) PullRound { 102 + var source *PullSource2 103 + if record.Source != nil { 104 + source = &PullSource2{} 105 + source.Branch = record.Source.Branch 106 + if record.Source.Repo != nil { 107 + source.RepoAt = syntax.ATURI(*record.Source.Repo) 108 + } else { 109 + source.RepoAt = syntax.ATURI(record.Target.Repo) 110 + } 111 + } 112 + var ( 113 + body string 114 + mentions = make([]syntax.DID, len(record.Mentions)) 115 + references = make([]syntax.ATURI, len(record.References)) 116 + ) 117 + if record.Body != nil { 118 + body = *record.Body 119 + } 120 + for i, v := range record.Mentions { 121 + mentions[i] = syntax.DID(v) 122 + } 123 + for i, v := range record.References { 124 + references[i] = syntax.ATURI(v) 125 + } 126 + created, err := time.Parse(record.CreatedAt, time.RFC3339) 127 + if err != nil { 128 + created = time.Now() 129 + } 130 + return PullRound{ 131 + Cid: cid, 132 + Target: PullTarget{ 133 + RepoAt: syntax.ATURI(record.Target.Repo), 134 + Branch: record.Target.Branch, 135 + }, 136 + Source: source, 137 + Patch: record.Patch, 138 + Title: record.Title, 139 + Body: body, 140 + Created: created, 141 + } 142 + } 143 + 144 + // NOTE: AsRecord doesn't include the patch blob id in returned atproto record 145 + func (p PullRound) AsRecord() tangled.RepoPull { 146 + var source *tangled.RepoPull_Source 147 + 148 + if p.Source != nil { 149 + repoAt := p.Source.RepoAt.String() 150 + source = &tangled.RepoPull_Source{ 151 + Repo: &repoAt, 152 + Branch: p.Source.Branch, 153 + } 154 + } 155 + 156 + var body *string 157 + if p.Body != "" { 158 + body = &p.Body 159 + } 160 + return tangled.RepoPull{ 161 + Target: &tangled.RepoPull_Target{ 162 + Repo: p.Target.RepoAt.String(), 163 + Branch: p.Target.Branch, 164 + }, 165 + Source: source, 166 + Title: p.Title, 167 + Body: body, 168 + Mentions: toStringList(p.Mentions), 169 + References: toStringList(p.References), 170 + } 171 + } 172 + 173 + func (p *PullRound) AtUri() syntax.ATURI { 174 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.Did, tangled.RepoPullNSID, p.Rkey)) 175 + } 176 + 177 + func (p *PullRound) Validate() error { 178 + // TODO: validate patches 179 + return nil 180 + } 181 + 182 + func toStringList[T fmt.Stringer](list []T) []string { 183 + slist := make([]string, len(list)) 184 + for i, v := range list { 185 + slist[i] = v.String() 186 + } 187 + return slist 188 + }
+12
appview/service/pull/errors.go
···
··· 1 + package pull 2 + 3 + import "errors" 4 + 5 + var ( 6 + ErrUnAuthenticated = errors.New("user session missing") 7 + ErrForbidden = errors.New("unauthorized operation") 8 + ErrDatabaseFail = errors.New("db op fail") 9 + ErrPDSFail = errors.New("pds op fail") 10 + ErrIndexerFail = errors.New("indexer fail") 11 + ErrValidationFail = errors.New("pull validation fail") 12 + )
+41
appview/service/pull/merge.go
···
··· 1 + package pull 2 + 3 + import ( 4 + "context" 5 + 6 + "tangled.org/core/appview/models" 7 + "tangled.org/core/appview/pages/repoinfo" 8 + "tangled.org/core/appview/session" 9 + ) 10 + 11 + func (s *Service) MergePull(ctx context.Context, pull *models.Pull2) error { 12 + l := s.logger.With("method", "MergePull") 13 + sess := session.FromContext(ctx) 14 + if sess == nil { 15 + l.Error("user session is missing in context") 16 + return ErrForbidden 17 + } 18 + sessDid := sess.Data.AccountDID 19 + l = l.With("did", sessDid) 20 + 21 + // TODO: make this more granular 22 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(sessDid.String(), pull.Repo.Knot, pull.Repo.DidSlashRepo())} 23 + isRepoOwner := roles.IsOwner() 24 + isCollaborator := roles.IsCollaborator() 25 + isAuthor := sessDid == pull.Did 26 + if !(isRepoOwner || isCollaborator || isAuthor) { 27 + l.Error("user is not authorized") 28 + return ErrForbidden 29 + } 30 + 31 + // 1. request knot to apply series of patches 32 + // 2. create new pull.state record 33 + // 3. update db 34 + 35 + panic("unimplemented") 36 + 37 + pull.State = models.PullMerged 38 + 39 + // s.notifier.NewPullState(ctx, sessDid, pull) 40 + return nil 41 + }
+239
appview/service/pull/pull.go
···
··· 1 + package pull 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "time" 7 + 8 + "github.com/bluesky-social/indigo/api/atproto" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + lexutil "github.com/bluesky-social/indigo/lex/util" 11 + "tangled.org/core/api/tangled" 12 + "tangled.org/core/appview/config" 13 + "tangled.org/core/appview/db" 14 + pulls_indexer "tangled.org/core/appview/indexer/pulls" 15 + "tangled.org/core/appview/mentions" 16 + "tangled.org/core/appview/models" 17 + "tangled.org/core/appview/notify" 18 + "tangled.org/core/appview/session" 19 + "tangled.org/core/idresolver" 20 + "tangled.org/core/rbac" 21 + "tangled.org/core/tid" 22 + ) 23 + 24 + type Service struct { 25 + config *config.Config 26 + db *db.DB 27 + enforcer *rbac.Enforcer 28 + indexer *pulls_indexer.Indexer 29 + logger *slog.Logger 30 + notifier notify.Notifier 31 + idResolver *idresolver.Resolver 32 + mentionsResolver *mentions.Resolver 33 + } 34 + 35 + func NewService( 36 + logger *slog.Logger, 37 + config *config.Config, 38 + db *db.DB, 39 + enforcer *rbac.Enforcer, 40 + notifier notify.Notifier, 41 + idResolver *idresolver.Resolver, 42 + mentionsResolver *mentions.Resolver, 43 + indexer *pulls_indexer.Indexer, 44 + ) Service { 45 + return Service{ 46 + config, 47 + db, 48 + enforcer, 49 + indexer, 50 + logger, 51 + notifier, 52 + idResolver, 53 + mentionsResolver, 54 + } 55 + } 56 + 57 + // SubmitPull creates a new PR or resubmits existing PR. 58 + // `pull` can be `nil` for creating a new PR. 59 + func (s *Service) SubmitPull( 60 + ctx context.Context, 61 + pull *models.Pull2, 62 + target models.PullTarget, 63 + source *models.PullSource2, 64 + patch, title, body string, 65 + ) error { 66 + l := s.logger.With("method", "NewPullSubmission") 67 + sess := session.FromContext(ctx) 68 + if sess == nil { 69 + l.Error("user session is missing in context") 70 + return ErrForbidden 71 + } 72 + sessDid := sess.Data.AccountDID 73 + l = l.With("did", sessDid) 74 + 75 + var ( 76 + did syntax.DID 77 + rkey syntax.RecordKey 78 + ) 79 + if pull == nil { 80 + // new pr 81 + did = sessDid 82 + rkey = syntax.RecordKey(tid.TID()) 83 + } else { 84 + // resubmit 85 + if sessDid != pull.Did { 86 + l.Error("only author can edit the pull") 87 + return ErrForbidden 88 + } 89 + did = pull.Did 90 + rkey = pull.Rkey 91 + } 92 + 93 + mentions, references := s.mentionsResolver.Resolve(ctx, body) 94 + 95 + round := models.PullRound{ 96 + Did: did, 97 + Rkey: rkey, 98 + Target: target, 99 + Source: source, 100 + Patch: patch, 101 + Title: title, 102 + Body: body, 103 + Mentions: mentions, 104 + References: references, 105 + Created: time.Now(), 106 + } 107 + if err := round.Validate(); err != nil { 108 + l.Error("validation error", "err", err) 109 + return ErrValidationFail 110 + } 111 + 112 + atpclient := sess.APIClient() 113 + record := round.AsRecord() 114 + 115 + var exCid *string 116 + if pull != nil { 117 + x := pull.CID().String() 118 + exCid = &x 119 + } 120 + resp, err := atproto.RepoPutRecord(ctx, atpclient, &atproto.RepoPutRecord_Input{ 121 + Collection: tangled.RepoPullNSID, 122 + SwapRecord: exCid, 123 + Record: &lexutil.LexiconTypeDecoder{ 124 + Val: &record, 125 + }, 126 + }) 127 + if err != nil { 128 + l.Error("atproto.RepoPutRecord failed", "err", err) 129 + return ErrPDSFail 130 + } 131 + round.Cid = syntax.CID(resp.Cid) 132 + 133 + tx, err := s.db.BeginTx(ctx, nil) 134 + if err != nil { 135 + l.Error("db.BeginTx failed", "err", err) 136 + return ErrDatabaseFail 137 + } 138 + defer tx.Rollback() 139 + 140 + if err := db.NewPullRound(tx, 0, &round); err != nil { 141 + l.Error("db.UpdatePull2 failed", "err", err) 142 + return ErrDatabaseFail 143 + } 144 + 145 + if err = tx.Commit(); err != nil { 146 + l.Error("tx.Commit failed", "err", err) 147 + return ErrDatabaseFail 148 + } 149 + 150 + if pull == nil { 151 + // s.notifier.NewPull(ctx, &round) 152 + } else { 153 + pull.Submissions = append(pull.Submissions, &round) 154 + // s.notifier.ResubmitPull(ctx, &round) 155 + } 156 + 157 + return nil 158 + } 159 + 160 + func (s *Service) DeletePull(ctx context.Context, pull *models.Pull2) error { 161 + l := s.logger.With("method", "DeletePull") 162 + sess := session.FromContext(ctx) 163 + if sess == nil { 164 + l.Error("user session is missing in context") 165 + return ErrForbidden 166 + } 167 + sessDid := sess.Data.AccountDID 168 + l = l.With("did", sessDid) 169 + 170 + if sessDid != pull.Did { 171 + l.Error("only author can delete the pull") 172 + return ErrForbidden 173 + } 174 + 175 + tx, err := s.db.BeginTx(ctx, nil) 176 + if err != nil { 177 + l.Error("db.BeginTx failed", "err", err) 178 + return ErrDatabaseFail 179 + } 180 + defer tx.Rollback() 181 + 182 + if err := db.DeletePull2(tx, pull.AtUri()); err != nil { 183 + l.Error("db.DeletePull2 failed", "err", err) 184 + return ErrDatabaseFail 185 + } 186 + 187 + atpclient := sess.APIClient() 188 + _, err = atproto.RepoDeleteRecord(ctx, atpclient, &atproto.RepoDeleteRecord_Input{ 189 + Collection: tangled.RepoIssueNSID, 190 + Repo: pull.Did.String(), 191 + Rkey: pull.Rkey.String(), 192 + }) 193 + if err != nil { 194 + l.Error("atproto.RepoDeleteRecord failed", "err", err) 195 + return ErrPDSFail 196 + } 197 + 198 + if err := tx.Commit(); err != nil { 199 + l.Error("tx.Commit failed", "err", err) 200 + return ErrDatabaseFail 201 + } 202 + 203 + pull.State = models.PullDeleted 204 + 205 + // s.notifier.DeletePull(ctx, pull) 206 + return nil 207 + } 208 + 209 + func (s *Service) ListPulls(ctx context.Context, repo *models.Repo, searchOpts models.PullSearchOptions) ([]*models.Pull2, error) { 210 + l := s.logger.With("method", "ListPulls") 211 + 212 + var pulls []*models.Pull2 213 + var err error 214 + if searchOpts.Keyword != "" { 215 + res, err := s.indexer.Search(ctx, searchOpts) 216 + if err != nil { 217 + l.Error("failed to search for pulls", "err", err) 218 + return nil, ErrIndexerFail 219 + } 220 + l.Debug("searched pulls with indexer", "count", len(res.Hits)) 221 + pulls, err = db.GetPulls2(s.db, db.FilterIn("id", res.Hits)) 222 + if err != nil { 223 + l.Error("failed to get pulls", "err", err) 224 + return nil, ErrDatabaseFail 225 + } 226 + } else { 227 + pulls, err = db.GetPullsPaginated( 228 + s.db, 229 + searchOpts.Page, 230 + db.FilterEq("repo_at", repo.RepoAt()), 231 + db.FilterEq("state", searchOpts.State), 232 + ) 233 + if err != nil { 234 + l.Error("failed to get pulls", "err", err) 235 + return nil, ErrDatabaseFail 236 + } 237 + } 238 + return pulls, nil 239 + }
+47
appview/service/pull/state.go
···
··· 1 + package pull 2 + 3 + import ( 4 + "context" 5 + 6 + "tangled.org/core/appview/db" 7 + "tangled.org/core/appview/models" 8 + "tangled.org/core/appview/pages/repoinfo" 9 + "tangled.org/core/appview/session" 10 + ) 11 + 12 + func (s *Service) ClosePull(ctx context.Context, pull *models.Pull2) error { 13 + l := s.logger.With("method", "CloseIssue") 14 + sess := session.FromContext(ctx) 15 + if sess == nil { 16 + l.Error("user session is missing in context") 17 + return ErrUnAuthenticated 18 + } 19 + sessDid := sess.Data.AccountDID 20 + l = l.With("did", sessDid) 21 + 22 + // TODO: make this more granular 23 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(sessDid.String(), pull.Repo.Knot, pull.Repo.DidSlashRepo())} 24 + isRepoOwner := roles.IsOwner() 25 + isCollaborator := roles.IsCollaborator() 26 + isAuthor := sessDid == pull.Did 27 + if !(isRepoOwner || isCollaborator || isAuthor) { 28 + l.Error("user is not authorized") 29 + return ErrForbidden 30 + } 31 + 32 + err := db.ClosePull2(s.db, pull.AtUri()) 33 + if err != nil { 34 + l.Error("db.ClosePull2 failed", "err", err) 35 + return ErrDatabaseFail 36 + } 37 + 38 + // change the issue state (this will pass down to the notifiers) 39 + pull.State = models.PullClosed 40 + 41 + panic("unimplemented") 42 + // s.notifier.NewPullState(ctx, sessDid, pull) 43 + } 44 + 45 + func (s *Service) ReopenPull(ctx context.Context, pull *models.Pull2) error { 46 + panic("unimplemented") 47 + }