* origin/main: (62 commits) Add test for #30674 (#30679) Fix border-radius of header+segment boxes (#30667) Fix a panic bug when head repository deleting (#30674) Fix some bug on migrations (#30647) Fix checkbox field markup (#30666) Avoid doubled border for the PR info segment (#30663) Interpolate runs-on with variables when scheduling tasks (#30640) Initial support for colorblindness-friendly themes (#30625) Fix flash message for flex-container (#30657) Perform Newest sort type correctly when sorting issues (#30644) Fix project name wrapping, remove horizontal margin on header (#30631) Add a db consistency check to remove runners that do not belong to a repository (#30614) Fix wrong table name (#30557) Fix compare api swagger (#30648) [skip ci] Updated translations via Crowdin Fix queue test (#30646) Enable jquery-related eslint rules that have no violations (#30632) Enable more `revive` linter rules (#30608) Remove obsolete CSS text classes (#30576) Hide diff stats on empty PRs (#30629) ...pull/28163/head
@@ -310,7 +310,7 @@ rules: | |||
jquery/no-merge: [2] | |||
jquery/no-param: [2] | |||
jquery/no-parent: [0] | |||
jquery/no-parents: [0] | |||
jquery/no-parents: [2] | |||
jquery/no-parse-html: [2] | |||
jquery/no-prop: [2] | |||
jquery/no-proxy: [2] | |||
@@ -319,8 +319,8 @@ rules: | |||
jquery/no-show: [2] | |||
jquery/no-size: [2] | |||
jquery/no-sizzle: [2] | |||
jquery/no-slide: [0] | |||
jquery/no-submit: [0] | |||
jquery/no-slide: [2] | |||
jquery/no-submit: [2] | |||
jquery/no-text: [0] | |||
jquery/no-toggle: [2] | |||
jquery/no-trigger: [0] | |||
@@ -458,7 +458,7 @@ rules: | |||
no-jquery/no-other-utils: [2] | |||
no-jquery/no-param: [2] | |||
no-jquery/no-parent: [0] | |||
no-jquery/no-parents: [0] | |||
no-jquery/no-parents: [2] | |||
no-jquery/no-parse-html-literal: [0] | |||
no-jquery/no-parse-html: [2] | |||
no-jquery/no-parse-json: [2] |
@@ -38,6 +38,8 @@ jobs: | |||
- uses: actions/setup-node@v4 | |||
with: | |||
node-version: 20 | |||
cache: npm | |||
cache-dependency-path: package-lock.json | |||
- run: pip install poetry | |||
- run: make deps-py | |||
- run: make deps-frontend | |||
@@ -65,6 +67,8 @@ jobs: | |||
- uses: actions/setup-node@v4 | |||
with: | |||
node-version: 20 | |||
cache: npm | |||
cache-dependency-path: package-lock.json | |||
- run: make deps-frontend | |||
- run: make lint-swagger | |||
@@ -134,6 +138,8 @@ jobs: | |||
- uses: actions/setup-node@v4 | |||
with: | |||
node-version: 20 | |||
cache: npm | |||
cache-dependency-path: package-lock.json | |||
- run: make deps-frontend | |||
- run: make lint-frontend | |||
- run: make checks-frontend | |||
@@ -181,6 +187,8 @@ jobs: | |||
- uses: actions/setup-node@v4 | |||
with: | |||
node-version: 20 | |||
cache: npm | |||
cache-dependency-path: package-lock.json | |||
- run: make deps-frontend | |||
- run: make lint-md | |||
- run: make docs |
@@ -24,6 +24,8 @@ jobs: | |||
- uses: actions/setup-node@v4 | |||
with: | |||
node-version: 20 | |||
cache: npm | |||
cache-dependency-path: package-lock.json | |||
- run: make deps-frontend frontend deps-backend | |||
- run: npx playwright install --with-deps | |||
- run: make test-e2e-sqlite |
@@ -25,6 +25,8 @@ jobs: | |||
- uses: actions/setup-node@v4 | |||
with: | |||
node-version: 20 | |||
cache: npm | |||
cache-dependency-path: package-lock.json | |||
- run: make deps-frontend deps-backend | |||
# xgo build | |||
- run: make release |
@@ -24,6 +24,8 @@ jobs: | |||
- uses: actions/setup-node@v4 | |||
with: | |||
node-version: 20 | |||
cache: npm | |||
cache-dependency-path: package-lock.json | |||
- run: make deps-frontend deps-backend | |||
# xgo build | |||
- run: make release |
@@ -26,6 +26,8 @@ jobs: | |||
- uses: actions/setup-node@v4 | |||
with: | |||
node-version: 20 | |||
cache: npm | |||
cache-dependency-path: package-lock.json | |||
- run: make deps-frontend deps-backend | |||
# xgo build | |||
- run: make release |
@@ -1,13 +1,14 @@ | |||
linters: | |||
enable-all: false | |||
disable-all: true | |||
fast: false | |||
enable: | |||
- bidichk | |||
# - deadcode # deprecated - https://github.com/golangci/golangci-lint/issues/1841 | |||
- depguard | |||
- dupl | |||
- errcheck | |||
- forbidigo | |||
- gocritic | |||
# - gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time. | |||
- gofmt | |||
- gofumpt | |||
- gosimple | |||
@@ -17,20 +18,18 @@ linters: | |||
- nolintlint | |||
- revive | |||
- staticcheck | |||
# - structcheck # deprecated - https://github.com/golangci/golangci-lint/issues/1841 | |||
- stylecheck | |||
- typecheck | |||
- unconvert | |||
- unused | |||
# - varcheck # deprecated - https://github.com/golangci/golangci-lint/issues/1841 | |||
- wastedassign | |||
enable-all: false | |||
disable-all: true | |||
fast: false | |||
run: | |||
timeout: 10m | |||
output: | |||
sort-results: true | |||
linters-settings: | |||
stylecheck: | |||
checks: ["all", "-ST1005", "-ST1003"] | |||
@@ -47,27 +46,37 @@ linters-settings: | |||
errorCode: 1 | |||
warningCode: 1 | |||
rules: | |||
- name: atomic | |||
- name: bare-return | |||
- name: blank-imports | |||
- name: constant-logical-expr | |||
- name: context-as-argument | |||
- name: context-keys-type | |||
- name: dot-imports | |||
- name: duplicated-imports | |||
- name: empty-lines | |||
- name: error-naming | |||
- name: error-return | |||
- name: error-strings | |||
- name: error-naming | |||
- name: errorf | |||
- name: exported | |||
- name: identical-branches | |||
- name: if-return | |||
- name: increment-decrement | |||
- name: var-naming | |||
- name: var-declaration | |||
- name: indent-error-flow | |||
- name: modifies-value-receiver | |||
- name: package-comments | |||
- name: range | |||
- name: receiver-naming | |||
- name: redefines-builtin-id | |||
- name: string-of-int | |||
- name: superfluous-else | |||
- name: time-naming | |||
- name: unconditional-recursion | |||
- name: unexported-return | |||
- name: indent-error-flow | |||
- name: errorf | |||
- name: duplicated-imports | |||
- name: modifies-value-receiver | |||
- name: unreachable-code | |||
- name: var-declaration | |||
- name: var-naming | |||
gofumpt: | |||
extra-rules: true | |||
depguard: | |||
@@ -93,8 +102,8 @@ issues: | |||
max-issues-per-linter: 0 | |||
max-same-issues: 0 | |||
exclude-dirs: [node_modules, public, web_src] | |||
exclude-case-sensitive: true | |||
exclude-rules: | |||
# Exclude some linters from running on tests files. | |||
- path: _test\.go | |||
linters: | |||
- gocyclo | |||
@@ -112,19 +121,19 @@ issues: | |||
- path: cmd | |||
linters: | |||
- forbidigo | |||
- linters: | |||
- text: "webhook" | |||
linters: | |||
- dupl | |||
text: "webhook" | |||
- linters: | |||
- text: "`ID' should not be capitalized" | |||
linters: | |||
- gocritic | |||
text: "`ID' should not be capitalized" | |||
- linters: | |||
- text: "swagger" | |||
linters: | |||
- unused | |||
- deadcode | |||
text: "swagger" | |||
- linters: | |||
- text: "argument x is overwritten before first use" | |||
linters: | |||
- staticcheck | |||
text: "argument x is overwritten before first use" | |||
- text: "commentFormatting: put a space between `//` and comment text" | |||
linters: | |||
- gocritic |
@@ -110,7 +110,6 @@ LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(G | |||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64 | |||
GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) | |||
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) | |||
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) | |||
@@ -144,9 +143,9 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN | |||
GO_DIRS := build cmd models modules routers services tests | |||
WEB_DIRS := web_src/js web_src/css | |||
ESLINT_FILES := web_src/js tools *.config.js tests/e2e | |||
ESLINT_FILES := web_src/js tools *.js tests/e2e | |||
STYLELINT_FILES := web_src/css web_src/js/components/*.vue | |||
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github | |||
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github $(filter-out CHANGELOG.md, $(wildcard *.go *.js *.md *.yml *.yaml *.toml)) | |||
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini | |||
GO_SOURCES := $(wildcard *.go) | |||
@@ -423,7 +422,7 @@ lint-go-windows: | |||
lint-go-vet: | |||
@echo "Running go vet..." | |||
@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet | |||
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES) | |||
@$(GO) vet -vettool=gitea-vet ./... | |||
.PHONY: lint-editorconfig | |||
lint-editorconfig: | |||
@@ -779,7 +778,7 @@ generate-backend: $(TAGS_PREREQ) generate-go | |||
.PHONY: generate-go | |||
generate-go: $(TAGS_PREREQ) | |||
@echo "Running go generate..." | |||
@CC= GOOS= GOARCH= $(GO) generate -tags '$(TAGS)' $(GO_PACKAGES) | |||
@CC= GOOS= GOARCH= $(GO) generate -tags '$(TAGS)' ./... | |||
.PHONY: security-check | |||
security-check: |
@@ -4,6 +4,7 @@ | |||
package cmd | |||
import ( | |||
"errors" | |||
"fmt" | |||
"os" | |||
"text/tabwriter" | |||
@@ -91,7 +92,7 @@ func runListAuth(c *cli.Context) error { | |||
func runDeleteAuth(c *cli.Context) error { | |||
if !c.IsSet("id") { | |||
return fmt.Errorf("--id flag is missing") | |||
return errors.New("--id flag is missing") | |||
} | |||
ctx, cancel := installSignals() |
@@ -4,6 +4,7 @@ | |||
package cmd | |||
import ( | |||
"errors" | |||
"fmt" | |||
"net/url" | |||
@@ -193,7 +194,7 @@ func runAddOauth(c *cli.Context) error { | |||
func runUpdateOauth(c *cli.Context) error { | |||
if !c.IsSet("id") { | |||
return fmt.Errorf("--id flag is missing") | |||
return errors.New("--id flag is missing") | |||
} | |||
ctx, cancel := installSignals() |
@@ -5,7 +5,6 @@ package cmd | |||
import ( | |||
"errors" | |||
"fmt" | |||
"strings" | |||
auth_model "code.gitea.io/gitea/models/auth" | |||
@@ -166,7 +165,7 @@ func runAddSMTP(c *cli.Context) error { | |||
func runUpdateSMTP(c *cli.Context) error { | |||
if !c.IsSet("id") { | |||
return fmt.Errorf("--id flag is missing") | |||
return errors.New("--id flag is missing") | |||
} | |||
ctx, cancel := installSignals() |
@@ -4,6 +4,7 @@ | |||
package cmd | |||
import ( | |||
"errors" | |||
"fmt" | |||
"strings" | |||
@@ -42,7 +43,7 @@ var microcmdUserDelete = &cli.Command{ | |||
func runDeleteUser(c *cli.Context) error { | |||
if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { | |||
return fmt.Errorf("You must provide the id, username or email of a user to delete") | |||
return errors.New("You must provide the id, username or email of a user to delete") | |||
} | |||
ctx, cancel := installSignals() |
@@ -4,6 +4,7 @@ | |||
package cmd | |||
import ( | |||
"errors" | |||
"fmt" | |||
auth_model "code.gitea.io/gitea/models/auth" | |||
@@ -42,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{ | |||
func runGenerateAccessToken(c *cli.Context) error { | |||
if !c.IsSet("username") { | |||
return fmt.Errorf("You must provide a username to generate a token for") | |||
return errors.New("You must provide a username to generate a token for") | |||
} | |||
ctx, cancel := installSignals() | |||
@@ -68,7 +69,7 @@ func runGenerateAccessToken(c *cli.Context) error { | |||
return err | |||
} | |||
if exist { | |||
return fmt.Errorf("access token name has been used already") | |||
return errors.New("access token name has been used already") | |||
} | |||
// make sure the scopes are valid |
@@ -87,6 +87,10 @@ var CmdDump = &cli.Command{ | |||
Name: "skip-index", | |||
Usage: "Skip bleve index data", | |||
}, | |||
&cli.BoolFlag{ | |||
Name: "skip-db", | |||
Usage: "Skip database", | |||
}, | |||
&cli.StringFlag{ | |||
Name: "type", | |||
Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")), | |||
@@ -185,35 +189,41 @@ func runDump(ctx *cli.Context) error { | |||
} | |||
} | |||
tmpDir := ctx.String("tempdir") | |||
if _, err := os.Stat(tmpDir); os.IsNotExist(err) { | |||
fatal("Path does not exist: %s", tmpDir) | |||
} | |||
if ctx.Bool("skip-db") { | |||
// Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere. | |||
dumper.GlobalExcludeAbsPath(setting.Database.Path) | |||
log.Info("Skipping database") | |||
} else { | |||
tmpDir := ctx.String("tempdir") | |||
if _, err := os.Stat(tmpDir); os.IsNotExist(err) { | |||
fatal("Path does not exist: %s", tmpDir) | |||
} | |||
dbDump, err := os.CreateTemp(tmpDir, "gitea-db.sql") | |||
if err != nil { | |||
fatal("Failed to create tmp file: %v", err) | |||
} | |||
defer func() { | |||
_ = dbDump.Close() | |||
if err := util.Remove(dbDump.Name()); err != nil { | |||
log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err) | |||
dbDump, err := os.CreateTemp(tmpDir, "gitea-db.sql") | |||
if err != nil { | |||
fatal("Failed to create tmp file: %v", err) | |||
} | |||
}() | |||
defer func() { | |||
_ = dbDump.Close() | |||
if err := util.Remove(dbDump.Name()); err != nil { | |||
log.Warn("Unable to remove temporary file: %s: Error: %v", dbDump.Name(), err) | |||
} | |||
}() | |||
targetDBType := ctx.String("database") | |||
if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() { | |||
log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType) | |||
} else { | |||
log.Info("Dumping database...") | |||
} | |||
targetDBType := ctx.String("database") | |||
if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() { | |||
log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType) | |||
} else { | |||
log.Info("Dumping database...") | |||
} | |||
if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil { | |||
fatal("Failed to dump database: %v", err) | |||
} | |||
if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil { | |||
fatal("Failed to dump database: %v", err) | |||
} | |||
if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil { | |||
fatal("Failed to include gitea-db.sql: %v", err) | |||
if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil { | |||
fatal("Failed to include gitea-db.sql: %v", err) | |||
} | |||
} | |||
log.Info("Adding custom configuration file from %s", setting.CustomConf) |
@@ -157,9 +157,9 @@ func runViewDo(c *cli.Context) error { | |||
} | |||
if len(matchedAssetFiles) == 0 { | |||
return fmt.Errorf("no files matched the given pattern") | |||
return errors.New("no files matched the given pattern") | |||
} else if len(matchedAssetFiles) > 1 { | |||
return fmt.Errorf("too many files matched the given pattern, try to be more specific") | |||
return errors.New("too many files matched the given pattern, try to be more specific") | |||
} | |||
data, err := matchedAssetFiles[0].fs.ReadFile(matchedAssetFiles[0].name) | |||
@@ -180,7 +180,7 @@ func runExtractDo(c *cli.Context) error { | |||
} | |||
if c.NArg() == 0 { | |||
return fmt.Errorf("a list of pattern of files to extract is mandatory (e.g. '**' for all)") | |||
return errors.New("a list of pattern of files to extract is mandatory (e.g. '**' for all)") | |||
} | |||
destdir := "." |
@@ -465,7 +465,7 @@ func hookPrintResult(output, isCreate bool, branch, url string) { | |||
fmt.Fprintf(os.Stderr, " %s\n", url) | |||
} | |||
fmt.Fprintln(os.Stderr, "") | |||
os.Stderr.Sync() | |||
_ = os.Stderr.Sync() | |||
} | |||
func pushOptions() map[string]string { |
@@ -4,6 +4,7 @@ | |||
package cmd | |||
import ( | |||
"errors" | |||
"fmt" | |||
"os" | |||
@@ -249,7 +250,7 @@ func runAddFileLogger(c *cli.Context) error { | |||
if c.IsSet("filename") { | |||
vals["filename"] = c.String("filename") | |||
} else { | |||
return fmt.Errorf("filename must be set when creating a file logger") | |||
return errors.New("filename must be set when creating a file logger") | |||
} | |||
if c.IsSet("rotate") { | |||
vals["rotate"] = c.Bool("rotate") |
@@ -1231,7 +1231,8 @@ LEVEL = Info | |||
;DEFAULT_THEME = gitea-auto | |||
;; | |||
;; All available themes. Allow users select personalized themes regardless of the value of `DEFAULT_THEME`. | |||
;THEMES = gitea-auto,gitea-light,gitea-dark | |||
;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css" | |||
;THEMES = | |||
;; | |||
;; All available reactions users can choose on issues/prs and comments. | |||
;; Values can be emoji alias (:smile:) or a unicode emoji. | |||
@@ -2377,22 +2378,6 @@ LEVEL = Info | |||
;; Enable issue by repository metrics; default is false | |||
;ENABLED_ISSUE_BY_REPOSITORY = false | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||
;[task] | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||
;; | |||
;; Task queue type, could be `channel` or `redis`. | |||
;QUEUE_TYPE = channel | |||
;; | |||
;; Task queue length, available only when `QUEUE_TYPE` is `channel`. | |||
;QUEUE_LENGTH = 1000 | |||
;; | |||
;; Task queue connection string, available only when `QUEUE_TYPE` is `redis`. | |||
;; If there is a password of redis, use `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` or `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` for `redis-clsuter`. | |||
;QUEUE_CONN_STR = "redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s" | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||
;[migrations] |
@@ -214,10 +214,9 @@ The following configuration set `Content-Type: application/vnd.android.package-a | |||
- `SITEMAP_PAGING_NUM`: **20**: Number of items that are displayed in a single subsitemap. | |||
- `GRAPH_MAX_COMMIT_NUM`: **100**: Number of maximum commits shown in the commit graph. | |||
- `CODE_COMMENT_LINES`: **4**: Number of line of codes shown for a code comment. | |||
- `DEFAULT_THEME`: **gitea-auto**: \[gitea-auto, gitea-light, gitea-dark\]: Set the default theme for the Gitea installation. | |||
- `DEFAULT_THEME`: **gitea-auto**: Set the default theme for the Gitea installation, custom themes could be provided by "{CustomPath}/public/assets/css/theme-*.css". | |||
- `SHOW_USER_EMAIL`: **true**: Whether the email of the user should be shown in the Explore Users page. | |||
- `THEMES`: **gitea-auto,gitea-light,gitea-dark**: All available themes. Allow users select personalized themes. | |||
regardless of the value of `DEFAULT_THEME`. | |||
- `THEMES`: **_empty_**: All available themes by "{CustomPath}/public/assets/css/theme-*.css". Allow users select personalized themes. | |||
- `MAX_DISPLAY_FILE_SIZE`: **8388608**: Max size of files to be displayed (default is 8MiB) | |||
- `AMBIGUOUS_UNICODE_DETECTION`: **true**: Detect ambiguous unicode characters in file contents and show warnings on the UI | |||
- `REACTIONS`: All available reactions users can choose on issues/prs and comments | |||
@@ -1198,14 +1197,6 @@ in this mapping or the filetype using heuristics. | |||
- `DEFAULT_UI_LOCATION`: Default location of time on the UI, so that we can display correct user's time on UI. i.e. Asia/Shanghai | |||
## Task (`task`) | |||
Task queue configuration has been moved to `queue.task`. However, the below configuration values are kept for backwards compatibility: | |||
- `QUEUE_TYPE`: **channel**: Task queue type, could be `channel` or `redis`. | |||
- `QUEUE_LENGTH`: **1000**: Task queue length, available only when `QUEUE_TYPE` is `channel`. | |||
- `QUEUE_CONN_STR`: **redis://127.0.0.1:6379/0**: Task queue connection string, available only when `QUEUE_TYPE` is `redis`. If redis needs a password, use `redis://123@127.0.0.1:6379/0` or `redis+cluster://123@127.0.0.1:6379/0`. | |||
## Migrations (`migrations`) | |||
- `MAX_ATTEMPTS`: **3**: Max attempts per http/https request on migrations. |
@@ -212,10 +212,9 @@ menu: | |||
- `SITEMAP_PAGING_NUM`: **20**: 在单个子SiteMap中显示的项数。 | |||
- `GRAPH_MAX_COMMIT_NUM`: **100**: 提交图中显示的最大commit数量。 | |||
- `CODE_COMMENT_LINES`: **4**: 在代码评论中能够显示的最大代码行数。 | |||
- `DEFAULT_THEME`: **gitea-auto**: \[gitea-auto, gitea-light, gitea-dark\]: 在Gitea安装时候设置的默认主题。 | |||
- `DEFAULT_THEME`: **gitea-auto**: 在Gitea安装时候设置的默认主题,自定义的主题可以通过 "{CustomPath}/public/assets/css/theme-*.css" 提供。 | |||
- `SHOW_USER_EMAIL`: **true**: 用户的电子邮件是否应该显示在`Explore Users`页面中。 | |||
- `THEMES`: **gitea-auto,gitea-light,gitea-dark**: 所有可用的主题。允许用户选择个性化的主题, | |||
而不受DEFAULT_THEME 值的影响。 | |||
- `THEMES`: **_empty_**: 所有可用的主题(由 "{CustomPath}/public/assets/css/theme-*.css" 提供)。允许用户选择个性化的主题, | |||
- `MAX_DISPLAY_FILE_SIZE`: **8388608**: 能够显示文件的最大大小(默认为8MiB)。 | |||
- `REACTIONS`: 用户可以在问题(Issue)、Pull Request(PR)以及评论中选择的所有可选的反应。 | |||
这些值可以是表情符号别名(例如::smile:)或Unicode表情符号。 | |||
@@ -1128,15 +1127,6 @@ ALLOW_DATA_URI_IMAGES = true | |||
- `DEFAULT_UI_LOCATION`:在 UI 上的默认时间位置,以便我们可以在 UI 上显示正确的用户时间。例如:Asia/Shanghai | |||
## 任务 (`task`) | |||
任务队列配置已移动到 `queue.task`。然而,以下配置值仍保留以确保向后兼容: | |||
- `QUEUE_TYPE`:**channel**:任务队列类型,可以是 `channel` 或 `redis`。 | |||
- `QUEUE_LENGTH`:**1000**:任务队列长度,仅在 `QUEUE_TYPE` 为 `channel` 时可用。 | |||
- `QUEUE_CONN_STR`:**redis://127.0.0.1:6379/0**:任务队列连接字符串,仅在 `QUEUE_TYPE` 为 `redis` 时可用。 | |||
如果 redis 需要密码,使用 `redis://123@127.0.0.1:6379/0` 或 `redis+cluster://123@127.0.0.1:6379/0`。 | |||
## 迁移 (`migrations`) | |||
- `MAX_ATTEMPTS`:**3**:每次 http/https 请求的最大尝试次数(用于迁移)。 |
@@ -381,7 +381,7 @@ To make a custom theme available to all users: | |||
1. Add a CSS file to `$GITEA_CUSTOM/public/assets/css/theme-<theme-name>.css`. | |||
The value of `$GITEA_CUSTOM` of your instance can be queried by calling `gitea help` and looking up the value of "CustomPath". | |||
2. Add `<theme-name>` to the comma-separated list of setting `THEMES` in `app.ini` | |||
2. Add `<theme-name>` to the comma-separated list of setting `THEMES` in `app.ini`, or leave `THEMES` empty to allow all themes. | |||
Community themes are listed in [gitea/awesome-gitea#themes](https://gitea.com/gitea/awesome-gitea#themes). | |||
@@ -178,17 +178,6 @@ At some point, a customer or third party needs access to a specific repo and onl | |||
Use [Fail2Ban](administration/fail2ban-setup.md) to monitor and stop automated login attempts or other malicious behavior based on log patterns | |||
## How to add/use custom themes | |||
Gitea supports three official themes right now, `gitea-light`, `gitea-dark`, and `gitea-auto` (automatically switches between the previous two depending on operating system settings). | |||
To add your own theme, currently the only way is to provide a complete theme (not just color overrides) | |||
As an example, let's say our theme is `arc-blue` (this is a real theme, and can be found [in this issue](https://github.com/go-gitea/gitea/issues/6011)) | |||
Name the `.css` file `theme-arc-blue.css` and add it to your custom folder in `custom/public/assets/css` | |||
Allow users to use it by adding `arc-blue` to the list of `THEMES` in your `app.ini` | |||
## SSHD vs built-in SSH | |||
SSHD is the built-in SSH server on most Unix systems. |
@@ -182,17 +182,6 @@ Gitea不提供内置的Pages服务器。您需要一个专用的域名来提供 | |||
使用 [Fail2Ban](administration/fail2ban-setup.md) 监视并阻止基于日志模式的自动登录尝试或其他恶意行为。 | |||
## 如何添加/使用自定义主题 | |||
Gitea 目前支持三个官方主题,分别是 `gitea-light`、`gitea-dark` 和 `gitea-auto`(根据操作系统设置自动切换前两个主题)。 | |||
要添加自己的主题,目前唯一的方法是提供一个完整的主题(不仅仅是颜色覆盖)。 | |||
假设我们的主题是 `arc-blue`(这是一个真实的主题,可以在[此问题](https://github.com/go-gitea/gitea/issues/6011)中找到) | |||
将`.css`文件命名为`theme-arc-blue.css`并将其添加到`custom/public/assets/css`文件夹中 | |||
通过将`arc-blue`添加到`app.ini`中的`THEMES`列表中,允许用户使用该主题 | |||
## SSHD vs 内建SSH | |||
SSHD是大多数Unix系统上内建的SSH服务器。 |
@@ -16,7 +16,6 @@ require ( | |||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 | |||
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 | |||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 | |||
github.com/NYTimes/gziphandler v1.1.1 | |||
github.com/PuerkitoBio/goquery v1.9.1 | |||
github.com/alecthomas/chroma/v2 v2.13.0 | |||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb | |||
@@ -67,7 +66,7 @@ require ( | |||
github.com/json-iterator/go v1.1.12 | |||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 | |||
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 | |||
github.com/klauspost/compress v1.17.7 | |||
github.com/klauspost/compress v1.17.8 | |||
github.com/klauspost/cpuid/v2 v2.2.7 | |||
github.com/lib/pq v1.10.9 | |||
github.com/markbates/goth v1.79.0 |
@@ -70,8 +70,6 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa | |||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= | |||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= | |||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= | |||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= | |||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= | |||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= | |||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= | |||
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= | |||
@@ -500,8 +498,8 @@ github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs | |||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= | |||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= | |||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= | |||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= | |||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= | |||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= | |||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= | |||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= | |||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | |||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= |
@@ -98,13 +98,10 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error { | |||
return nil | |||
} | |||
if run.Repo == nil { | |||
repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) | |||
if err != nil { | |||
return err | |||
} | |||
run.Repo = repo | |||
if err := run.LoadRepo(ctx); err != nil { | |||
return err | |||
} | |||
if err := run.Repo.LoadAttributes(ctx); err != nil { | |||
return err | |||
} | |||
@@ -120,6 +117,19 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error { | |||
return nil | |||
} | |||
func (run *ActionRun) LoadRepo(ctx context.Context) error { | |||
if run == nil || run.Repo != nil { | |||
return nil | |||
} | |||
repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) | |||
if err != nil { | |||
return err | |||
} | |||
run.Repo = repo | |||
return nil | |||
} | |||
func (run *ActionRun) Duration() time.Duration { | |||
return calculateDuration(run.Started, run.Stopped, run.Status) + run.PreviousDuration | |||
} |
@@ -270,7 +270,7 @@ func CountRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) { | |||
// Only affect action runners were a owner ID is set, as actions runners | |||
// could also be created on a repository. | |||
return db.GetEngine(ctx).Table("action_runner"). | |||
Join("LEFT", "user", "`action_runner`.owner_id = `user`.id"). | |||
Join("LEFT", "`user`", "`action_runner`.owner_id = `user`.id"). | |||
Where("`action_runner`.owner_id != ?", 0). | |||
And(builder.IsNull{"`user`.id"}). | |||
Count(new(ActionRunner)) | |||
@@ -279,7 +279,7 @@ func CountRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) { | |||
func FixRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) { | |||
subQuery := builder.Select("`action_runner`.id"). | |||
From("`action_runner`"). | |||
Join("LEFT", "user", "`action_runner`.owner_id = `user`.id"). | |||
Join("LEFT", "`user`", "`action_runner`.owner_id = `user`.id"). | |||
Where(builder.Neq{"`action_runner`.owner_id": 0}). | |||
And(builder.IsNull{"`user`.id"}) | |||
b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`") | |||
@@ -289,3 +289,25 @@ func FixRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) { | |||
} | |||
return res.RowsAffected() | |||
} | |||
func CountRunnersWithoutBelongingRepo(ctx context.Context) (int64, error) { | |||
return db.GetEngine(ctx).Table("action_runner"). | |||
Join("LEFT", "`repository`", "`action_runner`.repo_id = `repository`.id"). | |||
Where("`action_runner`.repo_id != ?", 0). | |||
And(builder.IsNull{"`repository`.id"}). | |||
Count(new(ActionRunner)) | |||
} | |||
func FixRunnersWithoutBelongingRepo(ctx context.Context) (int64, error) { | |||
subQuery := builder.Select("`action_runner`.id"). | |||
From("`action_runner`"). | |||
Join("LEFT", "`repository`", "`action_runner`.repo_id = `repository`.id"). | |||
Where(builder.Neq{"`action_runner`.repo_id": 0}). | |||
And(builder.IsNull{"`repository`.id"}) | |||
b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`") | |||
res, err := db.GetEngine(ctx).Exec(b) | |||
if err != nil { | |||
return 0, err | |||
} | |||
return res.RowsAffected() | |||
} |
@@ -92,6 +92,11 @@ func DeleteVariable(ctx context.Context, id int64) error { | |||
func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) { | |||
variables := map[string]string{} | |||
if err := run.LoadRepo(ctx); err != nil { | |||
log.Error("LoadRepo: %v", err) | |||
return nil, err | |||
} | |||
// Global | |||
globalVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{}) | |||
if err != nil { |
@@ -110,7 +110,6 @@ func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerific | |||
Reason: "gpg.error.no_committer_account", | |||
} | |||
} | |||
} | |||
} | |||
@@ -8,6 +8,7 @@ import ( | |||
"crypto/sha256" | |||
"encoding/base32" | |||
"encoding/base64" | |||
"errors" | |||
"fmt" | |||
"net" | |||
"net/url" | |||
@@ -294,7 +295,7 @@ func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOp | |||
return nil, err | |||
} | |||
if app.UID != opts.UserID { | |||
return nil, fmt.Errorf("UID mismatch") | |||
return nil, errors.New("UID mismatch") | |||
} | |||
builtinApps := BuiltinApplications() | |||
if _, builtin := builtinApps[app.ClientID]; builtin { |
@@ -13,8 +13,6 @@ import ( | |||
"github.com/stretchr/testify/assert" | |||
) | |||
//////////////////// Application | |||
func TestOAuth2Application_GenerateClientSecret(t *testing.T) { | |||
assert.NoError(t, unittest.PrepareTestDatabase()) | |||
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) |
@@ -227,7 +227,6 @@ func NamesToBean(names ...string) ([]any, error) { | |||
// Need to map provided names to beans... | |||
beanMap := make(map[string]any) | |||
for _, bean := range tables { | |||
beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean | |||
beanMap[strings.ToLower(x.TableName(bean))] = bean | |||
beanMap[strings.ToLower(x.TableName(bean, true))] = bean |
@@ -5,7 +5,7 @@ package git | |||
import ( | |||
"context" | |||
"fmt" | |||
"errors" | |||
"strings" | |||
"time" | |||
@@ -148,7 +148,7 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor | |||
} | |||
if !force && u.ID != lock.OwnerID { | |||
return nil, fmt.Errorf("user doesn't own lock and force flag is not set") | |||
return nil, errors.New("user doesn't own lock and force flag is not set") | |||
} | |||
if _, err := db.GetEngine(dbCtx).ID(id).Delete(new(LFSLock)); err != nil { |
@@ -62,11 +62,13 @@ func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission, | |||
return true | |||
} | |||
if len(p.Units) < 1 { | |||
// the code below depends on units to get the repository ID, not ideal but just keep it for now | |||
firstUnitRepoID := p.GetFirstUnitRepoID() | |||
if firstUnitRepoID == 0 { | |||
return false | |||
} | |||
prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, p.Units[0].RepoID, branch) | |||
prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, branch) | |||
if err != nil { | |||
return false | |||
} |
@@ -345,11 +345,9 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error | |||
return nil, err | |||
} | |||
} | |||
} else if opts.ReviewerTeam != nil { | |||
review.Type = ReviewTypeRequest | |||
review.ReviewerTeamID = opts.ReviewerTeam.ID | |||
} else { | |||
return nil, fmt.Errorf("provide either reviewer or reviewer team") | |||
} |
@@ -177,7 +177,6 @@ func RecreateTable(sess *xorm.Session, bean any) error { | |||
log.Error("Unable to recreate uniques on table %s. Error: %v", tableName, err) | |||
return err | |||
} | |||
case setting.Database.Type.IsMySQL(): | |||
// MySQL will drop all the constraints on the old table | |||
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil { | |||
@@ -228,7 +227,6 @@ func RecreateTable(sess *xorm.Session, bean any) error { | |||
return err | |||
} | |||
sequenceMap[sequence] = sequenceData | |||
} | |||
// CASCADE causes postgres to drop all the constraints on the old table | |||
@@ -293,9 +291,7 @@ func RecreateTable(sess *xorm.Session, bean any) error { | |||
return err | |||
} | |||
} | |||
} | |||
case setting.Database.Type.IsMSSQL(): | |||
// MSSQL will drop all the constraints on the old table | |||
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil { | |||
@@ -308,7 +304,6 @@ func RecreateTable(sess *xorm.Session, bean any) error { | |||
log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err) | |||
return err | |||
} | |||
default: | |||
log.Fatal("Unrecognized DB") | |||
} |
@@ -582,6 +582,10 @@ var migrations = []Migration{ | |||
NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary), | |||
// v296 -> v297 | |||
NewMigration("Add missing field of commit status summary table", v1_23.AddCommitStatusSummary2), | |||
// v297 -> v298 | |||
NewMigration("Add everyone_access_mode for repo_unit", v1_23.AddRepoUnitEveryoneAccessMode), | |||
// v298 -> v299 | |||
NewMigration("Drop wrongly created table o_auth2_application", v1_23.DropWronglyCreatedTable), | |||
} | |||
// GetCurrentDBVersion returns the current db version |
@@ -262,7 +262,6 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { | |||
for _, u := range units { | |||
var found bool | |||
for _, team := range teams { | |||
var teamU []*TeamUnit | |||
var unitEnabled bool | |||
err = sess.Where("team_id = ?", team.ID).Find(&teamU) | |||
@@ -331,12 +330,11 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { | |||
} | |||
if !protectedBranch.EnableApprovalsWhitelist { | |||
perm, err := getUserRepoPermission(sess, baseRepo, reviewer) | |||
if err != nil { | |||
return false, err | |||
} | |||
if perm.UnitsMode == nil { | |||
if len(perm.UnitsMode) == 0 { | |||
for _, u := range perm.Units { | |||
if u.Type == UnitTypeCode { | |||
return AccessModeWrite <= perm.AccessMode, nil |
@@ -43,11 +43,6 @@ func RemigrateU2FCredentials(x *xorm.Engine) error { | |||
if err != nil { | |||
return err | |||
} | |||
case schemas.ORACLE: | |||
_, err := x.Exec("ALTER TABLE webauthn_credential MODIFY credential_id VARCHAR(410)") | |||
if err != nil { | |||
return err | |||
} | |||
case schemas.MSSQL: | |||
// This column has an index on it. I could write all of the code to attempt to change the index OR | |||
// I could just use recreate table. |
@@ -9,9 +9,9 @@ import ( | |||
// AddConfidentialColumnToOAuth2ApplicationTable: add ConfidentialClient column, setting existing rows to true | |||
func AddConfidentialClientColumnToOAuth2ApplicationTable(x *xorm.Engine) error { | |||
type OAuth2Application struct { | |||
type oauth2Application struct { | |||
ID int64 | |||
ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"` | |||
} | |||
return x.Sync(new(OAuth2Application)) | |||
return x.Sync(new(oauth2Application)) | |||
} |
@@ -13,12 +13,12 @@ import ( | |||
func Test_AddConfidentialClientColumnToOAuth2ApplicationTable(t *testing.T) { | |||
// premigration | |||
type OAuth2Application struct { | |||
type oauth2Application struct { | |||
ID int64 | |||
} | |||
// Prepare and load the testing database | |||
x, deferable := base.PrepareTestEnv(t, 0, new(OAuth2Application)) | |||
x, deferable := base.PrepareTestEnv(t, 0, new(oauth2Application)) | |||
defer deferable() | |||
if x == nil || t.Failed() { | |||
return | |||
@@ -36,7 +36,7 @@ func Test_AddConfidentialClientColumnToOAuth2ApplicationTable(t *testing.T) { | |||
} | |||
got := []ExpectedOAuth2Application{} | |||
if err := x.Table("o_auth2_application").Select("id, confidential_client").Find(&got); !assert.NoError(t, err) { | |||
if err := x.Table("oauth2_application").Select("id, confidential_client").Find(&got); !assert.NoError(t, err) { | |||
return | |||
} | |||
@@ -104,7 +104,7 @@ func ChangeContainerMetadataMultiArch(x *xorm.Engine) error { | |||
// Convert to new metadata format | |||
new := &MetadataNew{ | |||
newMetadata := &MetadataNew{ | |||
Type: old.Type, | |||
IsTagged: old.IsTagged, | |||
Platform: old.Platform, | |||
@@ -119,7 +119,7 @@ func ChangeContainerMetadataMultiArch(x *xorm.Engine) error { | |||
Manifests: manifests, | |||
} | |||
metadataJSON, err := json.Marshal(new) | |||
metadataJSON, err := json.Marshal(newMetadata) | |||
if err != nil { | |||
return err | |||
} |
@@ -53,7 +53,7 @@ func expandHashReferencesToSha256(x *xorm.Engine) error { | |||
if setting.Database.Type.IsMySQL() { | |||
_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1])) | |||
} else if setting.Database.Type.IsMSSQL() { | |||
_, err = db.Exec(fmt.Sprintf("ALTER TABLE [%s] ALTER COLUMN [%s] VARCHAR(64)", alts[0], alts[1])) | |||
_, err = db.Exec(fmt.Sprintf("ALTER TABLE [%s] ALTER COLUMN [%s] NVARCHAR(64)", alts[0], alts[1])) | |||
} else { | |||
_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1])) | |||
} |
@@ -0,0 +1,17 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package v1_23 //nolint | |||
import ( | |||
"code.gitea.io/gitea/models/perm" | |||
"xorm.io/xorm" | |||
) | |||
func AddRepoUnitEveryoneAccessMode(x *xorm.Engine) error { | |||
type RepoUnit struct { //revive:disable-line:exported | |||
EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"` | |||
} | |||
return x.Sync(&RepoUnit{}) | |||
} |
@@ -0,0 +1,10 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package v1_23 //nolint | |||
import "xorm.io/xorm" | |||
func DropWronglyCreatedTable(x *xorm.Engine) error { | |||
return x.DropTables("o_auth2_application") | |||
} |
@@ -61,7 +61,6 @@ func AddScratchHash(x *xorm.Engine) error { | |||
if _, err := sess.ID(tfa.ID).Cols("scratch_salt, scratch_hash").Update(tfa); err != nil { | |||
return fmt.Errorf("couldn't add in scratch_hash and scratch_salt: %w", err) | |||
} | |||
} | |||
} | |||
@@ -81,7 +81,6 @@ func HashAppToken(x *xorm.Engine) error { | |||
if _, err := sess.ID(token.ID).Cols("token_hash, token_salt, token_last_eight, sha1").Update(token); err != nil { | |||
return fmt.Errorf("couldn't add in sha1, token_hash, token_salt and token_last_eight: %w", err) | |||
} | |||
} | |||
} | |||
@@ -118,7 +118,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error | |||
// Remove watches from all users and now unaccessible repos | |||
for _, user := range t.Members { | |||
has, err := access_model.HasAccess(ctx, user.ID, repo) | |||
has, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo) | |||
if err != nil { | |||
return err | |||
} else if has { | |||
@@ -544,7 +544,7 @@ func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Reposito | |||
} | |||
func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error { | |||
if has, err := access_model.HasAccess(ctx, user.ID, repo); err != nil || has { | |||
if has, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo); err != nil || has { | |||
return err | |||
} | |||
if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil { |
@@ -130,11 +130,11 @@ func (t *Team) GetUnitsMap() map[string]string { | |||
m := make(map[string]string) | |||
if t.AccessMode >= perm.AccessModeAdmin { | |||
for _, u := range unit.Units { | |||
m[u.NameKey] = t.AccessMode.String() | |||
m[u.NameKey] = t.AccessMode.ToString() | |||
} | |||
} else { | |||
for _, u := range t.Units { | |||
m[u.Unit().NameKey] = u.AccessMode.String() | |||
m[u.Unit().NameKey] = u.AccessMode.ToString() | |||
} | |||
} | |||
return m | |||
@@ -174,23 +174,27 @@ func (t *Team) LoadMembers(ctx context.Context) (err error) { | |||
return err | |||
} | |||
// UnitEnabled returns if the team has the given unit type enabled | |||
// UnitEnabled returns true if the team has the given unit type enabled | |||
func (t *Team) UnitEnabled(ctx context.Context, tp unit.Type) bool { | |||
return t.UnitAccessMode(ctx, tp) > perm.AccessModeNone | |||
} | |||
// UnitAccessMode returns if the team has the given unit type enabled | |||
// UnitAccessMode returns the access mode for the given unit type, "none" for non-existent units | |||
func (t *Team) UnitAccessMode(ctx context.Context, tp unit.Type) perm.AccessMode { | |||
accessMode, _ := t.UnitAccessModeEx(ctx, tp) | |||
return accessMode | |||
} | |||
func (t *Team) UnitAccessModeEx(ctx context.Context, tp unit.Type) (accessMode perm.AccessMode, exist bool) { | |||
if err := t.LoadUnits(ctx); err != nil { | |||
log.Warn("Error loading team (ID: %d) units: %s", t.ID, err.Error()) | |||
} | |||
for _, unit := range t.Units { | |||
if unit.Type == tp { | |||
return unit.AccessMode | |||
for _, u := range t.Units { | |||
if u.Type == tp { | |||
return u.AccessMode, true | |||
} | |||
} | |||
return perm.AccessModeNone | |||
return perm.AccessModeNone, false | |||
} | |||
// IsUsableTeamName tests if a name could be as team name | |||
@@ -222,9 +226,8 @@ func GetTeamIDsByNames(ctx context.Context, orgID int64, names []string, ignoreN | |||
if err != nil { | |||
if ignoreNonExistent { | |||
continue | |||
} else { | |||
return nil, err | |||
} | |||
return nil, err | |||
} | |||
ids = append(ids, u.ID) | |||
} |
@@ -287,9 +287,10 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) { | |||
// SearchVersions gets all versions of packages matching the search options | |||
func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { | |||
sess := db.GetEngine(ctx). | |||
Where(opts.ToConds()). | |||
Select("package_version.*"). | |||
Table("package_version"). | |||
Join("INNER", "package", "package.id = package_version.package_id") | |||
Join("INNER", "package", "package.id = package_version.package_id"). | |||
Where(opts.ToConds()) | |||
opts.configureOrderBy(sess) | |||
@@ -304,19 +305,18 @@ func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*Package | |||
// SearchLatestVersions gets the latest version of every package matching the search options | |||
func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { | |||
cond := opts.ToConds(). | |||
And(builder.Expr("pv2.id IS NULL")) | |||
joinCond := builder.Expr("package_version.package_id = pv2.package_id AND (package_version.created_unix < pv2.created_unix OR (package_version.created_unix = pv2.created_unix AND package_version.id < pv2.id))") | |||
if opts.IsInternal.Has() { | |||
joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()}) | |||
} | |||
in := builder. | |||
Select("MAX(package_version.id)"). | |||
From("package_version"). | |||
InnerJoin("package", "package.id = package_version.package_id"). | |||
Where(opts.ToConds()). | |||
GroupBy("package_version.package_id") | |||
sess := db.GetEngine(ctx). | |||
Select("package_version.*"). | |||
Table("package_version"). | |||
Join("LEFT", "package_version pv2", joinCond). | |||
Join("INNER", "package", "package.id = package_version.package_id"). | |||
Where(cond) | |||
Where(builder.In("package_version.id", in)) | |||
opts.configureOrderBy(sess) | |||
@@ -63,13 +63,11 @@ func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re | |||
} | |||
func maxAccessMode(modes ...perm.AccessMode) perm.AccessMode { | |||
max := perm.AccessModeNone | |||
maxMode := perm.AccessModeNone | |||
for _, mode := range modes { | |||
if mode > max { | |||
max = mode | |||
} | |||
maxMode = max(maxMode, mode) | |||
} | |||
return max | |||
return maxMode | |||
} | |||
type userAccess struct { |
@@ -79,17 +79,17 @@ func TestHasAccess(t *testing.T) { | |||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) | |||
assert.True(t, repo2.IsPrivate) | |||
has, err := access_model.HasAccess(db.DefaultContext, user1.ID, repo1) | |||
has, err := access_model.HasAnyUnitAccess(db.DefaultContext, user1.ID, repo1) | |||
assert.NoError(t, err) | |||
assert.True(t, has) | |||
_, err = access_model.HasAccess(db.DefaultContext, user1.ID, repo2) | |||
_, err = access_model.HasAnyUnitAccess(db.DefaultContext, user1.ID, repo2) | |||
assert.NoError(t, err) | |||
_, err = access_model.HasAccess(db.DefaultContext, user2.ID, repo1) | |||
_, err = access_model.HasAnyUnitAccess(db.DefaultContext, user2.ID, repo1) | |||
assert.NoError(t, err) | |||
_, err = access_model.HasAccess(db.DefaultContext, user2.ID, repo2) | |||
_, err = access_model.HasAnyUnitAccess(db.DefaultContext, user2.ID, repo2) | |||
assert.NoError(t, err) | |||
} | |||
@@ -6,6 +6,7 @@ package access | |||
import ( | |||
"context" | |||
"fmt" | |||
"slices" | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/models/organization" | |||
@@ -14,13 +15,17 @@ import ( | |||
"code.gitea.io/gitea/models/unit" | |||
user_model "code.gitea.io/gitea/models/user" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/util" | |||
) | |||
// Permission contains all the permissions related variables to a repository for a user | |||
type Permission struct { | |||
AccessMode perm_model.AccessMode | |||
Units []*repo_model.RepoUnit | |||
UnitsMode map[unit.Type]perm_model.AccessMode | |||
units []*repo_model.RepoUnit | |||
unitsMode map[unit.Type]perm_model.AccessMode | |||
everyoneAccessMode map[unit.Type]perm_model.AccessMode | |||
} | |||
// IsOwner returns true if current user is the owner of repository. | |||
@@ -33,25 +38,59 @@ func (p *Permission) IsAdmin() bool { | |||
return p.AccessMode >= perm_model.AccessModeAdmin | |||
} | |||
// HasAccess returns true if the current user has at least read access to any unit of this repository | |||
func (p *Permission) HasAccess() bool { | |||
if p.UnitsMode == nil { | |||
return p.AccessMode >= perm_model.AccessModeRead | |||
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository. | |||
// It doesn't count the "everyone access mode". | |||
func (p *Permission) HasAnyUnitAccess() bool { | |||
for _, v := range p.unitsMode { | |||
if v >= perm_model.AccessModeRead { | |||
return true | |||
} | |||
} | |||
return len(p.UnitsMode) > 0 | |||
return p.AccessMode >= perm_model.AccessModeRead | |||
} | |||
// UnitAccessMode returns current user accessmode to the specify unit of the repository | |||
func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode { | |||
if p.UnitsMode == nil { | |||
for _, u := range p.Units { | |||
if u.Type == unitType { | |||
return p.AccessMode | |||
} | |||
func (p *Permission) HasAnyUnitAccessOrEveryoneAccess() bool { | |||
for _, v := range p.everyoneAccessMode { | |||
if v >= perm_model.AccessModeRead { | |||
return true | |||
} | |||
return perm_model.AccessModeNone | |||
} | |||
return p.UnitsMode[unitType] | |||
return p.HasAnyUnitAccess() | |||
} | |||
// HasUnits returns true if the permission contains attached units | |||
func (p *Permission) HasUnits() bool { | |||
return len(p.units) > 0 | |||
} | |||
// GetFirstUnitRepoID returns the repo ID of the first unit, it is a fragile design and should NOT be used anymore | |||
// deprecated | |||
func (p *Permission) GetFirstUnitRepoID() int64 { | |||
if len(p.units) > 0 { | |||
return p.units[0].RepoID | |||
} | |||
return 0 | |||
} | |||
// UnitAccessMode returns current user access mode to the specify unit of the repository | |||
// It also considers "everyone access mode" | |||
func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode { | |||
// if the units map contains the access mode, use it, but admin/owner mode could override it | |||
if m, ok := p.unitsMode[unitType]; ok { | |||
return util.Iif(p.AccessMode >= perm_model.AccessModeAdmin, p.AccessMode, m) | |||
} | |||
// if the units map does not contain the access mode, return the default access mode if the unit exists | |||
unitDefaultAccessMode := max(p.AccessMode, p.everyoneAccessMode[unitType]) | |||
hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType }) | |||
return util.Iif(hasUnit, unitDefaultAccessMode, perm_model.AccessModeNone) | |||
} | |||
func (p *Permission) SetUnitsWithDefaultAccessMode(units []*repo_model.RepoUnit, mode perm_model.AccessMode) { | |||
p.units = units | |||
p.unitsMode = make(map[unit.Type]perm_model.AccessMode) | |||
for _, u := range p.units { | |||
p.unitsMode[u.Type] = mode | |||
} | |||
} | |||
// CanAccess returns true if user has mode access to the unit of the repository | |||
@@ -102,23 +141,33 @@ func (p *Permission) CanWriteIssuesOrPulls(isPull bool) bool { | |||
return p.CanWrite(unit.TypeIssues) | |||
} | |||
func (p *Permission) ReadableUnitTypes() []unit.Type { | |||
types := make([]unit.Type, 0, len(p.units)) | |||
for _, u := range p.units { | |||
if p.CanRead(u.Type) { | |||
types = append(types, u.Type) | |||
} | |||
} | |||
return types | |||
} | |||
func (p *Permission) LogString() string { | |||
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [ " | |||
args := []any{p.AccessMode.String(), len(p.Units), len(p.UnitsMode)} | |||
args := []any{p.AccessMode.ToString(), len(p.units), len(p.unitsMode)} | |||
for i, unit := range p.Units { | |||
for i, u := range p.units { | |||
config := "" | |||
if unit.Config != nil { | |||
configBytes, err := unit.Config.ToDB() | |||
if u.Config != nil { | |||
configBytes, err := u.Config.ToDB() | |||
config = string(configBytes) | |||
if err != nil { | |||
config = err.Error() | |||
} | |||
} | |||
format += "\nUnits[%d]: ID: %d RepoID: %d Type: %s Config: %s" | |||
args = append(args, i, unit.ID, unit.RepoID, unit.Type.LogString(), config) | |||
args = append(args, i, u.ID, u.RepoID, u.Type.LogString(), config) | |||
} | |||
for key, value := range p.UnitsMode { | |||
for key, value := range p.unitsMode { | |||
format += "\nUnitMode[%-v]: %-v" | |||
args = append(args, key.LogString(), value.LogString()) | |||
} | |||
@@ -126,23 +175,35 @@ func (p *Permission) LogString() string { | |||
return fmt.Sprintf(format, args...) | |||
} | |||
// GetUserRepoPermission returns the user permissions to the repository | |||
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (Permission, error) { | |||
var perm Permission | |||
if log.IsTrace() { | |||
defer func() { | |||
if user == nil { | |||
log.Trace("Permission Loaded for anonymous user in %-v:\nPermissions: %-+v", | |||
repo, | |||
perm) | |||
return | |||
func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) { | |||
if user == nil || user.ID <= 0 { | |||
return | |||
} | |||
for _, u := range perm.units { | |||
if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] { | |||
if perm.everyoneAccessMode == nil { | |||
perm.everyoneAccessMode = make(map[unit.Type]perm_model.AccessMode) | |||
} | |||
log.Trace("Permission Loaded for %-v in %-v:\nPermissions: %-+v", | |||
user, | |||
repo, | |||
perm) | |||
}() | |||
perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode | |||
} | |||
} | |||
} | |||
// GetUserRepoPermission returns the user permissions to the repository | |||
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { | |||
defer func() { | |||
if err == nil { | |||
applyEveryoneRepoPermission(user, &perm) | |||
} | |||
if log.IsTrace() { | |||
log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm) | |||
} | |||
}() | |||
if err = repo.LoadUnits(ctx); err != nil { | |||
return perm, err | |||
} | |||
perm.units = repo.Units | |||
// anonymous user visit private repo. | |||
// TODO: anonymous user visit public unit of private repo??? | |||
@@ -152,7 +213,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
} | |||
var isCollaborator bool | |||
var err error | |||
if user != nil { | |||
isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID) | |||
if err != nil { | |||
@@ -160,7 +220,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
} | |||
} | |||
if err := repo.LoadOwner(ctx); err != nil { | |||
if err = repo.LoadOwner(ctx); err != nil { | |||
return perm, err | |||
} | |||
@@ -171,12 +231,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
return perm, nil | |||
} | |||
if err := repo.LoadUnits(ctx); err != nil { | |||
return perm, err | |||
} | |||
perm.Units = repo.Units | |||
// anonymous visit public repo | |||
if user == nil { | |||
perm.AccessMode = perm_model.AccessModeRead | |||
@@ -195,19 +249,16 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
return perm, err | |||
} | |||
if err := repo.LoadOwner(ctx); err != nil { | |||
return perm, err | |||
} | |||
if !repo.Owner.IsOrganization() { | |||
return perm, nil | |||
} | |||
perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) | |||
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode) | |||
// Collaborators on organization | |||
if isCollaborator { | |||
for _, u := range repo.Units { | |||
perm.UnitsMode[u.Type] = perm.AccessMode | |||
perm.unitsMode[u.Type] = perm.AccessMode | |||
} | |||
} | |||
@@ -221,7 +272,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
for _, team := range teams { | |||
if team.AccessMode >= perm_model.AccessModeAdmin { | |||
perm.AccessMode = perm_model.AccessModeOwner | |||
perm.UnitsMode = nil | |||
perm.unitsMode = nil | |||
return perm, nil | |||
} | |||
} | |||
@@ -229,30 +280,26 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use | |||
for _, u := range repo.Units { | |||
var found bool | |||
for _, team := range teams { | |||
teamMode := team.UnitAccessMode(ctx, u.Type) | |||
if teamMode > perm_model.AccessModeNone { | |||
m := perm.UnitsMode[u.Type] | |||
if m < teamMode { | |||
perm.UnitsMode[u.Type] = teamMode | |||
} | |||
if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist { | |||
perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode) | |||
found = true | |||
} | |||
} | |||
// for a public repo on an organization, a non-restricted user has read permission on non-team defined units. | |||
if !found && !repo.IsPrivate && !user.IsRestricted { | |||
if _, ok := perm.UnitsMode[u.Type]; !ok { | |||
perm.UnitsMode[u.Type] = perm_model.AccessModeRead | |||
if _, ok := perm.unitsMode[u.Type]; !ok { | |||
perm.unitsMode[u.Type] = perm_model.AccessModeRead | |||
} | |||
} | |||
} | |||
// remove no permission units | |||
perm.Units = make([]*repo_model.RepoUnit, 0, len(repo.Units)) | |||
for t := range perm.UnitsMode { | |||
perm.units = make([]*repo_model.RepoUnit, 0, len(repo.Units)) | |||
for t := range perm.unitsMode { | |||
for _, u := range repo.Units { | |||
if u.Type == t { | |||
perm.Units = append(perm.Units, u) | |||
perm.units = append(perm.units, u) | |||
} | |||
} | |||
} | |||
@@ -334,7 +381,7 @@ func HasAccessUnit(ctx context.Context, user *user_model.User, repo *repo_model. | |||
// Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface. | |||
func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) { | |||
if user.IsOrganization() { | |||
return false, fmt.Errorf("Organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID) | |||
return false, fmt.Errorf("organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID) | |||
} | |||
perm, err := GetUserRepoPermission(ctx, repo, user) | |||
if err != nil { | |||
@@ -344,8 +391,8 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model. | |||
perm.CanAccessAny(perm_model.AccessModeRead, unit.TypePullRequests), nil | |||
} | |||
// HasAccess returns true if user has access to repo | |||
func HasAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (bool, error) { | |||
// HasAnyUnitAccess see the comment of "perm.HasAnyUnitAccess" | |||
func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (bool, error) { | |||
var user *user_model.User | |||
var err error | |||
if userID > 0 { | |||
@@ -358,7 +405,7 @@ func HasAccess(ctx context.Context, userID int64, repo *repo_model.Repository) ( | |||
if err != nil { | |||
return false, err | |||
} | |||
return perm.HasAccess(), nil | |||
return perm.HasAnyUnitAccess(), nil | |||
} | |||
// getUsersWithAccessMode returns users that have at least given access mode to the repository. |
@@ -0,0 +1,136 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package access | |||
import ( | |||
"testing" | |||
perm_model "code.gitea.io/gitea/models/perm" | |||
repo_model "code.gitea.io/gitea/models/repo" | |||
"code.gitea.io/gitea/models/unit" | |||
user_model "code.gitea.io/gitea/models/user" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestHasAnyUnitAccess(t *testing.T) { | |||
perm := Permission{} | |||
assert.False(t, perm.HasAnyUnitAccess()) | |||
perm = Permission{ | |||
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}}, | |||
} | |||
assert.False(t, perm.HasAnyUnitAccess()) | |||
assert.False(t, perm.HasAnyUnitAccessOrEveryoneAccess()) | |||
perm = Permission{ | |||
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}}, | |||
everyoneAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead}, | |||
} | |||
assert.False(t, perm.HasAnyUnitAccess()) | |||
assert.True(t, perm.HasAnyUnitAccessOrEveryoneAccess()) | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeRead, | |||
units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}}, | |||
} | |||
assert.True(t, perm.HasAnyUnitAccess()) | |||
perm = Permission{ | |||
unitsMode: map[unit.Type]perm_model.AccessMode{unit.TypeWiki: perm_model.AccessModeRead}, | |||
} | |||
assert.True(t, perm.HasAnyUnitAccess()) | |||
} | |||
func TestApplyEveryoneRepoPermission(t *testing.T) { | |||
perm := Permission{ | |||
AccessMode: perm_model.AccessModeNone, | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, | |||
}, | |||
} | |||
applyEveryoneRepoPermission(nil, &perm) | |||
assert.False(t, perm.CanRead(unit.TypeWiki)) | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeNone, | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, | |||
}, | |||
} | |||
applyEveryoneRepoPermission(&user_model.User{ID: 0}, &perm) | |||
assert.False(t, perm.CanRead(unit.TypeWiki)) | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeNone, | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, | |||
}, | |||
} | |||
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) | |||
assert.True(t, perm.CanRead(unit.TypeWiki)) | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeWrite, | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, | |||
}, | |||
} | |||
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) | |||
// it should work the same as "EveryoneAccessMode: none" because the default AccessMode should be applied to units | |||
assert.True(t, perm.CanWrite(unit.TypeWiki)) | |||
perm = Permission{ | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead}, | |||
}, | |||
unitsMode: map[unit.Type]perm_model.AccessMode{ | |||
unit.TypeWiki: perm_model.AccessModeWrite, | |||
}, | |||
} | |||
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm) | |||
assert.True(t, perm.CanWrite(unit.TypeWiki)) | |||
} | |||
func TestUnitAccessMode(t *testing.T) { | |||
perm := Permission{ | |||
AccessMode: perm_model.AccessModeNone, | |||
} | |||
assert.Equal(t, perm_model.AccessModeNone, perm.UnitAccessMode(unit.TypeWiki), "no unit, no map, use AccessMode") | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeRead, | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki}, | |||
}, | |||
} | |||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "only unit, no map, use AccessMode") | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeAdmin, | |||
unitsMode: map[unit.Type]perm_model.AccessMode{ | |||
unit.TypeWiki: perm_model.AccessModeRead, | |||
}, | |||
} | |||
assert.Equal(t, perm_model.AccessModeAdmin, perm.UnitAccessMode(unit.TypeWiki), "no unit, only map, admin overrides map") | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeNone, | |||
unitsMode: map[unit.Type]perm_model.AccessMode{ | |||
unit.TypeWiki: perm_model.AccessModeRead, | |||
}, | |||
} | |||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "no unit, only map, use map") | |||
perm = Permission{ | |||
AccessMode: perm_model.AccessModeNone, | |||
units: []*repo_model.RepoUnit{ | |||
{Type: unit.TypeWiki}, | |||
}, | |||
unitsMode: map[unit.Type]perm_model.AccessMode{ | |||
unit.TypeWiki: perm_model.AccessModeRead, | |||
}, | |||
} | |||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map") | |||
} |
@@ -5,25 +5,25 @@ package perm | |||
import ( | |||
"fmt" | |||
"slices" | |||
"code.gitea.io/gitea/modules/util" | |||
) | |||
// AccessMode specifies the users access mode | |||
type AccessMode int | |||
const ( | |||
// AccessModeNone no access | |||
AccessModeNone AccessMode = iota // 0 | |||
// AccessModeRead read access | |||
AccessModeRead // 1 | |||
// AccessModeWrite write access | |||
AccessModeWrite // 2 | |||
// AccessModeAdmin admin access | |||
AccessModeAdmin // 3 | |||
// AccessModeOwner owner access | |||
AccessModeOwner // 4 | |||
AccessModeNone AccessMode = iota // 0: no access | |||
AccessModeRead // 1: read access | |||
AccessModeWrite // 2: write access | |||
AccessModeAdmin // 3: admin access | |||
AccessModeOwner // 4: owner access | |||
) | |||
func (mode AccessMode) String() string { | |||
// ToString returns the string representation of the access mode, do not make it a Stringer, otherwise it's difficult to render in templates | |||
func (mode AccessMode) ToString() string { | |||
switch mode { | |||
case AccessModeRead: | |||
return "read" | |||
@@ -39,19 +39,24 @@ func (mode AccessMode) String() string { | |||
} | |||
func (mode AccessMode) LogString() string { | |||
return fmt.Sprintf("<AccessMode:%d:%s>", mode, mode.String()) | |||
return fmt.Sprintf("<AccessMode:%d:%s>", mode, mode.ToString()) | |||
} | |||
// ParseAccessMode returns corresponding access mode to given permission string. | |||
func ParseAccessMode(permission string) AccessMode { | |||
func ParseAccessMode(permission string, allowed ...AccessMode) AccessMode { | |||
m := AccessModeNone | |||
switch permission { | |||
case "read": | |||
return AccessModeRead | |||
m = AccessModeRead | |||
case "write": | |||
return AccessModeWrite | |||
m = AccessModeWrite | |||
case "admin": | |||
return AccessModeAdmin | |||
m = AccessModeAdmin | |||
default: | |||
return AccessModeNone | |||
// the "owner" access is not really used for user input, it's mainly for checking access level in code, so don't parse it | |||
} | |||
if len(allowed) == 0 { | |||
return m | |||
} | |||
return util.Iif(slices.Contains(allowed, m), m, AccessModeNone) | |||
} |
@@ -0,0 +1,22 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package perm | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestAccessMode(t *testing.T) { | |||
names := []string{"none", "read", "write", "admin"} | |||
for i, name := range names { | |||
m := ParseAccessMode(name) | |||
assert.Equal(t, AccessMode(i), m) | |||
} | |||
assert.Equal(t, AccessMode(4), AccessModeOwner) | |||
assert.Equal(t, "owner", AccessModeOwner.ToString()) | |||
assert.Equal(t, AccessModeNone, ParseAccessMode("owner")) | |||
assert.Equal(t, AccessModeNone, ParseAccessMode("invalid")) | |||
} |
@@ -110,13 +110,11 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error { | |||
var items []string | |||
switch project.BoardType { | |||
case BoardTypeBugTriage: | |||
items = setting.Project.ProjectBoardBugTriageType | |||
case BoardTypeBasicKanban: | |||
items = setting.Project.ProjectBoardBasicKanbanType | |||
case BoardTypeNone: | |||
fallthrough | |||
default: |
@@ -10,6 +10,7 @@ import ( | |||
"strings" | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/models/perm" | |||
"code.gitea.io/gitea/models/unit" | |||
"code.gitea.io/gitea/modules/json" | |||
"code.gitea.io/gitea/modules/setting" | |||
@@ -41,11 +42,12 @@ func (err ErrUnitTypeNotExist) Unwrap() error { | |||
// RepoUnit describes all units of a repository | |||
type RepoUnit struct { //revive:disable-line:exported | |||
ID int64 | |||
RepoID int64 `xorm:"INDEX(s)"` | |||
Type unit.Type `xorm:"INDEX(s)"` | |||
Config convert.Conversion `xorm:"TEXT"` | |||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` | |||
ID int64 | |||
RepoID int64 `xorm:"INDEX(s)"` | |||
Type unit.Type `xorm:"INDEX(s)"` | |||
Config convert.Conversion `xorm:"TEXT"` | |||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` | |||
EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"` | |||
} | |||
func init() { |
@@ -170,7 +170,6 @@ func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64) | |||
// the owner of a private repo needs to be explicitly added. | |||
cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID}) | |||
} | |||
} else { | |||
// This is a "public" repository: | |||
// Any user that has read access, is a watcher or organization member can be requested to review |
@@ -5,6 +5,7 @@ package models | |||
import ( | |||
"context" | |||
"errors" | |||
"fmt" | |||
"code.gitea.io/gitea/models/db" | |||
@@ -147,7 +148,7 @@ func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error { | |||
func TestRepositoryReadyForTransfer(status repo_model.RepositoryStatus) error { | |||
switch status { | |||
case repo_model.RepositoryBeingMigrated: | |||
return fmt.Errorf("repo is not ready, currently migrating") | |||
return errors.New("repo is not ready, currently migrating") | |||
case repo_model.RepositoryPendingTransfer: | |||
return ErrRepoTransferInProgress{} | |||
} |
@@ -191,16 +191,13 @@ type Unit struct { | |||
NameKey string | |||
URI string | |||
DescKey string | |||
Idx int | |||
Priority int | |||
MaxAccessMode perm.AccessMode // The max access mode of the unit. i.e. Read means this unit can only be read. | |||
} | |||
// IsLessThan compares order of two units | |||
func (u Unit) IsLessThan(unit Unit) bool { | |||
if (u.Type == TypeExternalTracker || u.Type == TypeExternalWiki) && unit.Type != TypeExternalTracker && unit.Type != TypeExternalWiki { | |||
return false | |||
} | |||
return u.Idx < unit.Idx | |||
return u.Priority < unit.Priority | |||
} | |||
// MaxPerm returns the max perms of this unit | |||
@@ -236,7 +233,7 @@ var ( | |||
"repo.ext_issues", | |||
"/issues", | |||
"repo.ext_issues.desc", | |||
1, | |||
101, | |||
perm.AccessModeRead, | |||
} | |||
@@ -272,7 +269,7 @@ var ( | |||
"repo.ext_wiki", | |||
"/wiki", | |||
"repo.ext_wiki.desc", | |||
4, | |||
102, | |||
perm.AccessModeRead, | |||
} | |||
@@ -988,9 +988,8 @@ func GetUserIDsByNames(ctx context.Context, names []string, ignoreNonExistent bo | |||
if err != nil { | |||
if ignoreNonExistent { | |||
continue | |||
} else { | |||
return nil, err | |||
} | |||
return nil, err | |||
} | |||
ids = append(ids, u.ID) | |||
} |
@@ -63,16 +63,16 @@ func NewComplexity() { | |||
func setupComplexity(values []string) { | |||
if len(values) != 1 || values[0] != "off" { | |||
for _, val := range values { | |||
if complex, ok := charComplexities[val]; ok { | |||
validChars += complex.ValidChars | |||
requiredList = append(requiredList, complex) | |||
if complexity, ok := charComplexities[val]; ok { | |||
validChars += complexity.ValidChars | |||
requiredList = append(requiredList, complexity) | |||
} | |||
} | |||
if len(requiredList) == 0 { | |||
// No valid character classes found; use all classes as default | |||
for _, complex := range charComplexities { | |||
validChars += complex.ValidChars | |||
requiredList = append(requiredList, complex) | |||
for _, complexity := range charComplexities { | |||
validChars += complexity.ValidChars | |||
requiredList = append(requiredList, complexity) | |||
} | |||
} | |||
} |
@@ -307,10 +307,10 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu | |||
// Deal with the binary hash | |||
idx = 0 | |||
len := objectFormat.FullLength() / 2 | |||
for idx < len { | |||
length := objectFormat.FullLength() / 2 | |||
for idx < length { | |||
var read int | |||
read, err = rd.Read(shaBuf[idx:len]) | |||
read, err = rd.Read(shaBuf[idx:length]) | |||
n += read | |||
if err != nil { | |||
return mode, fname, sha, n, err |
@@ -468,7 +468,7 @@ func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) { | |||
_, _ = rd.Discard(1) | |||
} | |||
for { | |||
modifier, err := rd.ReadSlice('\x00') | |||
modifier, err := rd.ReadString('\x00') | |||
if err != nil { | |||
if err != io.EOF { | |||
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) |
@@ -49,9 +49,8 @@ readLoop: | |||
if len(line) > 0 && line[0] == ' ' { | |||
_, _ = signatureSB.Write(line[1:]) | |||
continue | |||
} else { | |||
pgpsig = false | |||
} | |||
pgpsig = false | |||
} | |||
if !message { |
@@ -232,7 +232,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err | |||
errChan <- err | |||
break | |||
} | |||
} | |||
}() | |||
@@ -251,18 +251,18 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) | |||
return nil, err | |||
} | |||
len := objectFormat.FullLength() | |||
length := objectFormat.FullLength() | |||
commits := []*Commit{} | |||
shaline := make([]byte, len+1) | |||
shaline := make([]byte, length+1) | |||
for { | |||
n, err := io.ReadFull(stdoutReader, shaline) | |||
if err != nil || n < len { | |||
if err != nil || n < length { | |||
if err == io.EOF { | |||
err = nil | |||
} | |||
return commits, err | |||
} | |||
objectID, err := NewIDFromString(string(shaline[0:len])) | |||
objectID, err := NewIDFromString(string(shaline[0:length])) | |||
if err != nil { | |||
return nil, err | |||
} |
@@ -64,7 +64,6 @@ func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string { | |||
// ex: git@try.gitea.io:go-gitea/gitea | |||
match := scpSyntax.FindAllStringSubmatch(refURI, -1) | |||
if len(match) > 0 { | |||
m := match[0] | |||
refHostname := m[2] | |||
pth := m[3] |
@@ -191,7 +191,6 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository, batch | |||
func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { | |||
batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) | |||
if len(changes.Updates) > 0 { | |||
// Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! | |||
if err := git.EnsureValidGitRepository(ctx, repo.RepoPath()); err != nil { | |||
log.Error("Unable to open git repo: %s for %-v: %v", repo.RepoPath(), repo, err) | |||
@@ -335,7 +334,6 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int | |||
if result, err = b.inner.Indexer.Search(facetRequest); err != nil { | |||
return 0, nil, nil, err | |||
} | |||
} | |||
languagesFacet := result.Facets["languages"] | |||
for _, term := range languagesFacet.Terms.Terms() { |
@@ -68,7 +68,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp | |||
searchOpt.Paginator = opts.Paginator | |||
switch opts.SortType { | |||
case "": | |||
case "", "latest": | |||
searchOpt.SortBy = SortByCreatedDesc | |||
case "oldest": | |||
searchOpt.SortBy = SortByCreatedAsc | |||
@@ -86,7 +86,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp | |||
searchOpt.SortBy = SortByDeadlineDesc | |||
case "priority", "priorityrepo", "project-column-sorting": | |||
// Unsupported sort type for search | |||
searchOpt.SortBy = SortByUpdatedDesc | |||
fallthrough | |||
default: | |||
searchOpt.SortBy = SortByUpdatedDesc | |||
} |
@@ -145,7 +145,6 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( | |||
query := elastic.NewBoolQuery() | |||
if options.Keyword != "" { | |||
searchType := esMultiMatchTypePhrasePrefix | |||
if options.IsFuzzyKeyword { | |||
searchType = esMultiMatchTypeBestFields |
@@ -125,7 +125,6 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms | |||
if mode.Colorize { | |||
buf = append(buf, resetBytes...) | |||
} | |||
} | |||
if flags&(Lshortfile|Llongfile) != 0 { | |||
if mode.Colorize { |
@@ -466,7 +466,6 @@ func TestColorPreview(t *testing.T) { | |||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) | |||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) | |||
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase) | |||
} | |||
negativeTests := []string{ | |||
@@ -549,7 +548,6 @@ func TestMathBlock(t *testing.T) { | |||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) | |||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) | |||
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase) | |||
} | |||
} | |||
@@ -48,10 +48,11 @@ const maxNuspecFileSize = 3 * 1024 * 1024 | |||
// Package represents a Nuget package | |||
type Package struct { | |||
PackageType PackageType | |||
ID string | |||
Version string | |||
Metadata *Metadata | |||
PackageType PackageType | |||
ID string | |||
Version string | |||
Metadata *Metadata | |||
NuspecContent *bytes.Buffer | |||
} | |||
// Metadata represents the metadata of a Nuget package | |||
@@ -138,8 +139,9 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) { | |||
// ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package | |||
func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) { | |||
var nuspecBuf bytes.Buffer | |||
var p nuspecPackage | |||
if err := xml.NewDecoder(r).Decode(&p); err != nil { | |||
if err := xml.NewDecoder(io.TeeReader(r, &nuspecBuf)).Decode(&p); err != nil { | |||
return nil, err | |||
} | |||
@@ -212,10 +214,11 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) { | |||
} | |||
} | |||
return &Package{ | |||
PackageType: packageType, | |||
ID: p.Metadata.ID, | |||
Version: toNormalizedVersion(v), | |||
Metadata: m, | |||
PackageType: packageType, | |||
ID: p.Metadata.ID, | |||
Version: toNormalizedVersion(v), | |||
Metadata: m, | |||
NuspecContent: &nuspecBuf, | |||
}, nil | |||
} | |||
@@ -147,35 +147,35 @@ func (e *MarshalEncoder) marshalIntInternal(i int64) error { | |||
return e.w.WriteByte(byte(i - 5)) | |||
} | |||
var len int | |||
var length int | |||
if 122 < i && i <= 0xff { | |||
len = 1 | |||
length = 1 | |||
} else if 0xff < i && i <= 0xffff { | |||
len = 2 | |||
length = 2 | |||
} else if 0xffff < i && i <= 0xffffff { | |||
len = 3 | |||
length = 3 | |||
} else if 0xffffff < i && i <= 0x3fffffff { | |||
len = 4 | |||
length = 4 | |||
} else if -0x100 <= i && i < -123 { | |||
len = -1 | |||
length = -1 | |||
} else if -0x10000 <= i && i < -0x100 { | |||
len = -2 | |||
length = -2 | |||
} else if -0x1000000 <= i && i < -0x100000 { | |||
len = -3 | |||
length = -3 | |||
} else if -0x40000000 <= i && i < -0x1000000 { | |||
len = -4 | |||
length = -4 | |||
} else { | |||
return ErrInvalidIntRange | |||
} | |||
if err := e.w.WriteByte(byte(len)); err != nil { | |||
if err := e.w.WriteByte(byte(length)); err != nil { | |||
return err | |||
} | |||
if len < 0 { | |||
len = -len | |||
if length < 0 { | |||
length = -length | |||
} | |||
for c := 0; c < len; c++ { | |||
for c := 0; c < length; c++ { | |||
if err := e.w.WriteByte(byte(i >> uint(8*c) & 0xff)); err != nil { | |||
return err | |||
} | |||
@@ -244,13 +244,13 @@ func (e *MarshalEncoder) marshalArray(arr reflect.Value) error { | |||
return err | |||
} | |||
len := arr.Len() | |||
length := arr.Len() | |||
if err := e.marshalIntInternal(int64(len)); err != nil { | |||
if err := e.marshalIntInternal(int64(length)); err != nil { | |||
return err | |||
} | |||
for i := 0; i < len; i++ { | |||
for i := 0; i < length; i++ { | |||
if err := e.marshal(arr.Index(i).Interface()); err != nil { | |||
return err | |||
} |
@@ -339,7 +339,6 @@ func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int | |||
} | |||
sort.Slice(processes, after(processes)) | |||
if !flat { | |||
var sortChildren func(process *Process) | |||
sortChildren = func(process *Process) { |
@@ -8,7 +8,7 @@ import ( | |||
"time" | |||
) | |||
const ( | |||
var ( | |||
backoffBegin = 50 * time.Millisecond | |||
backoffUpper = 2 * time.Second | |||
) | |||
@@ -18,6 +18,14 @@ type ( | |||
backoffFuncErr func() (retry bool, err error) | |||
) | |||
func mockBackoffDuration(d time.Duration) func() { | |||
oldBegin, oldUpper := backoffBegin, backoffUpper | |||
backoffBegin, backoffUpper = d, d | |||
return func() { | |||
backoffBegin, backoffUpper = oldBegin, oldUpper | |||
} | |||
} | |||
func backoffRetErr[T any](ctx context.Context, begin, upper time.Duration, end <-chan time.Time, fn backoffFuncRetErr[T]) (ret T, err error) { | |||
d := begin | |||
for { |
@@ -63,6 +63,8 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh | |||
// TODO: the logic could be improved in the future, to avoid a data-race between "doStartNewWorker" and "workerNum" | |||
// The root problem is that if we skip "doStartNewWorker" here, the "workerNum" might be decreased by other workers later | |||
// So ideally, it should check whether there are enough workers by some approaches, and start new workers if necessary. | |||
// This data-race is not serious, as long as a new worker will be started soon to make sure there are enough workers, | |||
// so no need to hugely refactor at the moment. | |||
q.workerNumMu.Lock() | |||
noWorker := q.workerNum == 0 | |||
if full || noWorker { | |||
@@ -136,6 +138,14 @@ func (q *WorkerPoolQueue[T]) basePushForShutdown(items ...T) bool { | |||
return true | |||
} | |||
func resetIdleTicker(t *time.Ticker, dur time.Duration) { | |||
t.Reset(dur) | |||
select { | |||
case <-t.C: | |||
default: | |||
} | |||
} | |||
// doStartNewWorker starts a new worker for the queue, the worker reads from worker's channel and handles the items. | |||
func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) { | |||
wp.wg.Add(1) | |||
@@ -146,8 +156,6 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) { | |||
log.Debug("Queue %q starts new worker", q.GetName()) | |||
defer log.Debug("Queue %q stops idle worker", q.GetName()) | |||
atomic.AddInt32(&q.workerStartedCounter, 1) // Only increase counter, used for debugging | |||
t := time.NewTicker(workerIdleDuration) | |||
defer t.Stop() | |||
@@ -169,11 +177,7 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) { | |||
} | |||
q.doWorkerHandle(batch) | |||
// reset the idle ticker, and drain the tick after reset in case a tick is already triggered | |||
t.Reset(workerIdleDuration) | |||
select { | |||
case <-t.C: | |||
default: | |||
} | |||
resetIdleTicker(t, workerIdleDuration) // key code for TestWorkerPoolQueueWorkerIdleReset | |||
case <-t.C: | |||
q.workerNumMu.Lock() | |||
keepWorking = q.workerNum <= 1 // keep the last worker running |
@@ -40,8 +40,6 @@ type WorkerPoolQueue[T any] struct { | |||
workerMaxNum int | |||
workerActiveNum int | |||
workerNumMu sync.Mutex | |||
workerStartedCounter int32 | |||
} | |||
type flushType chan struct{} |
@@ -5,8 +5,10 @@ package queue | |||
import ( | |||
"context" | |||
"slices" | |||
"strconv" | |||
"sync" | |||
"sync/atomic" | |||
"testing" | |||
"time" | |||
@@ -250,22 +252,34 @@ func TestWorkerPoolQueueShutdown(t *testing.T) { | |||
func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) { | |||
defer test.MockVariableValue(&workerIdleDuration, 10*time.Millisecond)() | |||
defer mockBackoffDuration(5 * time.Millisecond)() | |||
var q *WorkerPoolQueue[int] | |||
var handledCount atomic.Int32 | |||
var hasOnlyOneWorkerRunning atomic.Bool | |||
handler := func(items ...int) (unhandled []int) { | |||
time.Sleep(50 * time.Millisecond) | |||
handledCount.Add(int32(len(items))) | |||
// make each work have different duration, and check the active worker number periodically | |||
var activeNums []int | |||
for i := 0; i < 5-items[0]%2; i++ { | |||
time.Sleep(workerIdleDuration * 2) | |||
activeNums = append(activeNums, q.GetWorkerActiveNumber()) | |||
} | |||
// When the queue never becomes empty, the existing workers should keep working | |||
// It is not 100% true at the moment because the data-race in workergroup.go is not resolved, see that TODO */ | |||
// If the "active worker numbers" is like [2 2 ... 1 1], it means that an existing worker exited and the no new worker is started. | |||
if slices.Equal([]int{1, 1}, activeNums[len(activeNums)-2:]) { | |||
hasOnlyOneWorkerRunning.Store(true) | |||
} | |||
return nil | |||
} | |||
q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false) | |||
q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false) | |||
stop := runWorkerPoolQueue(q) | |||
for i := 0; i < 20; i++ { | |||
for i := 0; i < 100; i++ { | |||
assert.NoError(t, q.Push(i)) | |||
} | |||
time.Sleep(500 * time.Millisecond) | |||
assert.EqualValues(t, 2, q.GetWorkerNumber()) | |||
assert.EqualValues(t, 2, q.GetWorkerActiveNumber()) | |||
// when the queue never becomes empty, the existing workers should keep working | |||
assert.EqualValues(t, 2, q.workerStartedCounter) | |||
assert.Greater(t, int(handledCount.Load()), 4) // make sure there are enough items handled during the test | |||
assert.False(t, hasOnlyOneWorkerRunning.Load(), "a slow handler should not block other workers from starting") | |||
stop() | |||
} |
@@ -32,7 +32,6 @@ func CreateTemporaryPath(prefix string) (string, error) { | |||
if err != nil { | |||
log.Error("Unable to create temporary directory: %s-*.git (%v)", prefix, err) | |||
return "", fmt.Errorf("Failed to create dir %s-*.git: %w", prefix, err) | |||
} | |||
return basePath, nil | |||
} |
@@ -6,9 +6,6 @@ package session | |||
import ( | |||
"net/http" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/web/middleware" | |||
"gitea.com/go-chi/session" | |||
) | |||
@@ -21,10 +18,12 @@ type Store interface { | |||
// RegenerateSession regenerates the underlying session and returns the new store | |||
func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, error) { | |||
// Ensure that a cookie with a trailing slash does not take precedence over | |||
// the cookie written by the middleware. | |||
middleware.DeleteLegacySiteCookie(resp, setting.SessionConfig.CookieName) | |||
for _, f := range BeforeRegenerateSession { | |||
f(resp, req) | |||
} | |||
s, err := session.RegenerateSession(resp, req) | |||
return s, err | |||
} | |||
// BeforeRegenerateSession is a list of functions that are called before a session is regenerated. | |||
var BeforeRegenerateSession []func(http.ResponseWriter, *http.Request) |
@@ -318,7 +318,7 @@ func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) { | |||
// StartupProblems contains the messages for various startup problems, including: setting option, file/folder, etc | |||
var StartupProblems []string | |||
func logStartupProblem(skip int, level log.Level, format string, args ...any) { | |||
func LogStartupProblem(skip int, level log.Level, format string, args ...any) { | |||
msg := fmt.Sprintf(format, args...) | |||
log.Log(skip+1, level, "%s", msg) | |||
StartupProblems = append(StartupProblems, msg) | |||
@@ -326,14 +326,14 @@ func logStartupProblem(skip int, level log.Level, format string, args ...any) { | |||
func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) { | |||
if rootCfg.Section(oldSection).HasKey(oldKey) { | |||
logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version) | |||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version) | |||
} | |||
} | |||
// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini | |||
func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) { | |||
if rootCfg.Section(oldSection).HasKey(oldKey) { | |||
logStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey) | |||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey) | |||
} | |||
} | |||
@@ -174,7 +174,7 @@ func GetGeneralTokenSigningSecret() []byte { | |||
} | |||
if generalSigningSecret.CompareAndSwap(old, &jwtSecret) { | |||
// FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...) | |||
logStartupProblem(1, log.WARN, "OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes") | |||
LogStartupProblem(1, log.WARN, "OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes") | |||
return jwtSecret | |||
} | |||
return *generalSigningSecret.Load() |
@@ -235,7 +235,7 @@ var configuredPaths = make(map[string]string) | |||
func checkOverlappedPath(name, path string) { | |||
// TODO: some paths shouldn't overlap (storage.xxx.path), while some could (data path is the base path for storage path) | |||
if targetName, ok := configuredPaths[path]; ok && targetName != name { | |||
logStartupProblem(1, log.ERROR, "Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name) | |||
LogStartupProblem(1, log.ERROR, "Configured path %q is used by %q and %q at the same time. The paths must be unique to prevent data loss.", path, targetName, name) | |||
} | |||
configuredPaths[path] = name | |||
} |
@@ -19,9 +19,8 @@ func loadTimeFrom(rootCfg ConfigProvider) { | |||
DefaultUILocation, err = time.LoadLocation(zone) | |||
if err != nil { | |||
log.Fatal("Load time zone failed: %v", err) | |||
} else { | |||
log.Info("Default UI Location is %v", zone) | |||
} | |||
log.Info("Default UI Location is %v", zone) | |||
} | |||
if DefaultUILocation == nil { | |||
DefaultUILocation = time.Local |
@@ -82,7 +82,6 @@ var UI = struct { | |||
ReactionMaxUserNum: 10, | |||
MaxDisplayFileSize: 8388608, | |||
DefaultTheme: `gitea-auto`, | |||
Themes: []string{`gitea-auto`, `gitea-light`, `gitea-dark`}, | |||
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, | |||
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, | |||
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, |
@@ -22,6 +22,7 @@ import ( | |||
"code.gitea.io/gitea/modules/timeutil" | |||
"code.gitea.io/gitea/modules/util" | |||
"code.gitea.io/gitea/services/gitdiff" | |||
"code.gitea.io/gitea/services/webtheme" | |||
) | |||
// NewFuncMap returns functions for injecting to templates | |||
@@ -34,6 +35,7 @@ func NewFuncMap() template.FuncMap { | |||
// ----------------------------------------------------------------- | |||
// html/template related functions | |||
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. | |||
"Iif": Iif, | |||
"Eval": Eval, | |||
"SafeHTML": SafeHTML, | |||
"HTMLFormat": HTMLFormat, | |||
@@ -136,12 +138,7 @@ func NewFuncMap() template.FuncMap { | |||
"DisableImportLocal": func() bool { | |||
return !setting.ImportLocalPaths | |||
}, | |||
"ThemeName": func(user *user_model.User) string { | |||
if user == nil || user.Theme == "" { | |||
return setting.UI.DefaultTheme | |||
} | |||
return user.Theme | |||
}, | |||
"UserThemeName": UserThemeName, | |||
"NotificationSettings": func() map[string]any { | |||
return map[string]any{ | |||
"MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond), | |||
@@ -238,6 +235,17 @@ func DotEscape(raw string) string { | |||
return strings.ReplaceAll(raw, ".", "\u200d.\u200d") | |||
} | |||
// Iif is an "inline-if", similar util.Iif[T] but templates need the non-generic version, | |||
// and it could be simply used as "{{Iif expr trueVal}}" (omit the falseVal). | |||
func Iif(condition bool, vals ...any) any { | |||
if condition { | |||
return vals[0] | |||
} else if len(vals) > 1 { | |||
return vals[1] | |||
} | |||
return nil | |||
} | |||
// Eval the expression and return the result, see the comment of eval.Expr for details. | |||
// To use this helper function in templates, pass each token as a separate parameter. | |||
// | |||
@@ -249,3 +257,13 @@ func Eval(tokens ...any) (any, error) { | |||
n, err := eval.Expr(tokens...) | |||
return n.Value, err | |||
} | |||
func UserThemeName(user *user_model.User) string { | |||
if user == nil || user.Theme == "" { | |||
return setting.UI.DefaultTheme | |||
} | |||
if webtheme.IsThemeAvailable(user.Theme) { | |||
return user.Theme | |||
} | |||
return setting.UI.DefaultTheme | |||
} |
@@ -138,10 +138,9 @@ func wrapTmplErrMsg(msg string) { | |||
if setting.IsProd { | |||
// in prod mode, Gitea must have correct templates to run | |||
log.Fatal("Gitea can't run with template errors: %s", msg) | |||
} else { | |||
// in dev mode, do not need to really exit, because the template errors could be fixed by developer soon and the templates get reloaded | |||
log.Error("There are template errors but Gitea continues to run in dev mode: %s", msg) | |||
} | |||
// in dev mode, do not need to really exit, because the template errors could be fixed by developer soon and the templates get reloaded | |||
log.Error("There are template errors but Gitea continues to run in dev mode: %s", msg) | |||
} | |||
type templateErrorPrettier struct { |
@@ -84,9 +84,8 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) { | |||
if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil { | |||
if firstRun { | |||
log.Fatal("Failed to parse mail template, err: %v", err) | |||
} else { | |||
log.Error("Failed to parse mail template, err: %v", err) | |||
} | |||
log.Error("Failed to parse mail template, err: %v", err) | |||
} | |||
} | |||
} |
@@ -121,9 +121,9 @@ func Test_NormalizeEOL(t *testing.T) { | |||
} | |||
func Test_RandomInt(t *testing.T) { | |||
int, err := CryptoRandomInt(255) | |||
assert.True(t, int >= 0) | |||
assert.True(t, int <= 255) | |||
randInt, err := CryptoRandomInt(255) | |||
assert.True(t, randInt >= 0) | |||
assert.True(t, randInt <= 255) | |||
assert.NoError(t, err) | |||
} | |||
@@ -128,6 +128,16 @@ func hasResponseBeenWritten(argsIn []reflect.Value) bool { | |||
return false | |||
} | |||
func wrapHandlerProvider[T http.Handler](hp func(next http.Handler) T, funcInfo *routing.FuncInfo) func(next http.Handler) http.Handler { | |||
return func(next http.Handler) http.Handler { | |||
h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info | |||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { | |||
routing.UpdateFuncInfo(req.Context(), funcInfo) | |||
h.ServeHTTP(resp, req) | |||
}) | |||
} | |||
} | |||
// toHandlerProvider converts a handler to a handler provider | |||
// A handler provider is a function that takes a "next" http.Handler, it can be used as a middleware | |||
func toHandlerProvider(handler any) func(next http.Handler) http.Handler { | |||
@@ -138,13 +148,9 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler { | |||
} | |||
if hp, ok := handler.(func(next http.Handler) http.Handler); ok { | |||
return func(next http.Handler) http.Handler { | |||
h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info | |||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { | |||
routing.UpdateFuncInfo(req.Context(), funcInfo) | |||
h.ServeHTTP(resp, req) | |||
}) | |||
} | |||
return wrapHandlerProvider(hp, funcInfo) | |||
} else if hp, ok := handler.(func(http.Handler) http.HandlerFunc); ok { | |||
return wrapHandlerProvider(hp, funcInfo) | |||
} | |||
provider := func(next http.Handler) http.Handler { |
@@ -9,6 +9,7 @@ import ( | |||
"net/url" | |||
"strings" | |||
"code.gitea.io/gitea/modules/session" | |||
"code.gitea.io/gitea/modules/setting" | |||
) | |||
@@ -48,12 +49,12 @@ func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) { | |||
// Previous versions would use a cookie path with a trailing /. | |||
// These are more specific than cookies without a trailing /, so | |||
// we need to delete these if they exist. | |||
DeleteLegacySiteCookie(resp, name) | |||
deleteLegacySiteCookie(resp, name) | |||
} | |||
// DeleteLegacySiteCookie deletes the cookie with the given name at the cookie | |||
// deleteLegacySiteCookie deletes the cookie with the given name at the cookie | |||
// path with a trailing /, which would unintentionally override the cookie. | |||
func DeleteLegacySiteCookie(resp http.ResponseWriter, name string) { | |||
func deleteLegacySiteCookie(resp http.ResponseWriter, name string) { | |||
if setting.SessionConfig.CookiePath == "" || strings.HasSuffix(setting.SessionConfig.CookiePath, "/") { | |||
// If the cookie path ends with /, no legacy cookies will take | |||
// precedence, so do nothing. The exception is that cookies with no | |||
@@ -74,3 +75,11 @@ func DeleteLegacySiteCookie(resp http.ResponseWriter, name string) { | |||
} | |||
resp.Header().Add("Set-Cookie", cookie.String()) | |||
} | |||
func init() { | |||
session.BeforeRegenerateSession = append(session.BeforeRegenerateSession, func(resp http.ResponseWriter, _ *http.Request) { | |||
// Ensure that a cookie with a trailing slash does not take precedence over | |||
// the cookie written by the middleware. | |||
deleteLegacySiteCookie(resp, setting.SessionConfig.CookieName) | |||
}) | |||
} |
@@ -0,0 +1,10 @@ | |||
Copyright (C) 1985, 1990 Regents of the University of California. | |||
Permission to use, copy, modify, and distribute this | |||
software and its documentation for any purpose and without | |||
fee is hereby granted, provided that the above copyright | |||
notice appear in all copies. The University of California | |||
makes no representations about the suitability of this | |||
software for any purpose. It is provided "as is" without | |||
express or implied warranty. Export of this software outside | |||
of the United States of America may require an export license. |