mirror of
https://github.com/go-gitea/gitea.git
synced 2025-11-03 08:02:36 +09:00
Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97171be1b4 | ||
|
|
9ef2a338d8 | ||
|
|
66ccae8b90 | ||
|
|
0e6489317e | ||
|
|
67dc1ff926 | ||
|
|
4ee4c06b07 | ||
|
|
3063e37802 | ||
|
|
a40e15a116 | ||
|
|
8f75f61b64 | ||
|
|
25e409e025 | ||
|
|
f8f24d83cf | ||
|
|
15e93a751c | ||
|
|
5a9b3bfa50 | ||
|
|
dd901983c0 | ||
|
|
7f962a16c9 | ||
|
|
5d81f6d73f | ||
|
|
eee4a752a5 | ||
|
|
b3516767fb | ||
|
|
8d1be2a9c5 | ||
|
|
e46f9ff534 | ||
|
|
690e810bcc | ||
|
|
35983ac0a8 | ||
|
|
4b3400bd9c | ||
|
|
7758df4264 | ||
|
|
f994f3cac6 | ||
|
|
f514b2651e | ||
|
|
347101f2a8 | ||
|
|
b5f8c4a510 | ||
|
|
d6cee7c596 | ||
|
|
987219ab3c | ||
|
|
92280637a4 | ||
|
|
5fadcf997e | ||
|
|
be94f7bc07 | ||
|
|
9054a6670c | ||
|
|
fc82204fca | ||
|
|
6f8e62fa9c | ||
|
|
a2c6ecc093 | ||
|
|
523a84e5d0 | ||
|
|
869ee4fc38 | ||
|
|
16a332464d | ||
|
|
d03e7fd65e | ||
|
|
92f2d904f0 | ||
|
|
9c3511f0b1 | ||
|
|
69d35ee911 | ||
|
|
b8a7c20474 | ||
|
|
2cc76009dc | ||
|
|
730742230f | ||
|
|
8939c3845a | ||
|
|
d634e7576f | ||
|
|
7ded86f5af | ||
|
|
27a60fd91b | ||
|
|
e3021fae79 | ||
|
|
81126daf53 | ||
|
|
1c7339e385 | ||
|
|
039924aa2a | ||
|
|
b5007c6154 | ||
|
|
ae595aa913 | ||
|
|
aeeccc9642 | ||
|
|
37e99d9b34 | ||
|
|
9da6d4ea85 | ||
|
|
8844e62cb4 | ||
|
|
de7026528b | ||
|
|
ee3f5e8fac | ||
|
|
b2707bcd18 | ||
|
|
0512b02b01 | ||
|
|
99545ae2fd | ||
|
|
7697df9f93 | ||
|
|
d17f8ffcc1 | ||
|
|
5e9cc919cf | ||
|
|
cc6ec56738 | ||
|
|
76bd60fc1d | ||
|
|
744f7c8200 | ||
|
|
da33b708af | ||
|
|
8fa3925874 | ||
|
|
7794ff0874 | ||
|
|
7c17d0a73e | ||
|
|
a014d071e4 | ||
|
|
312565e3c2 | ||
|
|
58daaf66e8 | ||
|
|
f076ada601 | ||
|
|
92436b8b2a | ||
|
|
2df7d0835a | ||
|
|
200cb6140d | ||
|
|
2746c6f1aa | ||
|
|
23971a77a0 | ||
|
|
ebac324ff2 | ||
|
|
7df1204795 | ||
|
|
159544a950 | ||
|
|
9780da583d | ||
|
|
a8eaf43f97 | ||
|
|
b6fd8741ee | ||
|
|
2674d27fb8 | ||
|
|
6f3837284d | ||
|
|
c30f4f4be5 | ||
|
|
4578288ea3 | ||
|
|
826fffb59e | ||
|
|
2196ba5e42 | ||
|
|
12347f07ae | ||
|
|
a3c5358d35 | ||
|
|
987d014468 | ||
|
|
e08eed9040 | ||
|
|
4ffa49aa04 | ||
|
|
72837530bf | ||
|
|
eef635523a | ||
|
|
8f45a11919 | ||
|
|
e72d001708 | ||
|
|
8d9ea68f19 | ||
|
|
bf664c2e85 | ||
|
|
52d298890b | ||
|
|
c09e43acf5 | ||
|
|
3b4af01633 | ||
|
|
b4e2d5e8ee | ||
|
|
2984a7c121 | ||
|
|
80cc87b3d8 | ||
|
|
10b6047498 | ||
|
|
2c47b06869 | ||
|
|
31f2a325dc | ||
|
|
fcbbc24cc4 | ||
|
|
1454e1b6eb | ||
|
|
d70348836b | ||
|
|
940a930d13 | ||
|
|
45d21a0d5c | ||
|
|
15ad001aef | ||
|
|
ed1828ca92 | ||
|
|
3cfff5af0d | ||
|
|
6f6c66a07d | ||
|
|
d65af69c2b | ||
|
|
12c24c2189 |
7
.github/workflows/pull-db-tests.yml
vendored
7
.github/workflows/pull-db-tests.yml
vendored
@@ -202,12 +202,11 @@ jobs:
|
||||
test-mssql:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
needs: files-changed
|
||||
# specifying the version of ubuntu in use as mssql fails on newer kernels
|
||||
# pending resolution from vendor
|
||||
runs-on: ubuntu-20.04
|
||||
# NOTE: mssql-2017 docker image will panic when run on hosts that have Ubuntu newer than 20.04
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2017-latest
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||
env:
|
||||
ACCEPT_EULA: Y
|
||||
MSSQL_PID: Standard
|
||||
|
||||
4
.github/workflows/release-tag-version.yml
vendored
4
.github/workflows/release-tag-version.yml
vendored
@@ -88,9 +88,9 @@ jobs:
|
||||
# 1.2
|
||||
# 1.2.3
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -126,9 +126,9 @@ jobs:
|
||||
# 1.2
|
||||
# 1.2.3
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
|
||||
157
CHANGELOG.md
157
CHANGELOG.md
@@ -4,6 +4,163 @@ This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
|
||||
|
||||
* Enhancements
|
||||
* Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
|
||||
* Also check default ssh-cert location for host (#34099) (#34100) (#34116)
|
||||
* BUGFIXES
|
||||
* Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124)
|
||||
* Get changed files based on merge base when checking `pull_request` actions trigger (#34106) (#34120)
|
||||
* Fix invalid version in RPM package path (#34112) (#34115)
|
||||
* Return default avatar url when user id is zero rather than updating database (#34094) (#34095)
|
||||
* Add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070)
|
||||
* Try to fix check-attr bug (#34029) (#34033)
|
||||
* Git client will follow 301 but 307 (#34005) (#34010)
|
||||
* Fix block expensive for 1.23 (#34127)
|
||||
* Fix markdown frontmatter rendering (#34102) (#34107)
|
||||
* Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)
|
||||
* Do not show 500 error when default branch doesn't exist (#34096) (#34097)
|
||||
* Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065)
|
||||
* Simplify emoji rendering (#34048) (#34049)
|
||||
* Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047)
|
||||
* Pull request updates will also trigger code owners review requests (#33744) (#34045)
|
||||
* Fix org repo creation being limited by user limits (#34030) (#34044)
|
||||
* Fix git client accessing renamed repo (#34034) (#34043)
|
||||
* Fix the issue with error message logging for the `check-attr` command on Windows OS. (#34035) (#34036)
|
||||
* Polyfill WeakRef (#34025) (#34028)
|
||||
|
||||
## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
|
||||
|
||||
* SECURITY
|
||||
* Fix LFS URL (#33840) (#33843)
|
||||
* Update jwt and redis packages (#33984) (#33987)
|
||||
* Update golang crypto and net (#33989)
|
||||
* BUGFIXES
|
||||
* Drop timeout for requests made to the internal hook api (#33947) (#33970)
|
||||
* Fix maven panic when no package exists (#33888) (#33889)
|
||||
* Fix markdown render (#33870) (#33875)
|
||||
* Fix auto concurrency cancellation skips commit status updates (#33764) (#33849)
|
||||
* Fix oauth2 auth (#33961) (#33962)
|
||||
* Fix incorrect 1.23 translations (#33932)
|
||||
* Try to figure out attribute checker problem (#33901) (#33902)
|
||||
* Ignore trivial errors when updating push data (#33864) (#33887)
|
||||
* Fix some UI problems for 1.23 (#33856)
|
||||
* Removing unwanted ui container (#33833) (#33835)
|
||||
* Support disable passkey auth (#33348) (#33819)
|
||||
* Do not call "git diff" when listing PRs (#33817)
|
||||
* Try to fix ACME (3rd) (#33807) (#33808)
|
||||
* Fix incorrect code search indexer options (#33992) #33999
|
||||
|
||||
## [1.23.5](https://github.com/go-gitea/gitea/releases/tag/v1.23.5) - 2025-03-03
|
||||
|
||||
* SECURITY
|
||||
* Bump x/oauth2 & x/crypto (#33704) (#33727)
|
||||
* PERFORMANCE
|
||||
* Optimize user dashboard loading (#33686) (#33708)
|
||||
* BUGFIXES
|
||||
* Fix navbar dropdown item align (#33782)
|
||||
* Fix inconsistent closed issue list icon (#33722) (#33728)
|
||||
* Fix for Maven Package Naming Convention Handling (#33678) (#33679)
|
||||
* Improve Open-with URL encoding (#33666) (#33680)
|
||||
* Deleting repository should unlink all related packages (#33653) (#33673)
|
||||
* Fix omitempty bug (#33663) (#33670)
|
||||
* Upgrade go-crypto from 1.1.4 to 1.1.6 (#33745) (#33754)
|
||||
* Fix OCI image.version annotation for releases to use full semver (#33698) (#33701)
|
||||
* Try to fix ACME path when renew (#33668) (#33693)
|
||||
* Fix mCaptcha bug (#33659) (#33661)
|
||||
* Git graph: don't show detached commits (#33645) (#33650)
|
||||
* Use MatchPhraseQuery for bleve code search (#33628)
|
||||
* Adjust appearence of commit status webhook (#33778) #33789
|
||||
|
||||
## [1.23.4](https://github.com/go-gitea/gitea/releases/tag/v1.23.4) - 2025-02-16
|
||||
|
||||
* SECURITY
|
||||
* Enhance routers for the Actions variable operations (#33547) (#33553)
|
||||
* Enhance routers for the Actions runner operations (#33549) (#33555)
|
||||
* Fix project issues list and counting (#33594) #33619
|
||||
* PERFORMANCES
|
||||
* Performance optimization for pull request files loading comments attachments (#33585) (#33592)
|
||||
* BUGFIXES
|
||||
* Add a transaction to `pickTask` (#33543) (#33563)
|
||||
* Fix mirror bug (#33597) (#33607)
|
||||
* Use default Git timeout when checking repo health (#33593) (#33598)
|
||||
* Fix PR's target branch dropdown (#33589) (#33591)
|
||||
* Fix various problems (artifact order, api empty slice, assignee check, fuzzy prompt, mirror proxy, adopt git) (#33569) (#33577)
|
||||
* Rework suggestion backend (#33538) (#33546)
|
||||
* Fix context usage (#33554) (#33557)
|
||||
* Only show the latest version in the Arch index (#33262) (#33580)
|
||||
* Skip deletion error for action artifacts (#33476) (#33568)
|
||||
* Make actions URL in commit status webhooks absolute (#33620) #33632
|
||||
* Add missing locale (#33641) #33642
|
||||
|
||||
## [1.23.3](https://github.com/go-gitea/gitea/releases/tag/v1.23.3) - 2025-02-06
|
||||
|
||||
* Security
|
||||
* Build Gitea with Golang v1.23.6 to fix security bugs
|
||||
* BUGFIXES
|
||||
* Fix a bug caused by status webhook template #33512
|
||||
|
||||
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04
|
||||
|
||||
* BREAKING
|
||||
* Add tests for webhook and fix some webhook bugs (#33396) (#33442)
|
||||
* Package webhook’s Organization was incorrectly used as the User struct. This PR fixes the issue.
|
||||
* This changelog is just a hint. The change is not really breaking because most fields are the same, most users are not affected.
|
||||
* ENHANCEMENTS
|
||||
* Clone button enhancements (#33362) (#33404)
|
||||
* Repo homepage styling tweaks (#33289) (#33381)
|
||||
* Add a confirm dialog for "sync fork" (#33270) (#33273)
|
||||
* Make tracked time representation display as hours (#33315) (#33334)
|
||||
* Improve sync fork behavior (#33319) (#33332)
|
||||
* BUGFIXES
|
||||
* Fix code button alignment (#33345) (#33351)
|
||||
* Correct bot label `vertical-align` (#33477) (#33480)
|
||||
* Fix SSH LFS memory usage (#33455) (#33460)
|
||||
* Fix issue sidebar dropdown keyboard support (#33447) (#33450)
|
||||
* Fix user avatar (#33439)
|
||||
* Fix `GetCommitBranchStart` bug (#33298) (#33421)
|
||||
* Add pubdate for repository rss and add some tests (#33411) (#33416)
|
||||
* Add missed auto merge feed message on dashboard (#33309) (#33405)
|
||||
* Fix issue suggestion bug (#33389) (#33391)
|
||||
* Make issue suggestion work for all editors (#33340) (#33342)
|
||||
* Fix issue count (#33338) (#33341)
|
||||
* Fix Account linking page (#33325) (#33327)
|
||||
* Fix closed dependency title (#33285) (#33287)
|
||||
* Fix sidebar milestone link (#33269) (#33272)
|
||||
* Fix missing license when sync mirror (#33255) (#33258)
|
||||
* Fix upload file form (#33230) (#33233)
|
||||
* Fix mirror bug (#33224) (#33225)
|
||||
* Fix system admin cannot fork or get private fork with API (#33401) (#33417)
|
||||
* Fix push message behavior (#33215) (#33317)
|
||||
* Trivial fixes (#33304) (#33312)
|
||||
* Fix "stop time tracking button" on navbar (#33084) (#33300)
|
||||
* Fix tag route and empty repo (#33253)
|
||||
* Fix cache test triggered by non memory cache (#33220) (#33221)
|
||||
* Revert empty lfs ref name (#33454) (#33457)
|
||||
* Fix flex width (#33414) (#33418)
|
||||
* Fix commit status events (#33320) #33493
|
||||
* Fix unnecessary comment when moving issue on the same project column (#33496) #33499
|
||||
* Add timetzdata build tag to binary releases (#33463) #33503
|
||||
* MISC
|
||||
* Use ProtonMail/go-crypto to replace keybase/go-crypto (#33402) (#33410)
|
||||
* Update katex to latest version (#33361)
|
||||
* Update go tool dependencies (#32916) (#33355)
|
||||
|
||||
## [1.23.1](https://github.com/go-gitea/gitea/releases/tag/v1.23.1) - 2025-01-09
|
||||
|
||||
* ENHANCEMENTS
|
||||
* Move repo size to sidebar (#33155) (#33182)
|
||||
* BUGFIXES
|
||||
* Use updated path to s6-svscan after alpine upgrade (#33185) (#33188)
|
||||
* Fix fuzz test (#33156) (#33158)
|
||||
* Fix raw file API ref handling (#33172) (#33189)
|
||||
* Fix ACME panic (#33178) (#33186)
|
||||
* Fix branch dropdown not display ref name (#33159) (#33183)
|
||||
* Fix assignee list overlapping in Issue sidebar (#33176) (#33181)
|
||||
* Fix sync fork for consistency (#33147) #33192
|
||||
* Fix editor markdown not incrementing in a numbered list (#33187) #33193
|
||||
|
||||
## [1.23.0](https://github.com/go-gitea/gitea/releases/tag/v1.23.0) - 2025-01-08
|
||||
|
||||
* BREAKING
|
||||
|
||||
10
Makefile
10
Makefile
@@ -26,17 +26,17 @@ COMMA := ,
|
||||
XGO_VERSION := go-1.23.x
|
||||
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.0.3
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
|
||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.15.3
|
||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.0
|
||||
|
||||
DOCKER_IMAGE ?= gitea/gitea
|
||||
DOCKER_TAG ?= latest
|
||||
@@ -508,7 +508,7 @@ unit-test-coverage:
|
||||
tidy:
|
||||
$(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
|
||||
$(GO) mod tidy -compat=$(MIN_GO_VERSION)
|
||||
@$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
|
||||
$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
|
||||
|
||||
vendor: go.mod go.sum
|
||||
$(GO) mod vendor
|
||||
|
||||
5
assets/go-licenses.json
generated
5
assets/go-licenses.json
generated
@@ -744,11 +744,6 @@
|
||||
"path": "github.com/kevinburke/ssh_config/LICENSE",
|
||||
"licenseText": "Copyright (c) 2017 Kevin Burke.\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\n===================\n\nThe lexer and parser borrow heavily from github.com/pelletier/go-toml. The\nlicense for that project is copied below.\n\nThe MIT License (MIT)\n\nCopyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/keybase/go-crypto",
|
||||
"path": "github.com/keybase/go-crypto/LICENSE",
|
||||
"licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/klauspost/compress",
|
||||
"path": "github.com/klauspost/compress/LICENSE",
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -61,6 +62,16 @@ var microcmdUserCreate = &cli.Command{
|
||||
Name: "access-token",
|
||||
Usage: "Generate access token for the user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-name",
|
||||
Usage: `Name of the generated access token`,
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "access-token-scopes",
|
||||
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
Value: "all",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "restricted",
|
||||
Usage: "Make a restricted user account",
|
||||
@@ -162,23 +173,39 @@ func runCreateUser(c *cli.Context) error {
|
||||
IsRestricted: restricted,
|
||||
}
|
||||
|
||||
var accessTokenName string
|
||||
var accessTokenScope auth_model.AccessTokenScope
|
||||
if c.IsSet("access-token") {
|
||||
accessTokenName = strings.TrimSpace(c.String("access-token-name"))
|
||||
if accessTokenName == "" {
|
||||
return errors.New("access-token-name cannot be empty")
|
||||
}
|
||||
var err error
|
||||
accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid access token scope provided: %w", err)
|
||||
}
|
||||
if !accessTokenScope.HasPermissionScope() {
|
||||
return errors.New("access token does not have any permission")
|
||||
}
|
||||
} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
|
||||
return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
}
|
||||
|
||||
// arguments should be prepared before creating the user & access token, in case there is anything wrong
|
||||
|
||||
// create the user
|
||||
if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
|
||||
return fmt.Errorf("CreateUser: %w", err)
|
||||
}
|
||||
|
||||
if c.Bool("access-token") {
|
||||
t := &auth_model.AccessToken{
|
||||
Name: "gitea-admin",
|
||||
UID: u.ID,
|
||||
}
|
||||
|
||||
// create the access token
|
||||
if accessTokenScope != "" {
|
||||
t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
|
||||
if err := auth_model.NewAccessToken(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Access token was successfully created... %s\n", t.Token)
|
||||
}
|
||||
|
||||
fmt.Printf("New user '%s' has been successfully created!\n", username)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,37 +8,97 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAdminUserCreate(t *testing.T) {
|
||||
app := NewMainApp(AppVersion{})
|
||||
|
||||
reset := func() {
|
||||
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
|
||||
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
|
||||
require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
|
||||
}
|
||||
t.Run("MustChangePassword", func(t *testing.T) {
|
||||
type check struct{ IsAdmin, MustChangePassword bool }
|
||||
createCheck := func(name, args string) check {
|
||||
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
|
||||
return check{u.IsAdmin, u.MustChangePassword}
|
||||
}
|
||||
reset()
|
||||
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
|
||||
|
||||
reset()
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
|
||||
|
||||
reset()
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
|
||||
assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
|
||||
assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
|
||||
assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
|
||||
})
|
||||
|
||||
createUser := func(name, args string) error {
|
||||
return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
|
||||
}
|
||||
|
||||
type createCheck struct{ IsAdmin, MustChangePassword bool }
|
||||
createUser := func(name, args string) createCheck {
|
||||
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
|
||||
return createCheck{u.IsAdmin, u.MustChangePassword}
|
||||
}
|
||||
reset()
|
||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
|
||||
t.Run("AccessToken", func(t *testing.T) {
|
||||
// no generated access token
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
|
||||
reset()
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
|
||||
// using "--access-token" only means "all" access
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password --access-token"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
|
||||
hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hasScopes)
|
||||
|
||||
reset()
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
|
||||
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
|
||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
|
||||
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
|
||||
// using "--access-token" with name & scopes
|
||||
reset()
|
||||
assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
|
||||
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hasScopes)
|
||||
hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasScopes)
|
||||
|
||||
// using "--access-token-name" without "--access-token"
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token-name new-token-name")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
|
||||
// using "--access-token-scopes" without "--access-token"
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token-scopes read:issue")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
|
||||
|
||||
// empty permission
|
||||
reset()
|
||||
err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
|
||||
assert.ErrorContains(t, err, "access token does not have any permission")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "scopes",
|
||||
Value: "",
|
||||
Usage: "Comma separated list of scopes to apply to access token",
|
||||
Value: "all",
|
||||
Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
},
|
||||
},
|
||||
Action: runGenerateAccessToken,
|
||||
@@ -43,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
|
||||
func runGenerateAccessToken(c *cli.Context) error {
|
||||
if !c.IsSet("username") {
|
||||
return errors.New("You must provide a username to generate a token for")
|
||||
return errors.New("you must provide a username to generate a token for")
|
||||
}
|
||||
|
||||
ctx, cancel := installSignals()
|
||||
@@ -77,6 +77,9 @@ func runGenerateAccessToken(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid access token scope provided: %w", err)
|
||||
}
|
||||
if !accessTokenScope.HasPermissionScope() {
|
||||
return errors.New("access token does not have any permission")
|
||||
}
|
||||
t.Scope = accessTokenScope
|
||||
|
||||
// create the token
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
var CmdMigrate = &cli.Command{
|
||||
Name: "migrate",
|
||||
Usage: "Migrate the database",
|
||||
Description: "This is a command for migrating the database, so that you can run gitea admin create-user before starting the server.",
|
||||
Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`,
|
||||
Action: runMigrate,
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
)
|
||||
@@ -54,8 +55,6 @@ func runACME(listenAddr string, m http.Handler) error {
|
||||
altTLSALPNPort = p
|
||||
}
|
||||
|
||||
magic := &certmagic.Default
|
||||
magic.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
|
||||
// Try to use private CA root if provided, otherwise defaults to system's trust
|
||||
var certPool *x509.CertPool
|
||||
if setting.AcmeCARoot != "" {
|
||||
@@ -65,8 +64,20 @@ func runACME(listenAddr string, m http.Handler) error {
|
||||
log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
|
||||
}
|
||||
}
|
||||
myACME := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
|
||||
CA: setting.AcmeURL,
|
||||
// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
|
||||
// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
|
||||
// And one more thing, no idea why we should set the global default variables here
|
||||
// But it seems that the current ACME code needs these global variables to make renew work.
|
||||
// Otherwise, "renew" will use incorrect storage path
|
||||
oldDefaultACME := certmagic.DefaultACME
|
||||
certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
|
||||
certmagic.DefaultACME = certmagic.ACMEIssuer{
|
||||
// try to use the default values provided by DefaultACME
|
||||
CA: util.IfZero(setting.AcmeURL, oldDefaultACME.CA),
|
||||
TestCA: oldDefaultACME.TestCA,
|
||||
Logger: oldDefaultACME.Logger,
|
||||
HTTPProxy: oldDefaultACME.HTTPProxy,
|
||||
|
||||
TrustedRoots: certPool,
|
||||
Email: setting.AcmeEmail,
|
||||
Agreed: setting.AcmeTOS,
|
||||
@@ -75,8 +86,10 @@ func runACME(listenAddr string, m http.Handler) error {
|
||||
ListenHost: setting.HTTPAddr,
|
||||
AltTLSALPNPort: altTLSALPNPort,
|
||||
AltHTTPPort: altHTTPPort,
|
||||
})
|
||||
}
|
||||
|
||||
magic := certmagic.NewDefault()
|
||||
myACME := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME)
|
||||
magic.Issuers = []certmagic.Issuer{myACME}
|
||||
|
||||
// this obtains certificates or renews them if necessary
|
||||
|
||||
@@ -774,6 +774,9 @@ LEVEL = Info
|
||||
;ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
||||
;;
|
||||
;; User must sign in to view anything.
|
||||
;; After 1.23.7, it could be set to "expensive" to block anonymous users accessing some pages which consume a lot of resources,
|
||||
;; for example: block anonymous AI crawlers from accessing repo code pages.
|
||||
;; The "expensive" mode is experimental and subject to change.
|
||||
;REQUIRE_SIGNIN_VIEW = false
|
||||
;;
|
||||
;; Mail notification
|
||||
@@ -784,10 +787,13 @@ LEVEL = Info
|
||||
;; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token
|
||||
;ENABLE_BASIC_AUTHENTICATION = true
|
||||
;;
|
||||
;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 login methods.
|
||||
;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 or passkey login methods if they are enabled.
|
||||
;; If you set it to false, maybe it also needs to set ENABLE_BASIC_AUTHENTICATION to false to completely disable password-based authentication.
|
||||
;ENABLE_PASSWORD_SIGNIN_FORM = true
|
||||
;;
|
||||
;; Allow users to sign-in with a passkey
|
||||
;ENABLE_PASSKEY_AUTHENTICATION = true
|
||||
;;
|
||||
;; More detail: https://github.com/gogits/gogs/issues/165
|
||||
;ENABLE_REVERSE_PROXY_AUTHENTICATION = false
|
||||
; Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible.
|
||||
|
||||
@@ -31,6 +31,18 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then
|
||||
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
|
||||
fi
|
||||
|
||||
if [ -e /data/ssh/ssh_host_ed25519-cert.pub ]; then
|
||||
SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -e /data/ssh/ssh_host_rsa-cert.pub ]; then
|
||||
SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -e /data/ssh/ssh_host_ecdsa-cert.pub ]; then
|
||||
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa-cert.pub"}
|
||||
fi
|
||||
|
||||
if [ -d /etc/ssh ]; then
|
||||
SSH_PORT=${SSH_PORT:-"22"} \
|
||||
SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \
|
||||
|
||||
@@ -37,5 +37,5 @@ done
|
||||
if [ $# -gt 0 ]; then
|
||||
exec "$@"
|
||||
else
|
||||
exec /bin/s6-svscan /etc/s6
|
||||
exec /usr/bin/s6-svscan /etc/s6
|
||||
fi
|
||||
|
||||
23
go.mod
23
go.mod
@@ -1,6 +1,6 @@
|
||||
module code.gitea.io/gitea
|
||||
|
||||
go 1.23
|
||||
go 1.23.6
|
||||
|
||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
||||
// But some CAs use negative serial number, just relax the check. related:
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
|
||||
github.com/ProtonMail/go-crypto v1.1.4
|
||||
github.com/ProtonMail/go-crypto v1.1.6
|
||||
github.com/PuerkitoBio/goquery v1.10.0
|
||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3
|
||||
github.com/alecthomas/chroma/v2 v2.15.0
|
||||
@@ -64,7 +64,7 @@ require (
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/go-github/v61 v61.0.0
|
||||
github.com/google/licenseclassifier/v2 v2.0.0
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db
|
||||
@@ -79,7 +79,6 @@ require (
|
||||
github.com/jhillyerd/enmime v1.3.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4
|
||||
github.com/klauspost/compress v1.17.11
|
||||
github.com/klauspost/cpuid/v2 v2.2.8
|
||||
github.com/lib/pq v1.10.9
|
||||
@@ -101,7 +100,7 @@ require (
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/quasoft/websspi v1.1.2
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/redis/go-redis/v9 v9.7.3
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||
github.com/sassoftware/go-rpmutils v0.4.0
|
||||
@@ -119,13 +118,13 @@ require (
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/image v0.21.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/oauth2 v0.23.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/sys v0.29.0
|
||||
golang.org/x/text v0.21.0
|
||||
golang.org/x/net v0.37.0
|
||||
golang.org/x/oauth2 v0.27.0
|
||||
golang.org/x/sync v0.12.0
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/text v0.23.0
|
||||
golang.org/x/tools v0.29.0
|
||||
google.golang.org/grpc v1.67.1
|
||||
google.golang.org/protobuf v1.35.1
|
||||
@@ -216,7 +215,7 @@ require (
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/go-webauthn/x v0.1.15 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
|
||||
|
||||
45
go.sum
45
go.sum
@@ -71,8 +71,8 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.1.4 h1:G5U5asvD5N/6/36oIw3k2bOfBn5XVcZrb7PBjzzKKoE=
|
||||
github.com/ProtonMail/go-crypto v1.1.4/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
|
||||
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
|
||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||
@@ -373,10 +373,11 @@ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7w
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0=
|
||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
@@ -510,8 +511,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 h1:cTxwSmnaqLoo+4tLukHoB9iqHOu3LmLhRmgUxZo6Vp4=
|
||||
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
@@ -660,8 +659,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
|
||||
github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw=
|
||||
github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
|
||||
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
|
||||
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
|
||||
github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
@@ -835,8 +834,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
|
||||
@@ -871,10 +870,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -886,8 +885,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -921,8 +920,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -934,8 +933,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -946,8 +945,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
16
main_timezones.go
Normal file
16
main_timezones.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
// Golang has the ability to load OS's timezone data from most UNIX systems (https://github.com/golang/go/blob/master/src/time/zoneinfo_unix.go)
|
||||
// Even if the timezone data is missing, users could install the related packages to get it.
|
||||
// But on Windows, although `zoneinfo_windows.go` tries to load the timezone data from Windows registry,
|
||||
// some users still suffer from the issue that the timezone data is missing: https://github.com/go-gitea/gitea/issues/33235
|
||||
// So we import the tzdata package to make sure the timezone data is included in the binary.
|
||||
//
|
||||
// For non-Windows package builders, they could still use the "TAGS=timetzdata" to include the tzdata package in the binary.
|
||||
// If we decided to add the tzdata for other platforms, modify the "go:build" directive above.
|
||||
import _ "time/tzdata"
|
||||
@@ -114,6 +114,12 @@ type FindArtifactsOptions struct {
|
||||
Status int
|
||||
}
|
||||
|
||||
func (opts FindArtifactsOptions) ToOrders() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
var _ db.FindOptionsOrder = (*FindArtifactsOptions)(nil)
|
||||
|
||||
func (opts FindArtifactsOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
@@ -132,7 +138,7 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond {
|
||||
return cond
|
||||
}
|
||||
|
||||
// ActionArtifactMeta is the meta data of an artifact
|
||||
// ActionArtifactMeta is the meta-data of an artifact
|
||||
type ActionArtifactMeta struct {
|
||||
ArtifactName string
|
||||
FileSize int64
|
||||
|
||||
@@ -194,7 +194,7 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
|
||||
|
||||
// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
|
||||
// It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
|
||||
func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
|
||||
func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) ([]*ActionRunJob, error) {
|
||||
// Find all runs in the specified repository, reference, and workflow with non-final status
|
||||
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
|
||||
RepoID: repoID,
|
||||
@@ -204,14 +204,16 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
|
||||
Status: []Status{StatusRunning, StatusWaiting, StatusBlocked},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If there are no runs found, there's no need to proceed with cancellation, so return nil.
|
||||
if total == 0 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cancelledJobs := make([]*ActionRunJob, 0, total)
|
||||
|
||||
// Iterate over each found run and cancel its associated jobs.
|
||||
for _, run := range runs {
|
||||
// Find all jobs associated with the current run.
|
||||
@@ -219,7 +221,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
|
||||
RunID: run.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return cancelledJobs, err
|
||||
}
|
||||
|
||||
// Iterate over each job and attempt to cancel it.
|
||||
@@ -238,27 +240,29 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
|
||||
// Update the job's status and stopped time in the database.
|
||||
n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
|
||||
if err != nil {
|
||||
return err
|
||||
return cancelledJobs, err
|
||||
}
|
||||
|
||||
// If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again.
|
||||
if n == 0 {
|
||||
return fmt.Errorf("job has changed, try again")
|
||||
return cancelledJobs, fmt.Errorf("job has changed, try again")
|
||||
}
|
||||
|
||||
cancelledJobs = append(cancelledJobs, job)
|
||||
// Continue with the next job.
|
||||
continue
|
||||
}
|
||||
|
||||
// If the job has an associated task, try to stop the task, effectively cancelling the job.
|
||||
if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil {
|
||||
return err
|
||||
return cancelledJobs, err
|
||||
}
|
||||
cancelledJobs = append(cancelledJobs, job)
|
||||
}
|
||||
}
|
||||
|
||||
// Return nil to indicate successful cancellation of all running and waiting jobs.
|
||||
return nil
|
||||
return cancelledJobs, nil
|
||||
}
|
||||
|
||||
// InsertRun inserts a run
|
||||
|
||||
@@ -167,6 +167,7 @@ func init() {
|
||||
|
||||
type FindRunnerOptions struct {
|
||||
db.ListOptions
|
||||
IDs []int64
|
||||
RepoID int64
|
||||
OwnerID int64 // it will be ignored if RepoID is set
|
||||
Sort string
|
||||
@@ -178,6 +179,14 @@ type FindRunnerOptions struct {
|
||||
func (opts FindRunnerOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
|
||||
if len(opts.IDs) > 0 {
|
||||
if len(opts.IDs) == 1 {
|
||||
cond = cond.And(builder.Eq{"id": opts.IDs[0]})
|
||||
} else {
|
||||
cond = cond.And(builder.In("id", opts.IDs))
|
||||
}
|
||||
}
|
||||
|
||||
if opts.RepoID > 0 {
|
||||
c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID})
|
||||
if opts.WithAvailable {
|
||||
|
||||
@@ -120,21 +120,22 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error {
|
||||
func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) ([]*ActionRunJob, error) {
|
||||
// If actions disabled when there is schedule task, this will remove the outdated schedule tasks
|
||||
// There is no other place we can do this because the app.ini will be changed manually
|
||||
if err := DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
|
||||
return fmt.Errorf("DeleteCronTaskByRepo: %v", err)
|
||||
return nil, fmt.Errorf("DeleteCronTaskByRepo: %v", err)
|
||||
}
|
||||
// cancel running cron jobs of this repository and delete old schedules
|
||||
if err := CancelPreviousJobs(
|
||||
jobs, err := CancelPreviousJobs(
|
||||
ctx,
|
||||
repo.ID,
|
||||
repo.DefaultBranch,
|
||||
"",
|
||||
webhook_module.HookEventSchedule,
|
||||
); err != nil {
|
||||
return fmt.Errorf("CancelPreviousJobs: %v", err)
|
||||
)
|
||||
if err != nil {
|
||||
return jobs, fmt.Errorf("CancelPreviousJobs: %v", err)
|
||||
}
|
||||
return nil
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data strin
|
||||
|
||||
type FindVariablesOpts struct {
|
||||
db.ListOptions
|
||||
IDs []int64
|
||||
RepoID int64
|
||||
OwnerID int64 // it will be ignored if RepoID is set
|
||||
Name string
|
||||
@@ -65,6 +66,15 @@ type FindVariablesOpts struct {
|
||||
|
||||
func (opts FindVariablesOpts) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
|
||||
if len(opts.IDs) > 0 {
|
||||
if len(opts.IDs) == 1 {
|
||||
cond = cond.And(builder.Eq{"id": opts.IDs[0]})
|
||||
} else {
|
||||
cond = cond.And(builder.In("id", opts.IDs))
|
||||
}
|
||||
}
|
||||
|
||||
// Since we now support instance-level variables,
|
||||
// there is no need to check for null values for `owner_id` and `repo_id`
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
@@ -85,12 +95,12 @@ func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariab
|
||||
return db.Find[ActionVariable](ctx, opts)
|
||||
}
|
||||
|
||||
func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
|
||||
count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data").
|
||||
Update(&ActionVariable{
|
||||
Name: variable.Name,
|
||||
Data: variable.Data,
|
||||
})
|
||||
func UpdateVariableCols(ctx context.Context, variable *ActionVariable, cols ...string) (bool, error) {
|
||||
variable.Name = strings.ToUpper(variable.Name)
|
||||
count, err := db.GetEngine(ctx).
|
||||
ID(variable.ID).
|
||||
Cols(cols...).
|
||||
Update(variable)
|
||||
return count != 0, err
|
||||
}
|
||||
|
||||
|
||||
@@ -72,9 +72,9 @@ func (at ActionType) String() string {
|
||||
case ActionRenameRepo:
|
||||
return "rename_repo"
|
||||
case ActionStarRepo:
|
||||
return "star_repo"
|
||||
return "star_repo" // will not displayed in feeds.tmpl
|
||||
case ActionWatchRepo:
|
||||
return "watch_repo"
|
||||
return "watch_repo" // will not displayed in feeds.tmpl
|
||||
case ActionCommitRepo:
|
||||
return "commit_repo"
|
||||
case ActionCreateIssue:
|
||||
@@ -454,6 +454,24 @@ func ActivityReadable(user, doer *user_model.User) bool {
|
||||
doer != nil && (doer.IsAdmin || user.ID == doer.ID)
|
||||
}
|
||||
|
||||
func FeedDateCond(opts GetFeedsOptions) builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.Date == "" {
|
||||
return cond
|
||||
}
|
||||
|
||||
dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
|
||||
if err != nil {
|
||||
log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
|
||||
} else {
|
||||
dateHigh := dateLow.Add(86399000000000) // 23h59m59s
|
||||
|
||||
cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
|
||||
cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) {
|
||||
cond := builder.NewCond()
|
||||
|
||||
@@ -534,17 +552,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
|
||||
cond = cond.And(builder.Eq{"is_deleted": false})
|
||||
}
|
||||
|
||||
if opts.Date != "" {
|
||||
dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
|
||||
if err != nil {
|
||||
log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
|
||||
} else {
|
||||
dateHigh := dateLow.Add(86399000000000) // 23h59m59s
|
||||
|
||||
cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
|
||||
cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
|
||||
}
|
||||
}
|
||||
cond = cond.And(FeedDateCond(opts))
|
||||
|
||||
return cond, nil
|
||||
}
|
||||
|
||||
@@ -208,9 +208,31 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
|
||||
return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
|
||||
}
|
||||
|
||||
cond, err := ActivityQueryCondition(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
var err error
|
||||
var cond builder.Cond
|
||||
// if the actor is the requested user or is an administrator, we can skip the ActivityQueryCondition
|
||||
if opts.Actor != nil && opts.RequestedUser != nil && (opts.Actor.IsAdmin || opts.Actor.ID == opts.RequestedUser.ID) {
|
||||
cond = builder.Eq{
|
||||
"user_id": opts.RequestedUser.ID,
|
||||
}.And(
|
||||
FeedDateCond(opts),
|
||||
)
|
||||
|
||||
if !opts.IncludeDeleted {
|
||||
cond = cond.And(builder.Eq{"is_deleted": false})
|
||||
}
|
||||
|
||||
if !opts.IncludePrivate {
|
||||
cond = cond.And(builder.Eq{"is_private": false})
|
||||
}
|
||||
if opts.OnlyPerformedBy {
|
||||
cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
|
||||
}
|
||||
} else {
|
||||
cond, err = ActivityQueryCondition(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
actions := make([]*Action, 0, opts.PageSize)
|
||||
|
||||
@@ -44,7 +44,7 @@ func init() {
|
||||
// TranslatableMessage represents JSON struct that can be translated with a Locale
|
||||
type TranslatableMessage struct {
|
||||
Format string
|
||||
Args []any `json:"omitempty"`
|
||||
Args []any `json:",omitempty"`
|
||||
}
|
||||
|
||||
// LoadRepo loads repository of the task
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp"
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
@@ -141,7 +141,11 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified
|
||||
// Parse Subkeys
|
||||
subkeys := make([]*GPGKey, len(e.Subkeys))
|
||||
for i, k := range e.Subkeys {
|
||||
subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry)
|
||||
subkeyExpiry := expiry
|
||||
if k.Sig.KeyLifetimeSecs != nil {
|
||||
subkeyExpiry = k.PublicKey.CreationTime.Add(time.Duration(*k.Sig.KeyLifetimeSecs) * time.Second)
|
||||
}
|
||||
subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, subkeyExpiry)
|
||||
if err != nil {
|
||||
return nil, ErrGPGKeyParsing{ParseError: err}
|
||||
}
|
||||
@@ -156,7 +160,7 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified
|
||||
|
||||
emails := make([]*user_model.EmailAddress, 0, len(e.Identities))
|
||||
for _, ident := range e.Identities {
|
||||
if ident.Revocation != nil {
|
||||
if ident.Revoked(time.Now()) {
|
||||
continue
|
||||
}
|
||||
email := strings.ToLower(strings.TrimSpace(ident.UserId.Email))
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
)
|
||||
|
||||
// __________________ ________ ____ __.
|
||||
@@ -83,12 +83,12 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str
|
||||
verified := false
|
||||
// Handle provided signature
|
||||
if signature != "" {
|
||||
signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature))
|
||||
signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature), nil)
|
||||
if err != nil {
|
||||
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature))
|
||||
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature), nil)
|
||||
}
|
||||
if err != nil {
|
||||
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature))
|
||||
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Unable to validate token signature. Error: %v", err)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
)
|
||||
|
||||
// __________________ ________ ____ __.
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp"
|
||||
"github.com/keybase/go-crypto/openpgp/armor"
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
)
|
||||
|
||||
// __________________ ________ ____ __.
|
||||
@@ -80,7 +80,7 @@ func base64DecPubKey(content string) (*packet.PublicKey, error) {
|
||||
return pkey, nil
|
||||
}
|
||||
|
||||
// getExpiryTime extract the expire time of primary key based on sig
|
||||
// getExpiryTime extract the expiry time of primary key based on sig
|
||||
func getExpiryTime(e *openpgp.Entity) time.Time {
|
||||
expiry := time.Time{}
|
||||
// Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165
|
||||
@@ -88,12 +88,12 @@ func getExpiryTime(e *openpgp.Entity) time.Time {
|
||||
for _, ident := range e.Identities {
|
||||
if selfSig == nil {
|
||||
selfSig = ident.SelfSignature
|
||||
} else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
|
||||
} else if ident.SelfSignature != nil && ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
|
||||
selfSig = ident.SelfSignature
|
||||
break
|
||||
}
|
||||
}
|
||||
if selfSig.KeyLifetimeSecs != nil {
|
||||
if selfSig != nil && selfSig.KeyLifetimeSecs != nil {
|
||||
expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second)
|
||||
}
|
||||
return expiry
|
||||
|
||||
@@ -13,7 +13,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -403,3 +404,25 @@ func TestTryGetKeyIDFromSignature(t *testing.T) {
|
||||
IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c},
|
||||
}))
|
||||
}
|
||||
|
||||
func TestParseGPGKey(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
assert.NoError(t, db.Insert(db.DefaultContext, &user_model.EmailAddress{UID: 1, Email: "email1@example.com", IsActivated: true}))
|
||||
|
||||
// create a key for test email
|
||||
e, err := openpgp.NewEntity("name", "comment", "email1@example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
k, err := parseGPGKey(db.DefaultContext, 1, e, true)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, k.KeyID)
|
||||
assert.NotEmpty(t, k.Emails) // the key is valid, matches the email
|
||||
|
||||
// then revoke the key
|
||||
for _, id := range e.Identities {
|
||||
id.Revocations = append(id.Revocations, &packet.Signature{RevocationReason: util.ToPointer(packet.KeyCompromised)})
|
||||
}
|
||||
k, err = parseGPGKey(db.DefaultContext, 1, e, true)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, k.KeyID)
|
||||
assert.Empty(t, k.Emails) // the key is revoked, matches no email
|
||||
}
|
||||
|
||||
@@ -283,6 +283,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
|
||||
return bitmap.toScope(), nil
|
||||
}
|
||||
|
||||
func (s AccessTokenScope) HasPermissionScope() bool {
|
||||
return s != "" && s != AccessTokenScopePublicOnly
|
||||
}
|
||||
|
||||
// PublicOnly checks if this token scope is limited to public resources
|
||||
func (s AccessTokenScope) PublicOnly() (bool, error) {
|
||||
bitmap, err := s.parse()
|
||||
|
||||
@@ -167,9 +167,24 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
|
||||
BranchName: branchName,
|
||||
}
|
||||
}
|
||||
// FIXME: this design is not right: it doesn't check `branch.IsDeleted`, it doesn't make sense to make callers to check IsDeleted again and again.
|
||||
// It causes inconsistency with `GetBranches` and `git.GetBranch`, and will lead to strange bugs
|
||||
// In the future, there should be 2 functions: `GetBranchExisting` and `GetBranchWithDeleted`
|
||||
return &branch, nil
|
||||
}
|
||||
|
||||
// IsBranchExist returns true if the branch exists in the repository.
|
||||
func IsBranchExist(ctx context.Context, repoID int64, branchName string) (bool, error) {
|
||||
var branch Branch
|
||||
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch)
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if !has {
|
||||
return false, nil
|
||||
}
|
||||
return !branch.IsDeleted, nil
|
||||
}
|
||||
|
||||
func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
|
||||
branches := make([]*Branch, 0, len(branchNames))
|
||||
|
||||
@@ -440,6 +455,8 @@ type FindRecentlyPushedNewBranchesOptions struct {
|
||||
}
|
||||
|
||||
type RecentlyPushedNewBranch struct {
|
||||
BranchRepo *repo_model.Repository
|
||||
BranchName string
|
||||
BranchDisplayName string
|
||||
BranchLink string
|
||||
BranchCompareURL string
|
||||
@@ -540,7 +557,9 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o
|
||||
branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName)
|
||||
}
|
||||
newBranches = append(newBranches, &RecentlyPushedNewBranch{
|
||||
BranchRepo: branch.Repo,
|
||||
BranchDisplayName: branchDisplayName,
|
||||
BranchName: branch.Name,
|
||||
BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
|
||||
BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
|
||||
CommitTime: branch.CommitTime,
|
||||
|
||||
@@ -86,8 +86,10 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
|
||||
ids = append(ids, comment.ReviewID)
|
||||
}
|
||||
}
|
||||
if err := e.In("id", ids).Find(&reviews); err != nil {
|
||||
return nil, err
|
||||
if len(ids) > 0 {
|
||||
if err := e.In("id", ids).Find(&reviews); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
n := 0
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@@ -531,6 +532,45 @@ func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
|
||||
return issue, nil
|
||||
}
|
||||
|
||||
func isPullToCond(isPull optional.Option[bool]) builder.Cond {
|
||||
if isPull.Has() {
|
||||
return builder.Eq{"is_pull": isPull.Value()}
|
||||
}
|
||||
return builder.NewCond()
|
||||
}
|
||||
|
||||
func FindLatestUpdatedIssues(ctx context.Context, repoID int64, isPull optional.Option[bool], pageSize int) (IssueList, error) {
|
||||
issues := make([]*Issue, 0, pageSize)
|
||||
err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
|
||||
And(isPullToCond(isPull)).
|
||||
OrderBy("updated_unix DESC").
|
||||
Limit(pageSize).
|
||||
Find(&issues)
|
||||
return issues, err
|
||||
}
|
||||
|
||||
func FindIssuesSuggestionByKeyword(ctx context.Context, repoID int64, keyword string, isPull optional.Option[bool], excludedID int64, pageSize int) (IssueList, error) {
|
||||
cond := builder.NewCond()
|
||||
if excludedID > 0 {
|
||||
cond = cond.And(builder.Neq{"`id`": excludedID})
|
||||
}
|
||||
|
||||
// It seems that GitHub searches both title and content (maybe sorting by the search engine's ranking system?)
|
||||
// The first PR (https://github.com/go-gitea/gitea/pull/32327) uses "search indexer" to search "name(title) + content"
|
||||
// But it seems that searching "content" (especially LIKE by DB engine) generates worse (unusable) results.
|
||||
// So now (https://github.com/go-gitea/gitea/pull/33538) it only searches "name(title)", leave the improvements to the future.
|
||||
cond = cond.And(db.BuildCaseInsensitiveLike("`name`", keyword))
|
||||
|
||||
issues := make([]*Issue, 0, pageSize)
|
||||
err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
|
||||
And(isPullToCond(isPull)).
|
||||
And(cond).
|
||||
OrderBy("updated_unix DESC, `index` DESC").
|
||||
Limit(pageSize).
|
||||
Find(&issues)
|
||||
return issues, err
|
||||
}
|
||||
|
||||
// GetIssueWithAttrsByIndex returns issue by index in a repository.
|
||||
func GetIssueWithAttrsByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
|
||||
issue, err := GetIssueByIndex(ctx, repoID, index)
|
||||
|
||||
@@ -38,13 +38,30 @@ func (issue *Issue) projectID(ctx context.Context) int64 {
|
||||
}
|
||||
|
||||
// ProjectColumnID return project column id if issue was assigned to one
|
||||
func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
|
||||
func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) {
|
||||
var ip project_model.ProjectIssue
|
||||
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
|
||||
if err != nil || !has {
|
||||
return 0
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if !has {
|
||||
return 0, nil
|
||||
}
|
||||
return ip.ProjectColumnID
|
||||
return ip.ProjectColumnID, nil
|
||||
}
|
||||
|
||||
func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID int64) (map[int64]int64, error) {
|
||||
issues := make([]project_model.ProjectIssue, 0)
|
||||
if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&issues); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[int64]int64, len(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.ProjectColumnID == 0 {
|
||||
issue.ProjectColumnID = defaultColumnID
|
||||
}
|
||||
result[issue.IssueID] = issue.ProjectColumnID
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// LoadIssuesFromColumn load issues assigned to this column
|
||||
@@ -59,11 +76,11 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
|
||||
}
|
||||
|
||||
if b.Default {
|
||||
issues, err := Issues(ctx, &IssuesOptions{
|
||||
ProjectColumnID: db.NoConditionID,
|
||||
ProjectID: b.ProjectID,
|
||||
SortType: "project-column-sorting",
|
||||
})
|
||||
issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
|
||||
o.ProjectColumnID = db.NoConditionID
|
||||
o.ProjectID = b.ProjectID
|
||||
o.SortType = "project-column-sorting"
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -77,19 +94,6 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
|
||||
return issueList, nil
|
||||
}
|
||||
|
||||
// LoadIssuesFromColumnList load issues assigned to the columns
|
||||
func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList, opts *IssuesOptions) (map[int64]IssueList, error) {
|
||||
issuesMap := make(map[int64]IssueList, len(bs))
|
||||
for i := range bs {
|
||||
il, err := LoadIssuesFromColumn(ctx, bs[i], opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issuesMap[bs[i].ID] = il
|
||||
}
|
||||
return issuesMap, nil
|
||||
}
|
||||
|
||||
// IssueAssignOrRemoveProject changes the project associated with an issue
|
||||
// If newProjectID is 0, the issue is removed from the project
|
||||
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
|
||||
@@ -110,7 +114,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
|
||||
return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
|
||||
}
|
||||
if newColumnID == 0 {
|
||||
newDefaultColumn, err := newProject.GetDefaultColumn(ctx)
|
||||
newDefaultColumn, err := newProject.MustDefaultColumn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -49,9 +49,9 @@ type IssuesOptions struct { //nolint
|
||||
// prioritize issues from this repo
|
||||
PriorityRepoID int64
|
||||
IsArchived optional.Option[bool]
|
||||
Org *organization.Organization // issues permission scope
|
||||
Team *organization.Team // issues permission scope
|
||||
User *user_model.User // issues permission scope
|
||||
Owner *user_model.User // issues permission scope, it could be an organization or a user
|
||||
Team *organization.Team // issues permission scope
|
||||
Doer *user_model.User // issues permission scope
|
||||
}
|
||||
|
||||
// Copy returns a copy of the options.
|
||||
@@ -273,8 +273,12 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
|
||||
|
||||
applyLabelsCondition(sess, opts)
|
||||
|
||||
if opts.User != nil {
|
||||
sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
|
||||
if opts.Owner != nil {
|
||||
sess.And(repo_model.UserOwnedRepoCond(opts.Owner.ID))
|
||||
}
|
||||
|
||||
if opts.Doer != nil && !opts.Doer.IsAdmin {
|
||||
sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.Doer.ID, opts.Owner, opts.Team, opts.IsPull.Value()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,20 +325,20 @@ func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Typ
|
||||
}
|
||||
|
||||
// issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table
|
||||
func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond {
|
||||
func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_model.User, team *organization.Team, isPull bool) builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
unitType := unit.TypeIssues
|
||||
if isPull {
|
||||
unitType = unit.TypePullRequests
|
||||
}
|
||||
if org != nil {
|
||||
if owner != nil && owner.IsOrganization() {
|
||||
if team != nil {
|
||||
cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos
|
||||
cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, owner.ID, team.ID, unitType)) // special team member repos
|
||||
} else {
|
||||
cond = cond.And(
|
||||
builder.Or(
|
||||
repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos
|
||||
repo_model.UserOrgPublicUnitRepoCond(userID, org.ID), // user org public non-member repos, TODO: check repo has issues
|
||||
repo_model.UserOrgUnitRepoCond(repoIDstr, userID, owner.ID, unitType), // team member repos
|
||||
repo_model.UserOrgPublicUnitRepoCond(userID, owner.ID), // user org public non-member repos, TODO: check repo has issues
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func (s Stopwatch) Seconds() int64 {
|
||||
|
||||
// Duration returns a human-readable duration string based on local server time
|
||||
func (s Stopwatch) Duration() string {
|
||||
return util.SecToTime(s.Seconds())
|
||||
return util.SecToHours(s.Seconds())
|
||||
}
|
||||
|
||||
func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
|
||||
@@ -201,7 +201,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
|
||||
Doer: user,
|
||||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Content: util.SecToTime(timediff),
|
||||
Content: util.SecToHours(timediff),
|
||||
Type: CommentTypeStopTracking,
|
||||
TimeID: tt.ID,
|
||||
}); err != nil {
|
||||
|
||||
@@ -48,6 +48,8 @@ type Column struct {
|
||||
ProjectID int64 `xorm:"INDEX NOT NULL"`
|
||||
CreatorID int64 `xorm:"NOT NULL"`
|
||||
|
||||
NumIssues int64 `xorm:"-"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
@@ -57,20 +59,6 @@ func (Column) TableName() string {
|
||||
return "project_board" // TODO: the legacy table name should be project_column
|
||||
}
|
||||
|
||||
// NumIssues return counter of all issues assigned to the column
|
||||
func (c *Column) NumIssues(ctx context.Context) int {
|
||||
total, err := db.GetEngine(ctx).Table("project_issue").
|
||||
Where("project_id=?", c.ProjectID).
|
||||
And("project_board_id=?", c.ID).
|
||||
GroupBy("issue_id").
|
||||
Cols("issue_id").
|
||||
Count()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return int(total)
|
||||
}
|
||||
|
||||
func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
|
||||
issues := make([]*ProjectIssue, 0, 5)
|
||||
if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
|
||||
@@ -192,7 +180,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defaultColumn, err := project.GetDefaultColumn(ctx)
|
||||
defaultColumn, err := project.MustDefaultColumn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -257,8 +245,8 @@ func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
// GetDefaultColumn return default column and ensure only one exists
|
||||
func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
|
||||
// getDefaultColumn return default column and ensure only one exists
|
||||
func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) {
|
||||
var column Column
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("project_id=? AND `default` = ?", p.ID, true).
|
||||
@@ -270,6 +258,33 @@ func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
|
||||
if has {
|
||||
return &column, nil
|
||||
}
|
||||
return nil, ErrProjectColumnNotExist{ColumnID: 0}
|
||||
}
|
||||
|
||||
// MustDefaultColumn returns the default column for a project.
|
||||
// If one exists, it is returned
|
||||
// If none exists, the first column will be elevated to the default column of this project
|
||||
func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) {
|
||||
c, err := p.getDefaultColumn(ctx)
|
||||
if err != nil && !IsErrProjectColumnNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
if c != nil {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var column Column
|
||||
has, err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has {
|
||||
column.Default = true
|
||||
if _, err := db.GetEngine(ctx).ID(column.ID).Cols("`default`").Update(&column); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &column, nil
|
||||
}
|
||||
|
||||
// create a default column if none is found
|
||||
column = Column{
|
||||
|
||||
@@ -20,19 +20,19 @@ func TestGetDefaultColumn(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check if default column was added
|
||||
column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext)
|
||||
column, err := projectWithoutDefault.MustDefaultColumn(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(5), column.ProjectID)
|
||||
assert.Equal(t, "Uncategorized", column.Title)
|
||||
assert.Equal(t, "Done", column.Title)
|
||||
|
||||
projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check if multiple defaults were removed
|
||||
column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext)
|
||||
column, err = projectWithMultipleDefaults.MustDefaultColumn(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(6), column.ProjectID)
|
||||
assert.Equal(t, int64(9), column.ID)
|
||||
assert.Equal(t, int64(9), column.ID) // there are 2 default columns in the test data, use the latest one
|
||||
|
||||
// set 8 as default column
|
||||
assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8))
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
@@ -34,48 +33,6 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error
|
||||
return err
|
||||
}
|
||||
|
||||
// NumIssues return counter of all issues assigned to a project
|
||||
func (p *Project) NumIssues(ctx context.Context) int {
|
||||
c, err := db.GetEngine(ctx).Table("project_issue").
|
||||
Where("project_id=?", p.ID).
|
||||
GroupBy("issue_id").
|
||||
Cols("issue_id").
|
||||
Count()
|
||||
if err != nil {
|
||||
log.Error("NumIssues: %v", err)
|
||||
return 0
|
||||
}
|
||||
return int(c)
|
||||
}
|
||||
|
||||
// NumClosedIssues return counter of closed issues assigned to a project
|
||||
func (p *Project) NumClosedIssues(ctx context.Context) int {
|
||||
c, err := db.GetEngine(ctx).Table("project_issue").
|
||||
Join("INNER", "issue", "project_issue.issue_id=issue.id").
|
||||
Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, true).
|
||||
Cols("issue_id").
|
||||
Count()
|
||||
if err != nil {
|
||||
log.Error("NumClosedIssues: %v", err)
|
||||
return 0
|
||||
}
|
||||
return int(c)
|
||||
}
|
||||
|
||||
// NumOpenIssues return counter of open issues assigned to a project
|
||||
func (p *Project) NumOpenIssues(ctx context.Context) int {
|
||||
c, err := db.GetEngine(ctx).Table("project_issue").
|
||||
Join("INNER", "issue", "project_issue.issue_id=issue.id").
|
||||
Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, false).
|
||||
Cols("issue_id").
|
||||
Count()
|
||||
if err != nil {
|
||||
log.Error("NumOpenIssues: %v", err)
|
||||
return 0
|
||||
}
|
||||
return int(c)
|
||||
}
|
||||
|
||||
func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
|
||||
if c.ProjectID != newColumn.ProjectID {
|
||||
return fmt.Errorf("columns have to be in the same project")
|
||||
|
||||
@@ -97,6 +97,9 @@ type Project struct {
|
||||
Type Type
|
||||
|
||||
RenderedContent template.HTML `xorm:"-"`
|
||||
NumOpenIssues int64 `xorm:"-"`
|
||||
NumClosedIssues int64 `xorm:"-"`
|
||||
NumIssues int64 `xorm:"-"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
|
||||
@@ -54,6 +54,7 @@ func UpdateRepoLicenses(ctx context.Context, repo *Repository, commitID string,
|
||||
for _, o := range oldLicenses {
|
||||
// Update already existing license
|
||||
if o.License == license {
|
||||
o.CommitID = commitID
|
||||
if _, err := db.GetEngine(ctx).ID(o.ID).Cols("`commit_id`").Update(o); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -38,27 +38,32 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
|
||||
|
||||
u.Avatar = avatars.HashEmail(seed)
|
||||
|
||||
// Don't share the images so that we can delete them easily
|
||||
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
|
||||
if err := png.Encode(w, img); err != nil {
|
||||
log.Error("Encode: %v", err)
|
||||
_, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
|
||||
if err != nil {
|
||||
// If unable to Stat the avatar file (usually it means non-existing), then try to save a new one
|
||||
// Don't share the images so that we can delete them easily
|
||||
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
|
||||
if err := png.Encode(w, img); err != nil {
|
||||
log.Error("Encode: %v", err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err)
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err)
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar").Update(u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("New random avatar created: %d", u.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
|
||||
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
|
||||
if u.IsGhost() {
|
||||
// ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user
|
||||
// which comes from git configure information
|
||||
if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 {
|
||||
return avatars.DefaultAvatarLink()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,19 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUserAvatarLink(t *testing.T) {
|
||||
@@ -26,3 +32,37 @@ func TestUserAvatarLink(t *testing.T) {
|
||||
link = u.AvatarLink(db.DefaultContext)
|
||||
assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link)
|
||||
}
|
||||
|
||||
func TestUserAvatarGenerate(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
var err error
|
||||
tmpDir := t.TempDir()
|
||||
storage.Avatars, err = storage.NewLocalStorage(context.Background(), &setting.Storage{Path: tmpDir})
|
||||
require.NoError(t, err)
|
||||
|
||||
u := unittest.AssertExistsAndLoadBean(t, &User{ID: 2})
|
||||
|
||||
// there was no avatar, generate a new one
|
||||
assert.Empty(t, u.Avatar)
|
||||
err = GenerateRandomAvatar(db.DefaultContext, u)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, u.Avatar)
|
||||
|
||||
// make sure the generated one exists
|
||||
oldAvatarPath := u.CustomAvatarRelativePath()
|
||||
_, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
|
||||
require.NoError(t, err)
|
||||
// and try to change its content
|
||||
_, err = storage.Avatars.Save(u.CustomAvatarRelativePath(), strings.NewReader("abcd"), 4)
|
||||
require.NoError(t, err)
|
||||
|
||||
// try to generate again
|
||||
err = GenerateRandomAvatar(db.DefaultContext, u)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldAvatarPath, u.CustomAvatarRelativePath())
|
||||
f, err := storage.Avatars.Open(u.CustomAvatarRelativePath())
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
content, _ := io.ReadAll(f)
|
||||
assert.Equal(t, "abcd", string(content))
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ func NewGhostUser() *User {
|
||||
}
|
||||
}
|
||||
|
||||
func IsGhostUserName(name string) bool {
|
||||
return strings.EqualFold(name, GhostUserName)
|
||||
}
|
||||
|
||||
// IsGhost check if user is fake user for a deleted account
|
||||
func (u *User) IsGhost() bool {
|
||||
if u == nil {
|
||||
@@ -48,6 +52,10 @@ const (
|
||||
ActionsEmail = "teabot@gitea.io"
|
||||
)
|
||||
|
||||
func IsGiteaActionsUserName(name string) bool {
|
||||
return strings.EqualFold(name, ActionsUserName)
|
||||
}
|
||||
|
||||
// NewActionsUser creates and returns a fake user for running the actions.
|
||||
func NewActionsUser() *User {
|
||||
return &User{
|
||||
@@ -65,6 +73,16 @@ func NewActionsUser() *User {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) IsActions() bool {
|
||||
func (u *User) IsGiteaActions() bool {
|
||||
return u != nil && u.ID == ActionsUserID
|
||||
}
|
||||
|
||||
func GetSystemUserByName(name string) *User {
|
||||
if IsGhostUserName(name) {
|
||||
return NewGhostUser()
|
||||
}
|
||||
if IsGiteaActionsUserName(name) {
|
||||
return NewActionsUser()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
32
models/user/user_system_test.go
Normal file
32
models/user/user_system_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSystemUser(t *testing.T) {
|
||||
u, err := GetPossibleUserByID(db.DefaultContext, -1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Ghost", u.Name)
|
||||
assert.Equal(t, "ghost", u.LowerName)
|
||||
assert.True(t, u.IsGhost())
|
||||
assert.True(t, IsGhostUserName("gHost"))
|
||||
|
||||
u, err = GetPossibleUserByID(db.DefaultContext, -2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "gitea-actions", u.Name)
|
||||
assert.Equal(t, "gitea-actions", u.LowerName)
|
||||
assert.True(t, u.IsGiteaActions())
|
||||
assert.True(t, IsGiteaActionsUserName("Gitea-actionS"))
|
||||
|
||||
_, err = GetPossibleUserByID(db.DefaultContext, -3)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -299,6 +299,11 @@ func (w *Webhook) HasPackageEvent() bool {
|
||||
(w.ChooseEvents && w.HookEvents.Package)
|
||||
}
|
||||
|
||||
func (w *Webhook) HasStatusEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Status)
|
||||
}
|
||||
|
||||
// HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event.
|
||||
func (w *Webhook) HasPullRequestReviewRequestEvent() bool {
|
||||
return w.SendEverything ||
|
||||
@@ -337,6 +342,7 @@ func (w *Webhook) EventCheckers() []struct {
|
||||
{w.HasReleaseEvent, webhook_module.HookEventRelease},
|
||||
{w.HasPackageEvent, webhook_module.HookEventPackage},
|
||||
{w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest},
|
||||
{w.HasStatusEvent, webhook_module.HookEventStatus},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestWebhook_EventsArray(t *testing.T) {
|
||||
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
|
||||
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
|
||||
"pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release",
|
||||
"package", "pull_request_review_request",
|
||||
"package", "pull_request_review_request", "status",
|
||||
},
|
||||
(&Webhook{
|
||||
HookEvent: &webhook_module.HookEvent{SendEverything: true},
|
||||
|
||||
@@ -463,7 +463,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
matchTimes++
|
||||
}
|
||||
case "paths":
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||
} else {
|
||||
@@ -476,7 +476,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
|
||||
}
|
||||
}
|
||||
case "paths-ignore":
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
|
||||
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
|
||||
if err != nil {
|
||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
|
||||
} else {
|
||||
|
||||
@@ -15,5 +15,5 @@ func TestPamAuth(t *testing.T) {
|
||||
result, err := Auth("gitea", "user1", "false-pwd")
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "Authentication failure")
|
||||
assert.Len(t, result)
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
|
||||
9
modules/cache/cache.go
vendored
9
modules/cache/cache.go
vendored
@@ -37,10 +37,15 @@ func Init() error {
|
||||
}
|
||||
|
||||
const (
|
||||
testCacheKey = "DefaultCache.TestKey"
|
||||
SlowCacheThreshold = 100 * time.Microsecond
|
||||
testCacheKey = "DefaultCache.TestKey"
|
||||
// SlowCacheThreshold marks cache tests as slow
|
||||
// set to 30ms per discussion: https://github.com/go-gitea/gitea/issues/33190
|
||||
// TODO: Replace with metrics histogram
|
||||
SlowCacheThreshold = 30 * time.Millisecond
|
||||
)
|
||||
|
||||
// Test performs delete, put and get operations on a predefined key
|
||||
// returns
|
||||
func Test() (time.Duration, error) {
|
||||
if defaultCache == nil {
|
||||
return 0, fmt.Errorf("default cache not initialized")
|
||||
|
||||
3
modules/cache/cache_test.go
vendored
3
modules/cache/cache_test.go
vendored
@@ -43,7 +43,8 @@ func TestTest(t *testing.T) {
|
||||
elapsed, err := Test()
|
||||
assert.NoError(t, err)
|
||||
// mem cache should take from 300ns up to 1ms on modern hardware ...
|
||||
assert.Less(t, elapsed, time.Millisecond)
|
||||
assert.Positive(t, elapsed)
|
||||
assert.Less(t, elapsed, SlowCacheThreshold)
|
||||
}
|
||||
|
||||
func TestGetCache(t *testing.T) {
|
||||
|
||||
@@ -46,6 +46,7 @@ type Command struct {
|
||||
desc string
|
||||
globalArgsLength int
|
||||
brokenArgs []string
|
||||
cmd *exec.Cmd // for debug purpose only
|
||||
}
|
||||
|
||||
func (c *Command) String() string {
|
||||
@@ -314,6 +315,7 @@ func (c *Command) Run(opts *RunOpts) error {
|
||||
startTime := time.Now()
|
||||
|
||||
cmd := exec.CommandContext(ctx, c.prog, c.args...)
|
||||
c.cmd = cmd // for debug purpose only
|
||||
if opts.Env == nil {
|
||||
cmd.Env = os.Environ()
|
||||
} else {
|
||||
@@ -348,9 +350,10 @@ func (c *Command) Run(opts *RunOpts) error {
|
||||
// We need to check if the context is canceled by the program on Windows.
|
||||
// This is because Windows does not have signal checking when terminating the process.
|
||||
// It always returns exit code 1, unlike Linux, which has many exit codes for signals.
|
||||
// `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled.
|
||||
if runtime.GOOS == "windows" &&
|
||||
err != nil &&
|
||||
err.Error() == "" &&
|
||||
(err.Error() == "" || err.Error() == "exit status 1") &&
|
||||
cmd.ProcessState.ExitCode() == 1 &&
|
||||
ctx.Err() == context.Canceled {
|
||||
return ctx.Err()
|
||||
|
||||
@@ -360,5 +360,5 @@ func Test_GetCommitBranchStart(t *testing.T) {
|
||||
startCommitID, err := repo.GetCommitBranchStart(os.Environ(), "branch1", commit.ID.String())
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, startCommitID)
|
||||
assert.EqualValues(t, "9c9aef8dd84e02bc7ec12641deb4c930a7c30185", startCommitID)
|
||||
assert.EqualValues(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ func TestRefName(t *testing.T) {
|
||||
|
||||
// Test pull names
|
||||
assert.Equal(t, "1", RefName("refs/pull/1/head").PullName())
|
||||
assert.True(t, RefName("refs/pull/1/head").IsPull())
|
||||
assert.True(t, RefName("refs/pull/1/merge").IsPull())
|
||||
assert.Equal(t, "my/pull", RefName("refs/pull/my/pull/head").PullName())
|
||||
|
||||
// Test for branch names
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
@@ -102,7 +104,7 @@ type CheckAttributeReader struct {
|
||||
|
||||
stdinReader io.ReadCloser
|
||||
stdinWriter *os.File
|
||||
stdOut attributeWriter
|
||||
stdOut *nulSeparatedAttributeWriter
|
||||
cmd *Command
|
||||
env []string
|
||||
ctx context.Context
|
||||
@@ -152,7 +154,6 @@ func (c *CheckAttributeReader) Init(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run run cmd
|
||||
func (c *CheckAttributeReader) Run() error {
|
||||
defer func() {
|
||||
_ = c.stdinReader.Close()
|
||||
@@ -176,7 +177,7 @@ func (c *CheckAttributeReader) Run() error {
|
||||
func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) {
|
||||
defer func() {
|
||||
if err != nil && err != c.ctx.Err() {
|
||||
log.Error("Unexpected error when checking path %s in %s. Error: %v", path, c.Repo.Path, err)
|
||||
log.Error("Unexpected error when checking path %s in %s, error: %v", path, filepath.Base(c.Repo.Path), err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -191,9 +192,31 @@ func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reportTimeout := func() error {
|
||||
stdOutClosed := false
|
||||
select {
|
||||
case <-c.stdOut.closed:
|
||||
stdOutClosed = true
|
||||
default:
|
||||
}
|
||||
debugMsg := fmt.Sprintf("check path %q in repo %q", path, filepath.Base(c.Repo.Path))
|
||||
debugMsg += fmt.Sprintf(", stdOut: tmp=%q, pos=%d, closed=%v", string(c.stdOut.tmp), c.stdOut.pos, stdOutClosed)
|
||||
if c.cmd.cmd != nil {
|
||||
debugMsg += fmt.Sprintf(", process state: %q", c.cmd.cmd.ProcessState.String())
|
||||
}
|
||||
_ = c.Close()
|
||||
return fmt.Errorf("CheckPath timeout: %s", debugMsg)
|
||||
}
|
||||
|
||||
rs = make(map[string]string)
|
||||
for range c.Attributes {
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
// There is a strange "hang" problem in gitdiff.GetDiff -> CheckPath
|
||||
// So add a timeout here to mitigate the problem, and output more logs for debug purpose
|
||||
// In real world, if CheckPath runs long than seconds, it blocks the end user's operation,
|
||||
// and at the moment the CheckPath result is not so important, so we can just ignore it.
|
||||
return nil, reportTimeout()
|
||||
case attr, ok := <-c.stdOut.ReadAttribute():
|
||||
if !ok {
|
||||
return nil, c.ctx.Err()
|
||||
@@ -206,18 +229,12 @@ func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
// Close close pip after use
|
||||
func (c *CheckAttributeReader) Close() error {
|
||||
c.cancel()
|
||||
err := c.stdinWriter.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
type attributeWriter interface {
|
||||
io.WriteCloser
|
||||
ReadAttribute() <-chan attributeTriple
|
||||
}
|
||||
|
||||
type attributeTriple struct {
|
||||
Filename string
|
||||
Attribute string
|
||||
@@ -263,7 +280,7 @@ func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
}
|
||||
wr.tmp = append(wr.tmp, p...)
|
||||
return len(p), nil
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
|
||||
@@ -281,7 +298,7 @@ func (wr *nulSeparatedAttributeWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a check attribute reader for the current repository and provided commit ID
|
||||
// CheckAttributeReader creates a check attribute reader for the current repository and provided commit ID
|
||||
func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) {
|
||||
indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
|
||||
if err != nil {
|
||||
@@ -303,21 +320,21 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe
|
||||
}
|
||||
ctx, cancel := context.WithCancel(repo.Ctx)
|
||||
if err := checker.Init(ctx); err != nil {
|
||||
log.Error("Unable to open checker for %s. Error: %v", commitID, err)
|
||||
log.Error("Unable to open attribute checker for commit %s, error: %v", commitID, err)
|
||||
} else {
|
||||
go func() {
|
||||
err := checker.Run()
|
||||
if err != nil && err != ctx.Err() {
|
||||
log.Error("Unable to open checker for %s. Error: %v", commitID, err)
|
||||
if err != nil && !IsErrCanceledOrKilled(err) {
|
||||
log.Error("Attribute checker for commit %s exits with error: %v", commitID, err)
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
}
|
||||
deferable := func() {
|
||||
deferrable := func() {
|
||||
_ = checker.Close()
|
||||
cancel()
|
||||
deleteTemporaryFile()
|
||||
}
|
||||
|
||||
return checker, deferable
|
||||
return checker, deferrable
|
||||
}
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
mathRand "math/rand/v2"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
|
||||
@@ -95,3 +101,57 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
|
||||
Value: "unspecified",
|
||||
}, attr)
|
||||
}
|
||||
|
||||
func TestAttributeReader(t *testing.T) {
|
||||
t.Skip() // for debug purpose only, do not run in CI
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
timeout := 1 * time.Second
|
||||
repoPath := filepath.Join(testReposDir, "language_stats_repo")
|
||||
commitRef := "HEAD"
|
||||
|
||||
oneRound := func(t *testing.T, roundIdx int) {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
_ = cancel
|
||||
gitRepo, err := OpenRepository(ctx, repoPath)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetCommit(commitRef)
|
||||
require.NoError(t, err)
|
||||
|
||||
files, err := gitRepo.LsFiles()
|
||||
require.NoError(t, err)
|
||||
|
||||
randomFiles := slices.Clone(files)
|
||||
randomFiles = append(randomFiles, "any-file-1", "any-file-2")
|
||||
|
||||
t.Logf("Round %v with %d files", roundIdx, len(randomFiles))
|
||||
|
||||
attrReader, deferrable := gitRepo.CheckAttributeReader(commit.ID.String())
|
||||
defer deferrable()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
file := randomFiles[mathRand.IntN(len(randomFiles))]
|
||||
_, err := attrReader.CheckPath(file)
|
||||
if err != nil {
|
||||
for i := 0; i < 10; i++ {
|
||||
_, _ = attrReader.CheckPath(file)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
oneRound(t, i)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,6 +519,7 @@ func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCommitBranchStart returns the commit where the branch diverged
|
||||
func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) {
|
||||
cmd := NewCommand(repo.Ctx, "log", prettyLogFormat)
|
||||
cmd.AddDynamicArguments(endCommitID)
|
||||
@@ -533,7 +534,8 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s
|
||||
|
||||
parts := bytes.Split(bytes.TrimSpace(stdout), []byte{'\n'})
|
||||
|
||||
var startCommitID string
|
||||
// check the commits one by one until we find a commit contained by another branch
|
||||
// and we think this commit is the divergence point
|
||||
for _, commitID := range parts {
|
||||
branches, err := repo.getBranches(env, string(commitID), 2)
|
||||
if err != nil {
|
||||
@@ -541,11 +543,9 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s
|
||||
}
|
||||
for _, b := range branches {
|
||||
if b != branch {
|
||||
return startCommitID, nil
|
||||
return string(commitID), nil
|
||||
}
|
||||
}
|
||||
|
||||
startCommitID = string(commitID)
|
||||
}
|
||||
|
||||
return "", nil
|
||||
|
||||
@@ -29,7 +29,7 @@ func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bo
|
||||
}
|
||||
|
||||
if len(branches) == 0 {
|
||||
graphCmd.AddArguments("--all")
|
||||
graphCmd.AddArguments("--tags", "--branches")
|
||||
}
|
||||
|
||||
graphCmd.AddArguments("-C", "-M", "--date=iso-strict").
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -99,10 +100,13 @@ func (r *Request) Param(key, value string) *Request {
|
||||
return r
|
||||
}
|
||||
|
||||
// Body adds request raw body.
|
||||
// it supports string and []byte.
|
||||
// Body adds request raw body. It supports string, []byte and io.Reader as body.
|
||||
func (r *Request) Body(data any) *Request {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
switch t := data.(type) {
|
||||
case nil: // do nothing
|
||||
case string:
|
||||
bf := bytes.NewBufferString(t)
|
||||
r.req.Body = io.NopCloser(bf)
|
||||
@@ -111,6 +115,12 @@ func (r *Request) Body(data any) *Request {
|
||||
bf := bytes.NewBuffer(t)
|
||||
r.req.Body = io.NopCloser(bf)
|
||||
r.req.ContentLength = int64(len(t))
|
||||
case io.ReadCloser:
|
||||
r.req.Body = t
|
||||
case io.Reader:
|
||||
r.req.Body = io.NopCloser(t)
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported request body type %T", t))
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -141,7 +151,7 @@ func (r *Request) getResponse() (*http.Response, error) {
|
||||
}
|
||||
} else if r.req.Method == "POST" && r.req.Body == nil && len(paramBody) > 0 {
|
||||
r.Header("Content-Type", "application/x-www-form-urlencoded")
|
||||
r.Body(paramBody)
|
||||
r.Body(paramBody) // string
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -185,7 +195,11 @@ func (r *Request) getResponse() (*http.Response, error) {
|
||||
}
|
||||
|
||||
// Response executes request client gets response manually.
|
||||
// Caller MUST close the response body if no error occurs
|
||||
func (r *Request) Response() (*http.Response, error) {
|
||||
if r == nil {
|
||||
return nil, errors.New("invalid request")
|
||||
}
|
||||
return r.getResponse()
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
analyzer_custom "github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
|
||||
analyzer_keyword "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/camelcase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/unicodenorm"
|
||||
"github.com/blevesearch/bleve/v2/analysis/tokenizer/letter"
|
||||
@@ -70,7 +69,7 @@ const (
|
||||
filenameIndexerAnalyzer = "filenameIndexerAnalyzer"
|
||||
filenameIndexerTokenizer = "filenameIndexerTokenizer"
|
||||
repoIndexerDocType = "repoIndexerDocType"
|
||||
repoIndexerLatestVersion = 8
|
||||
repoIndexerLatestVersion = 9
|
||||
)
|
||||
|
||||
// generateBleveIndexMapping generates a bleve index mapping for the repo indexer
|
||||
@@ -107,7 +106,7 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) {
|
||||
"type": analyzer_custom.Name,
|
||||
"char_filters": []string{},
|
||||
"tokenizer": letter.Name,
|
||||
"token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name},
|
||||
"token_filters": []string{unicodeNormalizeName, lowercase.Name},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -266,7 +265,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
|
||||
pathQuery.FieldVal = "Filename"
|
||||
pathQuery.SetBoost(10)
|
||||
|
||||
contentQuery := bleve.NewMatchQuery(opts.Keyword)
|
||||
contentQuery := bleve.NewMatchPhraseQuery(opts.Keyword)
|
||||
contentQuery.FieldVal = "Content"
|
||||
|
||||
if opts.IsKeywordFuzzy {
|
||||
|
||||
@@ -165,35 +165,6 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
|
||||
},
|
||||
},
|
||||
},
|
||||
// Search for matches on the contents of files within the repo '62'.
|
||||
// This scenario yields two results (both are based on contents, the first one is an exact match where as the second is a 'fuzzy' one)
|
||||
{
|
||||
RepoIDs: []int64{62},
|
||||
Keyword: "This is not cheese",
|
||||
Langs: 1,
|
||||
Results: []codeSearchResult{
|
||||
{
|
||||
Filename: "potato/ham.md",
|
||||
Content: "This is not cheese",
|
||||
},
|
||||
{
|
||||
Filename: "ham.md",
|
||||
Content: "This is also not cheese",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Search for matches on the contents of files regardless of case.
|
||||
{
|
||||
RepoIDs: nil,
|
||||
Keyword: "dESCRIPTION",
|
||||
Langs: 1,
|
||||
Results: []codeSearchResult{
|
||||
{
|
||||
Filename: "README.md",
|
||||
Content: "# repo1\n\nDescription for repo1",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Search for an exact match on the filename within the repo '62' (case insenstive).
|
||||
// This scenario yields a single result (the file avocado.md on the repo '62')
|
||||
{
|
||||
@@ -233,6 +204,47 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
|
||||
},
|
||||
}
|
||||
|
||||
if name == "elastic_search" {
|
||||
// Additional scenarios for elastic_search only
|
||||
additional := []struct {
|
||||
RepoIDs []int64
|
||||
Keyword string
|
||||
Langs int
|
||||
Results []codeSearchResult
|
||||
}{
|
||||
// Search for matches on the contents of files within the repo '62'.
|
||||
// This scenario yields two results (both are based on contents, the first one is an exact match where as the second is a 'fuzzy' one)
|
||||
{
|
||||
RepoIDs: []int64{62},
|
||||
Keyword: "This is not cheese",
|
||||
Langs: 1,
|
||||
Results: []codeSearchResult{
|
||||
{
|
||||
Filename: "potato/ham.md",
|
||||
Content: "This is not cheese",
|
||||
},
|
||||
{
|
||||
Filename: "ham.md",
|
||||
Content: "This is also not cheese",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Search for matches on the contents of files regardless of case.
|
||||
{
|
||||
RepoIDs: nil,
|
||||
Keyword: "dESCRIPTION",
|
||||
Langs: 1,
|
||||
Results: []codeSearchResult{
|
||||
{
|
||||
Filename: "README.md",
|
||||
Content: "# repo1\n\nDescription for repo1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
keywords = append(keywords, additional...)
|
||||
}
|
||||
|
||||
for _, kw := range keywords {
|
||||
t.Run(kw.Keyword, func(t *testing.T) {
|
||||
total, res, langs, err := indexer.Search(context.TODO(), &internal.SearchOptions{
|
||||
|
||||
@@ -73,9 +73,9 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
||||
UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(),
|
||||
PriorityRepoID: 0,
|
||||
IsArchived: options.IsArchived,
|
||||
Org: nil,
|
||||
Owner: nil,
|
||||
Team: nil,
|
||||
User: nil,
|
||||
Doer: nil,
|
||||
}
|
||||
|
||||
if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 {
|
||||
|
||||
@@ -92,6 +92,11 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
|
||||
projectID = issue.Project.ID
|
||||
}
|
||||
|
||||
projectColumnID, err := issue.ProjectColumnID(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return &internal.IndexerData{
|
||||
ID: issue.ID,
|
||||
RepoID: issue.RepoID,
|
||||
@@ -106,7 +111,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
|
||||
NoLabel: len(labels) == 0,
|
||||
MilestoneID: issue.MilestoneID,
|
||||
ProjectID: projectID,
|
||||
ProjectColumnID: issue.ProjectColumnID(ctx),
|
||||
ProjectColumnID: projectColumnID,
|
||||
PosterID: issue.PosterID,
|
||||
AssigneeID: issue.AssigneeID,
|
||||
MentionIDs: mentionIDs,
|
||||
|
||||
@@ -72,10 +72,14 @@ func (c *HTTPClient) batch(ctx context.Context, operation string, objects []Poin
|
||||
|
||||
url := fmt.Sprintf("%s/objects/batch", c.endpoint)
|
||||
|
||||
// Original: In some lfs server implementations, they require the ref attribute. #32838
|
||||
// `ref` is an "optional object describing the server ref that the objects belong to"
|
||||
// but some (incorrect) lfs servers require it, so maybe adding an empty ref here doesn't break the correct ones.
|
||||
// but some (incorrect) lfs servers like aliyun require it, so maybe adding an empty ref here doesn't break the correct ones.
|
||||
// https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37
|
||||
request := &BatchRequest{operation, c.transferNames(), &Reference{}, objects}
|
||||
//
|
||||
// UPDATE: it can't use "empty ref" here because it breaks others like https://github.com/go-gitea/gitea/issues/33453
|
||||
request := &BatchRequest{operation, c.transferNames(), nil, objects}
|
||||
|
||||
payload := new(bytes.Buffer)
|
||||
err := json.NewEncoder(payload).Encode(request)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
@@ -29,7 +28,7 @@ var Capabilities = []string{
|
||||
"locking",
|
||||
}
|
||||
|
||||
var _ transfer.Backend = &GiteaBackend{}
|
||||
var _ transfer.Backend = (*GiteaBackend)(nil)
|
||||
|
||||
// GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API
|
||||
type GiteaBackend struct {
|
||||
@@ -71,24 +70,23 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
|
||||
g.logger.Log("json marshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
url := g.server.JoinPath("objects/batch").String()
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
|
||||
req := newInternalRequestLFS(g.ctx, g.server.JoinPath("objects/batch").String(), http.MethodPost, headers, bodyBytes)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
g.logger.Log("http request error", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
|
||||
return nil, statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
g.logger.Log("http read error", err)
|
||||
@@ -158,8 +156,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
|
||||
return pointers, nil
|
||||
}
|
||||
|
||||
// Download implements transfer.Backend. The returned reader must be closed by the
|
||||
// caller.
|
||||
// Download implements transfer.Backend. The returned reader must be closed by the caller.
|
||||
func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, int64, error) {
|
||||
idMapStr, exists := args[argID]
|
||||
if !exists {
|
||||
@@ -181,31 +178,30 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser,
|
||||
g.logger.Log("argument id incorrect")
|
||||
return nil, 0, transfer.ErrCorruptData
|
||||
}
|
||||
url := action.Href
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeOctetStream,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
|
||||
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodGet, headers, nil)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, 0, fmt.Errorf("failed to get response: %w", err)
|
||||
}
|
||||
// no need to close the body here by "defer resp.Body.Close()", see below
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, 0, statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
|
||||
respSize, err := strconv.ParseInt(resp.Header.Get("X-Gitea-LFS-Content-Length"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, 0, fmt.Errorf("failed to parse content length: %w", err)
|
||||
}
|
||||
respSize := int64(len(respBytes))
|
||||
respBuf := io.NopCloser(bytes.NewBuffer(respBytes))
|
||||
return respBuf, respSize, nil
|
||||
// transfer.Backend will check io.Closer interface and close this Body reader
|
||||
return resp.Body, respSize, nil
|
||||
}
|
||||
|
||||
// StartUpload implements transfer.Backend.
|
||||
// Upload implements transfer.Backend.
|
||||
func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer.Args) error {
|
||||
idMapStr, exists := args[argID]
|
||||
if !exists {
|
||||
@@ -227,22 +223,20 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer
|
||||
g.logger.Log("argument id incorrect")
|
||||
return transfer.ErrCorruptData
|
||||
}
|
||||
url := action.Href
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerContentType: mimeOctetStream,
|
||||
headerContentLength: strconv.FormatInt(size, 10),
|
||||
}
|
||||
reqBytes, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, http.MethodPut, headers, reqBytes)
|
||||
|
||||
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPut, headers, nil)
|
||||
req.Body(r)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
@@ -277,18 +271,18 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans
|
||||
// the server sent no verify action
|
||||
return transfer.SuccessStatus(), nil
|
||||
}
|
||||
url := action.Href
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
|
||||
req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPost, headers, bodyBytes)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
return transfer.NewStatus(transfer.StatusInternalServerError), err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return transfer.NewStatus(uint32(resp.StatusCode), http.StatusText(resp.StatusCode)), statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -43,14 +43,13 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) {
|
||||
g.logger.Log("json marshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
url := g.server.String()
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
|
||||
req := newInternalRequestLFS(g.ctx, g.server.String(), http.MethodPost, headers, bodyBytes)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
g.logger.Log("http request error", err)
|
||||
@@ -95,14 +94,13 @@ func (g *giteaLockBackend) Unlock(lock transfer.Lock) error {
|
||||
g.logger.Log("json marshal error", err)
|
||||
return err
|
||||
}
|
||||
url := g.server.JoinPath(lock.ID(), "unlock").String()
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
|
||||
req := newInternalRequestLFS(g.ctx, g.server.JoinPath(lock.ID(), "unlock").String(), http.MethodPost, headers, bodyBytes)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
g.logger.Log("http request error", err)
|
||||
@@ -176,16 +174,15 @@ func (g *giteaLockBackend) Range(cursor string, limit int, iter func(transfer.Lo
|
||||
}
|
||||
|
||||
func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, error) {
|
||||
urlq := g.server.JoinPath() // get a copy
|
||||
urlq.RawQuery = v.Encode()
|
||||
url := urlq.String()
|
||||
serverURLWithQuery := g.server.JoinPath() // get a copy
|
||||
serverURLWithQuery.RawQuery = v.Encode()
|
||||
headers := map[string]string{
|
||||
headerAuthorization: g.authToken,
|
||||
headerGiteaInternalAuth: g.internalAuth,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
|
||||
req := newInternalRequestLFS(g.ctx, serverURLWithQuery.String(), http.MethodGet, headers, nil)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
g.logger.Log("http request error", err)
|
||||
|
||||
@@ -5,15 +5,16 @@ package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/proxyprotocol"
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
||||
)
|
||||
@@ -60,8 +61,7 @@ const (
|
||||
|
||||
// Operations enum
|
||||
const (
|
||||
opNone = iota
|
||||
opDownload
|
||||
opDownload = iota + 1
|
||||
opUpload
|
||||
)
|
||||
|
||||
@@ -89,53 +89,60 @@ func statusCodeToErr(code int) error {
|
||||
}
|
||||
}
|
||||
|
||||
func newInternalRequest(ctx context.Context, url, method string, headers map[string]string, body []byte) *httplib.Request {
|
||||
req := httplib.NewRequest(url, method).
|
||||
SetContext(ctx).
|
||||
SetTimeout(10*time.Second, 60*time.Second).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
|
||||
if setting.Protocol == setting.HTTPUnix {
|
||||
req.SetTransport(&http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, "unix", setting.HTTPAddr)
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
if setting.LocalUseProxyProtocol {
|
||||
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return conn, err
|
||||
},
|
||||
})
|
||||
} else if setting.LocalUseProxyProtocol {
|
||||
req.SetTransport(&http.Transport{
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, err
|
||||
},
|
||||
})
|
||||
func toInternalLFSURL(s string) string {
|
||||
pos1 := strings.Index(s, "://")
|
||||
if pos1 == -1 {
|
||||
return ""
|
||||
}
|
||||
appSubURLWithSlash := setting.AppSubURL + "/"
|
||||
pos2 := strings.Index(s[pos1+3:], appSubURLWithSlash)
|
||||
if pos2 == -1 {
|
||||
return ""
|
||||
}
|
||||
routePath := s[pos1+3+pos2+len(appSubURLWithSlash):]
|
||||
fields := strings.SplitN(routePath, "/", 3)
|
||||
if len(fields) < 3 || !strings.HasPrefix(fields[2], "info/lfs") {
|
||||
return ""
|
||||
}
|
||||
return setting.LocalURL + "api/internal/repo/" + routePath
|
||||
}
|
||||
|
||||
func isInternalLFSURL(s string) bool {
|
||||
if !strings.HasPrefix(s, setting.LocalURL) {
|
||||
return false
|
||||
}
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
routePath := util.PathJoinRelX(u.Path)
|
||||
subRoutePath, cut := strings.CutPrefix(routePath, "api/internal/repo/")
|
||||
if !cut {
|
||||
return false
|
||||
}
|
||||
fields := strings.SplitN(subRoutePath, "/", 3)
|
||||
if len(fields) < 3 || !strings.HasPrefix(fields[2], "info/lfs") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func newInternalRequestLFS(ctx context.Context, internalURL, method string, headers map[string]string, body any) *httplib.Request {
|
||||
if !isInternalLFSURL(internalURL) {
|
||||
return nil
|
||||
}
|
||||
req := private.NewInternalRequest(ctx, internalURL, method)
|
||||
for k, v := range headers {
|
||||
req.Header(k, v)
|
||||
}
|
||||
|
||||
req.Body(body)
|
||||
|
||||
switch body := body.(type) {
|
||||
case nil: // do nothing
|
||||
case []byte:
|
||||
req.Body(body) // []byte
|
||||
case io.Reader:
|
||||
req.Body(body) // io.Reader or io.ReadCloser
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported request body type %T", body))
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
54
modules/lfstransfer/backend/util_test.go
Normal file
54
modules/lfstransfer/backend/util_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestToInternalLFSURL(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.LocalURL, "http://localurl/")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
|
||||
cases := []struct {
|
||||
url string
|
||||
expected string
|
||||
}{
|
||||
{"http://appurl/any", ""},
|
||||
{"http://appurl/sub/any", ""},
|
||||
{"http://appurl/sub/owner/repo/any", ""},
|
||||
{"http://appurl/sub/owner/repo/info/any", ""},
|
||||
{"http://appurl/sub/owner/repo/info/lfs/any", "http://localurl/api/internal/repo/owner/repo/info/lfs/any"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.expected, toInternalLFSURL(c.url), c.url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsInternalLFSURL(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.LocalURL, "http://localurl/")()
|
||||
defer test.MockVariableValue(&setting.InternalToken, "mock-token")()
|
||||
cases := []struct {
|
||||
url string
|
||||
expected bool
|
||||
}{
|
||||
{"", false},
|
||||
{"http://otherurl/api/internal/repo/owner/repo/info/lfs/any", false},
|
||||
{"http://localurl/api/internal/repo/owner/repo/info/lfs/any", true},
|
||||
{"http://localurl/api/internal/repo/owner/repo/info", false},
|
||||
{"http://localurl/api/internal/misc/owner/repo/info/lfs/any", false},
|
||||
{"http://localurl/api/internal/owner/repo/info/lfs/any", false},
|
||||
{"http://localurl/api/internal/foo/bar", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
req := newInternalRequestLFS(context.Background(), c.url, "GET", nil, nil)
|
||||
assert.Equal(t, c.expected, req != nil, c.url)
|
||||
assert.Equal(t, c.expected, isInternalLFSURL(c.url), c.url)
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ type GlobalVarsType struct {
|
||||
LinkRegex *regexp.Regexp // fast matching a URL link, no any extra validation.
|
||||
}
|
||||
|
||||
var GlobalVars = sync.OnceValue[*GlobalVarsType](func() *GlobalVarsType {
|
||||
var GlobalVars = sync.OnceValue(func() *GlobalVarsType {
|
||||
v := &GlobalVarsType{}
|
||||
v.wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`)
|
||||
v.LinkRegex, _ = xurls.StrictMatchingScheme("https?://")
|
||||
|
||||
@@ -42,7 +42,7 @@ type globalVarsType struct {
|
||||
nulCleaner *strings.Replacer
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue[*globalVarsType](func() *globalVarsType {
|
||||
var globalVars = sync.OnceValue(func() *globalVarsType {
|
||||
v := &globalVarsType{}
|
||||
// NOTE: All below regex matching do not perform any extra validation.
|
||||
// Thus a link is produced even if the linked entity does not exist.
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
var reAttrClass = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp {
|
||||
var reAttrClass = sync.OnceValue(func() *regexp.Regexp {
|
||||
// TODO: it isn't a problem at the moment because our HTML contents are always well constructed
|
||||
return regexp.MustCompile(`(<[^>]+)\s+class="([^"]+)"([^>]*>)`)
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strconv"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
@@ -29,9 +30,7 @@ func (n *Details) Kind() ast.NodeKind {
|
||||
|
||||
// NewDetails returns a new Paragraph node.
|
||||
func NewDetails() *Details {
|
||||
return &Details{
|
||||
BaseBlock: ast.BaseBlock{},
|
||||
}
|
||||
return &Details{}
|
||||
}
|
||||
|
||||
// Summary is a block that contains the summary of details block
|
||||
@@ -54,9 +53,7 @@ func (n *Summary) Kind() ast.NodeKind {
|
||||
|
||||
// NewSummary returns a new Summary node.
|
||||
func NewSummary() *Summary {
|
||||
return &Summary{
|
||||
BaseBlock: ast.BaseBlock{},
|
||||
}
|
||||
return &Summary{}
|
||||
}
|
||||
|
||||
// TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox
|
||||
@@ -95,29 +92,6 @@ type Icon struct {
|
||||
Name []byte
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump .
|
||||
func (n *Icon) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["Name"] = string(n.Name)
|
||||
ast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindIcon is the NodeKind for Icon
|
||||
var KindIcon = ast.NewNodeKind("Icon")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *Icon) Kind() ast.NodeKind {
|
||||
return KindIcon
|
||||
}
|
||||
|
||||
// NewIcon returns a new Paragraph node.
|
||||
func NewIcon(name string) *Icon {
|
||||
return &Icon{
|
||||
BaseInline: ast.BaseInline{},
|
||||
Name: []byte(name),
|
||||
}
|
||||
}
|
||||
|
||||
// ColorPreview is an inline for a color preview
|
||||
type ColorPreview struct {
|
||||
ast.BaseInline
|
||||
@@ -175,3 +149,24 @@ func NewAttention(attentionType string) *Attention {
|
||||
AttentionType: attentionType,
|
||||
}
|
||||
}
|
||||
|
||||
var KindRawHTML = ast.NewNodeKind("RawHTML")
|
||||
|
||||
type RawHTML struct {
|
||||
ast.BaseBlock
|
||||
rawHTML template.HTML
|
||||
}
|
||||
|
||||
func (n *RawHTML) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["RawHTML"] = string(n.rawHTML)
|
||||
ast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
func (n *RawHTML) Kind() ast.NodeKind {
|
||||
return KindRawHTML
|
||||
}
|
||||
|
||||
func NewRawHTML(rawHTML template.HTML) *RawHTML {
|
||||
return &RawHTML{rawHTML: rawHTML}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,22 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
east "github.com/yuin/goldmark/extension/ast"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func nodeToTable(meta *yaml.Node) ast.Node {
|
||||
for {
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
switch meta.Kind {
|
||||
case yaml.DocumentNode:
|
||||
meta = meta.Content[0]
|
||||
continue
|
||||
default:
|
||||
}
|
||||
break
|
||||
for meta != nil && meta.Kind == yaml.DocumentNode {
|
||||
meta = meta.Content[0]
|
||||
}
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
switch meta.Kind {
|
||||
case yaml.MappingNode:
|
||||
@@ -72,12 +71,28 @@ func sequenceNodeToTable(meta *yaml.Node) ast.Node {
|
||||
return table
|
||||
}
|
||||
|
||||
func nodeToDetails(meta *yaml.Node, icon string) ast.Node {
|
||||
func nodeToDetails(g *ASTTransformer, meta *yaml.Node) ast.Node {
|
||||
for meta != nil && meta.Kind == yaml.DocumentNode {
|
||||
meta = meta.Content[0]
|
||||
}
|
||||
if meta == nil {
|
||||
return nil
|
||||
}
|
||||
if meta.Kind != yaml.MappingNode {
|
||||
return nil
|
||||
}
|
||||
var keys []string
|
||||
for i := 0; i < len(meta.Content); i += 2 {
|
||||
if meta.Content[i].Kind == yaml.ScalarNode {
|
||||
keys = append(keys, meta.Content[i].Value)
|
||||
}
|
||||
}
|
||||
details := NewDetails()
|
||||
details.SetAttributeString(g.renderInternal.SafeAttr("class"), g.renderInternal.SafeValue("frontmatter-content"))
|
||||
summary := NewSummary()
|
||||
summary.AppendChild(summary, NewIcon(icon))
|
||||
summaryInnerHTML := htmlutil.HTMLFormat("%s %s", svg.RenderHTML("octicon-table", 12), strings.Join(keys, ", "))
|
||||
summary.AppendChild(summary, NewRawHTML(summaryInnerHTML))
|
||||
details.AppendChild(details, summary)
|
||||
details.AppendChild(details, nodeToTable(meta))
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
@@ -51,7 +48,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
|
||||
tocList := make([]Header, 0, 20)
|
||||
if rc.yamlNode != nil {
|
||||
metaNode := rc.toMetaNode()
|
||||
metaNode := rc.toMetaNode(g)
|
||||
if metaNode != nil {
|
||||
node.InsertBefore(node, firstChild, metaNode)
|
||||
}
|
||||
@@ -111,11 +108,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
}
|
||||
}
|
||||
|
||||
// it is copied from old code, which is quite doubtful whether it is correct
|
||||
var reValidIconName = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
|
||||
})
|
||||
|
||||
// NewHTMLRenderer creates a HTMLRenderer to render in the gitea form.
|
||||
func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer {
|
||||
r := &HTMLRenderer{
|
||||
@@ -140,11 +132,11 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindDocument, r.renderDocument)
|
||||
reg.Register(KindDetails, r.renderDetails)
|
||||
reg.Register(KindSummary, r.renderSummary)
|
||||
reg.Register(KindIcon, r.renderIcon)
|
||||
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
||||
reg.Register(KindAttention, r.renderAttention)
|
||||
reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
|
||||
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||
reg.Register(KindRawHTML, r.renderRawHTML)
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
@@ -206,30 +198,14 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
func (r *HTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
n := node.(*Icon)
|
||||
|
||||
name := strings.TrimSpace(strings.ToLower(string(n.Name)))
|
||||
|
||||
if len(name) == 0 {
|
||||
// skip this
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
if !reValidIconName().MatchString(name) {
|
||||
// skip this
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// FIXME: the "icon xxx" is from Fomantic UI, it's really questionable whether it still works correctly
|
||||
err := r.renderInternal.FormatWithSafeAttrs(w, `<i class="icon %s"></i>`, name)
|
||||
n := node.(*RawHTML)
|
||||
_, err := w.WriteString(string(r.renderInternal.ProtectSafeAttrs(n.rawHTML)))
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
@@ -159,21 +159,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||
limit: setting.UI.MaxDisplayFileSize * 3,
|
||||
}
|
||||
|
||||
// FIXME: should we include a timeout to abort the renderer if it takes too long?
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Warn("Unable to render markdown due to panic in goldmark: %v", err)
|
||||
if (!setting.IsProd && !setting.IsInTesting) || log.IsDebug() {
|
||||
log.Error("Panic in markdown: %v\n%s", err, log.Stack(2))
|
||||
}
|
||||
}()
|
||||
|
||||
// FIXME: Don't read all to memory, but goldmark doesn't support
|
||||
pc := newParserContext(ctx)
|
||||
buf, err := io.ReadAll(input)
|
||||
if err != nil {
|
||||
log.Error("Unable to ReadAll: %v", err)
|
||||
@@ -181,14 +167,24 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||
}
|
||||
buf = giteautil.NormalizeEOL(buf)
|
||||
|
||||
// FIXME: should we include a timeout to abort the renderer if it takes too long?
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Error("Panic in markdown: %v\n%s", err, log.Stack(2))
|
||||
escapedHTML := template.HTMLEscapeString(giteautil.UnsafeBytesToString(buf))
|
||||
_, _ = output.Write(giteautil.UnsafeStringToBytes(escapedHTML))
|
||||
}()
|
||||
|
||||
pc := newParserContext(ctx)
|
||||
|
||||
// Preserve original length.
|
||||
bufWithMetadataLength := len(buf)
|
||||
|
||||
rc := &RenderConfig{
|
||||
Meta: markup.RenderMetaAsDetails,
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}
|
||||
rc := &RenderConfig{Meta: markup.RenderMetaAsDetails}
|
||||
buf, _ = ExtractMetadataBytes(buf, rc)
|
||||
|
||||
metaLength := bufWithMetadataLength - len(buf)
|
||||
|
||||
@@ -23,6 +23,11 @@ func TestAttention(t *testing.T) {
|
||||
defer svg.MockIcon("octicon-alert")()
|
||||
defer svg.MockIcon("octicon-stop")()
|
||||
|
||||
test := func(input, expected string) {
|
||||
result, err := markdown.RenderString(markup.NewTestRenderContext(), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result)))
|
||||
}
|
||||
renderAttention := func(attention, icon string) string {
|
||||
tmpl := `<blockquote class="attention-header attention-{attention}"><p><svg class="attention-icon attention-{attention} svg {icon}" width="16" height="16"></svg><strong class="attention-{attention}">{Attention}</strong></p>`
|
||||
tmpl = strings.ReplaceAll(tmpl, "{attention}", attention)
|
||||
@@ -31,12 +36,6 @@ func TestAttention(t *testing.T) {
|
||||
return tmpl
|
||||
}
|
||||
|
||||
test := func(input, expected string) {
|
||||
result, err := markdown.RenderString(markup.NewTestRenderContext(), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result)))
|
||||
}
|
||||
|
||||
test(`
|
||||
> [!NOTE]
|
||||
> text
|
||||
@@ -53,4 +52,7 @@ func TestAttention(t *testing.T) {
|
||||
|
||||
// legacy GitHub style
|
||||
test(`> **warning**`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
|
||||
|
||||
// edge case (it used to cause panic)
|
||||
test(">\ntext", "<blockquote>\n</blockquote>\n<p>text</p>")
|
||||
}
|
||||
|
||||
@@ -383,18 +383,74 @@ func TestColorPreview(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskList(t *testing.T) {
|
||||
func TestMarkdownFrontmatter(t *testing.T) {
|
||||
testcases := []struct {
|
||||
testcase string
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"MapInFrontmatter",
|
||||
`---
|
||||
key1: val1
|
||||
key2: val2
|
||||
---
|
||||
test
|
||||
`,
|
||||
`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> key1, key2</summary><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>key1</th>
|
||||
<th>key2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>val1</td>
|
||||
<td>val2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details><p>test</p>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
"ListInFrontmatter",
|
||||
`---
|
||||
- item1
|
||||
- item2
|
||||
---
|
||||
test
|
||||
`,
|
||||
`- item1
|
||||
- item2
|
||||
|
||||
<p>test</p>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
"StringInFrontmatter",
|
||||
`---
|
||||
anything
|
||||
---
|
||||
test
|
||||
`,
|
||||
`anything
|
||||
|
||||
<p>test</p>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
// data-source-position should take into account YAML frontmatter.
|
||||
"ListAfterFrontmatter",
|
||||
`---
|
||||
foo: bar
|
||||
---
|
||||
- [ ] task 1`,
|
||||
`<details><summary><i class="icon table"></i></summary><table>
|
||||
`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> foo</summary><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>foo</th>
|
||||
@@ -414,9 +470,9 @@ foo: bar
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
|
||||
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.input)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.name)
|
||||
assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
// RenderConfig represents rendering configuration for this file
|
||||
type RenderConfig struct {
|
||||
Meta markup.RenderMetaMode
|
||||
Icon string
|
||||
TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view
|
||||
Lang string
|
||||
yamlNode *yaml.Node
|
||||
@@ -74,7 +73,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||
|
||||
type yamlRenderConfig struct {
|
||||
Meta *string `yaml:"meta"`
|
||||
Icon *string `yaml:"details_icon"`
|
||||
Icon *string `yaml:"details_icon"` // deprecated, because there is no font icon, so no custom icon
|
||||
TOC *string `yaml:"include_toc"`
|
||||
Lang *string `yaml:"lang"`
|
||||
}
|
||||
@@ -96,10 +95,6 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||
rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta)
|
||||
}
|
||||
|
||||
if cfg.Gitea.Icon != nil {
|
||||
rc.Icon = strings.TrimSpace(strings.ToLower(*cfg.Gitea.Icon))
|
||||
}
|
||||
|
||||
if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" {
|
||||
rc.Lang = *cfg.Gitea.Lang
|
||||
}
|
||||
@@ -111,7 +106,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *RenderConfig) toMetaNode() ast.Node {
|
||||
func (rc *RenderConfig) toMetaNode(g *ASTTransformer) ast.Node {
|
||||
if rc.yamlNode == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -119,7 +114,7 @@ func (rc *RenderConfig) toMetaNode() ast.Node {
|
||||
case markup.RenderMetaAsTable:
|
||||
return nodeToTable(rc.yamlNode)
|
||||
case markup.RenderMetaAsDetails:
|
||||
return nodeToDetails(rc.yamlNode, rc.Icon)
|
||||
return nodeToDetails(g, rc.yamlNode)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -19,42 +20,36 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"empty", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "",
|
||||
},
|
||||
{
|
||||
"lang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "test",
|
||||
}, "lang: test",
|
||||
},
|
||||
{
|
||||
"metatable", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: table",
|
||||
},
|
||||
{
|
||||
"metanone", &RenderConfig{
|
||||
Meta: "none",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: none",
|
||||
},
|
||||
{
|
||||
"metadetails", &RenderConfig{
|
||||
Meta: "details",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: details",
|
||||
},
|
||||
{
|
||||
"metawrong", &RenderConfig{
|
||||
Meta: "details",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "gitea: wrong",
|
||||
},
|
||||
@@ -62,7 +57,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
"toc", &RenderConfig{
|
||||
TOC: "true",
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "include_toc: true",
|
||||
},
|
||||
@@ -70,14 +64,12 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
"tocfalse", &RenderConfig{
|
||||
TOC: "false",
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}, "include_toc: false",
|
||||
},
|
||||
{
|
||||
"toclang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
TOC: "true",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
@@ -88,7 +80,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"complexlang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
gitea:
|
||||
@@ -98,7 +89,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"complexlang2", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
lang: notright
|
||||
@@ -109,7 +99,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
{
|
||||
"complexlang", &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "testlang",
|
||||
}, `
|
||||
gitea:
|
||||
@@ -121,7 +110,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
Lang: "two",
|
||||
Meta: "table",
|
||||
TOC: "true",
|
||||
Icon: "smiley",
|
||||
}, `
|
||||
lang: one
|
||||
include_toc: true
|
||||
@@ -137,7 +125,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := &RenderConfig{
|
||||
Meta: "table",
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}
|
||||
if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", " ")), got); err != nil {
|
||||
@@ -145,18 +132,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
if got.Meta != tt.expected.Meta {
|
||||
t.Errorf("Meta Expected %s Got %s", tt.expected.Meta, got.Meta)
|
||||
}
|
||||
if got.Icon != tt.expected.Icon {
|
||||
t.Errorf("Icon Expected %s Got %s", tt.expected.Icon, got.Icon)
|
||||
}
|
||||
if got.Lang != tt.expected.Lang {
|
||||
t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang)
|
||||
}
|
||||
if got.TOC != tt.expected.TOC {
|
||||
t.Errorf("TOC Expected %q Got %q", tt.expected.TOC, got.TOC)
|
||||
}
|
||||
assert.Equal(t, tt.expected.Meta, got.Meta)
|
||||
assert.Equal(t, tt.expected.Lang, got.Lang)
|
||||
assert.Equal(t, tt.expected.TOC, got.TOC)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,9 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
|
||||
|
||||
// grab these nodes and make sure we adhere to the attention blockquote structure
|
||||
firstParagraph := v.FirstChild()
|
||||
if firstParagraph == nil {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
g.applyElementDir(firstParagraph)
|
||||
|
||||
attentionType, processedNodes := g.extractBlockquoteAttentionEmphasis(firstParagraph, reader)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup_test
|
||||
@@ -48,7 +48,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
||||
|
||||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
||||
policy.AllowStyles("color", "background-color").OnElements("div", "span", "p", "tr", "th", "td")
|
||||
|
||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ type GenerateTokenRequest struct {
|
||||
func GenerateActionsRunnerToken(ctx context.Context, scope string) (*ResponseText, ResponseExtra) {
|
||||
reqURL := setting.LocalURL + "api/internal/actions/generate_actions_runner_token"
|
||||
|
||||
req := newInternalRequest(ctx, reqURL, "POST", GenerateTokenRequest{
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", GenerateTokenRequest{
|
||||
Scope: scope,
|
||||
})
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
@@ -82,29 +82,32 @@ type HookProcReceiveRefResult struct {
|
||||
HeadBranch string
|
||||
}
|
||||
|
||||
func newInternalRequestAPIForHooks(ctx context.Context, hookName, ownerName, repoName string, opts HookOptions) *httplib.Request {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/%s/%s/%s", hookName, url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
|
||||
// This "timeout" applies to http.Client's timeout: A Timeout of zero means no timeout.
|
||||
// This "timeout" was previously set to `time.Duration(60+len(opts.OldCommitIDs))` seconds, but it caused unnecessary timeout failures.
|
||||
// It should be good enough to remove the client side timeout, only respect the "ctx" and server side timeout.
|
||||
req.SetReadWriteTimeout(0)
|
||||
return req
|
||||
}
|
||||
|
||||
// HookPreReceive check whether the provided commits are allowed
|
||||
func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||
req := newInternalRequest(ctx, reqURL, "POST", opts)
|
||||
req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
|
||||
req := newInternalRequestAPIForHooks(ctx, "pre-receive", ownerName, repoName, opts)
|
||||
_, extra := requestJSONResp(req, &ResponseText{})
|
||||
return extra
|
||||
}
|
||||
|
||||
// HookPostReceive updates services and users
|
||||
func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||
req := newInternalRequest(ctx, reqURL, "POST", opts)
|
||||
req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
|
||||
req := newInternalRequestAPIForHooks(ctx, "post-receive", ownerName, repoName, opts)
|
||||
return requestJSONResp(req, &HookPostReceiveResult{})
|
||||
}
|
||||
|
||||
// HookProcReceive proc-receive hook
|
||||
func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||
|
||||
req := newInternalRequest(ctx, reqURL, "POST", opts)
|
||||
req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
|
||||
req := newInternalRequestAPIForHooks(ctx, "proc-receive", ownerName, repoName, opts)
|
||||
return requestJSONResp(req, &HookProcReceiveResult{})
|
||||
}
|
||||
|
||||
@@ -115,7 +118,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R
|
||||
url.PathEscape(repoName),
|
||||
url.PathEscape(branch),
|
||||
)
|
||||
req := newInternalRequest(ctx, reqURL, "POST")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
_, extra := requestJSONResp(req, &ResponseText{})
|
||||
return extra
|
||||
}
|
||||
@@ -123,7 +126,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R
|
||||
// SSHLog sends ssh error log response
|
||||
func SSHLog(ctx context.Context, isErr bool, msg string) error {
|
||||
reqURL := setting.LocalURL + "api/internal/ssh/log"
|
||||
req := newInternalRequest(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg})
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg})
|
||||
_, extra := requestJSONResp(req, &ResponseText{})
|
||||
return extra.Error
|
||||
}
|
||||
|
||||
@@ -34,12 +34,16 @@ func getClientIP() string {
|
||||
return strings.Fields(sshConnEnv)[0]
|
||||
}
|
||||
|
||||
func newInternalRequest(ctx context.Context, url, method string, body ...any) *httplib.Request {
|
||||
func NewInternalRequest(ctx context.Context, url, method string) *httplib.Request {
|
||||
if setting.InternalToken == "" {
|
||||
log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q.
|
||||
Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(url, setting.LocalURL) {
|
||||
log.Fatal("Invalid internal request URL: %q", url)
|
||||
}
|
||||
|
||||
req := httplib.NewRequest(url, method).
|
||||
SetContext(ctx).
|
||||
Header("X-Real-IP", getClientIP()).
|
||||
@@ -82,13 +86,17 @@ Ensure you are running in the correct environment or set the correct configurati
|
||||
},
|
||||
})
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func newInternalRequestAPI(ctx context.Context, url, method string, body ...any) *httplib.Request {
|
||||
req := NewInternalRequest(ctx, url, method)
|
||||
if len(body) == 1 {
|
||||
req.Header("Content-Type", "application/json")
|
||||
jsonBytes, _ := json.Marshal(body[0])
|
||||
req.Body(jsonBytes)
|
||||
} else if len(body) > 1 {
|
||||
log.Fatal("Too many arguments for newInternalRequest")
|
||||
log.Fatal("Too many arguments for newInternalRequestAPI")
|
||||
}
|
||||
|
||||
req.SetTimeout(10*time.Second, 60*time.Second)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error {
|
||||
// Ask for running deliver hook and test pull request tasks.
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update/%d", keyID, repoID)
|
||||
req := newInternalRequest(ctx, reqURL, "POST")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
_, extra := requestJSONResp(req, &ResponseText{})
|
||||
return extra.Error
|
||||
}
|
||||
@@ -24,7 +24,7 @@ func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error {
|
||||
func AuthorizedPublicKeyByContent(ctx context.Context, content string) (*ResponseText, ResponseExtra) {
|
||||
// Ask for running deliver hook and test pull request tasks.
|
||||
reqURL := setting.LocalURL + "api/internal/ssh/authorized_keys"
|
||||
req := newInternalRequest(ctx, reqURL, "POST")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
req.Param("content", content)
|
||||
return requestJSONResp(req, &ResponseText{})
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ type Email struct {
|
||||
func SendEmail(ctx context.Context, subject, message string, to []string) (*ResponseText, ResponseExtra) {
|
||||
reqURL := setting.LocalURL + "api/internal/mail/send"
|
||||
|
||||
req := newInternalRequest(ctx, reqURL, "POST", Email{
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", Email{
|
||||
Subject: subject,
|
||||
Message: message,
|
||||
To: to,
|
||||
|
||||
@@ -18,21 +18,21 @@ import (
|
||||
// Shutdown calls the internal shutdown function
|
||||
func Shutdown(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/shutdown"
|
||||
req := newInternalRequest(ctx, reqURL, "POST")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Shutting down")
|
||||
}
|
||||
|
||||
// Restart calls the internal restart function
|
||||
func Restart(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/restart"
|
||||
req := newInternalRequest(ctx, reqURL, "POST")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Restarting")
|
||||
}
|
||||
|
||||
// ReloadTemplates calls the internal reload-templates function
|
||||
func ReloadTemplates(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/reload-templates"
|
||||
req := newInternalRequest(ctx, reqURL, "POST")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Reloaded")
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ type FlushOptions struct {
|
||||
// FlushQueues calls the internal flush-queues function
|
||||
func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/flush-queues"
|
||||
req := newInternalRequest(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking})
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking})
|
||||
if timeout > 0 {
|
||||
req.SetReadWriteTimeout(timeout + 10*time.Second)
|
||||
}
|
||||
@@ -55,28 +55,28 @@ func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) R
|
||||
// PauseLogging pauses logging
|
||||
func PauseLogging(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/pause-logging"
|
||||
req := newInternalRequest(ctx, reqURL, "POST")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Logging Paused")
|
||||
}
|
||||
|
||||
// ResumeLogging resumes logging
|
||||
func ResumeLogging(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/resume-logging"
|
||||
req := newInternalRequest(ctx, reqURL, "POST")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Logging Restarted")
|
||||
}
|
||||
|
||||
// ReleaseReopenLogging releases and reopens logging files
|
||||
func ReleaseReopenLogging(ctx context.Context) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/release-and-reopen-logging"
|
||||
req := newInternalRequest(ctx, reqURL, "POST")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Logging Restarted")
|
||||
}
|
||||
|
||||
// SetLogSQL sets database logging
|
||||
func SetLogSQL(ctx context.Context, on bool) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/set-log-sql?on=" + strconv.FormatBool(on)
|
||||
req := newInternalRequest(ctx, reqURL, "POST")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Log SQL setting set")
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ type LoggerOptions struct {
|
||||
// AddLogger adds a logger
|
||||
func AddLogger(ctx context.Context, logger, writer, mode string, config map[string]any) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/manager/add-logger"
|
||||
req := newInternalRequest(ctx, reqURL, "POST", LoggerOptions{
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", LoggerOptions{
|
||||
Logger: logger,
|
||||
Writer: writer,
|
||||
Mode: mode,
|
||||
@@ -103,7 +103,7 @@ func AddLogger(ctx context.Context, logger, writer, mode string, config map[stri
|
||||
// RemoveLogger removes a logger
|
||||
func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(logger), url.PathEscape(writer))
|
||||
req := newInternalRequest(ctx, reqURL, "POST")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST")
|
||||
return requestJSONClientMsg(req, "Removed")
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra {
|
||||
func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) ResponseExtra {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/processes?flat=%t&no-system=%t&stacktraces=%t&json=%t&cancel-pid=%s", flat, noSystem, stacktraces, json, url.QueryEscape(cancel))
|
||||
|
||||
req := newInternalRequest(ctx, reqURL, "GET")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "GET")
|
||||
callback := func(resp *http.Response, extra *ResponseExtra) {
|
||||
_, extra.Error = io.Copy(out, resp.Body)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type RestoreParams struct {
|
||||
func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) ResponseExtra {
|
||||
reqURL := setting.LocalURL + "api/internal/restore_repo"
|
||||
|
||||
req := newInternalRequest(ctx, reqURL, "POST", RestoreParams{
|
||||
req := newInternalRequestAPI(ctx, reqURL, "POST", RestoreParams{
|
||||
RepoDir: repoDir,
|
||||
OwnerName: ownerName,
|
||||
RepoName: repoName,
|
||||
|
||||
@@ -23,7 +23,7 @@ type KeyAndOwner struct {
|
||||
// ServNoCommand returns information about the provided key
|
||||
func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey, *user_model.User, error) {
|
||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", keyID)
|
||||
req := newInternalRequest(ctx, reqURL, "GET")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "GET")
|
||||
keyAndOwner, extra := requestJSONResp(req, &KeyAndOwner{})
|
||||
if extra.HasError() {
|
||||
return nil, nil, extra.Error
|
||||
@@ -58,6 +58,6 @@ func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, m
|
||||
reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb))
|
||||
}
|
||||
}
|
||||
req := newInternalRequest(ctx, reqURL, "GET")
|
||||
req := newInternalRequestAPI(ctx, reqURL, "GET")
|
||||
return requestJSONResp(req, &ServCommandResults{})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
@@ -52,9 +51,6 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
||||
{
|
||||
branches, _, err := gitRepo.GetBranchNames(0, 0)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "ref file is empty") {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches)
|
||||
|
||||
@@ -26,6 +26,7 @@ type ConfigKey interface {
|
||||
In(defaultVal string, candidates []string) string
|
||||
String() string
|
||||
Strings(delim string) []string
|
||||
Bool() (bool, error)
|
||||
|
||||
MustString(defaultVal string) string
|
||||
MustBool(defaultVal ...bool) bool
|
||||
|
||||
@@ -169,20 +169,24 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
|
||||
HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
|
||||
|
||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
||||
// if these are removed, the warning will not be shown
|
||||
if sec.HasKey("ENABLE_ACME") {
|
||||
EnableAcme = sec.Key("ENABLE_ACME").MustBool(false)
|
||||
} else {
|
||||
deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0")
|
||||
EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
|
||||
}
|
||||
|
||||
Protocol = HTTP
|
||||
protocolCfg := sec.Key("PROTOCOL").String()
|
||||
if protocolCfg != "https" && EnableAcme {
|
||||
log.Fatal("ACME could only be used with HTTPS protocol")
|
||||
}
|
||||
|
||||
switch protocolCfg {
|
||||
case "https":
|
||||
Protocol = HTTPS
|
||||
|
||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
||||
// if these are removed, the warning will not be shown
|
||||
if sec.HasKey("ENABLE_ACME") {
|
||||
EnableAcme = sec.Key("ENABLE_ACME").MustBool(false)
|
||||
} else {
|
||||
deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0")
|
||||
EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
|
||||
}
|
||||
if EnableAcme {
|
||||
AcmeURL = sec.Key("ACME_URL").MustString("")
|
||||
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
|
||||
@@ -210,6 +214,9 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", "v1.19.0")
|
||||
AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("")
|
||||
}
|
||||
if AcmeEmail == "" {
|
||||
log.Fatal("ACME Email is not set (ACME_EMAIL).")
|
||||
}
|
||||
} else {
|
||||
CertFile = sec.Key("CERT_FILE").String()
|
||||
KeyFile = sec.Key("KEY_FILE").String()
|
||||
|
||||
@@ -43,9 +43,11 @@ var Service = struct {
|
||||
ShowRegistrationButton bool
|
||||
EnablePasswordSignInForm bool
|
||||
ShowMilestonesDashboardPage bool
|
||||
RequireSignInView bool
|
||||
RequireSignInViewStrict bool
|
||||
BlockAnonymousAccessExpensive bool
|
||||
EnableNotifyMail bool
|
||||
EnableBasicAuth bool
|
||||
EnablePasskeyAuth bool
|
||||
EnableReverseProxyAuth bool
|
||||
EnableReverseProxyAuthAPI bool
|
||||
EnableReverseProxyAutoRegister bool
|
||||
@@ -158,9 +160,21 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
||||
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
|
||||
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
|
||||
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
|
||||
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
|
||||
|
||||
// boolean values are considered as "strict"
|
||||
var err error
|
||||
Service.RequireSignInViewStrict, err = sec.Key("REQUIRE_SIGNIN_VIEW").Bool()
|
||||
if s := sec.Key("REQUIRE_SIGNIN_VIEW").String(); err != nil && s != "" {
|
||||
// non-boolean value only supports "expensive" at the moment
|
||||
Service.BlockAnonymousAccessExpensive = s == "expensive"
|
||||
if !Service.BlockAnonymousAccessExpensive {
|
||||
log.Error("Invalid config option: REQUIRE_SIGNIN_VIEW = %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
|
||||
Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
|
||||
Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)
|
||||
Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
|
||||
Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool()
|
||||
Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool()
|
||||
|
||||
@@ -7,16 +7,14 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadServices(t *testing.T) {
|
||||
oldService := Service
|
||||
defer func() {
|
||||
Service = oldService
|
||||
}()
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[service]
|
||||
@@ -48,10 +46,7 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b
|
||||
}
|
||||
|
||||
func TestLoadServiceVisibilityModes(t *testing.T) {
|
||||
oldService := Service
|
||||
defer func() {
|
||||
Service = oldService
|
||||
}()
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
kases := map[string]func(){
|
||||
`
|
||||
@@ -130,3 +125,33 @@ ALLOWED_USER_VISIBILITY_MODES = public, limit, privated
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadServiceRequireSignInView(t *testing.T) {
|
||||
defer test.MockVariableValue(&Service)()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[service]
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.False(t, Service.RequireSignInViewStrict)
|
||||
assert.False(t, Service.BlockAnonymousAccessExpensive)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[service]
|
||||
REQUIRE_SIGNIN_VIEW = true
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.True(t, Service.RequireSignInViewStrict)
|
||||
assert.False(t, Service.BlockAnonymousAccessExpensive)
|
||||
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[service]
|
||||
REQUIRE_SIGNIN_VIEW = expensive
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
assert.False(t, Service.RequireSignInViewStrict)
|
||||
assert.True(t, Service.BlockAnonymousAccessExpensive)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func Clean(storage ObjectStorage) error {
|
||||
}
|
||||
|
||||
// SaveFrom saves data to the ObjectStorage with path p from the callback
|
||||
func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) error) error {
|
||||
func SaveFrom(objStorage ObjectStorage, path string, callback func(w io.Writer) error) error {
|
||||
pr, pw := io.Pipe()
|
||||
defer pr.Close()
|
||||
go func() {
|
||||
@@ -103,7 +103,7 @@ func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) err
|
||||
}
|
||||
}()
|
||||
|
||||
_, err := objStorage.Save(p, pr, -1)
|
||||
_, err := objStorage.Save(path, pr, -1)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -116,14 +116,7 @@ var (
|
||||
_ Payloader = &PackagePayload{}
|
||||
)
|
||||
|
||||
// _________ __
|
||||
// \_ ___ \_______ ____ _____ _/ |_ ____
|
||||
// / \ \/\_ __ \_/ __ \\__ \\ __\/ __ \
|
||||
// \ \____| | \/\ ___/ / __ \| | \ ___/
|
||||
// \______ /|__| \___ >____ /__| \___ >
|
||||
// \/ \/ \/ \/
|
||||
|
||||
// CreatePayload FIXME
|
||||
// CreatePayload represents a payload information of create event.
|
||||
type CreatePayload struct {
|
||||
Sha string `json:"sha"`
|
||||
Ref string `json:"ref"`
|
||||
@@ -157,13 +150,6 @@ func ParseCreateHook(raw []byte) (*CreatePayload, error) {
|
||||
return hook, nil
|
||||
}
|
||||
|
||||
// ________ .__ __
|
||||
// \______ \ ____ | | _____/ |_ ____
|
||||
// | | \_/ __ \| | _/ __ \ __\/ __ \
|
||||
// | ` \ ___/| |_\ ___/| | \ ___/
|
||||
// /_______ /\___ >____/\___ >__| \___ >
|
||||
// \/ \/ \/ \/
|
||||
|
||||
// PusherType define the type to push
|
||||
type PusherType string
|
||||
|
||||
@@ -186,13 +172,6 @@ func (p *DeletePayload) JSONPayload() ([]byte, error) {
|
||||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
// ___________ __
|
||||
// \_ _____/__________| | __
|
||||
// | __)/ _ \_ __ \ |/ /
|
||||
// | \( <_> ) | \/ <
|
||||
// \___ / \____/|__| |__|_ \
|
||||
// \/ \/
|
||||
|
||||
// ForkPayload represents fork payload
|
||||
type ForkPayload struct {
|
||||
Forkee *Repository `json:"forkee"`
|
||||
@@ -232,13 +211,6 @@ func (p *IssueCommentPayload) JSONPayload() ([]byte, error) {
|
||||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
// __________ .__
|
||||
// \______ \ ____ | | ____ _____ ______ ____
|
||||
// | _// __ \| | _/ __ \\__ \ / ___// __ \
|
||||
// | | \ ___/| |_\ ___/ / __ \_\___ \\ ___/
|
||||
// |____|_ /\___ >____/\___ >____ /____ >\___ >
|
||||
// \/ \/ \/ \/ \/ \/
|
||||
|
||||
// HookReleaseAction defines hook release action type
|
||||
type HookReleaseAction string
|
||||
|
||||
@@ -302,13 +274,6 @@ func (p *PushPayload) Branch() string {
|
||||
return strings.ReplaceAll(p.Ref, "refs/heads/", "")
|
||||
}
|
||||
|
||||
// .___
|
||||
// | | ______ ________ __ ____
|
||||
// | |/ ___// ___/ | \_/ __ \
|
||||
// | |\___ \ \___ \| | /\ ___/
|
||||
// |___/____ >____ >____/ \___ >
|
||||
// \/ \/ \/
|
||||
|
||||
// HookIssueAction FIXME
|
||||
type HookIssueAction string
|
||||
|
||||
@@ -371,13 +336,6 @@ type ChangesPayload struct {
|
||||
Ref *ChangesFromPayload `json:"ref,omitempty"`
|
||||
}
|
||||
|
||||
// __________ .__ .__ __________ __
|
||||
// \______ \__ __| | | | \______ \ ____ ________ __ ____ _______/ |_
|
||||
// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
|
||||
// | | | | / |_| |__ | | \ ___< <_| | | /\ ___/ \___ \ | |
|
||||
// |____| |____/|____/____/ |____|_ /\___ >__ |____/ \___ >____ > |__|
|
||||
// \/ \/ |__| \/ \/
|
||||
|
||||
// PullRequestPayload represents a payload information of pull request event.
|
||||
type PullRequestPayload struct {
|
||||
Action HookIssueAction `json:"action"`
|
||||
@@ -402,13 +360,6 @@ type ReviewPayload struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// __ __.__ __ .__
|
||||
// / \ / \__| | _|__|
|
||||
// \ \/\/ / | |/ / |
|
||||
// \ /| | <| |
|
||||
// \__/\ / |__|__|_ \__|
|
||||
// \/ \/
|
||||
|
||||
// HookWikiAction an action that happens to a wiki page
|
||||
type HookWikiAction string
|
||||
|
||||
@@ -435,13 +386,6 @@ func (p *WikiPayload) JSONPayload() ([]byte, error) {
|
||||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
//__________ .__ __
|
||||
//\______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
|
||||
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
|
||||
// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
|
||||
// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
|
||||
// \/ \/|__| \/ \/
|
||||
|
||||
// HookRepoAction an action that happens to a repo
|
||||
type HookRepoAction string
|
||||
|
||||
@@ -480,7 +424,7 @@ type PackagePayload struct {
|
||||
Action HookPackageAction `json:"action"`
|
||||
Repository *Repository `json:"repository"`
|
||||
Package *Package `json:"package"`
|
||||
Organization *User `json:"organization"`
|
||||
Organization *Organization `json:"organization"`
|
||||
Sender *User `json:"sender"`
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ type PullRequest struct {
|
||||
Comments int `json:"comments"`
|
||||
// number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
|
||||
ReviewComments int `json:"review_comments"`
|
||||
Additions int `json:"additions"`
|
||||
Deletions int `json:"deletions"`
|
||||
ChangedFiles int `json:"changed_files"`
|
||||
|
||||
Additions *int `json:"additions,omitempty"`
|
||||
Deletions *int `json:"deletions,omitempty"`
|
||||
ChangedFiles *int `json:"changed_files,omitempty"`
|
||||
|
||||
HTMLURL string `json:"html_url"`
|
||||
DiffURL string `json:"diff_url"`
|
||||
|
||||
@@ -133,3 +133,11 @@ type EditBranchProtectionOption struct {
|
||||
type UpdateBranchProtectionPriories struct {
|
||||
IDs []int64 `json:"ids"`
|
||||
}
|
||||
|
||||
type MergeUpstreamRequest struct {
|
||||
Branch string `json:"branch"`
|
||||
}
|
||||
|
||||
type MergeUpstreamResponse struct {
|
||||
MergeStyle string `json:"merge_type"`
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func NewFuncMap() template.FuncMap {
|
||||
// time / number / format
|
||||
"FileSize": base.FileSize,
|
||||
"CountFmt": base.FormatNumberSI,
|
||||
"Sec2Time": util.SecToTime,
|
||||
"Sec2Time": util.SecToHours,
|
||||
|
||||
"TimeEstimateString": timeEstimateString,
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user