diff --git a/models/repo/repo.go b/models/repo/repo.go
index 3fd6b94eb..57d85435e 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -414,6 +414,9 @@ func (repo *Repository) ComposeMetas() map[string]string {
 			switch unit.ExternalTrackerConfig().ExternalTrackerStyle {
 			case markup.IssueNameStyleAlphanumeric:
 				metas["style"] = markup.IssueNameStyleAlphanumeric
+			case markup.IssueNameStyleRegexp:
+				metas["style"] = markup.IssueNameStyleRegexp
+				metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern
 			default:
 				metas["style"] = markup.IssueNameStyleNumeric
 			}
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index 8c17d6138..da3e19dec 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -76,9 +76,10 @@ func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) {
 
 // ExternalTrackerConfig describes external tracker config
 type ExternalTrackerConfig struct {
-	ExternalTrackerURL    string
-	ExternalTrackerFormat string
-	ExternalTrackerStyle  string
+	ExternalTrackerURL           string
+	ExternalTrackerFormat        string
+	ExternalTrackerStyle         string
+	ExternalTrackerRegexpPattern string
 }
 
 // FromDB fills up a ExternalTrackerConfig from serialized format.
diff --git a/models/repo_test.go b/models/repo_test.go
index c9e66398d..f554ff16a 100644
--- a/models/repo_test.go
+++ b/models/repo_test.go
@@ -74,6 +74,9 @@ func TestMetas(t *testing.T) {
 	externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleNumeric
 	testSuccess(markup.IssueNameStyleNumeric)
 
+	externalTracker.ExternalTrackerConfig().ExternalTrackerStyle = markup.IssueNameStyleRegexp
+	testSuccess(markup.IssueNameStyleRegexp)
+
 	repo, err := repo_model.GetRepositoryByID(3)
 	assert.NoError(t, err)
 
diff --git a/modules/markup/html.go b/modules/markup/html.go
index c5d36e701..69d9ba3ef 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup/common"
 	"code.gitea.io/gitea/modules/references"
+	"code.gitea.io/gitea/modules/regexplru"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/templates/vars"
 	"code.gitea.io/gitea/modules/util"
@@ -33,6 +34,7 @@ import (
 const (
 	IssueNameStyleNumeric      = "numeric"
 	IssueNameStyleAlphanumeric = "alphanumeric"
+	IssueNameStyleRegexp       = "regexp"
 )
 
 var (
@@ -815,19 +817,35 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
 	)
 
 	next := node.NextSibling
+
 	for node != nil && node != next {
-		_, exttrack := ctx.Metas["format"]
-		alphanum := ctx.Metas["style"] == IssueNameStyleAlphanumeric
+		_, hasExtTrackFormat := ctx.Metas["format"]
 
 		// Repos with external issue trackers might still need to reference local PRs
 		// We need to concern with the first one that shows up in the text, whichever it is
-		found, ref = references.FindRenderizableReferenceNumeric(node.Data, exttrack && alphanum)
-		if exttrack && alphanum {
-			if found2, ref2 := references.FindRenderizableReferenceAlphanumeric(node.Data); found2 {
-				if !found || ref2.RefLocation.Start < ref.RefLocation.Start {
-					found = true
-					ref = ref2
-				}
+		isNumericStyle := ctx.Metas["style"] == "" || ctx.Metas["style"] == IssueNameStyleNumeric
+		foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle)
+
+		switch ctx.Metas["style"] {
+		case "", IssueNameStyleNumeric:
+			found, ref = foundNumeric, refNumeric
+		case IssueNameStyleAlphanumeric:
+			found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
+		case IssueNameStyleRegexp:
+			pattern, err := regexplru.GetCompiled(ctx.Metas["regexp"])
+			if err != nil {
+				return
+			}
+			found, ref = references.FindRenderizableReferenceRegexp(node.Data, pattern)
+		}
+
+		// Repos with external issue trackers might still need to reference local PRs
+		// We need to concern with the first one that shows up in the text, whichever it is
+		if hasExtTrackFormat && !isNumericStyle {
+			// If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that
+			if foundNumeric && refNumeric.RefLocation.Start < ref.RefLocation.Start {
+				found = foundNumeric
+				ref = refNumeric
 			}
 		}
 		if !found {
@@ -836,7 +854,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
 
 		var link *html.Node
 		reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
-		if exttrack && !ref.IsPull {
+		if hasExtTrackFormat && !ref.IsPull {
 			ctx.Metas["index"] = ref.Issue
 
 			res, err := vars.Expand(ctx.Metas["format"], ctx.Metas)
@@ -869,7 +887,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
 
 		// Decorate action keywords if actionable
 		var keyword *html.Node
-		if references.IsXrefActionable(ref, exttrack, alphanum) {
+		if references.IsXrefActionable(ref, hasExtTrackFormat) {
 			keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
 		} else {
 			keyword = &html.Node{
diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go
index f0eb3253e..25b0f7b7a 100644
--- a/modules/markup/html_internal_test.go
+++ b/modules/markup/html_internal_test.go
@@ -21,8 +21,8 @@ const (
 	TestRepoURL = TestAppURL + TestOrgRepo + "/"
 )
 
-// alphanumLink an HTML link to an alphanumeric-style issue
-func alphanumIssueLink(baseURL, class, name string) string {
+// externalIssueLink an HTML link to an alphanumeric-style issue
+func externalIssueLink(baseURL, class, name string) string {
 	return link(util.URLJoin(baseURL, name), class, name)
 }
 
@@ -54,6 +54,13 @@ var alphanumericMetas = map[string]string{
 	"style":  IssueNameStyleAlphanumeric,
 }
 
+var regexpMetas = map[string]string{
+	"format": "https://someurl.com/{user}/{repo}/{index}",
+	"user":   "someUser",
+	"repo":   "someRepo",
+	"style":  IssueNameStyleRegexp,
+}
+
 // these values should match the TestOrgRepo const above
 var localMetas = map[string]string{
 	"user": "gogits",
@@ -184,7 +191,7 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
 	test := func(s, expectedFmt string, names ...string) {
 		links := make([]interface{}, len(names))
 		for i, name := range names {
-			links[i] = alphanumIssueLink("https://someurl.com/someUser/someRepo/", "ref-issue ref-external-issue", name)
+			links[i] = externalIssueLink("https://someurl.com/someUser/someRepo/", "ref-issue ref-external-issue", name)
 		}
 		expected := fmt.Sprintf(expectedFmt, links...)
 		testRenderIssueIndexPattern(t, s, expected, &RenderContext{Metas: alphanumericMetas})
@@ -194,6 +201,43 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
 	test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890")
 }
 
+func TestRender_IssueIndexPattern5(t *testing.T) {
+	setting.AppURL = TestAppURL
+
+	// regexp: render inputs without valid mentions
+	test := func(s, expectedFmt, pattern string, ids, names []string) {
+		metas := regexpMetas
+		metas["regexp"] = pattern
+		links := make([]interface{}, len(ids))
+		for i, id := range ids {
+			links[i] = link(util.URLJoin("https://someurl.com/someUser/someRepo/", id), "ref-issue ref-external-issue", names[i])
+		}
+
+		expected := fmt.Sprintf(expectedFmt, links...)
+		testRenderIssueIndexPattern(t, s, expected, &RenderContext{Metas: metas})
+	}
+
+	test("abc ISSUE-123 def", "abc %s def",
+		"ISSUE-(\\d+)",
+		[]string{"123"},
+		[]string{"ISSUE-123"},
+	)
+
+	test("abc (ISSUE 123) def", "abc %s def",
+		"\\(ISSUE (\\d+)\\)",
+		[]string{"123"},
+		[]string{"(ISSUE 123)"},
+	)
+
+	test("abc ISSUE-123 def", "abc %s def",
+		"(ISSUE-(\\d+))",
+		[]string{"ISSUE-123"},
+		[]string{"ISSUE-123"},
+	)
+
+	testRenderIssueIndexPattern(t, "will not match", "will not match", &RenderContext{Metas: regexpMetas})
+}
+
 func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
 	if ctx.URLPrefix == "" {
 		ctx.URLPrefix = TestAppURL
@@ -202,7 +246,7 @@ func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *Rend
 	var buf strings.Builder
 	err := postProcess(ctx, []processor{issueIndexPatternProcessor}, strings.NewReader(input), &buf)
 	assert.NoError(t, err)
-	assert.Equal(t, expected, buf.String())
+	assert.Equal(t, expected, buf.String(), "input=%q", input)
 }
 
 func TestRender_AutoLink(t *testing.T) {
diff --git a/modules/references/references.go b/modules/references/references.go
index 630e62104..7f5086d09 100644
--- a/modules/references/references.go
+++ b/modules/references/references.go
@@ -351,6 +351,24 @@ func FindRenderizableReferenceNumeric(content string, prOnly bool) (bool, *Rende
 	}
 }
 
+// FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string.
+func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bool, *RenderizableReference) {
+	match := pattern.FindStringSubmatchIndex(content)
+	if len(match) < 4 {
+		return false, nil
+	}
+
+	action, location := findActionKeywords([]byte(content), match[2])
+
+	return true, &RenderizableReference{
+		Issue:          content[match[2]:match[3]],
+		RefLocation:    &RefSpan{Start: match[0], End: match[1]},
+		Action:         action,
+		ActionLocation: location,
+		IsPull:         false,
+	}
+}
+
 // FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string.
 func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) {
 	match := issueAlphanumericPattern.FindStringSubmatchIndex(content)
@@ -547,7 +565,7 @@ func findActionKeywords(content []byte, start int) (XRefAction, *RefSpan) {
 }
 
 // IsXrefActionable returns true if the xref action is actionable (i.e. produces a result when resolved)
-func IsXrefActionable(ref *RenderizableReference, extTracker, alphaNum bool) bool {
+func IsXrefActionable(ref *RenderizableReference, extTracker bool) bool {
 	if extTracker {
 		// External issues cannot be automatically closed
 		return false
diff --git a/modules/regexplru/regexplru.go b/modules/regexplru/regexplru.go
new file mode 100644
index 000000000..97c7cff4c
--- /dev/null
+++ b/modules/regexplru/regexplru.go
@@ -0,0 +1,45 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package regexplru
+
+import (
+	"regexp"
+
+	"code.gitea.io/gitea/modules/log"
+
+	lru "github.com/hashicorp/golang-lru"
+)
+
+var lruCache *lru.Cache
+
+func init() {
+	var err error
+	lruCache, err = lru.New(1000)
+	if err != nil {
+		log.Fatal("failed to new LRU cache, err: %v", err)
+	}
+}
+
+// GetCompiled works like regexp.Compile, the compiled expr or error is stored in LRU cache
+func GetCompiled(expr string) (r *regexp.Regexp, err error) {
+	v, ok := lruCache.Get(expr)
+	if !ok {
+		r, err = regexp.Compile(expr)
+		if err != nil {
+			lruCache.Add(expr, err)
+			return nil, err
+		}
+		lruCache.Add(expr, r)
+	} else {
+		r, ok = v.(*regexp.Regexp)
+		if !ok {
+			if err, ok = v.(error); ok {
+				return nil, err
+			}
+			panic("impossible")
+		}
+	}
+	return r, nil
+}
diff --git a/modules/regexplru/regexplru_test.go b/modules/regexplru/regexplru_test.go
new file mode 100644
index 000000000..041f0dcfb
--- /dev/null
+++ b/modules/regexplru/regexplru_test.go
@@ -0,0 +1,27 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package regexplru
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRegexpLru(t *testing.T) {
+	r, err := GetCompiled("a")
+	assert.NoError(t, err)
+	assert.True(t, r.MatchString("a"))
+
+	r, err = GetCompiled("a")
+	assert.NoError(t, err)
+	assert.True(t, r.MatchString("a"))
+
+	assert.EqualValues(t, 1, lruCache.Len())
+
+	_, err = GetCompiled("(")
+	assert.Error(t, err)
+	assert.EqualValues(t, 2, lruCache.Len())
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index b9ba6e113..c4ad71471 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1811,6 +1811,9 @@ settings.tracker_url_format_error = The external issue tracker URL format is not
 settings.tracker_issue_style = External Issue Tracker Number Format
 settings.tracker_issue_style.numeric = Numeric
 settings.tracker_issue_style.alphanumeric = Alphanumeric
+settings.tracker_issue_style.regexp = Regular Expression
+settings.tracker_issue_style.regexp_pattern = Regular Expression Pattern
+settings.tracker_issue_style.regexp_pattern_desc = The first captured group will be used in place of {index}.
 settings.tracker_url_format_desc = Use the placeholders {user}, {repo} and {index} for the username, repository name and issue index.
 settings.enable_timetracker = Enable Time Tracking
 settings.allow_only_contributors_to_track_time = Let Only Contributors Track Time
diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go
index 1a7a41ae9..f49ef6e85 100644
--- a/routers/web/repo/setting.go
+++ b/routers/web/repo/setting.go
@@ -434,9 +434,10 @@ func SettingsPost(ctx *context.Context) {
 				RepoID: repo.ID,
 				Type:   unit_model.TypeExternalTracker,
 				Config: &repo_model.ExternalTrackerConfig{
-					ExternalTrackerURL:    form.ExternalTrackerURL,
-					ExternalTrackerFormat: form.TrackerURLFormat,
-					ExternalTrackerStyle:  form.TrackerIssueStyle,
+					ExternalTrackerURL:           form.ExternalTrackerURL,
+					ExternalTrackerFormat:        form.TrackerURLFormat,
+					ExternalTrackerStyle:         form.TrackerIssueStyle,
+					ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
 				},
 			})
 			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 2bcb91f8c..738a77d2b 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -141,6 +141,7 @@ type RepoSettingForm struct {
 	ExternalTrackerURL                    string
 	TrackerURLFormat                      string
 	TrackerIssueStyle                     string
+	ExternalTrackerRegexpPattern          string
 	EnableCloseIssuesViaCommitInAnyBranch bool
 	EnableProjects                        bool
 	EnablePackages                        bool
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index f44d9c98a..67a98aff4 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -361,16 +361,27 @@
 								
{{.i18n.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc" | Str2html}}