2018-08-06 04:43:22 +00:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2022-11-27 18:20:29 +00:00
// SPDX-License-Identifier: MIT
2018-08-06 04:43:22 +00:00
2022-06-13 09:37:59 +00:00
package issues
2018-08-06 04:43:22 +00:00
import (
2021-12-10 01:27:50 +00:00
"context"
2020-04-18 13:50:25 +00:00
"fmt"
2023-12-28 09:38:59 +00:00
"slices"
2019-11-14 02:57:36 +00:00
"strings"
2018-08-06 04:43:22 +00:00
2021-09-19 11:49:59 +00:00
"code.gitea.io/gitea/models/db"
2022-06-12 15:51:54 +00:00
git_model "code.gitea.io/gitea/models/git"
2022-03-29 06:29:02 +00:00
"code.gitea.io/gitea/models/organization"
2021-11-28 11:58:28 +00:00
"code.gitea.io/gitea/models/perm"
2022-05-11 10:09:36 +00:00
access_model "code.gitea.io/gitea/models/perm/access"
2021-11-09 19:57:58 +00:00
"code.gitea.io/gitea/models/unit"
2021-11-24 09:49:20 +00:00
user_model "code.gitea.io/gitea/models/user"
2022-06-13 09:37:59 +00:00
"code.gitea.io/gitea/modules/structs"
2019-08-15 14:46:21 +00:00
"code.gitea.io/gitea/modules/timeutil"
2022-07-19 13:20:28 +00:00
"code.gitea.io/gitea/modules/util"
2018-08-06 04:43:22 +00:00
2019-06-23 15:22:43 +00:00
"xorm.io/builder"
2018-08-06 04:43:22 +00:00
)
2022-06-13 09:37:59 +00:00
// ErrReviewNotExist represents a "ReviewNotExist" kind of error.
type ErrReviewNotExist struct {
ID int64
}
// IsErrReviewNotExist checks if an error is a ErrReviewNotExist.
func IsErrReviewNotExist ( err error ) bool {
_ , ok := err . ( ErrReviewNotExist )
return ok
}
func ( err ErrReviewNotExist ) Error ( ) string {
return fmt . Sprintf ( "review does not exist [id: %d]" , err . ID )
}
2022-10-18 05:50:37 +00:00
func ( err ErrReviewNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2022-06-13 09:37:59 +00:00
// ErrNotValidReviewRequest an not allowed review request modify
type ErrNotValidReviewRequest struct {
Reason string
UserID int64
RepoID int64
}
// IsErrNotValidReviewRequest checks if an error is a ErrNotValidReviewRequest.
func IsErrNotValidReviewRequest ( err error ) bool {
_ , ok := err . ( ErrNotValidReviewRequest )
return ok
}
func ( err ErrNotValidReviewRequest ) Error ( ) string {
return fmt . Sprintf ( "%s [user_id: %d, repo_id: %d]" ,
err . Reason ,
err . UserID ,
err . RepoID )
}
2022-10-18 05:50:37 +00:00
func ( err ErrNotValidReviewRequest ) Unwrap ( ) error {
return util . ErrInvalidArgument
}
2024-03-28 15:19:24 +00:00
// ErrReviewRequestOnClosedPR represents an error when an user tries to request a re-review on a closed or merged PR.
type ErrReviewRequestOnClosedPR struct { }
// IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR.
func IsErrReviewRequestOnClosedPR ( err error ) bool {
_ , ok := err . ( ErrReviewRequestOnClosedPR )
return ok
}
func ( err ErrReviewRequestOnClosedPR ) Error ( ) string {
return "cannot request a re-review on a closed or merged PR"
}
func ( err ErrReviewRequestOnClosedPR ) Unwrap ( ) error {
return util . ErrPermissionDenied
}
2018-08-06 04:43:22 +00:00
// ReviewType defines the sort of feedback a review gives
type ReviewType int
// ReviewTypeUnknown unknown review type
const ReviewTypeUnknown ReviewType = - 1
const (
// ReviewTypePending is a review which is not published yet
ReviewTypePending ReviewType = iota
// ReviewTypeApprove approves changes
ReviewTypeApprove
// ReviewTypeComment gives general feedback
ReviewTypeComment
// ReviewTypeReject gives feedback blocking merge
ReviewTypeReject
2020-04-06 16:33:34 +00:00
// ReviewTypeRequest request review from others
ReviewTypeRequest
2018-08-06 04:43:22 +00:00
)
// Icon returns the corresponding icon for the review type
func ( rt ReviewType ) Icon ( ) string {
switch rt {
case ReviewTypeApprove :
2020-04-03 05:12:42 +00:00
return "check"
2018-08-06 04:43:22 +00:00
case ReviewTypeReject :
2020-07-17 15:15:12 +00:00
return "diff"
2020-04-03 05:12:42 +00:00
case ReviewTypeComment :
2018-10-19 13:36:41 +00:00
return "comment"
2020-04-06 16:33:34 +00:00
case ReviewTypeRequest :
2020-07-17 15:15:12 +00:00
return "dot-fill"
2018-08-06 04:43:22 +00:00
default :
return "comment"
}
}
// Review represents collection of code comments giving feedback for a PR
type Review struct {
2020-01-23 17:28:15 +00:00
ID int64 ` xorm:"pk autoincr" `
Type ReviewType
2022-03-29 06:29:02 +00:00
Reviewer * user_model . User ` xorm:"-" `
ReviewerID int64 ` xorm:"index" `
ReviewerTeamID int64 ` xorm:"NOT NULL DEFAULT 0" `
ReviewerTeam * organization . Team ` xorm:"-" `
2020-01-23 17:28:15 +00:00
OriginalAuthor string
OriginalAuthorID int64
Issue * Issue ` xorm:"-" `
IssueID int64 ` xorm:"index" `
Content string ` xorm:"TEXT" `
2019-12-04 01:08:56 +00:00
// Official is a review made by an assigned approver (counts towards approval)
2021-02-11 17:32:25 +00:00
Official bool ` xorm:"NOT NULL DEFAULT false" `
2024-01-19 16:05:02 +00:00
CommitID string ` xorm:"VARCHAR(64)" `
2021-02-11 17:32:25 +00:00
Stale bool ` xorm:"NOT NULL DEFAULT false" `
Dismissed bool ` xorm:"NOT NULL DEFAULT false" `
2018-08-06 04:43:22 +00:00
2019-08-15 14:46:21 +00:00
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
2018-08-06 04:43:22 +00:00
// CodeComments are the initial code comments of the review
CodeComments CodeComments ` xorm:"-" `
2020-01-23 17:28:15 +00:00
Comments [ ] * Comment ` xorm:"-" `
2018-08-06 04:43:22 +00:00
}
2021-09-19 11:49:59 +00:00
func init ( ) {
db . RegisterModel ( new ( Review ) )
}
2022-01-19 23:26:57 +00:00
// LoadCodeComments loads CodeComments
func ( r * Review ) LoadCodeComments ( ctx context . Context ) ( err error ) {
2020-05-02 00:20:51 +00:00
if r . CodeComments != nil {
2023-07-09 11:58:06 +00:00
return err
2020-05-02 00:20:51 +00:00
}
2024-05-21 15:23:22 +00:00
if err = r . LoadIssue ( ctx ) ; err != nil {
2023-07-09 11:58:06 +00:00
return err
2019-11-15 12:59:21 +00:00
}
2023-06-21 16:08:12 +00:00
r . CodeComments , err = fetchCodeCommentsByReview ( ctx , r . Issue , nil , r , false )
2022-06-20 10:02:49 +00:00
return err
2018-08-06 04:43:22 +00:00
}
2024-05-21 15:23:22 +00:00
func ( r * Review ) LoadIssue ( ctx context . Context ) ( err error ) {
2020-05-02 00:20:51 +00:00
if r . Issue != nil {
2023-07-09 11:58:06 +00:00
return err
2020-05-02 00:20:51 +00:00
}
2022-06-13 09:37:59 +00:00
r . Issue , err = GetIssueByID ( ctx , r . IssueID )
2022-06-20 10:02:49 +00:00
return err
2018-08-06 04:43:22 +00:00
}
2022-11-19 08:12:33 +00:00
// LoadReviewer loads reviewer
func ( r * Review ) LoadReviewer ( ctx context . Context ) ( err error ) {
2020-10-12 19:55:13 +00:00
if r . ReviewerID == 0 || r . Reviewer != nil {
2023-07-09 11:58:06 +00:00
return err
2018-08-06 04:43:22 +00:00
}
Implement actions (#21937)
Close #13539.
Co-authored by: @lunny @appleboy @fuxiaohei and others.
Related projects:
- https://gitea.com/gitea/actions-proto-def
- https://gitea.com/gitea/actions-proto-go
- https://gitea.com/gitea/act
- https://gitea.com/gitea/act_runner
### Summary
The target of this PR is to bring a basic implementation of "Actions",
an internal CI/CD system of Gitea. That means even though it has been
merged, the state of the feature is **EXPERIMENTAL**, and please note
that:
- It is disabled by default;
- It shouldn't be used in a production environment currently;
- It shouldn't be used in a public Gitea instance currently;
- Breaking changes may be made before it's stable.
**Please comment on #13539 if you have any different product design
ideas**, all decisions reached there will be adopted here. But in this
PR, we don't talk about **naming, feature-creep or alternatives**.
### ⚠️ Breaking
`gitea-actions` will become a reserved user name. If a user with the
name already exists in the database, it is recommended to rename it.
### Some important reviews
- What is `DEFAULT_ACTIONS_URL` in `app.ini` for?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1055954954
- Why the api for runners is not under the normal `/api/v1` prefix?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061173592
- Why DBFS?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061301178
- Why ignore events triggered by `gitea-actions` bot?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1063254103
- Why there's no permission control for actions?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1090229868
### What it looks like
<details>
#### Manage runners
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205870657-c72f590e-2e08-4cd4-be7f-2e0abb299bbf.png">
#### List runs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872794-50fde990-2b45-48c1-a178-908e4ec5b627.png">
#### View logs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872501-9b7b9000-9542-4991-8f55-18ccdada77c3.png">
</details>
### How to try it
<details>
#### 1. Start Gitea
Clone this branch and [install from
source](https://docs.gitea.io/en-us/install-from-source).
Add additional configurations in `app.ini` to enable Actions:
```ini
[actions]
ENABLED = true
```
Start it.
If all is well, you'll see the management page of runners:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205877365-8e30a780-9b10-4154-b3e8-ee6c3cb35a59.png">
#### 2. Start runner
Clone the [act_runner](https://gitea.com/gitea/act_runner), and follow
the
[README](https://gitea.com/gitea/act_runner/src/branch/main/README.md)
to start it.
If all is well, you'll see a new runner has been added:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205878000-216f5937-e696-470d-b66c-8473987d91c3.png">
#### 3. Enable actions for a repo
Create a new repo or open an existing one, check the `Actions` checkbox
in settings and submit.
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879705-53e09208-73c0-4b3e-a123-2dcf9aba4b9c.png">
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879383-23f3d08f-1a85-41dd-a8b3-54e2ee6453e8.png">
If all is well, you'll see a new tab "Actions":
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205881648-a8072d8c-5803-4d76-b8a8-9b2fb49516c1.png">
#### 4. Upload workflow files
Upload some workflow files to `.gitea/workflows/xxx.yaml`, you can
follow the [quickstart](https://docs.github.com/en/actions/quickstart)
of GitHub Actions. Yes, Gitea Actions is compatible with GitHub Actions
in most cases, you can use the same demo:
```yaml
name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
```
If all is well, you'll see a new run in `Actions` tab:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884473-79a874bc-171b-4aaf-acd5-0241a45c3b53.png">
#### 5. Check the logs of jobs
Click a run and you'll see the logs:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884800-994b0374-67f7-48ff-be9a-4c53f3141547.png">
#### 6. Go on
You can try more examples in [the
documents](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
of GitHub Actions, then you might find a lot of bugs.
Come on, PRs are welcome.
</details>
See also: [Feature Preview: Gitea
Actions](https://blog.gitea.io/2022/12/feature-preview-gitea-actions/)
---------
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2023-01-31 01:45:19 +00:00
r . Reviewer , err = user_model . GetPossibleUserByID ( ctx , r . ReviewerID )
2024-02-14 16:31:51 +00:00
if err != nil {
if ! user_model . IsErrUserNotExist ( err ) {
return fmt . Errorf ( "GetPossibleUserByID [%d]: %w" , r . ReviewerID , err )
}
r . ReviewerID = user_model . GhostUserID
r . Reviewer = user_model . NewGhostUser ( )
return nil
}
2022-06-20 10:02:49 +00:00
return err
2018-08-06 04:43:22 +00:00
}
2022-11-19 08:12:33 +00:00
// LoadReviewerTeam loads reviewer team
func ( r * Review ) LoadReviewerTeam ( ctx context . Context ) ( err error ) {
2020-10-12 19:55:13 +00:00
if r . ReviewerTeamID == 0 || r . ReviewerTeam != nil {
2023-07-09 11:58:06 +00:00
return nil
2020-10-12 19:55:13 +00:00
}
2022-05-20 14:08:52 +00:00
r . ReviewerTeam , err = organization . GetTeamByID ( ctx , r . ReviewerTeamID )
2022-06-20 10:02:49 +00:00
return err
2020-10-12 19:55:13 +00:00
}
2022-01-19 23:26:57 +00:00
// LoadAttributes loads all attributes except CodeComments
func ( r * Review ) LoadAttributes ( ctx context . Context ) ( err error ) {
2024-05-21 15:23:22 +00:00
if err = r . LoadIssue ( ctx ) ; err != nil {
2023-07-09 11:58:06 +00:00
return err
2018-08-06 04:43:22 +00:00
}
2022-01-19 23:26:57 +00:00
if err = r . LoadCodeComments ( ctx ) ; err != nil {
2023-07-09 11:58:06 +00:00
return err
2020-05-02 00:20:51 +00:00
}
2022-11-19 08:12:33 +00:00
if err = r . LoadReviewer ( ctx ) ; err != nil {
2023-07-09 11:58:06 +00:00
return err
2018-08-06 04:43:22 +00:00
}
2022-11-19 08:12:33 +00:00
if err = r . LoadReviewerTeam ( ctx ) ; err != nil {
2023-07-09 11:58:06 +00:00
return err
2020-10-12 19:55:13 +00:00
}
2022-06-20 10:02:49 +00:00
return err
2018-08-06 04:43:22 +00:00
}
2024-09-06 06:40:02 +00:00
// HTMLTypeColorName returns the color used in the ui indicating the review
2023-05-10 09:16:44 +00:00
func ( r * Review ) HTMLTypeColorName ( ) string {
switch r . Type {
case ReviewTypeApprove :
2024-09-06 06:40:02 +00:00
if ! r . Official {
return "grey"
}
2023-08-03 21:28:21 +00:00
if r . Stale {
return "yellow"
}
2023-05-10 09:16:44 +00:00
return "green"
case ReviewTypeComment :
return "grey"
case ReviewTypeReject :
return "red"
case ReviewTypeRequest :
return "yellow"
}
return "grey"
}
2024-09-06 06:40:02 +00:00
// TooltipContent returns the locale string describing the review type
func ( r * Review ) TooltipContent ( ) string {
switch r . Type {
case ReviewTypeApprove :
if r . Stale {
return "repo.issues.review.stale"
}
if ! r . Official {
return "repo.issues.review.unofficial"
}
return "repo.issues.review.official"
case ReviewTypeComment :
2024-09-18 19:46:41 +00:00
return "repo.issues.review.commented"
2024-09-06 06:40:02 +00:00
case ReviewTypeReject :
return "repo.issues.review.rejected"
case ReviewTypeRequest :
return "repo.issues.review.requested"
}
return ""
}
2022-05-20 14:08:52 +00:00
// GetReviewByID returns the review by the given ID
func GetReviewByID ( ctx context . Context , id int64 ) ( * Review , error ) {
2018-08-06 04:43:22 +00:00
review := new ( Review )
2022-05-20 14:08:52 +00:00
if has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( review ) ; err != nil {
2018-08-06 04:43:22 +00:00
return nil , err
} else if ! has {
return nil , ErrReviewNotExist { ID : id }
}
2023-10-24 02:54:59 +00:00
return review , nil
2018-08-06 04:43:22 +00:00
}
// CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
type CreateReviewOptions struct {
2020-10-12 19:55:13 +00:00
Content string
Type ReviewType
Issue * Issue
2021-11-24 09:49:20 +00:00
Reviewer * user_model . User
2022-03-29 06:29:02 +00:00
ReviewerTeam * organization . Team
2020-10-12 19:55:13 +00:00
Official bool
CommitID string
Stale bool
2019-12-04 01:08:56 +00:00
}
2020-10-12 19:55:13 +00:00
// IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals)
2023-09-28 23:24:36 +00:00
func IsOfficialReviewer ( ctx context . Context , issue * Issue , reviewer * user_model . User ) ( bool , error ) {
2024-03-21 13:13:08 +00:00
if err := issue . LoadPullRequest ( ctx ) ; err != nil {
2019-12-04 01:08:56 +00:00
return false , err
}
2023-01-16 08:00:22 +00:00
2024-03-21 13:13:08 +00:00
pr := issue . PullRequest
2023-01-16 08:00:22 +00:00
rule , err := git_model . GetFirstMatchProtectedBranchRule ( ctx , pr . BaseRepoID , pr . BaseBranch )
if err != nil {
2019-12-04 01:08:56 +00:00
return false , err
}
2023-01-16 08:00:22 +00:00
if rule == nil {
2023-09-28 23:24:36 +00:00
// if no rule is found, then user with write access can make official reviews
err := pr . LoadBaseRepo ( ctx )
if err != nil {
return false , err
}
writeAccess , err := access_model . HasAccessUnit ( ctx , reviewer , pr . BaseRepo , unit . TypeCode , perm . AccessModeWrite )
if err != nil {
return false , err
}
return writeAccess , nil
2019-12-04 01:08:56 +00:00
}
2023-09-28 23:24:36 +00:00
official , err := git_model . IsUserOfficialReviewer ( ctx , rule , reviewer )
if official || err != nil {
return official , err
2020-10-12 19:55:13 +00:00
}
return false , nil
}
// IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals)
2022-05-20 14:08:52 +00:00
func IsOfficialReviewerTeam ( ctx context . Context , issue * Issue , team * organization . Team ) ( bool , error ) {
2024-03-21 13:13:08 +00:00
if err := issue . LoadPullRequest ( ctx ) ; err != nil {
2020-10-12 19:55:13 +00:00
return false , err
}
2024-03-21 13:13:08 +00:00
pb , err := git_model . GetFirstMatchProtectedBranchRule ( ctx , issue . PullRequest . BaseRepoID , issue . PullRequest . BaseBranch )
2023-01-16 08:00:22 +00:00
if err != nil {
2020-10-12 19:55:13 +00:00
return false , err
}
2023-01-16 08:00:22 +00:00
if pb == nil {
2020-10-12 19:55:13 +00:00
return false , nil
}
2023-01-16 08:00:22 +00:00
if ! pb . EnableApprovalsWhitelist {
2022-12-10 02:46:31 +00:00
return team . UnitAccessMode ( ctx , unit . TypeCode ) >= perm . AccessModeWrite , nil
2020-10-12 19:55:13 +00:00
}
2023-12-28 09:38:59 +00:00
return slices . Contains ( pb . ApprovalsWhitelistTeamIDs , team . ID ) , nil
2018-08-06 04:43:22 +00:00
}
2022-05-20 14:08:52 +00:00
// CreateReview creates a new review based on opts
func CreateReview ( ctx context . Context , opts CreateReviewOptions ) ( * Review , error ) {
2024-02-19 13:42:18 +00:00
ctx , committer , err := db . TxContext ( ctx )
if err != nil {
return nil , err
}
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2018-08-06 04:43:22 +00:00
review := & Review {
2020-10-12 19:55:13 +00:00
Issue : opts . Issue ,
IssueID : opts . Issue . ID ,
Reviewer : opts . Reviewer ,
ReviewerTeam : opts . ReviewerTeam ,
Content : opts . Content ,
Official : opts . Official ,
CommitID : opts . CommitID ,
Stale : opts . Stale ,
}
2024-02-19 13:42:18 +00:00
2020-10-12 19:55:13 +00:00
if opts . Reviewer != nil {
2024-02-19 13:42:18 +00:00
review . Type = opts . Type
2020-10-12 19:55:13 +00:00
review . ReviewerID = opts . Reviewer . ID
2024-02-19 13:42:18 +00:00
reviewCond := builder . Eq { "reviewer_id" : opts . Reviewer . ID , "issue_id" : opts . Issue . ID }
// make sure user review requests are cleared
if opts . Type != ReviewTypePending {
if _ , err := sess . Where ( reviewCond . And ( builder . Eq { "type" : ReviewTypeRequest } ) ) . Delete ( new ( Review ) ) ; err != nil {
return nil , err
}
2020-10-12 19:55:13 +00:00
}
2024-02-19 13:42:18 +00:00
// make sure if the created review gets dismissed no old review surface
// other types can be ignored, as they don't affect branch protection
if opts . Type == ReviewTypeApprove || opts . Type == ReviewTypeReject {
if _ , err := sess . Where ( reviewCond . And ( builder . In ( "type" , ReviewTypeApprove , ReviewTypeReject ) ) ) .
Cols ( "dismissed" ) . Update ( & Review { Dismissed : true } ) ; err != nil {
return nil , err
}
}
} else if opts . ReviewerTeam != nil {
review . Type = ReviewTypeRequest
2020-10-12 19:55:13 +00:00
review . ReviewerTeamID = opts . ReviewerTeam . ID
2024-02-19 13:42:18 +00:00
} else {
return nil , fmt . Errorf ( "provide either reviewer or reviewer team" )
}
if _ , err := sess . Insert ( review ) ; err != nil {
return nil , err
2018-08-06 04:43:22 +00:00
}
2024-02-19 13:42:18 +00:00
return review , committer . Commit ( )
2018-08-06 04:43:22 +00:00
}
2022-05-20 14:08:52 +00:00
// GetCurrentReview returns the current pending review of reviewer for given issue
func GetCurrentReview ( ctx context . Context , reviewer * user_model . User , issue * Issue ) ( * Review , error ) {
2018-08-06 04:43:22 +00:00
if reviewer == nil {
return nil , nil
}
2022-05-20 14:08:52 +00:00
reviews , err := FindReviews ( ctx , FindReviewOptions {
2018-08-06 04:43:22 +00:00
Type : ReviewTypePending ,
IssueID : issue . ID ,
ReviewerID : reviewer . ID ,
} )
if err != nil {
return nil , err
}
if len ( reviews ) == 0 {
return nil , ErrReviewNotExist { }
}
2018-10-18 11:23:05 +00:00
reviews [ 0 ] . Reviewer = reviewer
reviews [ 0 ] . Issue = issue
2018-08-06 04:43:22 +00:00
return reviews [ 0 ] , nil
}
2019-11-24 05:46:16 +00:00
// ReviewExists returns whether a review exists for a particular line of code in the PR
2023-09-29 12:12:54 +00:00
func ReviewExists ( ctx context . Context , issue * Issue , treePath string , line int64 ) ( bool , error ) {
return db . GetEngine ( ctx ) . Cols ( "id" ) . Exist ( & Comment { IssueID : issue . ID , TreePath : treePath , Line : line , Type : CommentTypeCode } )
2019-11-24 05:46:16 +00:00
}
2019-11-14 02:57:36 +00:00
// ContentEmptyErr represents an content empty error
2021-03-14 18:52:12 +00:00
type ContentEmptyErr struct { }
2019-11-14 02:57:36 +00:00
func ( ContentEmptyErr ) Error ( ) string {
return "Review content is empty"
}
// IsContentEmptyErr returns true if err is a ContentEmptyErr
func IsContentEmptyErr ( err error ) bool {
_ , ok := err . ( ContentEmptyErr )
return ok
}
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
2023-09-29 12:12:54 +00:00
func SubmitReview ( ctx context . Context , doer * user_model . User , issue * Issue , reviewType ReviewType , content , commitID string , stale bool , attachmentUUIDs [ ] string ) ( * Review , * Comment , error ) {
ctx , committer , err := db . TxContext ( ctx )
2021-11-19 13:39:57 +00:00
if err != nil {
2019-11-14 02:57:36 +00:00
return nil , nil , err
}
2021-11-19 13:39:57 +00:00
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2019-11-14 02:57:36 +00:00
2021-03-14 18:52:12 +00:00
official := false
2019-12-04 01:08:56 +00:00
2022-05-20 14:08:52 +00:00
review , err := GetCurrentReview ( ctx , doer , issue )
2019-11-14 02:57:36 +00:00
if err != nil {
if ! IsErrReviewNotExist ( err ) {
return nil , nil , err
}
2019-11-14 20:58:01 +00:00
if reviewType != ReviewTypeApprove && len ( strings . TrimSpace ( content ) ) == 0 {
2019-11-14 02:57:36 +00:00
return nil , nil , ContentEmptyErr { }
}
2019-12-04 01:08:56 +00:00
if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
2022-05-20 14:08:52 +00:00
if _ , err := db . Exec ( ctx , "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?" , false , issue . ID , doer . ID ) ; err != nil {
2019-12-04 01:08:56 +00:00
return nil , nil , err
}
2022-05-20 14:08:52 +00:00
if official , err = IsOfficialReviewer ( ctx , issue , doer ) ; err != nil {
2019-12-04 01:08:56 +00:00
return nil , nil , err
}
}
2019-11-14 02:57:36 +00:00
// No current review. Create a new one!
2022-05-20 14:08:52 +00:00
if review , err = CreateReview ( ctx , CreateReviewOptions {
2019-11-14 02:57:36 +00:00
Type : reviewType ,
Issue : issue ,
Reviewer : doer ,
Content : content ,
2019-12-04 01:08:56 +00:00
Official : official ,
2020-01-09 01:47:45 +00:00
CommitID : commitID ,
Stale : stale ,
2020-10-12 19:55:13 +00:00
} ) ; err != nil {
2019-11-14 02:57:36 +00:00
return nil , nil , err
}
} else {
2022-01-19 23:26:57 +00:00
if err := review . LoadCodeComments ( ctx ) ; err != nil {
2019-11-14 02:57:36 +00:00
return nil , nil , err
}
2019-11-14 20:58:01 +00:00
if reviewType != ReviewTypeApprove && len ( review . CodeComments ) == 0 && len ( strings . TrimSpace ( content ) ) == 0 {
2019-11-14 02:57:36 +00:00
return nil , nil , ContentEmptyErr { }
}
2019-12-04 01:08:56 +00:00
if reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject {
// Only reviewers latest review of type approve and reject shall count as "official", so existing reviews needs to be cleared
2022-05-20 14:08:52 +00:00
if _ , err := db . Exec ( ctx , "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?" , false , issue . ID , doer . ID ) ; err != nil {
2019-12-04 01:08:56 +00:00
return nil , nil , err
}
2022-05-20 14:08:52 +00:00
if official , err = IsOfficialReviewer ( ctx , issue , doer ) ; err != nil {
2019-12-04 01:08:56 +00:00
return nil , nil , err
}
}
review . Official = official
2019-11-14 02:57:36 +00:00
review . Issue = issue
review . Content = content
review . Type = reviewType
2020-01-09 01:47:45 +00:00
review . CommitID = commitID
review . Stale = stale
2019-12-04 01:08:56 +00:00
2020-01-09 01:47:45 +00:00
if _ , err := sess . ID ( review . ID ) . Cols ( "content, type, official, commit_id, stale" ) . Update ( review ) ; err != nil {
2019-11-14 02:57:36 +00:00
return nil , nil , err
}
2018-08-06 04:43:22 +00:00
}
2019-11-14 02:57:36 +00:00
2022-12-10 02:46:31 +00:00
comm , err := CreateComment ( ctx , & CreateCommentOptions {
2021-06-15 01:12:33 +00:00
Type : CommentTypeReview ,
Doer : doer ,
Content : review . Content ,
Issue : issue ,
Repo : issue . Repo ,
ReviewID : review . ID ,
Attachments : attachmentUUIDs ,
2019-11-14 02:57:36 +00:00
} )
if err != nil || comm == nil {
return nil , nil , err
}
2020-10-12 19:55:13 +00:00
// try to remove team review request if need
if issue . Repo . Owner . IsOrganization ( ) && ( reviewType == ReviewTypeApprove || reviewType == ReviewTypeReject ) {
teamReviewRequests := make ( [ ] * Review , 0 , 10 )
2021-10-07 20:39:59 +00:00
if err := sess . SQL ( "SELECT * FROM review WHERE issue_id = ? AND reviewer_team_id > 0 AND type = ?" , issue . ID , ReviewTypeRequest ) . Find ( & teamReviewRequests ) ; err != nil {
2020-10-12 19:55:13 +00:00
return nil , nil , err
}
for _ , teamReviewRequest := range teamReviewRequests {
2022-03-29 06:29:02 +00:00
ok , err := organization . IsTeamMember ( ctx , issue . Repo . OwnerID , teamReviewRequest . ReviewerTeamID , doer . ID )
2020-10-12 19:55:13 +00:00
if err != nil {
return nil , nil , err
} else if ! ok {
continue
}
2023-12-25 20:25:29 +00:00
if _ , err := db . DeleteByID [ Review ] ( ctx , teamReviewRequest . ID ) ; err != nil {
2020-10-12 19:55:13 +00:00
return nil , nil , err
}
}
}
2019-11-14 02:57:36 +00:00
comm . Review = review
2021-11-19 13:39:57 +00:00
return review , comm , committer . Commit ( )
2018-08-06 04:43:22 +00:00
}
2018-11-22 13:17:36 +00:00
2020-10-12 19:55:13 +00:00
// GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request
2022-05-20 14:08:52 +00:00
func GetReviewByIssueIDAndUserID ( ctx context . Context , issueID , userID int64 ) ( * Review , error ) {
2020-10-12 19:55:13 +00:00
review := new ( Review )
2020-04-06 16:33:34 +00:00
2023-12-20 15:19:58 +00:00
has , err := db . GetEngine ( ctx ) . Where (
builder . In ( "type" , ReviewTypeApprove , ReviewTypeReject , ReviewTypeRequest ) .
And ( builder . Eq { "issue_id" : issueID , "reviewer_id" : userID , "original_author_id" : 0 } ) ) .
Desc ( "id" ) .
2020-10-12 19:55:13 +00:00
Get ( review )
if err != nil {
return nil , err
}
if ! has {
return nil , ErrReviewNotExist { }
}
return review , nil
}
2022-01-10 09:32:37 +00:00
// GetTeamReviewerByIssueIDAndTeamID get the latest review request of reviewer team for a pull request
2023-12-20 15:19:58 +00:00
func GetTeamReviewerByIssueIDAndTeamID ( ctx context . Context , issueID , teamID int64 ) ( * Review , error ) {
review := new ( Review )
2020-10-12 19:55:13 +00:00
2023-12-20 15:19:58 +00:00
has , err := db . GetEngine ( ctx ) . Where ( builder . Eq { "issue_id" : issueID , "reviewer_team_id" : teamID } ) .
Desc ( "id" ) .
Get ( review )
if err != nil {
2020-04-06 16:33:34 +00:00
return nil , err
}
2020-10-12 19:55:13 +00:00
if ! has {
return nil , ErrReviewNotExist { 0 }
}
2022-06-20 10:02:49 +00:00
return review , err
2020-04-06 16:33:34 +00:00
}
2020-01-09 01:47:45 +00:00
// MarkReviewsAsStale marks existing reviews as stale
2023-09-29 12:12:54 +00:00
func MarkReviewsAsStale ( ctx context . Context , issueID int64 ) ( err error ) {
_ , err = db . GetEngine ( ctx ) . Exec ( "UPDATE `review` SET stale=? WHERE issue_id=?" , true , issueID )
2020-01-09 01:47:45 +00:00
2022-06-20 10:02:49 +00:00
return err
2020-01-09 01:47:45 +00:00
}
// MarkReviewsAsNotStale marks existing reviews as not stale for a giving commit SHA
2023-09-29 12:12:54 +00:00
func MarkReviewsAsNotStale ( ctx context . Context , issueID int64 , commitID string ) ( err error ) {
_ , err = db . GetEngine ( ctx ) . Exec ( "UPDATE `review` SET stale=? WHERE issue_id=? AND commit_id=?" , false , issueID , commitID )
2020-01-09 01:47:45 +00:00
2022-06-20 10:02:49 +00:00
return err
2020-01-09 01:47:45 +00:00
}
2020-01-23 17:28:15 +00:00
2021-02-11 17:32:25 +00:00
// DismissReview change the dismiss status of a review
2023-07-20 07:18:52 +00:00
func DismissReview ( ctx context . Context , review * Review , isDismiss bool ) ( err error ) {
2021-02-11 17:32:25 +00:00
if review . Dismissed == isDismiss || ( review . Type != ReviewTypeApprove && review . Type != ReviewTypeReject ) {
return nil
}
review . Dismissed = isDismiss
2021-04-15 10:03:11 +00:00
if review . ID == 0 {
return ErrReviewNotExist { }
}
2023-07-20 07:18:52 +00:00
_ , err = db . GetEngine ( ctx ) . ID ( review . ID ) . Cols ( "dismissed" ) . Update ( review )
2021-02-11 17:32:25 +00:00
2022-06-20 10:02:49 +00:00
return err
2021-02-11 17:32:25 +00:00
}
2020-01-23 17:28:15 +00:00
// InsertReviews inserts review and review comments
2023-09-29 12:12:54 +00:00
func InsertReviews ( ctx context . Context , reviews [ ] * Review ) error {
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 15:41:00 +00:00
if err != nil {
2020-01-23 17:28:15 +00:00
return err
}
2021-11-21 15:41:00 +00:00
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2020-01-23 17:28:15 +00:00
for _ , review := range reviews {
if _ , err := sess . NoAutoTime ( ) . Insert ( review ) ; err != nil {
return err
}
if _ , err := sess . NoAutoTime ( ) . Insert ( & Comment {
Type : CommentTypeReview ,
Content : review . Content ,
PosterID : review . ReviewerID ,
OriginalAuthor : review . OriginalAuthor ,
OriginalAuthorID : review . OriginalAuthorID ,
IssueID : review . IssueID ,
ReviewID : review . ID ,
CreatedUnix : review . CreatedUnix ,
UpdatedUnix : review . UpdatedUnix ,
} ) ; err != nil {
return err
}
for _ , c := range review . Comments {
c . ReviewID = review . ID
}
2020-04-20 03:04:08 +00:00
if len ( review . Comments ) > 0 {
if _ , err := sess . NoAutoTime ( ) . Insert ( review . Comments ) ; err != nil {
return err
}
2020-01-23 17:28:15 +00:00
}
}
2021-11-21 15:41:00 +00:00
return committer . Commit ( )
2020-01-23 17:28:15 +00:00
}
2020-04-06 16:33:34 +00:00
2020-04-30 20:24:08 +00:00
// AddReviewRequest add a review request from one reviewer
2023-08-10 02:39:21 +00:00
func AddReviewRequest ( ctx context . Context , issue * Issue , reviewer , doer * user_model . User ) ( * Comment , error ) {
ctx , committer , err := db . TxContext ( ctx )
2021-11-19 13:39:57 +00:00
if err != nil {
2020-04-06 16:33:34 +00:00
return nil , err
}
2021-11-19 13:39:57 +00:00
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2020-04-06 16:33:34 +00:00
2022-05-20 14:08:52 +00:00
review , err := GetReviewByIssueIDAndUserID ( ctx , issue . ID , reviewer . ID )
2020-10-12 19:55:13 +00:00
if err != nil && ! IsErrReviewNotExist ( err ) {
2020-04-06 16:33:34 +00:00
return nil , err
}
2024-03-28 15:19:24 +00:00
if review != nil {
// skip it when reviewer hase been request to review
if review . Type == ReviewTypeRequest {
return nil , committer . Commit ( ) // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
}
if issue . IsClosed {
return nil , ErrReviewRequestOnClosedPR { }
}
if issue . IsPull {
if err := issue . LoadPullRequest ( ctx ) ; err != nil {
return nil , err
}
if issue . PullRequest . HasMerged {
return nil , ErrReviewRequestOnClosedPR { }
}
}
2020-04-06 16:33:34 +00:00
}
2023-09-28 23:24:36 +00:00
// if the reviewer is an official reviewer,
// remove the official flag in the all previous reviews
official , err := IsOfficialReviewer ( ctx , issue , reviewer )
2020-10-12 19:55:13 +00:00
if err != nil {
return nil , err
} else if official {
2020-04-06 16:33:34 +00:00
if _ , err := sess . Exec ( "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_id=?" , false , issue . ID , reviewer . ID ) ; err != nil {
return nil , err
}
}
2022-05-20 14:08:52 +00:00
review , err = CreateReview ( ctx , CreateReviewOptions {
2020-04-06 16:33:34 +00:00
Type : ReviewTypeRequest ,
Issue : issue ,
Reviewer : reviewer ,
Official : official ,
Stale : false ,
2020-10-20 18:18:25 +00:00
} )
if err != nil {
2020-10-12 19:55:13 +00:00
return nil , err
2020-04-06 16:33:34 +00:00
}
2022-12-10 02:46:31 +00:00
comment , err := CreateComment ( ctx , & CreateCommentOptions {
2020-04-06 16:33:34 +00:00
Type : CommentTypeReviewRequest ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
RemovedAssignee : false , // Use RemovedAssignee as !isRequest
AssigneeID : reviewer . ID , // Use AssigneeID as reviewer ID
2020-10-20 18:18:25 +00:00
ReviewID : review . ID ,
2020-04-06 16:33:34 +00:00
} )
if err != nil {
return nil , err
}
2024-02-13 08:45:31 +00:00
// func caller use the created comment to retrieve created review too.
comment . Review = review
2021-11-19 13:39:57 +00:00
return comment , committer . Commit ( )
2020-04-06 16:33:34 +00:00
}
2021-03-14 18:52:12 +00:00
// RemoveReviewRequest remove a review request from one reviewer
2023-08-10 02:39:21 +00:00
func RemoveReviewRequest ( ctx context . Context , issue * Issue , reviewer , doer * user_model . User ) ( * Comment , error ) {
ctx , committer , err := db . TxContext ( ctx )
2021-11-19 13:39:57 +00:00
if err != nil {
2020-10-12 19:55:13 +00:00
return nil , err
}
2021-11-19 13:39:57 +00:00
defer committer . Close ( )
2020-10-12 19:55:13 +00:00
2022-05-20 14:08:52 +00:00
review , err := GetReviewByIssueIDAndUserID ( ctx , issue . ID , reviewer . ID )
2020-10-12 19:55:13 +00:00
if err != nil && ! IsErrReviewNotExist ( err ) {
return nil , err
2020-04-06 16:33:34 +00:00
}
2020-10-12 19:55:13 +00:00
if review == nil || review . Type != ReviewTypeRequest {
2020-04-06 16:33:34 +00:00
return nil , nil
}
2022-05-20 14:08:52 +00:00
if _ , err = db . DeleteByBean ( ctx , review ) ; err != nil {
2020-10-12 19:55:13 +00:00
return nil , err
}
2022-05-20 14:08:52 +00:00
official , err := IsOfficialReviewer ( ctx , issue , reviewer )
2020-10-12 19:55:13 +00:00
if err != nil {
return nil , err
} else if official {
2023-01-15 05:00:09 +00:00
if err := restoreLatestOfficialReview ( ctx , issue . ID , reviewer . ID ) ; err != nil {
2020-10-12 19:55:13 +00:00
return nil , err
}
}
2022-12-10 02:46:31 +00:00
comment , err := CreateComment ( ctx , & CreateCommentOptions {
2020-10-12 19:55:13 +00:00
Type : CommentTypeReviewRequest ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
RemovedAssignee : true , // Use RemovedAssignee as !isRequest
AssigneeID : reviewer . ID , // Use AssigneeID as reviewer ID
} )
if err != nil {
return nil , err
}
2021-11-19 13:39:57 +00:00
return comment , committer . Commit ( )
2020-10-12 19:55:13 +00:00
}
2023-01-15 05:00:09 +00:00
// Recalculate the latest official review for reviewer
func restoreLatestOfficialReview ( ctx context . Context , issueID , reviewerID int64 ) error {
review , err := GetReviewByIssueIDAndUserID ( ctx , issueID , reviewerID )
if err != nil && ! IsErrReviewNotExist ( err ) {
return err
}
if review != nil {
if _ , err := db . Exec ( ctx , "UPDATE `review` SET official=? WHERE id=?" , true , review . ID ) ; err != nil {
return err
}
}
return nil
}
2020-10-12 19:55:13 +00:00
// AddTeamReviewRequest add a review request from one team
2023-08-10 02:39:21 +00:00
func AddTeamReviewRequest ( ctx context . Context , issue * Issue , reviewer * organization . Team , doer * user_model . User ) ( * Comment , error ) {
ctx , committer , err := db . TxContext ( ctx )
2021-11-19 13:39:57 +00:00
if err != nil {
2020-04-06 16:33:34 +00:00
return nil , err
}
2021-11-19 13:39:57 +00:00
defer committer . Close ( )
2020-04-06 16:33:34 +00:00
2022-05-20 14:08:52 +00:00
review , err := GetTeamReviewerByIssueIDAndTeamID ( ctx , issue . ID , reviewer . ID )
2020-10-12 19:55:13 +00:00
if err != nil && ! IsErrReviewNotExist ( err ) {
2020-04-06 16:33:34 +00:00
return nil , err
}
2020-10-12 19:55:13 +00:00
// This team already has been requested to review - therefore skip this.
if review != nil {
return nil , nil
}
2022-05-20 14:08:52 +00:00
official , err := IsOfficialReviewerTeam ( ctx , issue , reviewer )
2020-04-06 16:33:34 +00:00
if err != nil {
2022-10-24 19:29:17 +00:00
return nil , fmt . Errorf ( "isOfficialReviewerTeam(): %w" , err )
2020-10-12 19:55:13 +00:00
} else if ! official {
2022-05-20 14:08:52 +00:00
if official , err = IsOfficialReviewer ( ctx , issue , doer ) ; err != nil {
2022-10-24 19:29:17 +00:00
return nil , fmt . Errorf ( "isOfficialReviewer(): %w" , err )
2020-10-12 19:55:13 +00:00
}
}
2022-05-20 14:08:52 +00:00
if review , err = CreateReview ( ctx , CreateReviewOptions {
2020-10-12 19:55:13 +00:00
Type : ReviewTypeRequest ,
Issue : issue ,
ReviewerTeam : reviewer ,
Official : official ,
Stale : false ,
} ) ; err != nil {
return nil , err
2020-04-06 16:33:34 +00:00
}
if official {
2022-05-20 14:08:52 +00:00
if _ , err := db . Exec ( ctx , "UPDATE `review` SET official=? WHERE issue_id=? AND reviewer_team_id=?" , false , issue . ID , reviewer . ID ) ; err != nil {
2020-10-12 19:55:13 +00:00
return nil , err
}
}
2022-12-10 02:46:31 +00:00
comment , err := CreateComment ( ctx , & CreateCommentOptions {
2020-10-12 19:55:13 +00:00
Type : CommentTypeReviewRequest ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
RemovedAssignee : false , // Use RemovedAssignee as !isRequest
AssigneeTeamID : reviewer . ID , // Use AssigneeTeamID as reviewer team ID
2020-10-20 18:18:25 +00:00
ReviewID : review . ID ,
2020-10-12 19:55:13 +00:00
} )
if err != nil {
2022-12-10 02:46:31 +00:00
return nil , fmt . Errorf ( "CreateComment(): %w" , err )
2020-10-12 19:55:13 +00:00
}
2021-11-19 13:39:57 +00:00
return comment , committer . Commit ( )
2020-10-12 19:55:13 +00:00
}
2021-03-14 18:52:12 +00:00
// RemoveTeamReviewRequest remove a review request from one team
2023-08-10 02:39:21 +00:00
func RemoveTeamReviewRequest ( ctx context . Context , issue * Issue , reviewer * organization . Team , doer * user_model . User ) ( * Comment , error ) {
ctx , committer , err := db . TxContext ( ctx )
2021-11-19 13:39:57 +00:00
if err != nil {
2020-10-12 19:55:13 +00:00
return nil , err
}
2021-11-19 13:39:57 +00:00
defer committer . Close ( )
2020-10-12 19:55:13 +00:00
2022-05-20 14:08:52 +00:00
review , err := GetTeamReviewerByIssueIDAndTeamID ( ctx , issue . ID , reviewer . ID )
2020-10-12 19:55:13 +00:00
if err != nil && ! IsErrReviewNotExist ( err ) {
return nil , err
}
if review == nil {
return nil , nil
}
2020-04-06 16:33:34 +00:00
2022-05-20 14:08:52 +00:00
if _ , err = db . DeleteByBean ( ctx , review ) ; err != nil {
2020-10-12 19:55:13 +00:00
return nil , err
}
2022-05-20 14:08:52 +00:00
official , err := IsOfficialReviewerTeam ( ctx , issue , reviewer )
2020-10-12 19:55:13 +00:00
if err != nil {
2022-10-24 19:29:17 +00:00
return nil , fmt . Errorf ( "isOfficialReviewerTeam(): %w" , err )
2020-10-12 19:55:13 +00:00
}
if official {
// recalculate which is the latest official review from that team
2022-05-20 14:08:52 +00:00
review , err := GetReviewByIssueIDAndUserID ( ctx , issue . ID , - reviewer . ID )
2020-10-12 19:55:13 +00:00
if err != nil && ! IsErrReviewNotExist ( err ) {
2020-04-06 16:33:34 +00:00
return nil , err
}
if review != nil {
2022-05-20 14:08:52 +00:00
if _ , err := db . Exec ( ctx , "UPDATE `review` SET official=? WHERE id=?" , true , review . ID ) ; err != nil {
2020-04-06 16:33:34 +00:00
return nil , err
}
}
}
2020-10-12 19:55:13 +00:00
if doer == nil {
2021-11-19 13:39:57 +00:00
return nil , committer . Commit ( )
2020-04-06 16:33:34 +00:00
}
2022-12-10 02:46:31 +00:00
comment , err := CreateComment ( ctx , & CreateCommentOptions {
2020-04-06 16:33:34 +00:00
Type : CommentTypeReviewRequest ,
Doer : doer ,
Repo : issue . Repo ,
Issue : issue ,
RemovedAssignee : true , // Use RemovedAssignee as !isRequest
2020-10-12 19:55:13 +00:00
AssigneeTeamID : reviewer . ID , // Use AssigneeTeamID as reviewer team ID
2020-04-06 16:33:34 +00:00
} )
if err != nil {
2022-12-10 02:46:31 +00:00
return nil , fmt . Errorf ( "CreateComment(): %w" , err )
2020-04-06 16:33:34 +00:00
}
2021-11-19 13:39:57 +00:00
return comment , committer . Commit ( )
2020-04-06 16:33:34 +00:00
}
2020-04-18 13:50:25 +00:00
// MarkConversation Add or remove Conversation mark for a code comment
2023-09-29 12:12:54 +00:00
func MarkConversation ( ctx context . Context , comment * Comment , doer * user_model . User , isResolve bool ) ( err error ) {
2020-04-18 13:50:25 +00:00
if comment . Type != CommentTypeCode {
return nil
}
if isResolve {
if comment . ResolveDoerID != 0 {
return nil
}
2023-09-29 12:12:54 +00:00
if _ , err = db . GetEngine ( ctx ) . Exec ( "UPDATE `comment` SET resolve_doer_id=? WHERE id=?" , doer . ID , comment . ID ) ; err != nil {
2020-04-18 13:50:25 +00:00
return err
}
} else {
if comment . ResolveDoerID == 0 {
return nil
}
2023-09-29 12:12:54 +00:00
if _ , err = db . GetEngine ( ctx ) . Exec ( "UPDATE `comment` SET resolve_doer_id=? WHERE id=?" , 0 , comment . ID ) ; err != nil {
2020-04-18 13:50:25 +00:00
return err
}
}
return nil
}
// CanMarkConversation Add or remove Conversation mark for a code comment permission check
// the PR writer , offfcial reviewer and poster can do it
2023-09-29 12:12:54 +00:00
func CanMarkConversation ( ctx context . Context , issue * Issue , doer * user_model . User ) ( permResult bool , err error ) {
2020-04-18 13:50:25 +00:00
if doer == nil || issue == nil {
return false , fmt . Errorf ( "issue or doer is nil" )
}
if doer . ID != issue . PosterID {
2023-09-29 12:12:54 +00:00
if err = issue . LoadRepo ( ctx ) ; err != nil {
2020-04-18 13:50:25 +00:00
return false , err
}
2023-09-29 12:12:54 +00:00
p , err := access_model . GetUserRepoPermission ( ctx , issue . Repo , doer )
2020-04-18 13:50:25 +00:00
if err != nil {
return false , err
}
2021-11-28 11:58:28 +00:00
permResult = p . CanAccess ( perm . AccessModeWrite , unit . TypePullRequests )
2020-04-18 13:50:25 +00:00
if ! permResult {
2023-09-29 12:12:54 +00:00
if permResult , err = IsOfficialReviewer ( ctx , issue , doer ) ; err != nil {
2020-04-18 13:50:25 +00:00
return false , err
}
}
if ! permResult {
return false , nil
}
}
return true , nil
}
2020-05-02 00:20:51 +00:00
// DeleteReview delete a review and it's code comments
2023-09-29 12:12:54 +00:00
func DeleteReview ( ctx context . Context , r * Review ) error {
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 15:41:00 +00:00
if err != nil {
2020-05-02 00:20:51 +00:00
return err
}
2021-11-21 15:41:00 +00:00
defer committer . Close ( )
2020-05-02 00:20:51 +00:00
if r . ID == 0 {
return fmt . Errorf ( "review is not allowed to be 0" )
}
2020-10-20 18:18:25 +00:00
if r . Type == ReviewTypeRequest {
return fmt . Errorf ( "review request can not be deleted using this method" )
}
2020-05-02 00:20:51 +00:00
opts := FindCommentsOptions {
Type : CommentTypeCode ,
IssueID : r . IssueID ,
ReviewID : r . ID ,
}
2023-12-25 20:25:29 +00:00
if _ , err := db . Delete [ Comment ] ( ctx , opts ) ; err != nil {
2020-05-02 00:20:51 +00:00
return err
}
opts = FindCommentsOptions {
Type : CommentTypeReview ,
IssueID : r . IssueID ,
ReviewID : r . ID ,
}
2023-12-25 20:25:29 +00:00
if _ , err := db . Delete [ Comment ] ( ctx , opts ) ; err != nil {
2020-05-02 00:20:51 +00:00
return err
}
2023-11-05 13:25:40 +00:00
opts = FindCommentsOptions {
Type : CommentTypeDismissReview ,
IssueID : r . IssueID ,
ReviewID : r . ID ,
}
2023-12-25 20:25:29 +00:00
if _ , err := db . Delete [ Comment ] ( ctx , opts ) ; err != nil {
2023-11-05 13:25:40 +00:00
return err
}
2023-12-25 20:25:29 +00:00
if _ , err := db . DeleteByID [ Review ] ( ctx , r . ID ) ; err != nil {
2020-05-02 00:20:51 +00:00
return err
}
2023-01-15 05:00:09 +00:00
if r . Official {
if err := restoreLatestOfficialReview ( ctx , r . IssueID , r . ReviewerID ) ; err != nil {
return err
}
}
2021-11-21 15:41:00 +00:00
return committer . Commit ( )
2020-05-02 00:20:51 +00:00
}
// GetCodeCommentsCount return count of CodeComments a Review has
2023-09-29 12:12:54 +00:00
func ( r * Review ) GetCodeCommentsCount ( ctx context . Context ) int {
2020-05-02 00:20:51 +00:00
opts := FindCommentsOptions {
Type : CommentTypeCode ,
IssueID : r . IssueID ,
ReviewID : r . ID ,
}
2023-01-17 21:03:44 +00:00
conds := opts . ToConds ( )
2020-05-02 00:20:51 +00:00
if r . ID == 0 {
conds = conds . And ( builder . Eq { "invalidated" : false } )
}
2023-09-29 12:12:54 +00:00
count , err := db . GetEngine ( ctx ) . Where ( conds ) . Count ( new ( Comment ) )
2020-05-02 00:20:51 +00:00
if err != nil {
return 0
}
return int ( count )
}
// HTMLURL formats a URL-string to the related review issue-comment
2023-09-29 12:12:54 +00:00
func ( r * Review ) HTMLURL ( ctx context . Context ) string {
2020-05-02 00:20:51 +00:00
opts := FindCommentsOptions {
Type : CommentTypeReview ,
IssueID : r . IssueID ,
ReviewID : r . ID ,
}
comment := new ( Comment )
2023-09-29 12:12:54 +00:00
has , err := db . GetEngine ( ctx ) . Where ( opts . ToConds ( ) ) . Get ( comment )
2020-05-02 00:20:51 +00:00
if err != nil || ! has {
return ""
}
2023-09-29 12:12:54 +00:00
return comment . HTMLURL ( ctx )
2020-05-02 00:20:51 +00:00
}
2022-02-01 18:20:28 +00:00
// RemapExternalUser ExternalUserRemappable interface
func ( r * Review ) RemapExternalUser ( externalName string , externalID , userID int64 ) error {
r . OriginalAuthor = externalName
r . OriginalAuthorID = externalID
r . ReviewerID = userID
return nil
}
// GetUserID ExternalUserRemappable interface
func ( r * Review ) GetUserID ( ) int64 { return r . ReviewerID }
// GetExternalName ExternalUserRemappable interface
func ( r * Review ) GetExternalName ( ) string { return r . OriginalAuthor }
// GetExternalID ExternalUserRemappable interface
func ( r * Review ) GetExternalID ( ) int64 { return r . OriginalAuthorID }
2022-06-13 09:37:59 +00:00
// UpdateReviewsMigrationsByType updates reviews' migrations information via given git service type and original id and poster id
2023-09-29 12:12:54 +00:00
func UpdateReviewsMigrationsByType ( ctx context . Context , tp structs . GitServiceType , originalAuthorID string , posterID int64 ) error {
_ , err := db . GetEngine ( ctx ) . Table ( "review" ) .
2022-06-13 09:37:59 +00:00
Where ( "original_author_id = ?" , originalAuthorID ) .
And ( migratedIssueCond ( tp ) ) .
2023-07-04 18:36:08 +00:00
Update ( map [ string ] any {
2022-06-13 09:37:59 +00:00
"reviewer_id" : posterID ,
"original_author" : "" ,
"original_author_id" : 0 ,
} )
return err
}