Skip to content

Commit 6a90c7e

Browse files
guillep2klafriks
authored andcommitted
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
1 parent 2011a5b commit 6a90c7e

File tree

5 files changed

+309
-65
lines changed

5 files changed

+309
-65
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
---
2+
date: "2019-11-21T17:00:00-03:00"
3+
title: "Usage: Automatically Linked References"
4+
slug: "automatically-linked-references"
5+
weight: 15
6+
toc: true
7+
draft: false
8+
menu:
9+
sidebar:
10+
parent: "usage"
11+
name: "Automatically Linked References"
12+
weight: 15
13+
identifier: "automatically-linked-references"
14+
---
15+
16+
# Automatically Linked References in Issues, Pull Requests and Commit Messages
17+
18+
When an issue, pull request or comment is posted, the text description is parsed
19+
in search for references. These references will be shown as links in the Issue View
20+
and, in some cases, produce certain _actions_.
21+
22+
Likewise, commit messages are parsed when they are listed, and _actions_
23+
are can be triggered when they are pushed to the main branch.
24+
25+
To prevent the creation of unintended references, there are certain rules
26+
for them to be recognized. For example, they should not be included inside code
27+
text. They should also be reasonably cleared from their surrounding text
28+
(for example, using spaces).
29+
30+
## User, Team and Organization Mentions
31+
32+
When a text in the form `@username` is found and `username` matches the name
33+
of an existing user, a _mention_ reference is created. This will be shown
34+
by changing the text into a link to said user's profile, and possibly create
35+
a notification for the mentioned user depending on whether they have
36+
the necessary permission to access the contents.
37+
38+
Example:
39+
40+
> [@John](#), can you give this a look?
41+
42+
This is also valid for teams and organizations:
43+
44+
> [@Documenters](#), we need to plan for this.
45+
46+
> [@CoolCompanyInc](#), this issue concerns us all!
47+
48+
Teams will receive mail notifications when appropriate, but whole organizations won't.
49+
50+
Commit messages do not produce user notifications.
51+
52+
## Commits
53+
54+
Commits can be referenced using their SHA1 hash, or a portion of it of
55+
at least seven characters. They will be shown as a link to the corresponding
56+
commit.
57+
58+
Example:
59+
60+
> This bug was introduced in [e59ff077](#)
61+
62+
## Issues and Pull Requests
63+
64+
A reference to another issue or pull request can be created using the simple
65+
notation `#1234`, where _1234_ is the number of an issue or pull request
66+
in the same repository. These references will be shown as links to the
67+
referenced content.
68+
69+
The effect of creating this type of reference is that a _notice_ will be
70+
created in the referenced document, provided the creator of the reference
71+
has reading permissions on it.
72+
73+
Example:
74+
75+
> This seems related to [#1234](#)
76+
77+
Issues and pull requests in other repositories can be referred to as well
78+
using the form `owner/repository#1234`:
79+
80+
> This seems related to [mike/compiler#1234](#)
81+
82+
Alternatively, the `!1234` notation can be used as well. Even when in Gitea
83+
a pull request is a form of issue, the `#1234` form will always link to
84+
an issue; if the linked entry happens to be a pull request instead, Gitea
85+
will redirect as appropriate. With the `!1234` notation, a pull request
86+
link will be created, which will be redirected to an issue if required.
87+
However, this distinction could be important if an external tracker is
88+
used, where links to issues and pull requests are not interchangeable.
89+
90+
## Actionable References in Pull Requests and Commit Messages
91+
92+
Sometimes a commit or pull request may fix or bring back a problem documented
93+
in a particular issue. Gitea supports closing and reopening the referenced
94+
issues by preceding the reference with a particular _keyword_. Common keywords
95+
include "closes", "fixes", "reopens", etc. This list can be
96+
[customized]({{< ref "/doc/advanced/config-cheat-sheet.en-us.md" >}}) by the
97+
site administrator.
98+
99+
Example:
100+
101+
> This PR _closes_ [#1234](#)
102+
103+
If the actionable reference is accepted, this will create a notice on the
104+
referenced issue announcing that it will be closed when the referencing PR
105+
is merged.
106+
107+
For an actionable reference to be accepted, _at least one_ of the following
108+
conditions must be met:
109+
110+
* The commenter has permissions to close or reopen the issue at the moment
111+
of creating the reference.
112+
* The reference is inside a commit message.
113+
* The reference is posted as part of the pull request description.
114+
115+
In the last case, the issue will be closed or reopened only if the merger
116+
of the pull request has permissions to do so.
117+
118+
Additionally, only pull requests and commit messages can create an action,
119+
and only issues can be closed or reopened this way.
120+
121+
The default _keywords_ are:
122+
123+
* **Closing**: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved
124+
* **Reopening**: reopen, reopens, reopened
125+
126+
## External Trackers
127+
128+
Gitea supports the use of external issue trackers, and references to issues
129+
hosted externally can be created in pull requests. However, if the external
130+
tracker uses numbers to identify issues, they will be indistinguishable from
131+
the pull requests hosted in Gitea. To address this, Gitea allows the use of
132+
the `!` marker to identify pull requests. For example:
133+
134+
> This is issue [#1234](#), and links to the external tracker.
135+
136+
> This is pull request [!1234](#), and links to a pull request in Gitea.
137+
138+
The `!` and `#` can be used interchangeably for issues and pull request _except_
139+
for this case, where a distinction is required.
140+
141+
## Issues and Pull Requests References Summary
142+
143+
This table illustrates the different kinds of cross-reference for issues and pull requests.
144+
In the examples, `User1/Repo1` refers to the repository where the reference is used, while
145+
`UserZ/RepoZ` indicates a different repository.
146+
147+
| Reference in User1/Repo1 | Repo1 issues are external | RepoZ issues are external | Should render |
148+
|---------------------------|:-------------------------:|:-------------------------:|----------------------------------|
149+
| `#1234` | no | N/A | A link to issue/pull 1234 in `User1/Repo1` |
150+
| `!1234` | no | N/A | A link to issue/pull 1234 in `User1/Repo1` |
151+
| `#1234` | yes | N/A | A link to _external issue_ 1234 for `User1/Repo1` |
152+
| `!1234` | yes | N/A | A link to _PR_ 1234 for `User1/Repo1` |
153+
| `User1/Repo1#1234` | no | N/A | A link to issue/pull 1234 in `User1/Repo1` |
154+
| `User1/Repo1!1234` | no | N/A | A link to issue/pull 1234 in `User1/Repo1` |
155+
| `User1/Repo1#1234` | yes | N/A | A link to _external issue_ 1234 for `User1/Repo1` |
156+
| `User1/Repo1!1234` | yes | N/A | A link to _PR_ 1234 for `User1/Repo1` |
157+
| `UserZ/RepoZ#1234` | N/A | no | A link to issue/pull 1234 in `UserZ/RepoZ` |
158+
| `UserZ/RepoZ!1234` | N/A | no | A link to issue/pull 1234 in `UserZ/RepoZ` |
159+
| _Not supported_ | N/A | yes | A link to _external issue_ 1234 for `UserZ/RepoZ` |
160+
| `UserZ/RepoZ!1234` | N/A | yes | A link to _PR_ 1234 for `UserZ/RepoZ` |
161+
| **Alphanumeric issue IDs:** | - | - | - |
162+
| `AAA-1234` | yes | N/A | A link to _external issue_ `AAA-1234` for `User1/Repo1` |
163+
| `!1234` | yes | N/A | A link to _PR_ 1234 for `User1/Repo1` |
164+
| `User1/Repo1!1234` | yes | N/A | A link to _PR_ 1234 for `User1/Repo1` |
165+
| _Not supported_ | N/A | yes | A link to _external issue_ `AAA-1234` for `UserZ/RepoZ` |
166+
| `UserZ/RepoZ!1234` | N/A | yes | A link to _PR_ 1234 in `UserZ/RepoZ` |
167+
168+
_The last section is for repositories with external issue trackers that use alphanumeric format._
169+
170+
_**N/A**: not applicable._
171+
172+
Note: automatic references between repositories with different types of issues (external vs. internal) are not fully supported
173+
and may render invalid links.

modules/markup/html.go

+27-9
Original file line numberDiff line numberDiff line change
@@ -640,24 +640,42 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
640640
ref *references.RenderizableReference
641641
)
642642

643-
if ctx.metas["style"] == IssueNameStyleAlphanumeric {
644-
found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
645-
} else {
646-
found, ref = references.FindRenderizableReferenceNumeric(node.Data)
643+
_, exttrack := ctx.metas["format"]
644+
alphanum := ctx.metas["style"] == IssueNameStyleAlphanumeric
645+
646+
// Repos with external issue trackers might still need to reference local PRs
647+
// We need to concern with the first one that shows up in the text, whichever it is
648+
found, ref = references.FindRenderizableReferenceNumeric(node.Data, exttrack && alphanum)
649+
if exttrack && alphanum {
650+
if found2, ref2 := references.FindRenderizableReferenceAlphanumeric(node.Data); found2 {
651+
if !found || ref2.RefLocation.Start < ref.RefLocation.Start {
652+
found = true
653+
ref = ref2
654+
}
655+
}
647656
}
648657
if !found {
649658
return
650659
}
651660

652661
var link *html.Node
653662
reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
654-
if _, ok := ctx.metas["format"]; ok {
663+
if exttrack && !ref.IsPull {
655664
ctx.metas["index"] = ref.Issue
656665
link = createLink(com.Expand(ctx.metas["format"], ctx.metas), reftext, "issue")
657-
} else if ref.Owner == "" {
658-
link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "issues", ref.Issue), reftext, "issue")
659666
} else {
660-
link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, "issues", ref.Issue), reftext, "issue")
667+
// Path determines the type of link that will be rendered. It's unknown at this point whether
668+
// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
669+
// Gitea will redirect on click as appropriate.
670+
path := "issues"
671+
if ref.IsPull {
672+
path = "pulls"
673+
}
674+
if ref.Owner == "" {
675+
link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], path, ref.Issue), reftext, "issue")
676+
} else {
677+
link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "issue")
678+
}
661679
}
662680

663681
if ref.Action == references.XRefActionNone {
@@ -667,7 +685,7 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
667685

668686
// Decorate action keywords if actionable
669687
var keyword *html.Node
670-
if references.IsXrefActionable(ref.Action) {
688+
if references.IsXrefActionable(ref, exttrack, alphanum) {
671689
keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
672690
} else {
673691
keyword = &html.Node{

modules/markup/html_internal_test.go

+37-21
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ func alphanumIssueLink(baseURL, class, name string) string {
2525
}
2626

2727
// numericLink an HTML to a numeric-style issue
28-
func numericIssueLink(baseURL, class string, index int) string {
29-
return link(util.URLJoin(baseURL, strconv.Itoa(index)), class, fmt.Sprintf("#%d", index))
28+
func numericIssueLink(baseURL, class string, index int, marker string) string {
29+
return link(util.URLJoin(baseURL, strconv.Itoa(index)), class, fmt.Sprintf("%s%d", marker, index))
3030
}
3131

3232
// link an HTML link
@@ -75,8 +75,12 @@ func TestRender_IssueIndexPattern(t *testing.T) {
7575
test("#abcd")
7676
test("test#1234")
7777
test("#1234test")
78-
test(" test #1234test")
78+
test("#abcd")
79+
test("test!1234")
80+
test("!1234test")
81+
test(" test !1234test")
7982
test("/home/gitea/#1234")
83+
test("/home/gitea/!1234")
8084

8185
// should not render issue mention without leading space
8286
test("test#54321 issue")
@@ -90,42 +94,54 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
9094
setting.AppSubURL = AppSubURL
9195

9296
// numeric: render inputs with valid mentions
93-
test := func(s, expectedFmt string, indices ...int) {
97+
test := func(s, expectedFmt, marker string, indices ...int) {
98+
var path, prefix string
99+
if marker == "!" {
100+
path = "pulls"
101+
prefix = "http://localhost:3000/someUser/someRepo/pulls/"
102+
} else {
103+
path = "issues"
104+
prefix = "https://someurl.com/someUser/someRepo/"
105+
}
106+
94107
links := make([]interface{}, len(indices))
95108
for i, index := range indices {
96-
links[i] = numericIssueLink(util.URLJoin(setting.AppSubURL, "issues"), "issue", index)
109+
links[i] = numericIssueLink(util.URLJoin(setting.AppSubURL, path), "issue", index, marker)
97110
}
98111
expectedNil := fmt.Sprintf(expectedFmt, links...)
99112
testRenderIssueIndexPattern(t, s, expectedNil, &postProcessCtx{metas: localMetas})
100113

101114
for i, index := range indices {
102-
links[i] = numericIssueLink("https://someurl.com/someUser/someRepo/", "issue", index)
115+
links[i] = numericIssueLink(prefix, "issue", index, marker)
103116
}
104117
expectedNum := fmt.Sprintf(expectedFmt, links...)
105118
testRenderIssueIndexPattern(t, s, expectedNum, &postProcessCtx{metas: numericMetas})
106119
}
107120

108121
// should render freestanding mentions
109-
test("#1234 test", "%s test", 1234)
110-
test("test #8 issue", "test %s issue", 8)
111-
test("test issue #1234", "test issue %s", 1234)
112-
test("fixes issue #1234.", "fixes issue %s.", 1234)
122+
test("#1234 test", "%s test", "#", 1234)
123+
test("test #8 issue", "test %s issue", "#", 8)
124+
test("!1234 test", "%s test", "!", 1234)
125+
test("test !8 issue", "test %s issue", "!", 8)
126+
test("test issue #1234", "test issue %s", "#", 1234)
127+
test("fixes issue #1234.", "fixes issue %s.", "#", 1234)
113128

114129
// should render mentions in parentheses / brackets
115-
test("(#54321 issue)", "(%s issue)", 54321)
116-
test("[#54321 issue]", "[%s issue]", 54321)
117-
test("test (#9801 extra) issue", "test (%s extra) issue", 9801)
118-
test("test (#1)", "test (%s)", 1)
130+
test("(#54321 issue)", "(%s issue)", "#", 54321)
131+
test("[#54321 issue]", "[%s issue]", "#", 54321)
132+
test("test (#9801 extra) issue", "test (%s extra) issue", "#", 9801)
133+
test("test (!9801 extra) issue", "test (%s extra) issue", "!", 9801)
134+
test("test (#1)", "test (%s)", "#", 1)
119135

120136
// should render multiple issue mentions in the same line
121-
test("#54321 #1243", "%s %s", 54321, 1243)
122-
test("wow (#54321 #1243)", "wow (%s %s)", 54321, 1243)
123-
test("(#4)(#5)", "(%s)(%s)", 4, 5)
124-
test("#1 (#4321) test", "%s (%s) test", 1, 4321)
137+
test("#54321 #1243", "%s %s", "#", 54321, 1243)
138+
test("wow (#54321 #1243)", "wow (%s %s)", "#", 54321, 1243)
139+
test("(#4)(#5)", "(%s)(%s)", "#", 4, 5)
140+
test("#1 (#4321) test", "%s (%s) test", "#", 1, 4321)
125141

126142
// should render with :
127-
test("#1234: test", "%s: test", 1234)
128-
test("wow (#54321: test)", "wow (%s: test)", 54321)
143+
test("#1234: test", "%s: test", "#", 1234)
144+
test("wow (#54321: test)", "wow (%s: test)", "#", 54321)
129145
}
130146

131147
func TestRender_IssueIndexPattern3(t *testing.T) {
@@ -201,7 +217,7 @@ func TestRender_AutoLink(t *testing.T) {
201217

202218
// render valid issue URLs
203219
test(util.URLJoin(setting.AppSubURL, "issues", "3333"),
204-
numericIssueLink(util.URLJoin(setting.AppSubURL, "issues"), "issue", 3333))
220+
numericIssueLink(util.URLJoin(setting.AppSubURL, "issues"), "issue", 3333, "#"))
205221

206222
// render valid commit URLs
207223
tmp := util.URLJoin(AppSubURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae")

0 commit comments

Comments
 (0)