mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-24 03:21:49 +01:00
Close/reopen issues by keywords in titles and comments (#8866)
* Add close/reopen from comment functionality * Fix comment * Rewrite closing/reopening template * Check xref permissions, move action to services/pull * Fix RefIsPull field * Add xref tests * Fix xref unique filter * Only highlight keywords for actionable xrefs * Fix xref neuter filter * Fix check return status * Restart CI
This commit is contained in:
parent
08ae6bb7ed
commit
b15f26b1cf
7 changed files with 300 additions and 46 deletions
|
@ -5,6 +5,8 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/references"
|
"code.gitea.io/gitea/modules/references"
|
||||||
|
|
||||||
|
@ -27,13 +29,9 @@ type crossReferencesContext struct {
|
||||||
|
|
||||||
func neuterCrossReferences(e Engine, issueID int64, commentID int64) error {
|
func neuterCrossReferences(e Engine, issueID int64, commentID int64) error {
|
||||||
active := make([]*Comment, 0, 10)
|
active := make([]*Comment, 0, 10)
|
||||||
sess := e.Where("`ref_action` IN (?, ?, ?)", references.XRefActionNone, references.XRefActionCloses, references.XRefActionReopens)
|
sess := e.Where("`ref_action` IN (?, ?, ?)", references.XRefActionNone, references.XRefActionCloses, references.XRefActionReopens).
|
||||||
if issueID != 0 {
|
And("`ref_issue_id` = ?", issueID).
|
||||||
sess = sess.And("`ref_issue_id` = ?", issueID)
|
And("`ref_comment_id` = ?", commentID)
|
||||||
}
|
|
||||||
if commentID != 0 {
|
|
||||||
sess = sess.And("`ref_comment_id` = ?", commentID)
|
|
||||||
}
|
|
||||||
if err := sess.Find(&active); err != nil || len(active) == 0 {
|
if err := sess.Find(&active); err != nil || len(active) == 0 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -87,7 +85,7 @@ func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesC
|
||||||
RefIssueID: ctx.OrigIssue.ID,
|
RefIssueID: ctx.OrigIssue.ID,
|
||||||
RefCommentID: refCommentID,
|
RefCommentID: refCommentID,
|
||||||
RefAction: xref.Action,
|
RefAction: xref.Action,
|
||||||
RefIsPull: xref.Issue.IsPull,
|
RefIsPull: ctx.OrigIssue.IsPull,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -98,9 +96,10 @@ func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesC
|
||||||
func (issue *Issue) getCrossReferences(e *xorm.Session, ctx *crossReferencesContext, plaincontent, mdcontent string) ([]*crossReference, error) {
|
func (issue *Issue) getCrossReferences(e *xorm.Session, ctx *crossReferencesContext, plaincontent, mdcontent string) ([]*crossReference, error) {
|
||||||
xreflist := make([]*crossReference, 0, 5)
|
xreflist := make([]*crossReference, 0, 5)
|
||||||
var (
|
var (
|
||||||
refRepo *Repository
|
refRepo *Repository
|
||||||
refIssue *Issue
|
refIssue *Issue
|
||||||
err error
|
refAction references.XRefAction
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
allrefs := append(references.FindAllIssueReferences(plaincontent), references.FindAllIssueReferencesMarkdown(mdcontent)...)
|
allrefs := append(references.FindAllIssueReferences(plaincontent), references.FindAllIssueReferencesMarkdown(mdcontent)...)
|
||||||
|
@ -122,15 +121,13 @@ func (issue *Issue) getCrossReferences(e *xorm.Session, ctx *crossReferencesCont
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if refIssue, err = ctx.OrigIssue.findReferencedIssue(e, ctx, refRepo, ref.Index); err != nil {
|
if refIssue, refAction, err = ctx.OrigIssue.verifyReferencedIssue(e, ctx, refRepo, ref); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if refIssue != nil {
|
if refIssue != nil {
|
||||||
xreflist = ctx.OrigIssue.updateCrossReferenceList(xreflist, &crossReference{
|
xreflist = ctx.OrigIssue.updateCrossReferenceList(xreflist, &crossReference{
|
||||||
Issue: refIssue,
|
Issue: refIssue,
|
||||||
// FIXME: currently ignore keywords
|
Action: refAction,
|
||||||
// Action: ref.Action,
|
|
||||||
Action: references.XRefActionNone,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,25 +150,42 @@ func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *cross
|
||||||
return append(list, xref)
|
return append(list, xref)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issue *Issue) findReferencedIssue(e Engine, ctx *crossReferencesContext, repo *Repository, index int64) (*Issue, error) {
|
// verifyReferencedIssue will check if the referenced issue exists, and whether the doer has permission to do what
|
||||||
refIssue := &Issue{RepoID: repo.ID, Index: index}
|
func (issue *Issue) verifyReferencedIssue(e Engine, ctx *crossReferencesContext, repo *Repository,
|
||||||
|
ref references.IssueReference) (*Issue, references.XRefAction, error) {
|
||||||
|
|
||||||
|
refIssue := &Issue{RepoID: repo.ID, Index: ref.Index}
|
||||||
|
refAction := ref.Action
|
||||||
|
|
||||||
if has, _ := e.Get(refIssue); !has {
|
if has, _ := e.Get(refIssue); !has {
|
||||||
return nil, nil
|
return nil, references.XRefActionNone, nil
|
||||||
}
|
}
|
||||||
if err := refIssue.loadRepo(e); err != nil {
|
if err := refIssue.loadRepo(e); err != nil {
|
||||||
return nil, err
|
return nil, references.XRefActionNone, err
|
||||||
}
|
}
|
||||||
// Check user permissions
|
|
||||||
if refIssue.RepoID != ctx.OrigIssue.RepoID {
|
// 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 {
|
||||||
perm, err := getUserRepoPermission(e, refIssue.Repo, ctx.Doer)
|
perm, err := getUserRepoPermission(e, refIssue.Repo, ctx.Doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, references.XRefActionNone, err
|
||||||
}
|
}
|
||||||
if !perm.CanReadIssuesOrPulls(refIssue.IsPull) {
|
if !perm.CanReadIssuesOrPulls(refIssue.IsPull) {
|
||||||
return nil, nil
|
return nil, references.XRefActionNone, nil
|
||||||
|
}
|
||||||
|
if ref.Action != references.XRefActionNone &&
|
||||||
|
ctx.Doer.ID != refIssue.PosterID &&
|
||||||
|
!perm.CanWriteIssuesOrPulls(refIssue.IsPull) {
|
||||||
|
refAction = references.XRefActionNone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return refIssue, nil
|
|
||||||
|
return refIssue, refAction, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issue *Issue) neuterCrossReferences(e Engine) error {
|
func (issue *Issue) neuterCrossReferences(e Engine) error {
|
||||||
|
@ -203,7 +217,7 @@ func (comment *Comment) addCrossReferences(e *xorm.Session, doer *User) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (comment *Comment) neuterCrossReferences(e Engine) error {
|
func (comment *Comment) neuterCrossReferences(e Engine) error {
|
||||||
return neuterCrossReferences(e, 0, comment.ID)
|
return neuterCrossReferences(e, comment.IssueID, comment.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadRefComment loads comment that created this reference from database
|
// LoadRefComment loads comment that created this reference from database
|
||||||
|
@ -268,3 +282,40 @@ func (comment *Comment) RefIssueIdent() string {
|
||||||
// FIXME: check this name for cross-repository references (#7901 if it gets merged)
|
// FIXME: check this name for cross-repository references (#7901 if it gets merged)
|
||||||
return "#" + com.ToStr(comment.RefIssue.Index)
|
return "#" + com.ToStr(comment.RefIssue.Index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// __________ .__ .__ __________ __
|
||||||
|
// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_
|
||||||
|
// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
|
||||||
|
// | | | | / |_| |_| | \ ___< <_| | | /\ ___/ \___ \ | |
|
||||||
|
// |____| |____/|____/____/____|_ /\___ >__ |____/ \___ >____ > |__|
|
||||||
|
// \/ \/ |__| \/ \/
|
||||||
|
|
||||||
|
// ResolveCrossReferences will return the list of references to close/reopen by this PR
|
||||||
|
func (pr *PullRequest) ResolveCrossReferences() ([]*Comment, error) {
|
||||||
|
unfiltered := make([]*Comment, 0, 5)
|
||||||
|
if err := x.
|
||||||
|
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 {
|
||||||
|
return nil, fmt.Errorf("get reference: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
164
models/issue_xref_test.go
Normal file
164
models/issue_xref_test.go
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/references"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestXRef_AddCrossReferences(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
|
// Issue #1 to test against
|
||||||
|
itarget := testCreateIssue(t, 1, 2, "title1", "content1", false)
|
||||||
|
|
||||||
|
// PR to close issue #1
|
||||||
|
content := fmt.Sprintf("content2, closes #%d", itarget.Index)
|
||||||
|
pr := testCreateIssue(t, 1, 2, "title2", content, true)
|
||||||
|
ref := AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: 0}).(*Comment)
|
||||||
|
assert.Equal(t, CommentTypePullRef, ref.Type)
|
||||||
|
assert.Equal(t, pr.RepoID, ref.RefRepoID)
|
||||||
|
assert.Equal(t, true, ref.RefIsPull)
|
||||||
|
assert.Equal(t, references.XRefActionCloses, ref.RefAction)
|
||||||
|
|
||||||
|
// Comment on PR to reopen issue #1
|
||||||
|
content = fmt.Sprintf("content2, reopens #%d", itarget.Index)
|
||||||
|
c := testCreateComment(t, 1, 2, pr.ID, content)
|
||||||
|
ref = AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: c.ID}).(*Comment)
|
||||||
|
assert.Equal(t, CommentTypeCommentRef, ref.Type)
|
||||||
|
assert.Equal(t, pr.RepoID, ref.RefRepoID)
|
||||||
|
assert.Equal(t, true, ref.RefIsPull)
|
||||||
|
assert.Equal(t, references.XRefActionReopens, ref.RefAction)
|
||||||
|
|
||||||
|
// Issue mentioning issue #1
|
||||||
|
content = fmt.Sprintf("content3, mentions #%d", itarget.Index)
|
||||||
|
i := testCreateIssue(t, 1, 2, "title3", content, false)
|
||||||
|
ref = AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
|
||||||
|
assert.Equal(t, CommentTypeIssueRef, ref.Type)
|
||||||
|
assert.Equal(t, pr.RepoID, ref.RefRepoID)
|
||||||
|
assert.Equal(t, false, ref.RefIsPull)
|
||||||
|
assert.Equal(t, references.XRefActionNone, ref.RefAction)
|
||||||
|
|
||||||
|
// Issue #4 to test against
|
||||||
|
itarget = testCreateIssue(t, 3, 3, "title4", "content4", false)
|
||||||
|
|
||||||
|
// Cross-reference to issue #4 by admin
|
||||||
|
content = fmt.Sprintf("content5, mentions user3/repo3#%d", itarget.Index)
|
||||||
|
i = testCreateIssue(t, 2, 1, "title5", content, false)
|
||||||
|
ref = AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
|
||||||
|
assert.Equal(t, CommentTypeIssueRef, ref.Type)
|
||||||
|
assert.Equal(t, i.RepoID, ref.RefRepoID)
|
||||||
|
assert.Equal(t, false, ref.RefIsPull)
|
||||||
|
assert.Equal(t, references.XRefActionNone, ref.RefAction)
|
||||||
|
|
||||||
|
// Cross-reference to issue #4 with no permission
|
||||||
|
content = fmt.Sprintf("content6, mentions user3/repo3#%d", itarget.Index)
|
||||||
|
i = testCreateIssue(t, 4, 5, "title6", content, false)
|
||||||
|
AssertNotExistsBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXRef_NeuterCrossReferences(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
|
// Issue #1 to test against
|
||||||
|
itarget := testCreateIssue(t, 1, 2, "title1", "content1", false)
|
||||||
|
|
||||||
|
// Issue mentioning issue #1
|
||||||
|
title := fmt.Sprintf("title2, mentions #%d", itarget.Index)
|
||||||
|
i := testCreateIssue(t, 1, 2, title, "content2", false)
|
||||||
|
ref := AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
|
||||||
|
assert.Equal(t, CommentTypeIssueRef, ref.Type)
|
||||||
|
assert.Equal(t, references.XRefActionNone, ref.RefAction)
|
||||||
|
|
||||||
|
d := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||||
|
i.Title = "title2, no mentions"
|
||||||
|
assert.NoError(t, i.ChangeTitle(d, title))
|
||||||
|
|
||||||
|
ref = AssertExistsAndLoadBean(t, &Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*Comment)
|
||||||
|
assert.Equal(t, CommentTypeIssueRef, ref.Type)
|
||||||
|
assert.Equal(t, references.XRefActionNeutered, ref.RefAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXRef_ResolveCrossReferences(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
|
d := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||||
|
|
||||||
|
i1 := testCreateIssue(t, 1, 2, "title1", "content1", false)
|
||||||
|
i2 := testCreateIssue(t, 1, 2, "title2", "content2", false)
|
||||||
|
i3 := testCreateIssue(t, 1, 2, "title3", "content3", false)
|
||||||
|
assert.NoError(t, i3.ChangeStatus(d, true))
|
||||||
|
|
||||||
|
pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index))
|
||||||
|
rp := AssertExistsAndLoadBean(t, &Comment{IssueID: i1.ID, RefIssueID: pr.Issue.ID, RefCommentID: 0}).(*Comment)
|
||||||
|
|
||||||
|
c1 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i2.Index))
|
||||||
|
r1 := AssertExistsAndLoadBean(t, &Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c1.ID}).(*Comment)
|
||||||
|
|
||||||
|
// Must be ignored
|
||||||
|
c2 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("mentions #%d", i2.Index))
|
||||||
|
AssertExistsAndLoadBean(t, &Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c2.ID})
|
||||||
|
|
||||||
|
// Must be superseded by c4/r4
|
||||||
|
c3 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("reopens #%d", i3.Index))
|
||||||
|
AssertExistsAndLoadBean(t, &Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c3.ID})
|
||||||
|
|
||||||
|
c4 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i3.Index))
|
||||||
|
r4 := AssertExistsAndLoadBean(t, &Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c4.ID}).(*Comment)
|
||||||
|
|
||||||
|
refs, err := pr.ResolveCrossReferences()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, refs, 3)
|
||||||
|
assert.Equal(t, rp.ID, refs[0].ID, "bad ref rp: %+v", refs[0])
|
||||||
|
assert.Equal(t, r1.ID, refs[1].ID, "bad ref r1: %+v", refs[1])
|
||||||
|
assert.Equal(t, r4.ID, refs[2].ID, "bad ref r4: %+v", refs[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispull bool) *Issue {
|
||||||
|
r := AssertExistsAndLoadBean(t, &Repository{ID: repo}).(*Repository)
|
||||||
|
d := AssertExistsAndLoadBean(t, &User{ID: doer}).(*User)
|
||||||
|
i := &Issue{RepoID: r.ID, PosterID: d.ID, Poster: d, Title: title, Content: content, IsPull: ispull}
|
||||||
|
|
||||||
|
sess := x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
assert.NoError(t, sess.Begin())
|
||||||
|
_, err := sess.SetExpr("`index`", "coalesce(MAX(`index`),0)+1").Where("repo_id=?", repo).Insert(i)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
i, err = getIssueByID(sess, i.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, i.addCrossReferences(sess, d))
|
||||||
|
assert.NoError(t, sess.Commit())
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreatePR(t *testing.T, repo, doer int64, title, content string) *PullRequest {
|
||||||
|
r := AssertExistsAndLoadBean(t, &Repository{ID: repo}).(*Repository)
|
||||||
|
d := AssertExistsAndLoadBean(t, &User{ID: doer}).(*User)
|
||||||
|
i := &Issue{RepoID: r.ID, PosterID: d.ID, Poster: d, Title: title, Content: content, IsPull: true}
|
||||||
|
pr := &PullRequest{HeadRepoID: repo, BaseRepoID: repo, HeadBranch: "head", BaseBranch: "base"}
|
||||||
|
assert.NoError(t, NewPullRequest(r, i, nil, nil, pr, nil))
|
||||||
|
pr.Issue = i
|
||||||
|
return pr
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreateComment(t *testing.T, repo, doer, issue int64, content string) *Comment {
|
||||||
|
d := AssertExistsAndLoadBean(t, &User{ID: doer}).(*User)
|
||||||
|
i := AssertExistsAndLoadBean(t, &Issue{ID: issue}).(*Issue)
|
||||||
|
c := &Comment{Type: CommentTypeComment, PosterID: doer, Poster: d, IssueID: issue, Issue: i, Content: content}
|
||||||
|
|
||||||
|
sess := x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
assert.NoError(t, sess.Begin())
|
||||||
|
_, err := sess.Insert(c)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, c.addCrossReferences(sess, d))
|
||||||
|
assert.NoError(t, sess.Commit())
|
||||||
|
return c
|
||||||
|
}
|
|
@ -659,8 +659,16 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decorate action keywords
|
// Decorate action keywords if actionable
|
||||||
keyword := createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
|
var keyword *html.Node
|
||||||
|
if references.IsXrefActionable(ref.Action) {
|
||||||
|
keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
|
||||||
|
} else {
|
||||||
|
keyword = &html.Node{
|
||||||
|
Type: html.TextNode,
|
||||||
|
Data: node.Data[ref.ActionLocation.Start:ref.ActionLocation.End],
|
||||||
|
}
|
||||||
|
}
|
||||||
spaces := &html.Node{
|
spaces := &html.Node{
|
||||||
Type: html.TextNode,
|
Type: html.TextNode,
|
||||||
Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start],
|
Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start],
|
||||||
|
|
|
@ -350,3 +350,8 @@ func findActionKeywords(content []byte, start int) (XRefAction, *RefSpan) {
|
||||||
}
|
}
|
||||||
return XRefActionNone, nil
|
return XRefActionNone, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsXrefActionable returns true if the xref action is actionable (i.e. produces a result when resolved)
|
||||||
|
func IsXrefActionable(a XRefAction) bool {
|
||||||
|
return a == XRefActionCloses || a == XRefActionReopens
|
||||||
|
}
|
||||||
|
|
|
@ -876,10 +876,13 @@ issues.create_comment = Comment
|
||||||
issues.closed_at = `closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.closed_at = `closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.reopened_at = `reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.reopened_at = `reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.ref_issue_at = `referenced this issue %[1]s`
|
issues.ref_issue_from = `<a href="%[3]s">referenced this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.ref_pull_at = `referenced this pull request %[1]s`
|
issues.ref_pull_from = `<a href="%[3]s">referenced this pull request %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.ref_issue_ext_at = `referenced this issue from %[1]s %[2]s`
|
issues.ref_closing_from = `<a href="%[3]s">referenced a pull request %[4]s that will close this issue</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
issues.ref_pull_ext_at = `referenced this pull request from %[1]s %[2]s`
|
issues.ref_reopening_from = `<a href="%[3]s">referenced a pull request %[4]s that will reopen this issue</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
|
issues.ref_closed_from = `<a href="%[3]s">closed this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
|
issues.ref_reopened_from = `<a href="%[3]s">reopened this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
|
issues.ref_from = `from %[1]s`
|
||||||
issues.poster = Poster
|
issues.poster = Poster
|
||||||
issues.collaborator = Collaborator
|
issues.collaborator = Collaborator
|
||||||
issues.owner = Owner
|
issues.owner = Owner
|
||||||
|
|
|
@ -21,8 +21,10 @@ import (
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/notification"
|
"code.gitea.io/gitea/modules/notification"
|
||||||
|
"code.gitea.io/gitea/modules/references"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
issue_service "code.gitea.io/gitea/services/issue"
|
||||||
|
|
||||||
"github.com/mcuadros/go-version"
|
"github.com/mcuadros/go-version"
|
||||||
)
|
)
|
||||||
|
@ -447,6 +449,26 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
|
||||||
|
|
||||||
notification.NotifyIssueChangeStatus(doer, pr.Issue, true)
|
notification.NotifyIssueChangeStatus(doer, pr.Issue, true)
|
||||||
|
|
||||||
|
// Resolve cross references
|
||||||
|
refs, err := pr.ResolveCrossReferences()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("ResolveCrossReferences: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ref := range refs {
|
||||||
|
if err = ref.LoadIssue(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = ref.Issue.LoadRepo(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
close := (ref.RefAction == references.XRefActionCloses)
|
||||||
|
if err = issue_service.ChangeStatus(ref.Issue, doer, close); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,20 @@
|
||||||
</a>
|
</a>
|
||||||
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.GetDisplayName}}</a> {{$.i18n.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}}</span>
|
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.GetDisplayName}}</a> {{$.i18n.Tr "repo.issues.closed_at" .EventTag $createdStr | Safe}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{else if or (eq .Type 3) (eq .Type 5) (eq .Type 6)}}
|
{{else if eq .Type 3 5 6}}
|
||||||
|
{{ $refFrom:= "" }}
|
||||||
|
{{if ne .RefRepoID .Issue.RepoID}}
|
||||||
|
{{ $refFrom = $.i18n.Tr "repo.issues.ref_from" .RefRepo.FullName }}
|
||||||
|
{{end}}
|
||||||
|
{{ $refTr := "repo.issues.ref_issue_from" }}
|
||||||
|
{{if .Issue.IsPull}}
|
||||||
|
{{ $refTr = "repo.issues.ref_pull_from" }}
|
||||||
|
{{else if eq .RefAction 1 }}
|
||||||
|
{{ $refTr = "repo.issues.ref_closing_from" }}
|
||||||
|
{{else if eq .RefAction 2 }}
|
||||||
|
{{ $refTr = "repo.issues.ref_reopening_from" }}
|
||||||
|
{{end}}
|
||||||
|
{{ $createdStr:= TimeSinceUnix .CreatedUnix $.Lang }}
|
||||||
<div class="event" id="{{.HashTag}}">
|
<div class="event" id="{{.HashTag}}">
|
||||||
<span class="octicon octicon-bookmark"></span>
|
<span class="octicon octicon-bookmark"></span>
|
||||||
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
|
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
|
||||||
|
@ -96,19 +109,7 @@
|
||||||
</a>
|
</a>
|
||||||
{{if eq .RefAction 3}}<del>{{end}}
|
{{if eq .RefAction 3}}<del>{{end}}
|
||||||
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.GetDisplayName}}</a>
|
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.GetDisplayName}}</a>
|
||||||
{{if (eq .RefRepoID .Issue.RepoID)}}
|
{{$.i18n.Tr $refTr .EventTag $createdStr .RefCommentHTMLURL $refFrom | Safe}}
|
||||||
{{if .Issue.IsPull}}
|
|
||||||
<a href="{{.RefCommentHTMLURL}}">{{$.i18n.Tr "repo.issues.ref_pull_at" $createdStr | Safe}}</a>
|
|
||||||
{{else}}
|
|
||||||
<a href="{{.RefCommentHTMLURL}}">{{$.i18n.Tr "repo.issues.ref_issue_at" $createdStr | Safe}}</a>
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
|
||||||
{{if .Issue.IsPull}}
|
|
||||||
<a href="{{.RefCommentHTMLURL}}">{{$.i18n.Tr "repo.issues.ref_pull_ext_at" .RefRepo.FullName $createdStr | Safe}}</a>
|
|
||||||
{{else}}
|
|
||||||
<a href="{{.RefCommentHTMLURL}}">{{$.i18n.Tr "repo.issues.ref_issue_ext_at" .RefRepo.FullName $createdStr | Safe}}</a>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
</span>
|
</span>
|
||||||
{{if eq .RefAction 3}}</del>{{end}}
|
{{if eq .RefAction 3}}</del>{{end}}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue