2019-09-20 08:45:38 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2019-09-20 08:45:38 +03:00
2022-06-13 12:37:59 +03:00
package issues
2019-09-20 08:45:38 +03:00
import (
2021-11-19 16:39:57 +03:00
"context"
2019-11-18 16:13:07 +03:00
"fmt"
2021-09-19 14:49:59 +03:00
"code.gitea.io/gitea/models/db"
2022-05-11 13:09:36 +03:00
access_model "code.gitea.io/gitea/models/perm/access"
2021-12-10 04:27:50 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
2019-09-20 08:45:38 +03:00
"code.gitea.io/gitea/modules/log"
2019-10-14 01:29:10 +03:00
"code.gitea.io/gitea/modules/references"
2019-09-20 08:45:38 +03:00
)
type crossReference struct {
Issue * Issue
2019-10-14 01:29:10 +03:00
Action references . XRefAction
2019-09-20 08:45:38 +03:00
}
// crossReferencesContext is context to pass along findCrossReference functions
type crossReferencesContext struct {
Type CommentType
2021-11-24 12:49:20 +03:00
Doer * user_model . User
2019-09-20 08:45:38 +03:00
OrigIssue * Issue
OrigComment * Comment
2019-11-19 02:43:03 +03:00
RemoveOld bool
2019-09-20 08:45:38 +03:00
}
2022-05-20 17:08:52 +03:00
func findOldCrossReferences ( ctx context . Context , issueID , commentID int64 ) ( [ ] * Comment , error ) {
2019-09-20 08:45:38 +03:00
active := make ( [ ] * Comment , 0 , 10 )
2022-05-20 17:08:52 +03:00
return active , db . GetEngine ( ctx ) . Where ( "`ref_action` IN (?, ?, ?)" , references . XRefActionNone , references . XRefActionCloses , references . XRefActionReopens ) .
2019-11-18 16:13:07 +03:00
And ( "`ref_issue_id` = ?" , issueID ) .
2019-11-19 02:43:03 +03:00
And ( "`ref_comment_id` = ?" , commentID ) .
Find ( & active )
}
2022-05-20 17:08:52 +03:00
func neuterCrossReferences ( ctx context . Context , issueID , commentID int64 ) error {
active , err := findOldCrossReferences ( ctx , issueID , commentID )
2019-11-19 02:43:03 +03:00
if err != nil {
2019-09-20 08:45:38 +03:00
return err
}
ids := make ( [ ] int64 , len ( active ) )
for i , c := range active {
ids [ i ] = c . ID
}
2024-02-14 21:19:57 +03:00
return neuterCrossReferencesIDs ( ctx , ids )
2019-11-19 02:43:03 +03:00
}
2024-02-14 21:19:57 +03:00
func neuterCrossReferencesIDs ( ctx context . Context , ids [ ] int64 ) error {
2022-05-20 17:08:52 +03:00
_ , err := db . GetEngine ( ctx ) . In ( "id" , ids ) . Cols ( "`ref_action`" ) . Update ( & Comment { RefAction : references . XRefActionNeutered } )
2019-09-20 08:45:38 +03:00
return err
}
2022-06-13 12:37:59 +03:00
// AddCrossReferences add cross repositories references.
func ( issue * Issue ) AddCrossReferences ( stdCtx context . Context , doer * user_model . User , removeOld bool ) error {
2019-09-20 08:45:38 +03:00
var commentType CommentType
if issue . IsPull {
commentType = CommentTypePullRef
} else {
commentType = CommentTypeIssueRef
}
ctx := & crossReferencesContext {
Type : commentType ,
Doer : doer ,
OrigIssue : issue ,
2019-11-19 02:43:03 +03:00
RemoveOld : removeOld ,
2019-09-20 08:45:38 +03:00
}
2021-11-19 16:39:57 +03:00
return issue . createCrossReferences ( stdCtx , ctx , issue . Title , issue . Content )
2019-09-20 08:45:38 +03:00
}
2021-11-19 16:39:57 +03:00
func ( issue * Issue ) createCrossReferences ( stdCtx context . Context , ctx * crossReferencesContext , plaincontent , mdcontent string ) error {
2021-12-10 04:27:50 +03:00
xreflist , err := ctx . OrigIssue . getCrossReferences ( stdCtx , ctx , plaincontent , mdcontent )
2019-09-20 08:45:38 +03:00
if err != nil {
return err
}
2019-11-19 02:43:03 +03:00
if ctx . RemoveOld {
var commentID int64
if ctx . OrigComment != nil {
commentID = ctx . OrigComment . ID
}
2022-05-20 17:08:52 +03:00
active , err := findOldCrossReferences ( stdCtx , ctx . OrigIssue . ID , commentID )
2019-11-19 02:43:03 +03:00
if err != nil {
return err
}
ids := make ( [ ] int64 , 0 , len ( active ) )
for _ , c := range active {
found := false
for i , x := range xreflist {
if x . Issue . ID == c . IssueID && x . Action == c . RefAction {
found = true
xreflist = append ( xreflist [ : i ] , xreflist [ i + 1 : ] ... )
break
}
}
if ! found {
ids = append ( ids , c . ID )
}
}
if len ( ids ) > 0 {
2024-02-14 21:19:57 +03:00
if err = neuterCrossReferencesIDs ( stdCtx , ids ) ; err != nil {
2019-11-19 02:43:03 +03:00
return err
}
}
}
2019-09-20 08:45:38 +03:00
for _ , xref := range xreflist {
2019-11-15 21:18:09 +03:00
var refCommentID int64
if ctx . OrigComment != nil {
refCommentID = ctx . OrigComment . ID
}
2021-03-14 21:52:12 +03:00
opts := & CreateCommentOptions {
2019-11-15 21:18:09 +03:00
Type : ctx . Type ,
Doer : ctx . Doer ,
Repo : xref . Issue . Repo ,
Issue : xref . Issue ,
RefRepoID : ctx . OrigIssue . RepoID ,
RefIssueID : ctx . OrigIssue . ID ,
RefCommentID : refCommentID ,
RefAction : xref . Action ,
2019-11-18 16:13:07 +03:00
RefIsPull : ctx . OrigIssue . IsPull ,
2019-12-01 05:44:39 +03:00
}
2022-12-10 05:46:31 +03:00
_ , err := CreateComment ( stdCtx , opts )
2019-12-01 05:44:39 +03:00
if err != nil {
return err
}
2019-09-20 08:45:38 +03:00
}
return nil
}
2021-12-10 04:27:50 +03:00
func ( issue * Issue ) getCrossReferences ( stdCtx context . Context , ctx * crossReferencesContext , plaincontent , mdcontent string ) ( [ ] * crossReference , error ) {
2019-09-20 08:45:38 +03:00
xreflist := make ( [ ] * crossReference , 0 , 5 )
2019-10-14 01:29:10 +03:00
var (
2021-12-10 04:27:50 +03:00
refRepo * repo_model . Repository
2019-11-18 16:13:07 +03:00
refIssue * Issue
refAction references . XRefAction
err error
2019-10-14 01:29:10 +03:00
)
allrefs := append ( references . FindAllIssueReferences ( plaincontent ) , references . FindAllIssueReferencesMarkdown ( mdcontent ) ... )
for _ , ref := range allrefs {
if ref . Owner == "" && ref . Name == "" {
// Issues in the same repository
2022-04-08 12:11:15 +03:00
if err := ctx . OrigIssue . LoadRepo ( stdCtx ) ; err != nil {
2019-09-20 08:45:38 +03:00
return nil , err
}
2019-10-14 01:29:10 +03:00
refRepo = ctx . OrigIssue . Repo
} else {
// Issues in other repositories
2022-12-03 05:48:26 +03:00
refRepo , err = repo_model . GetRepositoryByOwnerAndName ( stdCtx , ref . Owner , ref . Name )
2019-09-20 08:45:38 +03:00
if err != nil {
2021-12-10 04:27:50 +03:00
if repo_model . IsErrRepoNotExist ( err ) {
2019-09-20 08:45:38 +03:00
continue
}
return nil , err
}
2019-10-14 01:29:10 +03:00
}
2021-12-10 04:27:50 +03:00
if refIssue , refAction , err = ctx . OrigIssue . verifyReferencedIssue ( stdCtx , ctx , refRepo , ref ) ; err != nil {
2019-10-14 01:29:10 +03:00
return nil , err
}
if refIssue != nil {
xreflist = ctx . OrigIssue . updateCrossReferenceList ( xreflist , & crossReference {
2019-11-18 16:13:07 +03:00
Issue : refIssue ,
Action : refAction ,
2019-10-14 01:29:10 +03:00
} )
2019-09-20 08:45:38 +03:00
}
}
return xreflist , nil
}
func ( issue * Issue ) updateCrossReferenceList ( list [ ] * crossReference , xref * crossReference ) [ ] * crossReference {
if xref . Issue . ID == issue . ID {
return list
}
for i , r := range list {
if r . Issue . ID == xref . Issue . ID {
2019-10-14 01:29:10 +03:00
if xref . Action != references . XRefActionNone {
2019-09-20 08:45:38 +03:00
list [ i ] . Action = xref . Action
}
return list
}
}
return append ( list , xref )
}
2019-11-18 16:13:07 +03:00
// verifyReferencedIssue will check if the referenced issue exists, and whether the doer has permission to do what
2021-12-10 04:27:50 +03:00
func ( issue * Issue ) verifyReferencedIssue ( stdCtx context . Context , ctx * crossReferencesContext , repo * repo_model . Repository ,
2022-02-23 23:16:07 +03:00
ref references . IssueReference ,
) ( * Issue , references . XRefAction , error ) {
2019-11-18 16:13:07 +03:00
refIssue := & Issue { RepoID : repo . ID , Index : ref . Index }
refAction := ref . Action
2021-12-10 04:27:50 +03:00
e := db . GetEngine ( stdCtx )
2019-11-18 16:13:07 +03:00
2019-09-20 08:45:38 +03:00
if has , _ := e . Get ( refIssue ) ; ! has {
2019-11-18 16:13:07 +03:00
return nil , references . XRefActionNone , nil
2019-09-20 08:45:38 +03:00
}
2022-04-08 12:11:15 +03:00
if err := refIssue . LoadRepo ( stdCtx ) ; err != nil {
2019-11-18 16:13:07 +03:00
return nil , references . XRefActionNone , err
2019-09-20 08:45:38 +03:00
}
2019-11-18 16:13:07 +03:00
// Close/reopen actions can only be set from pull requests to issues
if refIssue . IsPull || ! issue . IsPull {
refAction = references . XRefActionNone
}
// Check doer permissions; set action to None if the doer can't change the destination
if refIssue . RepoID != ctx . OrigIssue . RepoID || ref . Action != references . XRefActionNone {
2022-05-11 13:09:36 +03:00
perm , err := access_model . GetUserRepoPermission ( stdCtx , refIssue . Repo , ctx . Doer )
2019-09-20 08:45:38 +03:00
if err != nil {
2019-11-18 16:13:07 +03:00
return nil , references . XRefActionNone , err
2019-09-20 08:45:38 +03:00
}
if ! perm . CanReadIssuesOrPulls ( refIssue . IsPull ) {
2019-11-18 16:13:07 +03:00
return nil , references . XRefActionNone , nil
}
2024-03-04 11:16:03 +03:00
if user_model . IsUserBlockedBy ( stdCtx , ctx . Doer , refIssue . PosterID , refIssue . Repo . OwnerID ) {
return nil , references . XRefActionNone , nil
}
2019-11-18 20:03:49 +03:00
// Accept close/reopening actions only if the poster is able to close the
// referenced issue manually at this moment. The only exception is
// the poster of a new PR referencing an issue on the same repo: then the merger
// should be responsible for checking whether the reference should resolve.
2019-11-18 16:13:07 +03:00
if ref . Action != references . XRefActionNone &&
ctx . Doer . ID != refIssue . PosterID &&
2019-11-18 20:03:49 +03:00
! perm . CanWriteIssuesOrPulls ( refIssue . IsPull ) &&
( refIssue . RepoID != ctx . OrigIssue . RepoID || ctx . OrigComment != nil ) {
2019-11-18 16:13:07 +03:00
refAction = references . XRefActionNone
2019-09-20 08:45:38 +03:00
}
}
2019-11-18 16:13:07 +03:00
return refIssue , refAction , nil
2019-09-20 08:45:38 +03:00
}
2022-06-13 12:37:59 +03:00
// AddCrossReferences add cross references
2022-06-20 13:02:49 +03:00
func ( c * Comment ) AddCrossReferences ( stdCtx context . Context , doer * user_model . User , removeOld bool ) error {
if c . Type != CommentTypeCode && c . Type != CommentTypeComment {
2019-09-20 08:45:38 +03:00
return nil
}
2022-11-19 11:12:33 +03:00
if err := c . LoadIssue ( stdCtx ) ; err != nil {
2019-09-20 08:45:38 +03:00
return err
}
ctx := & crossReferencesContext {
Type : CommentTypeCommentRef ,
Doer : doer ,
2022-06-20 13:02:49 +03:00
OrigIssue : c . Issue ,
OrigComment : c ,
2019-11-19 02:43:03 +03:00
RemoveOld : removeOld ,
2019-09-20 08:45:38 +03:00
}
2022-06-20 13:02:49 +03:00
return c . Issue . createCrossReferences ( stdCtx , ctx , "" , c . Content )
2019-09-20 08:45:38 +03:00
}
2022-06-20 13:02:49 +03:00
func ( c * Comment ) neuterCrossReferences ( ctx context . Context ) error {
return neuterCrossReferences ( ctx , c . IssueID , c . ID )
2019-09-20 08:45:38 +03:00
}
// LoadRefComment loads comment that created this reference from database
2023-09-29 15:12:54 +03:00
func ( c * Comment ) LoadRefComment ( ctx context . Context ) ( err error ) {
2022-06-20 13:02:49 +03:00
if c . RefComment != nil {
2019-09-20 08:45:38 +03:00
return nil
}
2023-09-29 15:12:54 +03:00
c . RefComment , err = GetCommentByID ( ctx , c . RefCommentID )
2022-06-20 13:02:49 +03:00
return err
2019-09-20 08:45:38 +03:00
}
// LoadRefIssue loads comment that created this reference from database
2023-09-29 15:12:54 +03:00
func ( c * Comment ) LoadRefIssue ( ctx context . Context ) ( err error ) {
2022-06-20 13:02:49 +03:00
if c . RefIssue != nil {
2019-09-20 08:45:38 +03:00
return nil
}
2023-09-29 15:12:54 +03:00
c . RefIssue , err = GetIssueByID ( ctx , c . RefIssueID )
2019-09-20 08:45:38 +03:00
if err == nil {
2023-09-29 15:12:54 +03:00
err = c . RefIssue . LoadRepo ( ctx )
2019-09-20 08:45:38 +03:00
}
2022-06-20 13:02:49 +03:00
return err
2019-09-20 08:45:38 +03:00
}
// CommentTypeIsRef returns true if CommentType is a reference from another issue
func CommentTypeIsRef ( t CommentType ) bool {
return t == CommentTypeCommentRef || t == CommentTypePullRef || t == CommentTypeIssueRef
}
2023-02-09 19:31:30 +03:00
// RefCommentLink returns the relative URL for the comment that created this reference
2023-09-29 15:12:54 +03:00
func ( c * Comment ) RefCommentLink ( ctx context . Context ) string {
2022-05-19 00:36:49 +03:00
// Edge case for when the reference is inside the title or the description of the referring issue
2022-06-20 13:02:49 +03:00
if c . RefCommentID == 0 {
2023-09-29 15:12:54 +03:00
return c . RefIssueLink ( ctx )
2021-05-11 23:43:35 +03:00
}
2023-09-29 15:12:54 +03:00
if err := c . LoadRefComment ( ctx ) ; err != nil { // Silently dropping errors :unamused:
2022-06-20 13:02:49 +03:00
log . Error ( "LoadRefComment(%d): %v" , c . RefCommentID , err )
2019-09-20 08:45:38 +03:00
return ""
}
2023-09-29 15:12:54 +03:00
return c . RefComment . Link ( ctx )
2019-09-20 08:45:38 +03:00
}
2023-02-09 19:31:30 +03:00
// RefIssueLink returns the relative URL of the issue where this reference was created
2023-09-29 15:12:54 +03:00
func ( c * Comment ) RefIssueLink ( ctx context . Context ) string {
if err := c . LoadRefIssue ( ctx ) ; err != nil { // Silently dropping errors :unamused:
2022-06-20 13:02:49 +03:00
log . Error ( "LoadRefIssue(%d): %v" , c . RefCommentID , err )
2019-09-20 08:45:38 +03:00
return ""
}
2023-02-09 19:31:30 +03:00
return c . RefIssue . Link ( )
2019-09-20 08:45:38 +03:00
}
// RefIssueTitle returns the title of the issue where this reference was created
2023-09-29 15:12:54 +03:00
func ( c * Comment ) RefIssueTitle ( ctx context . Context ) string {
if err := c . LoadRefIssue ( ctx ) ; err != nil { // Silently dropping errors :unamused:
2022-06-20 13:02:49 +03:00
log . Error ( "LoadRefIssue(%d): %v" , c . RefCommentID , err )
2019-09-20 08:45:38 +03:00
return ""
}
2022-06-20 13:02:49 +03:00
return c . RefIssue . Title
2019-09-20 08:45:38 +03:00
}
// RefIssueIdent returns the user friendly identity (e.g. "#1234") of the issue where this reference was created
2023-09-29 15:12:54 +03:00
func ( c * Comment ) RefIssueIdent ( ctx context . Context ) string {
if err := c . LoadRefIssue ( ctx ) ; err != nil { // Silently dropping errors :unamused:
2022-06-20 13:02:49 +03:00
log . Error ( "LoadRefIssue(%d): %v" , c . RefCommentID , err )
2019-09-20 08:45:38 +03:00
return ""
}
// FIXME: check this name for cross-repository references (#7901 if it gets merged)
2022-06-20 13:02:49 +03:00
return fmt . Sprintf ( "#%d" , c . RefIssue . Index )
2019-09-20 08:45:38 +03:00
}
2019-11-18 16:13:07 +03:00
// __________ .__ .__ __________ __
// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_
// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
// | | | | / |_| |_| | \ ___< <_| | | /\ ___/ \___ \ | |
// |____| |____/|____/____/____|_ /\___ >__ |____/ \___ >____ > |__|
// \/ \/ |__| \/ \/
// ResolveCrossReferences will return the list of references to close/reopen by this PR
2022-05-03 22:46:28 +03:00
func ( pr * PullRequest ) ResolveCrossReferences ( ctx context . Context ) ( [ ] * Comment , error ) {
2019-11-18 16:13:07 +03:00
unfiltered := make ( [ ] * Comment , 0 , 5 )
2022-05-03 22:46:28 +03:00
if err := db . GetEngine ( ctx ) .
2019-11-18 16:13:07 +03:00
Where ( "ref_repo_id = ? AND ref_issue_id = ?" , pr . Issue . RepoID , pr . Issue . ID ) .
In ( "ref_action" , [ ] references . XRefAction { references . XRefActionCloses , references . XRefActionReopens } ) .
OrderBy ( "id" ) .
Find ( & unfiltered ) ; err != nil {
2022-10-24 22:29:17 +03:00
return nil , fmt . Errorf ( "get reference: %w" , err )
2019-11-18 16:13:07 +03:00
}
refs := make ( [ ] * Comment , 0 , len ( unfiltered ) )
for _ , ref := range unfiltered {
found := false
for i , r := range refs {
if r . IssueID == ref . IssueID {
// Keep only the latest
refs [ i ] = ref
found = true
break
}
}
if ! found {
refs = append ( refs , ref )
}
}
return refs , nil
}