diff --git a/.dockerignore b/.dockerignore index 1ce2a87611e..2a414208890 100644 --- a/.dockerignore +++ b/.dockerignore @@ -76,7 +76,6 @@ cpu.out /yarn-error.log /npm-debug.log* /public/js -/public/serviceworker.js /public/css /public/fonts /public/img/webpack diff --git a/.gitattributes b/.gitattributes index 16167a2cd65..6d4698d6a8c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,7 +2,6 @@ *.tmpl linguist-language=Handlebars /assets/*.json linguist-generated /public/img/svg/*.svg linguist-generated -/public/vendor/** -text -eol linguist-vendored /templates/swagger/v1_json.tmpl linguist-generated /vendor/** -text -eol linguist-vendored /web_src/fomantic/build/** linguist-generated diff --git a/.github/file-filters.yml b/.github/file-filters.yml deleted file mode 100644 index 26231c93642..00000000000 --- a/.github/file-filters.yml +++ /dev/null @@ -1,15 +0,0 @@ -docs: &docs - - "**/*.md" - - "docs/**" - -backend: &backend - - "**/*.go" - - "**/*.tmpl" - - "go.mod" - - "go.sum" - -frontend: &frontend - - "**/*.js" - - "web_src/**" - - "package.json" - - "package-lock.json" diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 2efd6767197..9a9c54720b4 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -1,32 +1,53 @@ -name: files changed +name: files-changed on: workflow_call: outputs: - docs: - description: "whether docs files changed" - value: ${{ jobs.files-changed.outputs.docs }} backend: description: "whether backend files changed" - value: ${{ jobs.files-changed.outputs.backend }} + value: ${{ jobs.detect.outputs.backend }} frontend: description: "whether frontend files changed" - value: ${{ jobs.files-changed.outputs.frontend }} + value: ${{ jobs.detect.outputs.frontend }} + docs: + description: "whether docs files changed" + value: ${{ jobs.detect.outputs.docs }} + actions: + description: "whether actions files changed" + value: ${{ jobs.detect.outputs.actions }} jobs: - files-changed: + detect: name: detect which files changed runs-on: ubuntu-latest timeout-minutes: 3 # Map a step output to a job output outputs: - docs: ${{ steps.changes.outputs.docs }} backend: ${{ steps.changes.outputs.backend }} frontend: ${{ steps.changes.outputs.frontend }} + docs: ${{ steps.changes.outputs.docs }} + actions: ${{ steps.changes.outputs.actions }} steps: - uses: actions/checkout@v3 - - name: Check for backend file changes - uses: dorny/paths-filter@v2 + - uses: dorny/paths-filter@v2 id: changes with: - filters: .github/file-filters.yml + filters: | + backend: + - "**/*.go" + - "**/*.tmpl" + - "go.mod" + - "go.sum" + + frontend: + - "**/*.js" + - "web_src/**" + - "package.json" + - "package-lock.json" + + docs: + - "**/*.md" + - "docs/**" + + actions: + - ".github/workflows/*" diff --git a/.github/workflows/pull-compliance-docs.yml b/.github/workflows/pull-compliance-docs.yml deleted file mode 100644 index 0f6720a64dc..00000000000 --- a/.github/workflows/pull-compliance-docs.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: compliance-docs - -on: - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - files-changed: - uses: ./.github/workflows/files-changed.yml - - compliance-docs: - if: needs.files-changed.outputs.docs == 'true' - needs: files-changed - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 20 - - run: make deps-frontend - - run: make lint-md - - run: make docs # test if build could succeed diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index e108379b309..b7c1ab8a33c 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -25,6 +25,7 @@ jobs: - run: make lint-backend env: TAGS: bindata sqlite sqlite_unlock_notify + lint-go-windows: if: needs.files-changed.outputs.backend == 'true' needs: files-changed @@ -41,6 +42,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify GOOS: windows GOARCH: amd64 + lint-go-gogit: if: needs.files-changed.outputs.backend == 'true' needs: files-changed @@ -55,6 +57,7 @@ jobs: - run: make lint-go env: TAGS: bindata gogit sqlite sqlite_unlock_notify + checks-backend: if: needs.files-changed.outputs.backend == 'true' needs: files-changed @@ -67,6 +70,7 @@ jobs: check-latest: true - run: make deps-backend deps-tools - run: make --always-make checks-backend # ensure the "go-licenses" make target runs + frontend: if: needs.files-changed.outputs.frontend == 'true' needs: files-changed @@ -79,6 +83,7 @@ jobs: - run: make deps-frontend - run: make lint-frontend - run: make checks-frontend + backend: if: needs.files-changed.outputs.backend == 'true' needs: files-changed @@ -113,3 +118,25 @@ jobs: env: GOOS: linux GOARCH: 386 + + docs: + if: needs.files-changed.outputs.docs == 'true' + needs: files-changed + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + - run: make deps-frontend + - run: make lint-md + - run: make docs # test if build could succeed + + actions: + if: needs.files-changed.outputs.actions == 'true' + needs: files-changed + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + - run: make lint-actions diff --git a/.gitignore b/.gitignore index ad141c30cd9..c53caa98064 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ cpu.out *.db *.log +*.log.*.gz /gitea /gitea-vet @@ -77,7 +78,6 @@ cpu.out /yarn-error.log /npm-debug.log* /public/js -/public/serviceworker.js /public/css /public/fonts /public/img/webpack diff --git a/.golangci.yml b/.golangci.yml index 342fe97837b..22de387facb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -86,6 +86,7 @@ linters-settings: - io/ioutil: "use os or io instead" - golang.org/x/exp: "it's experimental and unreliable." - code.gitea.io/gitea/modules/git/internal: "do not use the internal package, use AddXxx function instead" + - gopkg.in/ini.v1: "do not use the ini package, use gitea's config system instead" issues: max-issues-per-linter: 0 diff --git a/.ignore b/.ignore index 7836ba8bced..cd869d51cb9 100644 --- a/.ignore +++ b/.ignore @@ -1,8 +1,8 @@ *.min.css *.min.js +/assets/*.json /modules/options/bindata.go /modules/public/bindata.go /modules/templates/bindata.go -/public/vendor/plugins /vendor node_modules diff --git a/Makefile b/Makefile index 16841796b78..664839273b5 100644 --- a/Makefile +++ b/Makefile @@ -114,7 +114,7 @@ FOMANTIC_WORK_DIR := web_src/fomantic WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f) WEBPACK_CONFIGS := webpack.config.js WEBPACK_DEST := public/js/index.js public/css/index.css -WEBPACK_DEST_ENTRIES := public/js public/css public/fonts public/img/webpack public/serviceworker.js +WEBPACK_DEST_ENTRIES := public/js public/css public/fonts public/img/webpack BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST)) diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 49f18e7de70..3718741cc7f 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -525,8 +525,8 @@ "licenseText": "Copyright (c) 2011 The Snappy-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/google/go-github/v51/github", - "path": "github.com/google/go-github/v51/github/LICENSE", + "name": "github.com/google/go-github/v52/github", + "path": "github.com/google/go-github/v52/github/LICENSE", "licenseText": "Copyright (c) 2013 The go-github 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" }, { diff --git a/build/backport-locales.go b/build/backport-locales.go index 054b623d698..03462153485 100644 --- a/build/backport-locales.go +++ b/build/backport-locales.go @@ -12,7 +12,7 @@ import ( "path/filepath" "strings" - "gopkg.in/ini.v1" + "code.gitea.io/gitea/modules/setting" ) func main() { @@ -22,14 +22,13 @@ func main() { os.Exit(1) } - ini.PrettyFormat = false mustNoErr := func(err error) { if err != nil { panic(err) } } - collectInis := func(ref string) map[string]*ini.File { - inis := map[string]*ini.File{} + collectInis := func(ref string) map[string]setting.ConfigProvider { + inis := map[string]setting.ConfigProvider{} err := filepath.WalkDir("options/locale", func(path string, d os.DirEntry, err error) error { if err != nil { return err @@ -37,10 +36,7 @@ func main() { if d.IsDir() || !strings.HasSuffix(d.Name(), ".ini") { return nil } - cfg, err := ini.LoadSources(ini.LoadOptions{ - IgnoreInlineComment: true, - UnescapeValueCommentSymbols: true, - }, path) + cfg, err := setting.NewConfigProviderForLocale(path) mustNoErr(err) inis[path] = cfg fmt.Printf("collecting: %s @ %s\n", path, ref) diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go index 358f4ed3c81..3027722583a 100644 --- a/contrib/backport/backport.go +++ b/contrib/backport/backport.go @@ -17,7 +17,7 @@ import ( "strings" "syscall" - "github.com/google/go-github/v51/github" + "github.com/google/go-github/v52/github" "github.com/urfave/cli" "gopkg.in/yaml.v3" ) diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go index ae8535d8912..3405d7d429b 100644 --- a/contrib/environment-to-ini/environment-to-ini.go +++ b/contrib/environment-to-ini/environment-to-ini.go @@ -9,10 +9,8 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "github.com/urfave/cli" - "gopkg.in/ini.v1" ) // EnvironmentPrefix environment variables prefixed with this represent ini values to write @@ -97,19 +95,10 @@ func runEnvironmentToIni(c *cli.Context) error { providedWorkPath := c.String("work-path") setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath) - cfg := ini.Empty() - confFileExists, err := util.IsFile(setting.CustomConf) + cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true}) if err != nil { - log.Fatal("Unable to check if %s is a file. Error: %v", setting.CustomConf, err) + log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err) } - if confFileExists { - if err := cfg.Append(setting.CustomConf); err != nil { - log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err) - } - } else { - log.Warn("Custom config '%s' not found, ignore this if you're running first time", setting.CustomConf) - } - cfg.NameMapper = ini.SnackCase prefixGitea := c.String("prefix") + "__" suffixFile := "__FILE" diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 2f9bd126e76..9771fcfeb2c 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -693,17 +693,13 @@ LEVEL = Info ;PULL = 300 ;GC = 60 -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Git Reflog timeout in days -;[git.reflog] -;ENABLED = true -;EXPIRATION = 90 - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Git config options ;; This section only does "set" config, a removed config key from this section won't be removed from git config automatically. The format is `some.configKey = value`. ;[git.config] ;diff.algorithm = histogram +;core.logAllRefUpdates = true +;gc.reflogExpire = 90 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1057,7 +1053,7 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; List of reasons why a Pull Request or Issue can be locked ;LOCK_REASONS = Too heated,Off-topic,Resolved,Spam -;; Maximum number of pinned Issues +;; Maximum number of pinned Issues per repo ;; Set to 0 to disable pinning Issues ;MAX_PINNED = 3 @@ -1193,10 +1189,6 @@ LEVEL = Info ;; Number of line of codes shown for a code comment ;CODE_COMMENT_LINES = 4 ;; -;; Value of `theme-color` meta tag, used by some mobile browers for chrome and -;; out-of-viewport areas. Default is unset which uses body color. -;THEME_COLOR_META_TAG = -;; ;; Max size of files to be displayed (default is 8MiB) ;MAX_DISPLAY_FILE_SIZE = 8388608 ;; @@ -1225,9 +1217,6 @@ LEVEL = Info ;; Whether to search within description at repository search on explore page. ;SEARCH_REPO_DESCRIPTION = true ;; -;; Whether to enable a Service Worker to cache frontend assets -;USE_SERVICE_WORKER = false -;; ;; Whether to only show relevant repos on the explore page when no keyword is specified and default sorting is used. ;; A repo is considered irrelevant if it's a fork or if it has no metadata (no description, no icon, no topic). ;ONLY_SHOW_RELEVANT_REPOS = false diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index d0cfca2afd6..0fdf3518081 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -144,7 +144,7 @@ limit defined in `REPO_SIZE_LIMIT` or at the repository itself. ### Repository - Issue (`repository.issue`) - `LOCK_REASONS`: **Too heated,Off-topic,Resolved,Spam**: A list of reasons why a Pull Request or Issue can be locked -- `MAX_PINNED`: **3**: Maximum number of pinned Issues. Set to 0 to disable pinning Issues. +- `MAX_PINNED`: **3**: Maximum number of pinned Issues per Repo. Set to 0 to disable pinning Issues. ### Repository - Upload (`repository.upload`) @@ -224,7 +224,6 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `SHOW_USER_EMAIL`: **true**: Whether the email of the user should be shown in the Explore Users page. - `THEMES`: **auto,gitea,arc-green**: All available themes. Allow users select personalized themes. regardless of the value of `DEFAULT_THEME`. -- `THEME_COLOR_META_TAG`: **\**: Value of `theme-color` meta tag, used by some mobile browsers for chrome and out-of-viewport areas. Default is unset which uses body color. - `MAX_DISPLAY_FILE_SIZE`: **8388608**: Max size of files to be displayed (default is 8MiB) - `REACTIONS`: All available reactions users can choose on issues/prs and comments Values can be emoji alias (:smile:) or a unicode emoji. @@ -234,7 +233,6 @@ The following configuration set `Content-Type: application/vnd.android.package-a add it to this config. - `DEFAULT_SHOW_FULL_NAME`: **false**: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used. - `SEARCH_REPO_DESCRIPTION`: **true**: Whether to search within description at repository search on explore page. -- `USE_SERVICE_WORKER`: **false**: Whether to enable a Service Worker to cache frontend assets. - `ONLY_SHOW_RELEVANT_REPOS`: **false** Whether to only show relevant repos on the explore page when no keyword is specified and default sorting is used. A repo is considered irrelevant if it's a fork or if it has no metadata (no description, no icon, no topic). @@ -1068,17 +1066,14 @@ Default templates for project boards: - `PULL`: **300**: Git pull from internal repositories timeout seconds. - `GC`: **60**: Git repository GC timeout seconds. -### Git - Reflog settings (`git.reflog`) - -- `ENABLED`: **true** Set to true to enable Git to write changes to reflogs in each repo. -- `EXPIRATION`: **90** Reflog entry lifetime, in days. Entries are removed opportunistically by Git. - ### Git - Config options (`git.config`) The key/value pairs in this section will be used as git config. This section only does "set" config, a removed config key from this section won't be removed from git config automatically. The format is `some.configKey = value`. - `diff.algorithm`: **histogram** +- `core.logAllRefUpdates`: **true** +- `gc.reflogExpire`: **90** ## Metrics (`metrics`) diff --git a/docs/content/doc/administration/reverse-proxies.en-us.md b/docs/content/doc/administration/reverse-proxies.en-us.md index a6efd86549a..7272eb5aa46 100644 --- a/docs/content/doc/administration/reverse-proxies.en-us.md +++ b/docs/content/doc/administration/reverse-proxies.en-us.md @@ -191,14 +191,6 @@ git.example.com { } ``` -If you still use Caddy v1, use: - -```apacheconf -git.example.com { - proxy / localhost:3000 -} -``` - ## Caddy with a sub-path In case you already have a site, and you want Gitea to share the domain name, you can setup Caddy to serve Gitea under a sub-path by adding the following to your server block in your Caddyfile: @@ -212,14 +204,6 @@ git.example.com { } ``` -Or, for Caddy v1: - -```apacheconf -git.example.com { - proxy /git/ localhost:3000 -} -``` - Then set `[server] ROOT_URL = http://git.example.com/git/` in your configuration. ## IIS diff --git a/docs/content/doc/development/hacking-on-gitea.en-us.md b/docs/content/doc/development/hacking-on-gitea.en-us.md index 8ac7fcc34c4..0f999215d9b 100644 --- a/docs/content/doc/development/hacking-on-gitea.en-us.md +++ b/docs/content/doc/development/hacking-on-gitea.en-us.md @@ -123,9 +123,8 @@ to the Gitea sources. Otherwise, changes can't be pushed. ## Building Gitea (Basic) Take a look at our -instructions -for building -from source. +[instructions]({{< relref "doc/installation/from-source.en-us.md" >}}) +for [building from source]({{< relref "doc/installation/from-source.en-us.md" >}}). The simplest recommended way to build from source is: @@ -266,7 +265,7 @@ OpenAPI 3 documentation. When creating new configuration options, it is not enough to add them to the `modules/setting` files. You should add information to `custom/conf/app.ini` and to the -configuration cheat sheet +[configuration cheat sheet]({{< relref "doc/administration/config-cheat-sheet.en-us.md" >}}) found in `docs/content/doc/administer/config-cheat-sheet.en-us.md` ### Changing the logo diff --git a/docs/content/doc/development/hacking-on-gitea.zh-cn.md b/docs/content/doc/development/hacking-on-gitea.zh-cn.md index 66a95cdd693..6f0ce6bc0bf 100644 --- a/docs/content/doc/development/hacking-on-gitea.zh-cn.md +++ b/docs/content/doc/development/hacking-on-gitea.zh-cn.md @@ -115,8 +115,8 @@ git fetch --all --prune ## 构建 Gitea(基本) 看看我们的 -说明 -关于如何 从源代码构建 。 +[说明]({{< relref "doc/installation/from-source.zh-cn.md" >}}) +关于如何[从源代码构建]({{< relref "doc/installation/from-source.zh-cn.md" >}}) 。 从源代码构建的最简单推荐方法是: @@ -249,7 +249,7 @@ make swagger-check ### 创建新的配置选项 创建新的配置选项时,将它们添加到 `modules/setting` 的对应文件。您应该将信息添加到 `custom/conf/app.ini` -并到 配置备忘单 +并到[配置备忘单]({{< relref "doc/administration/config-cheat-sheet.zh-cn.md" >}}) 在 `docs/content/doc/advanced/config-cheat-sheet.zh-cn.md` 中找到 ### 更改Logo diff --git a/docs/content/doc/development/oauth2-provider.en-us.md b/docs/content/doc/development/oauth2-provider.en-us.md index b279e97a6eb..03833b5ac01 100644 --- a/docs/content/doc/development/oauth2-provider.en-us.md +++ b/docs/content/doc/development/oauth2-provider.en-us.md @@ -1,5 +1,5 @@ --- -date: "2019-04-19:44:00+01:00" +date: "2023-06-01T08:40:00+08:00" title: "OAuth2 provider" slug: "oauth2-provider" weight: 41 @@ -40,46 +40,47 @@ At the moment Gitea only supports the [**Authorization Code Grant**](https://too - [Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636) - [OpenID Connect (OIDC)](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) -To use the Authorization Code Grant as a third party application it is required to register a new application via the "Settings" (`/user/settings/applications`) section of the settings. +To use the Authorization Code Grant as a third party application it is required to register a new application via the "Settings" (`/user/settings/applications`) section of the settings. To test or debug you can use the web-tool https://oauthdebugger.com/. ## Scopes -Gitea supports the following scopes for tokens: +Gitea supports scoped access tokens, which allow users the ability to restrict tokens to operate only on selected url routes. Scopes are grouped by high-level API routes, and further refined to the following: -| Name | Description | -| ---- | ----------- | -| **(no scope)** | Grants read-only access to public user profile and public repositories. | -| **repo** | Full control over all repositories. | -|     **repo:status** | Grants read/write access to commit status in all repositories. | -|     **public_repo** | Grants read/write access to public repositories only. | -| **admin:repo_hook** | Grants access to repository hooks of all repositories. This is included in the `repo` scope. | -|     **write:repo_hook** | Grants read/write access to repository hooks | -|     **read:repo_hook** | Grants read-only access to repository hooks | -| **admin:org** | Grants full access to organization settings | -|     **write:org** | Grants read/write access to organization settings | -|     **read:org** | Grants read-only access to organization settings | -| **admin:public_key** | Grants full access for managing public keys | -|     **write:public_key** | Grant read/write access to public keys | -|     **read:public_key** | Grant read-only access to public keys | -| **admin:org_hook** | Grants full access to organizational-level hooks | -| **admin:user_hook** | Grants full access to user-level hooks | -| **notification** | Grants full access to notifications | -| **user** | Grants full access to user profile info | -|     **read:user** | Grants read access to user's profile | -|     **user:email** | Grants read access to user's email addresses | -|     **user:follow** | Grants access to follow/un-follow a user | -| **delete_repo** | Grants access to delete repositories as an admin | -| **package** | Grants full access to hosted packages | -|     **write:package** | Grants read/write access to packages | -|     **read:package** | Grants read access to packages | -|     **delete:package** | Grants delete access to packages | -| **admin:gpg_key** | Grants full access for managing GPG keys | -|     **write:gpg_key** | Grants read/write access to GPG keys | -|     **read:gpg_key** | Grants read-only access to GPG keys | -| **admin:application** | Grants full access to manage applications | -|     **write:application** | Grants read/write access for managing applications | -|     **read:application** | Grants read access for managing applications | -| **sudo** | Allows to perform actions as the site admin. | +- `read`: `GET` routes +- `write`: `POST`, `PUT`, `PATCH`, and `DELETE` routes (in addition to `GET`) + +Gitea token scopes are as follows: + +| Name | Description | +| ---- |--------------------------------------------------------------------------------------------------------------------------------------------------| +| **(no scope)** | Not supported. A scope is required even for public repositories. | +| **activitypub** | `activitypub` API routes: ActivityPub related operations. | +|     **read:activitypub** | Grants read access for ActivityPub operations. | +|     **write:activitypub** | Grants read/write/delete access for ActivityPub operations. | +| **admin** | `/admin/*` API routes: Site-wide administrative operations (hidden for non-admin accounts). | +|     **read:admin** | Grants read access for admin operations, such as getting cron jobs or registered user emails. | +|     **write:admin** | Grants read/write/delete access for admin operations, such as running cron jobs or updating user accounts. | | +| **issue** | `issues/*`, `labels/*`, `milestones/*` API routes: Issue-related operations. | +|     **read:issue** | Grants read access for issues operations, such as getting issue comments, issue attachments, and milestones. | +|     **write:issue** | Grants read/write/delete access for issues operations, such as posting or editing an issue comment or attachment, and updating milestones. | +| **misc** | miscellaneous and settings top-level API routes. | +|     **read:misc** | Grants read access to miscellaneous operations, such as getting label and gitignore templates. | +|     **write:misc** | Grants read/write/delete access to miscellaneous operations, such as markup utility operations. | +| **notification** | `notification/*` API routes: user notification operations. | +|     **read:notification** | Grants read access to user notifications, such as which notifications users are subscribed to and read new notifications. | +|     **write:notification** | Grants read/write/delete access to user notifications, such as marking notifications as read. | +| **organization** | `orgs/*` and `teams/*` API routes: Organization and team management operations. | +|     **read:organization** | Grants read access to org and team status, such as listing all orgs a user has visibility to, teams, and team members. | +|     **write:organization** | Grants read/write/delete access to org and team status, such as creating and updating teams and updating org settings. | +| **package** | `/packages/*` API routes: Packages operations | +|     **read:package** | Grants read access to package operations, such as reading and downloading available packages. | +|     **write:package** | Grants read/write/delete access to package operations. Currently the same as `read:package`. | +| **repository** | `/repos/*` API routes except `/repos/issues/*`: Repository file, pull-request, and release operations. | +|     **read:repository** | Grants read access to repository operations, such as getting repository files, releases, collaborators. | +|     **write:repository** | Grants read/write/delete access to repository operations, such as getting updating repository files, creating pull requests, updating collaborators. | +| **user** | `/user/*` and `/users/*` API routes: User-related operations. | +|     **read:user** | Grants read access to user operations, such as getting user repo subscriptions and user settings. | +|     **write:user** | Grants read/write/delete access to user operations, such as updating user repo subscriptions, followed users, and user settings. | ## Client types @@ -87,17 +88,19 @@ Gitea supports both confidential and public client types, [as defined by RFC 674 For public clients, a redirect URI of a loopback IP address such as `http://127.0.0.1/` allows any port. Avoid using `localhost`, [as recommended by RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252#section-8.3). -## Example +## Examples + +### Confidential client **Note:** This example does not use PKCE. -1. Redirect to user to the authorization endpoint in order to get their consent for accessing the resources: +1. Redirect the user to the authorization endpoint in order to get their consent for accessing the resources: ```curl - https://[YOUR-GITEA-URL]/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI& response_type=code&state=STATE + https://[YOUR-GITEA-URL]/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE ``` - The `CLIENT_ID` can be obtained by registering an application in the settings. The `STATE` is a random string that will be send back to your application after the user authorizes. The `state` parameter is optional but should be used to prevent CSRF attacks. + The `CLIENT_ID` can be obtained by registering an application in the settings. The `STATE` is a random string that will be sent back to your application after the user authorizes. The `state` parameter is optional, but should be used to prevent CSRF attacks. ![Authorization Page](/authorize.png) @@ -107,7 +110,7 @@ For public clients, a redirect URI of a loopback IP address such as `http://127. https://[REDIRECT_URI]?code=RETURNED_CODE&state=STATE ``` -2. Using the provided `code` from the redirect, you can request a new application and refresh token. The access token endpoints accepts POST requests with `application/json` and `application/x-www-form-urlencoded` body, for example: +2. Using the provided `code` from the redirect, you can request a new application and refresh token. The access token endpoint accepts POST requests with `application/json` and `application/x-www-form-urlencoded` body, for example: ```curl POST https://[YOUR-GITEA-URL]/login/oauth/access_token @@ -134,7 +137,69 @@ For public clients, a redirect URI of a loopback IP address such as `http://127. } ``` - The `CLIENT_SECRET` is the unique secret code generated for this application. Please note that the secret will only be visible after you created/registered the application with Gitea and cannot be recovered. If you lose the secret you must regenerate the secret via the application's settings. + The `CLIENT_SECRET` is the unique secret code generated for this application. Please note that the secret will only be visible after you created/registered the application with Gitea and cannot be recovered. If you lose the secret, you must regenerate the secret via the application's settings. + + The `REDIRECT_URI` in the `access_token` request must match the `REDIRECT_URI` in the `authorize` request. + +3. Use the `access_token` to make [API requests](https://docs.gitea.io/en-us/api-usage#oauth2) to access the user's resources. + +### Public client (PKCE) + +PKCE (Proof Key for Code Exchange) is an extension to the OAuth flow which allows for a secure credential exchange without the requirement to provide a client secret. + +**Note**: Please ensure you have registered your OAuth application as a public client. + +To achieve this, you have to provide a `code_verifier` for every authorization request. A `code_verifier` has to be a random string with a minimum length of 43 characters and a maximum length of 128 characters. It can contain alphanumeric characters as well as the characters `-`, `.`, `_` and `~`. + +Using this `code_verifier` string, a new one called `code_challenge` is created by using one of two methods: + +- If you have the required functionality on your client, set `code_challenge` to be a URL-safe base64-encoded string of the SHA256 hash of `code_verifier`. In that case, your `code_challenge_method` becomes `S256`. +- If you are unable to do so, you can provide your `code_verifier` as a plain string to `code_challenge`. Then you have to set your `code_challenge_method` as `plain`. + +After you have generated this values, you can continue with your request. + +1. Redirect the user to the authorization endpoint in order to get their consent for accessing the resources: + + ```curl + https://[YOUR-GITEA-URL]/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&code_challenge_method=CODE_CHALLENGE_METHOD&code_challenge=CODE_CHALLENGE&state=STATE + ``` + + The `CLIENT_ID` can be obtained by registering an application in the settings. The `STATE` is a random string that will be sent back to your application after the user authorizes. The `state` parameter is optional, but should be used to prevent CSRF attacks. + + ![Authorization Page](/authorize.png) + + The user will now be asked to authorize your application. If they authorize it, the user will be redirected to the `REDIRECT_URL`, for example: + + ```curl + https://[REDIRECT_URI]?code=RETURNED_CODE&state=STATE + ``` + +2. Using the provided `code` from the redirect, you can request a new application and refresh token. The access token endpoint accepts POST requests with `application/json` and `application/x-www-form-urlencoded` body, for example: + + ```curl + POST https://[YOUR-GITEA-URL]/login/oauth/access_token + ``` + + ```json + { + "client_id": "YOUR_CLIENT_ID", + "code": "RETURNED_CODE", + "grant_type": "authorization_code", + "redirect_uri": "REDIRECT_URI", + "code_verifier": "CODE_VERIFIER", + } + ``` + + Response: + + ```json + { + "access_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJnbnQiOjIsInR0IjowLCJleHAiOjE1NTUxNzk5MTIsImlhdCI6MTU1NTE3NjMxMn0.0-iFsAwBtxuckA0sNZ6QpBQmywVPz129u75vOM7wPJecw5wqGyBkmstfJHAjEOqrAf_V5Z-1QYeCh_Cz4RiKug", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJnbnQiOjIsInR0IjoxLCJjbnQiOjEsImV4cCI6MTU1NzgwNDMxMiwiaWF0IjoxNTU1MTc2MzEyfQ.S_HZQBy4q9r5SEzNGNIoFClT43HPNDbUdHH-GYNYYdkRfft6XptJBkUQscZsGxOW975Yk6RbgtGvq1nkEcklOw" + } + ``` The `REDIRECT_URI` in the `access_token` request must match the `REDIRECT_URI` in the `authorize` request. diff --git a/docs/content/doc/installation/from-binary.en-us.md b/docs/content/doc/installation/from-binary.en-us.md index 5c8973fa941..4c501aa30e3 100644 --- a/docs/content/doc/installation/from-binary.en-us.md +++ b/docs/content/doc/installation/from-binary.en-us.md @@ -159,7 +159,7 @@ After you complete the above steps, you can run Gitea two ways: ### 1. Creating a service file to start Gitea automatically (recommended) -See how to create [Linux service]({{< relref "run-as-service-in-ubuntu.en-us.md" >}}) +See how to create [Linux service]({{< relref "doc/installation/run-as-service-in-ubuntu.en-us.md" >}}) ### 2. Running from command-line/terminal @@ -200,7 +200,7 @@ Older Linux distributions (such as Debian 7 and CentOS 6) may not be able to loa Gitea binary, usually producing an error such as `./gitea: /lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC\_2.14' not found (required by ./gitea)`. This is due to the integrated SQLite support in the binaries provided by dl.gitea.com. In this situation, it is usually -possible to [install from source]({{< relref "from-source.en-us.md" >}}), without including +possible to [install from source]({{< relref "doc/installation/from-source.en-us.md" >}}), without including SQLite support. ### Running Gitea on another port diff --git a/docs/content/doc/installation/from-binary.fr-fr.md b/docs/content/doc/installation/from-binary.fr-fr.md index eea3819384d..f5273054bcc 100644 --- a/docs/content/doc/installation/from-binary.fr-fr.md +++ b/docs/content/doc/installation/from-binary.fr-fr.md @@ -36,7 +36,7 @@ Après avoir suivi les étapes ci-dessus, vous aurez un binaire `gitea` dans vot ### Anciennes version de glibc -Les anciennes distributions Linux (comme Debian 7 ou CentOS 6) peuvent ne pas être capable d'exécuter le binaire Gitea, résultant généralement une erreur du type ```./gitea: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.14' not found (required by ./gitea)```. Cette erreur est due au driver SQLite que nous intégrons dans le binaire Gitea. Dans le futur, nous fournirons des binaires sans la dépendance pour la bibliothèque glibc. En attendant, vous pouvez mettre à jour votre distribution ou installer Gitea depuis le [code source]({{< relref "from-source.fr-fr.md" >}}). +Les anciennes distributions Linux (comme Debian 7 ou CentOS 6) peuvent ne pas être capable d'exécuter le binaire Gitea, résultant généralement une erreur du type ```./gitea: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.14' not found (required by ./gitea)```. Cette erreur est due au driver SQLite que nous intégrons dans le binaire Gitea. Dans le futur, nous fournirons des binaires sans la dépendance pour la bibliothèque glibc. En attendant, vous pouvez mettre à jour votre distribution ou installer Gitea depuis le [code source]({{< relref "doc/installation/from-source.fr-fr.md" >}}). ### Exécuter Gitea avec un autre port diff --git a/docs/content/doc/installation/from-binary.zh-cn.md b/docs/content/doc/installation/from-binary.zh-cn.md index 126ebf6e06b..85fc4736289 100644 --- a/docs/content/doc/installation/from-binary.zh-cn.md +++ b/docs/content/doc/installation/from-binary.zh-cn.md @@ -143,7 +143,7 @@ cp gitea /usr/local/bin/gitea ### 1. 创建服务自动启动 Gitea(推荐) -学习创建 [Linux 服务]({{< relref "run-as-service-in-ubuntu.zh-cn.md" >}}) +学习创建 [Linux 服务]({{< relref "doc/installation/run-as-service-in-ubuntu.zh-cn.md" >}}) ### 2. 通过命令行终端运行 @@ -175,4 +175,4 @@ GITEA_WORK_DIR=/var/lib/gitea/ /usr/local/bin/gitea web -c /etc/gitea/app.ini > 更多经验总结,请参考英文版 [Troubleshooting](/en-us/install-from-binary/#troubleshooting) -如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "support.zh-cn.md" >}}) +如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "doc/help/support.zh-cn.md" >}}) diff --git a/docs/content/doc/installation/from-package.en-us.md b/docs/content/doc/installation/from-package.en-us.md index 15283d58378..b427ae9a49d 100644 --- a/docs/content/doc/installation/from-package.en-us.md +++ b/docs/content/doc/installation/from-package.en-us.md @@ -24,7 +24,7 @@ menu: ## macOS Currently, the only supported method of installation on MacOS is [Homebrew](http://brew.sh/). -Following the [deployment from binary]({{< relref "from-binary.en-us.md" >}}) guide may work, +Following the [deployment from binary]({{< relref "doc/installation/from-binary.en-us.md" >}}) guide may work, but is not supported. To install Gitea via `brew`: ``` @@ -87,7 +87,7 @@ There is a [Gitea](https://chocolatey.org/packages/gitea) package for Windows by choco install gitea ``` -Or follow the [deployment from binary]({{< relref "from-binary.en-us.md" >}}) guide. +Or follow the [deployment from binary]({{< relref "doc/installation/from-binary.en-us.md" >}}) guide. ## FreeBSD diff --git a/docs/content/doc/installation/from-package.fr-fr.md b/docs/content/doc/installation/from-package.fr-fr.md index b7e842400d0..0a00cd7f0c6 100644 --- a/docs/content/doc/installation/from-package.fr-fr.md +++ b/docs/content/doc/installation/from-package.fr-fr.md @@ -19,15 +19,15 @@ menu: ## Linux -Nous n'avons pas encore publié de paquet pour Linux, nous allons mettre à jour cette page directement lorsque nous commencerons à publier des paquets pour toutes distributions Linux. En attendant, vous devriez suivre les [instructions d'installation]({{< relref "from-binary.fr-fr.md" >}}) avec le binaire pré-compilé. +Nous n'avons pas encore publié de paquet pour Linux, nous allons mettre à jour cette page directement lorsque nous commencerons à publier des paquets pour toutes distributions Linux. En attendant, vous devriez suivre les [instructions d'installation]({{< relref "doc/installation/from-binary.fr-fr.md" >}}) avec le binaire pré-compilé. ## Windows -Nous n'avons pas encore publié de paquet pour Windows, nous allons mettre à jour cette page directement lorsque nous commencerons à publier des paquets sous la forme de fichiers `MSI` ou via [Chocolatey](https://chocolatey.org/). En attendant, vous devriez suivre les [instructions d'installation]({{< relref "from-binary.fr-fr.md" >}}) avec le binaire pré-compilé. +Nous n'avons pas encore publié de paquet pour Windows, nous allons mettre à jour cette page directement lorsque nous commencerons à publier des paquets sous la forme de fichiers `MSI` ou via [Chocolatey](https://chocolatey.org/). En attendant, vous devriez suivre les [instructions d'installation]({{< relref "doc/installation/from-binary.fr-fr.md" >}}) avec le binaire pré-compilé. ## macOS -Actuellement, nous ne supportons que l'installation via `brew` pour macOS. Si vous n'utilisez pas [Homebrew](http://brew.sh/), vous pouvez suivre les [instructions d'installation]({{< relref "from-binary.fr-fr.md" >}}) avec le binaire pré-compilé. Pour installer Gitea depuis `brew`, utilisez les commandes suivantes : +Actuellement, nous ne supportons que l'installation via `brew` pour macOS. Si vous n'utilisez pas [Homebrew](http://brew.sh/), vous pouvez suivre les [instructions d'installation]({{< relref "doc/installation/from-binary.fr-fr.md" >}}) avec le binaire pré-compilé. Pour installer Gitea depuis `brew`, utilisez les commandes suivantes : ``` brew tap go-gitea/gitea diff --git a/docs/content/doc/installation/from-package.zh-cn.md b/docs/content/doc/installation/from-package.zh-cn.md index a8e92694f96..4845dd11537 100644 --- a/docs/content/doc/installation/from-package.zh-cn.md +++ b/docs/content/doc/installation/from-package.zh-cn.md @@ -23,7 +23,7 @@ menu: ## macOS -macOS 平台下当前我们仅支持通过 `brew` 来安装。如果你没有安装 [Homebrew](http://brew.sh/),你也可以查看 [从二进制安装]({{< relref "from-binary.zh-cn.md" >}})。在你安装了 `brew` 之后, 你可以执行以下命令: +macOS 平台下当前我们仅支持通过 `brew` 来安装。如果你没有安装 [Homebrew](http://brew.sh/),你也可以查看 [从二进制安装]({{< relref "doc/installation/from-binary.zh-cn.md" >}})。在你安装了 `brew` 之后, 你可以执行以下命令: ``` brew tap gitea/tap https://gitea.com/gitea/homebrew-gitea @@ -77,7 +77,7 @@ OpenSUSE 构建服务为 [openSUSE 和 SLE](https://software.opensuse.org/downlo choco install gitea ``` -你也可以 [从二进制安装]({{< relref "from-binary.zh-cn.md" >}}) 。 +你也可以 [从二进制安装]({{< relref "doc/installation/from-binary.zh-cn.md" >}}) 。 ## FreeBSD @@ -106,7 +106,3 @@ make install clean 如果这里没有找到你喜欢的包管理器,可以使用 Gitea 第三方软件包。这里有一个完整的列表: [awesome-gitea](https://gitea.com/gitea/awesome-gitea/src/branch/master/README.md#user-content-packages)。 如果你知道其他 Gitea 第三方软件包,请发送 PR 来添加它。 - -## 需要帮助? - -如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "support.zh-cn.md" >}}) diff --git a/docs/content/doc/installation/from-package.zh-tw.md b/docs/content/doc/installation/from-package.zh-tw.md index c5347991ec8..c01653e1d02 100644 --- a/docs/content/doc/installation/from-package.zh-tw.md +++ b/docs/content/doc/installation/from-package.zh-tw.md @@ -19,7 +19,7 @@ menu: ## Linux -目前尚未發佈任何 Linux 套件,如果我們發佈了,會直接更新此網頁。在這之前請先參考[執行檔安裝]({{< relref "from-binary.zh-tw.md" >}})方式。 +目前尚未發佈任何 Linux 套件,如果我們發佈了,會直接更新此網頁。在這之前請先參考[執行檔安裝]({{< relref "doc/installation/from-binary.zh-tw.md" >}})方式。 ## Windows @@ -29,11 +29,11 @@ menu: choco install gitea ``` -也可以參考[執行檔安裝]({{< relref "from-binary.zh-tw.md" >}})方式。 +也可以參考[執行檔安裝]({{< relref "doc/installation/from-binary.zh-tw.md" >}})方式。 ## macOS -目前我們只支援透過 `brew` 來安裝套件。假如您尚未使用 [Homebrew](http://brew.sh/),您就必須參考[執行檔安裝]({{< relref "from-binary.zh-tw.md" >}})方式。透過 `brew` 安裝 Gitea,您只需要執行底下指令: +目前我們只支援透過 `brew` 來安裝套件。假如您尚未使用 [Homebrew](http://brew.sh/),您就必須參考[執行檔安裝]({{< relref "doc/installation/from-binary.zh-tw.md" >}})方式。透過 `brew` 安裝 Gitea,您只需要執行底下指令: ``` brew tap go-gitea/gitea diff --git a/docs/content/doc/installation/from-source.en-us.md b/docs/content/doc/installation/from-source.en-us.md index 57db1a36aec..dfded3d9fd1 100644 --- a/docs/content/doc/installation/from-source.en-us.md +++ b/docs/content/doc/installation/from-source.en-us.md @@ -35,8 +35,7 @@ executable path, you will have to manage this yourself. **Note 2**: Go version {{< min-go-version >}} or higher is required. However, it is recommended to obtain the same version as our continuous integration, see the advice given in -Hacking on -Gitea +[Hacking on Gitea]({{< relref "doc/development/hacking-on-gitea.en-us.md" >}}) **Table of Contents** @@ -133,6 +132,8 @@ If pre-built frontend files are present it is possible to only build the backend TAGS="bindata" make backend ``` +Webpack source maps are by default enabled in development builds and disabled in production builds. They can be enabled by setting the `ENABLE_SOURCEMAP=true` environment variable. + ## Test After following the steps above, a `gitea` binary will be available in the working directory. diff --git a/docs/content/doc/installation/from-source.zh-cn.md b/docs/content/doc/installation/from-source.zh-cn.md index 9cd835ae58d..d9f54ba3c11 100644 --- a/docs/content/doc/installation/from-source.zh-cn.md +++ b/docs/content/doc/installation/from-source.zh-cn.md @@ -103,7 +103,3 @@ GOOS=linux GOARCH=arm64 make build ```bash CC=aarch64-unknown-linux-gnu-gcc GOOS=linux GOARCH=arm64 TAGS="bindata sqlite sqlite_unlock_notify" make build ``` - -## 需要帮助? - -如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "support.zh-cn.md" >}}) diff --git a/docs/content/doc/usage/actions/act-runner.en-us.md b/docs/content/doc/usage/actions/act-runner.en-us.md index ef8ed35019f..b654fe2efa4 100644 --- a/docs/content/doc/usage/actions/act-runner.en-us.md +++ b/docs/content/doc/usage/actions/act-runner.en-us.md @@ -172,6 +172,40 @@ It is because the act runner will run jobs in docker containers, so it needs to As mentioned, you can remove it if you want to run jobs in the host directly. To be clear, the "host" actually means the container which is running the act runner now, instead of the host machine. +### Configuring cache when starting a Runner using docker image + +If you do not intend to use `actions/cache` in workflow, you can ignore this section. + +If you use `actions/cache` without any additional configuration, it will return the following error: +> Failed to restore: getCacheEntry failed: connect ETIMEDOUT IP:PORT + +The error occurs because the runner container and job container are on different networks, so the job container cannot access the runner container. + +Therefore, it is essential to configure the cache action to ensure its proper functioning. Follow these steps: + +- 1.Obtain the LAN IP address of the host machine where the runner container is running. +- 2.Find an available port number on the host machine where the runner container is running. +- 3.Configure the following settings in the configuration file: + +```yaml +cache: + enabled: true + dir: "" + # Use the LAN IP obtained in step 1 + host: "192.168.8.17" + # Use the port number obtained in step 2 + port: 8088 +``` + +- 4.When starting the container, map the cache port to the host machine: + +```bash +docker run \ + --name gitea-docker-runner \ + -p 8088:8088 \ + -d gitea/act_runner:nightly +``` + ### Labels The labels of a runner are used to determine which jobs the runner can run, and how to run them. diff --git a/docs/content/doc/usage/actions/act-runner.zh-cn.md b/docs/content/doc/usage/actions/act-runner.zh-cn.md index dc73f4cd6fb..8d0899feac7 100644 --- a/docs/content/doc/usage/actions/act-runner.zh-cn.md +++ b/docs/content/doc/usage/actions/act-runner.zh-cn.md @@ -169,6 +169,39 @@ docker run \ 如前所述,如果要在主机上直接运行Job,可以将其移除。 需要明确的是,这里的 "主机" 实际上指的是当前运行 Act Runner的容器,而不是主机机器本身。 +### 当您使用 Docker 镜像启动 Runner,如何配置 Cache + +如果你不打算在工作流中使用 `actions/cache`,你可以忽略本段。 + +如果您在使用 `actions/cache` 时没有进行额外的配置,将会返回以下错误信息: +> Failed to restore: getCacheEntry failed: connect ETIMEDOUT IP:PORT + +这个错误的原因是 runner 容器和作业容器位于不同的网络中,因此作业容器无法访问 runner 容器。 +因此,配置 cache 动作以确保其正常运行是非常重要的。请按照以下步骤操作: + +- 1.获取 Runner 容器所在主机的 LAN(本地局域网) IP 地址。 +- 2.获取一个 Runner 容器所在主机的空闲端口号。 +- 3.在配置文件中如下配置: + +```yaml +cache: +enabled: true +dir: "" +# 使用步骤 1. 获取的 LAN IP +host: "192.168.8.17" +# 使用步骤 2. 获取的端口号 +port: 8088 +``` + +- 4.启动容器时, 将 Cache 端口映射至主机。 + +```bash +docker run \ + --name gitea-docker-runner \ + -p 8088:8088 \ + -d gitea/act_runner:nightly +``` + ### 标签 Runner的标签用于确定Runner可以运行哪些Job以及如何运行它们。 diff --git a/docs/content/doc/usage/labels.en-us.md b/docs/content/doc/usage/labels.en-us.md index bf60951d8c6..8467f7e0372 100644 --- a/docs/content/doc/usage/labels.en-us.md +++ b/docs/content/doc/usage/labels.en-us.md @@ -27,7 +27,7 @@ For organizations, you can define organization-wide labels that are shared with Labels have a mandatory name, a mandatory color, an optional description, and must either be exclusive or not (see `Scoped Labels` below). -When you create a repository, you can ensure certain labels exist by using the `Issue Labels` option. This option lists a number of available label sets that are [configured globally on your instance](../customizing-gitea/#labels). Its contained labels will all be created as well while creating the repository. +When you create a repository, you can ensure certain labels exist by using the `Issue Labels` option. This option lists a number of available label sets that are [configured globally on your instance](../administration/customizing-gitea/#labels). Its contained labels will all be created as well while creating the repository. ## Scoped Labels diff --git a/docs/content/doc/usage/labels.zh-cn.md b/docs/content/doc/usage/labels.zh-cn.md index 10fef72e750..07dd2bf8543 100644 --- a/docs/content/doc/usage/labels.zh-cn.md +++ b/docs/content/doc/usage/labels.zh-cn.md @@ -27,7 +27,7 @@ menu: 标签具有必填的名称和颜色,可选的描述,以及必须是独占的或非独占的(见下面的“作用域标签”)。 -当您创建一个仓库时,可以通过使用 `工单标签(Issue Labels)` 选项来选择标签集。该选项列出了一些在您的实例上 [全局配置的可用标签集](../customizing-gitea/#labels)。在创建仓库时,这些标签也将被创建。 +当您创建一个仓库时,可以通过使用 `工单标签(Issue Labels)` 选项来选择标签集。该选项列出了一些在您的实例上 [全局配置的可用标签集](../administration/customizing-gitea/#labels)。在创建仓库时,这些标签也将被创建。 ## 作用域标签 diff --git a/docs/content/doc/usage/packages/cran.zh-cn.md b/docs/content/doc/usage/packages/cran.zh-cn.md new file mode 100644 index 00000000000..fec9a56feb1 --- /dev/null +++ b/docs/content/doc/usage/packages/cran.zh-cn.md @@ -0,0 +1,93 @@ +--- +date: "2023-01-01T00:00:00+00:00" +title: "CRAN 软件包注册表" +slug: "cran" +draft: false +toc: false +menu: + sidebar: + parent: "packages" + name: "CRAN" + weight: 35 + identifier: "cran" +--- + +# CRAN 软件包注册表 + +将 [R](https://www.r-project.org/) 软件包发布到您的用户或组织的类似 [CRAN](https://cran.r-project.org/) 的注册表。 + +**目录** + +{{< toc >}} + +## 要求 + +要使用CRAN软件包注册表,您需要安装 [R](https://cran.r-project.org/)。 + +## 配置软件包注册表 + +要注册软件包注册表,您需要将其添加到 `Rprofile.site` 文件中,可以是系统级别、用户级别 `~/.Rprofile` 或项目级别: + +``` +options("repos" = c(getOption("repos"), c(gitea="https://gitea.example.com/api/packages/{owner}/cran"))) +``` + +| 参数 | 描述 | +| ------- | -------------- | +| `owner` | 软件包的所有者 | + +如果需要提供凭据,可以将它们嵌入到URL(`https://user:password@gitea.example.com/...`)中。 + +## 发布软件包 + +要发布 R 软件包,请执行带有软件包内容的 HTTP `PUT` 操作。 + +源代码软件包: + +``` +PUT https://gitea.example.com/api/packages/{owner}/cran/src +``` + +| 参数 | 描述 | +| ------- | -------------- | +| `owner` | 软件包的所有者 | + +二进制软件包: + +``` +PUT https://gitea.example.com/api/packages/{owner}/cran/bin?platform={platform}&rversion={rversion} +``` + +| 参数 | 描述 | +| ---------- | -------------- | +| `owner` | 软件包的所有者 | +| `platform` | 平台的名称 | +| `rversion` | 二进制的R版本 | + +例如: + +```shell +curl --user your_username:your_password_or_token \ + --upload-file path/to/package.zip \ + https://gitea.example.com/api/packages/testuser/cran/bin?platform=windows&rversion=4.2 +``` + +如果同名和版本的软件包已存在,则无法发布软件包。您必须首先删除现有的软件包。 + +## 安装软件包 + +要从软件包注册表中安装R软件包,请执行以下命令: + +```shell +install.packages("{package_name}") +``` + +| 参数 | 描述 | +| -------------- | ----------------- | +| `package_name` | The package name. | + +例如: + +```shell +install.packages("testpackage") +``` diff --git a/docs/content/doc/usage/permissions.en-us.md b/docs/content/doc/usage/permissions.en-us.md index 0778f6943f7..655c67de86c 100644 --- a/docs/content/doc/usage/permissions.en-us.md +++ b/docs/content/doc/usage/permissions.en-us.md @@ -25,7 +25,7 @@ Gitea supports permissions for repository so that you can give different access ## Unit -In Gitea, we call a sub module of a repository `Unit`. Now we have following units. +In Gitea, we call a sub module of a repository `Unit`. Now we have following possible units. | Name | Description | Permissions | | --------------- | ---------------------------------------------------- | ----------- | @@ -37,6 +37,8 @@ In Gitea, we call a sub module of a repository `Unit`. Now we have following uni | ExternalWiki | Link to an external wiki | Read | | ExternalTracker | Link to an external issue tracker | Read | | Projects | The URL to the template repository | Read Write | +| Packages | Packages which linked to this repository | Read Write | +| Actions | Review actions logs or restart/cacnel pipelines | Read Write | | Settings | Manage the repository | Admin | With different permissions, people could do different things with these units. @@ -51,6 +53,8 @@ With different permissions, people could do different things with these units. | ExternalWiki | Link to an external wiki | - | - | | ExternalTracker | Link to an external issue tracker | - | - | | Projects | View the boards | Change issues across boards | - | +| Packages | View the packages | Upload/Delete packages | - | +| Actions | View the Actions logs | Approve / Cancel / Restart | - | | Settings | - | - | Manage the repository | And there are some differences for permissions between individual repositories and organization repositories. @@ -60,16 +64,27 @@ And there are some differences for permissions between individual repositories a For individual repositories, the creators are the only owners of repositories and have no limit to change anything of this repository or delete it. Repositories owners could add collaborators to help maintain the repositories. Collaborators could have `Read`, `Write` and `Admin` permissions. +For a private repository, the experience is similar to visiting an anonymous public repository. You have access to all the available content within the repository, including the ability to clone the code, create issues, respond to issue comments, submit pull requests, and more. If you have 'Write' permission, you can push code to specific branches of the repository, provided it's permitted by the branch protection rules. Additionally, you can make changes to the wiki pages. With 'Admin' permission, you have the ability to modify the repository's settings. + +But you cannot delete or transfer this repository if you are not that repository's owner. + ## Organization Repository -Different from individual repositories, the owner of organization repositories are the owner team of this organization. +For individual repositories, the owner is the user who created it. For organization repositories, the owners are the members of the owner team on this organization. All the permissions depends on the team permission settings. -### Team +### Owner Team -A team in an organization has unit permissions settings. It can have members and repositories scope. A team could access all the repositories in this organization or special repositories changed by the owner team. A team could also be allowed to create new -repositories. +The owner team will be created when the organization is created, and the creator will become the first member of the owner team. The owner team cannot be deleted and there is at least one member. -The owner team will be created when the organization is created, and the creator will become the first member of the owner team. -Every member of an organization must be in at least one team. The owner team cannot be deleted and only -members of the owner team can create a new team. An admin team can be created to manage some of the repositories, whose members can do anything with these repositories. -The Generate team can be created by the owner team to do the operations allowed by their permissions. +### Admin Team + +When creating teams, there are two types of teams. One is the admin team, another is the general team. An admin team can be created to manage some of the repositories, whose members can do anything with these repositories. Only members of the owner or admin team can create a new team. + +### General Team + +A general team in an organization has unit permissions settings. It can have members and repositories scope. + +- A team could access all the repositories in this organization or special repositories. +- A team could also be allowed to create new repositories or not. + +The General team can be created to do the operations allowed by their permissions. One member could join multiple teams. diff --git a/docs/content/doc/usage/pull-request.en-us.md b/docs/content/doc/usage/pull-request.en-us.md index feb8f848c95..f9f4b385558 100644 --- a/docs/content/doc/usage/pull-request.en-us.md +++ b/docs/content/doc/usage/pull-request.en-us.md @@ -66,4 +66,4 @@ The first value of the list will be used in helpers. ## Pull Request Templates -You can find more information about pull request templates at the page [Issue and Pull Request templates](../issue-pull-request-templates). +You can find more information about pull request templates at the page [Issue and Pull Request templates](issue-pull-request-templates). diff --git a/docs/content/doc/usage/pull-request.zh-tw.md b/docs/content/doc/usage/pull-request.zh-tw.md index 54cd1783210..9bbfa87863f 100644 --- a/docs/content/doc/usage/pull-request.zh-tw.md +++ b/docs/content/doc/usage/pull-request.zh-tw.md @@ -31,4 +31,4 @@ WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP] ## 合併請求範本 -您可以在[問題與合併請求範本](../issue-pull-request-templates)找到更多關於合併請求範本的資訊。 +您可以在[問題與合併請求範本](issue-pull-request-templates)找到更多關於合併請求範本的資訊。 diff --git a/docs/content/page/index.en-us.md b/docs/content/page/index.en-us.md index dc450b64c90..0b6be622b46 100644 --- a/docs/content/page/index.en-us.md +++ b/docs/content/page/index.en-us.md @@ -301,6 +301,6 @@ You can try it out using [the online demo](https://try.gitea.io/). - [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) - [github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb) -## Software and Service Support +## Integrated support -- [Drone](https://github.com/drone/drone) (CI) + Please visit [Awesome Gitea](https://gitea.com/gitea/awesome-gitea/) to get more third-party integrated support diff --git a/docs/content/page/index.zh-cn.md b/docs/content/page/index.zh-cn.md index da25d8ee136..ee214b630ac 100644 --- a/docs/content/page/index.zh-cn.md +++ b/docs/content/page/index.zh-cn.md @@ -64,10 +64,6 @@ Gitea的首要目标是创建一个极易安装,运行非常快速,安装和 - [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) - [github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb) -## 软件及服务支持 +## 集成支持 -- [Drone](https://github.com/drone/drone) (CI) - -## 需要帮助? - -如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "support.zh-cn.md" >}}) +请访问 [Awesome Gitea](https://gitea.com/gitea/awesome-gitea/) 获得更多的第三方集成支持 diff --git a/docs/content/page/index.zh-tw.md b/docs/content/page/index.zh-tw.md index 368be02de96..ebff6aacd3c 100644 --- a/docs/content/page/index.zh-tw.md +++ b/docs/content/page/index.zh-tw.md @@ -282,6 +282,6 @@ Gitea 是從 [Gogs](http://gogs.io) Fork 出來的,請閱讀部落格文章 [G - [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) - [github.com/denisenkom/go-mssqldb](https://github.com/denisenkom/go-mssqldb) -## 軟體和服務支援 +## 集成支持 -- [Drone](https://github.com/drone/drone) (CI) +請訪問 [Awesome Gitea](https://gitea.com/gitea/awesome-gitea/) 獲得更多的第三方集成支持 diff --git a/go.mod b/go.mod index b77e350f0ba..e92c08836e2 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/golang-jwt/jwt/v4 v4.5.0 - github.com/google/go-github/v51 v51.0.0 + github.com/google/go-github/v52 v52.0.0 github.com/google/pprof v0.0.0-20230502171905-255e3b9b56de github.com/google/uuid v1.3.0 github.com/gorilla/feeds v1.1.1 @@ -107,10 +107,10 @@ require ( github.com/yuin/goldmark v1.5.4 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 github.com/yuin/goldmark-meta v1.1.0 - golang.org/x/crypto v0.8.0 + golang.org/x/crypto v0.9.0 golang.org/x/image v0.7.0 golang.org/x/net v0.10.0 - golang.org/x/oauth2 v0.7.0 + golang.org/x/oauth2 v0.8.0 golang.org/x/sys v0.8.0 golang.org/x/text v0.9.0 golang.org/x/tools v0.8.0 @@ -136,7 +136,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230528122434-6f98819771a1 // indirect github.com/RoaringBitmap/roaring v1.2.3 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect github.com/andybalholm/brotli v1.0.5 // indirect diff --git a/go.sum b/go.sum index 8e5b728aac9..0a3e409690e 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,8 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cq github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= -github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756 h1:L6S7kR7SlhQKplIBpkra3s6yhcZV51lhRnXmYc4HohI= -github.com/ProtonMail/go-crypto v0.0.0-20230426101702-58e86b294756/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= +github.com/ProtonMail/go-crypto v0.0.0-20230528122434-6f98819771a1 h1:JMDGhoQvXNTqH6Y3MC0IUw6tcZvaUdujNqzK2HYWZc8= +github.com/ProtonMail/go-crypto v0.0.0-20230528122434-6f98819771a1/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -227,6 +227,7 @@ github.com/bufbuild/connect-go v1.7.0/go.mod h1:GmMJYR6orFqD0Y6ZgX8pwQ8j9baizDrI github.com/buildkite/terminal-to-html/v3 v3.7.0 h1:chdLUSpiOj/A4v3dzxyOqixXI6aw7IDA6Dk77FXsvNU= github.com/buildkite/terminal-to-html/v3 v3.7.0/go.mod h1:g0ME1XqbkBSgXR9YmlIHcJIjzaMyWW+HbsG0rPb5puo= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE= github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= @@ -564,8 +565,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v51 v51.0.0 h1:KCjsbgPV28VoRftdP+K2mQL16jniUsLAJknsOVKwHyU= -github.com/google/go-github/v51 v51.0.0/go.mod h1:kZj/rn/c1lSUbr/PFWl2hhusPV7a5XNYKcwPrd5L3Us= +github.com/google/go-github/v52 v52.0.0 h1:uyGWOY+jMQ8GVGSX8dkSwCzlehU3WfdxQ7GweO/JP7M= +github.com/google/go-github/v52 v52.0.0/go.mod h1:WJV6VEEUPuMo5pXqqa2ZCZEdbQqua4zAk2MZTIo+m+4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-tpm v0.1.2-0.20190725015402-ae6dd98980d4/go.mod h1:H9HbmUG2YgV/PHITkO7p6wxEEj/v5nlsVWIwumwH2NI= @@ -1318,10 +1319,11 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1430,8 +1432,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/models/actions/runner.go b/models/actions/runner.go index f1638eb0baa..bf703c444b6 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -69,7 +69,11 @@ func (r *ActionRunner) BelongsToOwnerType() types.OwnerType { return types.OwnerTypeRepository } if r.OwnerID != 0 { - return types.OwnerTypeOrganization + if r.Owner.Type == user_model.UserTypeOrganization { + return types.OwnerTypeOrganization + } else if r.Owner.Type == user_model.UserTypeIndividual { + return types.OwnerTypeIndividual + } } return types.OwnerTypeSystemGlobal } diff --git a/models/activities/statistic.go b/models/activities/statistic.go index 138f4d8fe9e..9d379cd0c4f 100644 --- a/models/activities/statistic.go +++ b/models/activities/statistic.go @@ -21,7 +21,7 @@ import ( type Statistic struct { Counter struct { User, Org, PublicKey, - Repo, Watch, Star, Action, Access, + Repo, Watch, Star, Access, Issue, IssueClosed, IssueOpen, Comment, Oauth, Follow, Mirror, Release, AuthSource, Webhook, @@ -55,7 +55,6 @@ func GetStatistic() (stats Statistic) { stats.Counter.Repo, _ = repo_model.CountRepositories(db.DefaultContext, repo_model.CountRepositoryOptions{}) stats.Counter.Watch, _ = e.Count(new(repo_model.Watch)) stats.Counter.Star, _ = e.Count(new(repo_model.Star)) - stats.Counter.Action, _ = db.EstimateCount(db.DefaultContext, new(Action)) stats.Counter.Access, _ = e.Count(new(access_model.Access)) type IssueCount struct { @@ -83,7 +82,7 @@ func GetStatistic() (stats Statistic) { Find(&stats.Counter.IssueByRepository) } - issueCounts := []IssueCount{} + var issueCounts []IssueCount _ = e.Select("COUNT(*) AS count, is_closed").Table("issue").GroupBy("is_closed").Find(&issueCounts) for _, c := range issueCounts { diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index bda0668c45f..53a5c28b4a5 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -51,14 +51,6 @@ func (app *OAuth2Application) TableName() string { return "oauth2_application" } -// PrimaryRedirectURI returns the first redirect uri or an empty string if empty -func (app *OAuth2Application) PrimaryRedirectURI() string { - if len(app.RedirectURIs) == 0 { - return "" - } - return app.RedirectURIs[0] -} - // ContainsRedirectURI checks if redirectURI is allowed for app func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool { if !app.ConfidentialClient { diff --git a/models/auth/token.go b/models/auth/token.go index 3f9f117f733..9374fe38c22 100644 --- a/models/auth/token.go +++ b/models/auth/token.go @@ -112,6 +112,15 @@ func NewAccessToken(t *AccessToken) error { return err } +// DisplayPublicOnly whether to display this as a public-only token. +func (t *AccessToken) DisplayPublicOnly() bool { + publicOnly, err := t.Scope.PublicOnly() + if err != nil { + return false + } + return publicOnly +} + func getAccessTokenIDFromCache(token string) int64 { if successfulAccessTokenCache == nil { return 0 diff --git a/models/auth/token_scope.go b/models/auth/token_scope.go index 06c89fecc2e..61e684ea272 100644 --- a/models/auth/token_scope.go +++ b/models/auth/token_scope.go @@ -6,113 +6,122 @@ package auth import ( "fmt" "strings" + + "code.gitea.io/gitea/models/perm" +) + +// AccessTokenScopeCategory represents the scope category for an access token +type AccessTokenScopeCategory int + +const ( + AccessTokenScopeCategoryActivityPub = iota + AccessTokenScopeCategoryAdmin + AccessTokenScopeCategoryMisc + AccessTokenScopeCategoryNotification + AccessTokenScopeCategoryOrganization + AccessTokenScopeCategoryPackage + AccessTokenScopeCategoryIssue + AccessTokenScopeCategoryRepository + AccessTokenScopeCategoryUser +) + +// AllAccessTokenScopeCategories contains all access token scope categories +var AllAccessTokenScopeCategories = []AccessTokenScopeCategory{ + AccessTokenScopeCategoryActivityPub, + AccessTokenScopeCategoryAdmin, + AccessTokenScopeCategoryMisc, + AccessTokenScopeCategoryNotification, + AccessTokenScopeCategoryOrganization, + AccessTokenScopeCategoryPackage, + AccessTokenScopeCategoryIssue, + AccessTokenScopeCategoryRepository, + AccessTokenScopeCategoryUser, +} + +// AccessTokenScopeLevel represents the access levels without a given scope category +type AccessTokenScopeLevel int + +const ( + NoAccess AccessTokenScopeLevel = iota + Read + Write ) // AccessTokenScope represents the scope for an access token. type AccessTokenScope string +// for all categories, write implies read const ( - AccessTokenScopeAll AccessTokenScope = "all" + AccessTokenScopeAll AccessTokenScope = "all" + AccessTokenScopePublicOnly AccessTokenScope = "public-only" // limited to public orgs/repos - AccessTokenScopeRepo AccessTokenScope = "repo" - AccessTokenScopeRepoStatus AccessTokenScope = "repo:status" - AccessTokenScopePublicRepo AccessTokenScope = "public_repo" + AccessTokenScopeReadActivityPub AccessTokenScope = "read:activitypub" + AccessTokenScopeWriteActivityPub AccessTokenScope = "write:activitypub" - AccessTokenScopeAdminOrg AccessTokenScope = "admin:org" - AccessTokenScopeWriteOrg AccessTokenScope = "write:org" - AccessTokenScopeReadOrg AccessTokenScope = "read:org" + AccessTokenScopeReadAdmin AccessTokenScope = "read:admin" + AccessTokenScopeWriteAdmin AccessTokenScope = "write:admin" - AccessTokenScopeAdminPublicKey AccessTokenScope = "admin:public_key" - AccessTokenScopeWritePublicKey AccessTokenScope = "write:public_key" - AccessTokenScopeReadPublicKey AccessTokenScope = "read:public_key" + AccessTokenScopeReadMisc AccessTokenScope = "read:misc" + AccessTokenScopeWriteMisc AccessTokenScope = "write:misc" - AccessTokenScopeAdminRepoHook AccessTokenScope = "admin:repo_hook" - AccessTokenScopeWriteRepoHook AccessTokenScope = "write:repo_hook" - AccessTokenScopeReadRepoHook AccessTokenScope = "read:repo_hook" + AccessTokenScopeReadNotification AccessTokenScope = "read:notification" + AccessTokenScopeWriteNotification AccessTokenScope = "write:notification" - AccessTokenScopeAdminOrgHook AccessTokenScope = "admin:org_hook" + AccessTokenScopeReadOrganization AccessTokenScope = "read:organization" + AccessTokenScopeWriteOrganization AccessTokenScope = "write:organization" - AccessTokenScopeAdminUserHook AccessTokenScope = "admin:user_hook" + AccessTokenScopeReadPackage AccessTokenScope = "read:package" + AccessTokenScopeWritePackage AccessTokenScope = "write:package" - AccessTokenScopeNotification AccessTokenScope = "notification" + AccessTokenScopeReadIssue AccessTokenScope = "read:issue" + AccessTokenScopeWriteIssue AccessTokenScope = "write:issue" - AccessTokenScopeUser AccessTokenScope = "user" - AccessTokenScopeReadUser AccessTokenScope = "read:user" - AccessTokenScopeUserEmail AccessTokenScope = "user:email" - AccessTokenScopeUserFollow AccessTokenScope = "user:follow" + AccessTokenScopeReadRepository AccessTokenScope = "read:repository" + AccessTokenScopeWriteRepository AccessTokenScope = "write:repository" - AccessTokenScopeDeleteRepo AccessTokenScope = "delete_repo" - - AccessTokenScopePackage AccessTokenScope = "package" - AccessTokenScopeWritePackage AccessTokenScope = "write:package" - AccessTokenScopeReadPackage AccessTokenScope = "read:package" - AccessTokenScopeDeletePackage AccessTokenScope = "delete:package" - - AccessTokenScopeAdminGPGKey AccessTokenScope = "admin:gpg_key" - AccessTokenScopeWriteGPGKey AccessTokenScope = "write:gpg_key" - AccessTokenScopeReadGPGKey AccessTokenScope = "read:gpg_key" - - AccessTokenScopeAdminApplication AccessTokenScope = "admin:application" - AccessTokenScopeWriteApplication AccessTokenScope = "write:application" - AccessTokenScopeReadApplication AccessTokenScope = "read:application" - - AccessTokenScopeSudo AccessTokenScope = "sudo" + AccessTokenScopeReadUser AccessTokenScope = "read:user" + AccessTokenScopeWriteUser AccessTokenScope = "write:user" ) -// AccessTokenScopeBitmap represents a bitmap of access token scopes. -type AccessTokenScopeBitmap uint64 +// accessTokenScopeBitmap represents a bitmap of access token scopes. +type accessTokenScopeBitmap uint64 // Bitmap of each scope, including the child scopes. const ( - // AccessTokenScopeAllBits is the bitmap of all access token scopes, except `sudo`. - AccessTokenScopeAllBits AccessTokenScopeBitmap = AccessTokenScopeRepoBits | - AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits | AccessTokenScopeAdminUserHookBits | - AccessTokenScopeNotificationBits | AccessTokenScopeUserBits | AccessTokenScopeDeleteRepoBits | - AccessTokenScopePackageBits | AccessTokenScopeAdminGPGKeyBits | AccessTokenScopeAdminApplicationBits + // AccessTokenScopeAllBits is the bitmap of all access token scopes + accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits | + accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits | + accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits | + accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits - AccessTokenScopeRepoBits AccessTokenScopeBitmap = 1< 64 scopes, @@ -120,61 +129,110 @@ const ( ) // allAccessTokenScopes contains all access token scopes. -// The order is important: parent scope must precedes child scopes. +// The order is important: parent scope must precede child scopes. var allAccessTokenScopes = []AccessTokenScope{ - AccessTokenScopeRepo, AccessTokenScopeRepoStatus, AccessTokenScopePublicRepo, - AccessTokenScopeAdminOrg, AccessTokenScopeWriteOrg, AccessTokenScopeReadOrg, - AccessTokenScopeAdminPublicKey, AccessTokenScopeWritePublicKey, AccessTokenScopeReadPublicKey, - AccessTokenScopeAdminRepoHook, AccessTokenScopeWriteRepoHook, AccessTokenScopeReadRepoHook, - AccessTokenScopeAdminOrgHook, - AccessTokenScopeAdminUserHook, - AccessTokenScopeNotification, - AccessTokenScopeUser, AccessTokenScopeReadUser, AccessTokenScopeUserEmail, AccessTokenScopeUserFollow, - AccessTokenScopeDeleteRepo, - AccessTokenScopePackage, AccessTokenScopeWritePackage, AccessTokenScopeReadPackage, AccessTokenScopeDeletePackage, - AccessTokenScopeAdminGPGKey, AccessTokenScopeWriteGPGKey, AccessTokenScopeReadGPGKey, - AccessTokenScopeAdminApplication, AccessTokenScopeWriteApplication, AccessTokenScopeReadApplication, - AccessTokenScopeSudo, + AccessTokenScopePublicOnly, + AccessTokenScopeWriteActivityPub, AccessTokenScopeReadActivityPub, + AccessTokenScopeWriteAdmin, AccessTokenScopeReadAdmin, + AccessTokenScopeWriteMisc, AccessTokenScopeReadMisc, + AccessTokenScopeWriteNotification, AccessTokenScopeReadNotification, + AccessTokenScopeWriteOrganization, AccessTokenScopeReadOrganization, + AccessTokenScopeWritePackage, AccessTokenScopeReadPackage, + AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue, + AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository, + AccessTokenScopeWriteUser, AccessTokenScopeReadUser, } // allAccessTokenScopeBits contains all access token scopes. -var allAccessTokenScopeBits = map[AccessTokenScope]AccessTokenScopeBitmap{ - AccessTokenScopeRepo: AccessTokenScopeRepoBits, - AccessTokenScopeRepoStatus: AccessTokenScopeRepoStatusBits, - AccessTokenScopePublicRepo: AccessTokenScopePublicRepoBits, - AccessTokenScopeAdminOrg: AccessTokenScopeAdminOrgBits, - AccessTokenScopeWriteOrg: AccessTokenScopeWriteOrgBits, - AccessTokenScopeReadOrg: AccessTokenScopeReadOrgBits, - AccessTokenScopeAdminPublicKey: AccessTokenScopeAdminPublicKeyBits, - AccessTokenScopeWritePublicKey: AccessTokenScopeWritePublicKeyBits, - AccessTokenScopeReadPublicKey: AccessTokenScopeReadPublicKeyBits, - AccessTokenScopeAdminRepoHook: AccessTokenScopeAdminRepoHookBits, - AccessTokenScopeWriteRepoHook: AccessTokenScopeWriteRepoHookBits, - AccessTokenScopeReadRepoHook: AccessTokenScopeReadRepoHookBits, - AccessTokenScopeAdminOrgHook: AccessTokenScopeAdminOrgHookBits, - AccessTokenScopeAdminUserHook: AccessTokenScopeAdminUserHookBits, - AccessTokenScopeNotification: AccessTokenScopeNotificationBits, - AccessTokenScopeUser: AccessTokenScopeUserBits, - AccessTokenScopeReadUser: AccessTokenScopeReadUserBits, - AccessTokenScopeUserEmail: AccessTokenScopeUserEmailBits, - AccessTokenScopeUserFollow: AccessTokenScopeUserFollowBits, - AccessTokenScopeDeleteRepo: AccessTokenScopeDeleteRepoBits, - AccessTokenScopePackage: AccessTokenScopePackageBits, - AccessTokenScopeWritePackage: AccessTokenScopeWritePackageBits, - AccessTokenScopeReadPackage: AccessTokenScopeReadPackageBits, - AccessTokenScopeDeletePackage: AccessTokenScopeDeletePackageBits, - AccessTokenScopeAdminGPGKey: AccessTokenScopeAdminGPGKeyBits, - AccessTokenScopeWriteGPGKey: AccessTokenScopeWriteGPGKeyBits, - AccessTokenScopeReadGPGKey: AccessTokenScopeReadGPGKeyBits, - AccessTokenScopeAdminApplication: AccessTokenScopeAdminApplicationBits, - AccessTokenScopeWriteApplication: AccessTokenScopeWriteApplicationBits, - AccessTokenScopeReadApplication: AccessTokenScopeReadApplicationBits, - AccessTokenScopeSudo: AccessTokenScopeSudoBits, +var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{ + AccessTokenScopeAll: accessTokenScopeAllBits, + AccessTokenScopePublicOnly: accessTokenScopePublicOnlyBits, + AccessTokenScopeReadActivityPub: accessTokenScopeReadActivityPubBits, + AccessTokenScopeWriteActivityPub: accessTokenScopeWriteActivityPubBits, + AccessTokenScopeReadAdmin: accessTokenScopeReadAdminBits, + AccessTokenScopeWriteAdmin: accessTokenScopeWriteAdminBits, + AccessTokenScopeReadMisc: accessTokenScopeReadMiscBits, + AccessTokenScopeWriteMisc: accessTokenScopeWriteMiscBits, + AccessTokenScopeReadNotification: accessTokenScopeReadNotificationBits, + AccessTokenScopeWriteNotification: accessTokenScopeWriteNotificationBits, + AccessTokenScopeReadOrganization: accessTokenScopeReadOrganizationBits, + AccessTokenScopeWriteOrganization: accessTokenScopeWriteOrganizationBits, + AccessTokenScopeReadPackage: accessTokenScopeReadPackageBits, + AccessTokenScopeWritePackage: accessTokenScopeWritePackageBits, + AccessTokenScopeReadIssue: accessTokenScopeReadIssueBits, + AccessTokenScopeWriteIssue: accessTokenScopeWriteIssueBits, + AccessTokenScopeReadRepository: accessTokenScopeReadRepositoryBits, + AccessTokenScopeWriteRepository: accessTokenScopeWriteRepositoryBits, + AccessTokenScopeReadUser: accessTokenScopeReadUserBits, + AccessTokenScopeWriteUser: accessTokenScopeWriteUserBits, } -// Parse parses the scope string into a bitmap, thus removing possible duplicates. -func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) { - var bitmap AccessTokenScopeBitmap +// readAccessTokenScopes maps a scope category to the read permission scope +var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]AccessTokenScope{ + Read: { + AccessTokenScopeCategoryActivityPub: AccessTokenScopeReadActivityPub, + AccessTokenScopeCategoryAdmin: AccessTokenScopeReadAdmin, + AccessTokenScopeCategoryMisc: AccessTokenScopeReadMisc, + AccessTokenScopeCategoryNotification: AccessTokenScopeReadNotification, + AccessTokenScopeCategoryOrganization: AccessTokenScopeReadOrganization, + AccessTokenScopeCategoryPackage: AccessTokenScopeReadPackage, + AccessTokenScopeCategoryIssue: AccessTokenScopeReadIssue, + AccessTokenScopeCategoryRepository: AccessTokenScopeReadRepository, + AccessTokenScopeCategoryUser: AccessTokenScopeReadUser, + }, + Write: { + AccessTokenScopeCategoryActivityPub: AccessTokenScopeWriteActivityPub, + AccessTokenScopeCategoryAdmin: AccessTokenScopeWriteAdmin, + AccessTokenScopeCategoryMisc: AccessTokenScopeWriteMisc, + AccessTokenScopeCategoryNotification: AccessTokenScopeWriteNotification, + AccessTokenScopeCategoryOrganization: AccessTokenScopeWriteOrganization, + AccessTokenScopeCategoryPackage: AccessTokenScopeWritePackage, + AccessTokenScopeCategoryIssue: AccessTokenScopeWriteIssue, + AccessTokenScopeCategoryRepository: AccessTokenScopeWriteRepository, + AccessTokenScopeCategoryUser: AccessTokenScopeWriteUser, + }, +} + +// GetRequiredScopes gets the specific scopes for a given level and categories +func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope { + scopes := make([]AccessTokenScope, 0, len(scopeCategories)) + for _, cat := range scopeCategories { + scopes = append(scopes, accessTokenScopes[level][cat]) + } + return scopes +} + +// ContainsCategory checks if a list of categories contains a specific category +func ContainsCategory(categories []AccessTokenScopeCategory, category AccessTokenScopeCategory) bool { + for _, c := range categories { + if c == category { + return true + } + } + return false +} + +// GetScopeLevelFromAccessMode converts permission access mode to scope level +func GetScopeLevelFromAccessMode(mode perm.AccessMode) AccessTokenScopeLevel { + switch mode { + case perm.AccessModeNone: + return NoAccess + case perm.AccessModeRead: + return Read + case perm.AccessModeWrite: + return Write + case perm.AccessModeAdmin: + return Write + case perm.AccessModeOwner: + return Write + default: + return NoAccess + } +} + +// parse the scope string into a bitmap, thus removing possible duplicates. +func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) { + var bitmap accessTokenScopeBitmap // The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code remainingScopes := string(s) @@ -196,7 +254,7 @@ func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) { continue } if singleScope == AccessTokenScopeAll { - bitmap |= AccessTokenScopeAllBits + bitmap |= accessTokenScopeAllBits continue } @@ -217,26 +275,42 @@ func (s AccessTokenScope) StringSlice() []string { // Normalize returns a normalized scope string without any duplicates. func (s AccessTokenScope) Normalize() (AccessTokenScope, error) { - bitmap, err := s.Parse() + bitmap, err := s.parse() if err != nil { return "", err } - return bitmap.ToScope(), nil + return bitmap.toScope(), nil } -// HasScope returns true if the string has the given scope -func (s AccessTokenScope) HasScope(scope AccessTokenScope) (bool, error) { - bitmap, err := s.Parse() +// PublicOnly checks if this token scope is limited to public resources +func (s AccessTokenScope) PublicOnly() (bool, error) { + bitmap, err := s.parse() if err != nil { return false, err } - return bitmap.HasScope(scope) + return bitmap.hasScope(AccessTokenScopePublicOnly) } // HasScope returns true if the string has the given scope -func (bitmap AccessTokenScopeBitmap) HasScope(scope AccessTokenScope) (bool, error) { +func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) { + bitmap, err := s.parse() + if err != nil { + return false, err + } + + for _, s := range scopes { + if has, err := bitmap.hasScope(s); !has || err != nil { + return has, err + } + } + + return true, nil +} + +// hasScope returns true if the string has the given scope +func (bitmap accessTokenScopeBitmap) hasScope(scope AccessTokenScope) (bool, error) { expectedBits, ok := allAccessTokenScopeBits[scope] if !ok { return false, fmt.Errorf("invalid access token scope: %s", scope) @@ -245,17 +319,17 @@ func (bitmap AccessTokenScopeBitmap) HasScope(scope AccessTokenScope) (bool, err return bitmap&expectedBits == expectedBits, nil } -// ToScope returns a normalized scope string without any duplicates. -func (bitmap AccessTokenScopeBitmap) ToScope() AccessTokenScope { +// toScope returns a normalized scope string without any duplicates. +func (bitmap accessTokenScopeBitmap) toScope() AccessTokenScope { var scopes []string // iterate over all scopes, and reconstruct the bitmap // if the reconstructed bitmap doesn't change, then the scope is already included - var reconstruct AccessTokenScopeBitmap + var reconstruct accessTokenScopeBitmap for _, singleScope := range allAccessTokenScopes { // no need for error checking here, since we know the scope is valid - if ok, _ := bitmap.HasScope(singleScope); ok { + if ok, _ := bitmap.hasScope(singleScope); ok { current := reconstruct | allAccessTokenScopeBits[singleScope] if current == reconstruct { continue @@ -269,7 +343,7 @@ func (bitmap AccessTokenScopeBitmap) ToScope() AccessTokenScope { scope := AccessTokenScope(strings.Join(scopes, ",")) scope = AccessTokenScope(strings.ReplaceAll( string(scope), - "repo,admin:org,admin:public_key,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application", + "write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", )) return scope diff --git a/models/auth/token_scope_test.go b/models/auth/token_scope_test.go index b96a5fd4694..a6097e45d7f 100644 --- a/models/auth/token_scope_test.go +++ b/models/auth/token_scope_test.go @@ -4,44 +4,35 @@ package auth import ( + "fmt" "testing" "github.com/stretchr/testify/assert" ) +type scopeTestNormalize struct { + in AccessTokenScope + out AccessTokenScope + err error +} + func TestAccessTokenScope_Normalize(t *testing.T) { - tests := []struct { - in AccessTokenScope - out AccessTokenScope - err error - }{ + tests := []scopeTestNormalize{ {"", "", nil}, - {"repo", "repo", nil}, - {"repo,repo:status", "repo", nil}, - {"repo,public_repo", "repo", nil}, - {"admin:public_key,write:public_key", "admin:public_key", nil}, - {"admin:public_key,read:public_key", "admin:public_key", nil}, - {"write:public_key,read:public_key", "write:public_key", nil}, // read is include in write - {"admin:repo_hook,write:repo_hook", "admin:repo_hook", nil}, - {"admin:repo_hook,read:repo_hook", "admin:repo_hook", nil}, - {"repo,admin:repo_hook,read:repo_hook", "repo", nil}, // admin:repo_hook is a child scope of repo - {"repo,read:repo_hook", "repo", nil}, // read:repo_hook is a child scope of repo - {"user", "user", nil}, - {"user,read:user", "user", nil}, - {"user,admin:org,write:org", "admin:org,user", nil}, - {"admin:org,write:org,user", "admin:org,user", nil}, - {"package", "package", nil}, - {"package,write:package", "package", nil}, - {"package,write:package,delete:package", "package", nil}, - {"write:package,read:package", "write:package", nil}, // read is include in write - {"write:package,delete:package", "write:package,delete:package", nil}, // write and delete are not include in each other - {"admin:gpg_key", "admin:gpg_key", nil}, - {"admin:gpg_key,write:gpg_key", "admin:gpg_key", nil}, - {"admin:gpg_key,write:gpg_key,user", "user,admin:gpg_key", nil}, - {"admin:application,write:application,user", "user,admin:application", nil}, + {"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil}, {"all", "all", nil}, - {"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application", "all", nil}, - {"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application,sudo", "all,sudo", nil}, + {"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil}, + {"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil}, + } + + for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} { + tests = append(tests, + scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil}, + scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil}, + scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%[1]s,read:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil}, + scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil}, + scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil}, + ) } for _, test := range tests { @@ -53,31 +44,46 @@ func TestAccessTokenScope_Normalize(t *testing.T) { } } +type scopeTestHasScope struct { + in AccessTokenScope + scope AccessTokenScope + out bool + err error +} + func TestAccessTokenScope_HasScope(t *testing.T) { - tests := []struct { - in AccessTokenScope - scope AccessTokenScope - out bool - err error - }{ - {"repo", "repo", true, nil}, - {"repo", "repo:status", true, nil}, - {"repo", "public_repo", true, nil}, - {"repo", "admin:org", false, nil}, - {"repo", "admin:public_key", false, nil}, - {"repo:status", "repo", false, nil}, - {"repo:status", "public_repo", false, nil}, - {"admin:org", "write:org", true, nil}, - {"admin:org", "read:org", true, nil}, - {"admin:org", "admin:org", true, nil}, - {"user", "read:user", true, nil}, - {"package", "write:package", true, nil}, + tests := []scopeTestHasScope{ + {"read:admin", "write:package", false, nil}, + {"all", "write:package", true, nil}, + {"write:package", "all", false, nil}, + {"public-only", "read:issue", false, nil}, + } + + for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} { + tests = append(tests, + scopeTestHasScope{ + AccessTokenScope(fmt.Sprintf("read:%s", scope)), + AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil, + }, + scopeTestHasScope{ + AccessTokenScope(fmt.Sprintf("write:%s", scope)), + AccessTokenScope(fmt.Sprintf("write:%s", scope)), true, nil, + }, + scopeTestHasScope{ + AccessTokenScope(fmt.Sprintf("write:%s", scope)), + AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil, + }, + scopeTestHasScope{ + AccessTokenScope(fmt.Sprintf("read:%s", scope)), + AccessTokenScope(fmt.Sprintf("write:%s", scope)), false, nil, + }, + ) } for _, test := range tests { t.Run(string(test.in), func(t *testing.T) { - scope, err := test.in.HasScope(test.scope) - assert.Equal(t, test.out, scope) + hasScope, err := test.in.HasScope(test.scope) + assert.Equal(t, test.out, hasScope) assert.Equal(t, test.err, err) }) } diff --git a/models/db/context.go b/models/db/context.go index 670f6272aa9..59be1e13891 100644 --- a/models/db/context.go +++ b/models/db/context.go @@ -9,7 +9,6 @@ import ( "xorm.io/builder" "xorm.io/xorm" - "xorm.io/xorm/schemas" ) // DefaultContext is the default context to run xorm queries in @@ -241,30 +240,6 @@ func TableName(bean interface{}) string { return x.TableName(bean) } -// EstimateCount returns an estimate of total number of rows in table -func EstimateCount(ctx context.Context, bean interface{}) (int64, error) { - e := GetEngine(ctx) - e.Context(ctx) - - var rows int64 - var err error - tablename := TableName(bean) - switch x.Dialect().URI().DBType { - case schemas.MYSQL: - _, err = e.Context(ctx).SQL("SELECT table_rows FROM information_schema.tables WHERE tables.table_name = ? AND tables.table_schema = ?;", tablename, x.Dialect().URI().DBName).Get(&rows) - case schemas.POSTGRES: - // the table can live in multiple schemas of a postgres database - // See https://wiki.postgresql.org/wiki/Count_estimate - tablename = x.TableName(bean, true) - _, err = e.Context(ctx).SQL("SELECT reltuples::bigint AS estimate FROM pg_class WHERE oid = ?::regclass;", tablename).Get(&rows) - case schemas.MSSQL: - _, err = e.Context(ctx).SQL("sp_spaceused ?;", tablename).Get(&rows) - default: - return e.Context(ctx).Count(tablename) - } - return rows, err -} - // InTransaction returns true if the engine is in a transaction otherwise return false func InTransaction(ctx context.Context) bool { _, ok := inTransaction(ctx) diff --git a/models/db/index.go b/models/db/index.go index 7609d8fb6e6..259ddd6ade7 100644 --- a/models/db/index.go +++ b/models/db/index.go @@ -71,10 +71,31 @@ func postgresGetNextResourceIndex(ctx context.Context, tableName string, groupID return strconv.ParseInt(string(res[0]["max_index"]), 10, 64) } +func mysqlGetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) { + if _, err := GetEngine(ctx).Exec(fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ + "VALUES (?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1", + tableName), groupID); err != nil { + return 0, err + } + + var idx int64 + _, err := GetEngine(ctx).SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ?", tableName), groupID).Get(&idx) + if err != nil { + return 0, err + } + if idx == 0 { + return 0, errors.New("cannot get the correct index") + } + return idx, nil +} + // GetNextResourceIndex generates a resource index, it must run in the same transaction where the resource is created func GetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) { - if setting.Database.Type.IsPostgreSQL() { + switch { + case setting.Database.Type.IsPostgreSQL(): return postgresGetNextResourceIndex(ctx, tableName, groupID) + case setting.Database.Type.IsMySQL(): + return mysqlGetNextResourceIndex(ctx, tableName, groupID) } e := GetEngine(ctx) diff --git a/models/git/commit_status.go b/models/git/commit_status.go index 6028e466493..a018bb0553a 100644 --- a/models/git/commit_status.go +++ b/models/git/commit_status.go @@ -64,10 +64,32 @@ func postgresGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) return strconv.ParseInt(string(res[0]["max_index"]), 10, 64) } +func mysqlGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) { + if _, err := db.GetEngine(ctx).Exec("INSERT INTO `commit_status_index` (repo_id, sha, max_index) "+ + "VALUES (?,?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1", + repoID, sha); err != nil { + return 0, err + } + + var idx int64 + _, err := db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?", + repoID, sha).Get(&idx) + if err != nil { + return 0, err + } + if idx == 0 { + return 0, errors.New("cannot get the correct index") + } + return idx, nil +} + // GetNextCommitStatusIndex retried 3 times to generate a resource index func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) { - if setting.Database.Type.IsPostgreSQL() { + switch { + case setting.Database.Type.IsPostgreSQL(): return postgresGetCommitStatusIndex(ctx, repoID, sha) + case setting.Database.Type.IsMySQL(): + return mysqlGetCommitStatusIndex(ctx, repoID, sha) } e := db.GetEngine(ctx) @@ -75,7 +97,7 @@ func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (in // try to update the max_index to next value, and acquire the write-lock for the record res, err := e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha) if err != nil { - return 0, err + return 0, fmt.Errorf("update failed: %w", err) } affected, err := res.RowsAffected() if err != nil { @@ -86,18 +108,18 @@ func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (in _, errIns := e.Exec("INSERT INTO `commit_status_index` (repo_id, sha, max_index) VALUES (?, ?, 0)", repoID, sha) res, err = e.Exec("UPDATE `commit_status_index` SET max_index=max_index+1 WHERE repo_id=? AND sha=?", repoID, sha) if err != nil { - return 0, err + return 0, fmt.Errorf("update2 failed: %w", err) } affected, err = res.RowsAffected() if err != nil { - return 0, err + return 0, fmt.Errorf("RowsAffected failed: %w", err) } // if the update still can not update any records, the record must not exist and there must be some errors (insert error) if affected == 0 { if errIns == nil { return 0, errors.New("impossible error when GetNextCommitStatusIndex, insert and update both succeeded but no record is updated") } - return 0, errIns + return 0, fmt.Errorf("insert failed: %w", errIns) } } @@ -105,7 +127,7 @@ func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (in var newIdx int64 has, err := e.SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id=? AND sha=?", repoID, sha).Get(&newIdx) if err != nil { - return 0, err + return 0, fmt.Errorf("select failed: %w", err) } if !has { return 0, errors.New("impossible error when GetNextCommitStatusIndex, upsert succeeded but no record can be selected") diff --git a/models/issues/issue.go b/models/issues/issue.go index 5015824e9b3..eab18f4892c 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -687,6 +687,8 @@ func (issue *Issue) HasOriginalAuthor() bool { return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0 } +var ErrIssueMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned issues has been readched") + // IsPinned returns if a Issue is pinned func (issue *Issue) IsPinned() bool { return issue.PinOrder != 0 @@ -707,7 +709,7 @@ func (issue *Issue) Pin(ctx context.Context, user *user_model.User) error { // Check if the maximum allowed Pins reached if maxPin >= setting.Repository.Issue.MaxPinned { - return fmt.Errorf("You have reached the max number of pinned Issues") + return ErrIssueMaxPinReached } _, err = db.GetEngine(ctx).Table("issue"). @@ -856,10 +858,15 @@ func GetPinnedIssues(ctx context.Context, repoID int64, isPull bool) ([]*Issue, // IsNewPinnedAllowed returns if a new Issue or Pull request can be pinned func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, error) { var maxPin int - _, err := db.GetEngine(ctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&maxPin) + _, err := db.GetEngine(ctx).SQL("SELECT COUNT(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ? AND pin_order > 0", repoID, isPull).Get(&maxPin) if err != nil { return false, err } return maxPin < setting.Repository.Issue.MaxPinned, nil } + +// IsErrIssueMaxPinReached returns if the error is, that the User can't pin more Issues +func IsErrIssueMaxPinReached(err error) bool { + return err == ErrIssueMaxPinReached +} diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 6ddadd27ed4..dad21c14776 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -34,7 +34,7 @@ func (issues IssueList) getRepoIDs() []int64 { } // LoadRepositories loads issues' all repositories -func (issues IssueList) LoadRepositories(ctx context.Context) ([]*repo_model.Repository, error) { +func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.RepositoryList, error) { if len(issues) == 0 { return nil, nil } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 6f56c009a20..a781d07c1be 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -495,6 +495,8 @@ var migrations = []Migration{ NewMigration("Add Actions Artifact table", v1_20.CreateActionArtifactTable), // v258 -> 259 NewMigration("Add PinOrder Column", v1_20.AddPinOrderToIssue), + // v259 -> 260 + NewMigration("Convert scoped access tokens", v1_20.ConvertScopedAccessTokens), // to modify later NewMigration("Add size limit on repository", v1_20.AddSizeLimitOnRepo), } diff --git a/models/migrations/v1_20/main_test.go b/models/migrations/v1_20/main_test.go new file mode 100644 index 00000000000..92a1a9f6226 --- /dev/null +++ b/models/migrations/v1_20/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_20 //nolint + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" +) + +func TestMain(m *testing.M) { + base.MainTest(m) +} diff --git a/models/migrations/v1_20/v259.go b/models/migrations/v1_20/v259.go new file mode 100644 index 00000000000..5b8ced4ad7b --- /dev/null +++ b/models/migrations/v1_20/v259.go @@ -0,0 +1,360 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_20 //nolint + +import ( + "fmt" + "strings" + + "code.gitea.io/gitea/modules/log" + + "xorm.io/xorm" +) + +// unknownAccessTokenScope represents the scope for an access token that isn't +// known be an old token or a new token. +type unknownAccessTokenScope string + +// AccessTokenScope represents the scope for an access token. +type AccessTokenScope string + +// for all categories, write implies read +const ( + AccessTokenScopeAll AccessTokenScope = "all" + AccessTokenScopePublicOnly AccessTokenScope = "public-only" // limited to public orgs/repos + + AccessTokenScopeReadActivityPub AccessTokenScope = "read:activitypub" + AccessTokenScopeWriteActivityPub AccessTokenScope = "write:activitypub" + + AccessTokenScopeReadAdmin AccessTokenScope = "read:admin" + AccessTokenScopeWriteAdmin AccessTokenScope = "write:admin" + + AccessTokenScopeReadMisc AccessTokenScope = "read:misc" + AccessTokenScopeWriteMisc AccessTokenScope = "write:misc" + + AccessTokenScopeReadNotification AccessTokenScope = "read:notification" + AccessTokenScopeWriteNotification AccessTokenScope = "write:notification" + + AccessTokenScopeReadOrganization AccessTokenScope = "read:organization" + AccessTokenScopeWriteOrganization AccessTokenScope = "write:organization" + + AccessTokenScopeReadPackage AccessTokenScope = "read:package" + AccessTokenScopeWritePackage AccessTokenScope = "write:package" + + AccessTokenScopeReadIssue AccessTokenScope = "read:issue" + AccessTokenScopeWriteIssue AccessTokenScope = "write:issue" + + AccessTokenScopeReadRepository AccessTokenScope = "read:repository" + AccessTokenScopeWriteRepository AccessTokenScope = "write:repository" + + AccessTokenScopeReadUser AccessTokenScope = "read:user" + AccessTokenScopeWriteUser AccessTokenScope = "write:user" +) + +// accessTokenScopeBitmap represents a bitmap of access token scopes. +type accessTokenScopeBitmap uint64 + +// Bitmap of each scope, including the child scopes. +const ( + // AccessTokenScopeAllBits is the bitmap of all access token scopes + accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits | + accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits | + accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits | + accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits + + accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota + + accessTokenScopeReadActivityPubBits accessTokenScopeBitmap = 1 << iota + accessTokenScopeWriteActivityPubBits accessTokenScopeBitmap = 1< 64 scopes, + // refactoring the whole implementation in this file (and only this file) is needed. +) + +// allAccessTokenScopes contains all access token scopes. +// The order is important: parent scope must precede child scopes. +var allAccessTokenScopes = []AccessTokenScope{ + AccessTokenScopePublicOnly, + AccessTokenScopeWriteActivityPub, AccessTokenScopeReadActivityPub, + AccessTokenScopeWriteAdmin, AccessTokenScopeReadAdmin, + AccessTokenScopeWriteMisc, AccessTokenScopeReadMisc, + AccessTokenScopeWriteNotification, AccessTokenScopeReadNotification, + AccessTokenScopeWriteOrganization, AccessTokenScopeReadOrganization, + AccessTokenScopeWritePackage, AccessTokenScopeReadPackage, + AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue, + AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository, + AccessTokenScopeWriteUser, AccessTokenScopeReadUser, +} + +// allAccessTokenScopeBits contains all access token scopes. +var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{ + AccessTokenScopeAll: accessTokenScopeAllBits, + AccessTokenScopePublicOnly: accessTokenScopePublicOnlyBits, + AccessTokenScopeReadActivityPub: accessTokenScopeReadActivityPubBits, + AccessTokenScopeWriteActivityPub: accessTokenScopeWriteActivityPubBits, + AccessTokenScopeReadAdmin: accessTokenScopeReadAdminBits, + AccessTokenScopeWriteAdmin: accessTokenScopeWriteAdminBits, + AccessTokenScopeReadMisc: accessTokenScopeReadMiscBits, + AccessTokenScopeWriteMisc: accessTokenScopeWriteMiscBits, + AccessTokenScopeReadNotification: accessTokenScopeReadNotificationBits, + AccessTokenScopeWriteNotification: accessTokenScopeWriteNotificationBits, + AccessTokenScopeReadOrganization: accessTokenScopeReadOrganizationBits, + AccessTokenScopeWriteOrganization: accessTokenScopeWriteOrganizationBits, + AccessTokenScopeReadPackage: accessTokenScopeReadPackageBits, + AccessTokenScopeWritePackage: accessTokenScopeWritePackageBits, + AccessTokenScopeReadIssue: accessTokenScopeReadIssueBits, + AccessTokenScopeWriteIssue: accessTokenScopeWriteIssueBits, + AccessTokenScopeReadRepository: accessTokenScopeReadRepositoryBits, + AccessTokenScopeWriteRepository: accessTokenScopeWriteRepositoryBits, + AccessTokenScopeReadUser: accessTokenScopeReadUserBits, + AccessTokenScopeWriteUser: accessTokenScopeWriteUserBits, +} + +// hasScope returns true if the string has the given scope +func (bitmap accessTokenScopeBitmap) hasScope(scope AccessTokenScope) (bool, error) { + expectedBits, ok := allAccessTokenScopeBits[scope] + if !ok { + return false, fmt.Errorf("invalid access token scope: %s", scope) + } + + return bitmap&expectedBits == expectedBits, nil +} + +// toScope returns a normalized scope string without any duplicates. +func (bitmap accessTokenScopeBitmap) toScope(unknownScopes *[]unknownAccessTokenScope) AccessTokenScope { + var scopes []string + + // Preserve unknown scopes, and put them at the beginning so that it's clear + // when debugging. + if unknownScopes != nil { + for _, unknownScope := range *unknownScopes { + scopes = append(scopes, string(unknownScope)) + } + } + + // iterate over all scopes, and reconstruct the bitmap + // if the reconstructed bitmap doesn't change, then the scope is already included + var reconstruct accessTokenScopeBitmap + + for _, singleScope := range allAccessTokenScopes { + // no need for error checking here, since we know the scope is valid + if ok, _ := bitmap.hasScope(singleScope); ok { + current := reconstruct | allAccessTokenScopeBits[singleScope] + if current == reconstruct { + continue + } + + reconstruct = current + scopes = append(scopes, string(singleScope)) + } + } + + scope := AccessTokenScope(strings.Join(scopes, ",")) + scope = AccessTokenScope(strings.ReplaceAll( + string(scope), + "write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", + "all", + )) + return scope +} + +// parse the scope string into a bitmap, thus removing possible duplicates. +func (s AccessTokenScope) parse() (accessTokenScopeBitmap, *[]unknownAccessTokenScope) { + var bitmap accessTokenScopeBitmap + var unknownScopes []unknownAccessTokenScope + + // The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code + remainingScopes := string(s) + for len(remainingScopes) > 0 { + i := strings.IndexByte(remainingScopes, ',') + var v string + if i < 0 { + v = remainingScopes + remainingScopes = "" + } else if i+1 >= len(remainingScopes) { + v = remainingScopes[:i] + remainingScopes = "" + } else { + v = remainingScopes[:i] + remainingScopes = remainingScopes[i+1:] + } + singleScope := AccessTokenScope(v) + if singleScope == "" { + continue + } + if singleScope == AccessTokenScopeAll { + bitmap |= accessTokenScopeAllBits + continue + } + + bits, ok := allAccessTokenScopeBits[singleScope] + if !ok { + unknownScopes = append(unknownScopes, unknownAccessTokenScope(string(singleScope))) + } + bitmap |= bits + } + + return bitmap, &unknownScopes +} + +// NormalizePreservingUnknown returns a normalized scope string without any +// duplicates. Unknown scopes are included. +func (s AccessTokenScope) NormalizePreservingUnknown() AccessTokenScope { + bitmap, unknownScopes := s.parse() + + return bitmap.toScope(unknownScopes) +} + +// OldAccessTokenScope represents the scope for an access token. +type OldAccessTokenScope string + +const ( + OldAccessTokenScopeAll OldAccessTokenScope = "all" + + OldAccessTokenScopeRepo OldAccessTokenScope = "repo" + OldAccessTokenScopeRepoStatus OldAccessTokenScope = "repo:status" + OldAccessTokenScopePublicRepo OldAccessTokenScope = "public_repo" + + OldAccessTokenScopeAdminOrg OldAccessTokenScope = "admin:org" + OldAccessTokenScopeWriteOrg OldAccessTokenScope = "write:org" + OldAccessTokenScopeReadOrg OldAccessTokenScope = "read:org" + + OldAccessTokenScopeAdminPublicKey OldAccessTokenScope = "admin:public_key" + OldAccessTokenScopeWritePublicKey OldAccessTokenScope = "write:public_key" + OldAccessTokenScopeReadPublicKey OldAccessTokenScope = "read:public_key" + + OldAccessTokenScopeAdminRepoHook OldAccessTokenScope = "admin:repo_hook" + OldAccessTokenScopeWriteRepoHook OldAccessTokenScope = "write:repo_hook" + OldAccessTokenScopeReadRepoHook OldAccessTokenScope = "read:repo_hook" + + OldAccessTokenScopeAdminOrgHook OldAccessTokenScope = "admin:org_hook" + + OldAccessTokenScopeNotification OldAccessTokenScope = "notification" + + OldAccessTokenScopeUser OldAccessTokenScope = "user" + OldAccessTokenScopeReadUser OldAccessTokenScope = "read:user" + OldAccessTokenScopeUserEmail OldAccessTokenScope = "user:email" + OldAccessTokenScopeUserFollow OldAccessTokenScope = "user:follow" + + OldAccessTokenScopeDeleteRepo OldAccessTokenScope = "delete_repo" + + OldAccessTokenScopePackage OldAccessTokenScope = "package" + OldAccessTokenScopeWritePackage OldAccessTokenScope = "write:package" + OldAccessTokenScopeReadPackage OldAccessTokenScope = "read:package" + OldAccessTokenScopeDeletePackage OldAccessTokenScope = "delete:package" + + OldAccessTokenScopeAdminGPGKey OldAccessTokenScope = "admin:gpg_key" + OldAccessTokenScopeWriteGPGKey OldAccessTokenScope = "write:gpg_key" + OldAccessTokenScopeReadGPGKey OldAccessTokenScope = "read:gpg_key" + + OldAccessTokenScopeAdminApplication OldAccessTokenScope = "admin:application" + OldAccessTokenScopeWriteApplication OldAccessTokenScope = "write:application" + OldAccessTokenScopeReadApplication OldAccessTokenScope = "read:application" + + OldAccessTokenScopeSudo OldAccessTokenScope = "sudo" +) + +var accessTokenScopeMap = map[OldAccessTokenScope][]AccessTokenScope{ + OldAccessTokenScopeAll: {AccessTokenScopeAll}, + OldAccessTokenScopeRepo: {AccessTokenScopeWriteRepository}, + OldAccessTokenScopeRepoStatus: {AccessTokenScopeWriteRepository}, + OldAccessTokenScopePublicRepo: {AccessTokenScopePublicOnly, AccessTokenScopeWriteRepository}, + OldAccessTokenScopeAdminOrg: {AccessTokenScopeWriteOrganization}, + OldAccessTokenScopeWriteOrg: {AccessTokenScopeWriteOrganization}, + OldAccessTokenScopeReadOrg: {AccessTokenScopeReadOrganization}, + OldAccessTokenScopeAdminPublicKey: {AccessTokenScopeWriteUser}, + OldAccessTokenScopeWritePublicKey: {AccessTokenScopeWriteUser}, + OldAccessTokenScopeReadPublicKey: {AccessTokenScopeReadUser}, + OldAccessTokenScopeAdminRepoHook: {AccessTokenScopeWriteRepository}, + OldAccessTokenScopeWriteRepoHook: {AccessTokenScopeWriteRepository}, + OldAccessTokenScopeReadRepoHook: {AccessTokenScopeReadRepository}, + OldAccessTokenScopeAdminOrgHook: {AccessTokenScopeWriteOrganization}, + OldAccessTokenScopeNotification: {AccessTokenScopeWriteNotification}, + OldAccessTokenScopeUser: {AccessTokenScopeWriteUser}, + OldAccessTokenScopeReadUser: {AccessTokenScopeReadUser}, + OldAccessTokenScopeUserEmail: {AccessTokenScopeWriteUser}, + OldAccessTokenScopeUserFollow: {AccessTokenScopeWriteUser}, + OldAccessTokenScopeDeleteRepo: {AccessTokenScopeWriteRepository}, + OldAccessTokenScopePackage: {AccessTokenScopeWritePackage}, + OldAccessTokenScopeWritePackage: {AccessTokenScopeWritePackage}, + OldAccessTokenScopeReadPackage: {AccessTokenScopeReadPackage}, + OldAccessTokenScopeDeletePackage: {AccessTokenScopeWritePackage}, + OldAccessTokenScopeAdminGPGKey: {AccessTokenScopeWriteUser}, + OldAccessTokenScopeWriteGPGKey: {AccessTokenScopeWriteUser}, + OldAccessTokenScopeReadGPGKey: {AccessTokenScopeReadUser}, + OldAccessTokenScopeAdminApplication: {AccessTokenScopeWriteUser}, + OldAccessTokenScopeWriteApplication: {AccessTokenScopeWriteUser}, + OldAccessTokenScopeReadApplication: {AccessTokenScopeReadUser}, + OldAccessTokenScopeSudo: {AccessTokenScopeWriteAdmin}, +} + +type AccessToken struct { + ID int64 `xorm:"pk autoincr"` + Scope string +} + +func ConvertScopedAccessTokens(x *xorm.Engine) error { + var tokens []*AccessToken + + if err := x.Find(&tokens); err != nil { + return err + } + + for _, token := range tokens { + var scopes []string + allNewScopesMap := make(map[AccessTokenScope]bool) + for _, oldScope := range strings.Split(token.Scope, ",") { + if newScopes, exists := accessTokenScopeMap[OldAccessTokenScope(oldScope)]; exists { + for _, newScope := range newScopes { + allNewScopesMap[newScope] = true + } + } else { + log.Debug("access token scope not recognized as old token scope %s; preserving it", oldScope) + scopes = append(scopes, oldScope) + } + } + + for s := range allNewScopesMap { + scopes = append(scopes, string(s)) + } + scope := AccessTokenScope(strings.Join(scopes, ",")) + + // normalize the scope + normScope := scope.NormalizePreservingUnknown() + + token.Scope = string(normScope) + + // update the db entry with the new scope + if _, err := x.Cols("scope").ID(token.ID).Update(token); err != nil { + return err + } + } + + return nil +} diff --git a/models/migrations/v1_20/v259_test.go b/models/migrations/v1_20/v259_test.go new file mode 100644 index 00000000000..5bc9a71391e --- /dev/null +++ b/models/migrations/v1_20/v259_test.go @@ -0,0 +1,110 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_20 //nolint + +import ( + "sort" + "strings" + "testing" + + "code.gitea.io/gitea/models/migrations/base" + + "github.com/stretchr/testify/assert" +) + +type testCase struct { + Old OldAccessTokenScope + New AccessTokenScope +} + +func createOldTokenScope(scopes ...OldAccessTokenScope) OldAccessTokenScope { + s := make([]string, 0, len(scopes)) + for _, os := range scopes { + s = append(s, string(os)) + } + return OldAccessTokenScope(strings.Join(s, ",")) +} + +func createNewTokenScope(scopes ...AccessTokenScope) AccessTokenScope { + s := make([]string, 0, len(scopes)) + for _, os := range scopes { + s = append(s, string(os)) + } + return AccessTokenScope(strings.Join(s, ",")) +} + +func Test_ConvertScopedAccessTokens(t *testing.T) { + tests := []testCase{ + { + createOldTokenScope(OldAccessTokenScopeRepo, OldAccessTokenScopeUserFollow), + createNewTokenScope(AccessTokenScopeWriteRepository, AccessTokenScopeWriteUser), + }, + { + createOldTokenScope(OldAccessTokenScopeUser, OldAccessTokenScopeWritePackage, OldAccessTokenScopeSudo), + createNewTokenScope(AccessTokenScopeWriteAdmin, AccessTokenScopeWritePackage, AccessTokenScopeWriteUser), + }, + { + createOldTokenScope(), + createNewTokenScope(), + }, + { + createOldTokenScope(OldAccessTokenScopeReadGPGKey, OldAccessTokenScopeReadOrg, OldAccessTokenScopeAll), + createNewTokenScope(AccessTokenScopeAll), + }, + { + createOldTokenScope(OldAccessTokenScopeReadGPGKey, "invalid"), + createNewTokenScope("invalid", AccessTokenScopeReadUser), + }, + } + + // add a test for each individual mapping + for oldScope, newScope := range accessTokenScopeMap { + tests = append(tests, testCase{ + oldScope, + createNewTokenScope(newScope...), + }) + } + + x, deferable := base.PrepareTestEnv(t, 0, new(AccessToken)) + defer deferable() + if x == nil || t.Failed() { + t.Skip() + return + } + + // verify that no fixtures were loaded + count, err := x.Count(&AccessToken{}) + assert.NoError(t, err) + assert.Equal(t, int64(0), count) + + for _, tc := range tests { + _, err = x.Insert(&AccessToken{ + Scope: string(tc.Old), + }) + assert.NoError(t, err) + } + + // migrate the scopes + err = ConvertScopedAccessTokens(x) + assert.NoError(t, err) + + // migrate the scopes again (migration should be idempotent) + err = ConvertScopedAccessTokens(x) + assert.NoError(t, err) + + tokens := make([]AccessToken, 0) + err = x.Find(&tokens) + assert.NoError(t, err) + assert.Equal(t, len(tests), len(tokens)) + + // sort the tokens (insertion order by auto-incrementing primary key) + sort.Slice(tokens, func(i, j int) bool { + return tokens[i].ID < tokens[j].ID + }) + + // verify that the converted scopes are equal to the expected test result + for idx, newToken := range tokens { + assert.Equal(t, string(tests[idx].New), newToken.Scope) + } +} diff --git a/models/organization/org.go b/models/organization/org.go index 30b76fb1a06..05db6ba15f8 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -710,8 +710,8 @@ func (org *Organization) GetUserTeams(userID int64) ([]*Team, error) { type AccessibleReposEnvironment interface { CountRepos() (int64, error) RepoIDs(page, pageSize int) ([]int64, error) - Repos(page, pageSize int) ([]*repo_model.Repository, error) - MirrorRepos() ([]*repo_model.Repository, error) + Repos(page, pageSize int) (repo_model.RepositoryList, error) + MirrorRepos() (repo_model.RepositoryList, error) AddKeyword(keyword string) SetSort(db.SearchOrderBy) } @@ -813,7 +813,7 @@ func (env *accessibleReposEnv) RepoIDs(page, pageSize int) ([]int64, error) { Find(&repoIDs) } -func (env *accessibleReposEnv) Repos(page, pageSize int) ([]*repo_model.Repository, error) { +func (env *accessibleReposEnv) Repos(page, pageSize int) (repo_model.RepositoryList, error) { repoIDs, err := env.RepoIDs(page, pageSize) if err != nil { return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err) @@ -842,7 +842,7 @@ func (env *accessibleReposEnv) MirrorRepoIDs() ([]int64, error) { Find(&repoIDs) } -func (env *accessibleReposEnv) MirrorRepos() ([]*repo_model.Repository, error) { +func (env *accessibleReposEnv) MirrorRepos() (repo_model.RepositoryList, error) { repoIDs, err := env.MirrorRepoIDs() if err != nil { return nil, fmt.Errorf("MirrorRepoIDs: %w", err) diff --git a/models/organization/org_repo.go b/models/organization/org_repo.go index 99638916b0b..f7e59928f42 100644 --- a/models/organization/org_repo.go +++ b/models/organization/org_repo.go @@ -11,7 +11,7 @@ import ( ) // GetOrgRepositories get repos belonging to the given organization -func GetOrgRepositories(ctx context.Context, orgID int64) ([]*repo_model.Repository, error) { +func GetOrgRepositories(ctx context.Context, orgID int64) (repo_model.RepositoryList, error) { var orgRepos []*repo_model.Repository return orgRepos, db.GetEngine(ctx).Where("owner_id = ?", orgID).Find(&orgRepos) } diff --git a/models/organization/org_test.go b/models/organization/org_test.go index 27e124a62bc..27a173d497c 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -346,7 +346,7 @@ func TestAccessibleReposEnv_Repos(t *testing.T) { assert.NoError(t, err) repos, err := env.Repos(1, 100) assert.NoError(t, err) - expectedRepos := make([]*repo_model.Repository, len(expectedRepoIDs)) + expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs)) for i, repoID := range expectedRepoIDs { expectedRepos[i] = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) @@ -365,7 +365,7 @@ func TestAccessibleReposEnv_MirrorRepos(t *testing.T) { assert.NoError(t, err) repos, err := env.MirrorRepos() assert.NoError(t, err) - expectedRepos := make([]*repo_model.Repository, len(expectedRepoIDs)) + expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs)) for i, repoID := range expectedRepoIDs { expectedRepos[i] = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) diff --git a/models/organization/team_repo.go b/models/organization/team_repo.go index e6b50ecff7e..1184e392636 100644 --- a/models/organization/team_repo.go +++ b/models/organization/team_repo.go @@ -37,7 +37,7 @@ type SearchTeamRepoOptions struct { } // GetRepositories returns paginated repositories in team of organization. -func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) ([]*repo_model.Repository, error) { +func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (repo_model.RepositoryList, error) { sess := db.GetEngine(ctx) if opts.TeamID > 0 { sess = sess.In("id", diff --git a/models/repo/star.go b/models/repo/star.go index b3d3d795f8f..89bdb7ac05c 100644 --- a/models/repo/star.go +++ b/models/repo/star.go @@ -85,3 +85,17 @@ func GetStargazers(repo *Repository, opts db.ListOptions) ([]*user_model.User, e users := make([]*user_model.User, 0, 8) return users, sess.Find(&users) } + +// ClearRepoStars clears all stars for a repository and from the user that starred it. +// Used when a repository is set to private. +func ClearRepoStars(ctx context.Context, repoID int64) error { + if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars=num_stars-1 WHERE id IN (SELECT `uid` FROM `star` WHERE repo_id = ?)", repoID); err != nil { + return err + } + + if _, err := db.Exec(ctx, "UPDATE `repository` SET num_stars = 0 WHERE id = ?", repoID); err != nil { + return err + } + + return db.DeleteBeans(ctx, Star{RepoID: repoID}) +} diff --git a/models/repo/star_test.go b/models/repo/star_test.go index 7221a6c8ebb..f15ac12ebea 100644 --- a/models/repo/star_test.go +++ b/models/repo/star_test.go @@ -51,3 +51,21 @@ func TestRepository_GetStargazers2(t *testing.T) { assert.NoError(t, err) assert.Len(t, gazers, 0) } + +func TestClearRepoStars(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + const userID = 2 + const repoID = 1 + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + assert.NoError(t, repo_model.StarRepo(userID, repoID, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + assert.NoError(t, repo_model.StarRepo(userID, repoID, false)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + assert.NoError(t, repo_model.ClearRepoStars(db.DefaultContext, repoID)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: userID, RepoID: repoID}) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gazers, err := repo_model.GetStargazers(repo, db.ListOptions{Page: 0}) + assert.NoError(t, err) + assert.Len(t, gazers, 0) +} diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index b2f19196c70..399040580cd 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -19,42 +19,52 @@ import ( const NonexistentID = int64(math.MaxInt64) type testCond struct { - query interface{} - args []interface{} + query any + args []any } +type testOrderBy string + // Cond create a condition with arguments for a test -func Cond(query interface{}, args ...interface{}) interface{} { +func Cond(query any, args ...any) any { return &testCond{query: query, args: args} } -func whereConditions(e db.Engine, conditions []interface{}) db.Engine { +// OrderBy creates "ORDER BY" a test query +func OrderBy(orderBy string) any { + return testOrderBy(orderBy) +} + +func whereOrderConditions(e db.Engine, conditions []any) db.Engine { + orderBy := "id" // query must have the "ORDER BY", otherwise the result is not deterministic for _, condition := range conditions { switch cond := condition.(type) { case *testCond: e = e.Where(cond.query, cond.args...) + case testOrderBy: + orderBy = string(cond) default: e = e.Where(cond) } } - return e + return e.OrderBy(orderBy) } // LoadBeanIfExists loads beans from fixture database if exist -func LoadBeanIfExists(bean interface{}, conditions ...interface{}) (bool, error) { +func LoadBeanIfExists(bean any, conditions ...any) (bool, error) { e := db.GetEngine(db.DefaultContext) - return whereConditions(e, conditions).Get(bean) + return whereOrderConditions(e, conditions).Get(bean) } // BeanExists for testing, check if a bean exists -func BeanExists(t assert.TestingT, bean interface{}, conditions ...interface{}) bool { +func BeanExists(t assert.TestingT, bean any, conditions ...any) bool { exists, err := LoadBeanIfExists(bean, conditions...) assert.NoError(t, err) return exists } // AssertExistsAndLoadBean assert that a bean exists and load it from the test database -func AssertExistsAndLoadBean[T any](t assert.TestingT, bean T, conditions ...interface{}) T { +func AssertExistsAndLoadBean[T any](t assert.TestingT, bean T, conditions ...any) T { exists, err := LoadBeanIfExists(bean, conditions...) assert.NoError(t, err) assert.True(t, exists, @@ -64,9 +74,9 @@ func AssertExistsAndLoadBean[T any](t assert.TestingT, bean T, conditions ...int } // AssertExistsAndLoadMap assert that a row exists and load it from the test database -func AssertExistsAndLoadMap(t assert.TestingT, table string, conditions ...interface{}) map[string]string { +func AssertExistsAndLoadMap(t assert.TestingT, table string, conditions ...any) map[string]string { e := db.GetEngine(db.DefaultContext).Table(table) - res, err := whereConditions(e, conditions).Query() + res, err := whereOrderConditions(e, conditions).Query() assert.NoError(t, err) assert.True(t, len(res) == 1, "Expected to find one row in %s (with conditions %+v), but found %d", @@ -84,15 +94,15 @@ func AssertExistsAndLoadMap(t assert.TestingT, table string, conditions ...inter } // GetCount get the count of a bean -func GetCount(t assert.TestingT, bean interface{}, conditions ...interface{}) int { +func GetCount(t assert.TestingT, bean any, conditions ...any) int { e := db.GetEngine(db.DefaultContext) - count, err := whereConditions(e, conditions).Count(bean) + count, err := whereOrderConditions(e, conditions).Count(bean) assert.NoError(t, err) return int(count) } // AssertNotExistsBean assert that a bean does not exist in the test database -func AssertNotExistsBean(t assert.TestingT, bean interface{}, conditions ...interface{}) { +func AssertNotExistsBean(t assert.TestingT, bean any, conditions ...any) { exists, err := LoadBeanIfExists(bean, conditions...) assert.NoError(t, err) assert.False(t, exists) @@ -100,20 +110,20 @@ func AssertNotExistsBean(t assert.TestingT, bean interface{}, conditions ...inte // AssertExistsIf asserts that a bean exists or does not exist, depending on // what is expected. -func AssertExistsIf(t assert.TestingT, expected bool, bean interface{}, conditions ...interface{}) { +func AssertExistsIf(t assert.TestingT, expected bool, bean any, conditions ...any) { exists, err := LoadBeanIfExists(bean, conditions...) assert.NoError(t, err) assert.Equal(t, expected, exists) } // AssertSuccessfulInsert assert that beans is successfully inserted -func AssertSuccessfulInsert(t assert.TestingT, beans ...interface{}) { +func AssertSuccessfulInsert(t assert.TestingT, beans ...any) { err := db.Insert(db.DefaultContext, beans...) assert.NoError(t, err) } // AssertCount assert the count of a bean -func AssertCount(t assert.TestingT, bean, expected interface{}) { +func AssertCount(t assert.TestingT, bean, expected any) { assert.EqualValues(t, expected, GetCount(t, bean)) } diff --git a/models/user/user.go b/models/user/user.go index 07d8177b6a9..2077d55f513 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -406,6 +406,11 @@ func (u *User) IsIndividual() bool { return u.Type == UserTypeIndividual } +// IsBot returns whether or not the user is of type bot +func (u *User) IsBot() bool { + return u.Type == UserTypeBot +} + // DisplayName returns full name if it's not empty, // returns username otherwise. func (u *User) DisplayName() string { diff --git a/modules/context/permission.go b/modules/context/permission.go index cc53fb99ed7..0f72b8e244d 100644 --- a/modules/context/permission.go +++ b/modules/context/permission.go @@ -111,28 +111,36 @@ func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) { } } -// RequireRepoScopedToken check whether personal access token has repo scope -func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository) { +// CheckRepoScopedToken check whether personal access token has repo scope +func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) { if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true { return } - var err error scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) if ok { // it's a personal access token but not oauth2 token var scopeMatched bool - scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeRepo) + + requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository) + + // check if scope only applies to public resources + publicOnly, err := scope.PublicOnly() if err != nil { ctx.ServerError("HasScope", err) return } - if !scopeMatched && !repo.IsPrivate { - scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopePublicRepo) - if err != nil { - ctx.ServerError("HasScope", err) - return - } + + if publicOnly && repo.IsPrivate { + ctx.Error(http.StatusForbidden) + return } + + scopeMatched, err = scope.HasScope(requiredScopes...) + if err != nil { + ctx.ServerError("HasScope", err) + return + } + if !scopeMatched { ctx.Error(http.StatusForbidden) return diff --git a/modules/git/git.go b/modules/git/git.go index 2e0a16fb5cb..f9c0ed669f4 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -201,23 +201,6 @@ func InitFull(ctx context.Context) (err error) { return syncGitConfig() } -func enableReflogs() error { - if err := configSet("core.logAllRefUpdates", "true"); err != nil { - return err - } - err := configSet("gc.reflogExpire", fmt.Sprintf("%d", setting.Git.Reflog.Expiration)) - return err -} - -func disableReflogs() error { - if err := configUnsetAll("core.logAllRefUpdates", "true"); err != nil { - return err - } else if err := configUnsetAll("gc.reflogExpire", ""); err != nil { - return err - } - return nil -} - // syncGitConfig only modifies gitconfig, won't change global variables (otherwise there will be data-race problem) func syncGitConfig() (err error) { if err = os.MkdirAll(HomeDir(), os.ModePerm); err != nil { @@ -249,16 +232,6 @@ func syncGitConfig() (err error) { return err } - if setting.Git.Reflog.Enabled { - if err := enableReflogs(); err != nil { - return err - } - } else { - if err := disableReflogs(); err != nil { - return err - } - } - if CheckGitVersionAtLeast("2.10") == nil { if err := configSet("receive.advertisePushOptions", "true"); err != nil { return err diff --git a/modules/git/ref.go b/modules/git/ref.go index d3d1320e506..73095f8dbee 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -53,6 +53,11 @@ func (ref *Reference) Commit() (*Commit, error) { return ref.repo.getCommit(ref.Object) } +// ShortName returns the short name of the reference +func (ref *Reference) ShortName() string { + return RefName(ref.Name).ShortName() +} + // RefGroup returns the group type of the reference func (ref *Reference) RefGroup() string { return RefName(ref.Name).RefGroup() diff --git a/modules/log/event_writer_base.go b/modules/log/event_writer_base.go index 1d45d579c03..b8b13e4bc44 100644 --- a/modules/log/event_writer_base.go +++ b/modules/log/event_writer_base.go @@ -68,18 +68,16 @@ func (b *EventWriterBaseImpl) Run(ctx context.Context) { } } - for { - if b.GetPauseChan != nil { - pause := b.GetPauseChan() - if pause != nil { - select { - case <-pause: - case <-ctx.Done(): - return - } + handlePaused := func() { + if pause := b.GetPauseChan(); pause != nil { + select { + case <-pause: + case <-ctx.Done(): } } + } + for { select { case <-ctx.Done(): return @@ -88,6 +86,8 @@ func (b *EventWriterBaseImpl) Run(ctx context.Context) { return } + handlePaused() + if exprRegexp != nil { fileLineCaller := fmt.Sprintf("%s:%d:%s", event.Origin.Filename, event.Origin.Line, event.Origin.Caller) matched := exprRegexp.Match([]byte(fileLineCaller)) || exprRegexp.Match([]byte(event.Origin.MsgSimpleText)) @@ -149,7 +149,7 @@ func eventWriterStartGo(ctx context.Context, w EventWriter, shared bool) { if shared { ctxDesc = "Logger: EventWriter (shared): " + w.GetWriterName() } - writerCtx, writerCancel := newContext(ctx, ctxDesc) + writerCtx, writerCancel := newProcessTypedContext(ctx, ctxDesc) go func() { defer writerCancel() defer close(w.Base().stopped) diff --git a/modules/log/init.go b/modules/log/init.go index 38a3ad60a5b..3fb5200ad70 100644 --- a/modules/log/init.go +++ b/modules/log/init.go @@ -7,16 +7,12 @@ import ( "context" "runtime" "strings" - "sync/atomic" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/util/rotatingfilewriter" ) -var ( - projectPackagePrefix string - processTraceDisabled atomic.Int64 -) +var projectPackagePrefix string func init() { _, filename, _, _ := runtime.Caller(0) @@ -28,25 +24,21 @@ func init() { rotatingfilewriter.ErrorPrintf = FallbackErrorf - process.Trace = func(start bool, pid process.IDType, description string, parentPID process.IDType, typ string) { - // the logger manager has its own mutex lock, so it's safe to use "Load" here - if processTraceDisabled.Load() != 0 { - return - } + process.TraceCallback = func(skip int, start bool, pid process.IDType, description string, parentPID process.IDType, typ string) { if start && parentPID != "" { - Log(1, TRACE, "Start %s: %s (from %s) (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(parentPID, FgYellow), NewColoredValue(typ, Reset)) + Log(skip+1, TRACE, "Start %s: %s (from %s) (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(parentPID, FgYellow), NewColoredValue(typ, Reset)) } else if start { - Log(1, TRACE, "Start %s: %s (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(typ, Reset)) + Log(skip+1, TRACE, "Start %s: %s (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(typ, Reset)) } else { - Log(1, TRACE, "Done %s: %s", NewColoredValue(pid, FgHiYellow), NewColoredValue(description, Reset)) + Log(skip+1, TRACE, "Done %s: %s", NewColoredValue(pid, FgHiYellow), NewColoredValue(description, Reset)) } } } -func newContext(parent context.Context, desc string) (ctx context.Context, cancel context.CancelFunc) { +func newProcessTypedContext(parent context.Context, desc string) (ctx context.Context, cancel context.CancelFunc) { // the "process manager" also calls "log.Trace()" to output logs, so if we want to create new contexts by the manager, we need to disable the trace temporarily - processTraceDisabled.Add(1) - defer processTraceDisabled.Add(-1) + process.TraceLogDisable(true) + defer process.TraceLogDisable(false) ctx, _, cancel = process.GetManager().AddTypedContext(parent, desc, process.SystemProcessType, false) return ctx, cancel } diff --git a/modules/log/logger_impl.go b/modules/log/logger_impl.go index abd72d326fa..903d8cefc2a 100644 --- a/modules/log/logger_impl.go +++ b/modules/log/logger_impl.go @@ -230,7 +230,7 @@ func (l *LoggerImpl) GetLevel() Level { func NewLoggerWithWriters(ctx context.Context, name string, writer ...EventWriter) *LoggerImpl { l := &LoggerImpl{} - l.ctx, l.ctxCancel = newContext(ctx, "Logger: "+name) + l.ctx, l.ctxCancel = newProcessTypedContext(ctx, "Logger: "+name) l.LevelLogger = BaseLoggerToGeneralLogger(l) l.eventWriters = map[string]EventWriter{} l.syncLevelInternal() diff --git a/modules/log/manager.go b/modules/log/manager.go index b5d6cbf8e10..0417bbe6e99 100644 --- a/modules/log/manager.go +++ b/modules/log/manager.go @@ -137,6 +137,6 @@ func GetManager() *LoggerManager { func NewManager() *LoggerManager { m := &LoggerManager{writers: map[string]EventWriter{}, loggers: map[string]*LoggerImpl{}} - m.ctx, m.ctxCancel = newContext(context.Background(), "LoggerManager") + m.ctx, m.ctxCancel = newProcessTypedContext(context.Background(), "LoggerManager") return m } diff --git a/modules/metrics/collector.go b/modules/metrics/collector.go index 94699c161cd..33678256c3c 100755 --- a/modules/metrics/collector.go +++ b/modules/metrics/collector.go @@ -18,7 +18,6 @@ const namespace = "gitea_" // exposes gitea metrics for prometheus type Collector struct { Accesses *prometheus.Desc - Actions *prometheus.Desc Attachments *prometheus.Desc BuildInfo *prometheus.Desc Comments *prometheus.Desc @@ -56,11 +55,6 @@ func NewCollector() Collector { "Number of Accesses", nil, nil, ), - Actions: prometheus.NewDesc( - namespace+"actions", - "Number of Actions", - nil, nil, - ), Attachments: prometheus.NewDesc( namespace+"attachments", "Number of Attachments", @@ -207,7 +201,6 @@ func NewCollector() Collector { // Describe returns all possible prometheus.Desc func (c Collector) Describe(ch chan<- *prometheus.Desc) { ch <- c.Accesses - ch <- c.Actions ch <- c.Attachments ch <- c.BuildInfo ch <- c.Comments @@ -246,11 +239,6 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) { prometheus.GaugeValue, float64(stats.Counter.Access), ) - ch <- prometheus.MustNewConstMetric( - c.Actions, - prometheus.GaugeValue, - float64(stats.Counter.Action), - ) ch <- prometheus.MustNewConstMetric( c.Attachments, prometheus.GaugeValue, diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index 87eae3f414a..e1762bb1ee9 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -28,7 +28,7 @@ type Notifier interface { NotifyDeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) NotifyIssueChangeMilestone(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) - NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) + NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) NotifyIssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) NotifyIssueClearLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) NotifyIssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string) diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index 187c36beeb4..338790b3564 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -120,8 +120,8 @@ func (*NullNotifier) NotifyIssueChangeContent(ctx context.Context, doer *user_mo func (*NullNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { } -// NotifyPullReviewRequest places a place holder function -func (*NullNotifier) NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { +// NotifyPullRequestReviewRequest places a place holder function +func (*NullNotifier) NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { } // NotifyIssueClearLabels places a place holder function diff --git a/modules/notification/mail/mail.go b/modules/notification/mail/mail.go index 7e54df44c40..a5fed51ebd0 100644 --- a/modules/notification/mail/mail.go +++ b/modules/notification/mail/mail.go @@ -123,7 +123,7 @@ func (m *mailNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *user } } -func (m *mailNotifier) NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { +func (m *mailNotifier) NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotifications() != user_model.EmailNotificationsDisabled { ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL()) if err := mailer.SendIssueAssignedMail(ctx, issue, doer, ct, comment, []*user_model.User{reviewer}); err != nil { diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 72dd1c99e53..6153c9e3d68 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -230,10 +230,10 @@ func NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue } } -// NotifyPullReviewRequest notifies Request Review change -func NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { +// NotifyPullRequestReviewRequest notifies Request Review change +func NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { for _, notifier := range notifiers { - notifier.NotifyPullReviewRequest(ctx, doer, issue, reviewer, isRequest, comment) + notifier.NotifyPullRequestReviewRequest(ctx, doer, issue, reviewer, isRequest, comment) } } diff --git a/modules/notification/ui/ui.go b/modules/notification/ui/ui.go index 6c5f22c1229..2ca1a7700f2 100644 --- a/modules/notification/ui/ui.go +++ b/modules/notification/ui/ui.go @@ -229,7 +229,7 @@ func (ns *notificationService) NotifyIssueChangeAssignee(ctx context.Context, do } } -func (ns *notificationService) NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { +func (ns *notificationService) NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { if isRequest { opts := issueNotificationOpts{ IssueID: issue.ID, diff --git a/modules/process/manager.go b/modules/process/manager.go index 25d503c5944..56908c04086 100644 --- a/modules/process/manager.go +++ b/modules/process/manager.go @@ -6,10 +6,10 @@ package process import ( "context" - "log" "runtime/pprof" "strconv" "sync" + "sync/atomic" "time" ) @@ -44,18 +44,35 @@ type IDType string // - it is simply an alias for context.CancelFunc and is only for documentary purposes type FinishedFunc = context.CancelFunc -var Trace = defaultTrace // this global can be overridden by particular logging packages - thus avoiding import cycles +var ( + traceDisabled atomic.Int64 + TraceCallback = defaultTraceCallback // this global can be overridden by particular logging packages - thus avoiding import cycles +) -func defaultTrace(start bool, pid IDType, description string, parentPID IDType, typ string) { - if start && parentPID != "" { - log.Printf("start process %s: %s (from %s) (%s)", pid, description, parentPID, typ) - } else if start { - log.Printf("start process %s: %s (%s)", pid, description, typ) +// defaultTraceCallback is a no-op. Without a proper TraceCallback (provided by the logger system), this "Trace" level messages shouldn't be outputted. +func defaultTraceCallback(skip int, start bool, pid IDType, description string, parentPID IDType, typ string) { +} + +// TraceLogDisable disables (or revert the disabling) the trace log for the process lifecycle. +// eg: the logger system shouldn't print the trace log for themselves, that's cycle dependency (Logger -> ProcessManager -> TraceCallback -> Logger ...) +// Theoretically, such trace log should only be enabled when the logger system is ready with a proper level, so the default TraceCallback is a no-op. +func TraceLogDisable(v bool) { + if v { + traceDisabled.Add(1) } else { - log.Printf("end process %s: %s", pid, description) + traceDisabled.Add(-1) } } +func Trace(start bool, pid IDType, description string, parentPID IDType, typ string) { + if traceDisabled.Load() != 0 { + // the traceDisabled counter is mainly for recursive calls, so no concurrency problem. + // because the counter can't be 0 since the caller function hasn't returned (decreased the counter) yet. + return + } + TraceCallback(1, start, pid, description, parentPID, typ) +} + // Manager manages all processes and counts PIDs. type Manager struct { mutex sync.Mutex diff --git a/modules/queue/base_levelqueue.go b/modules/queue/base_levelqueue.go index afde5021169..efc57c9c9c8 100644 --- a/modules/queue/base_levelqueue.go +++ b/modules/queue/base_levelqueue.go @@ -5,16 +5,21 @@ package queue import ( "context" + "sync/atomic" "code.gitea.io/gitea/modules/nosql" + "code.gitea.io/gitea/modules/queue/lqinternal" "gitea.com/lunny/levelqueue" + "github.com/syndtr/goleveldb/leveldb" ) type baseLevelQueue struct { - internal *levelqueue.Queue - conn string - cfg *BaseConfig + internal atomic.Pointer[levelqueue.Queue] + + conn string + cfg *BaseConfig + db *leveldb.DB } var _ baseQueue = (*baseLevelQueue)(nil) @@ -31,21 +36,23 @@ func newBaseLevelQueueSimple(cfg *BaseConfig) (baseQueue, error) { if err != nil { return nil, err } - q := &baseLevelQueue{conn: conn, cfg: cfg} - q.internal, err = levelqueue.NewQueue(db, []byte(cfg.QueueFullName), false) + q := &baseLevelQueue{conn: conn, cfg: cfg, db: db} + lq, err := levelqueue.NewQueue(db, []byte(cfg.QueueFullName), false) if err != nil { return nil, err } - + q.internal.Store(lq) return q, nil } func (q *baseLevelQueue) PushItem(ctx context.Context, data []byte) error { - return baseLevelQueueCommon(q.cfg, q.internal, nil).PushItem(ctx, data) + c := baseLevelQueueCommon(q.cfg, nil, func() baseLevelQueuePushPoper { return q.internal.Load() }) + return c.PushItem(ctx, data) } func (q *baseLevelQueue) PopItem(ctx context.Context) ([]byte, error) { - return baseLevelQueueCommon(q.cfg, q.internal, nil).PopItem(ctx) + c := baseLevelQueueCommon(q.cfg, nil, func() baseLevelQueuePushPoper { return q.internal.Load() }) + return c.PopItem(ctx) } func (q *baseLevelQueue) HasItem(ctx context.Context, data []byte) (bool, error) { @@ -53,20 +60,24 @@ func (q *baseLevelQueue) HasItem(ctx context.Context, data []byte) (bool, error) } func (q *baseLevelQueue) Len(ctx context.Context) (int, error) { - return int(q.internal.Len()), nil + return int(q.internal.Load().Len()), nil } func (q *baseLevelQueue) Close() error { - err := q.internal.Close() + err := q.internal.Load().Close() _ = nosql.GetManager().CloseLevelDB(q.conn) + q.db = nil // the db is not managed by us, it's managed by the nosql manager return err } func (q *baseLevelQueue) RemoveAll(ctx context.Context) error { - for q.internal.Len() > 0 { - if _, err := q.internal.LPop(); err != nil { - return err - } + lqinternal.RemoveLevelQueueKeys(q.db, []byte(q.cfg.QueueFullName)) + lq, err := levelqueue.NewQueue(q.db, []byte(q.cfg.QueueFullName), false) + if err != nil { + return err } + old := q.internal.Load() + q.internal.Store(lq) + _ = old.Close() // Not ideal for concurrency. Luckily, the levelqueue only sets its db=nil because it doesn't manage the db, so far so good return nil } diff --git a/modules/queue/base_levelqueue_common.go b/modules/queue/base_levelqueue_common.go index 409a965517e..78d3b85a8a2 100644 --- a/modules/queue/base_levelqueue_common.go +++ b/modules/queue/base_levelqueue_common.go @@ -17,6 +17,7 @@ import ( "github.com/syndtr/goleveldb/leveldb" ) +// baseLevelQueuePushPoper is the common interface for levelqueue.Queue and levelqueue.UniqueQueue type baseLevelQueuePushPoper interface { RPush(data []byte) error LPop() ([]byte, error) @@ -24,9 +25,9 @@ type baseLevelQueuePushPoper interface { } type baseLevelQueueCommonImpl struct { - length int - internal baseLevelQueuePushPoper - mu *sync.Mutex + length int + internalFunc func() baseLevelQueuePushPoper + mu *sync.Mutex } func (q *baseLevelQueueCommonImpl) PushItem(ctx context.Context, data []byte) error { @@ -36,11 +37,11 @@ func (q *baseLevelQueueCommonImpl) PushItem(ctx context.Context, data []byte) er defer q.mu.Unlock() } - cnt := int(q.internal.Len()) + cnt := int(q.internalFunc().Len()) if cnt >= q.length { return true, nil } - retry, err = false, q.internal.RPush(data) + retry, err = false, q.internalFunc().RPush(data) if err == levelqueue.ErrAlreadyInQueue { err = ErrAlreadyInQueue } @@ -55,7 +56,7 @@ func (q *baseLevelQueueCommonImpl) PopItem(ctx context.Context) ([]byte, error) defer q.mu.Unlock() } - data, err = q.internal.LPop() + data, err = q.internalFunc().LPop() if err == levelqueue.ErrNotFound { return true, nil, nil } @@ -66,8 +67,8 @@ func (q *baseLevelQueueCommonImpl) PopItem(ctx context.Context) ([]byte, error) }) } -func baseLevelQueueCommon(cfg *BaseConfig, internal baseLevelQueuePushPoper, mu *sync.Mutex) *baseLevelQueueCommonImpl { - return &baseLevelQueueCommonImpl{length: cfg.Length, internal: internal} +func baseLevelQueueCommon(cfg *BaseConfig, mu *sync.Mutex, internalFunc func() baseLevelQueuePushPoper) *baseLevelQueueCommonImpl { + return &baseLevelQueueCommonImpl{length: cfg.Length, mu: mu, internalFunc: internalFunc} } func prepareLevelDB(cfg *BaseConfig) (conn string, db *leveldb.DB, err error) { diff --git a/modules/queue/base_levelqueue_test.go b/modules/queue/base_levelqueue_test.go index 712a0892cd4..b881802ca2b 100644 --- a/modules/queue/base_levelqueue_test.go +++ b/modules/queue/base_levelqueue_test.go @@ -6,9 +6,12 @@ package queue import ( "testing" + "code.gitea.io/gitea/modules/queue/lqinternal" "code.gitea.io/gitea/modules/setting" + "gitea.com/lunny/levelqueue" "github.com/stretchr/testify/assert" + "github.com/syndtr/goleveldb/leveldb" ) func TestBaseLevelDB(t *testing.T) { @@ -21,3 +24,55 @@ func TestBaseLevelDB(t *testing.T) { testQueueBasic(t, newBaseLevelQueueSimple, toBaseConfig("baseLevelQueue", setting.QueueSettings{Datadir: t.TempDir() + "/queue-test", Length: 10}), false) testQueueBasic(t, newBaseLevelQueueUnique, toBaseConfig("baseLevelQueueUnique", setting.QueueSettings{ConnStr: "leveldb://" + t.TempDir() + "/queue-test", Length: 10}), true) } + +func TestCorruptedLevelQueue(t *testing.T) { + // sometimes the levelqueue could be in a corrupted state, this test is to make sure it can recover from it + dbDir := t.TempDir() + "/levelqueue-test" + db, err := leveldb.OpenFile(dbDir, nil) + if !assert.NoError(t, err) { + return + } + defer db.Close() + + assert.NoError(t, db.Put([]byte("other-key"), []byte("other-value"), nil)) + + nameQueuePrefix := []byte("queue_name") + nameSetPrefix := []byte("set_name") + lq, err := levelqueue.NewUniqueQueue(db, nameQueuePrefix, nameSetPrefix, false) + assert.NoError(t, err) + assert.NoError(t, lq.RPush([]byte("item-1"))) + + itemKey := lqinternal.QueueItemKeyBytes(nameQueuePrefix, 1) + itemValue, err := db.Get(itemKey, nil) + assert.NoError(t, err) + assert.Equal(t, []byte("item-1"), itemValue) + + // there should be 5 keys in db: queue low, queue high, 1 queue item, 1 set item, and "other-key" + keys := lqinternal.ListLevelQueueKeys(db) + assert.Len(t, keys, 5) + + // delete the queue item key, to corrupt the queue + assert.NoError(t, db.Delete(itemKey, nil)) + // now the queue is corrupted, it never works again + _, err = lq.LPop() + assert.ErrorIs(t, err, levelqueue.ErrNotFound) + assert.NoError(t, lq.Close()) + + // remove all the queue related keys to reset the queue + lqinternal.RemoveLevelQueueKeys(db, nameQueuePrefix) + lqinternal.RemoveLevelQueueKeys(db, nameSetPrefix) + // now there should be only 1 key in db: "other-key" + keys = lqinternal.ListLevelQueueKeys(db) + assert.Len(t, keys, 1) + assert.Equal(t, []byte("other-key"), keys[0]) + + // re-create a queue from db + lq, err = levelqueue.NewUniqueQueue(db, nameQueuePrefix, nameSetPrefix, false) + assert.NoError(t, err) + assert.NoError(t, lq.RPush([]byte("item-new-1"))) + // now the queue works again + itemValue, err = lq.LPop() + assert.NoError(t, err) + assert.Equal(t, []byte("item-new-1"), itemValue) + assert.NoError(t, lq.Close()) +} diff --git a/modules/queue/base_levelqueue_unique.go b/modules/queue/base_levelqueue_unique.go index 1acd504e327..968a4e98d46 100644 --- a/modules/queue/base_levelqueue_unique.go +++ b/modules/queue/base_levelqueue_unique.go @@ -6,18 +6,21 @@ package queue import ( "context" "sync" - "unsafe" + "sync/atomic" "code.gitea.io/gitea/modules/nosql" + "code.gitea.io/gitea/modules/queue/lqinternal" "gitea.com/lunny/levelqueue" "github.com/syndtr/goleveldb/leveldb" ) type baseLevelQueueUnique struct { - internal *levelqueue.UniqueQueue - conn string - cfg *BaseConfig + internal atomic.Pointer[levelqueue.UniqueQueue] + + conn string + cfg *BaseConfig + db *leveldb.DB mu sync.Mutex // the levelqueue.UniqueQueue is not thread-safe, there is no mutex protecting the underlying queue&set together } @@ -29,39 +32,42 @@ func newBaseLevelQueueUnique(cfg *BaseConfig) (baseQueue, error) { if err != nil { return nil, err } - q := &baseLevelQueueUnique{conn: conn, cfg: cfg} - q.internal, err = levelqueue.NewUniqueQueue(db, []byte(cfg.QueueFullName), []byte(cfg.SetFullName), false) + q := &baseLevelQueueUnique{conn: conn, cfg: cfg, db: db} + lq, err := levelqueue.NewUniqueQueue(db, []byte(cfg.QueueFullName), []byte(cfg.SetFullName), false) if err != nil { return nil, err } - + q.internal.Store(lq) return q, nil } func (q *baseLevelQueueUnique) PushItem(ctx context.Context, data []byte) error { - return baseLevelQueueCommon(q.cfg, q.internal, &q.mu).PushItem(ctx, data) + c := baseLevelQueueCommon(q.cfg, &q.mu, func() baseLevelQueuePushPoper { return q.internal.Load() }) + return c.PushItem(ctx, data) } func (q *baseLevelQueueUnique) PopItem(ctx context.Context) ([]byte, error) { - return baseLevelQueueCommon(q.cfg, q.internal, &q.mu).PopItem(ctx) + c := baseLevelQueueCommon(q.cfg, &q.mu, func() baseLevelQueuePushPoper { return q.internal.Load() }) + return c.PopItem(ctx) } func (q *baseLevelQueueUnique) HasItem(ctx context.Context, data []byte) (bool, error) { q.mu.Lock() defer q.mu.Unlock() - return q.internal.Has(data) + return q.internal.Load().Has(data) } func (q *baseLevelQueueUnique) Len(ctx context.Context) (int, error) { q.mu.Lock() defer q.mu.Unlock() - return int(q.internal.Len()), nil + return int(q.internal.Load().Len()), nil } func (q *baseLevelQueueUnique) Close() error { q.mu.Lock() defer q.mu.Unlock() - err := q.internal.Close() + err := q.internal.Load().Close() + q.db = nil // the db is not managed by us, it's managed by the nosql manager _ = nosql.GetManager().CloseLevelDB(q.conn) return err } @@ -69,28 +75,14 @@ func (q *baseLevelQueueUnique) Close() error { func (q *baseLevelQueueUnique) RemoveAll(ctx context.Context) error { q.mu.Lock() defer q.mu.Unlock() - - type levelUniqueQueue struct { - q *levelqueue.Queue - set *levelqueue.Set - db *leveldb.DB - } - lq := (*levelUniqueQueue)(unsafe.Pointer(q.internal)) - - for lq.q.Len() > 0 { - if _, err := lq.q.LPop(); err != nil { - return err - } - } - - // the "set" must be cleared after the "list" because there is no transaction. - // it's better to have duplicate items than losing items. - members, err := lq.set.Members() + lqinternal.RemoveLevelQueueKeys(q.db, []byte(q.cfg.QueueFullName)) + lqinternal.RemoveLevelQueueKeys(q.db, []byte(q.cfg.SetFullName)) + lq, err := levelqueue.NewUniqueQueue(q.db, []byte(q.cfg.QueueFullName), []byte(q.cfg.SetFullName), false) if err != nil { - return err // seriously corrupted - } - for _, v := range members { - _, _ = lq.set.Remove(v) + return err } + old := q.internal.Load() + q.internal.Store(lq) + _ = old.Close() // Not ideal for concurrency. Luckily, the levelqueue only sets its db=nil because it doesn't manage the db, so far so good return nil } diff --git a/modules/queue/lqinternal/lqinternal.go b/modules/queue/lqinternal/lqinternal.go new file mode 100644 index 00000000000..89aa4e59894 --- /dev/null +++ b/modules/queue/lqinternal/lqinternal.go @@ -0,0 +1,48 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package lqinternal + +import ( + "bytes" + "encoding/binary" + + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/opt" +) + +func QueueItemIDBytes(id int64) []byte { + buf := make([]byte, 8) + binary.PutVarint(buf, id) + return buf +} + +func QueueItemKeyBytes(prefix []byte, id int64) []byte { + key := make([]byte, len(prefix), len(prefix)+1+8) + copy(key, prefix) + key = append(key, '-') + return append(key, QueueItemIDBytes(id)...) +} + +func RemoveLevelQueueKeys(db *leveldb.DB, namePrefix []byte) { + keyPrefix := make([]byte, len(namePrefix)+1) + copy(keyPrefix, namePrefix) + keyPrefix[len(namePrefix)] = '-' + + it := db.NewIterator(nil, &opt.ReadOptions{Strict: opt.NoStrict}) + defer it.Release() + for it.Next() { + if bytes.HasPrefix(it.Key(), keyPrefix) { + _ = db.Delete(it.Key(), nil) + } + } +} + +func ListLevelQueueKeys(db *leveldb.DB) (res [][]byte) { + it := db.NewIterator(nil, &opt.ReadOptions{Strict: opt.NoStrict}) + defer it.Release() + for it.Next() { + res = append(res, it.Key()) + } + return res +} diff --git a/modules/repository/create.go b/modules/repository/create.go index 63be5cac252..c1190965415 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -399,6 +399,10 @@ func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibili if err != nil { return err } + + if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil { + return err + } } // Create/Remove git-daemon-export-ok for git-daemon... diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 62e1f31b9e5..bcb43f15e1d 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -9,7 +9,6 @@ import ( "fmt" "io" "net/http" - "path" "strings" "time" @@ -26,8 +25,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - - "gopkg.in/ini.v1" ) /* @@ -240,14 +237,14 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, // cleanUpMigrateGitConfig removes mirror info which prevents "push --all". // This also removes possible user credentials. -func cleanUpMigrateGitConfig(configPath string) error { - cfg, err := ini.Load(configPath) - if err != nil { - return fmt.Errorf("open config file: %w", err) - } - cfg.DeleteSection("remote \"origin\"") - if err = cfg.SaveToIndent(configPath, "\t"); err != nil { - return fmt.Errorf("save config file: %w", err) +func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { + cmd := git.NewCommand(ctx, "remote", "rm", "origin") + // if the origin does not exist + _, stderr, err := cmd.RunStdString(&git.RunOpts{ + Dir: repoPath, + }) + if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") { + return err } return nil } @@ -270,7 +267,7 @@ func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo } if repo.HasWiki() { - if err := cleanUpMigrateGitConfig(path.Join(repo.WikiPath(), "config")); err != nil { + if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil { return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err) } } diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go index dca9f2bb47b..63488037059 100644 --- a/modules/setting/config_env.go +++ b/modules/setting/config_env.go @@ -10,8 +10,6 @@ import ( "strings" "code.gitea.io/gitea/modules/log" - - "gopkg.in/ini.v1" ) const escapeRegexpString = "_0[xX](([0-9a-fA-F][0-9a-fA-F])+)_" @@ -89,7 +87,7 @@ func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, sect return ok, section, key, useFileValue } -func EnvironmentToConfig(cfg *ini.File, prefixGitea, suffixFile string, envs []string) (changed bool) { +func EnvironmentToConfig(cfg ConfigProvider, prefixGitea, suffixFile string, envs []string) (changed bool) { for _, kv := range envs { idx := strings.IndexByte(kv, '=') if idx < 0 { diff --git a/modules/setting/config_env_test.go b/modules/setting/config_env_test.go index d49464ecf78..d574554bcc0 100644 --- a/modules/setting/config_env_test.go +++ b/modules/setting/config_env_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "gopkg.in/ini.v1" ) func TestDecodeEnvSectionKey(t *testing.T) { @@ -71,15 +70,15 @@ func TestDecodeEnvironmentKey(t *testing.T) { } func TestEnvironmentToConfig(t *testing.T) { - cfg := ini.Empty() + cfg, _ := NewConfigProviderFromData("") changed := EnvironmentToConfig(cfg, "GITEA__", "__FILE", nil) assert.False(t, changed) - cfg, err := ini.Load([]byte(` + cfg, err := NewConfigProviderFromData(` [sec] key = old -`)) +`) assert.NoError(t, err) changed = EnvironmentToConfig(cfg, "GITEA__", "__FILE", []string{"GITEA__sec__key=new"}) diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index 37f5754ffdb..8b317d94e32 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -8,36 +8,71 @@ import ( "os" "path/filepath" "strings" + "time" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" - ini "gopkg.in/ini.v1" + "gopkg.in/ini.v1" //nolint:depguard ) +type ConfigKey interface { + Name() string + Value() string + SetValue(v string) + + In(defaultVal string, candidates []string) string + String() string + Strings(delim string) []string + + MustString(defaultVal string) string + MustBool(defaultVal ...bool) bool + MustInt(defaultVal ...int) int + MustInt64(defaultVal ...int64) int64 + MustDuration(defaultVal ...time.Duration) time.Duration +} + type ConfigSection interface { Name() string - MapTo(interface{}) error + MapTo(any) error HasKey(key string) bool - NewKey(name, value string) (*ini.Key, error) - Key(key string) *ini.Key - Keys() []*ini.Key - ChildSections() []*ini.Section + NewKey(name, value string) (ConfigKey, error) + Key(key string) ConfigKey + Keys() []ConfigKey + ChildSections() []ConfigSection } // ConfigProvider represents a config provider type ConfigProvider interface { Section(section string) ConfigSection + Sections() []ConfigSection NewSection(name string) (ConfigSection, error) GetSection(name string) (ConfigSection, error) Save() error + SaveTo(filename string) error } +type iniConfigProvider struct { + opts *Options + ini *ini.File + newFile bool // whether the file has not existed previously +} + +type iniConfigSection struct { + sec *ini.Section +} + +var ( + _ ConfigProvider = (*iniConfigProvider)(nil) + _ ConfigSection = (*iniConfigSection)(nil) + _ ConfigKey = (*ini.Key)(nil) +) + // ConfigSectionKey only searches the keys in the given section, but it is O(n). // ini package has a special behavior: with "[sec] a=1" and an empty "[sec.sub]", // then in "[sec.sub]", Key()/HasKey() can always see "a=1" because it always tries parent sections. // It returns nil if the key doesn't exist. -func ConfigSectionKey(sec ConfigSection, key string) *ini.Key { +func ConfigSectionKey(sec ConfigSection, key string) ConfigKey { if sec == nil { return nil } @@ -64,7 +99,7 @@ func ConfigSectionKeyString(sec ConfigSection, key string, def ...string) string // and the returned key is safe to be used with "MustXxx", it doesn't change the parent's values. // Otherwise, ini.Section.Key().MustXxx would pollute the parent section's keys. // It never returns nil. -func ConfigInheritedKey(sec ConfigSection, key string) *ini.Key { +func ConfigInheritedKey(sec ConfigSection, key string) ConfigKey { k := sec.Key(key) if k != nil && k.String() != "" { newKey, _ := sec.NewKey(k.Name(), k.String()) @@ -85,41 +120,64 @@ func ConfigInheritedKeyString(sec ConfigSection, key string, def ...string) stri return "" } -type iniFileConfigProvider struct { - opts *Options - *ini.File - newFile bool // whether the file has not existed previously +func (s *iniConfigSection) Name() string { + return s.sec.Name() } -// NewConfigProviderFromData this function is only for testing +func (s *iniConfigSection) MapTo(v any) error { + return s.sec.MapTo(v) +} + +func (s *iniConfigSection) HasKey(key string) bool { + return s.sec.HasKey(key) +} + +func (s *iniConfigSection) NewKey(name, value string) (ConfigKey, error) { + return s.sec.NewKey(name, value) +} + +func (s *iniConfigSection) Key(key string) ConfigKey { + return s.sec.Key(key) +} + +func (s *iniConfigSection) Keys() (keys []ConfigKey) { + for _, k := range s.sec.Keys() { + keys = append(keys, k) + } + return keys +} + +func (s *iniConfigSection) ChildSections() (sections []ConfigSection) { + for _, s := range s.sec.ChildSections() { + sections = append(sections, &iniConfigSection{s}) + } + return sections +} + +// NewConfigProviderFromData this function is mainly for testing purpose func NewConfigProviderFromData(configContent string) (ConfigProvider, error) { - var cfg *ini.File - var err error - if configContent == "" { - cfg = ini.Empty() - } else { - cfg, err = ini.Load(strings.NewReader(configContent)) - if err != nil { - return nil, err - } + cfg, err := ini.Load(strings.NewReader(configContent)) + if err != nil { + return nil, err } cfg.NameMapper = ini.SnackCase - return &iniFileConfigProvider{ - File: cfg, + return &iniConfigProvider{ + ini: cfg, newFile: true, }, nil } type Options struct { - CustomConf string // the ini file path - AllowEmpty bool // whether not finding configuration files is allowed (only true for the tests) - ExtraConfig string - DisableLoadCommonSettings bool + CustomConf string // the ini file path + AllowEmpty bool // whether not finding configuration files is allowed + ExtraConfig string + + DisableLoadCommonSettings bool // only used by "Init()", not used by "NewConfigProvider()" } -// newConfigProviderFromFile load configuration from file. +// NewConfigProviderFromFile load configuration from file. // NOTE: do not print any log except error. -func newConfigProviderFromFile(opts *Options) (*iniFileConfigProvider, error) { +func NewConfigProviderFromFile(opts *Options) (ConfigProvider, error) { cfg := ini.Empty() newFile := true @@ -147,61 +205,77 @@ func newConfigProviderFromFile(opts *Options) (*iniFileConfigProvider, error) { } cfg.NameMapper = ini.SnackCase - return &iniFileConfigProvider{ + return &iniConfigProvider{ opts: opts, - File: cfg, + ini: cfg, newFile: newFile, }, nil } -func (p *iniFileConfigProvider) Section(section string) ConfigSection { - return p.File.Section(section) +func (p *iniConfigProvider) Section(section string) ConfigSection { + return &iniConfigSection{sec: p.ini.Section(section)} } -func (p *iniFileConfigProvider) NewSection(name string) (ConfigSection, error) { - return p.File.NewSection(name) +func (p *iniConfigProvider) Sections() (sections []ConfigSection) { + for _, s := range p.ini.Sections() { + sections = append(sections, &iniConfigSection{s}) + } + return sections } -func (p *iniFileConfigProvider) GetSection(name string) (ConfigSection, error) { - return p.File.GetSection(name) +func (p *iniConfigProvider) NewSection(name string) (ConfigSection, error) { + sec, err := p.ini.NewSection(name) + if err != nil { + return nil, err + } + return &iniConfigSection{sec: sec}, nil } -// Save save the content into file -func (p *iniFileConfigProvider) Save() error { - if p.opts.CustomConf == "" { +func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) { + sec, err := p.ini.GetSection(name) + if err != nil { + return nil, err + } + return &iniConfigSection{sec: sec}, nil +} + +// Save saves the content into file +func (p *iniConfigProvider) Save() error { + filename := p.opts.CustomConf + if filename == "" { if !p.opts.AllowEmpty { return fmt.Errorf("custom config path must not be empty") } return nil } - if p.newFile { - if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil { - return fmt.Errorf("failed to create '%s': %v", CustomConf, err) + if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil { + return fmt.Errorf("failed to create '%s': %v", filename, err) } } - if err := p.SaveTo(p.opts.CustomConf); err != nil { - return fmt.Errorf("failed to save '%s': %v", p.opts.CustomConf, err) + if err := p.ini.SaveTo(filename); err != nil { + return fmt.Errorf("failed to save '%s': %v", filename, err) } // Change permissions to be more restrictive - fi, err := os.Stat(CustomConf) + fi, err := os.Stat(filename) if err != nil { return fmt.Errorf("failed to determine current conf file permissions: %v", err) } if fi.Mode().Perm() > 0o600 { - if err = os.Chmod(CustomConf, 0o600); err != nil { + if err = os.Chmod(filename, 0o600); err != nil { log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.") } } return nil } -// a file is an implementation ConfigProvider and other implementations are possible, i.e. from docker, k8s, … -var _ ConfigProvider = &iniFileConfigProvider{} +func (p *iniConfigProvider) SaveTo(filename string) error { + return p.ini.SaveTo(filename) +} -func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting interface{}) { +func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) { if err := rootCfg.Section(sectionName).MapTo(setting); err != nil { log.Fatal("Failed to map %s settings: %v", sectionName, err) } @@ -219,3 +293,23 @@ func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) { log.Error("Deprecated `[%s]` `%s` present which has been copied to database table sys_setting", oldSection, oldKey) } } + +// NewConfigProviderForLocale loads locale configuration from source and others. "string" if for a local file path, "[]byte" is for INI content +func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, error) { + iniFile, err := ini.LoadSources(ini.LoadOptions{ + IgnoreInlineComment: true, + UnescapeValueCommentSymbols: true, + }, source, others...) + if err != nil { + return nil, fmt.Errorf("unable to load locale ini: %w", err) + } + iniFile.BlockMode = false + return &iniConfigProvider{ + ini: iniFile, + newFile: true, + }, nil +} + +func init() { + ini.PrettyFormat = false +} diff --git a/modules/setting/config_provider_test.go b/modules/setting/config_provider_test.go index 76f7048d59c..17650edea40 100644 --- a/modules/setting/config_provider_test.go +++ b/modules/setting/config_provider_test.go @@ -4,6 +4,7 @@ package setting import ( + "os" "testing" "github.com/stretchr/testify/assert" @@ -64,3 +65,57 @@ key = 123 assert.Equal(t, "", ConfigSectionKeyString(sec, "empty")) assert.Equal(t, "def", ConfigSectionKeyString(secSub, "empty")) } + +func TestNewConfigProviderFromFile(t *testing.T) { + _, err := NewConfigProviderFromFile(&Options{CustomConf: "no-such.ini", AllowEmpty: false}) + assert.ErrorContains(t, err, "unable to find configuration file") + + // load non-existing file and save + testFile := t.TempDir() + "/test.ini" + testFile1 := t.TempDir() + "/test1.ini" + cfg, err := NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) + assert.NoError(t, err) + + sec, _ := cfg.NewSection("foo") + _, _ = sec.NewKey("k1", "a") + assert.NoError(t, cfg.Save()) + _, _ = sec.NewKey("k2", "b") + assert.NoError(t, cfg.SaveTo(testFile1)) + + bs, err := os.ReadFile(testFile) + assert.NoError(t, err) + assert.Equal(t, "[foo]\nk1=a\n", string(bs)) + + bs, err = os.ReadFile(testFile1) + assert.NoError(t, err) + assert.Equal(t, "[foo]\nk1=a\nk2=b\n", string(bs)) + + // load existing file and save + cfg, err = NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) + assert.NoError(t, err) + assert.Equal(t, "a", cfg.Section("foo").Key("k1").String()) + sec, _ = cfg.NewSection("bar") + _, _ = sec.NewKey("k1", "b") + assert.NoError(t, cfg.Save()) + bs, err = os.ReadFile(testFile) + assert.NoError(t, err) + assert.Equal(t, "[foo]\nk1=a\n\n[bar]\nk1=b\n", string(bs)) +} + +func TestNewConfigProviderForLocale(t *testing.T) { + // load locale from file + localeFile := t.TempDir() + "/locale.ini" + _ = os.WriteFile(localeFile, []byte(`k1=a`), 0o644) + cfg, err := NewConfigProviderForLocale(localeFile) + assert.NoError(t, err) + assert.Equal(t, "a", cfg.Section("").Key("k1").String()) + + // load locale from bytes + cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar")) + assert.NoError(t, err) + assert.Equal(t, "foo", cfg.Section("").Key("k1").String()) + cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar"), []byte("k2=xxx")) + assert.NoError(t, err) + assert.Equal(t, "foo", cfg.Section("").Key("k1").String()) + assert.Equal(t, "xxx", cfg.Section("").Key("k2").String()) +} diff --git a/modules/setting/git.go b/modules/setting/git.go index 29ec37f866e..48a4e7f30de 100644 --- a/modules/setting/git.go +++ b/modules/setting/git.go @@ -16,10 +16,7 @@ var Git = struct { Path string HomePath string DisableDiffHighlight bool - Reflog struct { - Enabled bool - Expiration int - } `ini:"git.reflog"` + MaxGitDiffLines int MaxGitDiffLineCharacters int MaxGitDiffFiles int @@ -42,13 +39,6 @@ var Git = struct { GC int `ini:"GC"` } `ini:"git.timeout"` }{ - Reflog: struct { - Enabled bool - Expiration int - }{ - Enabled: true, - Expiration: 90, - }, DisableDiffHighlight: false, MaxGitDiffLines: 1000, MaxGitDiffLineCharacters: 5000, @@ -79,9 +69,19 @@ var Git = struct { }, } -var GitConfig = struct { - Options map[string]string -}{ +type GitConfigType struct { + Options map[string]string // git config key is case-insensitive, always use lower-case +} + +func (c *GitConfigType) SetOption(key, val string) { + c.Options[strings.ToLower(key)] = val +} + +func (c *GitConfigType) GetOption(key string) string { + return c.Options[strings.ToLower(key)] +} + +var GitConfig = GitConfigType{ Options: make(map[string]string), } @@ -93,12 +93,22 @@ func loadGitFrom(rootCfg ConfigProvider) { secGitConfig := rootCfg.Section("git.config") GitConfig.Options = make(map[string]string) - for _, key := range secGitConfig.Keys() { - // git config key is case-insensitive, so always use lower-case - GitConfig.Options[strings.ToLower(key.Name())] = key.String() + GitConfig.SetOption("diff.algorithm", "histogram") + GitConfig.SetOption("core.logAllRefUpdates", "true") + GitConfig.SetOption("gc.reflogExpire", "90") + + secGitReflog := rootCfg.Section("git.reflog") + if secGitReflog.HasKey("ENABLED") { + deprecatedSetting(rootCfg, "git.reflog", "ENABLED", "git.config", "core.logAllRefUpdates", "1.21") + GitConfig.SetOption("core.logAllRefUpdates", secGitReflog.Key("ENABLED").In("true", []string{"true", "false"})) } - if _, ok := GitConfig.Options["diff.algorithm"]; !ok { - GitConfig.Options["diff.algorithm"] = "histogram" + if secGitReflog.HasKey("EXPIRATION") { + deprecatedSetting(rootCfg, "git.reflog", "EXPIRATION", "git.config", "core.reflogExpire", "1.21") + GitConfig.SetOption("gc.reflogExpire", secGitReflog.Key("EXPIRATION").String()) + } + + for _, key := range secGitConfig.Keys() { + GitConfig.SetOption(key.Name(), key.String()) } Git.HomePath = sec.Key("HOME_PATH").MustString("home") diff --git a/modules/setting/git_test.go b/modules/setting/git_test.go index 1da8c87cc86..441c514d8c1 100644 --- a/modules/setting/git_test.go +++ b/modules/setting/git_test.go @@ -23,8 +23,6 @@ a.b = 1 `) assert.NoError(t, err) loadGitFrom(cfg) - - assert.Len(t, GitConfig.Options, 2) assert.EqualValues(t, "1", GitConfig.Options["a.b"]) assert.EqualValues(t, "histogram", GitConfig.Options["diff.algorithm"]) @@ -34,7 +32,34 @@ diff.algorithm = other `) assert.NoError(t, err) loadGitFrom(cfg) - - assert.Len(t, GitConfig.Options, 1) assert.EqualValues(t, "other", GitConfig.Options["diff.algorithm"]) } + +func TestGitReflog(t *testing.T) { + oldGit := Git + oldGitConfig := GitConfig + defer func() { + Git = oldGit + GitConfig = oldGitConfig + }() + + // default reflog config without legacy options + cfg, err := NewConfigProviderFromData(``) + assert.NoError(t, err) + loadGitFrom(cfg) + + assert.EqualValues(t, "true", GitConfig.GetOption("core.logAllRefUpdates")) + assert.EqualValues(t, "90", GitConfig.GetOption("gc.reflogExpire")) + + // custom reflog config by legacy options + cfg, err = NewConfigProviderFromData(` +[git.reflog] +ENABLED = false +EXPIRATION = 123 +`) + assert.NoError(t, err) + loadGitFrom(cfg) + + assert.EqualValues(t, "false", GitConfig.GetOption("core.logAllRefUpdates")) + assert.EqualValues(t, "123", GitConfig.GetOption("gc.reflogExpire")) +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 71cd9a12a99..1967d9e79aa 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -201,7 +201,7 @@ func Init(opts *Options) { opts.CustomConf = CustomConf } var err error - CfgProvider, err = newConfigProviderFromFile(opts) + CfgProvider, err = NewConfigProviderFromFile(opts) if err != nil { log.Fatal("Init[%v]: %v", opts, err) } diff --git a/modules/setting/ui.go b/modules/setting/ui.go index bf2b2d4bb7e..41438db40d5 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -22,7 +22,6 @@ var UI = struct { GraphMaxCommitNum int CodeCommentLines int ReactionMaxUserNum int - ThemeColorMetaTag string MaxDisplayFileSize int64 ShowUserEmail bool DefaultShowFullName bool @@ -33,7 +32,6 @@ var UI = struct { CustomEmojis []string CustomEmojisMap map[string]string `ini:"-"` SearchRepoDescription bool - UseServiceWorker bool OnlyShowRelevantRepos bool Notification struct { @@ -77,7 +75,6 @@ var UI = struct { GraphMaxCommitNum: 100, CodeCommentLines: 4, ReactionMaxUserNum: 10, - ThemeColorMetaTag: ``, MaxDisplayFileSize: 8388608, DefaultTheme: `auto`, Themes: []string{`auto`, `gitea`, `arc-green`}, @@ -138,7 +135,6 @@ func loadUIFrom(rootCfg ConfigProvider) { UI.ShowUserEmail = sec.Key("SHOW_USER_EMAIL").MustBool(true) UI.DefaultShowFullName = sec.Key("DEFAULT_SHOW_FULL_NAME").MustBool(false) UI.SearchRepoDescription = sec.Key("SEARCH_REPO_DESCRIPTION").MustBool(true) - UI.UseServiceWorker = sec.Key("USE_SERVICE_WORKER").MustBool(false) // OnlyShowRelevantRepos=false is important for many private/enterprise instances, // because many private repositories do not have "description/topic", users just want to search by their names. diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index 328d7e47c8e..48fcebe6f87 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -64,6 +64,37 @@ func (o *UpdateFileOptions) Branch() string { return o.FileOptions.BranchName } +// ChangeFileOperation for creating, updating or deleting a file +type ChangeFileOperation struct { + // indicates what to do with the file + // required: true + // enum: create,update,delete + Operation string `json:"operation" binding:"Required"` + // path to the existing or new file + // required: true + Path string `json:"path" binding:"Required;MaxSize(500)"` + // new or updated file content, must be base64 encoded + Content string `json:"content"` + // sha is the SHA for the file that already exists, required for update or delete + SHA string `json:"sha"` + // old path of the file to move + FromPath string `json:"from_path"` +} + +// ChangeFilesOptions options for creating, updating or deleting multiple files +// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) +type ChangeFilesOptions struct { + FileOptions + // list of file operations + // required: true + Files []*ChangeFileOperation `json:"files" binding:"Required"` +} + +// Branch returns branch name +func (o *ChangeFilesOptions) Branch() string { + return o.FileOptions.BranchName +} + // FileOptionInterface provides a unified interface for the different file options type FileOptionInterface interface { Branch() string @@ -126,6 +157,13 @@ type FileResponse struct { Verification *PayloadCommitVerification `json:"verification"` } +// FilesResponse contains information about multiple files from a repo +type FilesResponse struct { + Files []*ContentsResponse `json:"files"` + Commit *FileCommitResponse `json:"commit"` + Verification *PayloadCommitVerification `json:"verification"` +} + // FileDeleteResponse contains information about a repo's file that was deleted type FileDeleteResponse struct { Content interface{} `json:"content"` // to be set to nil diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 7460f99feac..2f2ef440497 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -118,9 +118,6 @@ func NewFuncMap() template.FuncMap { "CustomEmojis": func() map[string]string { return setting.UI.CustomEmojisMap }, - "ThemeColorMetaTag": func() string { - return setting.UI.ThemeColorMetaTag - }, "MetaAuthor": func() string { return setting.UI.Meta.Author }, @@ -130,9 +127,6 @@ func NewFuncMap() template.FuncMap { "MetaKeywords": func() string { return setting.UI.Meta.Keywords }, - "UseServiceWorker": func() bool { - return setting.UI.UseServiceWorker - }, "EnableTimetracking": func() bool { return setting.Service.EnableTimetracking }, diff --git a/modules/test/utils.go b/modules/test/utils.go index 7686d3f289f..282895eaa90 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -5,9 +5,14 @@ package test import ( "net/http" + "strings" ) // RedirectURL returns the redirect URL of a http response. func RedirectURL(resp http.ResponseWriter) string { return resp.Header().Get("Location") } + +func IsNormalPageCompleted(s string) bool { + return strings.Contains(s, `