Merge remote-tracking branch 'upstream/main' into limit-repo-size

Signed-off-by: DmitryFrolovTri <23313323+DmitryFrolovTri@users.noreply.github.com>
This commit is contained in:
DmitryFrolovTri
2023-06-07 21:09:16 +00:00
755 changed files with 8989 additions and 129854 deletions
-1
View File
@@ -76,7 +76,6 @@ cpu.out
/yarn-error.log
/npm-debug.log*
/public/js
/public/serviceworker.js
/public/css
/public/fonts
/public/img/webpack
-1
View File
@@ -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
-15
View File
@@ -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"
+32 -11
View File
@@ -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/*"
@@ -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
+27
View File
@@ -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
+1 -1
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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))
+2 -2
View File
@@ -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"
},
{
+4 -8
View File
@@ -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)
+1 -1
View File
@@ -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"
)
@@ -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"
+3 -14
View File
@@ -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
@@ -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`: **\<empty\>**: 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`)
@@ -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
@@ -123,9 +123,8 @@ to the Gitea sources. Otherwise, changes can't be pushed.
## Building Gitea (Basic)
Take a look at our
<a href='{{< relref "doc/installation/from-source.en-us.md" >}}'>instructions</a>
for <a href='{{< relref "doc/installation/from-source.en-us.md" >}}'>building
from source</a>.
[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
<a href='{{< relref "doc/administration/config-cheat-sheet.en-us.md" >}}'>configuration cheat sheet</a>
[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
@@ -115,8 +115,8 @@ git fetch --all --prune
## 构建 Gitea(基本)
看看我们的
<a href='{{ < relref "doc/installation/from-source.en-us.md" > }}'>说明</a>
关于如何 <a href='{{ < relref "doc/installation/from-source.en-us.md" > }}'>从源代码构建</a>
[说明]({{< 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`
并到 <a href = '{{ < relref "doc/administration/config-cheat-sheet.zh-cn.md" > }}'>配置备忘单</a>
并到[配置备忘单]({{< relref "doc/administration/config-cheat-sheet.zh-cn.md" >}})
`docs/content/doc/advanced/config-cheat-sheet.zh-cn.md` 中找到
### 更改Logo
@@ -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. |
| &nbsp;&nbsp;&nbsp; **repo:status** | Grants read/write access to commit status in all repositories. |
| &nbsp;&nbsp;&nbsp; **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. |
| &nbsp;&nbsp;&nbsp; **write:repo_hook** | Grants read/write access to repository hooks |
| &nbsp;&nbsp;&nbsp; **read:repo_hook** | Grants read-only access to repository hooks |
| **admin:org** | Grants full access to organization settings |
| &nbsp;&nbsp;&nbsp; **write:org** | Grants read/write access to organization settings |
| &nbsp;&nbsp;&nbsp; **read:org** | Grants read-only access to organization settings |
| **admin:public_key** | Grants full access for managing public keys |
| &nbsp;&nbsp;&nbsp; **write:public_key** | Grant read/write access to public keys |
| &nbsp;&nbsp;&nbsp; **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 |
| &nbsp;&nbsp;&nbsp; **read:user** | Grants read access to user's profile |
| &nbsp;&nbsp;&nbsp; **user:email** | Grants read access to user's email addresses |
| &nbsp;&nbsp;&nbsp; **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 |
| &nbsp;&nbsp;&nbsp; **write:package** | Grants read/write access to packages |
| &nbsp;&nbsp;&nbsp; **read:package** | Grants read access to packages |
| &nbsp;&nbsp;&nbsp; **delete:package** | Grants delete access to packages |
| **admin:gpg_key** | Grants full access for managing GPG keys |
| &nbsp;&nbsp;&nbsp; **write:gpg_key** | Grants read/write access to GPG keys |
| &nbsp;&nbsp;&nbsp; **read:gpg_key** | Grants read-only access to GPG keys |
| **admin:application** | Grants full access to manage applications |
| &nbsp;&nbsp;&nbsp; **write:application** | Grants read/write access for managing applications |
| &nbsp;&nbsp;&nbsp; **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. |
| &nbsp;&nbsp;&nbsp; **read:activitypub** | Grants read access for ActivityPub operations. |
| &nbsp;&nbsp;&nbsp; **write:activitypub** | Grants read/write/delete access for ActivityPub operations. |
| **admin** | `/admin/*` API routes: Site-wide administrative operations (hidden for non-admin accounts). |
| &nbsp;&nbsp;&nbsp; **read:admin** | Grants read access for admin operations, such as getting cron jobs or registered user emails. |
| &nbsp;&nbsp;&nbsp; **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. |
| &nbsp;&nbsp;&nbsp; **read:issue** | Grants read access for issues operations, such as getting issue comments, issue attachments, and milestones. |
| &nbsp;&nbsp;&nbsp; **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. |
| &nbsp;&nbsp;&nbsp; **read:misc** | Grants read access to miscellaneous operations, such as getting label and gitignore templates. |
| &nbsp;&nbsp;&nbsp; **write:misc** | Grants read/write/delete access to miscellaneous operations, such as markup utility operations. |
| **notification** | `notification/*` API routes: user notification operations. |
| &nbsp;&nbsp;&nbsp; **read:notification** | Grants read access to user notifications, such as which notifications users are subscribed to and read new notifications. |
| &nbsp;&nbsp;&nbsp; **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. |
| &nbsp;&nbsp;&nbsp; **read:organization** | Grants read access to org and team status, such as listing all orgs a user has visibility to, teams, and team members. |
| &nbsp;&nbsp;&nbsp; **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 |
| &nbsp;&nbsp;&nbsp; **read:package** | Grants read access to package operations, such as reading and downloading available packages. |
| &nbsp;&nbsp;&nbsp; **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. |
| &nbsp;&nbsp;&nbsp; **read:repository** | Grants read access to repository operations, such as getting repository files, releases, collaborators. |
| &nbsp;&nbsp;&nbsp; **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. |
| &nbsp;&nbsp;&nbsp; **read:user** | Grants read access to user operations, such as getting user repo subscriptions and user settings. |
| &nbsp;&nbsp;&nbsp; **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.
@@ -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
@@ -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
@@ -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" >}})
@@ -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
@@ -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
@@ -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" >}})
@@ -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
@@ -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
<a href='{{< relref "doc/development/hacking-on-gitea.en-us.md" >}}'>Hacking on
Gitea</a>
[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.
@@ -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" >}})
@@ -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.
@@ -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以及如何运行它们。
+1 -1
View File
@@ -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
+1 -1
View File
@@ -27,7 +27,7 @@ menu:
标签具有必填的名称和颜色,可选的描述,以及必须是独占的或非独占的(见下面的“作用域标签”)。
当您创建一个仓库时,可以通过使用 `工单标签(Issue Labels` 选项来选择标签集。该选项列出了一些在您的实例上 [全局配置的可用标签集](../customizing-gitea/#labels)。在创建仓库时,这些标签也将被创建。
当您创建一个仓库时,可以通过使用 `工单标签(Issue Labels` 选项来选择标签集。该选项列出了一些在您的实例上 [全局配置的可用标签集](../administration/customizing-gitea/#labels)。在创建仓库时,这些标签也将被创建。
## 作用域标签
@@ -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")
```
+24 -9
View File
@@ -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.
+1 -1
View File
@@ -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).
+1 -1
View File
@@ -31,4 +31,4 @@ WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
## 合併請求範本
您可以在[問題與合併請求範本](../issue-pull-request-templates)找到更多關於合併請求範本的資訊。
您可以在[問題與合併請求範本](issue-pull-request-templates)找到更多關於合併請求範本的資訊。
+2 -2
View File
@@ -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
+2 -6
View File
@@ -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/) 获得更多的第三方集成支持
+2 -2
View File
@@ -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/) 獲得更多的第三方集成支持
+4 -4
View File
@@ -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
+10 -8
View File
@@ -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=
+5 -1
View File
@@ -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
}
+2 -3
View File
@@ -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 {
-8
View File
@@ -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 {
+9
View File
@@ -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
+213 -139
View File
@@ -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<<iota | AccessTokenScopeRepoStatusBits | AccessTokenScopePublicRepoBits | AccessTokenScopeAdminRepoHookBits
AccessTokenScopeRepoStatusBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopePublicRepoBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota
AccessTokenScopeAdminOrgBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteOrgBits
AccessTokenScopeWriteOrgBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadOrgBits
AccessTokenScopeReadOrgBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadActivityPubBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteActivityPubBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadActivityPubBits
AccessTokenScopeAdminPublicKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWritePublicKeyBits
AccessTokenScopeWritePublicKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadPublicKeyBits
AccessTokenScopeReadPublicKeyBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadAdminBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteAdminBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadAdminBits
AccessTokenScopeAdminRepoHookBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteRepoHookBits
AccessTokenScopeWriteRepoHookBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadRepoHookBits
AccessTokenScopeReadRepoHookBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadMiscBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteMiscBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadMiscBits
AccessTokenScopeAdminOrgHookBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadNotificationBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteNotificationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadNotificationBits
AccessTokenScopeAdminUserHookBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadOrganizationBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteOrganizationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadOrganizationBits
AccessTokenScopeNotificationBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadPackageBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWritePackageBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadPackageBits
AccessTokenScopeUserBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadUserBits | AccessTokenScopeUserEmailBits | AccessTokenScopeUserFollowBits
AccessTokenScopeReadUserBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeUserEmailBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeUserFollowBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadIssueBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteIssueBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadIssueBits
AccessTokenScopeDeleteRepoBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadRepositoryBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteRepositoryBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadRepositoryBits
AccessTokenScopePackageBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWritePackageBits | AccessTokenScopeDeletePackageBits
AccessTokenScopeWritePackageBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadPackageBits
AccessTokenScopeReadPackageBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeDeletePackageBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeAdminGPGKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteGPGKeyBits
AccessTokenScopeWriteGPGKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadGPGKeyBits
AccessTokenScopeReadGPGKeyBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeAdminApplicationBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteApplicationBits
AccessTokenScopeWriteApplicationBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadApplicationBits
AccessTokenScopeReadApplicationBits AccessTokenScopeBitmap = 1 << iota
AccessTokenScopeSudoBits AccessTokenScopeBitmap = 1 << iota
accessTokenScopeReadUserBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadUserBits
// The current implementation only supports up to 64 token scopes.
// If we need to support > 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
+56 -50
View File
@@ -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)
})
}
-25
View File
@@ -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)
+22 -1
View File
@@ -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)
+28 -6
View File
@@ -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")
+9 -2
View File
@@ -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
}
+1 -1
View File
@@ -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
}
+2
View File
@@ -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),
}
+14
View File
@@ -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)
}
+360
View File
@@ -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<<iota | accessTokenScopeReadActivityPubBits
accessTokenScopeReadAdminBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteAdminBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadAdminBits
accessTokenScopeReadMiscBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteMiscBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadMiscBits
accessTokenScopeReadNotificationBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteNotificationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadNotificationBits
accessTokenScopeReadOrganizationBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteOrganizationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadOrganizationBits
accessTokenScopeReadPackageBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWritePackageBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadPackageBits
accessTokenScopeReadIssueBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteIssueBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadIssueBits
accessTokenScopeReadRepositoryBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteRepositoryBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadRepositoryBits
accessTokenScopeReadUserBits accessTokenScopeBitmap = 1 << iota
accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadUserBits
// The current implementation only supports up to 64 token scopes.
// If we need to support > 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
}
+110
View File
@@ -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)
}
}
+4 -4
View File
@@ -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)
+1 -1
View File
@@ -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)
}
+2 -2
View File
@@ -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})
+1 -1
View File
@@ -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",
+14
View File
@@ -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})
}
+18
View File
@@ -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)
}
+27 -17
View File
@@ -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))
}
+5
View File
@@ -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 {
+18 -10
View File
@@ -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
-27
View File
@@ -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
+5
View File
@@ -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()
+10 -10
View File
@@ -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)
+8 -16
View File
@@ -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
}
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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
}
-12
View File
@@ -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,
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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 {
+3 -3
View File
@@ -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)
}
}
+1 -1
View File
@@ -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,
+25 -8
View File
@@ -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
+25 -14
View File
@@ -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
}
+9 -8
View File
@@ -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) {
+55
View File
@@ -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())
}
+25 -33
View File
@@ -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
}
+48
View File
@@ -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
}
+4
View File
@@ -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...
+9 -12
View File
@@ -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)
}
}
+1 -3
View File
@@ -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 {
+3 -4
View File
@@ -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"})
+145 -51
View File
@@ -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
}
+55
View File
@@ -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())
}
+29 -19
View File
@@ -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")
+29 -4
View File
@@ -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"))
}
+1 -1
View File
@@ -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)
}
-4
View File
@@ -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.
+38
View File
@@ -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
-6
View File
@@ -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
},
+5
View File
@@ -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, `<footer class="page-footer"`) && strings.Contains(s, `</html>`)
}
+2 -7
View File
@@ -7,8 +7,7 @@ import (
"fmt"
"code.gitea.io/gitea/modules/log"
"gopkg.in/ini.v1"
"code.gitea.io/gitea/modules/setting"
)
// This file implements the static LocaleStore that will not watch for changes
@@ -47,14 +46,10 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more
l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string)}
store.localeMap[l.langName] = l
iniFile, err := ini.LoadSources(ini.LoadOptions{
IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true,
}, source, moreSource)
iniFile, err := setting.NewConfigProviderForLocale(source, moreSource)
if err != nil {
return fmt.Errorf("unable to load ini: %w", err)
}
iniFile.BlockMode = false
for _, section := range iniFile.Sections() {
for _, key := range section.Keys() {
+18 -1
View File
@@ -3,7 +3,10 @@
package util
import "unicode/utf8"
import (
"strings"
"unicode/utf8"
)
// in UTF8 "…" is 3 bytes so doesn't really gain us anything...
const (
@@ -35,3 +38,17 @@ func SplitStringAtByteN(input string, n int) (left, right string) {
return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:]
}
// SplitTrimSpace splits the string at given separator and trims leading and trailing space
func SplitTrimSpace(input, sep string) []string {
// replace CRLF with LF
input = strings.ReplaceAll(input, "\r\n", "\n")
var stringList []string
for _, s := range strings.Split(input, sep) {
// trim leading and trailing space
stringList = append(stringList, strings.TrimSpace(s))
}
return stringList
}
@@ -0,0 +1,7 @@
Linking [name of library] statically or dynamically with other modules is making a combined work based on [name of library]. Thus, the terms and conditions of the GNU General Public License cover the whole combination.
As a special exception, the copyright holders of [name of library] give you permission to combine [name of library] program with free software programs or libraries that are released under the GNU LGPL and with independent modules that communicate with [name of library] solely through the [name of library's interface] interface. You may copy and distribute such a system following the terms of the GNU GPL for [name of library] and the licenses of the other code concerned, provided that you include the source code of that other code when and as the GNU GPL requires distribution of source code and provided that you do not modify the [name of library's interface] interface.
Note that people who make modified versions of [name of library] are not obligated to grant this special exception for their modified versions; it is their choice whether to do so. The GNU General Public License gives permission to release a modified version without this exception; this exception also makes it possible to release a modified version which carries forward this exception. If you modify the [name of library's interface] interface, this exception does not apply to your modified version of [name of library], and you must remove this exception when you distribute your modified version.
This exception is an additional permission under section 7 of the GNU General Public License, version 3 ("GPLv3")

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