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