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"}} - +
+ + {{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}} + + {{if not $.DisableDownloadSourceArchives}} + + {{end}} +
{{end}} diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index 0e1d8f8036..f59c6d67ce 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -14,13 +14,13 @@
- {{ctx.Locale.Tr "repo.issues.filter_milestone_all"}} - {{ctx.Locale.Tr "repo.issues.filter_milestone_none"}} + {{ctx.Locale.Tr "repo.issues.filter_milestone_all"}} + {{ctx.Locale.Tr "repo.issues.filter_milestone_none"}} {{if .OpenMilestones}}
{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}
{{range .OpenMilestones}} - + {{svg "octicon-milestone" 16 "mr-2"}} {{.Name}} @@ -30,7 +30,7 @@
{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}
{{range .ClosedMilestones}} - + {{svg "octicon-milestone" 16 "mr-2"}} {{.Name}} @@ -51,15 +51,15 @@ {{svg "octicon-search" 16}} - {{ctx.Locale.Tr "repo.issues.filter_project_all"}} - {{ctx.Locale.Tr "repo.issues.filter_project_none"}} + {{ctx.Locale.Tr "repo.issues.filter_project_all"}} + {{ctx.Locale.Tr "repo.issues.filter_project_none"}} {{if .OpenProjects}}
{{ctx.Locale.Tr "repo.issues.new.open_projects"}}
{{range .OpenProjects}} - + {{svg .IconName 18 "tw-mr-2 tw-shrink-0"}}{{.Title}} {{end}} @@ -70,7 +70,7 @@ {{ctx.Locale.Tr "repo.issues.new.closed_projects"}} {{range .ClosedProjects}} - + {{svg .IconName 18 "tw-mr-2"}}{{.Title}} {{end}} @@ -82,7 +82,7 @@ - {{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}} - {{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}} + {{ctx.Locale.Tr "repo.issues.filter_assginee_no_select"}} + {{ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee"}}
{{range .Assignees}} - + {{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}} {{end}} @@ -127,14 +127,14 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}} {{end}} @@ -146,11 +146,11 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}} diff --git a/templates/repo/issue/search.tmpl b/templates/repo/issue/search.tmpl index f1c0ea3290..17abe263e7 100644 --- a/templates/repo/issue/search.tmpl +++ b/templates/repo/issue/search.tmpl @@ -10,11 +10,11 @@ {{end}} {{if .PageIsPullList}} - {{template "shared/search/combo_fuzzy" dict "Value" .Keyword "IsFuzzy" .IsFuzzy "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} + {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} {{else if .PageIsMilestones}} - {{template "shared/search/combo_fuzzy" dict "Value" .Keyword "IsFuzzy" .IsFuzzy "Placeholder" (ctx.Locale.Tr "search.milestone_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} + {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.milestone_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} {{else}} - {{template "shared/search/combo_fuzzy" dict "Value" .Keyword "IsFuzzy" .IsFuzzy "Placeholder" (ctx.Locale.Tr "search.issue_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} + {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.issue_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} {{end}} diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index b71fde685e..6318d02139 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -21,7 +21,7 @@ {{end}} {{range .Labels}} - {{RenderLabel $.Context ctx.Locale .}} + {{RenderLabel $.Context ctx.Locale .}} {{end}} diff --git a/templates/shared/label_filter.tmpl b/templates/shared/label_filter.tmpl index 2269271aac..50040c208d 100644 --- a/templates/shared/label_filter.tmpl +++ b/templates/shared/label_filter.tmpl @@ -23,8 +23,8 @@ {{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}
- {{ctx.Locale.Tr "repo.issues.filter_label_no_select"}} - {{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}} + {{ctx.Locale.Tr "repo.issues.filter_label_no_select"}} + {{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}} {{$previousExclusiveScope := "_no_scope"}} {{range .Labels}} {{$exclusiveScope := .ExclusiveScope}} @@ -32,7 +32,7 @@
{{end}} {{$previousExclusiveScope = $exclusiveScope}} - + {{if .IsExcluded}} {{svg "octicon-circle-slash"}} {{else if .IsSelected}} diff --git a/templates/shared/search/combo.tmpl b/templates/shared/search/combo.tmpl index 788db95cc1..9fcc2e9f32 100644 --- a/templates/shared/search/combo.tmpl +++ b/templates/shared/search/combo.tmpl @@ -3,6 +3,10 @@ {{/* Placeholder (optional) - placeholder text to be used */}} {{/* Tooltip (optional) - a tooltip to be displayed on button hover */}}
- {{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}} + {{template "shared/search/input" + dict + "Value" .Value + "Disabled" .Disabled + "Placeholder" .Placeholder}} {{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}}
diff --git a/templates/shared/search/combo_fuzzy.tmpl b/templates/shared/search/combo_fuzzy.tmpl deleted file mode 100644 index 6dfec4c288..0000000000 --- a/templates/shared/search/combo_fuzzy.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -{{/* Value - value of the search field (for search results page) */}} -{{/* Disabled (optional) - if search field/button has to be disabled */}} -{{/* Placeholder (optional) - placeholder text to be used */}} -{{/* IsFuzzy - state of the fuzzy/union search toggle */}} -{{/* Tooltip (optional) - a tooltip to be displayed on button hover */}} -
- {{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}} - {{template "shared/search/fuzzy" - dict - "Disabled" .Disabled - "IsFuzzy" .IsFuzzy}} - {{template "shared/search/button" dict "Disabled" .Disabled "Tooltip" .Tooltip}} -
diff --git a/templates/shared/search/fuzzy.tmpl b/templates/shared/search/fuzzy.tmpl deleted file mode 100644 index f0344c32b7..0000000000 --- a/templates/shared/search/fuzzy.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -{{/* Disabled (optional) - if dropdown has to be disabled */}} -{{/* IsFuzzy - state of the fuzzy search toggle */}} - diff --git a/templates/user/dashboard/dashboard.tmpl b/templates/user/dashboard/dashboard.tmpl index 5dc46dc0a5..3ce3c1eb73 100644 --- a/templates/user/dashboard/dashboard.tmpl +++ b/templates/user/dashboard/dashboard.tmpl @@ -5,7 +5,11 @@
{{template "base/alert" .}} {{template "user/heatmap" .}} - {{template "user/dashboard/feeds" .}} + {{if .Feeds}} + {{template "user/dashboard/feeds" .}} + {{else}} + {{template "user/dashboard/guide" .}} + {{end}}
{{template "user/dashboard/repolist" .}} diff --git a/templates/user/dashboard/guide.tmpl b/templates/user/dashboard/guide.tmpl new file mode 100644 index 0000000000..a80c211030 --- /dev/null +++ b/templates/user/dashboard/guide.tmpl @@ -0,0 +1,16 @@ +
+ {{svg "octicon-inbox" 64 "tw-text-placeholder-text"}} +

{{ctx.Locale.Tr "home.welcome.no_activity"}}

+

{{ctx.Locale.Tr "home.welcome.activity_hint"}}

+
+ {{ctx.Locale.Tr "home.explore_repos"}} + {{if not .UsersPageIsDisabled}} + · + {{ctx.Locale.Tr "home.explore_users"}} + {{end}} + {{if not .OrganizationsPageIsDisabled}} + · + {{ctx.Locale.Tr "home.explore_orgs"}} + {{end}} +
+
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 2039bf44e3..e25a9d8219 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -5,11 +5,11 @@ {{template "base/alert" .}}
- + {{svg "octicon-issue-opened" 16 "tw-mr-2"}} {{ctx.Locale.PrettyNumber .IssueStats.OpenCount}} {{ctx.Locale.Tr "repo.issues.open_title"}} - + {{svg "octicon-issue-closed" 16 "tw-mr-2"}} {{ctx.Locale.PrettyNumber .IssueStats.ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}} @@ -20,9 +20,9 @@ {{if .PageIsPulls}} - {{template "shared/search/combo_fuzzy" dict "Value" $.Keyword "IsFuzzy" $.IsFuzzy "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} + {{template "shared/search/combo" dict "Value" $.Keyword "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} {{else}} - {{template "shared/search/combo_fuzzy" dict "Value" $.Keyword "IsFuzzy" $.IsFuzzy "Placeholder" (ctx.Locale.Tr "search.issue_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} + {{template "shared/search/combo" dict "Value" $.Keyword "Placeholder" (ctx.Locale.Tr "search.issue_kind") "Tooltip" (ctx.Locale.Tr "explore.go_to")}} {{end}}
@@ -38,29 +38,29 @@ {{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/tests/e2e/release.test.e2e.ts b/tests/e2e/release.test.e2e.ts index 4d916f717e..044e7b93ab 100644 --- a/tests/e2e/release.test.e2e.ts +++ b/tests/e2e/release.test.e2e.ts @@ -30,8 +30,7 @@ test('External Release Attachments', async ({page, isMobile}) => { await validate_form({page}, 'fieldset'); const textarea = page.locator('input[name=tag_name]'); await textarea.pressSequentially('2.0'); - await expect(page.locator('input[name=title]')).toHaveAttribute('placeholder', '2.0'); - await page.fill('input[name=title]', '2.0'); + await expect(page.locator('input[name=title]')).toHaveValue('2.0'); await page.click('#add-external-link'); await page.click('#add-external-link'); await page.fill('input[name=attachment-new-name-2]', 'Test'); diff --git a/tests/integration/api_admin_actions_test.go b/tests/integration/api_admin_actions_test.go index 22590dc4c4..fd55f0fd2e 100644 --- a/tests/integration/api_admin_actions_test.go +++ b/tests/integration/api_admin_actions_test.go @@ -11,7 +11,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/routers/api/v1/shared" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -31,9 +31,9 @@ func TestAPISearchActionJobs_GlobalRunner(t *testing.T) { ).AddTokenAuth(token) res := MakeRequest(t, req, http.StatusOK) - var jobs shared.RunJobList + var jobs []*api.ActionRunJob DecodeJSON(t, res, &jobs) - assert.Len(t, jobs.Body, 1) - assert.EqualValues(t, job.ID, jobs.Body[0].ID) + assert.Len(t, jobs, 1) + assert.EqualValues(t, job.ID, jobs[0].ID) } diff --git a/tests/integration/api_org_actions_test.go b/tests/integration/api_org_actions_test.go index c8ebbdf293..8c1948fc4a 100644 --- a/tests/integration/api_org_actions_test.go +++ b/tests/integration/api_org_actions_test.go @@ -11,7 +11,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/routers/api/v1/shared" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -30,9 +30,9 @@ func TestAPISearchActionJobs_OrgRunner(t *testing.T) { AddTokenAuth(token) res := MakeRequest(t, req, http.StatusOK) - var jobs shared.RunJobList + var jobs []*api.ActionRunJob DecodeJSON(t, res, &jobs) - assert.Len(t, jobs.Body, 1) - assert.EqualValues(t, job.ID, jobs.Body[0].ID) + assert.Len(t, jobs, 1) + assert.EqualValues(t, job.ID, jobs[0].ID) } diff --git a/tests/integration/api_repo_actions_test.go b/tests/integration/api_repo_actions_test.go index 9c3b6aa2b6..ec8bb501e3 100644 --- a/tests/integration/api_repo_actions_test.go +++ b/tests/integration/api_repo_actions_test.go @@ -12,7 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/routers/api/v1/shared" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -35,9 +35,9 @@ func TestAPISearchActionJobs_RepoRunner(t *testing.T) { ).AddTokenAuth(token) res := MakeRequest(t, req, http.StatusOK) - var jobs shared.RunJobList + var jobs []*api.ActionRunJob DecodeJSON(t, res, &jobs) - assert.Len(t, jobs.Body, 1) - assert.EqualValues(t, job.ID, jobs.Body[0].ID) + assert.Len(t, jobs, 1) + assert.EqualValues(t, job.ID, jobs[0].ID) } diff --git a/tests/integration/api_user_actions_test.go b/tests/integration/api_user_actions_test.go index f9c9c1df4e..a03d4e95ae 100644 --- a/tests/integration/api_user_actions_test.go +++ b/tests/integration/api_user_actions_test.go @@ -11,7 +11,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/routers/api/v1/shared" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -30,9 +30,9 @@ func TestAPISearchActionJobs_UserRunner(t *testing.T) { AddTokenAuth(token) res := MakeRequest(t, req, http.StatusOK) - var jobs shared.RunJobList + var jobs []*api.ActionRunJob DecodeJSON(t, res, &jobs) - assert.Len(t, jobs.Body, 1) - assert.EqualValues(t, job.ID, jobs.Body[0].ID) + assert.Len(t, jobs, 1) + assert.EqualValues(t, job.ID, jobs[0].ID) } diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 3cdb0b8a28..19eddf926f 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -138,27 +138,25 @@ func TestViewIssuesKeyword(t *testing.T) { }) // keyword: 'firstt' - // should not match when fuzzy searching is disabled - req = NewRequestf(t, "GET", "%s/issues?q=%st&fuzzy=false", repo.Link(), keyword) + // should not match when using phrase search + req = NewRequestf(t, "GET", "%s/issues?q=\"%st\"", repo.Link(), keyword) resp = MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) issuesSelection = getIssuesSelection(t, htmlDoc) assert.EqualValues(t, 0, issuesSelection.Length()) - // should match as 'first' when fuzzy seaeching is enabled - for _, fmt := range []string{"%s/issues?q=%st&fuzzy=true", "%s/issues?q=%st"} { - req = NewRequestf(t, "GET", fmt, repo.Link(), keyword) - resp = MakeRequest(t, req, http.StatusOK) - htmlDoc = NewHTMLParser(t, resp.Body) - issuesSelection = getIssuesSelection(t, htmlDoc) - assert.EqualValues(t, 1, issuesSelection.Length()) - issuesSelection.Each(func(_ int, selection *goquery.Selection) { - issue := getIssue(t, repo.ID, selection) - assert.False(t, issue.IsClosed) - assert.False(t, issue.IsPull) - assertMatch(t, issue, keyword) - }) - } + // should match as 'first' when using a standard query + req = NewRequestf(t, "GET", "%s/issues?q=%st", repo.Link(), keyword) + resp = MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + issuesSelection = getIssuesSelection(t, htmlDoc) + assert.EqualValues(t, 1, issuesSelection.Length()) + issuesSelection.Each(func(_ int, selection *goquery.Selection) { + issue := getIssue(t, repo.ID, selection) + assert.False(t, issue.IsClosed) + assert.False(t, issue.IsPull) + assertMatch(t, issue, keyword) + }) } func TestViewIssuesSearchOptions(t *testing.T) { diff --git a/tests/integration/repo_archive_test.go b/tests/integration/repo_archive_test.go index 75fe78eeed..a5c076952c 100644 --- a/tests/integration/repo_archive_test.go +++ b/tests/integration/repo_archive_test.go @@ -1,13 +1,21 @@ // Copyright 2024 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package integration import ( + "archive/tar" + "compress/gzip" + "fmt" "io" "net/http" + "net/url" "testing" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/routers" @@ -32,3 +40,48 @@ func TestRepoDownloadArchive(t *testing.T) { assert.Empty(t, resp.Header().Get("Content-Encoding")) assert.Len(t, bs, 320) } + +func TestRepoDownloadArchiveSubdir(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + defer test.MockVariableValue(&setting.EnableGzip, true)() + defer test.MockVariableValue(&web.GzipMinSize, 10)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + // Create a subdirectory + err := createOrReplaceFileInBranch(user, repo, "subdir/test.txt", "master", "Test") + require.NoError(t, err) + + t.Run("Frontend", func(t *testing.T) { + resp := MakeRequest(t, NewRequestf(t, "GET", "/%s/src/branch/master/subdir", repo.FullName()), http.StatusOK) + page := NewHTMLParser(t, resp.Body) + + page.AssertElement(t, fmt.Sprintf(".folder-actions a.archive-link[href='/%s/archive/master:subdir.zip'][type='application/zip']", repo.FullName()), true) + page.AssertElement(t, fmt.Sprintf(".folder-actions a.archive-link[href='/%s/archive/master:subdir.tar.gz'][type='application/gzip']", repo.FullName()), true) + }) + + t.Run("Backend", func(t *testing.T) { + resp := MakeRequest(t, NewRequestf(t, "GET", "/%s/archive/master:subdir.tar.gz", repo.FullName()), http.StatusOK) + + uncompressedStream, err := gzip.NewReader(resp.Body) + require.NoError(t, err) + + tarReader := tar.NewReader(uncompressedStream) + + header, err := tarReader.Next() + require.NoError(t, err) + assert.Equal(t, tar.TypeDir, int32(header.Typeflag)) + assert.Equal(t, fmt.Sprintf("%s/", repo.Name), header.Name) + + header, err = tarReader.Next() + require.NoError(t, err) + assert.Equal(t, tar.TypeReg, int32(header.Typeflag)) + assert.Equal(t, fmt.Sprintf("%s/test.txt", repo.Name), header.Name) + + _, err = tarReader.Next() + assert.Equal(t, io.EOF, err) + }) + }) +} diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index 01d905895a..5445ce6d4c 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -1120,7 +1120,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=") assert.Contains(t, href, "&assignee=") assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=") }) assert.True(t, called) }) @@ -1145,32 +1144,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=") assert.Contains(t, href, "&assignee=") assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=") - }) - assert.True(t, called) - }) - - t.Run("Fuzzy", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - - req := NewRequest(t, "GET", "/user2/repo1/issues?fuzzy=false") - resp := MakeRequest(t, req, http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - - called := false - htmlDoc.Find("#issue-filters a[href^='?']").Each(func(_ int, s *goquery.Selection) { - called = true - href, _ := s.Attr("href") - assert.Contains(t, href, "?q=&") - assert.Contains(t, href, "&type=") - assert.Contains(t, href, "&sort=") - assert.Contains(t, href, "&state=") - assert.Contains(t, href, "&labels=") - assert.Contains(t, href, "&milestone=") - assert.Contains(t, href, "&project=") - assert.Contains(t, href, "&assignee=") - assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=false") }) assert.True(t, called) }) @@ -1195,7 +1168,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=") assert.Contains(t, href, "&assignee=") assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=") }) assert.True(t, called) }) @@ -1220,7 +1192,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=") assert.Contains(t, href, "&assignee=") assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=") }) assert.True(t, called) }) @@ -1245,7 +1216,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=") assert.Contains(t, href, "&assignee=") assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=") }) assert.True(t, called) }) @@ -1270,7 +1240,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=") assert.Contains(t, href, "&assignee=") assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=") }) assert.True(t, called) }) @@ -1295,7 +1264,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=") assert.Contains(t, href, "&assignee=") assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=") }) assert.True(t, called) }) @@ -1320,7 +1288,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=1") assert.Contains(t, href, "&assignee=") assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=") }) assert.True(t, called) }) @@ -1345,7 +1312,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=") assert.Contains(t, href, "&assignee=1") assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=") }) assert.True(t, called) }) @@ -1370,7 +1336,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=") assert.Contains(t, href, "&assignee=") assert.Contains(t, href, "&poster=1") - assert.Contains(t, href, "&fuzzy=") }) assert.True(t, called) }) @@ -1395,7 +1360,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=") assert.Contains(t, href, "&assignee=") assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=") }) assert.True(t, called) }) @@ -1420,7 +1384,6 @@ func TestRepoIssueFilterLinks(t *testing.T) { assert.Contains(t, href, "&project=") assert.Contains(t, href, "&assignee=") assert.Contains(t, href, "&poster=") - assert.Contains(t, href, "&fuzzy=") assert.Contains(t, href, "&archived=true") }) assert.True(t, called) diff --git a/tests/integration/user_dashboard_test.go b/tests/integration/user_dashboard_test.go index 0ed5193c48..6621caca9b 100644 --- a/tests/integration/user_dashboard_test.go +++ b/tests/integration/user_dashboard_test.go @@ -1,5 +1,5 @@ // Copyright 2024 The Forgejo Authors. All rights reserved. -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later package integration @@ -38,6 +38,25 @@ func TestUserDashboardActionLinks(t *testing.T) { assert.EqualValues(t, locale.TrString("new_org.link"), strings.TrimSpace(links.Find("a[href='/org/create']").Text())) } +func TestUserDashboardFeedWelcome(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + // User2 has some activity in feed + session := loginUser(t, "user2") + page := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK).Body) + testUserDashboardFeedType(t, page, false) + + // User1 doesn't have any activity in feed + session = loginUser(t, "user1") + page = NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK).Body) + testUserDashboardFeedType(t, page, true) +} + +func testUserDashboardFeedType(t *testing.T, page *HTMLDoc, isEmpty bool) { + page.AssertElement(t, "#activity-feed", !isEmpty) + page.AssertElement(t, "#empty-feed", isEmpty) +} + func TestDashboardTitleRendering(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css index 4bb9fa38bf..3a1fc34ed3 100644 --- a/web_src/css/dashboard.css +++ b/web_src/css/dashboard.css @@ -79,3 +79,7 @@ .dashboard .secondary-nav .ui.dropdown { max-width: 100%; } + +.dashboard .help { + color: var(--color-secondary-dark-8); +} diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 4e07d9d9e3..a9720ec6da 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -153,7 +153,9 @@ border-radius: 0 var(--border-radius) var(--border-radius) 0 !important; } -.repository .clone-panel .dropdown .menu { +/* Dropdown alignment */ +.repository .clone-panel .dropdown .menu, +.repository .folder-actions .dropdown .menu { right: 0 !important; left: auto !important; } diff --git a/web_src/js/features/repo-release.js b/web_src/js/features/repo-release.js index 1ac44f22ef..90043f15f2 100644 --- a/web_src/js/features/repo-release.js +++ b/web_src/js/features/repo-release.js @@ -32,6 +32,7 @@ function initTagNameEditor() { const newTagHelperText = el.getAttribute('data-tag-helper-new'); const existingTagHelperText = el.getAttribute('data-tag-helper-existing'); + let previousTag = ''; document.getElementById('tag-name').addEventListener('keyup', (e) => { const value = e.target.value; const tagHelper = document.getElementById('tag-helper'); @@ -45,7 +46,10 @@ function initTagNameEditor() { } const title_input = document.getElementById('release-title'); - title_input.placeholder = value; + if (!title_input.value || previousTag === title_input.value) { + title_input.value = value; + } + previousTag = value; }); }