Alternate syntax for cross references (#9116)

* Add support for local vs. remote xrefs

* Add doc for references

* Docs: fix cases not currently supported

* One more doc fix

* Doc: mentions for teams and orgs

* Change !num ref concept, no change in functionality

* Fix test

* Improve table of issue reference types

* Fix paragraph mark
tokarchuk/v1.17
guillep2k 5 years ago committed by Lauris BH
parent 2011a5b818
commit 6a90c7e3dd
  1. 173
      docs/content/doc/usage/linked-references.en-us.md
  2. 36
      modules/markup/html.go
  3. 58
      modules/markup/html_internal_test.go
  4. 55
      modules/references/references.go
  5. 52
      modules/references/references_test.go

@ -0,0 +1,173 @@
---
date: "2019-11-21T17:00:00-03:00"
title: "Usage: Automatically Linked References"
slug: "automatically-linked-references"
weight: 15
toc: true
draft: false
menu:
sidebar:
parent: "usage"
name: "Automatically Linked References"
weight: 15
identifier: "automatically-linked-references"
---
# Automatically Linked References in Issues, Pull Requests and Commit Messages
When an issue, pull request or comment is posted, the text description is parsed
in search for references. These references will be shown as links in the Issue View
and, in some cases, produce certain _actions_.
Likewise, commit messages are parsed when they are listed, and _actions_
are can be triggered when they are pushed to the main branch.
To prevent the creation of unintended references, there are certain rules
for them to be recognized. For example, they should not be included inside code
text. They should also be reasonably cleared from their surrounding text
(for example, using spaces).
## User, Team and Organization Mentions
When a text in the form `@username` is found and `username` matches the name
of an existing user, a _mention_ reference is created. This will be shown
by changing the text into a link to said user's profile, and possibly create
a notification for the mentioned user depending on whether they have
the necessary permission to access the contents.
Example:
> [@John](#), can you give this a look?
This is also valid for teams and organizations:
> [@Documenters](#), we need to plan for this.
> [@CoolCompanyInc](#), this issue concerns us all!
Teams will receive mail notifications when appropriate, but whole organizations won't.
Commit messages do not produce user notifications.
## Commits
Commits can be referenced using their SHA1 hash, or a portion of it of
at least seven characters. They will be shown as a link to the corresponding
commit.
Example:
> This bug was introduced in [e59ff077](#)
## Issues and Pull Requests
A reference to another issue or pull request can be created using the simple
notation `#1234`, where _1234_ is the number of an issue or pull request
in the same repository. These references will be shown as links to the
referenced content.
The effect of creating this type of reference is that a _notice_ will be
created in the referenced document, provided the creator of the reference
has reading permissions on it.
Example:
> This seems related to [#1234](#)
Issues and pull requests in other repositories can be referred to as well
using the form `owner/repository#1234`:
> This seems related to [mike/compiler#1234](#)
Alternatively, the `!1234` notation can be used as well. Even when in Gitea
a pull request is a form of issue, the `#1234` form will always link to
an issue; if the linked entry happens to be a pull request instead, Gitea
will redirect as appropriate. With the `!1234` notation, a pull request
link will be created, which will be redirected to an issue if required.
However, this distinction could be important if an external tracker is
used, where links to issues and pull requests are not interchangeable.
## Actionable References in Pull Requests and Commit Messages
Sometimes a commit or pull request may fix or bring back a problem documented
in a particular issue. Gitea supports closing and reopening the referenced
issues by preceding the reference with a particular _keyword_. Common keywords
include "closes", "fixes", "reopens", etc. This list can be
[customized]({{< ref "/doc/advanced/config-cheat-sheet.en-us.md" >}}) by the
site administrator.
Example:
> This PR _closes_ [#1234](#)
If the actionable reference is accepted, this will create a notice on the
referenced issue announcing that it will be closed when the referencing PR
is merged.
For an actionable reference to be accepted, _at least one_ of the following
conditions must be met:
* The commenter has permissions to close or reopen the issue at the moment
of creating the reference.
* The reference is inside a commit message.
* The reference is posted as part of the pull request description.
In the last case, the issue will be closed or reopened only if the merger
of the pull request has permissions to do so.
Additionally, only pull requests and commit messages can create an action,
and only issues can be closed or reopened this way.
The default _keywords_ are:
* **Closing**: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved
* **Reopening**: reopen, reopens, reopened
## External Trackers
Gitea supports the use of external issue trackers, and references to issues
hosted externally can be created in pull requests. However, if the external
tracker uses numbers to identify issues, they will be indistinguishable from
the pull requests hosted in Gitea. To address this, Gitea allows the use of
the `!` marker to identify pull requests. For example:
> This is issue [#1234](#), and links to the external tracker.
> This is pull request [!1234](#), and links to a pull request in Gitea.
The `!` and `#` can be used interchangeably for issues and pull request _except_
for this case, where a distinction is required.
## Issues and Pull Requests References Summary
This table illustrates the different kinds of cross-reference for issues and pull requests.
In the examples, `User1/Repo1` refers to the repository where the reference is used, while
`UserZ/RepoZ` indicates a different repository.
| Reference in User1/Repo1 | Repo1 issues are external | RepoZ issues are external | Should render |
|---------------------------|:-------------------------:|:-------------------------:|----------------------------------|
| `#1234` | no | N/A | A link to issue/pull 1234 in `User1/Repo1` |
| `!1234` | no | N/A | A link to issue/pull 1234 in `User1/Repo1` |
| `#1234` | yes | N/A | A link to _external issue_ 1234 for `User1/Repo1` |
| `!1234` | yes | N/A | A link to _PR_ 1234 for `User1/Repo1` |
| `User1/Repo1#1234` | no | N/A | A link to issue/pull 1234 in `User1/Repo1` |
| `User1/Repo1!1234` | no | N/A | A link to issue/pull 1234 in `User1/Repo1` |
| `User1/Repo1#1234` | yes | N/A | A link to _external issue_ 1234 for `User1/Repo1` |
| `User1/Repo1!1234` | yes | N/A | A link to _PR_ 1234 for `User1/Repo1` |
| `UserZ/RepoZ#1234` | N/A | no | A link to issue/pull 1234 in `UserZ/RepoZ` |
| `UserZ/RepoZ!1234` | N/A | no | A link to issue/pull 1234 in `UserZ/RepoZ` |
| _Not supported_ | N/A | yes | A link to _external issue_ 1234 for `UserZ/RepoZ` |
| `UserZ/RepoZ!1234` | N/A | yes | A link to _PR_ 1234 for `UserZ/RepoZ` |
| **Alphanumeric issue IDs:** | - | - | - |
| `AAA-1234` | yes | N/A | A link to _external issue_ `AAA-1234` for `User1/Repo1` |
| `!1234` | yes | N/A | A link to _PR_ 1234 for `User1/Repo1` |
| `User1/Repo1!1234` | yes | N/A | A link to _PR_ 1234 for `User1/Repo1` |
| _Not supported_ | N/A | yes | A link to _external issue_ `AAA-1234` for `UserZ/RepoZ` |
| `UserZ/RepoZ!1234` | N/A | yes | A link to _PR_ 1234 in `UserZ/RepoZ` |
_The last section is for repositories with external issue trackers that use alphanumeric format._
_**N/A**: not applicable._
Note: automatic references between repositories with different types of issues (external vs. internal) are not fully supported
and may render invalid links.

@ -640,10 +640,19 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
ref *references.RenderizableReference ref *references.RenderizableReference
) )
if ctx.metas["style"] == IssueNameStyleAlphanumeric { _, exttrack := ctx.metas["format"]
found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data) alphanum := ctx.metas["style"] == IssueNameStyleAlphanumeric
} else {
found, ref = references.FindRenderizableReferenceNumeric(node.Data) // Repos with external issue trackers might still need to reference local PRs
// We need to concern with the first one that shows up in the text, whichever it is
found, ref = references.FindRenderizableReferenceNumeric(node.Data, exttrack && alphanum)
if exttrack && alphanum {
if found2, ref2 := references.FindRenderizableReferenceAlphanumeric(node.Data); found2 {
if !found || ref2.RefLocation.Start < ref.RefLocation.Start {
found = true
ref = ref2
}
}
} }
if !found { if !found {
return return
@ -651,13 +660,22 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
var link *html.Node var link *html.Node
reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
if _, ok := ctx.metas["format"]; ok { if exttrack && !ref.IsPull {
ctx.metas["index"] = ref.Issue ctx.metas["index"] = ref.Issue
link = createLink(com.Expand(ctx.metas["format"], ctx.metas), reftext, "issue") link = createLink(com.Expand(ctx.metas["format"], ctx.metas), reftext, "issue")
} else if ref.Owner == "" {
link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "issues", ref.Issue), reftext, "issue")
} else { } else {
link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, "issues", ref.Issue), reftext, "issue") // Path determines the type of link that will be rendered. It's unknown at this point whether
// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
// Gitea will redirect on click as appropriate.
path := "issues"
if ref.IsPull {
path = "pulls"
}
if ref.Owner == "" {
link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], path, ref.Issue), reftext, "issue")
} else {
link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "issue")
}
} }
if ref.Action == references.XRefActionNone { if ref.Action == references.XRefActionNone {
@ -667,7 +685,7 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
// Decorate action keywords if actionable // Decorate action keywords if actionable
var keyword *html.Node var keyword *html.Node
if references.IsXrefActionable(ref.Action) { if references.IsXrefActionable(ref, exttrack, alphanum) {
keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End]) keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
} else { } else {
keyword = &html.Node{ keyword = &html.Node{

@ -25,8 +25,8 @@ func alphanumIssueLink(baseURL, class, name string) string {
} }
// numericLink an HTML to a numeric-style issue // numericLink an HTML to a numeric-style issue
func numericIssueLink(baseURL, class string, index int) string { func numericIssueLink(baseURL, class string, index int, marker string) string {
return link(util.URLJoin(baseURL, strconv.Itoa(index)), class, fmt.Sprintf("#%d", index)) return link(util.URLJoin(baseURL, strconv.Itoa(index)), class, fmt.Sprintf("%s%d", marker, index))
} }
// link an HTML link // link an HTML link
@ -75,8 +75,12 @@ func TestRender_IssueIndexPattern(t *testing.T) {
test("#abcd") test("#abcd")
test("test#1234") test("test#1234")
test("#1234test") test("#1234test")
test(" test #1234test") test("#abcd")
test("test!1234")
test("!1234test")
test(" test !1234test")
test("/home/gitea/#1234") test("/home/gitea/#1234")
test("/home/gitea/!1234")
// should not render issue mention without leading space // should not render issue mention without leading space
test("test#54321 issue") test("test#54321 issue")
@ -90,42 +94,54 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
setting.AppSubURL = AppSubURL setting.AppSubURL = AppSubURL
// numeric: render inputs with valid mentions // numeric: render inputs with valid mentions
test := func(s, expectedFmt string, indices ...int) { test := func(s, expectedFmt, marker string, indices ...int) {
var path, prefix string
if marker == "!" {
path = "pulls"
prefix = "http://localhost:3000/someUser/someRepo/pulls/"
} else {
path = "issues"
prefix = "https://someurl.com/someUser/someRepo/"
}
links := make([]interface{}, len(indices)) links := make([]interface{}, len(indices))
for i, index := range indices { for i, index := range indices {
links[i] = numericIssueLink(util.URLJoin(setting.AppSubURL, "issues"), "issue", index) links[i] = numericIssueLink(util.URLJoin(setting.AppSubURL, path), "issue", index, marker)
} }
expectedNil := fmt.Sprintf(expectedFmt, links...) expectedNil := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expectedNil, &postProcessCtx{metas: localMetas}) testRenderIssueIndexPattern(t, s, expectedNil, &postProcessCtx{metas: localMetas})
for i, index := range indices { for i, index := range indices {
links[i] = numericIssueLink("https://someurl.com/someUser/someRepo/", "issue", index) links[i] = numericIssueLink(prefix, "issue", index, marker)
} }
expectedNum := fmt.Sprintf(expectedFmt, links...) expectedNum := fmt.Sprintf(expectedFmt, links...)
testRenderIssueIndexPattern(t, s, expectedNum, &postProcessCtx{metas: numericMetas}) testRenderIssueIndexPattern(t, s, expectedNum, &postProcessCtx{metas: numericMetas})
} }
// should render freestanding mentions // should render freestanding mentions
test("#1234 test", "%s test", 1234) test("#1234 test", "%s test", "#", 1234)
test("test #8 issue", "test %s issue", 8) test("test #8 issue", "test %s issue", "#", 8)
test("test issue #1234", "test issue %s", 1234) test("!1234 test", "%s test", "!", 1234)
test("fixes issue #1234.", "fixes issue %s.", 1234) test("test !8 issue", "test %s issue", "!", 8)
test("test issue #1234", "test issue %s", "#", 1234)
test("fixes issue #1234.", "fixes issue %s.", "#", 1234)
// should render mentions in parentheses / brackets // should render mentions in parentheses / brackets
test("(#54321 issue)", "(%s issue)", 54321) test("(#54321 issue)", "(%s issue)", "#", 54321)
test("[#54321 issue]", "[%s issue]", 54321) test("[#54321 issue]", "[%s issue]", "#", 54321)
test("test (#9801 extra) issue", "test (%s extra) issue", 9801) test("test (#9801 extra) issue", "test (%s extra) issue", "#", 9801)
test("test (#1)", "test (%s)", 1) test("test (!9801 extra) issue", "test (%s extra) issue", "!", 9801)
test("test (#1)", "test (%s)", "#", 1)
// should render multiple issue mentions in the same line // should render multiple issue mentions in the same line
test("#54321 #1243", "%s %s", 54321, 1243) test("#54321 #1243", "%s %s", "#", 54321, 1243)
test("wow (#54321 #1243)", "wow (%s %s)", 54321, 1243) test("wow (#54321 #1243)", "wow (%s %s)", "#", 54321, 1243)
test("(#4)(#5)", "(%s)(%s)", 4, 5) test("(#4)(#5)", "(%s)(%s)", "#", 4, 5)
test("#1 (#4321) test", "%s (%s) test", 1, 4321) test("#1 (#4321) test", "%s (%s) test", "#", 1, 4321)
// should render with : // should render with :
test("#1234: test", "%s: test", 1234) test("#1234: test", "%s: test", "#", 1234)
test("wow (#54321: test)", "wow (%s: test)", 54321) test("wow (#54321: test)", "wow (%s: test)", "#", 54321)
} }
func TestRender_IssueIndexPattern3(t *testing.T) { func TestRender_IssueIndexPattern3(t *testing.T) {
@ -201,7 +217,7 @@ func TestRender_AutoLink(t *testing.T) {
// render valid issue URLs // render valid issue URLs
test(util.URLJoin(setting.AppSubURL, "issues", "3333"), test(util.URLJoin(setting.AppSubURL, "issues", "3333"),
numericIssueLink(util.URLJoin(setting.AppSubURL, "issues"), "issue", 3333)) numericIssueLink(util.URLJoin(setting.AppSubURL, "issues"), "issue", 3333, "#"))
// render valid commit URLs // render valid commit URLs
tmp := util.URLJoin(AppSubURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae") tmp := util.URLJoin(AppSubURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae")

@ -29,12 +29,12 @@ var (
// mentionPattern matches all mentions in the form of "@user" // mentionPattern matches all mentions in the form of "@user"
mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`) mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`)
// issueNumericPattern matches string that references to a numeric issue, e.g. #1287 // issueNumericPattern matches string that references to a numeric issue, e.g. #1287
issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(#[0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`) issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([#!][0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`)
// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`) issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`)
// crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
// e.g. gogits/gogs#12345 // e.g. gogits/gogs#12345
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+#[0-9]+)(?:\s|$|\)|\]|\.(\s|$))`) crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|\.(\s|$))`)
issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
issueKeywordsOnce sync.Once issueKeywordsOnce sync.Once
@ -66,10 +66,14 @@ type IssueReference struct {
} }
// RenderizableReference contains an unverified cross-reference to with rendering information // RenderizableReference contains an unverified cross-reference to with rendering information
// The IsPull member means that a `!num` reference was used instead of `#num`.
// This kind of reference is used to make pulls available when an external issue tracker
// is used. Otherwise, `#` and `!` are completely interchangeable.
type RenderizableReference struct { type RenderizableReference struct {
Issue string Issue string
Owner string Owner string
Name string Name string
IsPull bool
RefLocation *RefSpan RefLocation *RefSpan
Action XRefAction Action XRefAction
ActionLocation *RefSpan ActionLocation *RefSpan
@ -79,6 +83,7 @@ type rawReference struct {
index int64 index int64
owner string owner string
name string name string
isPull bool
action XRefAction action XRefAction
issue string issue string
refLocation *RefSpan refLocation *RefSpan
@ -202,14 +207,14 @@ func FindAllIssueReferences(content string) []IssueReference {
} }
// FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string. // FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string.
func FindRenderizableReferenceNumeric(content string) (bool, *RenderizableReference) { func FindRenderizableReferenceNumeric(content string, prOnly bool) (bool, *RenderizableReference) {
match := issueNumericPattern.FindStringSubmatchIndex(content) match := issueNumericPattern.FindStringSubmatchIndex(content)
if match == nil { if match == nil {
if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); match == nil { if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); match == nil {
return false, nil return false, nil
} }
} }
r := getCrossReference([]byte(content), match[2], match[3], false) r := getCrossReference([]byte(content), match[2], match[3], false, prOnly)
if r == nil { if r == nil {
return false, nil return false, nil
} }
@ -218,6 +223,7 @@ func FindRenderizableReferenceNumeric(content string) (bool, *RenderizableRefere
Issue: r.issue, Issue: r.issue,
Owner: r.owner, Owner: r.owner,
Name: r.name, Name: r.name,
IsPull: r.isPull,
RefLocation: r.refLocation, RefLocation: r.refLocation,
Action: r.action, Action: r.action,
ActionLocation: r.actionLocation, ActionLocation: r.actionLocation,
@ -238,6 +244,7 @@ func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableR
RefLocation: &RefSpan{Start: match[2], End: match[3]}, RefLocation: &RefSpan{Start: match[2], End: match[3]},
Action: action, Action: action,
ActionLocation: location, ActionLocation: location,
IsPull: false,
} }
} }
@ -248,14 +255,14 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference
matches := issueNumericPattern.FindAllSubmatchIndex(content, -1) matches := issueNumericPattern.FindAllSubmatchIndex(content, -1)
for _, match := range matches { for _, match := range matches {
if ref := getCrossReference(content, match[2], match[3], false); ref != nil { if ref := getCrossReference(content, match[2], match[3], false, false); ref != nil {
ret = append(ret, ref) ret = append(ret, ref)
} }
} }
matches = crossReferenceIssueNumericPattern.FindAllSubmatchIndex(content, -1) matches = crossReferenceIssueNumericPattern.FindAllSubmatchIndex(content, -1)
for _, match := range matches { for _, match := range matches {
if ref := getCrossReference(content, match[2], match[3], false); ref != nil { if ref := getCrossReference(content, match[2], match[3], false, false); ref != nil {
ret = append(ret, ref) ret = append(ret, ref)
} }
} }
@ -273,12 +280,17 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference
if len(parts) != 5 || parts[0] != "" { if len(parts) != 5 || parts[0] != "" {
continue continue
} }
if parts[3] != "issues" && parts[3] != "pulls" { var sep string
if parts[3] == "issues" {
sep = "#"
} else if parts[3] == "pulls" {
sep = "!"
} else {
continue continue
} }
// Note: closing/reopening keywords not supported with URLs // Note: closing/reopening keywords not supported with URLs
bytes := []byte(parts[1] + "/" + parts[2] + "#" + parts[4]) bytes := []byte(parts[1] + "/" + parts[2] + sep + parts[4])
if ref := getCrossReference(bytes, 0, len(bytes), true); ref != nil { if ref := getCrossReference(bytes, 0, len(bytes), true, false); ref != nil {
ref.refLocation = nil ref.refLocation = nil
ret = append(ret, ref) ret = append(ret, ref)
} }
@ -288,13 +300,18 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference
return ret return ret
} }
func getCrossReference(content []byte, start, end int, fromLink bool) *rawReference { func getCrossReference(content []byte, start, end int, fromLink bool, prOnly bool) *rawReference {
refid := string(content[start:end]) refid := string(content[start:end])
parts := strings.Split(refid, "#") sep := strings.IndexAny(refid, "#!")
if len(parts) != 2 { if sep < 0 {
return nil return nil
} }
repo, issue := parts[0], parts[1] isPull := refid[sep] == '!'
if prOnly && !isPull {
return nil
}
repo := refid[:sep]
issue := refid[sep+1:]
index, err := strconv.ParseInt(issue, 10, 64) index, err := strconv.ParseInt(issue, 10, 64)
if err != nil { if err != nil {
return nil return nil
@ -309,11 +326,12 @@ func getCrossReference(content []byte, start, end int, fromLink bool) *rawRefere
index: index, index: index,
action: action, action: action,
issue: issue, issue: issue,
isPull: isPull,
refLocation: &RefSpan{Start: start, End: end}, refLocation: &RefSpan{Start: start, End: end},
actionLocation: location, actionLocation: location,
} }
} }
parts = strings.Split(strings.ToLower(repo), "/") parts := strings.Split(strings.ToLower(repo), "/")
if len(parts) != 2 { if len(parts) != 2 {
return nil return nil
} }
@ -328,6 +346,7 @@ func getCrossReference(content []byte, start, end int, fromLink bool) *rawRefere
name: name, name: name,
action: action, action: action,
issue: issue, issue: issue,
isPull: isPull,
refLocation: &RefSpan{Start: start, End: end}, refLocation: &RefSpan{Start: start, End: end},
actionLocation: location, actionLocation: location,
} }
@ -352,6 +371,10 @@ func findActionKeywords(content []byte, start int) (XRefAction, *RefSpan) {
} }
// IsXrefActionable returns true if the xref action is actionable (i.e. produces a result when resolved) // IsXrefActionable returns true if the xref action is actionable (i.e. produces a result when resolved)
func IsXrefActionable(a XRefAction) bool { func IsXrefActionable(ref *RenderizableReference, extTracker bool, alphaNum bool) bool {
return a == XRefActionCloses || a == XRefActionReopens if extTracker {
// External issues cannot be automatically closed
return false
}
return ref.Action == XRefActionCloses || ref.Action == XRefActionReopens
} }

@ -22,6 +22,7 @@ type testResult struct {
Owner string Owner string
Name string Name string
Issue string Issue string
IsPull bool
Action XRefAction Action XRefAction
RefLocation *RefSpan RefLocation *RefSpan
ActionLocation *RefSpan ActionLocation *RefSpan
@ -33,7 +34,13 @@ func TestFindAllIssueReferences(t *testing.T) {
{ {
"Simply closes: #29 yes", "Simply closes: #29 yes",
[]testResult{ []testResult{
{29, "", "", "29", XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}}, {29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}},
},
},
{
"Simply closes: !29 yes",
[]testResult{
{29, "", "", "29", true, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}},
}, },
}, },
{ {
@ -43,7 +50,7 @@ func TestFindAllIssueReferences(t *testing.T) {
{ {
" #124 yes, this is a reference.", " #124 yes, this is a reference.",
[]testResult{ []testResult{
{124, "", "", "124", XRefActionNone, &RefSpan{Start: 0, End: 4}, nil}, {124, "", "", "124", false, XRefActionNone, &RefSpan{Start: 0, End: 4}, nil},
}, },
}, },
{ {
@ -57,7 +64,13 @@ func TestFindAllIssueReferences(t *testing.T) {
{ {
"This user3/repo4#200 yes.", "This user3/repo4#200 yes.",
[]testResult{ []testResult{
{200, "user3", "repo4", "200", XRefActionNone, &RefSpan{Start: 5, End: 20}, nil}, {200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
},
},
{
"This user3/repo4!200 yes.",
[]testResult{
{200, "user3", "repo4", "200", true, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
}, },
}, },
{ {
@ -67,19 +80,19 @@ func TestFindAllIssueReferences(t *testing.T) {
{ {
"This [two](/user2/repo1/issues/921) yes.", "This [two](/user2/repo1/issues/921) yes.",
[]testResult{ []testResult{
{921, "user2", "repo1", "921", XRefActionNone, nil, nil}, {921, "user2", "repo1", "921", false, XRefActionNone, nil, nil},
}, },
}, },
{ {
"This [three](/user2/repo1/pulls/922) yes.", "This [three](/user2/repo1/pulls/922) yes.",
[]testResult{ []testResult{
{922, "user2", "repo1", "922", XRefActionNone, nil, nil}, {922, "user2", "repo1", "922", true, XRefActionNone, nil, nil},
}, },
}, },
{ {
"This [four](http://gitea.com:3000/user3/repo4/issues/203) yes.", "This [four](http://gitea.com:3000/user3/repo4/issues/203) yes.",
[]testResult{ []testResult{
{203, "user3", "repo4", "203", XRefActionNone, nil, nil}, {203, "user3", "repo4", "203", false, XRefActionNone, nil, nil},
}, },
}, },
{ {
@ -93,50 +106,50 @@ func TestFindAllIssueReferences(t *testing.T) {
{ {
"This http://gitea.com:3000/user4/repo5/pulls/202 yes.", "This http://gitea.com:3000/user4/repo5/pulls/202 yes.",
[]testResult{ []testResult{
{202, "user4", "repo5", "202", XRefActionNone, nil, nil}, {202, "user4", "repo5", "202", true, XRefActionNone, nil, nil},
}, },
}, },
{ {
"This http://GiTeA.COM:3000/user4/repo6/pulls/205 yes.", "This http://GiTeA.COM:3000/user4/repo6/pulls/205 yes.",
[]testResult{ []testResult{
{205, "user4", "repo6", "205", XRefActionNone, nil, nil}, {205, "user4", "repo6", "205", true, XRefActionNone, nil, nil},
}, },
}, },
{ {
"Reopens #15 yes", "Reopens #15 yes",
[]testResult{ []testResult{
{15, "", "", "15", XRefActionReopens, &RefSpan{Start: 8, End: 11}, &RefSpan{Start: 0, End: 7}}, {15, "", "", "15", false, XRefActionReopens, &RefSpan{Start: 8, End: 11}, &RefSpan{Start: 0, End: 7}},
}, },
}, },
{ {
"This closes #20 for you yes", "This closes #20 for you yes",
[]testResult{ []testResult{
{20, "", "", "20", XRefActionCloses, &RefSpan{Start: 12, End: 15}, &RefSpan{Start: 5, End: 11}}, {20, "", "", "20", false, XRefActionCloses, &RefSpan{Start: 12, End: 15}, &RefSpan{Start: 5, End: 11}},
}, },
}, },
{ {
"Do you fix user6/repo6#300 ? yes", "Do you fix user6/repo6#300 ? yes",
[]testResult{ []testResult{
{300, "user6", "repo6", "300", XRefActionCloses, &RefSpan{Start: 11, End: 26}, &RefSpan{Start: 7, End: 10}}, {300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 11, End: 26}, &RefSpan{Start: 7, End: 10}},
}, },
}, },
{ {
"For 999 #1235 no keyword, but yes", "For 999 #1235 no keyword, but yes",
[]testResult{ []testResult{
{1235, "", "", "1235", XRefActionNone, &RefSpan{Start: 8, End: 13}, nil}, {1235, "", "", "1235", false, XRefActionNone, &RefSpan{Start: 8, End: 13}, nil},
}, },
}, },
{ {
"Which abc. #9434 same as above", "Which abc. #9434 same as above",
[]testResult{ []testResult{
{9434, "", "", "9434", XRefActionNone, &RefSpan{Start: 11, End: 16}, nil}, {9434, "", "", "9434", false, XRefActionNone, &RefSpan{Start: 11, End: 16}, nil},
}, },
}, },
{ {
"This closes #600 and reopens #599", "This closes #600 and reopens #599",
[]testResult{ []testResult{
{600, "", "", "600", XRefActionCloses, &RefSpan{Start: 12, End: 16}, &RefSpan{Start: 5, End: 11}}, {600, "", "", "600", false, XRefActionCloses, &RefSpan{Start: 12, End: 16}, &RefSpan{Start: 5, End: 11}},
{599, "", "", "599", XRefActionReopens, &RefSpan{Start: 29, End: 33}, &RefSpan{Start: 21, End: 28}}, {599, "", "", "599", false, XRefActionReopens, &RefSpan{Start: 29, End: 33}, &RefSpan{Start: 21, End: 28}},
}, },
}, },
} }
@ -190,6 +203,7 @@ func testFixtures(t *testing.T, fixtures []testFixture, context string) {
index: e.Index, index: e.Index,
owner: e.Owner, owner: e.Owner,
name: e.Name, name: e.Name,
isPull: e.IsPull,
action: e.Action, action: e.Action,
issue: e.Issue, issue: e.Issue,
refLocation: e.RefLocation, refLocation: e.RefLocation,
@ -329,25 +343,25 @@ func TestCustomizeCloseKeywords(t *testing.T) {
{ {
"Simplemente cierra: #29 yes", "Simplemente cierra: #29 yes",
[]testResult{ []testResult{
{29, "", "", "29", XRefActionCloses, &RefSpan{Start: 20, End: 23}, &RefSpan{Start: 12, End: 18}}, {29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 20, End: 23}, &RefSpan{Start: 12, End: 18}},
}, },
}, },
{ {
"Closes: #123 no, this English.", "Closes: #123 no, this English.",
[]testResult{ []testResult{
{123, "", "", "123", XRefActionNone, &RefSpan{Start: 8, End: 12}, nil}, {123, "", "", "123", false, XRefActionNone, &RefSpan{Start: 8, End: 12}, nil},
}, },
}, },
{ {
"Cerró user6/repo6#300 yes", "Cerró user6/repo6#300 yes",
[]testResult{ []testResult{
{300, "user6", "repo6", "300", XRefActionCloses, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}}, {300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}},
}, },
}, },
{ {
"Reabre user3/repo4#200 yes", "Reabre user3/repo4#200 yes",
[]testResult{ []testResult{
{200, "user3", "repo4", "200", XRefActionReopens, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}}, {200, "user3", "repo4", "200", false, XRefActionReopens, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}},
}, },
}, },
} }

Loading…
Cancel
Save