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
2 changed files with 80 additions and 6 deletions
+24 -6
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)
}
}