code.gitea.io/gitea@v1.22.3/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 if user_model.IsUserBlockedBy(stdCtx, ctx.Doer, refIssue.PosterID, refIssue.Repo.OwnerID) { 218 return nil, references.XRefActionNone, nil 219 } 220 221 // Accept close/reopening actions only if the poster is able to close the 222 // referenced issue manually at this moment. The only exception is 223 // the poster of a new PR referencing an issue on the same repo: then the merger 224 // should be responsible for checking whether the reference should resolve. 225 if ref.Action != references.XRefActionNone && 226 ctx.Doer.ID != refIssue.PosterID && 227 !perm.CanWriteIssuesOrPulls(refIssue.IsPull) && 228 (refIssue.RepoID != ctx.OrigIssue.RepoID || ctx.OrigComment != nil) { 229 refAction = references.XRefActionNone 230 } 231 } 232 233 return refIssue, refAction, nil 234 } 235 236 // AddCrossReferences add cross references 237 func (c *Comment) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error { 238 if c.Type != CommentTypeCode && c.Type != CommentTypeComment { 239 return nil 240 } 241 if err := c.LoadIssue(stdCtx); err != nil { 242 return err 243 } 244 ctx := &crossReferencesContext{ 245 Type: CommentTypeCommentRef, 246 Doer: doer, 247 OrigIssue: c.Issue, 248 OrigComment: c, 249 RemoveOld: removeOld, 250 } 251 return c.Issue.createCrossReferences(stdCtx, ctx, "", c.Content) 252 } 253 254 func (c *Comment) neuterCrossReferences(ctx context.Context) error { 255 return neuterCrossReferences(ctx, c.IssueID, c.ID) 256 } 257 258 // LoadRefComment loads comment that created this reference from database 259 func (c *Comment) LoadRefComment(ctx context.Context) (err error) { 260 if c.RefComment != nil { 261 return nil 262 } 263 c.RefComment, err = GetCommentByID(ctx, c.RefCommentID) 264 return err 265 } 266 267 // LoadRefIssue loads comment that created this reference from database 268 func (c *Comment) LoadRefIssue(ctx context.Context) (err error) { 269 if c.RefIssue != nil { 270 return nil 271 } 272 c.RefIssue, err = GetIssueByID(ctx, c.RefIssueID) 273 if err == nil { 274 err = c.RefIssue.LoadRepo(ctx) 275 } 276 return err 277 } 278 279 // CommentTypeIsRef returns true if CommentType is a reference from another issue 280 func CommentTypeIsRef(t CommentType) bool { 281 return t == CommentTypeCommentRef || t == CommentTypePullRef || t == CommentTypeIssueRef 282 } 283 284 // RefCommentLink returns the relative URL for the comment that created this reference 285 func (c *Comment) RefCommentLink(ctx context.Context) string { 286 // Edge case for when the reference is inside the title or the description of the referring issue 287 if c.RefCommentID == 0 { 288 return c.RefIssueLink(ctx) 289 } 290 if err := c.LoadRefComment(ctx); err != nil { // Silently dropping errors :unamused: 291 log.Error("LoadRefComment(%d): %v", c.RefCommentID, err) 292 return "" 293 } 294 return c.RefComment.Link(ctx) 295 } 296 297 // RefIssueLink returns the relative URL of the issue where this reference was created 298 func (c *Comment) RefIssueLink(ctx context.Context) string { 299 if err := c.LoadRefIssue(ctx); err != nil { // Silently dropping errors :unamused: 300 log.Error("LoadRefIssue(%d): %v", c.RefCommentID, err) 301 return "" 302 } 303 return c.RefIssue.Link() 304 } 305 306 // RefIssueTitle returns the title of the issue where this reference was created 307 func (c *Comment) RefIssueTitle(ctx context.Context) string { 308 if err := c.LoadRefIssue(ctx); err != nil { // Silently dropping errors :unamused: 309 log.Error("LoadRefIssue(%d): %v", c.RefCommentID, err) 310 return "" 311 } 312 return c.RefIssue.Title 313 } 314 315 // RefIssueIdent returns the user friendly identity (e.g. "#1234") of the issue where this reference was created 316 func (c *Comment) RefIssueIdent(ctx context.Context) string { 317 if err := c.LoadRefIssue(ctx); err != nil { // Silently dropping errors :unamused: 318 log.Error("LoadRefIssue(%d): %v", c.RefCommentID, err) 319 return "" 320 } 321 // FIXME: check this name for cross-repository references (#7901 if it gets merged) 322 return fmt.Sprintf("#%d", c.RefIssue.Index) 323 } 324 325 // __________ .__ .__ __________ __ 326 // \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_ 327 // | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\ 328 // | | | | / |_| |_| | \ ___< <_| | | /\ ___/ \___ \ | | 329 // |____| |____/|____/____/____|_ /\___ >__ |____/ \___ >____ > |__| 330 // \/ \/ |__| \/ \/ 331 332 // ResolveCrossReferences will return the list of references to close/reopen by this PR 333 func (pr *PullRequest) ResolveCrossReferences(ctx context.Context) ([]*Comment, error) { 334 unfiltered := make([]*Comment, 0, 5) 335 if err := db.GetEngine(ctx). 336 Where("ref_repo_id = ? AND ref_issue_id = ?", pr.Issue.RepoID, pr.Issue.ID). 337 In("ref_action", []references.XRefAction{references.XRefActionCloses, references.XRefActionReopens}). 338 OrderBy("id"). 339 Find(&unfiltered); err != nil { 340 return nil, fmt.Errorf("get reference: %w", err) 341 } 342 343 refs := make([]*Comment, 0, len(unfiltered)) 344 for _, ref := range unfiltered { 345 found := false 346 for i, r := range refs { 347 if r.IssueID == ref.IssueID { 348 // Keep only the latest 349 refs[i] = ref 350 found = true 351 break 352 } 353 } 354 if !found { 355 refs = append(refs, ref) 356 } 357 } 358 359 return refs, nil 360 }