code.gitea.io/gitea@v1.21.7/models/issues/issue_xref.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package issues 5 6 import ( 7 "context" 8 "fmt" 9 10 "code.gitea.io/gitea/models/db" 11 access_model "code.gitea.io/gitea/models/perm/access" 12 repo_model "code.gitea.io/gitea/models/repo" 13 user_model "code.gitea.io/gitea/models/user" 14 "code.gitea.io/gitea/modules/log" 15 "code.gitea.io/gitea/modules/references" 16 ) 17 18 type crossReference struct { 19 Issue *Issue 20 Action references.XRefAction 21 } 22 23 // crossReferencesContext is context to pass along findCrossReference functions 24 type crossReferencesContext struct { 25 Type CommentType 26 Doer *user_model.User 27 OrigIssue *Issue 28 OrigComment *Comment 29 RemoveOld bool 30 } 31 32 func findOldCrossReferences(ctx context.Context, issueID, commentID int64) ([]*Comment, error) { 33 active := make([]*Comment, 0, 10) 34 return active, db.GetEngine(ctx).Where("`ref_action` IN (?, ?, ?)", references.XRefActionNone, references.XRefActionCloses, references.XRefActionReopens). 35 And("`ref_issue_id` = ?", issueID). 36 And("`ref_comment_id` = ?", commentID). 37 Find(&active) 38 } 39 40 func neuterCrossReferences(ctx context.Context, issueID, commentID int64) error { 41 active, err := findOldCrossReferences(ctx, issueID, commentID) 42 if err != nil { 43 return err 44 } 45 ids := make([]int64, len(active)) 46 for i, c := range active { 47 ids[i] = c.ID 48 } 49 return neuterCrossReferencesIds(ctx, ids) 50 } 51 52 func neuterCrossReferencesIds(ctx context.Context, ids []int64) error { 53 _, err := db.GetEngine(ctx).In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: references.XRefActionNeutered}) 54 return err 55 } 56 57 // AddCrossReferences add cross repositories references. 58 func (issue *Issue) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error { 59 var commentType CommentType 60 if issue.IsPull { 61 commentType = CommentTypePullRef 62 } else { 63 commentType = CommentTypeIssueRef 64 } 65 ctx := &crossReferencesContext{ 66 Type: commentType, 67 Doer: doer, 68 OrigIssue: issue, 69 RemoveOld: removeOld, 70 } 71 return issue.createCrossReferences(stdCtx, ctx, issue.Title, issue.Content) 72 } 73 74 func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossReferencesContext, plaincontent, mdcontent string) error { 75 xreflist, err := ctx.OrigIssue.getCrossReferences(stdCtx, ctx, plaincontent, mdcontent) 76 if err != nil { 77 return err 78 } 79 if ctx.RemoveOld { 80 var commentID int64 81 if ctx.OrigComment != nil { 82 commentID = ctx.OrigComment.ID 83 } 84 active, err := findOldCrossReferences(stdCtx, ctx.OrigIssue.ID, commentID) 85 if err != nil { 86 return err 87 } 88 ids := make([]int64, 0, len(active)) 89 for _, c := range active { 90 found := false 91 for i, x := range xreflist { 92 if x.Issue.ID == c.IssueID && x.Action == c.RefAction { 93 found = true 94 xreflist = append(xreflist[:i], xreflist[i+1:]...) 95 break 96 } 97 } 98 if !found { 99 ids = append(ids, c.ID) 100 } 101 } 102 if len(ids) > 0 { 103 if err = neuterCrossReferencesIds(stdCtx, ids); err != nil { 104 return err 105 } 106 } 107 } 108 for _, xref := range xreflist { 109 var refCommentID int64 110 if ctx.OrigComment != nil { 111 refCommentID = ctx.OrigComment.ID 112 } 113 opts := &CreateCommentOptions{ 114 Type: ctx.Type, 115 Doer: ctx.Doer, 116 Repo: xref.Issue.Repo, 117 Issue: xref.Issue, 118 RefRepoID: ctx.OrigIssue.RepoID, 119 RefIssueID: ctx.OrigIssue.ID, 120 RefCommentID: refCommentID, 121 RefAction: xref.Action, 122 RefIsPull: ctx.OrigIssue.IsPull, 123 } 124 _, err := CreateComment(stdCtx, opts) 125 if err != nil { 126 return err 127 } 128 } 129 return nil 130 } 131 132 func (issue *Issue) getCrossReferences(stdCtx context.Context, ctx *crossReferencesContext, plaincontent, mdcontent string) ([]*crossReference, error) { 133 xreflist := make([]*crossReference, 0, 5) 134 var ( 135 refRepo *repo_model.Repository 136 refIssue *Issue 137 refAction references.XRefAction 138 err error 139 ) 140 141 allrefs := append(references.FindAllIssueReferences(plaincontent), references.FindAllIssueReferencesMarkdown(mdcontent)...) 142 for _, ref := range allrefs { 143 if ref.Owner == "" && ref.Name == "" { 144 // Issues in the same repository 145 if err := ctx.OrigIssue.LoadRepo(stdCtx); err != nil { 146 return nil, err 147 } 148 refRepo = ctx.OrigIssue.Repo 149 } else { 150 // Issues in other repositories 151 refRepo, err = repo_model.GetRepositoryByOwnerAndName(stdCtx, ref.Owner, ref.Name) 152 if err != nil { 153 if repo_model.IsErrRepoNotExist(err) { 154 continue 155 } 156 return nil, err 157 } 158 } 159 if refIssue, refAction, err = ctx.OrigIssue.verifyReferencedIssue(stdCtx, ctx, refRepo, ref); err != nil { 160 return nil, err 161 } 162 if refIssue != nil { 163 xreflist = ctx.OrigIssue.updateCrossReferenceList(xreflist, &crossReference{ 164 Issue: refIssue, 165 Action: refAction, 166 }) 167 } 168 } 169 170 return xreflist, nil 171 } 172 173 func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *crossReference) []*crossReference { 174 if xref.Issue.ID == issue.ID { 175 return list 176 } 177 for i, r := range list { 178 if r.Issue.ID == xref.Issue.ID { 179 if xref.Action != references.XRefActionNone { 180 list[i].Action = xref.Action 181 } 182 return list 183 } 184 } 185 return append(list, xref) 186 } 187 188 // verifyReferencedIssue will check if the referenced issue exists, and whether the doer has permission to do what 189 func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossReferencesContext, repo *repo_model.Repository, 190 ref references.IssueReference, 191 ) (*Issue, references.XRefAction, error) { 192 refIssue := &Issue{RepoID: repo.ID, Index: ref.Index} 193 refAction := ref.Action 194 e := db.GetEngine(stdCtx) 195 196 if has, _ := e.Get(refIssue); !has { 197 return nil, references.XRefActionNone, nil 198 } 199 if err := refIssue.LoadRepo(stdCtx); err != nil { 200 return nil, references.XRefActionNone, err 201 } 202 203 // Close/reopen actions can only be set from pull requests to issues 204 if refIssue.IsPull || !issue.IsPull { 205 refAction = references.XRefActionNone 206 } 207 208 // Check doer permissions; set action to None if the doer can't change the destination 209 if refIssue.RepoID != ctx.OrigIssue.RepoID || ref.Action != references.XRefActionNone { 210 perm, err := access_model.GetUserRepoPermission(stdCtx, refIssue.Repo, ctx.Doer) 211 if err != nil { 212 return nil, references.XRefActionNone, err 213 } 214 if !perm.CanReadIssuesOrPulls(refIssue.IsPull) { 215 return nil, references.XRefActionNone, nil 216 } 217 // Accept close/reopening actions only if the poster is able to close the 218 // referenced issue manually at this moment. The only exception is 219 // the poster of a new PR referencing an issue on the same repo: then the merger 220 // should be responsible for checking whether the reference should resolve. 221 if ref.Action != references.XRefActionNone && 222 ctx.Doer.ID != refIssue.PosterID && 223 !perm.CanWriteIssuesOrPulls(refIssue.IsPull) && 224 (refIssue.RepoID != ctx.OrigIssue.RepoID || ctx.OrigComment != nil) { 225 refAction = references.XRefActionNone 226 } 227 } 228 229 return refIssue, refAction, nil 230 } 231 232 // AddCrossReferences add cross references 233 func (c *Comment) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error { 234 if c.Type != CommentTypeCode && c.Type != CommentTypeComment { 235 return nil 236 } 237 if err := c.LoadIssue(stdCtx); err != nil { 238 return err 239 } 240 ctx := &crossReferencesContext{ 241 Type: CommentTypeCommentRef, 242 Doer: doer, 243 OrigIssue: c.Issue, 244 OrigComment: c, 245 RemoveOld: removeOld, 246 } 247 return c.Issue.createCrossReferences(stdCtx, ctx, "", c.Content) 248 } 249 250 func (c *Comment) neuterCrossReferences(ctx context.Context) error { 251 return neuterCrossReferences(ctx, c.IssueID, c.ID) 252 } 253 254 // LoadRefComment loads comment that created this reference from database 255 func (c *Comment) LoadRefComment(ctx context.Context) (err error) { 256 if c.RefComment != nil { 257 return nil 258 } 259 c.RefComment, err = GetCommentByID(ctx, c.RefCommentID) 260 return err 261 } 262 263 // LoadRefIssue loads comment that created this reference from database 264 func (c *Comment) LoadRefIssue(ctx context.Context) (err error) { 265 if c.RefIssue != nil { 266 return nil 267 } 268 c.RefIssue, err = GetIssueByID(ctx, c.RefIssueID) 269 if err == nil { 270 err = c.RefIssue.LoadRepo(ctx) 271 } 272 return err 273 } 274 275 // CommentTypeIsRef returns true if CommentType is a reference from another issue 276 func CommentTypeIsRef(t CommentType) bool { 277 return t == CommentTypeCommentRef || t == CommentTypePullRef || t == CommentTypeIssueRef 278 } 279 280 // RefCommentLink returns the relative URL for the comment that created this reference 281 func (c *Comment) RefCommentLink(ctx context.Context) string { 282 // Edge case for when the reference is inside the title or the description of the referring issue 283 if c.RefCommentID == 0 { 284 return c.RefIssueLink(ctx) 285 } 286 if err := c.LoadRefComment(ctx); err != nil { // Silently dropping errors :unamused: 287 log.Error("LoadRefComment(%d): %v", c.RefCommentID, err) 288 return "" 289 } 290 return c.RefComment.Link(ctx) 291 } 292 293 // RefIssueLink returns the relative URL of the issue where this reference was created 294 func (c *Comment) RefIssueLink(ctx context.Context) string { 295 if err := c.LoadRefIssue(ctx); err != nil { // Silently dropping errors :unamused: 296 log.Error("LoadRefIssue(%d): %v", c.RefCommentID, err) 297 return "" 298 } 299 return c.RefIssue.Link() 300 } 301 302 // RefIssueTitle returns the title of the issue where this reference was created 303 func (c *Comment) RefIssueTitle(ctx context.Context) string { 304 if err := c.LoadRefIssue(ctx); err != nil { // Silently dropping errors :unamused: 305 log.Error("LoadRefIssue(%d): %v", c.RefCommentID, err) 306 return "" 307 } 308 return c.RefIssue.Title 309 } 310 311 // RefIssueIdent returns the user friendly identity (e.g. "#1234") of the issue where this reference was created 312 func (c *Comment) RefIssueIdent(ctx context.Context) string { 313 if err := c.LoadRefIssue(ctx); err != nil { // Silently dropping errors :unamused: 314 log.Error("LoadRefIssue(%d): %v", c.RefCommentID, err) 315 return "" 316 } 317 // FIXME: check this name for cross-repository references (#7901 if it gets merged) 318 return fmt.Sprintf("#%d", c.RefIssue.Index) 319 } 320 321 // __________ .__ .__ __________ __ 322 // \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_ 323 // | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\ 324 // | | | | / |_| |_| | \ ___< <_| | | /\ ___/ \___ \ | | 325 // |____| |____/|____/____/____|_ /\___ >__ |____/ \___ >____ > |__| 326 // \/ \/ |__| \/ \/ 327 328 // ResolveCrossReferences will return the list of references to close/reopen by this PR 329 func (pr *PullRequest) ResolveCrossReferences(ctx context.Context) ([]*Comment, error) { 330 unfiltered := make([]*Comment, 0, 5) 331 if err := db.GetEngine(ctx). 332 Where("ref_repo_id = ? AND ref_issue_id = ?", pr.Issue.RepoID, pr.Issue.ID). 333 In("ref_action", []references.XRefAction{references.XRefActionCloses, references.XRefActionReopens}). 334 OrderBy("id"). 335 Find(&unfiltered); err != nil { 336 return nil, fmt.Errorf("get reference: %w", err) 337 } 338 339 refs := make([]*Comment, 0, len(unfiltered)) 340 for _, ref := range unfiltered { 341 found := false 342 for i, r := range refs { 343 if r.IssueID == ref.IssueID { 344 // Keep only the latest 345 refs[i] = ref 346 found = true 347 break 348 } 349 } 350 if !found { 351 refs = append(refs, ref) 352 } 353 } 354 355 return refs, nil 356 }