0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-12-31 09:57:10 +01:00

Support closing keywords with URL references (#36221)

## Summary

This PR adds support for closing keywords (`closes`, `fixes`, `reopens`,
etc.) with full URL references in markdown links.

**Before:**
- `closes #123`  works
- `closes org/repo#123`  works  
- `Closes [this issue](https://gitea.io/user/repo/issues/123)`  didn't
work
- `Fixes [#456](https://gitea.io/org/project/issues/456)`  didn't work

**After:**
All of the above now work correctly.

## Problem

When users reference issues using full URLs in markdown links (e.g.,
`Closes [this issue](https://gitea.io/user/repo/issues/123)`), the
closing keywords were not detected. This was because the URL processing
code explicitly stated:

```go
// Note: closing/reopening keywords not supported with URLs
```

Both methods of writing the reference render the same in the UI, so
users expected the closing keywords to behave the same.

## Solution

The fix works by:
1. Passing the original (unstripped) content to
`findAllIssueReferencesBytes`
2. When processing URL links from markdown, finding the URL position in
the original content
3. For markdown links `[text](url)`, finding the opening bracket `[`
position
4. Using that position to detect closing keywords before the link

## Testing

Added test cases for:
- `Closes [this issue](url)` - single URL with closing keyword
- `This fixes [#456](url)` - keyword in middle of text
- `Reopens [PR](url)` - reopen keyword with pull request URL
- Multiple URLs where only one has a closing keyword

All existing tests continue to pass.

Fixes #27549
This commit is contained in:
Gregorius Bima Kharisma Wicaksana 2025-12-28 00:05:24 +07:00 committed by GitHub
parent 19e1997ee2
commit 83527d3f8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 80 additions and 6 deletions

View File

@ -248,7 +248,7 @@ func FindAllIssueReferencesMarkdown(content string) []IssueReference {
func findAllIssueReferencesMarkdown(content string) []*rawReference {
bcontent, links := mdstripper.StripMarkdownBytes([]byte(content))
return findAllIssueReferencesBytes(bcontent, links)
return findAllIssueReferencesBytes(bcontent, links, []byte(content))
}
func convertFullHTMLReferencesToShortRefs(re *regexp.Regexp, contentBytes *[]byte) {
@ -326,7 +326,7 @@ func FindAllIssueReferences(content string) []IssueReference {
} else {
log.Debug("No GiteaIssuePullPattern pattern")
}
return rawToIssueReferenceList(findAllIssueReferencesBytes(contentBytes, []string{}))
return rawToIssueReferenceList(findAllIssueReferencesBytes(contentBytes, []string{}, nil))
}
// FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string.
@ -406,7 +406,8 @@ func FindRenderizableReferenceAlphanumeric(content string) *RenderizableReferenc
}
// FindAllIssueReferencesBytes returns a list of unvalidated references found in a byte slice.
func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference {
// originalContent is optional and used to detect closing/reopening keywords for URL references.
func findAllIssueReferencesBytes(content []byte, links []string, originalContent []byte) []*rawReference {
ret := make([]*rawReference, 0, 10)
pos := 0
@ -470,10 +471,27 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference
default:
continue
}
// Note: closing/reopening keywords not supported with URLs
bytes := []byte(parts[1] + "/" + parts[2] + sep + parts[4])
if ref := getCrossReference(bytes, 0, len(bytes), true, false); ref != nil {
refBytes := []byte(parts[1] + "/" + parts[2] + sep + parts[4])
if ref := getCrossReference(refBytes, 0, len(refBytes), true, false); ref != nil {
ref.refLocation = nil
// Detect closing/reopening keywords by finding the URL position in original content
if originalContent != nil {
if idx := bytes.Index(originalContent, []byte(link)); idx > 0 {
// For markdown links [text](url), find the opening bracket before the URL
// to properly detect keywords like "closes [text](url)"
searchStart := idx
if idx >= 2 && originalContent[idx-1] == '(' {
// Find the matching '[' for this markdown link
bracketIdx := bytes.LastIndex(originalContent[:idx-1], []byte{'['})
if bracketIdx >= 0 {
searchStart = bracketIdx
}
}
action, location := findActionKeywords(originalContent, searchStart)
ref.action = action
ref.actionLocation = location
}
}
ret = append(ret, ref)
}
}

View File

@ -227,6 +227,62 @@ func TestFindAllIssueReferences(t *testing.T) {
testFixtures(t, fixtures, "default")
// Test closing/reopening keywords with URLs (issue #27549)
// Uses the same AppURL as testFixtures (https://gitea.com:3000/)
urlFixtures := []testFixture{
{
"Closes [this issue](https://gitea.com:3000/user/repo/issues/123)",
[]testResult{
{123, "user", "repo", "123", false, XRefActionCloses, nil, &RefSpan{Start: 0, End: 6}, ""},
},
},
{
"This fixes [#456](https://gitea.com:3000/org/project/issues/456)",
[]testResult{
{456, "org", "project", "456", false, XRefActionCloses, nil, &RefSpan{Start: 5, End: 10}, ""},
},
},
{
"Reopens [PR](https://gitea.com:3000/owner/repo/pulls/789)",
[]testResult{
{789, "owner", "repo", "789", true, XRefActionReopens, nil, &RefSpan{Start: 0, End: 7}, ""},
},
},
{
"See [issue](https://gitea.com:3000/user/repo/issues/100) but closes [another](https://gitea.com:3000/user/repo/issues/200)",
[]testResult{
{100, "user", "repo", "100", false, XRefActionNone, nil, nil, ""},
{200, "user", "repo", "200", false, XRefActionCloses, nil, &RefSpan{Start: 61, End: 67}, ""},
},
},
}
testFixtures(t, urlFixtures, "url-keywords")
// Test bare URLs (not markdown links) with closing keywords
// These use FindAllIssueReferences (non-markdown) which converts full URLs to short refs first
setting.AppURL = "https://gitea.com:3000/"
bareURLTests := []struct {
name string
input string
expected XRefAction
}{
{"Fixes bare URL", "Fixes https://gitea.com:3000/org/project/issues/456", XRefActionCloses},
{"Fixes with colon", "Fixes: https://gitea.com:3000/org/project/issues/456", XRefActionCloses},
{"Closes bare URL", "Closes https://gitea.com:3000/user/repo/issues/123", XRefActionCloses},
{"Closes with colon", "Closes: https://gitea.com:3000/user/repo/issues/123", XRefActionCloses},
}
for _, tt := range bareURLTests {
t.Run(tt.name, func(t *testing.T) {
refs := FindAllIssueReferences(tt.input)
assert.Len(t, refs, 1, "Expected 1 reference for: %s", tt.input)
if len(refs) > 0 {
assert.Equal(t, tt.expected, refs[0].Action, "Expected action %v for: %s", tt.expected, tt.input)
}
})
}
type alnumFixture struct {
input string
issue string