mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-23 05:17:29 +02:00
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:
@@ -76,7 +76,6 @@ cpu.out
|
||||
/yarn-error.log
|
||||
/npm-debug.log*
|
||||
/public/js
|
||||
/public/serviceworker.js
|
||||
/public/css
|
||||
/public/fonts
|
||||
/public/img/webpack
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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))
|
||||
|
||||
Generated
+2
-2
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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. |
|
||||
| **repo:status** | Grants read/write access to commit status in all repositories. |
|
||||
| **public_repo** | Grants read/write access to public repositories only. |
|
||||
| **admin:repo_hook** | Grants access to repository hooks of all repositories. This is included in the `repo` scope. |
|
||||
| **write:repo_hook** | Grants read/write access to repository hooks |
|
||||
| **read:repo_hook** | Grants read-only access to repository hooks |
|
||||
| **admin:org** | Grants full access to organization settings |
|
||||
| **write:org** | Grants read/write access to organization settings |
|
||||
| **read:org** | Grants read-only access to organization settings |
|
||||
| **admin:public_key** | Grants full access for managing public keys |
|
||||
| **write:public_key** | Grant read/write access to public keys |
|
||||
| **read:public_key** | Grant read-only access to public keys |
|
||||
| **admin:org_hook** | Grants full access to organizational-level hooks |
|
||||
| **admin:user_hook** | Grants full access to user-level hooks |
|
||||
| **notification** | Grants full access to notifications |
|
||||
| **user** | Grants full access to user profile info |
|
||||
| **read:user** | Grants read access to user's profile |
|
||||
| **user:email** | Grants read access to user's email addresses |
|
||||
| **user:follow** | Grants access to follow/un-follow a user |
|
||||
| **delete_repo** | Grants access to delete repositories as an admin |
|
||||
| **package** | Grants full access to hosted packages |
|
||||
| **write:package** | Grants read/write access to packages |
|
||||
| **read:package** | Grants read access to packages |
|
||||
| **delete:package** | Grants delete access to packages |
|
||||
| **admin:gpg_key** | Grants full access for managing GPG keys |
|
||||
| **write:gpg_key** | Grants read/write access to GPG keys |
|
||||
| **read:gpg_key** | Grants read-only access to GPG keys |
|
||||
| **admin:application** | Grants full access to manage applications |
|
||||
| **write:application** | Grants read/write access for managing applications |
|
||||
| **read:application** | Grants read access for managing applications |
|
||||
| **sudo** | Allows to perform actions as the site admin. |
|
||||
- `read`: `GET` routes
|
||||
- `write`: `POST`, `PUT`, `PATCH`, and `DELETE` routes (in addition to `GET`)
|
||||
|
||||
Gitea token scopes are as follows:
|
||||
|
||||
| Name | Description |
|
||||
| ---- |--------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **(no scope)** | Not supported. A scope is required even for public repositories. |
|
||||
| **activitypub** | `activitypub` API routes: ActivityPub related operations. |
|
||||
| **read:activitypub** | Grants read access for ActivityPub operations. |
|
||||
| **write:activitypub** | Grants read/write/delete access for ActivityPub operations. |
|
||||
| **admin** | `/admin/*` API routes: Site-wide administrative operations (hidden for non-admin accounts). |
|
||||
| **read:admin** | Grants read access for admin operations, such as getting cron jobs or registered user emails. |
|
||||
| **write:admin** | Grants read/write/delete access for admin operations, such as running cron jobs or updating user accounts. | |
|
||||
| **issue** | `issues/*`, `labels/*`, `milestones/*` API routes: Issue-related operations. |
|
||||
| **read:issue** | Grants read access for issues operations, such as getting issue comments, issue attachments, and milestones. |
|
||||
| **write:issue** | Grants read/write/delete access for issues operations, such as posting or editing an issue comment or attachment, and updating milestones. |
|
||||
| **misc** | miscellaneous and settings top-level API routes. |
|
||||
| **read:misc** | Grants read access to miscellaneous operations, such as getting label and gitignore templates. |
|
||||
| **write:misc** | Grants read/write/delete access to miscellaneous operations, such as markup utility operations. |
|
||||
| **notification** | `notification/*` API routes: user notification operations. |
|
||||
| **read:notification** | Grants read access to user notifications, such as which notifications users are subscribed to and read new notifications. |
|
||||
| **write:notification** | Grants read/write/delete access to user notifications, such as marking notifications as read. |
|
||||
| **organization** | `orgs/*` and `teams/*` API routes: Organization and team management operations. |
|
||||
| **read:organization** | Grants read access to org and team status, such as listing all orgs a user has visibility to, teams, and team members. |
|
||||
| **write:organization** | Grants read/write/delete access to org and team status, such as creating and updating teams and updating org settings. |
|
||||
| **package** | `/packages/*` API routes: Packages operations |
|
||||
| **read:package** | Grants read access to package operations, such as reading and downloading available packages. |
|
||||
| **write:package** | Grants read/write/delete access to package operations. Currently the same as `read:package`. |
|
||||
| **repository** | `/repos/*` API routes except `/repos/issues/*`: Repository file, pull-request, and release operations. |
|
||||
| **read:repository** | Grants read access to repository operations, such as getting repository files, releases, collaborators. |
|
||||
| **write:repository** | Grants read/write/delete access to repository operations, such as getting updating repository files, creating pull requests, updating collaborators. |
|
||||
| **user** | `/user/*` and `/users/*` API routes: User-related operations. |
|
||||
| **read:user** | Grants read access to user operations, such as getting user repo subscriptions and user settings. |
|
||||
| **write:user** | Grants read/write/delete access to user operations, such as updating user repo subscriptions, followed users, and user settings. |
|
||||
|
||||
## Client types
|
||||
|
||||
@@ -87,17 +88,19 @@ Gitea supports both confidential and public client types, [as defined by RFC 674
|
||||
|
||||
For public clients, a redirect URI of a loopback IP address such as `http://127.0.0.1/` allows any port. Avoid using `localhost`, [as recommended by RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252#section-8.3).
|
||||
|
||||
## Example
|
||||
## Examples
|
||||
|
||||
### Confidential client
|
||||
|
||||
**Note:** This example does not use PKCE.
|
||||
|
||||
1. Redirect to user to the authorization endpoint in order to get their consent for accessing the resources:
|
||||
1. Redirect the user to the authorization endpoint in order to get their consent for accessing the resources:
|
||||
|
||||
```curl
|
||||
https://[YOUR-GITEA-URL]/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI& response_type=code&state=STATE
|
||||
https://[YOUR-GITEA-URL]/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE
|
||||
```
|
||||
|
||||
The `CLIENT_ID` can be obtained by registering an application in the settings. The `STATE` is a random string that will be send back to your application after the user authorizes. The `state` parameter is optional but should be used to prevent CSRF attacks.
|
||||
The `CLIENT_ID` can be obtained by registering an application in the settings. The `STATE` is a random string that will be sent back to your application after the user authorizes. The `state` parameter is optional, but should be used to prevent CSRF attacks.
|
||||
|
||||

|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
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以及如何运行它们。
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -31,4 +31,4 @@ WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
|
||||
|
||||
## 合併請求範本
|
||||
|
||||
您可以在[問題與合併請求範本](../issue-pull-request-templates)找到更多關於合併請求範本的資訊。
|
||||
您可以在[問題與合併請求範本](issue-pull-request-templates)找到更多關於合併請求範本的資訊。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/) 获得更多的第三方集成支持
|
||||
|
||||
@@ -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/) 獲得更多的第三方集成支持
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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>`)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user