Merge pull request '[gitea] week 12 cherry-pick' (#2679) from algernon/forgejo:wcp/week-12 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2679
Reviewed-by: Otto <otto@codeberg.org>
Reviewed-by: oliverpool <oliverpool@noreply.codeberg.org>
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-03-20 08:15:06 +00:00
commit 2f78daa3af
174 changed files with 3157 additions and 2907 deletions

View file

@ -84,7 +84,6 @@ package "code.gitea.io/gitea/models/repo"
func (*releaseSorter).Swap func (*releaseSorter).Swap
func SortReleases func SortReleases
func FindReposMapByIDs func FindReposMapByIDs
func RepositoryListOfMap
func (SearchOrderBy).String func (SearchOrderBy).String
func IsErrTopicNotExist func IsErrTopicNotExist
func (ErrTopicNotExist).Error func (ErrTopicNotExist).Error
@ -178,6 +177,7 @@ package "code.gitea.io/gitea/modules/git"
func (ErrExecTimeout).Error func (ErrExecTimeout).Error
func (ErrUnsupportedVersion).Error func (ErrUnsupportedVersion).Error
func SetUpdateHook func SetUpdateHook
func GetObjectFormatOfRepo
func openRepositoryWithDefaultContext func openRepositoryWithDefaultContext
func IsTagExist func IsTagExist
func ToEntryMode func ToEntryMode

View file

@ -283,7 +283,7 @@ rules:
i/unambiguous: [0] i/unambiguous: [0]
init-declarations: [0] init-declarations: [0]
jquery/no-ajax-events: [2] jquery/no-ajax-events: [2]
jquery/no-ajax: [0] jquery/no-ajax: [2]
jquery/no-animate: [2] jquery/no-animate: [2]
jquery/no-attr: [0] jquery/no-attr: [0]
jquery/no-bind: [2] jquery/no-bind: [2]
@ -315,7 +315,7 @@ rules:
jquery/no-parent: [0] jquery/no-parent: [0]
jquery/no-parents: [0] jquery/no-parents: [0]
jquery/no-parse-html: [2] jquery/no-parse-html: [2]
jquery/no-prop: [0] jquery/no-prop: [2]
jquery/no-proxy: [2] jquery/no-proxy: [2]
jquery/no-ready: [2] jquery/no-ready: [2]
jquery/no-serialize: [2] jquery/no-serialize: [2]
@ -396,11 +396,11 @@ rules:
no-irregular-whitespace: [2] no-irregular-whitespace: [2]
no-iterator: [2] no-iterator: [2]
no-jquery/no-ajax-events: [2] no-jquery/no-ajax-events: [2]
no-jquery/no-ajax: [0] no-jquery/no-ajax: [2]
no-jquery/no-and-self: [2] no-jquery/no-and-self: [2]
no-jquery/no-animate-toggle: [2] no-jquery/no-animate-toggle: [2]
no-jquery/no-animate: [2] no-jquery/no-animate: [2]
no-jquery/no-append-html: [0] no-jquery/no-append-html: [2]
no-jquery/no-attr: [0] no-jquery/no-attr: [0]
no-jquery/no-bind: [2] no-jquery/no-bind: [2]
no-jquery/no-box-model: [2] no-jquery/no-box-model: [2]
@ -466,7 +466,7 @@ rules:
no-jquery/no-parse-html: [2] no-jquery/no-parse-html: [2]
no-jquery/no-parse-json: [2] no-jquery/no-parse-json: [2]
no-jquery/no-parse-xml: [2] no-jquery/no-parse-xml: [2]
no-jquery/no-prop: [0] no-jquery/no-prop: [2]
no-jquery/no-proxy: [2] no-jquery/no-proxy: [2]
no-jquery/no-ready-shorthand: [2] no-jquery/no-ready-shorthand: [2]
no-jquery/no-ready: [2] no-jquery/no-ready: [2]
@ -487,7 +487,7 @@ rules:
no-jquery/no-visibility: [2] no-jquery/no-visibility: [2]
no-jquery/no-when: [2] no-jquery/no-when: [2]
no-jquery/no-wrap: [2] no-jquery/no-wrap: [2]
no-jquery/variable-pattern: [0] no-jquery/variable-pattern: [2]
no-label-var: [2] no-label-var: [2]
no-labels: [0] # handled by no-restricted-syntax no-labels: [0] # handled by no-restricted-syntax
no-lone-blocks: [2] no-lone-blocks: [2]

View file

@ -881,10 +881,6 @@ release-sources: | $(DIST_DIRS)
release-docs: | $(DIST_DIRS) docs release-docs: | $(DIST_DIRS) docs
tar -czf $(DIST)/release/gitea-docs-$(VERSION).tar.gz -C ./docs . tar -czf $(DIST)/release/gitea-docs-$(VERSION).tar.gz -C ./docs .
.PHONY: docs
docs:
cd docs; bash scripts/trans-copy.sh;
.PHONY: deps .PHONY: deps
deps: deps-frontend deps-backend deps-tools deps-py deps: deps-frontend deps-backend deps-tools deps-py

View file

@ -163,7 +163,7 @@ clients don't even support HTML, so they show the text version included in the g
If the template fails to render, it will be noticed only at the moment the mail is sent. If the template fails to render, it will be noticed only at the moment the mail is sent.
A default subject is used if the subject template fails, and whatever was rendered successfully A default subject is used if the subject template fails, and whatever was rendered successfully
from the the _mail body_ is used, disregarding the rest. from the _mail body_ is used, disregarding the rest.
Please check [Gitea's logs](administration/logging-config.md) for error messages in case of trouble. Please check [Gitea's logs](administration/logging-config.md) for error messages in case of trouble.

View file

@ -333,14 +333,9 @@ Documentation for the website is found in `docs/`. If you change this you
can test your changes to ensure that they pass continuous integration using: can test your changes to ensure that they pass continuous integration using:
```bash ```bash
# from the docs directory within Gitea make lint-md
make trans-copy clean build
``` ```
You will require a copy of [Hugo](https://gohugo.io/) to run this task. Please
note: this may generate a number of untracked Git objects, which will need to
be cleaned up.
## Visual Studio Code ## Visual Studio Code
A `launch.json` and `tasks.json` are provided within `contrib/ide/vscode` for A `launch.json` and `tasks.json` are provided within `contrib/ide/vscode` for

View file

@ -307,13 +307,9 @@ TAGS="bindata sqlite sqlite_unlock_notify" make build test-sqlite
该网站的文档位于 `docs/` 中。如果你改变了文档内容,你可以使用以下测试方法进行持续集成: 该网站的文档位于 `docs/` 中。如果你改变了文档内容,你可以使用以下测试方法进行持续集成:
```bash ```bash
# 来自 Gitea 中的 docs 目录 make lint-md
make trans-copy clean build
``` ```
运行此任务依赖于 [Hugo](https://gohugo.io/)。请注意:这可能会生成一些未跟踪的 Git 对象,
需要被清理干净。
## Visual Studio Code ## Visual Studio Code
`contrib/ide/vscode` 中为 Visual Studio Code 提供了 `launch.json``tasks.json`。查看 `contrib/ide/vscode` 中为 Visual Studio Code 提供了 `launch.json``tasks.json`。查看

View file

@ -1,34 +0,0 @@
#!/usr/bin/env bash
set -e
#
# This script is used to copy the en-US content to our available locales as a
# fallback to always show all pages when displaying a specific locale that is
# missing some documents to be translated.
#
# Just execute the script without any argument and you will get the missing
# files copied into the content folder. We are calling this script within the CI
# server simply by `make trans-copy`.
#
declare -a LOCALES=(
"fr-fr"
"nl-nl"
"pt-br"
"zh-cn"
"zh-tw"
)
ROOT=$(realpath $(dirname $0)/..)
for SOURCE in $(find ${ROOT}/content -type f -iname *.en-us.md); do
for LOCALE in "${LOCALES[@]}"; do
DEST="${SOURCE%.en-us.md}.${LOCALE}.md"
if [[ ! -f ${DEST} ]]; then
cp ${SOURCE} ${DEST}
sed -i.bak "s/en\-us/${LOCALE}/g" ${DEST}
rm ${DEST}.bak
fi
done
done

4
go.mod
View file

@ -17,7 +17,7 @@ require (
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/PuerkitoBio/goquery v1.8.1 github.com/PuerkitoBio/goquery v1.8.1
github.com/alecthomas/chroma/v2 v2.12.0 github.com/alecthomas/chroma/v2 v2.13.0
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.3.10 github.com/blevesearch/bleve/v2 v2.3.10
github.com/buildkite/terminal-to-html/v3 v3.10.1 github.com/buildkite/terminal-to-html/v3 v3.10.1
@ -169,7 +169,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
github.com/fatih/color v1.16.0 // indirect github.com/fatih/color v1.16.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect

16
go.sum
View file

@ -103,14 +103,14 @@ github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAc
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68= github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw= github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw= github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
@ -235,8 +235,8 @@ github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmW
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=

View file

@ -150,6 +150,7 @@ type Action struct {
Repo *repo_model.Repository `xorm:"-"` Repo *repo_model.Repository `xorm:"-"`
CommentID int64 `xorm:"INDEX"` CommentID int64 `xorm:"INDEX"`
Comment *issues_model.Comment `xorm:"-"` Comment *issues_model.Comment `xorm:"-"`
Issue *issues_model.Issue `xorm:"-"` // get the issue id from content
IsDeleted bool `xorm:"NOT NULL DEFAULT false"` IsDeleted bool `xorm:"NOT NULL DEFAULT false"`
RefName string RefName string
IsPrivate bool `xorm:"NOT NULL DEFAULT false"` IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
@ -292,11 +293,6 @@ func (a *Action) GetRepoAbsoluteLink(ctx context.Context) string {
return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx)) return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx))
} }
// GetCommentHTMLURL returns link to action comment.
func (a *Action) GetCommentHTMLURL(ctx context.Context) string {
return a.getCommentHTMLURL(ctx)
}
func (a *Action) loadComment(ctx context.Context) (err error) { func (a *Action) loadComment(ctx context.Context) (err error) {
if a.CommentID == 0 || a.Comment != nil { if a.CommentID == 0 || a.Comment != nil {
return nil return nil
@ -305,7 +301,8 @@ func (a *Action) loadComment(ctx context.Context) (err error) {
return err return err
} }
func (a *Action) getCommentHTMLURL(ctx context.Context) string { // GetCommentHTMLURL returns link to action comment.
func (a *Action) GetCommentHTMLURL(ctx context.Context) string {
if a == nil { if a == nil {
return "#" return "#"
} }
@ -313,34 +310,19 @@ func (a *Action) getCommentHTMLURL(ctx context.Context) string {
if a.Comment != nil { if a.Comment != nil {
return a.Comment.HTMLURL(ctx) return a.Comment.HTMLURL(ctx)
} }
if len(a.GetIssueInfos()) == 0 {
if err := a.LoadIssue(ctx); err != nil || a.Issue == nil {
return "#" return "#"
} }
// Return link to issue if err := a.Issue.LoadRepo(ctx); err != nil {
issueIDString := a.GetIssueInfos()[0]
issueID, err := strconv.ParseInt(issueIDString, 10, 64)
if err != nil {
return "#" return "#"
} }
issue, err := issues_model.GetIssueByID(ctx, issueID) return a.Issue.HTMLURL()
if err != nil {
return "#"
}
if err = issue.LoadRepo(ctx); err != nil {
return "#"
}
return issue.HTMLURL()
} }
// GetCommentLink returns link to action comment. // GetCommentLink returns link to action comment.
func (a *Action) GetCommentLink(ctx context.Context) string { func (a *Action) GetCommentLink(ctx context.Context) string {
return a.getCommentLink(ctx)
}
func (a *Action) getCommentLink(ctx context.Context) string {
if a == nil { if a == nil {
return "#" return "#"
} }
@ -348,26 +330,15 @@ func (a *Action) getCommentLink(ctx context.Context) string {
if a.Comment != nil { if a.Comment != nil {
return a.Comment.Link(ctx) return a.Comment.Link(ctx)
} }
if len(a.GetIssueInfos()) == 0 {
if err := a.LoadIssue(ctx); err != nil || a.Issue == nil {
return "#" return "#"
} }
// Return link to issue if err := a.Issue.LoadRepo(ctx); err != nil {
issueIDString := a.GetIssueInfos()[0]
issueID, err := strconv.ParseInt(issueIDString, 10, 64)
if err != nil {
return "#" return "#"
} }
issue, err := issues_model.GetIssueByID(ctx, issueID) return a.Issue.Link()
if err != nil {
return "#"
}
if err = issue.LoadRepo(ctx); err != nil {
return "#"
}
return issue.Link()
} }
// GetBranch returns the action's repository branch. // GetBranch returns the action's repository branch.
@ -395,6 +366,10 @@ func (a *Action) GetCreate() time.Time {
return a.CreatedUnix.AsTime() return a.CreatedUnix.AsTime()
} }
func (a *Action) IsIssueEvent() bool {
return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request")
}
// GetIssueInfos returns a list of associated information with the action. // GetIssueInfos returns a list of associated information with the action.
func (a *Action) GetIssueInfos() []string { func (a *Action) GetIssueInfos() []string {
// make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length // make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length
@ -405,27 +380,52 @@ func (a *Action) GetIssueInfos() []string {
return ret return ret
} }
// GetIssueTitle returns the title of first issue associated with the action. func (a *Action) getIssueIndex() int64 {
func (a *Action) GetIssueTitle(ctx context.Context) string { infos := a.GetIssueInfos()
index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64) if len(infos) == 0 {
issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) return 0
if err != nil {
log.Error("GetIssueByIndex: %v", err)
return "500 when get issue"
} }
return issue.Title index, _ := strconv.ParseInt(infos[0], 10, 64)
return index
} }
// GetIssueContent returns the content of first issue associated with func (a *Action) LoadIssue(ctx context.Context) error {
// this action. if a.Issue != nil {
func (a *Action) GetIssueContent(ctx context.Context) string { return nil
index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
if err != nil {
log.Error("GetIssueByIndex: %v", err)
return "500 when get issue"
} }
return issue.Content if index := a.getIssueIndex(); index > 0 {
issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
if err != nil {
return err
}
a.Issue = issue
a.Issue.Repo = a.Repo
}
return nil
}
// GetIssueTitle returns the title of first issue associated with the action.
func (a *Action) GetIssueTitle(ctx context.Context) string {
if err := a.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err)
return "<500 when get issue>"
}
if a.Issue == nil {
return "<Issue not found>"
}
return a.Issue.Title
}
// GetIssueContent returns the content of first issue associated with this action.
func (a *Action) GetIssueContent(ctx context.Context) string {
if err := a.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err)
return "<500 when get issue>"
}
if a.Issue == nil {
return "<Content not found>"
}
return a.Issue.Content
} }
// GetFeedsOptions options for retrieving feeds // GetFeedsOptions options for retrieving feeds
@ -465,7 +465,7 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
return nil, 0, fmt.Errorf("FindAndCount: %w", err) return nil, 0, fmt.Errorf("FindAndCount: %w", err)
} }
if err := ActionList(actions).loadAttributes(ctx); err != nil { if err := ActionList(actions).LoadAttributes(ctx); err != nil {
return nil, 0, fmt.Errorf("LoadAttributes: %w", err) return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
} }

View file

@ -6,11 +6,16 @@ package activities
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
) )
// ActionList defines a list of actions // ActionList defines a list of actions
@ -24,7 +29,7 @@ func (actions ActionList) getUserIDs() []int64 {
return userIDs.Values() return userIDs.Values()
} }
func (actions ActionList) loadUsers(ctx context.Context) (map[int64]*user_model.User, error) { func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_model.User, error) {
if len(actions) == 0 { if len(actions) == 0 {
return nil, nil return nil, nil
} }
@ -52,7 +57,7 @@ func (actions ActionList) getRepoIDs() []int64 {
return repoIDs.Values() return repoIDs.Values()
} }
func (actions ActionList) loadRepositories(ctx context.Context) error { func (actions ActionList) LoadRepositories(ctx context.Context) error {
if len(actions) == 0 { if len(actions) == 0 {
return nil return nil
} }
@ -63,11 +68,11 @@ func (actions ActionList) loadRepositories(ctx context.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("find repository: %w", err) return fmt.Errorf("find repository: %w", err)
} }
for _, action := range actions { for _, action := range actions {
action.Repo = repoMaps[action.RepoID] action.Repo = repoMaps[action.RepoID]
} }
return nil repos := repo_model.RepositoryList(util.ValuesOfMap(repoMaps))
return repos.LoadUnits(ctx)
} }
func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]*user_model.User) (err error) { func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]*user_model.User) (err error) {
@ -75,37 +80,124 @@ func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]*
userMap = make(map[int64]*user_model.User) userMap = make(map[int64]*user_model.User)
} }
userSet := make(container.Set[int64], len(actions))
for _, action := range actions { for _, action := range actions {
if action.Repo == nil { if action.Repo == nil {
continue continue
} }
repoOwner, ok := userMap[action.Repo.OwnerID] if _, ok := userMap[action.Repo.OwnerID]; !ok {
if !ok { userSet.Add(action.Repo.OwnerID)
repoOwner, err = user_model.GetUserByID(ctx, action.Repo.OwnerID) }
if err != nil { }
if user_model.IsErrUserNotExist(err) {
continue if err := db.GetEngine(ctx).
} In("id", userSet.Values()).
return err Find(&userMap); err != nil {
} return fmt.Errorf("find user: %w", err)
userMap[repoOwner.ID] = repoOwner }
for _, action := range actions {
if action.Repo != nil {
action.Repo.Owner = userMap[action.Repo.OwnerID]
} }
action.Repo.Owner = repoOwner
} }
return nil return nil
} }
// loadAttributes loads all attributes // LoadAttributes loads all attributes
func (actions ActionList) loadAttributes(ctx context.Context) error { func (actions ActionList) LoadAttributes(ctx context.Context) error {
userMap, err := actions.loadUsers(ctx) // the load sequence cannot be changed because of the dependencies
userMap, err := actions.LoadActUsers(ctx)
if err != nil { if err != nil {
return err return err
} }
if err := actions.LoadRepositories(ctx); err != nil {
if err := actions.loadRepositories(ctx); err != nil {
return err return err
} }
if err := actions.loadRepoOwner(ctx, userMap); err != nil {
return actions.loadRepoOwner(ctx, userMap) return err
}
if err := actions.LoadIssues(ctx); err != nil {
return err
}
return actions.LoadComments(ctx)
}
func (actions ActionList) LoadComments(ctx context.Context) error {
if len(actions) == 0 {
return nil
}
commentIDs := make([]int64, 0, len(actions))
for _, action := range actions {
if action.CommentID > 0 {
commentIDs = append(commentIDs, action.CommentID)
}
}
commentsMap := make(map[int64]*issues_model.Comment, len(commentIDs))
if err := db.GetEngine(ctx).In("id", commentIDs).Find(&commentsMap); err != nil {
return fmt.Errorf("find comment: %w", err)
}
for _, action := range actions {
if action.CommentID > 0 {
action.Comment = commentsMap[action.CommentID]
if action.Comment != nil {
action.Comment.Issue = action.Issue
}
}
}
return nil
}
func (actions ActionList) LoadIssues(ctx context.Context) error {
if len(actions) == 0 {
return nil
}
conditions := builder.NewCond()
issueNum := 0
for _, action := range actions {
if action.IsIssueEvent() {
infos := action.GetIssueInfos()
if len(infos) == 0 {
continue
}
index, _ := strconv.ParseInt(infos[0], 10, 64)
if index > 0 {
conditions = conditions.Or(builder.Eq{
"repo_id": action.RepoID,
"`index`": index,
})
issueNum++
}
}
}
if !conditions.IsValid() {
return nil
}
issuesMap := make(map[string]*issues_model.Issue, issueNum)
issues := make([]*issues_model.Issue, 0, issueNum)
if err := db.GetEngine(ctx).Where(conditions).Find(&issues); err != nil {
return fmt.Errorf("find issue: %w", err)
}
for _, issue := range issues {
issuesMap[fmt.Sprintf("%d-%d", issue.RepoID, issue.Index)] = issue
}
for _, action := range actions {
if !action.IsIssueEvent() {
continue
}
if index := action.getIssueIndex(); index > 0 {
if issue, ok := issuesMap[fmt.Sprintf("%d-%d", action.RepoID, index)]; ok {
action.Issue = issue
action.Issue.Repo = action.Repo
}
}
}
return nil
} }

View file

@ -9,6 +9,7 @@ import (
asymkey_model "code.gitea.io/gitea/models/asymkey" asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
@ -29,7 +30,8 @@ type Statistic struct {
Mirror, Release, AuthSource, Webhook, Mirror, Release, AuthSource, Webhook,
Milestone, Label, HookTask, Milestone, Label, HookTask,
Team, UpdateTask, Project, Team, UpdateTask, Project,
ProjectBoard, Attachment int64 ProjectBoard, Attachment,
Branches, Tags, CommitStatus int64
IssueByLabel []IssueByLabelCount IssueByLabel []IssueByLabelCount
IssueByRepository []IssueByRepositoryCount IssueByRepository []IssueByRepositoryCount
} }
@ -58,6 +60,9 @@ func GetStatistic(ctx context.Context) (stats Statistic) {
stats.Counter.Watch, _ = e.Count(new(repo_model.Watch)) stats.Counter.Watch, _ = e.Count(new(repo_model.Watch))
stats.Counter.Star, _ = e.Count(new(repo_model.Star)) stats.Counter.Star, _ = e.Count(new(repo_model.Star))
stats.Counter.Access, _ = e.Count(new(access_model.Access)) stats.Counter.Access, _ = e.Count(new(access_model.Access))
stats.Counter.Branches, _ = e.Count(new(git_model.Branch))
stats.Counter.Tags, _ = e.Where("is_draft=?", false).Count(new(repo_model.Release))
stats.Counter.CommitStatus, _ = e.Count(new(git_model.CommitStatus))
type IssueCount struct { type IssueCount struct {
Count int64 Count int64

View file

@ -476,6 +476,16 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) {
} }
trackedTimes := make(map[int64]int64, len(issues)) trackedTimes := make(map[int64]int64, len(issues))
reposMap := make(map[int64]*repo_model.Repository, len(issues))
for _, issue := range issues {
reposMap[issue.RepoID] = issue.Repo
}
repos := repo_model.RepositoryListOfMap(reposMap)
if err := repos.LoadUnits(ctx); err != nil {
return err
}
ids := make([]int64, 0, len(issues)) ids := make([]int64, 0, len(issues))
for _, issue := range issues { for _, issue := range issues {
if issue.Repo.IsTimetrackerEnabled(ctx) { if issue.Repo.IsTimetrackerEnabled(ctx) {

View file

@ -393,7 +393,7 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64)
func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session { func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session {
// Query for pull requests where you are a reviewer or commenter, excluding // Query for pull requests where you are a reviewer or commenter, excluding
// any pull requests already returned by the the review requested filter. // any pull requests already returned by the review requested filter.
notPoster := builder.Neq{"issue.poster_id": reviewedID} notPoster := builder.Neq{"issue.poster_id": reviewedID}
reviewed := builder.In("issue.id", builder. reviewed := builder.In("issue.id", builder.
Select("issue_id"). Select("issue_id").

View file

@ -553,6 +553,9 @@ func (repo *Repository) GetBaseRepo(ctx context.Context) (err error) {
return nil return nil
} }
if repo.BaseRepo != nil {
return nil
}
repo.BaseRepo, err = GetRepositoryByID(ctx, repo.ForkID) repo.BaseRepo, err = GetRepositoryByID(ctx, repo.ForkID)
return err return err
} }

View file

@ -63,6 +63,41 @@ func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
return RepositoryList(ValuesRepository(repoMap)) return RepositoryList(ValuesRepository(repoMap))
} }
func (repos RepositoryList) LoadUnits(ctx context.Context) error {
if len(repos) == 0 {
return nil
}
// Load units.
units := make([]*RepoUnit, 0, len(repos)*6)
if err := db.GetEngine(ctx).
In("repo_id", repos.IDs()).
Find(&units); err != nil {
return fmt.Errorf("find units: %w", err)
}
unitsMap := make(map[int64][]*RepoUnit, len(repos))
for _, unit := range units {
if !unit.Type.UnitGlobalDisabled() {
unitsMap[unit.RepoID] = append(unitsMap[unit.RepoID], unit)
}
}
for _, repo := range repos {
repo.Units = unitsMap[repo.ID]
}
return nil
}
func (repos RepositoryList) IDs() []int64 {
repoIDs := make([]int64, len(repos))
for i := range repos {
repoIDs[i] = repos[i].ID
}
return repoIDs
}
// LoadAttributes loads the attributes for the given RepositoryList // LoadAttributes loads the attributes for the given RepositoryList
func (repos RepositoryList) LoadAttributes(ctx context.Context) error { func (repos RepositoryList) LoadAttributes(ctx context.Context) error {
if len(repos) == 0 { if len(repos) == 0 {

View file

@ -548,17 +548,17 @@ func validateEmailBasic(email string) error {
// validateEmailDomain checks whether the email domain is allowed or blocked // validateEmailDomain checks whether the email domain is allowed or blocked
func validateEmailDomain(email string) error { func validateEmailDomain(email string) error {
// if there is no allow list, then check email against block list if !IsEmailDomainAllowed(email) {
if len(setting.Service.EmailDomainAllowList) == 0 &&
validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) {
return ErrEmailInvalid{email}
}
// if there is an allow list, then check email against allow list
if len(setting.Service.EmailDomainAllowList) > 0 &&
!validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) {
return ErrEmailInvalid{email} return ErrEmailInvalid{email}
} }
return nil return nil
} }
func IsEmailDomainAllowed(email string) bool {
if len(setting.Service.EmailDomainAllowList) == 0 {
return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email)
}
return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email)
}

View file

@ -436,7 +436,7 @@ func (u *User) GetDisplayName() string {
return u.Name return u.Name
} }
// GetCompleteName returns the the full name and username in the form of // GetCompleteName returns the full name and username in the form of
// "Full Name (username)" if full name is not empty, otherwise it returns // "Full Name (username)" if full name is not empty, otherwise it returns
// "username". // "username".
func (u *User) GetCompleteName() string { func (u *User) GetCompleteName() string {

View file

@ -120,11 +120,12 @@ func TestReadingBlameOutputSha256(t *testing.T) {
}, },
} }
objectFormat, err := repo.GetObjectFormat()
assert.NoError(t, err)
for _, c := range cases { for _, c := range cases {
commit, err := repo.GetCommit(c.CommitID) commit, err := repo.GetCommit(c.CommitID)
assert.NoError(t, err) assert.NoError(t, err)
blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass)
blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, blameReader) assert.NotNil(t, blameReader)
defer blameReader.Close() defer blameReader.Close()

View file

@ -118,11 +118,13 @@ func TestReadingBlameOutput(t *testing.T) {
}, },
} }
objectFormat, err := repo.GetObjectFormat()
assert.NoError(t, err)
for _, c := range cases { for _, c := range cases {
commit, err := repo.GetCommit(c.CommitID) commit, err := repo.GetCommit(c.CommitID)
assert.NoError(t, err) assert.NoError(t, err)
blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass) blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, blameReader) assert.NotNil(t, blameReader)
defer blameReader.Close() defer blameReader.Close()

View file

@ -311,7 +311,7 @@ func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error)
return c.repo.GetFilesChangedBetween(pastCommit, c.ID.String()) return c.repo.GetFilesChangedBetween(pastCommit, c.ID.String())
} }
// FileChangedSinceCommit Returns true if the file given has changed since the the past commit // FileChangedSinceCommit Returns true if the file given has changed since the past commit
// YOU MUST ENSURE THAT pastCommit is a valid commit ID. // YOU MUST ENSURE THAT pastCommit is a valid commit ID.
func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) { func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) {
return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String()) return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())

View file

@ -152,10 +152,13 @@ func TestHasPreviousCommitSha256(t *testing.T) {
commit, err := repo.GetCommit("f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc") commit, err := repo.GetCommit("f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc")
assert.NoError(t, err) assert.NoError(t, err)
objectFormat, err := repo.GetObjectFormat()
assert.NoError(t, err)
parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c") parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c")
notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236") notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236")
assert.Equal(t, repo.objectFormat, parentSHA.Type()) assert.Equal(t, objectFormat, parentSHA.Type())
assert.Equal(t, repo.objectFormat.Name(), "sha256") assert.Equal(t, objectFormat.Name(), "sha256")
haz, err := commit.HasPreviousCommit(parentSHA) haz, err := commit.HasPreviousCommit(parentSHA)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -71,11 +71,6 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) {
repo.batchWriter, repo.batchReader, repo.batchCancel = CatFileBatch(ctx, repoPath) repo.batchWriter, repo.batchReader, repo.batchCancel = CatFileBatch(ctx, repoPath)
repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repoPath) repo.checkWriter, repo.checkReader, repo.checkCancel = CatFileBatchCheck(ctx, repoPath)
repo.objectFormat, err = repo.GetObjectFormat()
if err != nil {
return nil, err
}
return repo, nil return repo, nil
} }

View file

@ -246,7 +246,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions)
} }
}() }()
len := repo.objectFormat.FullLength() objectFormat, err := repo.GetObjectFormat()
if err != nil {
return nil, err
}
len := objectFormat.FullLength()
commits := []*Commit{} commits := []*Commit{}
shaline := make([]byte, len+1) shaline := make([]byte, len+1)
for { for {

View file

@ -41,7 +41,10 @@ func (repo *Repository) RemoveReference(name string) error {
// ConvertToHash returns a Hash object from a potential ID string // ConvertToHash returns a Hash object from a potential ID string
func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
objectFormat := repo.objectFormat objectFormat, err := repo.GetObjectFormat()
if err != nil {
return nil, err
}
if len(commitID) == hash.HexSize && objectFormat.IsValid(commitID) { if len(commitID) == hash.HexSize && objectFormat.IsValid(commitID) {
ID, err := NewIDFromString(commitID) ID, err := NewIDFromString(commitID)
if err == nil { if err == nil {

View file

@ -132,8 +132,11 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID)
// ConvertToGitID returns a GitHash object from a potential ID string // ConvertToGitID returns a GitHash object from a potential ID string
func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
IDType := repo.objectFormat objectFormat, err := repo.GetObjectFormat()
if len(commitID) == IDType.FullLength() && IDType.IsValid(commitID) { if err != nil {
return nil, err
}
if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) {
ID, err := NewIDFromString(commitID) ID, err := NewIDFromString(commitID)
if err == nil { if err == nil {
return ID, nil return ID, nil
@ -142,7 +145,7 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) {
wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx) wr, rd, cancel := repo.CatFileBatchCheck(repo.Ctx)
defer cancel() defer cancel()
_, err := wr.Write([]byte(commitID + "\n")) _, err = wr.Write([]byte(commitID + "\n"))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -283,8 +283,12 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error {
// If base is undefined empty SHA (zeros), it only returns the files changed in the head commit // If base is undefined empty SHA (zeros), it only returns the files changed in the head commit
// If base is the SHA of an empty tree (EmptyTreeSHA), it returns the files changes from the initial commit to the head commit // If base is the SHA of an empty tree (EmptyTreeSHA), it returns the files changes from the initial commit to the head commit
func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) { func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) {
objectFormat, err := repo.GetObjectFormat()
if err != nil {
return nil, err
}
cmd := NewCommand(repo.Ctx, "diff-tree", "--name-only", "--root", "--no-commit-id", "-r", "-z") cmd := NewCommand(repo.Ctx, "diff-tree", "--name-only", "--root", "--no-commit-id", "-r", "-z")
if base == repo.objectFormat.EmptyObjectID().String() { if base == objectFormat.EmptyObjectID().String() {
cmd.AddDynamicArguments(head) cmd.AddDynamicArguments(head)
} else { } else {
cmd.AddDynamicArguments(base, head) cmd.AddDynamicArguments(base, head)

View file

@ -126,17 +126,20 @@ func TestGetCommitFilesChanged(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
defer repo.Close() defer repo.Close()
objectFormat, err := repo.GetObjectFormat()
assert.NoError(t, err)
testCases := []struct { testCases := []struct {
base, head string base, head string
files []string files []string
}{ }{
{ {
repo.objectFormat.EmptyObjectID().String(), objectFormat.EmptyObjectID().String(),
"95bb4d39648ee7e325106df01a621c530863a653", "95bb4d39648ee7e325106df01a621c530863a653",
[]string{"file1.txt"}, []string{"file1.txt"},
}, },
{ {
repo.objectFormat.EmptyObjectID().String(), objectFormat.EmptyObjectID().String(),
"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2",
[]string{"file2.txt"}, []string{"file2.txt"},
}, },
@ -146,7 +149,7 @@ func TestGetCommitFilesChanged(t *testing.T) {
[]string{"file2.txt"}, []string{"file2.txt"},
}, },
{ {
repo.objectFormat.EmptyTree().String(), objectFormat.EmptyTree().String(),
"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2",
[]string{"file1.txt", "file2.txt"}, []string{"file1.txt", "file2.txt"},
}, },

View file

@ -94,6 +94,10 @@ func (repo *Repository) LsFiles(filenames ...string) ([]string, error) {
// RemoveFilesFromIndex removes given filenames from the index - it does not check whether they are present. // RemoveFilesFromIndex removes given filenames from the index - it does not check whether they are present.
func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error { func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error {
objectFormat, err := repo.GetObjectFormat()
if err != nil {
return err
}
cmd := NewCommand(repo.Ctx, "update-index", "--remove", "-z", "--index-info") cmd := NewCommand(repo.Ctx, "update-index", "--remove", "-z", "--index-info")
stdout := new(bytes.Buffer) stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer) stderr := new(bytes.Buffer)
@ -101,7 +105,7 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error {
for _, file := range filenames { for _, file := range filenames {
if file != "" { if file != "" {
buffer.WriteString("0 ") buffer.WriteString("0 ")
buffer.WriteString(repo.objectFormat.EmptyObjectID().String()) buffer.WriteString(objectFormat.EmptyObjectID().String())
buffer.WriteByte('\t') buffer.WriteByte('\t')
buffer.WriteString(file) buffer.WriteString(file)
buffer.WriteByte('\000') buffer.WriteByte('\000')

View file

@ -141,7 +141,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
break break
} }
tag, err := parseTagRef(repo.objectFormat, ref) tag, err := parseTagRef(ref)
if err != nil { if err != nil {
return nil, 0, fmt.Errorf("GetTagInfos: parse tag: %w", err) return nil, 0, fmt.Errorf("GetTagInfos: parse tag: %w", err)
} }
@ -161,7 +161,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) {
} }
// parseTagRef parses a tag from a 'git for-each-ref'-produced reference. // parseTagRef parses a tag from a 'git for-each-ref'-produced reference.
func parseTagRef(objectFormat ObjectFormat, ref map[string]string) (tag *Tag, err error) { func parseTagRef(ref map[string]string) (tag *Tag, err error) {
tag = &Tag{ tag = &Tag{
Type: ref["objecttype"], Type: ref["objecttype"],
Name: ref["refname:lstrip=2"], Name: ref["refname:lstrip=2"],

View file

@ -194,7 +194,6 @@ func TestRepository_GetAnnotatedTag(t *testing.T) {
} }
func TestRepository_parseTagRef(t *testing.T) { func TestRepository_parseTagRef(t *testing.T) {
sha1 := Sha1ObjectFormat
tests := []struct { tests := []struct {
name string name string
@ -351,7 +350,7 @@ Add changelog of v1.9.1 (#7859)
for _, test := range tests { for _, test := range tests {
tc := test // don't close over loop variable tc := test // don't close over loop variable
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
got, err := parseTagRef(sha1, tc.givenRef) got, err := parseTagRef(tc.givenRef)
if tc.wantErr { if tc.wantErr {
require.Error(t, err) require.Error(t, err)

View file

@ -21,7 +21,12 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
// GetTree find the tree object in the repository. // GetTree find the tree object in the repository.
func (repo *Repository) GetTree(idStr string) (*Tree, error) { func (repo *Repository) GetTree(idStr string) (*Tree, error) {
if len(idStr) != repo.objectFormat.FullLength() { objectFormat, err := repo.GetObjectFormat()
if err != nil {
return nil, err
}
if len(idStr) != objectFormat.FullLength() {
res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(&RunOpts{Dir: repo.Path}) res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(&RunOpts{Dir: repo.Path})
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -51,7 +51,11 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
case "tree": case "tree":
tree := NewTree(repo, id) tree := NewTree(repo, id)
tree.ResolvedID = id tree.ResolvedID = id
tree.entries, err = catBatchParseTreeEntries(repo.objectFormat, tree, rd, size) objectFormat, err := repo.GetObjectFormat()
if err != nil {
return nil, err
}
tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, size)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -69,7 +73,11 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
// GetTree find the tree object in the repository. // GetTree find the tree object in the repository.
func (repo *Repository) GetTree(idStr string) (*Tree, error) { func (repo *Repository) GetTree(idStr string) (*Tree, error) {
if len(idStr) != repo.objectFormat.FullLength() { objectFormat, err := repo.GetObjectFormat()
if err != nil {
return nil, err
}
if len(idStr) != objectFormat.FullLength() {
res, err := repo.GetRefCommitID(idStr) res, err := repo.GetRefCommitID(idStr)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -77,8 +77,11 @@ func (t *Tree) ListEntries() (Entries, error) {
return nil, runErr return nil, runErr
} }
var err error objectFormat, err := t.repo.GetObjectFormat()
t.entries, err = parseTreeEntries(t.repo.objectFormat, stdout, t) if err != nil {
return nil, err
}
t.entries, err = parseTreeEntries(objectFormat, stdout, t)
if err == nil { if err == nil {
t.entriesParsed = true t.entriesParsed = true
} }
@ -101,8 +104,11 @@ func (t *Tree) listEntriesRecursive(extraArgs TrustedCmdArgs) (Entries, error) {
return nil, runErr return nil, runErr
} }
var err error objectFormat, err := t.repo.GetObjectFormat()
t.entriesRecursive, err = parseTreeEntries(t.repo.objectFormat, stdout, t) if err != nil {
return nil, err
}
t.entriesRecursive, err = parseTreeEntries(objectFormat, stdout, t)
if err == nil { if err == nil {
t.entriesRecursiveParsed = true t.entriesRecursiveParsed = true
} }

View file

@ -233,7 +233,10 @@ func (g *Manager) setStateTransition(old, new state) bool {
// At the moment the total number of servers (numberOfServersToCreate) are pre-defined as a const before global init, // At the moment the total number of servers (numberOfServersToCreate) are pre-defined as a const before global init,
// so this function MUST be called if a server is not used. // so this function MUST be called if a server is not used.
func (g *Manager) InformCleanup() { func (g *Manager) InformCleanup() {
g.createServerWaitGroup.Done() g.createServerCond.L.Lock()
defer g.createServerCond.L.Unlock()
g.createdServer++
g.createServerCond.Signal()
} }
// Done allows the manager to be viewed as a context.Context, it returns a channel that is closed when the server is finished terminating // Done allows the manager to be viewed as a context.Context, it returns a channel that is closed when the server is finished terminating

View file

@ -42,8 +42,9 @@ type Manager struct {
terminateCtxCancel context.CancelFunc terminateCtxCancel context.CancelFunc
managerCtxCancel context.CancelFunc managerCtxCancel context.CancelFunc
runningServerWaitGroup sync.WaitGroup runningServerWaitGroup sync.WaitGroup
createServerWaitGroup sync.WaitGroup
terminateWaitGroup sync.WaitGroup terminateWaitGroup sync.WaitGroup
createServerCond sync.Cond
createdServer int
shutdownRequested chan struct{} shutdownRequested chan struct{}
toRunAtShutdown []func() toRunAtShutdown []func()
@ -52,7 +53,7 @@ type Manager struct {
func newGracefulManager(ctx context.Context) *Manager { func newGracefulManager(ctx context.Context) *Manager {
manager := &Manager{ctx: ctx, shutdownRequested: make(chan struct{})} manager := &Manager{ctx: ctx, shutdownRequested: make(chan struct{})}
manager.createServerWaitGroup.Add(numberOfServersToCreate) manager.createServerCond.L = &sync.Mutex{}
manager.prepare(ctx) manager.prepare(ctx)
manager.start() manager.start()
return manager return manager

View file

@ -57,20 +57,27 @@ func (g *Manager) start() {
// Handle clean up of unused provided listeners and delayed start-up // Handle clean up of unused provided listeners and delayed start-up
startupDone := make(chan struct{}) startupDone := make(chan struct{})
go func() { go func() {
defer close(startupDone) defer func() {
// Wait till we're done getting all the listeners and then close the unused ones close(startupDone)
func() { // Close the unused listeners and ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function
// FIXME: there is a fundamental design problem of the "manager" and the "wait group". _ = CloseProvidedListeners()
// If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned
// There is no clear solution besides a complete rewriting of the "manager"
defer func() {
_ = recover()
}()
g.createServerWaitGroup.Wait()
}() }()
// Ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function // Wait for all servers to be created
_ = CloseProvidedListeners() g.createServerCond.L.Lock()
g.notify(readyMsg) for {
if g.createdServer >= numberOfServersToCreate {
g.createServerCond.L.Unlock()
g.notify(readyMsg)
return
}
select {
case <-g.IsShutdown():
g.createServerCond.L.Unlock()
return
default:
}
g.createServerCond.Wait()
}
}() }()
if setting.StartupTimeout > 0 { if setting.StartupTimeout > 0 {
go func() { go func() {
@ -78,16 +85,7 @@ func (g *Manager) start() {
case <-startupDone: case <-startupDone:
return return
case <-g.IsShutdown(): case <-g.IsShutdown():
func() { g.createServerCond.Signal()
// When WaitGroup counter goes negative it will panic - we don't care about this so we can just ignore it.
defer func() {
_ = recover()
}()
// Ensure that the createServerWaitGroup stops waiting
for {
g.createServerWaitGroup.Done()
}
}()
return return
case <-time.After(setting.StartupTimeout): case <-time.After(setting.StartupTimeout):
log.Error("Startup took too long! Shutting down") log.Error("Startup took too long! Shutting down")

View file

@ -149,33 +149,35 @@ hammerLoop:
func (g *Manager) awaitServer(limit time.Duration) bool { func (g *Manager) awaitServer(limit time.Duration) bool {
c := make(chan struct{}) c := make(chan struct{})
go func() { go func() {
defer close(c) g.createServerCond.L.Lock()
func() { for {
// FIXME: there is a fundamental design problem of the "manager" and the "wait group". if g.createdServer >= numberOfServersToCreate {
// If nothing has started, the "Wait" just panics: sync: WaitGroup is reused before previous Wait has returned g.createServerCond.L.Unlock()
// There is no clear solution besides a complete rewriting of the "manager" close(c)
defer func() { return
_ = recover() }
}() select {
g.createServerWaitGroup.Wait() case <-g.IsShutdown():
}() g.createServerCond.L.Unlock()
return
default:
}
g.createServerCond.Wait()
}
}() }()
var tc <-chan time.Time
if limit > 0 { if limit > 0 {
select { tc = time.After(limit)
case <-c: }
return true // completed normally select {
case <-time.After(limit): case <-c:
return false // timed out return true // completed normally
case <-g.IsShutdown(): case <-tc:
return false return false // timed out
} case <-g.IsShutdown():
} else { g.createServerCond.Signal()
select { return false
case <-c:
return true // completed normally
case <-g.IsShutdown():
return false
}
} }
} }

View file

@ -91,11 +91,9 @@ func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision s
return nil, runErr return nil, runErr
} }
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
var err error var err error
objectFormat, err := git.GetObjectFormatOfRepo(ctx, repo.RepoPath())
if err != nil {
return nil, err
}
changes.Updates, err = parseGitLsTreeOutput(objectFormat, stdout) changes.Updates, err = parseGitLsTreeOutput(objectFormat, stdout)
return &changes, err return &changes, err
} }
@ -174,10 +172,8 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio
return nil, err return nil, err
} }
objectFormat, err := git.GetObjectFormatOfRepo(ctx, repo.RepoPath()) objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
if err != nil {
return nil, err
}
changes.Updates, err = parseGitLsTreeOutput(objectFormat, lsTreeStdout) changes.Updates, err = parseGitLsTreeOutput(objectFormat, lsTreeStdout)
return &changes, err return &changes, err
} }

View file

@ -4,6 +4,8 @@
package bleve package bleve
import ( import (
"code.gitea.io/gitea/modules/optional"
"github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/search/query" "github.com/blevesearch/bleve/v2/search/query"
) )
@ -39,18 +41,18 @@ func BoolFieldQuery(value bool, field string) *query.BoolFieldQuery {
return q return q
} }
func NumericRangeInclusiveQuery(min, max *int64, field string) *query.NumericRangeQuery { func NumericRangeInclusiveQuery(min, max optional.Option[int64], field string) *query.NumericRangeQuery {
var minF, maxF *float64 var minF, maxF *float64
var minI, maxI *bool var minI, maxI *bool
if min != nil { if min.Has() {
minF = new(float64) minF = new(float64)
*minF = float64(*min) *minF = float64(min.Value())
minI = new(bool) minI = new(bool)
*minI = true *minI = true
} }
if max != nil { if max.Has() {
maxF = new(float64) maxF = new(float64)
*maxF = float64(*max) *maxF = float64(max.Value())
maxI = new(bool) maxI = new(bool)
*maxI = true *maxI = true
} }

View file

@ -224,38 +224,41 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
queries = append(queries, bleve.NewDisjunctionQuery(milestoneQueries...)) queries = append(queries, bleve.NewDisjunctionQuery(milestoneQueries...))
} }
if options.ProjectID != nil { if options.ProjectID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ProjectID, "project_id")) queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectID.Value(), "project_id"))
} }
if options.ProjectBoardID != nil { if options.ProjectBoardID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ProjectBoardID, "project_board_id")) queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectBoardID.Value(), "project_board_id"))
} }
if options.PosterID != nil { if options.PosterID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(*options.PosterID, "poster_id")) queries = append(queries, inner_bleve.NumericEqualityQuery(options.PosterID.Value(), "poster_id"))
} }
if options.AssigneeID != nil { if options.AssigneeID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(*options.AssigneeID, "assignee_id")) queries = append(queries, inner_bleve.NumericEqualityQuery(options.AssigneeID.Value(), "assignee_id"))
} }
if options.MentionID != nil { if options.MentionID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(*options.MentionID, "mention_ids")) queries = append(queries, inner_bleve.NumericEqualityQuery(options.MentionID.Value(), "mention_ids"))
} }
if options.ReviewedID != nil { if options.ReviewedID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ReviewedID, "reviewed_ids")) queries = append(queries, inner_bleve.NumericEqualityQuery(options.ReviewedID.Value(), "reviewed_ids"))
} }
if options.ReviewRequestedID != nil { if options.ReviewRequestedID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(*options.ReviewRequestedID, "review_requested_ids")) queries = append(queries, inner_bleve.NumericEqualityQuery(options.ReviewRequestedID.Value(), "review_requested_ids"))
} }
if options.SubscriberID != nil { if options.SubscriberID.Has() {
queries = append(queries, inner_bleve.NumericEqualityQuery(*options.SubscriberID, "subscriber_ids")) queries = append(queries, inner_bleve.NumericEqualityQuery(options.SubscriberID.Value(), "subscriber_ids"))
} }
if options.UpdatedAfterUnix != nil || options.UpdatedBeforeUnix != nil { if options.UpdatedAfterUnix.Has() || options.UpdatedBeforeUnix.Has() {
queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(options.UpdatedAfterUnix, options.UpdatedBeforeUnix, "updated_unix")) queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(
options.UpdatedAfterUnix,
options.UpdatedBeforeUnix,
"updated_unix"))
} }
var indexerQuery query.Query = bleve.NewConjunctionQuery(queries...) var indexerQuery query.Query = bleve.NewConjunctionQuery(queries...)

View file

@ -15,22 +15,6 @@ import (
) )
func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_model.IssuesOptions, error) { func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_model.IssuesOptions, error) {
// See the comment of issues_model.SearchOptions for the reason why we need to convert
convertID := func(id *int64) int64 {
if id == nil {
return 0
}
if *id == 0 {
return db.NoConditionID
}
return *id
}
convertInt64 := func(i *int64) int64 {
if i == nil {
return 0
}
return *i
}
var sortType string var sortType string
switch options.SortBy { switch options.SortBy {
case internal.SortByCreatedAsc: case internal.SortByCreatedAsc:
@ -53,6 +37,18 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
sortType = "newest" sortType = "newest"
} }
// See the comment of issues_model.SearchOptions for the reason why we need to convert
convertID := func(id optional.Option[int64]) int64 {
if !id.Has() {
return 0
}
value := id.Value()
if value == 0 {
return db.NoConditionID
}
return value
}
opts := &issue_model.IssuesOptions{ opts := &issue_model.IssuesOptions{
Paginator: options.Paginator, Paginator: options.Paginator,
RepoIDs: options.RepoIDs, RepoIDs: options.RepoIDs,
@ -73,8 +69,8 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
IncludeMilestones: nil, IncludeMilestones: nil,
SortType: sortType, SortType: sortType,
IssueIDs: nil, IssueIDs: nil,
UpdatedAfterUnix: convertInt64(options.UpdatedAfterUnix), UpdatedAfterUnix: options.UpdatedAfterUnix.Value(),
UpdatedBeforeUnix: convertInt64(options.UpdatedBeforeUnix), UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(),
PriorityRepoID: 0, PriorityRepoID: 0,
IsArchived: optional.None[bool](), IsArchived: optional.None[bool](),
Org: nil, Org: nil,

View file

@ -6,6 +6,7 @@ package issues
import ( import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/optional"
) )
func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOptions { func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOptions {
@ -38,13 +39,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
} }
// See the comment of issues_model.SearchOptions for the reason why we need to convert // See the comment of issues_model.SearchOptions for the reason why we need to convert
convertID := func(id int64) *int64 { convertID := func(id int64) optional.Option[int64] {
if id > 0 { if id > 0 {
return &id return optional.Some(id)
} }
if id == db.NoConditionID { if id == db.NoConditionID {
var zero int64 return optional.None[int64]()
return &zero
} }
return nil return nil
} }
@ -59,10 +59,10 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
searchOpt.SubscriberID = convertID(opts.SubscriberID) searchOpt.SubscriberID = convertID(opts.SubscriberID)
if opts.UpdatedAfterUnix > 0 { if opts.UpdatedAfterUnix > 0 {
searchOpt.UpdatedAfterUnix = &opts.UpdatedAfterUnix searchOpt.UpdatedAfterUnix = optional.Some(opts.UpdatedAfterUnix)
} }
if opts.UpdatedBeforeUnix > 0 { if opts.UpdatedBeforeUnix > 0 {
searchOpt.UpdatedBeforeUnix = &opts.UpdatedBeforeUnix searchOpt.UpdatedBeforeUnix = optional.Some(opts.UpdatedBeforeUnix)
} }
searchOpt.Paginator = opts.Paginator searchOpt.Paginator = opts.Paginator

View file

@ -195,43 +195,43 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
query.Must(elastic.NewTermsQuery("milestone_id", toAnySlice(options.MilestoneIDs)...)) query.Must(elastic.NewTermsQuery("milestone_id", toAnySlice(options.MilestoneIDs)...))
} }
if options.ProjectID != nil { if options.ProjectID.Has() {
query.Must(elastic.NewTermQuery("project_id", *options.ProjectID)) query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value()))
} }
if options.ProjectBoardID != nil { if options.ProjectBoardID.Has() {
query.Must(elastic.NewTermQuery("project_board_id", *options.ProjectBoardID)) query.Must(elastic.NewTermQuery("project_board_id", options.ProjectBoardID.Value()))
} }
if options.PosterID != nil { if options.PosterID.Has() {
query.Must(elastic.NewTermQuery("poster_id", *options.PosterID)) query.Must(elastic.NewTermQuery("poster_id", options.PosterID.Value()))
} }
if options.AssigneeID != nil { if options.AssigneeID.Has() {
query.Must(elastic.NewTermQuery("assignee_id", *options.AssigneeID)) query.Must(elastic.NewTermQuery("assignee_id", options.AssigneeID.Value()))
} }
if options.MentionID != nil { if options.MentionID.Has() {
query.Must(elastic.NewTermQuery("mention_ids", *options.MentionID)) query.Must(elastic.NewTermQuery("mention_ids", options.MentionID.Value()))
} }
if options.ReviewedID != nil { if options.ReviewedID.Has() {
query.Must(elastic.NewTermQuery("reviewed_ids", *options.ReviewedID)) query.Must(elastic.NewTermQuery("reviewed_ids", options.ReviewedID.Value()))
} }
if options.ReviewRequestedID != nil { if options.ReviewRequestedID.Has() {
query.Must(elastic.NewTermQuery("review_requested_ids", *options.ReviewRequestedID)) query.Must(elastic.NewTermQuery("review_requested_ids", options.ReviewRequestedID.Value()))
} }
if options.SubscriberID != nil { if options.SubscriberID.Has() {
query.Must(elastic.NewTermQuery("subscriber_ids", *options.SubscriberID)) query.Must(elastic.NewTermQuery("subscriber_ids", options.SubscriberID.Value()))
} }
if options.UpdatedAfterUnix != nil || options.UpdatedBeforeUnix != nil { if options.UpdatedAfterUnix.Has() || options.UpdatedBeforeUnix.Has() {
q := elastic.NewRangeQuery("updated_unix") q := elastic.NewRangeQuery("updated_unix")
if options.UpdatedAfterUnix != nil { if options.UpdatedAfterUnix.Has() {
q.Gte(*options.UpdatedAfterUnix) q.Gte(options.UpdatedAfterUnix.Value())
} }
if options.UpdatedBeforeUnix != nil { if options.UpdatedBeforeUnix.Has() {
q.Lte(*options.UpdatedBeforeUnix) q.Lte(options.UpdatedBeforeUnix.Value())
} }
query.Must(q) query.Must(q)
} }

View file

@ -134,63 +134,60 @@ func searchIssueInRepo(t *testing.T) {
} }
func searchIssueByID(t *testing.T) { func searchIssueByID(t *testing.T) {
int64Pointer := func(x int64) *int64 {
return &x
}
tests := []struct { tests := []struct {
opts SearchOptions opts SearchOptions
expectedIDs []int64 expectedIDs []int64
}{ }{
{ {
SearchOptions{ opts: SearchOptions{
PosterID: int64Pointer(1), PosterID: optional.Some(int64(1)),
}, },
[]int64{11, 6, 3, 2, 1}, expectedIDs: []int64{11, 6, 3, 2, 1},
}, },
{ {
SearchOptions{ opts: SearchOptions{
AssigneeID: int64Pointer(1), AssigneeID: optional.Some(int64(1)),
}, },
[]int64{6, 1}, expectedIDs: []int64{6, 1},
}, },
{ {
SearchOptions{ opts: SearchOptions{
MentionID: int64Pointer(4), MentionID: optional.Some(int64(4)),
}, },
[]int64{1}, expectedIDs: []int64{1},
}, },
{ {
SearchOptions{ opts: SearchOptions{
ReviewedID: int64Pointer(1), ReviewedID: optional.Some(int64(1)),
}, },
[]int64{}, expectedIDs: []int64{},
}, },
{ {
SearchOptions{ opts: SearchOptions{
ReviewRequestedID: int64Pointer(1), ReviewRequestedID: optional.Some(int64(1)),
}, },
[]int64{12}, expectedIDs: []int64{12},
}, },
{ {
SearchOptions{ opts: SearchOptions{
SubscriberID: int64Pointer(1), SubscriberID: optional.Some(int64(1)),
}, },
[]int64{11, 6, 5, 3, 2, 1}, expectedIDs: []int64{11, 6, 5, 3, 2, 1},
}, },
{ {
// issue 20 request user 15 and team 5 which user 15 belongs to // issue 20 request user 15 and team 5 which user 15 belongs to
// the review request number of issue 20 should be 1 // the review request number of issue 20 should be 1
SearchOptions{ opts: SearchOptions{
ReviewRequestedID: int64Pointer(15), ReviewRequestedID: optional.Some(int64(15)),
}, },
[]int64{12, 20}, expectedIDs: []int64{12, 20},
}, },
{ {
// user 20 approved the issue 20, so return nothing // user 20 approved the issue 20, so return nothing
SearchOptions{ opts: SearchOptions{
ReviewRequestedID: int64Pointer(20), ReviewRequestedID: optional.Some(int64(20)),
}, },
[]int64{}, expectedIDs: []int64{},
}, },
} }
@ -318,16 +315,13 @@ func searchIssueByLabelID(t *testing.T) {
} }
func searchIssueByTime(t *testing.T) { func searchIssueByTime(t *testing.T) {
int64Pointer := func(i int64) *int64 {
return &i
}
tests := []struct { tests := []struct {
opts SearchOptions opts SearchOptions
expectedIDs []int64 expectedIDs []int64
}{ }{
{ {
SearchOptions{ SearchOptions{
UpdatedAfterUnix: int64Pointer(0), UpdatedAfterUnix: optional.Some(int64(0)),
}, },
[]int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1}, []int64{22, 21, 17, 16, 15, 14, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1},
}, },
@ -363,28 +357,25 @@ func searchIssueWithOrder(t *testing.T) {
} }
func searchIssueInProject(t *testing.T) { func searchIssueInProject(t *testing.T) {
int64Pointer := func(i int64) *int64 {
return &i
}
tests := []struct { tests := []struct {
opts SearchOptions opts SearchOptions
expectedIDs []int64 expectedIDs []int64
}{ }{
{ {
SearchOptions{ SearchOptions{
ProjectID: int64Pointer(1), ProjectID: optional.Some(int64(1)),
}, },
[]int64{5, 3, 2, 1}, []int64{5, 3, 2, 1},
}, },
{ {
SearchOptions{ SearchOptions{
ProjectBoardID: int64Pointer(1), ProjectBoardID: optional.Some(int64(1)),
}, },
[]int64{1}, []int64{1},
}, },
{ {
SearchOptions{ SearchOptions{
ProjectBoardID: int64Pointer(0), // issue with in default board ProjectBoardID: optional.Some(int64(0)), // issue with in default board
}, },
[]int64{2}, []int64{2},
}, },

View file

@ -89,22 +89,22 @@ type SearchOptions struct {
MilestoneIDs []int64 // milestones the issues have MilestoneIDs []int64 // milestones the issues have
ProjectID *int64 // project the issues belong to ProjectID optional.Option[int64] // project the issues belong to
ProjectBoardID *int64 // project board the issues belong to ProjectBoardID optional.Option[int64] // project board the issues belong to
PosterID *int64 // poster of the issues PosterID optional.Option[int64] // poster of the issues
AssigneeID *int64 // assignee of the issues, zero means no assignee AssigneeID optional.Option[int64] // assignee of the issues, zero means no assignee
MentionID *int64 // mentioned user of the issues MentionID optional.Option[int64] // mentioned user of the issues
ReviewedID *int64 // reviewer of the issues ReviewedID optional.Option[int64] // reviewer of the issues
ReviewRequestedID *int64 // requested reviewer of the issues ReviewRequestedID optional.Option[int64] // requested reviewer of the issues
SubscriberID *int64 // subscriber of the issues SubscriberID optional.Option[int64] // subscriber of the issues
UpdatedAfterUnix *int64 UpdatedAfterUnix optional.Option[int64]
UpdatedBeforeUnix *int64 UpdatedBeforeUnix optional.Option[int64]
db.Paginator db.Paginator

View file

@ -300,10 +300,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
ProjectID: func() *int64 { ProjectID: optional.Some(int64(1)),
id := int64(1)
return &id
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -321,10 +318,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
ProjectID: func() *int64 { ProjectID: optional.Some(int64(0)),
id := int64(0)
return &id
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -342,10 +336,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
ProjectBoardID: func() *int64 { ProjectBoardID: optional.Some(int64(1)),
id := int64(1)
return &id
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -363,10 +354,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
ProjectBoardID: func() *int64 { ProjectBoardID: optional.Some(int64(0)),
id := int64(0)
return &id
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -384,10 +372,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
PosterID: func() *int64 { PosterID: optional.Some(int64(1)),
id := int64(1)
return &id
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -405,10 +390,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
AssigneeID: func() *int64 { AssigneeID: optional.Some(int64(1)),
id := int64(1)
return &id
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -426,10 +408,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
AssigneeID: func() *int64 { AssigneeID: optional.Some(int64(0)),
id := int64(0)
return &id
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -447,10 +426,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
MentionID: func() *int64 { MentionID: optional.Some(int64(1)),
id := int64(1)
return &id
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -468,10 +444,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
ReviewedID: func() *int64 { ReviewedID: optional.Some(int64(1)),
id := int64(1)
return &id
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -489,10 +462,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
ReviewRequestedID: func() *int64 { ReviewRequestedID: optional.Some(int64(1)),
id := int64(1)
return &id
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -510,10 +480,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
SubscriberID: func() *int64 { SubscriberID: optional.Some(int64(1)),
id := int64(1)
return &id
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))
@ -531,14 +498,8 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
UpdatedAfterUnix: func() *int64 { UpdatedAfterUnix: optional.Some(int64(20)),
var t int64 = 20 UpdatedBeforeUnix: optional.Some(int64(30)),
return &t
}(),
UpdatedBeforeUnix: func() *int64 {
var t int64 = 30
return &t
}(),
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Equal(t, 5, len(result.Hits)) assert.Equal(t, 5, len(result.Hits))

View file

@ -6,6 +6,7 @@ package meilisearch
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"strconv" "strconv"
"strings" "strings"
@ -170,41 +171,41 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
query.And(inner_meilisearch.NewFilterIn("milestone_id", options.MilestoneIDs...)) query.And(inner_meilisearch.NewFilterIn("milestone_id", options.MilestoneIDs...))
} }
if options.ProjectID != nil { if options.ProjectID.Has() {
query.And(inner_meilisearch.NewFilterEq("project_id", *options.ProjectID)) query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value()))
} }
if options.ProjectBoardID != nil { if options.ProjectBoardID.Has() {
query.And(inner_meilisearch.NewFilterEq("project_board_id", *options.ProjectBoardID)) query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectBoardID.Value()))
} }
if options.PosterID != nil { if options.PosterID.Has() {
query.And(inner_meilisearch.NewFilterEq("poster_id", *options.PosterID)) query.And(inner_meilisearch.NewFilterEq("poster_id", options.PosterID.Value()))
} }
if options.AssigneeID != nil { if options.AssigneeID.Has() {
query.And(inner_meilisearch.NewFilterEq("assignee_id", *options.AssigneeID)) query.And(inner_meilisearch.NewFilterEq("assignee_id", options.AssigneeID.Value()))
} }
if options.MentionID != nil { if options.MentionID.Has() {
query.And(inner_meilisearch.NewFilterEq("mention_ids", *options.MentionID)) query.And(inner_meilisearch.NewFilterEq("mention_ids", options.MentionID.Value()))
} }
if options.ReviewedID != nil { if options.ReviewedID.Has() {
query.And(inner_meilisearch.NewFilterEq("reviewed_ids", *options.ReviewedID)) query.And(inner_meilisearch.NewFilterEq("reviewed_ids", options.ReviewedID.Value()))
} }
if options.ReviewRequestedID != nil { if options.ReviewRequestedID.Has() {
query.And(inner_meilisearch.NewFilterEq("review_requested_ids", *options.ReviewRequestedID)) query.And(inner_meilisearch.NewFilterEq("review_requested_ids", options.ReviewRequestedID.Value()))
} }
if options.SubscriberID != nil { if options.SubscriberID.Has() {
query.And(inner_meilisearch.NewFilterEq("subscriber_ids", *options.SubscriberID)) query.And(inner_meilisearch.NewFilterEq("subscriber_ids", options.SubscriberID.Value()))
} }
if options.UpdatedAfterUnix != nil { if options.UpdatedAfterUnix.Has() {
query.And(inner_meilisearch.NewFilterGte("updated_unix", *options.UpdatedAfterUnix)) query.And(inner_meilisearch.NewFilterGte("updated_unix", options.UpdatedAfterUnix.Value()))
} }
if options.UpdatedBeforeUnix != nil { if options.UpdatedBeforeUnix.Has() {
query.And(inner_meilisearch.NewFilterLte("updated_unix", *options.UpdatedBeforeUnix)) query.And(inner_meilisearch.NewFilterLte("updated_unix", options.UpdatedBeforeUnix.Value()))
} }
if options.SortBy == "" { if options.SortBy == "" {
@ -217,7 +218,14 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
skip, limit := indexer_internal.ParsePaginator(options.Paginator, maxTotalHits) skip, limit := indexer_internal.ParsePaginator(options.Paginator, maxTotalHits)
searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(options.Keyword, &meilisearch.SearchRequest{ 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)
}
searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()).Search(keyword, &meilisearch.SearchRequest{
Filter: query.Statement(), Filter: query.Statement(),
Limit: int64(limit), Limit: int64(limit),
Offset: int64(skip), Offset: int64(skip),
@ -228,7 +236,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
return nil, err return nil, err
} }
hits, err := nonFuzzyWorkaround(searchRes, options.Keyword, options.IsFuzzyKeyword) hits, err := convertHits(searchRes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -247,11 +255,20 @@ func parseSortBy(sortBy internal.SortBy) string {
return field + ":asc" return field + ":asc"
} }
// nonFuzzyWorkaround is needed as meilisearch does not have an exact search func doubleQuoteKeyword(k string) string {
// and you can only change "typo tolerance" per index. So we have to post-filter the results kp := strings.Split(k, " ")
// https://www.meilisearch.com/docs/learn/configuration/typo_tolerance#configuring-typo-tolerance parts := 0
// TODO: remove once https://github.com/orgs/meilisearch/discussions/377 is addressed for i := range kp {
func nonFuzzyWorkaround(searchRes *meilisearch.SearchResponse, keyword string, isFuzzy bool) ([]internal.Match, error) { part := strings.Trim(kp[i], "\"")
if part != "" {
kp[parts] = fmt.Sprintf(`"%s"`, part)
parts++
}
}
return strings.Join(kp[:parts], " ")
}
func convertHits(searchRes *meilisearch.SearchResponse) ([]internal.Match, error) {
hits := make([]internal.Match, 0, len(searchRes.Hits)) hits := make([]internal.Match, 0, len(searchRes.Hits))
for _, hit := range searchRes.Hits { for _, hit := range searchRes.Hits {
hit, ok := hit.(map[string]any) hit, ok := hit.(map[string]any)
@ -259,61 +276,11 @@ func nonFuzzyWorkaround(searchRes *meilisearch.SearchResponse, keyword string, i
return nil, ErrMalformedResponse return nil, ErrMalformedResponse
} }
if !isFuzzy {
keyword = strings.ToLower(keyword)
// declare a anon func to check if the title, content or at least one comment contains the keyword
found, err := func() (bool, error) {
// check if title match first
title, ok := hit["title"].(string)
if !ok {
return false, ErrMalformedResponse
} else if strings.Contains(strings.ToLower(title), keyword) {
return true, nil
}
// check if content has a match
content, ok := hit["content"].(string)
if !ok {
return false, ErrMalformedResponse
} else if strings.Contains(strings.ToLower(content), keyword) {
return true, nil
}
// now check for each comment if one has a match
// so we first try to cast and skip if there are no comments
comments, ok := hit["comments"].([]any)
if !ok {
return false, ErrMalformedResponse
} else if len(comments) == 0 {
return false, nil
}
// now we iterate over all and report as soon as we detect one match
for i := range comments {
comment, ok := comments[i].(string)
if !ok {
return false, ErrMalformedResponse
}
if strings.Contains(strings.ToLower(comment), keyword) {
return true, nil
}
}
// we got no match
return false, nil
}()
if err != nil {
return nil, err
} else if !found {
continue
}
}
issueID, ok := hit["id"].(float64) issueID, ok := hit["id"].(float64)
if !ok { if !ok {
return nil, ErrMalformedResponse return nil, ErrMalformedResponse
} }
hits = append(hits, internal.Match{ hits = append(hits, internal.Match{
ID: int64(issueID), ID: int64(issueID),
}) })

View file

@ -54,11 +54,10 @@ func TestMeilisearchIndexer(t *testing.T) {
tests.TestIndexer(t, indexer) tests.TestIndexer(t, indexer)
} }
func TestNonFuzzyWorkaround(t *testing.T) { func TestConvertHits(t *testing.T) {
// get unexpected return _, err := convertHits(&meilisearch.SearchResponse{
_, err := nonFuzzyWorkaround(&meilisearch.SearchResponse{
Hits: []any{"aa", "bb", "cc", "dd"}, Hits: []any{"aa", "bb", "cc", "dd"},
}, "bowling", false) })
assert.ErrorIs(t, err, ErrMalformedResponse) assert.ErrorIs(t, err, ErrMalformedResponse)
validResponse := &meilisearch.SearchResponse{ validResponse := &meilisearch.SearchResponse{
@ -83,14 +82,15 @@ func TestNonFuzzyWorkaround(t *testing.T) {
}, },
}, },
} }
hits, err := convertHits(validResponse)
// nonFuzzy
hits, err := nonFuzzyWorkaround(validResponse, "bowling", false)
assert.NoError(t, err)
assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}}, hits)
// fuzzy
hits, err = nonFuzzyWorkaround(validResponse, "bowling", true)
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}, {ID: 33}}, hits) assert.EqualValues(t, []internal.Match{{ID: 11}, {ID: 22}, {ID: 33}}, hits)
} }
func TestDoubleQuoteKeyword(t *testing.T) {
assert.EqualValues(t, "", doubleQuoteKeyword(""))
assert.EqualValues(t, `"a" "b" "c"`, doubleQuoteKeyword("a b c"))
assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword("a d g"))
assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword("a d g"))
assert.EqualValues(t, `"a" "d" "g"`, doubleQuoteKeyword(`a "" "d" """g`))
}

View file

@ -77,29 +77,62 @@ func writeField(w io.Writer, element, class, field string) error {
} }
// Render implements markup.Renderer // Render implements markup.Renderer
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
tmpBlock := bufio.NewWriter(output) tmpBlock := bufio.NewWriter(output)
maxSize := setting.UI.CSV.MaxFileSize
// FIXME: don't read all to memory if maxSize == 0 {
rawBytes, err := io.ReadAll(input) return r.tableRender(ctx, input, tmpBlock)
}
rawBytes, err := io.ReadAll(io.LimitReader(input, maxSize+1))
if err != nil { if err != nil {
return err return err
} }
if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < int64(len(rawBytes)) { if int64(len(rawBytes)) <= maxSize {
if _, err := tmpBlock.WriteString("<pre>"); err != nil { return r.tableRender(ctx, bytes.NewReader(rawBytes), tmpBlock)
return err }
} return r.fallbackRender(io.MultiReader(bytes.NewReader(rawBytes), input), tmpBlock)
if _, err := tmpBlock.WriteString(html.EscapeString(string(rawBytes))); err != nil { }
return err
} func (Renderer) fallbackRender(input io.Reader, tmpBlock *bufio.Writer) error {
if _, err := tmpBlock.WriteString("</pre>"); err != nil { _, err := tmpBlock.WriteString("<pre>")
return err if err != nil {
} return err
return tmpBlock.Flush()
} }
rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, bytes.NewReader(rawBytes)) scan := bufio.NewScanner(input)
scan.Split(bufio.ScanRunes)
for scan.Scan() {
switch scan.Text() {
case `&`:
_, err = tmpBlock.WriteString("&amp;")
case `'`:
_, err = tmpBlock.WriteString("&#39;") // "&#39;" is shorter than "&apos;" and apos was not in HTML until HTML5.
case `<`:
_, err = tmpBlock.WriteString("&lt;")
case `>`:
_, err = tmpBlock.WriteString("&gt;")
case `"`:
_, err = tmpBlock.WriteString("&#34;") // "&#34;" is shorter than "&quot;".
default:
_, err = tmpBlock.Write(scan.Bytes())
}
if err != nil {
return err
}
}
_, err = tmpBlock.WriteString("</pre>")
if err != nil {
return err
}
return tmpBlock.Flush()
}
func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock *bufio.Writer) error {
rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, input)
if err != nil { if err != nil {
return err return err
} }

View file

@ -4,6 +4,8 @@
package markup package markup
import ( import (
"bufio"
"bytes"
"strings" "strings"
"testing" "testing"
@ -29,4 +31,12 @@ func TestRenderCSV(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, v, buf.String()) assert.EqualValues(t, v, buf.String())
} }
t.Run("fallbackRender", func(t *testing.T) {
var buf bytes.Buffer
err := render.fallbackRender(strings.NewReader("1,<a>\n2,<b>"), bufio.NewWriter(&buf))
assert.NoError(t, err)
want := "<pre>1,&lt;a&gt;\n2,&lt;b&gt;</pre>"
assert.Equal(t, want, buf.String())
})
} }

View file

@ -609,7 +609,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
if ok && strings.Contains(mention, "/") { if ok && strings.Contains(mention, "/") {
mentionOrgAndTeam := strings.Split(mention, "/") mentionOrgAndTeam := strings.Split(mention, "/")
if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention")) replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
start = 0 start = 0
continue continue
@ -620,7 +620,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
mentionedUsername := mention[1:] mentionedUsername := mention[1:]
if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) { if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) {
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mentionedUsername), mention, "mention")) replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "mention"))
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
} else { } else {
node = node.NextSibling node = node.NextSibling
@ -898,9 +898,9 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
path = "pulls" path = "pulls"
} }
if ref.Owner == "" { if ref.Owner == "" {
link = createLink(util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue") link = createLink(util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue")
} else { } else {
link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue") link = createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue")
} }
} }
@ -939,7 +939,7 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
} }
reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
link := createLink(util.URLJoin(setting.AppSubURL, ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit") link := createLink(util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling
@ -1166,7 +1166,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
continue continue
} }
link := util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], "commit", hash) link := util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], "commit", hash)
replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit")) replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
start = 0 start = 0
node = node.NextSibling.NextSibling node = node.NextSibling.NextSibling

View file

@ -287,6 +287,7 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) {
} }
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
ctx.Links.AbsolutePrefix = true
if ctx.Links.Base == "" { if ctx.Links.Base == "" {
ctx.Links.Base = TestRepoURL ctx.Links.Base = TestRepoURL
} }

View file

@ -43,7 +43,8 @@ func TestRender_Commits(t *testing.T) {
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: ".md", RelativePath: ".md",
Links: markup.Links{ Links: markup.Links{
Base: markup.TestRepoURL, AbsolutePrefix: true,
Base: markup.TestRepoURL,
}, },
Metas: localMetas, Metas: localMetas,
}, input) }, input)
@ -96,7 +97,8 @@ func TestRender_CrossReferences(t *testing.T) {
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
RelativePath: "a.md", RelativePath: "a.md",
Links: markup.Links{ Links: markup.Links{
Base: setting.AppSubURL, AbsolutePrefix: true,
Base: setting.AppSubURL,
}, },
Metas: localMetas, Metas: localMetas,
}, input) }, input)
@ -579,7 +581,8 @@ func TestPostProcess_RenderDocument(t *testing.T) {
err := markup.PostProcess(&markup.RenderContext{ err := markup.PostProcess(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
Links: markup.Links{ Links: markup.Links{
Base: "https://example.com", AbsolutePrefix: true,
Base: "https://example.com",
}, },
Metas: localMetas, Metas: localMetas,
}, strings.NewReader(input), &res) }, strings.NewReader(input), &res)

View file

@ -105,7 +105,8 @@ func SpecializedMarkdown() goldmark.Markdown {
} }
// include language-x class as part of commonmark spec // include language-x class as part of commonmark spec
_, err = w.WriteString(`<code class="chroma language-` + string(language) + `">`) // the "display" class is used by "js/markup/math.js" to render the code element as a block
_, err = w.WriteString(`<code class="chroma language-` + string(language) + ` display">`)
if err != nil { if err != nil {
return return
} }

View file

@ -131,11 +131,11 @@ func testAnswers(baseURLContent, baseURLImages string) []string {
<li><a href="` + baseURLContent + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li> <li><a href="` + baseURLContent + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
<li><a href="` + baseURLContent + `/Tips" rel="nofollow">Tips</a></li> <li><a href="` + baseURLContent + `/Tips" rel="nofollow">Tips</a></li>
</ul> </ul>
<p>See commit <a href="http://localhost:3000/gogits/gogs/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p> <p>See commit <a href="/gogits/gogs/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p>
<p>Ideas and codes</p> <p>Ideas and codes</p>
<ul> <ul>
<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li> <li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li>
<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/gogits/gogs/issues/786" class="ref-issue" rel="nofollow">#786</a></li> <li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/gogits/gogs/issues/786" class="ref-issue" rel="nofollow">#786</a></li>
<li>Node graph editors <a href="https://github.com/ocornut/imgui/issues/306" rel="nofollow">https://github.com/ocornut/imgui/issues/306</a></li> <li>Node graph editors <a href="https://github.com/ocornut/imgui/issues/306" rel="nofollow">https://github.com/ocornut/imgui/issues/306</a></li>
<li><a href="` + baseURLContent + `/memory_editor_example" rel="nofollow">Memory Editor</a></li> <li><a href="` + baseURLContent + `/memory_editor_example" rel="nofollow">Memory Editor</a></li>
<li><a href="` + baseURLContent + `/plot_var_example" rel="nofollow">Plot var helper</a></li> <li><a href="` + baseURLContent + `/plot_var_example" rel="nofollow">Plot var helper</a></li>

View file

@ -82,9 +82,17 @@ type RenderContext struct {
} }
type Links struct { type Links struct {
Base string AbsolutePrefix bool
BranchPath string Base string
TreePath string BranchPath string
TreePath string
}
func (l *Links) Prefix() string {
if l.AbsolutePrefix {
return setting.AppURL
}
return setting.AppSubURL
} }
func (l *Links) HasBranchInfo() bool { func (l *Links) HasBranchInfo() bool {

View file

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/user" "code.gitea.io/gitea/modules/user"
"code.gitea.io/gitea/modules/util"
) )
var ForgejoVersion = "1.0.0" var ForgejoVersion = "1.0.0"
@ -161,9 +162,11 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
func loadRunModeFrom(rootCfg ConfigProvider) { func loadRunModeFrom(rootCfg ConfigProvider) {
rootSec := rootCfg.Section("") rootSec := rootCfg.Section("")
RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername()) RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername())
// The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches. // The following is a purposefully undocumented option. Please do not run Gitea as root. It will only cause future headaches.
// Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly. // Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly.
unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")
unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || util.OptionalBoolParse(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value()
RunMode = os.Getenv("GITEA_RUN_MODE") RunMode = os.Getenv("GITEA_RUN_MODE")
if RunMode == "" { if RunMode == "" {
RunMode = rootSec.Key("RUN_MODE").MustString("prod") RunMode = rootSec.Key("RUN_MODE").MustString("prod")

View file

@ -38,7 +38,7 @@ func NewFuncMap() template.FuncMap {
"SafeHTML": SafeHTML, "SafeHTML": SafeHTML,
"HTMLFormat": HTMLFormat, "HTMLFormat": HTMLFormat,
"HTMLEscape": HTMLEscape, "HTMLEscape": HTMLEscape,
"QueryEscape": url.QueryEscape, "QueryEscape": QueryEscape,
"JSEscape": JSEscapeSafe, "JSEscape": JSEscapeSafe,
"SanitizeHTML": SanitizeHTML, "SanitizeHTML": SanitizeHTML,
"URLJoin": util.URLJoin, "URLJoin": util.URLJoin,
@ -229,6 +229,10 @@ func JSEscapeSafe(s string) template.HTML {
return template.HTML(template.JSEscapeString(s)) return template.HTML(template.JSEscapeString(s))
} }
func QueryEscape(s string) template.URL {
return template.URL(url.QueryEscape(s))
}
// DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls // DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
func DotEscape(raw string) string { func DotEscape(raw string) string {
return strings.ReplaceAll(raw, ".", "\u200d.\u200d") return strings.ReplaceAll(raw, ".", "\u200d.\u200d")

View file

@ -61,7 +61,7 @@ func TestMain(m *testing.M) {
func TestApostrophesInMentions(t *testing.T) { func TestApostrophesInMentions(t *testing.T) {
rendered := RenderMarkdownToHtml(context.Background(), "@mention-user's comment") rendered := RenderMarkdownToHtml(context.Background(), "@mention-user's comment")
assert.EqualValues(t, "<p><a href=\"http://localhost:3000/mention-user\" rel=\"nofollow\">@mention-user</a>&#39;s comment</p>\n", rendered) assert.EqualValues(t, template.HTML("<p><a href=\"/mention-user\" rel=\"nofollow\">@mention-user</a>&#39;s comment</p>\n"), rendered)
} }
func TestRenderCommitBody(t *testing.T) { func TestRenderCommitBody(t *testing.T) {
@ -122,21 +122,21 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a582
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span> <span class="emoji" aria-label="thumbs up">👍</span>
<a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a> <a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a>
<a href="http://localhost:3000/mention-user" class="mention">@mention-user</a> test <a href="/mention-user" class="mention">@mention-user</a> test
<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a> <a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
space` space`
assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas)) assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas))
} }
func TestRenderCommitMessage(t *testing.T) { func TestRenderCommitMessage(t *testing.T) {
expected := `space <a href="http://localhost:3000/mention-user" class="mention">@mention-user</a> ` expected := `space <a href="/mention-user" class="mention">@mention-user</a> `
assert.EqualValues(t, expected, RenderCommitMessage(context.Background(), testInput, testMetas)) assert.EqualValues(t, expected, RenderCommitMessage(context.Background(), testInput, testMetas))
} }
func TestRenderCommitMessageLinkSubject(t *testing.T) { func TestRenderCommitMessageLinkSubject(t *testing.T) {
expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="http://localhost:3000/mention-user" class="mention">@mention-user</a>` expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="/mention-user" class="mention">@mention-user</a>`
assert.EqualValues(t, expected, RenderCommitMessageLinkSubject(context.Background(), testInput, "https://example.com/link", testMetas)) assert.EqualValues(t, expected, RenderCommitMessageLinkSubject(context.Background(), testInput, "https://example.com/link", testMetas))
} }
@ -160,14 +160,14 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span> <span class="emoji" aria-label="thumbs up">👍</span>
mail@domain.com mail@domain.com
@mention-user test @mention-user test
<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a> <a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
space space
` `
assert.EqualValues(t, expected, RenderIssueTitle(context.Background(), testInput, testMetas)) assert.EqualValues(t, expected, RenderIssueTitle(context.Background(), testInput, testMetas))
} }
func TestRenderMarkdownToHtml(t *testing.T) { func TestRenderMarkdownToHtml(t *testing.T) {
expected := `<p>space <a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a><br/> expected := `<p>space <a href="/mention-user" rel="nofollow">@mention-user</a><br/>
/just/a/path.bin /just/a/path.bin
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a> <a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
<a href="/file.bin" rel="nofollow">local link</a> <a href="/file.bin" rel="nofollow">local link</a>
@ -184,7 +184,7 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a582
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span> <span class="emoji" aria-label="thumbs up">👍</span>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a> <a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
<a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a> test <a href="/mention-user" rel="nofollow">@mention-user</a> test
#123 #123
space</p> space</p>
` `

View file

@ -13,6 +13,8 @@ import (
// DateTime renders an absolute time HTML element by datetime. // DateTime renders an absolute time HTML element by datetime.
func DateTime(format string, datetime any, extraAttrs ...string) template.HTML { func DateTime(format string, datetime any, extraAttrs ...string) template.HTML {
// TODO: remove the extraAttrs argument, it's not used in any call to DateTime
if p, ok := datetime.(*time.Time); ok { if p, ok := datetime.(*time.Time); ok {
datetime = *p datetime = *p
} }
@ -51,18 +53,16 @@ func DateTime(format string, datetime any, extraAttrs ...string) template.HTML {
attrs := make([]string, 0, 10+len(extraAttrs)) attrs := make([]string, 0, 10+len(extraAttrs))
attrs = append(attrs, extraAttrs...) attrs = append(attrs, extraAttrs...)
attrs = append(attrs, `data-tooltip-content`, `data-tooltip-interactive="true"`) attrs = append(attrs, `weekday=""`, `year="numeric"`)
attrs = append(attrs, `format="datetime"`, `weekday=""`, `year="numeric"`)
switch format { switch format {
case "short": case "short", "long": // date only
attrs = append(attrs, `month="short"`, `day="numeric"`) attrs = append(attrs, `month="`+format+`"`, `day="numeric"`)
case "long": return template.HTML(fmt.Sprintf(`<absolute-date %s date="%s">%s</absolute-date>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
attrs = append(attrs, `month="long"`, `day="numeric"`) case "full": // full date including time
case "full": attrs = append(attrs, `format="datetime"`, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`, `data-tooltip-content`, `data-tooltip-interactive="true"`)
attrs = append(attrs, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`) return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
default: default:
panic(fmt.Sprintf("Unsupported format %s", format)) panic(fmt.Sprintf("Unsupported format %s", format))
} }
return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
} }

View file

@ -18,6 +18,7 @@ func TestDateTime(t *testing.T) {
defer test.MockVariableValue(&setting.DefaultUILocation, testTz)() defer test.MockVariableValue(&setting.DefaultUILocation, testTz)()
refTimeStr := "2018-01-01T00:00:00Z" refTimeStr := "2018-01-01T00:00:00Z"
refDateStr := "2018-01-01"
refTime, _ := time.Parse(time.RFC3339, refTimeStr) refTime, _ := time.Parse(time.RFC3339, refTimeStr)
refTimeStamp := TimeStamp(refTime.Unix()) refTimeStamp := TimeStamp(refTime.Unix())
@ -27,17 +28,20 @@ func TestDateTime(t *testing.T) {
assert.EqualValues(t, "-", DateTime("short", TimeStamp(0))) assert.EqualValues(t, "-", DateTime("short", TimeStamp(0)))
actual := DateTime("short", "invalid") actual := DateTime("short", "invalid")
assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="invalid">invalid</relative-time>`, actual) assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="invalid">invalid</absolute-date>`, actual)
actual = DateTime("short", refTimeStr) actual = DateTime("short", refTimeStr)
assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</relative-time>`, actual) assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</absolute-date>`, actual)
actual = DateTime("short", refTime) actual = DateTime("short", refTime)
assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2018-01-01T00:00:00Z">2018-01-01</relative-time>`, actual) assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01</absolute-date>`, actual)
actual = DateTime("short", refDateStr)
assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01">2018-01-01</absolute-date>`, actual)
actual = DateTime("short", refTimeStamp) actual = DateTime("short", refTimeStamp)
assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" datetime="2017-12-31T19:00:00-05:00">2017-12-31</relative-time>`, actual) assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2017-12-31T19:00:00-05:00">2017-12-31</absolute-date>`, actual)
actual = DateTime("full", refTimeStamp) actual = DateTime("full", refTimeStamp)
assert.EqualValues(t, `<relative-time data-tooltip-content data-tooltip-interactive="true" format="datetime" weekday="" year="numeric" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual) assert.EqualValues(t, `<relative-time weekday="" year="numeric" format="datetime" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" data-tooltip-content data-tooltip-interactive="true" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual)
} }

View file

@ -126,7 +126,7 @@ func timeSinceUnix(then, now time.Time, _ translation.Locale) template.HTML {
} }
// declare data-tooltip-content attribute to switch from "title" tooltip to "tippy" tooltip // declare data-tooltip-content attribute to switch from "title" tooltip to "tippy" tooltip
htm := fmt.Sprintf(`<relative-time class="time-since" prefix="" %s datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>`, htm := fmt.Sprintf(`<relative-time prefix="" %s datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>`,
attrs, then.Format(time.RFC3339), friendlyText) attrs, then.Format(time.RFC3339), friendlyText)
return template.HTML(htm) return template.HTML(htm)
} }
@ -134,7 +134,7 @@ func timeSinceUnix(then, now time.Time, _ translation.Locale) template.HTML {
// TimeSince renders relative time HTML given a time.Time // TimeSince renders relative time HTML given a time.Time
func TimeSince(then time.Time, lang translation.Locale) template.HTML { func TimeSince(then time.Time, lang translation.Locale) template.HTML {
if setting.UI.PreferredTimestampTense == "absolute" { if setting.UI.PreferredTimestampTense == "absolute" {
return DateTime("full", then, `class="time-since"`) return DateTime("full", then)
} }
return timeSinceUnix(then, time.Now(), lang) return timeSinceUnix(then, time.Now(), lang)
} }

View file

@ -53,3 +53,12 @@ func Sorted[S ~[]E, E cmp.Ordered](values S) S {
slices.Sort(values) slices.Sort(values)
return values return values
} }
// TODO: Replace with "maps.Values" once available
func ValuesOfMap[K comparable, V any](m map[K]V) []V {
values := make([]V, 0, len(m))
for _, v := range m {
values = append(values, v)
}
return values
}

1416
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,9 +4,9 @@
"node": ">= 18.0.0" "node": ">= 18.0.0"
}, },
"dependencies": { "dependencies": {
"@citation-js/core": "0.7.6", "@citation-js/core": "0.7.9",
"@citation-js/plugin-bibtex": "0.7.8", "@citation-js/plugin-bibtex": "0.7.9",
"@citation-js/plugin-csl": "0.7.6", "@citation-js/plugin-csl": "0.7.9",
"@citation-js/plugin-software-formats": "0.6.1", "@citation-js/plugin-software-formats": "0.6.1",
"@claviska/jquery-minicolors": "2.3.6", "@claviska/jquery-minicolors": "2.3.6",
"@github/markdown-toolbar-element": "2.2.3", "@github/markdown-toolbar-element": "2.2.3",
@ -14,7 +14,6 @@
"@github/text-expander-element": "2.6.1", "@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.8.0", "@primer/octicons": "19.8.0",
"@webcomponents/custom-elements": "1.6.0",
"add-asset-webpack-plugin": "2.0.1", "add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2", "ansi_up": "6.0.2",
"asciinema-player": "3.7.0", "asciinema-player": "3.7.0",
@ -26,26 +25,28 @@
"dayjs": "1.11.10", "dayjs": "1.11.10",
"dropzone": "6.0.0-beta.2", "dropzone": "6.0.0-beta.2",
"easymde": "2.18.0", "easymde": "2.18.0",
"esbuild-loader": "4.0.3", "esbuild-loader": "4.1.0",
"escape-goat": "4.0.0", "escape-goat": "4.0.0",
"fast-glob": "3.3.2", "fast-glob": "3.3.2",
"htmx.org": "1.9.10", "htmx.org": "1.9.11",
"idiomorph": "0.3.0", "idiomorph": "0.3.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"katex": "0.16.9", "katex": "0.16.9",
"license-checker-webpack-plugin": "0.2.1", "license-checker-webpack-plugin": "0.2.1",
"mermaid": "10.8.0", "mermaid": "10.9.0",
"mini-css-extract-plugin": "2.8.1", "mini-css-extract-plugin": "2.8.1",
"minimatch": "9.0.3", "minimatch": "9.0.3",
"monaco-editor": "0.46.0", "monaco-editor": "0.47.0",
"monaco-editor-webpack-plugin": "7.1.0", "monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.3.0", "pdfobject": "2.3.0",
"postcss": "8.4.35", "postcss": "8.4.35",
"postcss-loader": "8.1.1", "postcss-loader": "8.1.1",
"postcss-nesting": "12.1.0",
"pretty-ms": "9.0.0", "pretty-ms": "9.0.0",
"sortablejs": "1.15.2", "sortablejs": "1.15.2",
"swagger-ui-dist": "5.11.8", "swagger-ui-dist": "5.12.0",
"tailwindcss": "3.4.1", "tailwindcss": "3.4.1",
"temporal-polyfill": "0.2.3",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tippy.js": "6.3.7", "tippy.js": "6.3.7",
@ -65,7 +66,7 @@
"@eslint-community/eslint-plugin-eslint-comments": "4.1.0", "@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
"@playwright/test": "1.42.1", "@playwright/test": "1.42.1",
"@stoplight/spectral-cli": "6.11.0", "@stoplight/spectral-cli": "6.11.0",
"@stylistic/eslint-plugin-js": "1.6.3", "@stylistic/eslint-plugin-js": "1.7.0",
"@stylistic/stylelint-plugin": "2.1.0", "@stylistic/stylelint-plugin": "2.1.0",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"eslint": "8.57.0", "eslint": "8.57.0",
@ -75,12 +76,12 @@
"eslint-plugin-jquery": "1.5.1", "eslint-plugin-jquery": "1.5.1",
"eslint-plugin-no-jquery": "2.7.0", "eslint-plugin-no-jquery": "2.7.0",
"eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-no-use-extend-native": "0.5.0",
"eslint-plugin-regexp": "2.2.0", "eslint-plugin-regexp": "2.3.0",
"eslint-plugin-sonarjs": "0.24.0", "eslint-plugin-sonarjs": "0.24.0",
"eslint-plugin-unicorn": "51.0.1", "eslint-plugin-unicorn": "51.0.1",
"eslint-plugin-vitest": "0.3.22", "eslint-plugin-vitest": "0.3.26",
"eslint-plugin-vitest-globals": "1.4.0", "eslint-plugin-vitest-globals": "1.4.0",
"eslint-plugin-vue": "9.22.0", "eslint-plugin-vue": "9.23.0",
"eslint-plugin-vue-scoped-css": "2.7.2", "eslint-plugin-vue-scoped-css": "2.7.2",
"eslint-plugin-wc": "2.0.4", "eslint-plugin-wc": "2.0.4",
"jsdom": "24.0.0", "jsdom": "24.0.0",
@ -90,7 +91,7 @@
"stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.4", "stylelint-declaration-strict-value": "1.10.4",
"svgo": "3.2.0", "svgo": "3.2.0",
"updates": "15.1.2", "updates": "15.3.1",
"vite-string-plugin": "1.1.5", "vite-string-plugin": "1.1.5",
"vitest": "1.3.1" "vitest": "1.3.1"
}, },

View file

@ -147,6 +147,11 @@ func CreateUser(ctx *context.APIContext) {
} }
return return
} }
if !user_model.IsEmailDomainAllowed(u.Email) {
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email))
}
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name) log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
// Send email notification. // Send email notification.
@ -220,6 +225,10 @@ func EditUser(ctx *context.APIContext) {
} }
return return
} }
if !user_model.IsEmailDomainAllowed(*form.Email) {
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email))
}
} }
opts := &user_service.UpdateOptions{ opts := &user_service.UpdateOptions{

View file

@ -269,28 +269,28 @@ func SearchIssues(ctx *context.APIContext) {
} }
if since != 0 { if since != 0 {
searchOpt.UpdatedAfterUnix = &since searchOpt.UpdatedAfterUnix = optional.Some(since)
} }
if before != 0 { if before != 0 {
searchOpt.UpdatedBeforeUnix = &before searchOpt.UpdatedBeforeUnix = optional.Some(before)
} }
if ctx.IsSigned { if ctx.IsSigned {
ctxUserID := ctx.Doer.ID ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") { if ctx.FormBool("created") {
searchOpt.PosterID = &ctxUserID searchOpt.PosterID = optional.Some(ctxUserID)
} }
if ctx.FormBool("assigned") { if ctx.FormBool("assigned") {
searchOpt.AssigneeID = &ctxUserID searchOpt.AssigneeID = optional.Some(ctxUserID)
} }
if ctx.FormBool("mentioned") { if ctx.FormBool("mentioned") {
searchOpt.MentionID = &ctxUserID searchOpt.MentionID = optional.Some(ctxUserID)
} }
if ctx.FormBool("review_requested") { if ctx.FormBool("review_requested") {
searchOpt.ReviewRequestedID = &ctxUserID searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
} }
if ctx.FormBool("reviewed") { if ctx.FormBool("reviewed") {
searchOpt.ReviewedID = &ctxUserID searchOpt.ReviewedID = optional.Some(ctxUserID)
} }
} }
@ -368,7 +368,7 @@ func ListIssues(ctx *context.APIContext) {
// required: false // required: false
// - name: created_by // - name: created_by
// in: query // in: query
// description: Only show items which were created by the the given user // description: Only show items which were created by the given user
// type: string // type: string
// - name: assigned_by // - name: assigned_by
// in: query // in: query
@ -502,10 +502,10 @@ func ListIssues(ctx *context.APIContext) {
SortBy: issue_indexer.SortByCreatedDesc, SortBy: issue_indexer.SortByCreatedDesc,
} }
if since != 0 { if since != 0 {
searchOpt.UpdatedAfterUnix = &since searchOpt.UpdatedAfterUnix = optional.Some(since)
} }
if before != 0 { if before != 0 {
searchOpt.UpdatedBeforeUnix = &before searchOpt.UpdatedBeforeUnix = optional.Some(before)
} }
if len(labelIDs) == 1 && labelIDs[0] == 0 { if len(labelIDs) == 1 && labelIDs[0] == 0 {
searchOpt.NoLabelOnly = true searchOpt.NoLabelOnly = true
@ -526,13 +526,13 @@ func ListIssues(ctx *context.APIContext) {
} }
if createdByID > 0 { if createdByID > 0 {
searchOpt.PosterID = &createdByID searchOpt.PosterID = optional.Some(createdByID)
} }
if assignedByID > 0 { if assignedByID > 0 {
searchOpt.AssigneeID = &assignedByID searchOpt.AssigneeID = optional.Some(assignedByID)
} }
if mentionedByID > 0 { if mentionedByID > 0 {
searchOpt.MentionID = &mentionedByID searchOpt.MentionID = optional.Some(mentionedByID)
} }
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)

View file

@ -1065,6 +1065,8 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
return nil, nil, nil, nil, "", "" return nil, nil, nil, nil, "", ""
} }
headBranch = headInfos[1] headBranch = headInfos[1]
// The head repository can also point to the same repo
isSameRepo = ctx.Repo.Owner.ID == headUser.ID
} else { } else {
ctx.NotFound() ctx.NotFound()

View file

@ -692,7 +692,7 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues
return nil, nil, true return nil, nil, true
} }
// validate the the review is for the given PR // validate the review is for the given PR
if review.IssueID != pr.IssueID { if review.IssueID != pr.IssueID {
ctx.NotFound("ReviewNotInPR") ctx.NotFound("ReviewNotInPR")
return nil, nil, true return nil, nil, true

View file

@ -34,7 +34,8 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
if err := markdown.RenderRaw(&markup.RenderContext{ if err := markdown.RenderRaw(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
Links: markup.Links{ Links: markup.Links{
Base: urlPrefix, AbsolutePrefix: true,
Base: urlPrefix,
}, },
}, strings.NewReader(text), ctx.Resp); err != nil { }, strings.NewReader(text), ctx.Resp); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error()) ctx.Error(http.StatusInternalServerError, err.Error())
@ -79,7 +80,8 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
if err := markup.Render(&markup.RenderContext{ if err := markup.Render(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
Links: markup.Links{ Links: markup.Links{
Base: urlPrefix, AbsolutePrefix: true,
Base: urlPrefix,
}, },
Metas: meta, Metas: meta,
IsWiki: wiki, IsWiki: wiki,

View file

@ -202,6 +202,11 @@ func NewUserPost(ctx *context.Context) {
} }
return return
} }
if !user_model.IsEmailDomainAllowed(u.Email) {
ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email))
}
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name) log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
// Send email notification. // Send email notification.
@ -425,6 +430,9 @@ func EditUserPost(ctx *context.Context) {
} }
return return
} }
if !user_model.IsEmailDomainAllowed(form.Email) {
ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", form.Email))
}
} }
opts := &user_service.UpdateOptions{ opts := &user_service.UpdateOptions{

View file

@ -148,12 +148,7 @@ func RestoreBranchPost(ctx *context.Context) {
return return
} }
objectFormat, err := git.GetObjectFormatOfRepo(ctx, ctx.Repo.Repository.RepoPath()) objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName)
if err != nil {
log.Error("RestoreBranch: CreateBranch: %w", err)
ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name))
return
}
// Don't return error below this // Don't return error below this
if err := repo_service.PushUpdate( if err := repo_service.PushUpdate(

View file

@ -2636,9 +2636,9 @@ func SearchIssues(ctx *context.Context) {
} }
} }
var projectID *int64 projectID := optional.None[int64]()
if v := ctx.FormInt64("project"); v > 0 { if v := ctx.FormInt64("project"); v > 0 {
projectID = &v projectID = optional.Some(v)
} }
// this api is also used in UI, // this api is also used in UI,
@ -2667,28 +2667,28 @@ func SearchIssues(ctx *context.Context) {
} }
if since != 0 { if since != 0 {
searchOpt.UpdatedAfterUnix = &since searchOpt.UpdatedAfterUnix = optional.Some(since)
} }
if before != 0 { if before != 0 {
searchOpt.UpdatedBeforeUnix = &before searchOpt.UpdatedBeforeUnix = optional.Some(before)
} }
if ctx.IsSigned { if ctx.IsSigned {
ctxUserID := ctx.Doer.ID ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") { if ctx.FormBool("created") {
searchOpt.PosterID = &ctxUserID searchOpt.PosterID = optional.Some(ctxUserID)
} }
if ctx.FormBool("assigned") { if ctx.FormBool("assigned") {
searchOpt.AssigneeID = &ctxUserID searchOpt.AssigneeID = optional.Some(ctxUserID)
} }
if ctx.FormBool("mentioned") { if ctx.FormBool("mentioned") {
searchOpt.MentionID = &ctxUserID searchOpt.MentionID = optional.Some(ctxUserID)
} }
if ctx.FormBool("review_requested") { if ctx.FormBool("review_requested") {
searchOpt.ReviewRequestedID = &ctxUserID searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
} }
if ctx.FormBool("reviewed") { if ctx.FormBool("reviewed") {
searchOpt.ReviewedID = &ctxUserID searchOpt.ReviewedID = optional.Some(ctxUserID)
} }
} }
@ -2795,9 +2795,9 @@ func ListIssues(ctx *context.Context) {
} }
} }
var projectID *int64 projectID := optional.None[int64]()
if v := ctx.FormInt64("project"); v > 0 { if v := ctx.FormInt64("project"); v > 0 {
projectID = &v projectID = optional.Some(v)
} }
isPull := optional.None[bool]() isPull := optional.None[bool]()
@ -2835,10 +2835,10 @@ func ListIssues(ctx *context.Context) {
SortBy: issue_indexer.SortByCreatedDesc, SortBy: issue_indexer.SortByCreatedDesc,
} }
if since != 0 { if since != 0 {
searchOpt.UpdatedAfterUnix = &since searchOpt.UpdatedAfterUnix = optional.Some(since)
} }
if before != 0 { if before != 0 {
searchOpt.UpdatedBeforeUnix = &before searchOpt.UpdatedBeforeUnix = optional.Some(before)
} }
if len(labelIDs) == 1 && labelIDs[0] == 0 { if len(labelIDs) == 1 && labelIDs[0] == 0 {
searchOpt.NoLabelOnly = true searchOpt.NoLabelOnly = true
@ -2859,13 +2859,13 @@ func ListIssues(ctx *context.Context) {
} }
if createdByID > 0 { if createdByID > 0 {
searchOpt.PosterID = &createdByID searchOpt.PosterID = optional.Some(createdByID)
} }
if assignedByID > 0 { if assignedByID > 0 {
searchOpt.AssigneeID = &assignedByID searchOpt.AssigneeID = optional.Some(assignedByID)
} }
if mentionedByID > 0 { if mentionedByID > 0 {
searchOpt.MentionID = &mentionedByID searchOpt.MentionID = optional.Some(mentionedByID)
} }
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)

View file

@ -543,9 +543,13 @@ func InitiateDownload(ctx *context.Context) {
// SearchRepo repositories via options // SearchRepo repositories via options
func SearchRepo(ctx *context.Context) { func SearchRepo(ctx *context.Context) {
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}
opts := &repo_model.SearchRepoOptions{ opts := &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{ ListOptions: db.ListOptions{
Page: ctx.FormInt("page"), Page: page,
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
}, },
Actor: ctx.Doer, Actor: ctx.Doer,

View file

@ -616,6 +616,7 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) {
return nil, nil return nil, nil
} }
ctx.Data["BaseLink"] = orCtx.Link ctx.Data["BaseLink"] = orCtx.Link
ctx.Data["BaseLinkNew"] = orCtx.LinkNew
var w *webhook.Webhook var w *webhook.Webhook
if orCtx.RepoID > 0 { if orCtx.RepoID > 0 {
@ -684,12 +685,7 @@ func TestWebhook(ctx *context.Context) {
commit := ctx.Repo.Commit commit := ctx.Repo.Commit
if commit == nil { if commit == nil {
ghost := user_model.NewGhostUser() ghost := user_model.NewGhostUser()
objectFormat, err := git.GetObjectFormatOfRepo(ctx, ctx.Repo.Repository.RepoPath()) objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName)
if err != nil {
ctx.Flash.Error("GetObjectFormatOfRepo: " + err.Error())
ctx.Status(http.StatusInternalServerError)
return
}
commit = &git.Commit{ commit = &git.Commit{
ID: objectFormat.EmptyObjectID(), ID: objectFormat.EmptyObjectID(),
Author: ghost.NewGitSig(), Author: ghost.NewGitSig(),

View file

@ -791,15 +791,15 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
case issues_model.FilterModeYourRepositories: case issues_model.FilterModeYourRepositories:
openClosedOpts.AllPublic = false openClosedOpts.AllPublic = false
case issues_model.FilterModeAssign: case issues_model.FilterModeAssign:
openClosedOpts.AssigneeID = &doerID openClosedOpts.AssigneeID = optional.Some(doerID)
case issues_model.FilterModeCreate: case issues_model.FilterModeCreate:
openClosedOpts.PosterID = &doerID openClosedOpts.PosterID = optional.Some(doerID)
case issues_model.FilterModeMention: case issues_model.FilterModeMention:
openClosedOpts.MentionID = &doerID openClosedOpts.MentionID = optional.Some(doerID)
case issues_model.FilterModeReviewRequested: case issues_model.FilterModeReviewRequested:
openClosedOpts.ReviewRequestedID = &doerID openClosedOpts.ReviewRequestedID = optional.Some(doerID)
case issues_model.FilterModeReviewed: case issues_model.FilterModeReviewed:
openClosedOpts.ReviewedID = &doerID openClosedOpts.ReviewedID = optional.Some(doerID)
} }
openClosedOpts.IsClosed = optional.Some(false) openClosedOpts.IsClosed = optional.Some(false)
ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts) ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
@ -817,23 +817,23 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
if err != nil { if err != nil {
return nil, err return nil, err
} }
ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = &doerID })) ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = optional.Some(doerID) }))
if err != nil { if err != nil {
return nil, err return nil, err
} }
ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = &doerID })) ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = optional.Some(doerID) }))
if err != nil { if err != nil {
return nil, err return nil, err
} }
ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = &doerID })) ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = optional.Some(doerID) }))
if err != nil { if err != nil {
return nil, err return nil, err
} }
ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = &doerID })) ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = optional.Some(doerID) }))
if err != nil { if err != nil {
return nil, err return nil, err
} }
ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = &doerID })) ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = optional.Some(doerID) }))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -1413,7 +1413,7 @@ func registerRoutes(m *web.Route) {
}) })
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
m.Post("/approve", reqRepoActionsWriter, actions.Approve) m.Post("/approve", reqRepoActionsWriter, actions.Approve)
m.Post("/artifacts", actions.ArtifactsView) m.Get("/artifacts", actions.ArtifactsView)
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView) m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView) m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)

View file

@ -10,9 +10,9 @@ import (
"strings" "strings"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
@ -109,11 +109,7 @@ func (f *RegisterForm) Validate(req *http.Request, errs binding.Errors) binding.
// domains in the whitelist or if it doesn't match any of // domains in the whitelist or if it doesn't match any of
// domains in the blocklist, if any such list is not empty. // domains in the blocklist, if any such list is not empty.
func (f *RegisterForm) IsEmailDomainAllowed() bool { func (f *RegisterForm) IsEmailDomainAllowed() bool {
if len(setting.Service.EmailDomainAllowList) == 0 { return user_model.IsEmailDomainAllowed(f.Email)
return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, f.Email)
}
return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, f.Email)
} }
// MustChangePasswordForm form for updating your password after account creation // MustChangePasswordForm form for updating your password after account creation

View file

@ -222,7 +222,8 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
body, err := markdown.RenderString(&markup.RenderContext{ body, err := markdown.RenderString(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
Links: markup.Links{ Links: markup.Links{
Base: ctx.Issue.Repo.HTMLURL(), AbsolutePrefix: true,
Base: ctx.Issue.Repo.HTMLURL(),
}, },
Metas: ctx.Issue.Repo.ComposeMetas(ctx), Metas: ctx.Issue.Repo.ComposeMetas(ctx),
}, ctx.Content) }, ctx.Content)

View file

@ -8,6 +8,8 @@ import (
"context" "context"
"fmt" "fmt"
"html/template" "html/template"
"io"
"mime/quotedprintable"
"regexp" "regexp"
"strings" "strings"
"testing" "testing"
@ -19,6 +21,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -67,6 +70,12 @@ func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Re
func TestComposeIssueCommentMessage(t *testing.T) { func TestComposeIssueCommentMessage(t *testing.T) {
doer, _, issue, comment := prepareMailerTest(t) doer, _, issue, comment := prepareMailerTest(t)
markup.Init(&markup.ProcessorHelper{
IsUsernameMentionable: func(ctx context.Context, username string) bool {
return username == doer.Name
},
})
setting.IncomingEmail.Enabled = true setting.IncomingEmail.Enabled = true
defer func() { setting.IncomingEmail.Enabled = false }() defer func() { setting.IncomingEmail.Enabled = false }()
@ -77,7 +86,8 @@ func TestComposeIssueCommentMessage(t *testing.T) {
msgs, err := composeIssueCommentMessages(&mailCommentContext{ msgs, err := composeIssueCommentMessages(&mailCommentContext{
Context: context.TODO(), // TODO: use a correct context Context: context.TODO(), // TODO: use a correct context
Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue, Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
Content: "test body", Comment: comment, Content: fmt.Sprintf("test @%s %s#%d body", doer.Name, issue.Repo.FullName(), issue.Index),
Comment: comment,
}, "en-US", recipients, false, "issue comment") }, "en-US", recipients, false, "issue comment")
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, msgs, 2) assert.Len(t, msgs, 2)
@ -96,6 +106,20 @@ func TestComposeIssueCommentMessage(t *testing.T) {
assert.Equal(t, "<user2/repo1/issues/1/comment/2@localhost>", gomailMsg.GetHeader("Message-ID")[0], "Message-ID header doesn't match") assert.Equal(t, "<user2/repo1/issues/1/comment/2@localhost>", gomailMsg.GetHeader("Message-ID")[0], "Message-ID header doesn't match")
assert.Equal(t, "<mailto:"+replyTo+">", gomailMsg.GetHeader("List-Post")[0]) assert.Equal(t, "<mailto:"+replyTo+">", gomailMsg.GetHeader("List-Post")[0])
assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 2) // url + mailto assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 2) // url + mailto
var buf bytes.Buffer
gomailMsg.WriteTo(&buf)
b, err := io.ReadAll(quotedprintable.NewReader(&buf))
assert.NoError(t, err)
// text/plain
assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, doer.HTMLURL()))
assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, issue.HTMLURL()))
// text/html
assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, doer.HTMLURL()))
assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, issue.HTMLURL()))
} }
func TestComposeIssueMessage(t *testing.T) { func TestComposeIssueMessage(t *testing.T) {

View file

@ -479,10 +479,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
log.Error("SyncMirrors [repo: %-v]: unable to GetRefCommitID [ref_name: %s]: %v", m.Repo, result.refName, err) log.Error("SyncMirrors [repo: %-v]: unable to GetRefCommitID [ref_name: %s]: %v", m.Repo, result.refName, err)
continue continue
} }
objectFormat, err := git.GetObjectFormatOfRepo(ctx, m.Repo.RepoPath()) objectFormat := git.ObjectFormatFromName(m.Repo.ObjectFormatName)
if err != nil {
log.Error("SyncMirrors [repo: %-v]: unable to GetHashTypeOfRepo: %v", m.Repo, err)
}
notify_service.SyncPushCommits(ctx, m.Repo.MustOwner(ctx), m.Repo, &repo_module.PushUpdateOptions{ notify_service.SyncPushCommits(ctx, m.Repo.MustOwner(ctx), m.Repo, &repo_module.PushUpdateOptions{
RefFullName: result.refName, RefFullName: result.refName,
OldCommitID: objectFormat.EmptyObjectID().String(), OldCommitID: objectFormat.EmptyObjectID().String(),

View file

@ -351,7 +351,7 @@ func TestPullRequest(ctx context.Context, doer *user_model.User, repoID, maxPR i
} }
if err == nil { if err == nil {
for _, pr := range prs { for _, pr := range prs {
objectFormat, _ := git.GetObjectFormatOfRepo(ctx, pr.BaseRepo.RepoPath()) objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() { if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() {
changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID) changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
if err != nil { if err != nil {

View file

@ -326,10 +326,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
} }
refName := git.RefNameFromTag(rel.TagName) refName := git.RefNameFromTag(rel.TagName)
objectFormat, err := git.GetObjectFormatOfRepo(ctx, repo.RepoPath()) objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
if err != nil {
return err
}
notify_service.PushCommits( notify_service.PushCommits(
ctx, doer, repo, ctx, doer, repo,
&repository.PushUpdateOptions{ &repository.PushUpdateOptions{

View file

@ -41,6 +41,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
remove_label_str: {{ctx.Locale.Tr "remove_label_str"}}, remove_label_str: {{ctx.Locale.Tr "remove_label_str"}},
modal_confirm: {{ctx.Locale.Tr "modal.confirm"}}, modal_confirm: {{ctx.Locale.Tr "modal.confirm"}},
modal_cancel: {{ctx.Locale.Tr "modal.cancel"}}, modal_cancel: {{ctx.Locale.Tr "modal.cancel"}},
more_items: {{ctx.Locale.Tr "more_items"}},
}, },
}; };
{{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}}

View file

@ -105,9 +105,46 @@
</div> </div>
<div> <div>
<h1>GiteaOriginUrl</h1> <h1>&lt;origin-url&gt;</h1>
<div><gitea-origin-url data-url="test/url"></gitea-origin-url></div> <div><origin-url data-url="test/url"></origin-url></div>
<div><gitea-origin-url data-url="/test/url"></gitea-origin-url></div> <div><origin-url data-url="/test/url"></origin-url></div>
</div>
<div>
<h1>&lt;overflow-menu&gt;</h1>
<overflow-menu class="ui secondary pointing tabular borderless menu">
<div class="overflow-menu-items">
<a class="active item">item</a>
<a class="item">item 1</a>
<a class="item">item 2</a>
<a class="item">item 3</a>
<a class="item">item 4</a>
<a class="item">item 5</a>
<a class="item">item 6</a>
<a class="item">item 7</a>
<a class="item">item 8</a>
<a class="item">item 9</a>
<a class="item">item 10</a>
<a class="item">item 11</a>
<a class="item">item 12</a>
<a class="item">item 13</a>
<a class="item">item 14</a>
<a class="item">item 15</a>
<a class="item">item 16</a>
<a class="item">item 17</a>
<a class="item">item 18</a>
</div>
</overflow-menu>
</div>
<div>
<h1>GiteaAbsoluteDate</h1>
<div><absolute-date date="2024-03-11" year="numeric" day="numeric" month="short"></absolute-date></div>
<div><absolute-date date="2024-03-11" year="numeric" day="numeric" month="long"></absolute-date></div>
<div><absolute-date date="2024-03-11" year="" day="numeric" month="numeric"></absolute-date></div>
<div><absolute-date date="2024-03-11" year="" day="numeric" month="numeric" weekday="long"></absolute-date></div>
<div><absolute-date date="2024-03-11T19:00:00-05:00" year="" day="numeric" month="numeric" weekday="long"></absolute-date></div>
<div class="tw-text-text-light-2">relative-time: <relative-time format="datetime" datetime="2024-03-11" year="" day="numeric" month="numeric"></relative-time></div>
</div> </div>
<div> <div>

View file

@ -1,5 +1,5 @@
<div class="ui secondary pointing tabular top attached borderless menu new-menu navbar"> <overflow-menu class="ui secondary pointing tabular top attached borderless menu navbar">
<div class="new-menu-inner"> <div class="overflow-menu-items tw-justify-center">
<a class="{{if .PageIsExploreRepositories}}active {{end}}item" href="{{AppSubUrl}}/explore/repos"> <a class="{{if .PageIsExploreRepositories}}active {{end}}item" href="{{AppSubUrl}}/explore/repos">
{{svg "octicon-repo"}} {{ctx.Locale.Tr "explore.repos"}} {{svg "octicon-repo"}} {{ctx.Locale.Tr "explore.repos"}}
</a> </a>
@ -17,4 +17,4 @@
</a> </a>
{{end}} {{end}}
</div> </div>
</div> </overflow-menu>

View file

@ -1,50 +1,49 @@
<div class="ui container"> <div class="ui container">
<div class="ui secondary stackable pointing menu"> <overflow-menu class="ui secondary pointing tabular borderless menu">
<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}"> <div class="overflow-menu-items">
{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}} <a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}">
{{if .RepoCount}} {{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
<div class="ui small label">{{.RepoCount}}</div> {{if .RepoCount}}
<div class="ui small label">{{.RepoCount}}</div>
{{end}}
</a>
{{if .CanReadProjects}}
<a class="{{if .PageIsViewProjects}}active {{end}}item" href="{{$.Org.HomeLink}}/-/projects">
{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}}
{{if .ProjectCount}}
<div class="ui small label">{{.ProjectCount}}</div>
{{end}}
</a>
{{end}} {{end}}
</a> {{if and .IsPackageEnabled .CanReadPackages}}
{{if .CanReadProjects}} <a class="{{if .IsPackagesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/packages">
<a class="{{if .PageIsViewProjects}}active {{end}}item" href="{{$.Org.HomeLink}}/-/projects"> {{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
{{svg "octicon-project-symlink"}} {{ctx.Locale.Tr "user.projects"}} </a>
{{if .ProjectCount}}
<div class="ui small label">{{.ProjectCount}}</div>
{{end}} {{end}}
</a> {{if and .IsRepoIndexerEnabled .CanReadCode}}
{{end}} <a class="{{if .IsCodePage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code">
{{if and .IsPackageEnabled .CanReadPackages}} {{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
<a class="{{if .IsPackagesPage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/packages"> </a>
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} {{end}}
</a> {{if .NumMembers}}
{{end}}
{{if and .IsRepoIndexerEnabled .CanReadCode}}
<a class="{{if .IsCodePage}}active {{end}}item" href="{{$.Org.HomeLink}}/-/code">
{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
</a>
{{end}}
{{if .NumMembers}}
<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members"> <a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
{{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}} {{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}}
<div class="ui small label">{{.NumMembers}}</div> <div class="ui small label">{{.NumMembers}}</div>
</a> </a>
{{end}} {{end}}
{{if .IsOrganizationMember}} {{if .IsOrganizationMember}}
<a class="{{if $.PageIsOrgTeams}}active {{end}}item" href="{{$.OrgLink}}/teams"> <a class="{{if $.PageIsOrgTeams}}active {{end}}item" href="{{$.OrgLink}}/teams">
{{svg "octicon-people"}} {{ctx.Locale.Tr "org.teams"}} {{svg "octicon-people"}} {{ctx.Locale.Tr "org.teams"}}
{{if .NumTeams}} {{if .NumTeams}}
<div class="ui small label">{{.NumTeams}}</div> <div class="ui small label">{{.NumTeams}}</div>
{{end}} {{end}}
</a> </a>
{{end}} {{end}}
{{if .IsOrganizationOwner}}
{{if .IsOrganizationOwner}} <a class="{{if .PageIsOrgSettings}}active {{end}}item" href="{{.OrgLink}}/settings">
<div class="right menu"> {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
<a class="{{if .PageIsOrgSettings}}active {{end}}item" href="{{.OrgLink}}/settings"> </a>
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} {{end}}
</a> </div>
</div> </overflow-menu>
{{end}}
</div>
</div> </div>

View file

@ -4,12 +4,12 @@
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.alpine.registry"}}</label> <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.alpine.registry"}}</label>
<div class="markup"><pre class="code-block"><code><gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></gitea-origin-url>/$branch/$repository</code></pre></div> <div class="markup"><pre class="code-block"><code><origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine"></origin-url>/$branch/$repository</code></pre></div>
<p>{{ctx.Locale.Tr "packages.alpine.registry.info"}}</p> <p>{{ctx.Locale.Tr "packages.alpine.registry.info"}}</p>
</div> </div>
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.registry.key"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.registry.key"}}</label>
<div class="markup"><pre class="code-block"><code>curl -JO <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></gitea-origin-url></code></pre></div> <div class="markup"><pre class="code-block"><code>curl -JO <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/alpine/key"></origin-url></code></pre></div>
</div> </div>
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.install"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.alpine.install"}}</label>

View file

@ -8,8 +8,8 @@
default = "forgejo" default = "forgejo"
[registries.forgejo] [registries.forgejo]
index = "sparse+<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cargo/"></gitea-origin-url>" # Sparse index index = "sparse+<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cargo/"></origin-url>" # Sparse index
# index = "<gitea-origin-url data-url="{{AppSubUrl}}/{{.PackageDescriptor.Owner.Name}}/_cargo-index.git"></gitea-origin-url>" # Git # index = "<origin-url data-url="{{AppSubUrl}}/{{.PackageDescriptor.Owner.Name}}/_cargo-index.git"></origin-url>" # Git
[net] [net]
git-fetch-with-cli = true</code></pre></div> git-fetch-with-cli = true</code></pre></div>

View file

@ -4,7 +4,7 @@
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.chef.registry"}}</label> <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.chef.registry"}}</label>
<div class="markup"><pre class="code-block"><code>knife[:supermarket_site] = '<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/chef"></gitea-origin-url>'</code></pre></div> <div class="markup"><pre class="code-block"><code>knife[:supermarket_site] = '<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/chef"></origin-url>'</code></pre></div>
</div> </div>
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.chef.install"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.chef.install"}}</label>

View file

@ -7,7 +7,7 @@
<div class="markup"><pre class="code-block"><code>{ <div class="markup"><pre class="code-block"><code>{
"repositories": [{ "repositories": [{
"type": "composer", "type": "composer",
"url": "<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/composer"></gitea-origin-url>" "url": "<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/composer"></origin-url>"
} }
] ]
}</code></pre></div> }</code></pre></div>

View file

@ -4,7 +4,7 @@
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.registry"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.registry"}}</label>
<div class="markup"><pre class="code-block"><code>conan remote add gitea <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conan"></gitea-origin-url></code></pre></div> <div class="markup"><pre class="code-block"><code>conan remote add gitea <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conan"></origin-url></code></pre></div>
</div> </div>
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.install"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conan.install"}}</label>

View file

@ -4,11 +4,11 @@
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.conda.registry"}}</label> <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.conda.registry"}}</label>
<div class="markup"><pre class="code-block"><code>channel_alias: <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url> <div class="markup"><pre class="code-block"><code>channel_alias: <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url>
channels: channels:
&#32;&#32;- <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url> &#32;&#32;- <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url>
default_channels: default_channels:
&#32;&#32;- <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></gitea-origin-url></code></pre></div> &#32;&#32;- <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/conda"></origin-url></code></pre></div>
</div> </div>
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conda.install"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.conda.install"}}</label>

View file

@ -4,7 +4,7 @@
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cran.registry"}}</label> <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.cran.registry"}}</label>
<div class="markup"><pre class="code-block"><code>options("repos" = c(getOption("repos"), c(forgejo="<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cran"></gitea-origin-url>")))</code></pre></div> <div class="markup"><pre class="code-block"><code>options("repos" = c(getOption("repos"), c(forgejo="<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/cran"></origin-url>")))</code></pre></div>
</div> </div>
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.cran.install"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.cran.install"}}</label>

View file

@ -4,8 +4,8 @@
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.debian.registry"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.debian.registry"}}</label>
<div class="markup"><pre class="code-block"><code>sudo curl <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key"></gitea-origin-url> -o /etc/apt/keyrings/forgejo-{{$.PackageDescriptor.Owner.Name}}.asc <div class="markup"><pre class="code-block"><code>sudo curl <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian/repository.key"></origin-url> -o /etc/apt/keyrings/forgejo-{{$.PackageDescriptor.Owner.Name}}.asc
echo "deb [signed-by=/etc/apt/keyrings/forgejo-{{$.PackageDescriptor.Owner.Name}}.asc] <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian"></gitea-origin-url> $distribution $component" | sudo tee -a /etc/apt/sources.list.d/forgejo.list echo "deb [signed-by=/etc/apt/keyrings/forgejo-{{$.PackageDescriptor.Owner.Name}}.asc] <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/debian"></origin-url> $distribution $component" | sudo tee -a /etc/apt/sources.list.d/forgejo.list
sudo apt update</code></pre></div> sudo apt update</code></pre></div>
<p>{{ctx.Locale.Tr "packages.debian.registry.info"}}</p> <p>{{ctx.Locale.Tr "packages.debian.registry.info"}}</p>
</div> </div>

View file

@ -6,7 +6,7 @@
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.generic.download"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.generic.download"}}</label>
<div class="markup"><pre class="code-block"><code> <div class="markup"><pre class="code-block"><code>
{{- range .PackageDescriptor.Files -}} {{- range .PackageDescriptor.Files -}}
curl -OJ <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}"></gitea-origin-url> curl -OJ <origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/generic/{{$.PackageDescriptor.Package.Name}}/{{$.PackageDescriptor.Version.Version}}/{{.File.Name}}"></origin-url>
{{end -}} {{end -}}
</code></pre></div> </code></pre></div>
</div> </div>

View file

@ -4,7 +4,7 @@
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.go.install"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.go.install"}}</label>
<div class="markup"><pre class="code-block"><code>GOPROXY=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go"></gitea-origin-url> go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div> <div class="markup"><pre class="code-block"><code>GOPROXY=<origin-url data-url="{{AppSubUrl}}/api/packages/{{$.PackageDescriptor.Owner.Name}}/go"></origin-url> go install {{$.PackageDescriptor.Package.Name}}@{{$.PackageDescriptor.Version.Version}}</code></pre></div>
</div> </div>
<div class="field"> <div class="field">
<label>{{ctx.Locale.Tr "packages.registry.documentation" "Go" "https://forgejo.org/docs/latest/user/packages/go/"}}</label> <label>{{ctx.Locale.Tr "packages.registry.documentation" "Go" "https://forgejo.org/docs/latest/user/packages/go/"}}</label>

View file

@ -4,7 +4,7 @@
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.helm.registry"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.helm.registry"}}</label>
<div class="markup"><pre class="code-block"><code>helm repo add {{AppDomain}} <gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/helm"></gitea-origin-url> <div class="markup"><pre class="code-block"><code>helm repo add {{AppDomain}} <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/helm"></origin-url>
helm repo update</code></pre></div> helm repo update</code></pre></div>
</div> </div>
<div class="field"> <div class="field">

View file

@ -7,19 +7,19 @@
<div class="markup"><pre class="code-block"><code>&lt;repositories&gt; <div class="markup"><pre class="code-block"><code>&lt;repositories&gt;
&lt;repository&gt; &lt;repository&gt;
&lt;id&gt;gitea&lt;/id&gt; &lt;id&gt;gitea&lt;/id&gt;
&lt;url&gt;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url>&lt;/url&gt; &lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
&lt;/repository&gt; &lt;/repository&gt;
&lt;/repositories&gt; &lt;/repositories&gt;
&lt;distributionManagement&gt; &lt;distributionManagement&gt;
&lt;repository&gt; &lt;repository&gt;
&lt;id&gt;gitea&lt;/id&gt; &lt;id&gt;gitea&lt;/id&gt;
&lt;url&gt;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url>&lt;/url&gt; &lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
&lt;/repository&gt; &lt;/repository&gt;
&lt;snapshotRepository&gt; &lt;snapshotRepository&gt;
&lt;id&gt;gitea&lt;/id&gt; &lt;id&gt;gitea&lt;/id&gt;
&lt;url&gt;<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url>&lt;/url&gt; &lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
&lt;/snapshotRepository&gt; &lt;/snapshotRepository&gt;
&lt;/distributionManagement&gt;</code></pre></div> &lt;/distributionManagement&gt;</code></pre></div>
</div> </div>
@ -37,7 +37,7 @@
</div> </div>
<div class="field"> <div class="field">
<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.maven.download"}}</label> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.maven.download"}}</label>
<div class="markup"><pre class="code-block"><code>mvn dependency:get -DremoteRepositories=<gitea-origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></gitea-origin-url> -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}</code></pre></div> <div class="markup"><pre class="code-block"><code>mvn dependency:get -DremoteRepositories=<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url> -Dartifact={{.PackageDescriptor.Metadata.GroupID}}:{{.PackageDescriptor.Metadata.ArtifactID}}:{{.PackageDescriptor.Version.Version}}</code></pre></div>
</div> </div>
<div class="field"> <div class="field">
<label>{{ctx.Locale.Tr "packages.registry.documentation" "Maven" "https://forgejo.org/docs/latest/user/packages/maven/"}}</label> <label>{{ctx.Locale.Tr "packages.registry.documentation" "Maven" "https://forgejo.org/docs/latest/user/packages/maven/"}}</label>

Some files were not shown because too many files have changed in this diff Show more