diff --git a/.forgejo/workflows/build-release.yml b/.forgejo/workflows/build-release.yml
index 0d7f94c5a6..1a98aebdf6 100644
--- a/.forgejo/workflows/build-release.yml
+++ b/.forgejo/workflows/build-release.yml
@@ -164,7 +164,7 @@ jobs:
- name: build container & release
if: ${{ secrets.TOKEN != '' }}
- uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.1
+ uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.4
with:
forgejo: "${{ env.GITHUB_SERVER_URL }}"
owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"
@@ -183,7 +183,7 @@ jobs:
- name: build rootless container
if: ${{ secrets.TOKEN != '' }}
- uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.1
+ uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.4
with:
forgejo: "${{ env.GITHUB_SERVER_URL }}"
owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"
diff --git a/.forgejo/workflows/publish-release.yml b/.forgejo/workflows/publish-release.yml
index b44d670fc4..0863a1597c 100644
--- a/.forgejo/workflows/publish-release.yml
+++ b/.forgejo/workflows/publish-release.yml
@@ -42,7 +42,7 @@ jobs:
- uses: https://data.forgejo.org/actions/checkout@v4
- name: copy & sign
- uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.3.1
+ uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.3.4
with:
from-forgejo: ${{ vars.FORGEJO }}
to-forgejo: ${{ vars.FORGEJO }}
diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml
index 4385cafa3f..e17c77beb0 100644
--- a/.forgejo/workflows/renovate.yml
+++ b/.forgejo/workflows/renovate.yml
@@ -28,7 +28,7 @@ jobs:
runs-on: docker
container:
- image: data.forgejo.org/renovate/renovate:39.171.2
+ image: data.forgejo.org/renovate/renovate:39.178.1
steps:
- name: Load renovate repo cache
diff --git a/Dockerfile b/Dockerfile
index 4b2eca7a4c..ebe41ed5c8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
-FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.23-alpine3.21 AS build-env
+FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 AS build-env
ARG GOPROXY
ENV GOPROXY=${GOPROXY:-direct}
diff --git a/Dockerfile.rootless b/Dockerfile.rootless
index 394eae0526..93004610a7 100644
--- a/Dockerfile.rootless
+++ b/Dockerfile.rootless
@@ -1,6 +1,6 @@
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
-FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.23-alpine3.21 AS build-env
+FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 AS build-env
ARG GOPROXY
ENV GOPROXY=${GOPROXY:-direct}
@@ -50,6 +50,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
FROM data.forgejo.org/oci/alpine:3.21
+ARG RELEASE_VERSION
LABEL maintainer="contact@forgejo.org" \
org.opencontainers.image.authors="Forgejo" \
org.opencontainers.image.url="https://forgejo.org" \
diff --git a/Makefile b/Makefile
index 3966463dac..830a30a924 100644
--- a/Makefile
+++ b/Makefile
@@ -48,8 +48,8 @@ GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 # renovate: datasour
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.30.0 # renovate: datasource=go
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.4.0 # renovate: datasource=go
-GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.18.0 # renovate: datasource=go
-RENOVATE_NPM_PACKAGE ?= renovate@39.171.2 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
+GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.18.1 # renovate: datasource=go
+RENOVATE_NPM_PACKAGE ?= renovate@39.178.1 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...
diff --git a/go.mod b/go.mod
index 5f6eccb856..351f736389 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,8 @@
module code.gitea.io/gitea
-go 1.23
+go 1.24
-toolchain go1.23.5
+toolchain go1.24.0
require (
code.forgejo.org/f3/gof3/v3 v3.10.2
@@ -84,7 +84,7 @@ require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0
github.com/pquerna/otp v1.4.0
- github.com/prometheus/client_golang v1.20.5
+ github.com/prometheus/client_golang v1.21.0
github.com/redis/go-redis/v9 v9.7.0
github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
@@ -101,7 +101,7 @@ require (
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go v0.119.0
go.uber.org/mock v0.4.0
- golang.org/x/crypto v0.33.0
+ golang.org/x/crypto v0.35.0
golang.org/x/image v0.23.0
golang.org/x/net v0.35.0
golang.org/x/oauth2 v0.24.0
@@ -239,7 +239,7 @@ require (
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
- github.com/prometheus/common v0.55.0 // indirect
+ github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rhysd/actionlint v1.6.27 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
diff --git a/go.sum b/go.sum
index b8eb73f65a..d64b8a74c1 100644
--- a/go.sum
+++ b/go.sum
@@ -1331,15 +1331,15 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
-github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
-github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
+github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
-github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
+github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
@@ -1507,8 +1507,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
-golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
+golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
diff --git a/models/fixtures/secret.yml b/models/fixtures/secret.yml
new file mode 100644
index 0000000000..ca780a73aa
--- /dev/null
+++ b/models/fixtures/secret.yml
@@ -0,0 +1 @@
+[] # empty
diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go
index 5552a9deb0..3dcc9fe3a4 100644
--- a/modules/indexer/issues/bleve/bleve.go
+++ b/modules/indexer/issues/bleve/bleve.go
@@ -156,25 +156,32 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
var queries []query.Query
if options.Keyword != "" {
- if options.IsFuzzyKeyword {
- fuzziness := 1
- if kl := len(options.Keyword); kl > 3 {
- fuzziness = 2
- } else if kl < 2 {
- fuzziness = 0
- }
- queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{
- inner_bleve.MatchQuery(options.Keyword, "title", issueIndexerAnalyzer, fuzziness),
- inner_bleve.MatchQuery(options.Keyword, "content", issueIndexerAnalyzer, fuzziness),
- inner_bleve.MatchQuery(options.Keyword, "comments", issueIndexerAnalyzer, fuzziness),
- }...))
- } else {
- queries = append(queries, bleve.NewDisjunctionQuery([]query.Query{
- inner_bleve.MatchPhraseQuery(options.Keyword, "title", issueIndexerAnalyzer, 0),
- inner_bleve.MatchPhraseQuery(options.Keyword, "content", issueIndexerAnalyzer, 0),
- inner_bleve.MatchPhraseQuery(options.Keyword, "comments", issueIndexerAnalyzer, 0),
- }...))
+ tokens, err := options.Tokens()
+ if err != nil {
+ return nil, err
}
+ q := bleve.NewBooleanQuery()
+ for _, token := range tokens {
+ fuzziness := 0
+ if token.Fuzzy {
+ // TODO: replace with "auto" after bleve update
+ fuzziness = min(len(token.Term)/4, 2)
+ }
+ innerQ := bleve.NewDisjunctionQuery(
+ inner_bleve.MatchPhraseQuery(token.Term, "title", issueIndexerAnalyzer, fuzziness),
+ inner_bleve.MatchPhraseQuery(token.Term, "content", issueIndexerAnalyzer, fuzziness),
+ inner_bleve.MatchPhraseQuery(token.Term, "comments", issueIndexerAnalyzer, fuzziness))
+
+ switch token.Kind {
+ case internal.BoolOptMust:
+ q.AddMust(innerQ)
+ case internal.BoolOptShould:
+ q.AddShould(innerQ)
+ case internal.BoolOptNot:
+ q.AddMustNot(innerQ)
+ }
+ }
+ queries = append(queries, q)
}
if len(options.RepoIDs) > 0 || options.AllPublic {
diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go
index 24e1ac8855..221ae6dd2f 100644
--- a/modules/indexer/issues/elasticsearch/elasticsearch.go
+++ b/modules/indexer/issues/elasticsearch/elasticsearch.go
@@ -23,6 +23,10 @@ const (
// Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types
esMultiMatchTypeBestFields = "best_fields"
esMultiMatchTypePhrasePrefix = "phrase_prefix"
+
+ // fuzziness options
+ // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/common-options.html#fuzziness
+ esFuzzyAuto = "AUTO"
)
var _ internal.Indexer = &Indexer{}
@@ -145,12 +149,30 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
query := elastic.NewBoolQuery()
if options.Keyword != "" {
- searchType := esMultiMatchTypePhrasePrefix
- if options.IsFuzzyKeyword {
- searchType = esMultiMatchTypeBestFields
+ q := elastic.NewBoolQuery()
+ tokens, err := options.Tokens()
+ if err != nil {
+ return nil, err
}
+ for _, token := range tokens {
+ innerQ := elastic.NewMultiMatchQuery(token.Term, "title", "content", "comments")
+ if token.Fuzzy {
+ // If the term is not a phrase use fuzziness set to AUTO
+ innerQ = innerQ.Type(esMultiMatchTypeBestFields).Fuzziness(esFuzzyAuto)
+ } else {
+ innerQ = innerQ.Type(esMultiMatchTypePhrasePrefix)
+ }
- query.Must(elastic.NewMultiMatchQuery(options.Keyword, "title", "content", "comments").Type(searchType))
+ switch token.Kind {
+ case internal.BoolOptMust:
+ q.Must(innerQ)
+ case internal.BoolOptShould:
+ q.Should(innerQ)
+ case internal.BoolOptNot:
+ q.MustNot(innerQ)
+ }
+ }
+ query.Must(q)
}
if len(options.RepoIDs) > 0 {
diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go
index dda2b7a5c1..0751060afc 100644
--- a/modules/indexer/issues/internal/model.go
+++ b/modules/indexer/issues/internal/model.go
@@ -74,8 +74,6 @@ type SearchResult struct {
type SearchOptions struct {
Keyword string // keyword to search
- IsFuzzyKeyword bool // if false the levenshtein distance is 0
-
RepoIDs []int64 // repository IDs which the issues belong to
AllPublic bool // if include all public repositories
diff --git a/modules/indexer/issues/internal/qstring.go b/modules/indexer/issues/internal/qstring.go
new file mode 100644
index 0000000000..fdb89b09e9
--- /dev/null
+++ b/modules/indexer/issues/internal/qstring.go
@@ -0,0 +1,112 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package internal
+
+import (
+ "io"
+ "strings"
+)
+
+type BoolOpt int
+
+const (
+ BoolOptMust BoolOpt = iota
+ BoolOptShould
+ BoolOptNot
+)
+
+type Token struct {
+ Term string
+ Kind BoolOpt
+ Fuzzy bool
+}
+
+type Tokenizer struct {
+ in *strings.Reader
+}
+
+func (t *Tokenizer) next() (tk Token, err error) {
+ var (
+ sb strings.Builder
+ r rune
+ )
+ tk.Kind = BoolOptShould
+ tk.Fuzzy = true
+
+ // skip all leading white space
+ for {
+ if r, _, err = t.in.ReadRune(); err == nil && r == ' ' {
+ //nolint:staticcheck,wastedassign // SA4006 the variable is used after the loop
+ r, _, err = t.in.ReadRune()
+ continue
+ }
+ break
+ }
+ if err != nil {
+ return tk, err
+ }
+
+ // check for +/- op, increment to the next rune in both cases
+ switch r {
+ case '+':
+ tk.Kind = BoolOptMust
+ r, _, err = t.in.ReadRune()
+ case '-':
+ tk.Kind = BoolOptNot
+ r, _, err = t.in.ReadRune()
+ }
+ if err != nil {
+ return tk, err
+ }
+
+ // parse the string, escaping special characters
+ for esc := false; err == nil; r, _, err = t.in.ReadRune() {
+ if esc {
+ if !strings.ContainsRune("+-\\\"", r) {
+ sb.WriteRune('\\')
+ }
+ sb.WriteRune(r)
+ esc = false
+ continue
+ }
+ switch r {
+ case '\\':
+ esc = true
+ case '"':
+ if !tk.Fuzzy {
+ goto nextEnd
+ }
+ tk.Fuzzy = false
+ case ' ', '\t':
+ if tk.Fuzzy {
+ goto nextEnd
+ }
+ sb.WriteRune(r)
+ default:
+ sb.WriteRune(r)
+ }
+ }
+nextEnd:
+
+ tk.Term = sb.String()
+ if err == io.EOF {
+ err = nil
+ } // do not consider EOF as an error at the end
+ return tk, err
+}
+
+// Tokenize the keyword
+func (o *SearchOptions) Tokens() (tokens []Token, err error) {
+ in := strings.NewReader(o.Keyword)
+ it := Tokenizer{in: in}
+
+ for token, err := it.next(); err == nil; token, err = it.next() {
+ tokens = append(tokens, token)
+ }
+ if err != nil && err != io.EOF {
+ return nil, err
+ }
+
+ return tokens, nil
+}
diff --git a/modules/indexer/issues/internal/qstring_test.go b/modules/indexer/issues/internal/qstring_test.go
new file mode 100644
index 0000000000..a911b86e2f
--- /dev/null
+++ b/modules/indexer/issues/internal/qstring_test.go
@@ -0,0 +1,171 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package internal
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type testIssueQueryStringOpt struct {
+ Keyword string
+ Results []Token
+}
+
+var testOpts = []testIssueQueryStringOpt{
+ {
+ Keyword: "Hello",
+ Results: []Token{
+ {
+ Term: "Hello",
+ Fuzzy: true,
+ Kind: BoolOptShould,
+ },
+ },
+ },
+ {
+ Keyword: "Hello World",
+ Results: []Token{
+ {
+ Term: "Hello",
+ Fuzzy: true,
+ Kind: BoolOptShould,
+ },
+ {
+ Term: "World",
+ Fuzzy: true,
+ Kind: BoolOptShould,
+ },
+ },
+ },
+ {
+ Keyword: "+Hello +World",
+ Results: []Token{
+ {
+ Term: "Hello",
+ Fuzzy: true,
+ Kind: BoolOptMust,
+ },
+ {
+ Term: "World",
+ Fuzzy: true,
+ Kind: BoolOptMust,
+ },
+ },
+ },
+ {
+ Keyword: "+Hello World",
+ Results: []Token{
+ {
+ Term: "Hello",
+ Fuzzy: true,
+ Kind: BoolOptMust,
+ },
+ {
+ Term: "World",
+ Fuzzy: true,
+ Kind: BoolOptShould,
+ },
+ },
+ },
+ {
+ Keyword: "+Hello -World",
+ Results: []Token{
+ {
+ Term: "Hello",
+ Fuzzy: true,
+ Kind: BoolOptMust,
+ },
+ {
+ Term: "World",
+ Fuzzy: true,
+ Kind: BoolOptNot,
+ },
+ },
+ },
+ {
+ Keyword: "\"Hello World\"",
+ Results: []Token{
+ {
+ Term: "Hello World",
+ Fuzzy: false,
+ Kind: BoolOptShould,
+ },
+ },
+ },
+ {
+ Keyword: "+\"Hello World\"",
+ Results: []Token{
+ {
+ Term: "Hello World",
+ Fuzzy: false,
+ Kind: BoolOptMust,
+ },
+ },
+ },
+ {
+ Keyword: "-\"Hello World\"",
+ Results: []Token{
+ {
+ Term: "Hello World",
+ Fuzzy: false,
+ Kind: BoolOptNot,
+ },
+ },
+ },
+ {
+ Keyword: "\"+Hello -World\"",
+ Results: []Token{
+ {
+ Term: "+Hello -World",
+ Fuzzy: false,
+ Kind: BoolOptShould,
+ },
+ },
+ },
+ {
+ Keyword: "\\+Hello", // \+Hello => +Hello
+ Results: []Token{
+ {
+ Term: "+Hello",
+ Fuzzy: true,
+ Kind: BoolOptShould,
+ },
+ },
+ },
+ {
+ Keyword: "\\\\Hello", // \\Hello => \Hello
+ Results: []Token{
+ {
+ Term: "\\Hello",
+ Fuzzy: true,
+ Kind: BoolOptShould,
+ },
+ },
+ },
+ {
+ Keyword: "\\\"Hello", // \"Hello => "Hello
+ Results: []Token{
+ {
+ Term: "\"Hello",
+ Fuzzy: true,
+ Kind: BoolOptShould,
+ },
+ },
+ },
+}
+
+func TestIssueQueryString(t *testing.T) {
+ var opt SearchOptions
+ for _, res := range testOpts {
+ t.Run(opt.Keyword, func(t *testing.T) {
+ opt.Keyword = res.Keyword
+ tokens, err := opt.Tokens()
+ require.NoError(t, err)
+ assert.Equal(t, res.Results, tokens)
+ })
+ }
+}
diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go
index e8e6a4e7d1..0763cd1297 100644
--- a/modules/indexer/issues/internal/tests/tests.go
+++ b/modules/indexer/issues/internal/tests/tests.go
@@ -131,6 +131,20 @@ var cases = []*testIndexerCase{
ExpectedIDs: []int64{1002, 1001, 1000},
ExpectedTotal: 3,
},
+ {
+ Name: "Keyword Exclude",
+ ExtraData: []*internal.IndexerData{
+ {ID: 1000, Title: "hi hello world"},
+ {ID: 1001, Content: "hi hello world"},
+ {ID: 1002, Comments: []string{"hello", "hello world"}},
+ },
+ SearchOptions: &internal.SearchOptions{
+ Keyword: "hello world -hi",
+ SortBy: internal.SortByCreatedDesc,
+ },
+ ExpectedIDs: []int64{1002},
+ ExpectedTotal: 1,
+ },
{
Name: "Keyword Fuzzy",
ExtraData: []*internal.IndexerData{
@@ -139,9 +153,8 @@ var cases = []*testIndexerCase{
{ID: 1002, Comments: []string{"hi", "hello world"}},
},
SearchOptions: &internal.SearchOptions{
- Keyword: "hello world",
- SortBy: internal.SortByCreatedDesc,
- IsFuzzyKeyword: true,
+ Keyword: "hello world",
+ SortBy: internal.SortByCreatedDesc,
},
ExpectedIDs: []int64{1002, 1001, 1000},
ExpectedTotal: 3,
diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go
index 7c291198f1..951f3c8bfb 100644
--- a/modules/indexer/issues/meilisearch/meilisearch.go
+++ b/modules/indexer/issues/meilisearch/meilisearch.go
@@ -232,20 +232,36 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
limit = 1
}
- keyword := options.Keyword
- if !options.IsFuzzyKeyword {
- // to make it non fuzzy ("typo tolerance" in meilisearch terms), we have to quote the keyword(s)
- // https://www.meilisearch.com/docs/reference/api/search#phrase-search
- keyword = doubleQuoteKeyword(keyword)
+ var keywords []string
+ if options.Keyword != "" {
+ tokens, err := options.Tokens()
+ if err != nil {
+ return nil, err
+ }
+ for _, token := range tokens {
+ if !token.Fuzzy {
+ // to make it a phrase search, we have to quote the keyword(s)
+ // https://www.meilisearch.com/docs/reference/api/search#phrase-search
+ token.Term = doubleQuoteKeyword(token.Term)
+ }
+
+ // internal.BoolOptShould (Default, requires no modifications)
+ // internal.BoolOptMust (Not supported by meilisearch)
+ if token.Kind == internal.BoolOptNot {
+ token.Term = "-" + token.Term
+ }
+ keywords = append(keywords, token.Term)
+ }
}
- searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(keyword, &meilisearch.SearchRequest{
- Filter: query.Statement(),
- Limit: int64(limit),
- Offset: int64(skip),
- Sort: sortBy,
- MatchingStrategy: meilisearch.All,
- })
+ searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).
+ Search(strings.Join(keywords, " "), &meilisearch.SearchRequest{
+ Filter: query.Statement(),
+ Limit: int64(limit),
+ Offset: int64(skip),
+ Sort: sortBy,
+ MatchingStrategy: meilisearch.All,
+ })
if err != nil {
return nil, err
}
diff --git a/modules/setting/service.go b/modules/setting/service.go
index 7a907023c4..cc84ac7257 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -1,9 +1,11 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
+// Copyright 2025 The Forgejo Authors. All rights reserved
// SPDX-License-Identifier: MIT
package setting
import (
+ "net/url"
"regexp"
"slices"
"strings"
@@ -145,6 +147,20 @@ func LoadServiceSetting() {
loadServiceFrom(CfgProvider)
}
+func appURLAsGlob(fqdn string) (glob.Glob, error) {
+ localFqdn, err := url.ParseRequestURI(fqdn)
+ if err != nil {
+ log.Error("Error in EmailDomainAllowList: %v", err)
+ return nil, err
+ }
+ appFqdn, err := glob.Compile(localFqdn.Hostname(), ',')
+ if err != nil {
+ log.Error("Error in EmailDomainAllowList: %v", err)
+ return nil, err
+ }
+ return appFqdn, nil
+}
+
func loadServiceFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("service")
Service.ActiveCodeLives = sec.Key("ACTIVE_CODE_LIVE_MINUTES").MustInt(180)
@@ -164,7 +180,15 @@ func loadServiceFrom(rootCfg ConfigProvider) {
if sec.HasKey("EMAIL_DOMAIN_WHITELIST") {
deprecatedSetting(rootCfg, "service", "EMAIL_DOMAIN_WHITELIST", "service", "EMAIL_DOMAIN_ALLOWLIST", "1.21")
}
- Service.EmailDomainAllowList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_WHITELIST", "EMAIL_DOMAIN_ALLOWLIST")
+ emailDomainAllowList := CompileEmailGlobList(sec, "EMAIL_DOMAIN_WHITELIST", "EMAIL_DOMAIN_ALLOWLIST")
+
+ if len(emailDomainAllowList) > 0 && Federation.Enabled {
+ appURL, err := appURLAsGlob(AppURL)
+ if err == nil {
+ emailDomainAllowList = append(emailDomainAllowList, appURL)
+ }
+ }
+ Service.EmailDomainAllowList = emailDomainAllowList
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
Service.EmailDomainBlockDisposable = sec.Key("EMAIL_DOMAIN_BLOCK_DISPOSABLE").MustBool(false)
if Service.EmailDomainBlockDisposable {
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index c9d30836ac..8350b914c5 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -1,5 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2017 The Gitea Authors. All rights reserved.
+// Copyright 2025 The Forgejo Authors. All rights reserved
// SPDX-License-Identifier: MIT
package setting
@@ -210,6 +211,7 @@ func LoadSettings() {
initAllLoggers()
loadDBSetting(CfgProvider)
+ loadFederationFrom(CfgProvider)
loadServiceFrom(CfgProvider)
loadOAuth2ClientFrom(CfgProvider)
loadCacheFrom(CfgProvider)
@@ -224,7 +226,6 @@ func LoadSettings() {
LoadQueueSettings()
loadProjectFrom(CfgProvider)
loadMimeTypeMapFrom(CfgProvider)
- loadFederationFrom(CfgProvider)
loadF3From(CfgProvider)
}
diff --git a/modules/setting/setting_test.go b/modules/setting/setting_test.go
index f77ee65974..6801844729 100644
--- a/modules/setting/setting_test.go
+++ b/modules/setting/setting_test.go
@@ -1,4 +1,5 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
+// Copyright 2025 The Forgejo Authors. All rights reserved
// SPDX-License-Identifier: MIT
package setting
@@ -9,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/json"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestMakeAbsoluteAssetURL(t *testing.T) {
@@ -30,3 +32,84 @@ func TestMakeManifestData(t *testing.T) {
jsonBytes := MakeManifestData(`Example App '\"`, "https://example.com", "https://example.com/foo/bar")
assert.True(t, json.Valid(jsonBytes))
}
+
+func TestLoadServiceDomainListsForFederation(t *testing.T) {
+ oldAppURL := AppURL
+ oldFederation := Federation
+ oldService := Service
+
+ defer func() {
+ AppURL = oldAppURL
+ Federation = oldFederation
+ Service = oldService
+ }()
+
+ cfg, err := NewConfigProviderFromData(`
+[federation]
+ENABLED = true
+[service]
+EMAIL_DOMAIN_ALLOWLIST = *.allow.random
+EMAIL_DOMAIN_BLOCKLIST = *.block.random
+`)
+
+ require.NoError(t, err)
+ loadServerFrom(cfg)
+ loadFederationFrom(cfg)
+ loadServiceFrom(cfg)
+
+ assert.True(t, match(Service.EmailDomainAllowList, "d1.allow.random"))
+ assert.True(t, match(Service.EmailDomainAllowList, "localhost"))
+}
+
+func TestLoadServiceDomainListsNoFederation(t *testing.T) {
+ oldAppURL := AppURL
+ oldFederation := Federation
+ oldService := Service
+
+ defer func() {
+ AppURL = oldAppURL
+ Federation = oldFederation
+ Service = oldService
+ }()
+
+ cfg, err := NewConfigProviderFromData(`
+[federation]
+ENABLED = false
+[service]
+EMAIL_DOMAIN_ALLOWLIST = *.allow.random
+EMAIL_DOMAIN_BLOCKLIST = *.block.random
+`)
+
+ require.NoError(t, err)
+ loadServerFrom(cfg)
+ loadFederationFrom(cfg)
+ loadServiceFrom(cfg)
+
+ assert.True(t, match(Service.EmailDomainAllowList, "d1.allow.random"))
+}
+
+func TestLoadServiceDomainListsFederationEmptyAllowList(t *testing.T) {
+ oldAppURL := AppURL
+ oldFederation := Federation
+ oldService := Service
+
+ defer func() {
+ AppURL = oldAppURL
+ Federation = oldFederation
+ Service = oldService
+ }()
+
+ cfg, err := NewConfigProviderFromData(`
+[federation]
+ENABLED = true
+[service]
+EMAIL_DOMAIN_BLOCKLIST = *.block.random
+`)
+
+ require.NoError(t, err)
+ loadServerFrom(cfg)
+ loadFederationFrom(cfg)
+ loadServiceFrom(cfg)
+
+ assert.Empty(t, Service.EmailDomainAllowList)
+}
diff --git a/modules/validation/email.go b/modules/validation/email.go
index bef816586f..326e93378a 100644
--- a/modules/validation/email.go
+++ b/modules/validation/email.go
@@ -1,5 +1,6 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors. All rights reserved
// SPDX-License-Identifier: MIT
package validation
@@ -100,11 +101,25 @@ func validateEmailDomain(email string) error {
}
func IsEmailDomainAllowed(email string) bool {
- if len(setting.Service.EmailDomainAllowList) == 0 {
- return !isEmailDomainListed(setting.Service.EmailDomainBlockList, email)
- }
+ return isEmailDomainAllowedInternal(
+ email,
+ setting.Service.EmailDomainAllowList,
+ setting.Service.EmailDomainBlockList)
+}
- return isEmailDomainListed(setting.Service.EmailDomainAllowList, email)
+func isEmailDomainAllowedInternal(
+ email string,
+ emailDomainAllowList []glob.Glob,
+ emailDomainBlockList []glob.Glob,
+) bool {
+ var result bool
+
+ if len(emailDomainAllowList) == 0 {
+ result = !isEmailDomainListed(emailDomainBlockList, email)
+ } else {
+ result = isEmailDomainListed(emailDomainAllowList, email)
+ }
+ return result
}
// isEmailDomainListed checks whether the domain of an email address
diff --git a/modules/validation/email_test.go b/modules/validation/email_test.go
index e5125a9357..ffdc6fd4ee 100644
--- a/modules/validation/email_test.go
+++ b/modules/validation/email_test.go
@@ -1,4 +1,5 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors. All rights reserved
// SPDX-License-Identifier: MIT
package validation
@@ -65,3 +66,8 @@ func TestEmailAddressValidate(t *testing.T) {
})
}
}
+
+func TestEmailDomainAllowList(t *testing.T) {
+ res := IsEmailDomainAllowed("someuser@localhost.localdomain")
+ assert.True(t, res)
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index c46a409178..bd7b91cd5a 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -119,7 +119,7 @@ preview = Preview
loading = Loading…
error = Error
-error404 = The page you are trying to reach either does not exist or you are not authorized to view it.
+error404 = The page you are trying to reach either does not exist, has been removed or you are not authorized to view it.
error413 = You have exhausted your quota.
go_back = Go Back
invalid_data = Invalid data: %v
@@ -1190,8 +1190,8 @@ template.issue_labels = Issue labels
template.one_item = Must select at least one template item
template.invalid = Must select a template repository
-archive.title = This repository is archived. You can view files and clone it, but you cannot make any changes to the state of this repository, such as pushing and creating new issues, pull requests or comments.
-archive.title_date = This repository has been archived on %s. You can view files and clone it, but you cannot make any changes to the state of this repository, such as pushing and creating new issues, pull requests or comments.
+archive.title = This repository is archived. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
+archive.title_date = This repository has been archived on %s. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
archive.issue.nocomment = This repository is archived. You cannot comment on issues.
archive.pull.nocomment = This repository is archived. You cannot comment on pull requests.
archive.pull.noreview = This repository is archived. You cannot review pull requests.
diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json
index f8b2bcd0f6..cc06102c46 100644
--- a/options/locale_next/locale_en-US.json
+++ b/options/locale_next/locale_en-US.json
@@ -1,4 +1,9 @@
{
+ "home.welcome.no_activity": "No activity",
+ "home.welcome.activity_hint": "There is nothing in your feed yet. Your actions and activity from repositories that you watch will show up here.",
+ "home.explore_repos": "Explore repositories",
+ "home.explore_users": "Explore users",
+ "home.explore_orgs": "Explore organizations",
"repo.pulls.merged_title_desc": {
"one": "merged %[1]d commit from %[2]s
into %[3]s
%[4]s",
"other": "merged %[1]d commits from %[2]s
into %[3]s
%[4]s"
diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go
index 53761a07e9..9c27078326 100644
--- a/routers/api/v1/shared/runners.go
+++ b/routers/api/v1/shared/runners.go
@@ -34,13 +34,6 @@ func GetRegistrationToken(ctx *context.APIContext, ownerID, repoID int64) {
ctx.JSON(http.StatusOK, RegistrationToken{Token: token.Token})
}
-// RunJobList is a list of action run jobs
-// swagger:response RunJobList
-type RunJobList struct {
- // in:body
- Body []*structs.ActionRunJob `json:"body"`
-}
-
func GetActionRunJobs(ctx *context.APIContext, ownerID, repoID int64) {
labels := strings.Split(ctx.FormTrim("labels"), ",")
@@ -54,8 +47,7 @@ func GetActionRunJobs(ctx *context.APIContext, ownerID, repoID int64) {
return
}
- res := new(RunJobList)
- res.Body = fromRunJobModelToResponse(total, labels)
+ res := fromRunJobModelToResponse(total, labels)
ctx.JSON(http.StatusOK, res)
}
diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go
index 665f4d0b85..e7ec6a81e4 100644
--- a/routers/api/v1/swagger/action.go
+++ b/routers/api/v1/swagger/action.go
@@ -32,3 +32,10 @@ type swaggerResponseVariableList struct {
// in:body
Body []api.ActionVariable `json:"body"`
}
+
+// RunJobList is a list of action run jobs
+// swagger:response RunJobList
+type swaggerRunJobList struct {
+ // in:body
+ Body []*api.ActionRunJob `json:"body"`
+}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 5f9b952119..b4c673ffd7 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -204,8 +204,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
keyword = ""
}
- isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
-
var mileIDs []int64
if milestoneID > 0 || milestoneID == db.NoConditionID { // -1 to get those issues which have no any milestone assigned
mileIDs = []int64{milestoneID}
@@ -226,7 +224,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
IssueIDs: nil,
}
if keyword != "" {
- allIssueIDs, err := issueIDsFromSearch(ctx, keyword, isFuzzy, statsOpts)
+ allIssueIDs, err := issueIDsFromSearch(ctx, keyword, statsOpts)
if err != nil {
if issue_indexer.IsAvailable(ctx) {
ctx.ServerError("issueIDsFromSearch", err)
@@ -294,7 +292,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
var issues issues_model.IssueList
{
- ids, err := issueIDsFromSearch(ctx, keyword, isFuzzy, &issues_model.IssuesOptions{
+ ids, err := issueIDsFromSearch(ctx, keyword, &issues_model.IssuesOptions{
Paginator: &db.ListOptions{
Page: pager.Paginater.Current(),
PageSize: setting.UI.IssuePagingNum,
@@ -458,16 +456,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["OpenCount"] = issueStats.OpenCount
ctx.Data["ClosedCount"] = issueStats.ClosedCount
ctx.Data["AllCount"] = issueStats.AllCount
- linkStr := "?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&fuzzy=%t&archived=%t"
+ linkStr := "?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t"
ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels),
- milestoneID, projectID, assigneeID, posterID, isFuzzy, archived)
+ milestoneID, projectID, assigneeID, posterID, archived)
ctx.Data["OpenLink"] = fmt.Sprintf(linkStr,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels),
- milestoneID, projectID, assigneeID, posterID, isFuzzy, archived)
+ milestoneID, projectID, assigneeID, posterID, archived)
ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr,
url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels),
- milestoneID, projectID, assigneeID, posterID, isFuzzy, archived)
+ milestoneID, projectID, assigneeID, posterID, archived)
ctx.Data["SelLabelIDs"] = labelIDs
ctx.Data["SelectLabels"] = selectLabels
ctx.Data["ViewType"] = viewType
@@ -476,7 +474,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["ProjectID"] = projectID
ctx.Data["AssigneeID"] = assigneeID
ctx.Data["PosterID"] = posterID
- ctx.Data["IsFuzzy"] = isFuzzy
ctx.Data["Keyword"] = keyword
ctx.Data["IsShowClosed"] = isShowClosed
switch {
@@ -499,17 +496,12 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
pager.AddParam(ctx, "assignee", "AssigneeID")
pager.AddParam(ctx, "poster", "PosterID")
pager.AddParam(ctx, "archived", "ShowArchivedLabels")
- pager.AddParam(ctx, "fuzzy", "IsFuzzy")
ctx.Data["Page"] = pager
}
-func issueIDsFromSearch(ctx *context.Context, keyword string, fuzzy bool, opts *issues_model.IssuesOptions) ([]int64, error) {
- ids, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts).Copy(
- func(o *issue_indexer.SearchOptions) {
- o.IsFuzzyKeyword = fuzzy
- },
- ))
+func issueIDsFromSearch(ctx *context.Context, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) {
+ ids, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts))
if err != nil {
return nil, fmt.Errorf("SearchIssues: %w", err)
}
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index d67af29071..a0841c0227 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -90,6 +90,8 @@ func Dashboard(ctx *context.Context) {
cnt, _ := organization.GetOrganizationCount(ctx, ctxUser)
ctx.Data["UserOrgsCount"] = cnt
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
+ ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
+ ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
ctx.Data["Date"] = date
var uid int64
@@ -463,8 +465,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
User: ctx.Doer,
}
- isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
-
// Search all repositories which
//
// As user:
@@ -594,9 +594,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// USING FINAL STATE OF opts FOR A QUERY.
var issues issues_model.IssueList
{
- issueIDs, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts).Copy(
- func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy },
- ))
+ issueIDs, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts))
if err != nil {
ctx.ServerError("issueIDsFromSearch", err)
return
@@ -622,9 +620,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// -------------------------------
// Fill stats to post to ctx.Data.
// -------------------------------
- issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
- func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy },
- ))
+ issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts))
if err != nil {
ctx.ServerError("getUserIssueStats", err)
return
@@ -679,7 +675,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["SelectLabels"] = selectedLabels
ctx.Data["PageIsOrgIssues"] = org != nil
- ctx.Data["IsFuzzy"] = isFuzzy
if isShowClosed {
ctx.Data["State"] = "closed"
@@ -695,7 +690,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
pager.AddParam(ctx, "labels", "SelectLabels")
pager.AddParam(ctx, "milestone", "MilestoneID")
pager.AddParam(ctx, "assignee", "AssigneeID")
- pager.AddParam(ctx, "fuzzy", "IsFuzzy")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplIssues)
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index 7bf4ee4a8e..871790ce28 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -141,9 +141,20 @@
{{template "repo/cite/cite_modal" .}}
{{end}}
{{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
-
- {{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
-
+
{{ctx.Locale.Tr "home.welcome.activity_hint"}}
+