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:
parent
19e1997ee2
commit
83527d3f8a
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user