jolheiser
·
2025-08-21
pr.go
1package git
2
3import (
4 "database/sql"
5 "errors"
6 "fmt"
7 "io"
8 "strings"
9 "time"
10
11 "github.com/jmoiron/sqlx"
12)
13
14var ErrPatchExists = errors.New("patch already exists for patch request")
15
16type PatchsetOp int
17
18const (
19 OpNormal PatchsetOp = iota
20 OpReview
21 OpAccept
22 OpClose
23)
24
25type GitPatchRequest interface {
26 GetUsers() ([]*User, error)
27 GetUserByID(userID int64) (*User, error)
28 GetUserByName(name string) (*User, error)
29 GetUserByPubkey(pubkey string) (*User, error)
30 GetRepos() ([]*Repo, error)
31 GetRepoByID(repoID int64) (*Repo, error)
32 GetRepoByName(user *User, repoName string) (*Repo, error)
33 CreateRepo(user *User, repoName string) (*Repo, error)
34 UpsertUser(pubkey, name string) (*User, error)
35 IsBanned(pubkey, ipAddress string) error
36 SubmitPatchRequest(repoID int64, userID int64, patchset io.Reader) (*PatchRequest, error)
37 SubmitPatchset(prID, userID int64, op PatchsetOp, patchset io.Reader) ([]*Patch, error)
38 GetPatchRequestByID(prID int64) (*PatchRequest, error)
39 GetPatchRequests() ([]*PatchRequest, error)
40 GetPatchRequestsByRepoID(repoID int64) ([]*PatchRequest, error)
41 GetPatchRequestsByPubkey(pubkey string) ([]*PatchRequest, error)
42 GetPatchsetsByPrID(prID int64) ([]*Patchset, error)
43 GetPatchsetByID(patchsetID int64) (*Patchset, error)
44 GetLatestPatchsetByPrID(prID int64) (*Patchset, error)
45 GetPatchesByPatchsetID(prID int64) ([]*Patch, error)
46 UpdatePatchRequestStatus(prID, userID int64, status Status, comment string) error
47 UpdatePatchRequestName(prID, userID int64, name string) error
48 DeletePatchsetByID(userID, prID int64, patchsetID int64) error
49 CreateEventLog(tx *sqlx.Tx, eventLog EventLog) error
50 GetEventLogs() ([]*EventLog, error)
51 GetEventLogsByRepoName(user *User, repoName string) ([]*EventLog, error)
52 GetEventLogsByPrID(prID int64) ([]*EventLog, error)
53 GetEventLogsByUserID(userID int64) ([]*EventLog, error)
54 DiffPatchsets(aset *Patchset, bset *Patchset) ([]*RangeDiffOutput, error)
55}
56
57type PrCmd struct {
58 Backend *Backend
59}
60
61var (
62 _ GitPatchRequest = PrCmd{}
63 _ GitPatchRequest = (*PrCmd)(nil)
64)
65
66func (pr PrCmd) IsBanned(pubkey, ipAddress string) error {
67 acl := []*Acl{}
68 err := pr.Backend.DB.Select(
69 &acl,
70 "SELECT * FROM acl WHERE permission='banned' AND (pubkey=? OR ip_address=?)",
71 pubkey,
72 ipAddress,
73 )
74 if len(acl) > 0 {
75 return fmt.Errorf("user has been banned")
76 }
77 return err
78}
79
80func (pr PrCmd) GetUsers() ([]*User, error) {
81 users := []*User{}
82 err := pr.Backend.DB.Select(&users, "SELECT * FROM app_users")
83 return users, err
84}
85
86func (pr PrCmd) GetUserByName(name string) (*User, error) {
87 var user User
88 err := pr.Backend.DB.Get(&user, "SELECT * FROM app_users WHERE name=?", name)
89 return &user, err
90}
91
92func (pr PrCmd) GetUserByID(id int64) (*User, error) {
93 var user User
94 err := pr.Backend.DB.Get(&user, "SELECT * FROM app_users WHERE id=?", id)
95 return &user, err
96}
97
98func (pr PrCmd) GetUserByPubkey(pubkey string) (*User, error) {
99 var user User
100 err := pr.Backend.DB.Get(&user, "SELECT * FROM app_users WHERE pubkey=?", pubkey)
101 return &user, err
102}
103
104func (pr PrCmd) computeUserName(name string) (string, error) {
105 var user User
106 err := pr.Backend.DB.Get(&user, "SELECT * FROM app_users WHERE name=?", name)
107 if err != nil {
108 return name, nil
109 }
110 // collision, generate random number and append
111 return fmt.Sprintf("%s%s", name, randSeq(4)), nil
112}
113
114func (pr PrCmd) CreateRepo(user *User, repoName string) (*Repo, error) {
115 var repoID int64
116 row := pr.Backend.DB.QueryRow(
117 "INSERT INTO repos (user_id, name) VALUES (?, ?) RETURNING id",
118 user.ID,
119 repoName,
120 )
121 err := row.Scan(&repoID)
122 if err != nil {
123 return nil, err
124 }
125
126 return pr.GetRepoByID(repoID)
127}
128
129func (pr PrCmd) GetRepoByID(repoID int64) (*Repo, error) {
130 var repo Repo
131 err := pr.Backend.DB.Get(&repo, "SELECT * FROM repos WHERE id=?", repoID)
132 return &repo, err
133}
134
135func (pr PrCmd) GetRepos() (repos []*Repo, err error) {
136 err = pr.Backend.DB.Select(
137 &repos,
138 "SELECT * from repos",
139 )
140 if err != nil {
141 return repos, err
142 }
143 if len(repos) == 0 {
144 return repos, fmt.Errorf("no repos found")
145 }
146 return repos, nil
147}
148
149func (pr PrCmd) GetRepoByName(user *User, repoName string) (*Repo, error) {
150 var repo Repo
151 var err error
152
153 if user == nil {
154 err = pr.Backend.DB.Get(&repo, "SELECT * FROM repos WHERE name=?", repoName)
155 } else {
156 err = pr.Backend.DB.Get(&repo, "SELECT * FROM repos WHERE user_id=? AND name=?", user.ID, repoName)
157 }
158
159 if err != nil {
160 return nil, fmt.Errorf("repo not found: %s", repoName)
161 }
162
163 return &repo, nil
164}
165
166func (pr PrCmd) createUser(pubkey, name string) (*User, error) {
167 if pubkey == "" {
168 return nil, fmt.Errorf("must provide pubkey when creating user")
169 }
170 if name == "" {
171 return nil, fmt.Errorf("must provide user name when creating user")
172 }
173
174 userName, err := pr.computeUserName(name)
175 if err != nil {
176 pr.Backend.Logger.Error("could not compute username", "err", err)
177 }
178
179 var userID int64
180 row := pr.Backend.DB.QueryRow(
181 "INSERT INTO app_users (pubkey, name) VALUES (?, ?) RETURNING id",
182 pubkey,
183 userName,
184 )
185 err = row.Scan(&userID)
186 if err != nil {
187 return nil, err
188 }
189 if userID == 0 {
190 return nil, fmt.Errorf("could not create user")
191 }
192
193 user, err := pr.GetUserByID(userID)
194 return user, err
195}
196
197func (pr PrCmd) UpsertUser(pubkey, name string) (*User, error) {
198 sanName := strings.ToLower(name)
199 if pubkey == "" {
200 return nil, fmt.Errorf("must provide pubkey during upsert")
201 }
202 user, err := pr.GetUserByPubkey(pubkey)
203 if err != nil {
204 user, err = pr.createUser(pubkey, sanName)
205 }
206 return user, err
207}
208
209func (pr PrCmd) GetPatchsetsByPrID(prID int64) ([]*Patchset, error) {
210 patchsets := []*Patchset{}
211 err := pr.Backend.DB.Select(
212 &patchsets,
213 "SELECT * FROM patchsets WHERE patch_request_id=? ORDER BY created_at ASC",
214 prID,
215 )
216 if err != nil {
217 return patchsets, err
218 }
219 if len(patchsets) == 0 {
220 return patchsets, fmt.Errorf("no patchsets found for patch request: %d", prID)
221 }
222 return patchsets, nil
223}
224
225func (pr PrCmd) GetPatchsetByID(patchsetID int64) (*Patchset, error) {
226 var patchset Patchset
227 err := pr.Backend.DB.Get(
228 &patchset,
229 "SELECT * FROM patchsets WHERE id=?",
230 patchsetID,
231 )
232 return &patchset, err
233}
234
235func (pr PrCmd) GetLatestPatchsetByPrID(prID int64) (*Patchset, error) {
236 patchsets, err := pr.GetPatchsetsByPrID(prID)
237 if err != nil {
238 return nil, err
239 }
240 if len(patchsets) == 0 {
241 return nil, fmt.Errorf("not patchsets found for patch request: %d", prID)
242 }
243 return patchsets[len(patchsets)-1], nil
244}
245
246func (pr PrCmd) GetPatchesByPatchsetID(patchsetID int64) ([]*Patch, error) {
247 patches := []*Patch{}
248 err := pr.Backend.DB.Select(
249 &patches,
250 "SELECT * FROM patches WHERE patchset_id=? ORDER BY created_at ASC, id ASC",
251 patchsetID,
252 )
253 return patches, err
254}
255
256func (cmd PrCmd) GetPatchRequests() ([]*PatchRequest, error) {
257 prs := []*PatchRequest{}
258 err := cmd.Backend.DB.Select(
259 &prs,
260 "SELECT * FROM patch_requests ORDER BY id DESC",
261 )
262 return prs, err
263}
264
265func (cmd PrCmd) GetPatchRequestsByRepoID(repoID int64) ([]*PatchRequest, error) {
266 prs := []*PatchRequest{}
267 err := cmd.Backend.DB.Select(
268 &prs,
269 "SELECT * FROM patch_requests WHERE repo_id=? ORDER BY id DESC",
270 repoID,
271 )
272 return prs, err
273}
274
275func (cmd PrCmd) GetPatchRequestsByPubkey(pubkey string) ([]*PatchRequest, error) {
276 prs := []*PatchRequest{}
277 err := cmd.Backend.DB.Select(
278 &prs,
279 "SELECT pr.* FROM patch_requests pr, app_users au WHERE pr.user_id=au.id AND au.pubkey=? ORDER BY id DESC",
280 pubkey,
281 )
282 return prs, err
283}
284
285func (cmd PrCmd) GetPatchRequestByID(prID int64) (*PatchRequest, error) {
286 pr := PatchRequest{}
287 err := cmd.Backend.DB.Get(
288 &pr,
289 "SELECT * FROM patch_requests WHERE id=? ORDER BY created_at DESC",
290 prID,
291 )
292 return &pr, err
293}
294
295// Status types: open, closed, accepted, reviewed.
296func (cmd PrCmd) UpdatePatchRequestStatus(prID int64, userID int64, status Status, comment string) error {
297 tx, err := cmd.Backend.DB.Beginx()
298 if err != nil {
299 return err
300 }
301
302 defer func() {
303 _ = tx.Rollback()
304 }()
305
306 _, err = tx.Exec(
307 "UPDATE patch_requests SET status=? WHERE id=?",
308 status,
309 prID,
310 )
311 if err != nil {
312 return err
313 }
314
315 pr, err := cmd.GetPatchRequestByID(prID)
316 if err != nil {
317 return err
318 }
319
320 err = cmd.CreateEventLog(tx, EventLog{
321 UserID: userID,
322 RepoID: sql.NullInt64{Int64: pr.RepoID, Valid: true},
323 PatchRequestID: sql.NullInt64{Int64: prID, Valid: true},
324 Event: "pr_status_changed",
325 Data: EventData{
326 Status: status,
327 Comment: comment,
328 },
329 })
330 if err != nil {
331 return err
332 }
333
334 return tx.Commit()
335}
336
337func (cmd PrCmd) UpdatePatchRequestName(prID int64, userID int64, name string) error {
338 if name == "" {
339 return fmt.Errorf("must provide name or text in order to update patch request")
340 }
341
342 tx, err := cmd.Backend.DB.Beginx()
343 if err != nil {
344 return err
345 }
346
347 defer func() {
348 _ = tx.Rollback()
349 }()
350
351 _, err = tx.Exec(
352 "UPDATE patch_requests SET name=? WHERE id=?",
353 name,
354 prID,
355 )
356 if err != nil {
357 return err
358 }
359
360 pr, err := cmd.GetPatchRequestByID(prID)
361 if err != nil {
362 return err
363 }
364
365 err = cmd.CreateEventLog(tx, EventLog{
366 UserID: userID,
367 RepoID: sql.NullInt64{Int64: pr.RepoID, Valid: true},
368 PatchRequestID: sql.NullInt64{Int64: prID, Valid: true},
369 Event: "pr_name_changed",
370 Data: EventData{
371 Name: name,
372 },
373 })
374 if err != nil {
375 return err
376 }
377
378 return tx.Commit()
379}
380
381func (cmd PrCmd) CreateEventLog(tx *sqlx.Tx, eventLog EventLog) error {
382 if eventLog.RepoID.Valid && eventLog.PatchRequestID.Valid {
383 var pr PatchRequest
384 err := tx.Get(
385 &pr,
386 "SELECT repo_id FROM patch_requests WHERE id=?",
387 eventLog.PatchRequestID,
388 )
389 if err != nil {
390 cmd.Backend.Logger.Error(
391 "could not find pr when creating eventLog",
392 "err", err,
393 )
394 return nil
395 }
396 eventLog.RepoID = sql.NullInt64{Int64: pr.RepoID, Valid: true}
397 }
398
399 _, err := tx.Exec(
400 "INSERT INTO event_logs (user_id, repo_id, patch_request_id, patchset_id, event, data) VALUES (?, ?, ?, ?, ?, ?)",
401 eventLog.UserID,
402 eventLog.RepoID,
403 eventLog.PatchRequestID.Int64,
404 eventLog.PatchsetID.Int64,
405 eventLog.Event,
406 eventLog.Data,
407 )
408 if err != nil {
409 cmd.Backend.Logger.Error(
410 "could not create eventLog",
411 "err", err,
412 )
413 }
414 return err
415}
416
417func (cmd PrCmd) createPatch(tx *sqlx.Tx, patch *Patch) (int64, error) {
418 patchExists := []Patch{}
419 _ = cmd.Backend.DB.Select(&patchExists, "SELECT * FROM patches WHERE patchset_id=? AND content_sha=?", patch.PatchsetID, patch.ContentSha)
420 if len(patchExists) > 0 {
421 return 0, ErrPatchExists
422 }
423
424 var patchID int64
425 row := tx.QueryRow(
426 "INSERT INTO patches (user_id, patchset_id, author_name, author_email, author_date, title, body, body_appendix, commit_sha, content_sha, base_commit_sha, raw_text) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
427 patch.UserID,
428 patch.PatchsetID,
429 patch.AuthorName,
430 patch.AuthorEmail,
431 patch.AuthorDate,
432 patch.Title,
433 patch.Body,
434 patch.BodyAppendix,
435 patch.CommitSha,
436 patch.ContentSha,
437 patch.BaseCommitSha,
438 patch.RawText,
439 )
440 err := row.Scan(&patchID)
441 if err != nil {
442 return 0, err
443 }
444 if patchID == 0 {
445 return 0, fmt.Errorf("could not create patch request")
446 }
447 return patchID, err
448}
449
450func (cmd PrCmd) SubmitPatchRequest(repoID int64, userID int64, patchset io.Reader) (*PatchRequest, error) {
451 tx, err := cmd.Backend.DB.Beginx()
452 if err != nil {
453 return nil, err
454 }
455
456 defer func() {
457 _ = tx.Rollback()
458 }()
459
460 patches, err := ParsePatchset(patchset)
461 if err != nil {
462 return nil, err
463 }
464
465 if len(patches) == 0 {
466 return nil, fmt.Errorf("after parsing patchset we did't find any patches, did you send us an empty patchset?")
467 }
468
469 prName := ""
470 prText := ""
471 if len(patches) > 0 {
472 prName = patches[0].Title
473 prText = patches[0].Body
474 }
475
476 var prID int64
477 row := tx.QueryRow(
478 "INSERT INTO patch_requests (user_id, repo_id, name, text, status, updated_at) VALUES(?, ?, ?, ?, ?, ?) RETURNING id",
479 userID,
480 repoID,
481 prName,
482 prText,
483 "open",
484 time.Now(),
485 )
486 err = row.Scan(&prID)
487 if err != nil {
488 return nil, err
489 }
490 if prID == 0 {
491 return nil, fmt.Errorf("could not create patch request")
492 }
493
494 var patchsetID int64
495 row = tx.QueryRow(
496 "INSERT INTO patchsets (user_id, patch_request_id) VALUES(?, ?) RETURNING id",
497 userID,
498 prID,
499 )
500 err = row.Scan(&patchsetID)
501 if err != nil {
502 return nil, err
503 }
504 if patchsetID == 0 {
505 return nil, fmt.Errorf("could not create patchset")
506 }
507
508 for _, patch := range patches {
509 patch.UserID = userID
510 patch.PatchsetID = patchsetID
511 _, err = cmd.createPatch(tx, patch)
512 if err != nil {
513 return nil, err
514 }
515 }
516
517 err = cmd.CreateEventLog(tx, EventLog{
518 UserID: userID,
519 RepoID: sql.NullInt64{Int64: repoID, Valid: true},
520 PatchRequestID: sql.NullInt64{Int64: prID, Valid: true},
521 PatchsetID: sql.NullInt64{Int64: patchsetID, Valid: true},
522 Event: "pr_created",
523 })
524 if err != nil {
525 return nil, err
526 }
527
528 err = tx.Commit()
529 if err != nil {
530 return nil, err
531 }
532
533 var pr PatchRequest
534 err = cmd.Backend.DB.Get(&pr, "SELECT * FROM patch_requests WHERE id=?", prID)
535 return &pr, err
536}
537
538func (cmd PrCmd) SubmitPatchset(prID int64, userID int64, op PatchsetOp, patchset io.Reader) ([]*Patch, error) {
539 fin := []*Patch{}
540 tx, err := cmd.Backend.DB.Beginx()
541 if err != nil {
542 return fin, err
543 }
544
545 defer func() {
546 _ = tx.Rollback()
547 }()
548
549 patches, err := ParsePatchset(patchset)
550 if err != nil {
551 return fin, err
552 }
553
554 isReview := op == OpReview || op == OpAccept || op == OpClose
555 var patchsetID int64
556 row := tx.QueryRow(
557 "INSERT INTO patchsets (user_id, patch_request_id, review) VALUES(?, ?, ?) RETURNING id",
558 userID,
559 prID,
560 isReview,
561 )
562 err = row.Scan(&patchsetID)
563 if err != nil {
564 return nil, err
565 }
566 if patchsetID == 0 {
567 return nil, fmt.Errorf("could not create patchset")
568 }
569
570 for _, patch := range patches {
571 patch.UserID = userID
572 patch.PatchsetID = patchsetID
573 patchID, err := cmd.createPatch(tx, patch)
574 if err == nil {
575 patch.ID = patchID
576 fin = append(fin, patch)
577 } else {
578 if !errors.Is(ErrPatchExists, err) {
579 return fin, err
580 }
581 }
582 }
583
584 if len(fin) > 0 {
585 event := "pr_patchset_added"
586 if op == OpReview {
587 event = "pr_reviewed"
588 }
589
590 pr, err := cmd.GetPatchRequestByID(prID)
591 if err != nil {
592 return fin, err
593 }
594
595 err = cmd.CreateEventLog(tx, EventLog{
596 UserID: userID,
597 RepoID: sql.NullInt64{Int64: pr.RepoID, Valid: true},
598 PatchRequestID: sql.NullInt64{Int64: prID, Valid: true},
599 PatchsetID: sql.NullInt64{Int64: patchsetID, Valid: true},
600 Event: event,
601 })
602 if err != nil {
603 return fin, err
604 }
605 }
606
607 err = tx.Commit()
608 if err != nil {
609 return fin, err
610 }
611
612 return fin, err
613}
614
615func (cmd PrCmd) DeletePatchsetByID(userID int64, prID int64, patchsetID int64) error {
616 tx, err := cmd.Backend.DB.Beginx()
617 if err != nil {
618 return err
619 }
620
621 defer func() {
622 _ = tx.Rollback()
623 }()
624
625 _, err = tx.Exec(
626 "DELETE FROM patchsets WHERE id=?", patchsetID,
627 )
628 if err != nil {
629 return err
630 }
631
632 pr, err := cmd.GetPatchRequestByID(prID)
633 if err != nil {
634 return err
635 }
636
637 err = cmd.CreateEventLog(tx, EventLog{
638 UserID: userID,
639 RepoID: sql.NullInt64{Int64: pr.RepoID, Valid: true},
640 PatchRequestID: sql.NullInt64{Int64: prID, Valid: true},
641 PatchsetID: sql.NullInt64{Int64: patchsetID, Valid: true},
642 Event: "pr_patchset_deleted",
643 })
644 if err != nil {
645 return err
646 }
647
648 return tx.Commit()
649}
650
651func (cmd PrCmd) GetEventLogs() ([]*EventLog, error) {
652 eventLogs := []*EventLog{}
653 err := cmd.Backend.DB.Select(
654 &eventLogs,
655 "SELECT * FROM event_logs ORDER BY created_at DESC",
656 )
657 return eventLogs, err
658}
659
660func (cmd PrCmd) GetEventLogsByRepoName(user *User, repoName string) ([]*EventLog, error) {
661 repo, err := cmd.GetRepoByName(user, repoName)
662 if err != nil {
663 return nil, err
664 }
665
666 eventLogs := []*EventLog{}
667 err = cmd.Backend.DB.Select(
668 &eventLogs,
669 "SELECT * FROM event_logs WHERE repo_id=? ORDER BY created_at DESC",
670 repo.ID,
671 )
672 return eventLogs, err
673}
674
675func (cmd PrCmd) GetEventLogsByPrID(prID int64) ([]*EventLog, error) {
676 eventLogs := []*EventLog{}
677 err := cmd.Backend.DB.Select(
678 &eventLogs,
679 "SELECT * FROM event_logs WHERE patch_request_id=? ORDER BY created_at DESC",
680 prID,
681 )
682 return eventLogs, err
683}
684
685func (cmd PrCmd) GetEventLogsByUserID(userID int64) ([]*EventLog, error) {
686 eventLogs := []*EventLog{}
687 query := `SELECT * FROM event_logs
688 WHERE user_id=?
689 OR patch_request_id IN (
690 SELECT id FROM patch_requests WHERE user_id=?
691 )
692 ORDER BY created_at DESC`
693 err := cmd.Backend.DB.Select(
694 &eventLogs,
695 query,
696 userID,
697 userID,
698 )
699 return eventLogs, err
700}
701
702func (cmd PrCmd) DiffPatchsets(prev *Patchset, next *Patchset) ([]*RangeDiffOutput, error) {
703 output := []*RangeDiffOutput{}
704 patches, err := cmd.GetPatchesByPatchsetID(next.ID)
705 if err != nil {
706 return output, err
707 }
708
709 for idx, patch := range patches {
710 patchStr := patch.RawText
711 if idx > 0 {
712 patchStr = startOfPatch + patch.RawText
713 }
714 diffFiles, _, err := ParsePatch(patchStr)
715 if err != nil {
716 continue
717 }
718 patch.Files = diffFiles
719 }
720
721 if prev == nil {
722 return output, nil
723 }
724
725 prevPatches, err := cmd.GetPatchesByPatchsetID(prev.ID)
726 if err != nil {
727 return output, fmt.Errorf("cannot get previous patchset patches: %w", err)
728 }
729
730 for idx, patch := range prevPatches {
731 patchStr := patch.RawText
732 if idx > 0 {
733 patchStr = startOfPatch + patch.RawText
734 }
735 diffFiles, _, err := ParsePatch(patchStr)
736 if err != nil {
737 continue
738 }
739 patch.Files = diffFiles
740 }
741
742 return RangeDiff(prevPatches, patches), nil
743}