You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue_xref.go 11KB

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