diff --git a/modules/references/references.go b/modules/references/references.go index 592bd4cbe4483..ef3568ebea027 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -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) } } diff --git a/modules/references/references_test.go b/modules/references/references_test.go index a15ae99f794ca..a6c6efcce52b6 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -227,6 +227,38 @@ 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") + type alnumFixture struct { input string issue string