Merge branch 'main' into lunny/add_comment_move_issue_column

This commit is contained in:
Lunny Xiao 2024-05-02 13:47:04 +08:00
commit 4afdf7c340
446 changed files with 9743 additions and 9092 deletions

View File

@ -4,6 +4,7 @@ reportUnusedDisableDirectives: true
ignorePatterns: ignorePatterns:
- /web_src/js/vendor - /web_src/js/vendor
- /web_src/fomantic - /web_src/fomantic
- /public/assets/js
parserOptions: parserOptions:
sourceType: module sourceType: module
@ -310,7 +311,7 @@ rules:
jquery/no-merge: [2] jquery/no-merge: [2]
jquery/no-param: [2] jquery/no-param: [2]
jquery/no-parent: [0] jquery/no-parent: [0]
jquery/no-parents: [0] jquery/no-parents: [2]
jquery/no-parse-html: [2] jquery/no-parse-html: [2]
jquery/no-prop: [2] jquery/no-prop: [2]
jquery/no-proxy: [2] jquery/no-proxy: [2]
@ -319,8 +320,8 @@ rules:
jquery/no-show: [2] jquery/no-show: [2]
jquery/no-size: [2] jquery/no-size: [2]
jquery/no-sizzle: [2] jquery/no-sizzle: [2]
jquery/no-slide: [0] jquery/no-slide: [2]
jquery/no-submit: [0] jquery/no-submit: [2]
jquery/no-text: [0] jquery/no-text: [0]
jquery/no-toggle: [2] jquery/no-toggle: [2]
jquery/no-trigger: [0] jquery/no-trigger: [0]
@ -458,7 +459,7 @@ rules:
no-jquery/no-other-utils: [2] no-jquery/no-other-utils: [2]
no-jquery/no-param: [2] no-jquery/no-param: [2]
no-jquery/no-parent: [0] 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-literal: [0]
no-jquery/no-parse-html: [2] no-jquery/no-parse-html: [2]
no-jquery/no-parse-json: [2] no-jquery/no-parse-json: [2]

View File

@ -1,25 +0,0 @@
name: disk-clean
on:
workflow_call:
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: false
docker-images: false
swap-storage: true

View File

@ -38,6 +38,8 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: pip install poetry - run: pip install poetry
- run: make deps-py - run: make deps-py
- run: make deps-frontend - run: make deps-frontend
@ -65,6 +67,8 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend - run: make deps-frontend
- run: make lint-swagger - run: make lint-swagger
@ -134,6 +138,8 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend - run: make deps-frontend
- run: make lint-frontend - run: make lint-frontend
- run: make checks-frontend - run: make checks-frontend
@ -181,6 +187,8 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend - run: make deps-frontend
- run: make lint-md - run: make lint-md
- run: make docs - run: make docs

View File

@ -24,6 +24,8 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend frontend deps-backend - run: make deps-frontend frontend deps-backend
- run: npx playwright install --with-deps - run: npx playwright install --with-deps
- run: make test-e2e-sqlite - run: make test-e2e-sqlite

View File

@ -9,8 +9,6 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
disk-clean:
uses: ./.github/workflows/disk-clean.yml
nightly-binary: nightly-binary:
runs-on: nscloud runs-on: nscloud
steps: steps:
@ -25,6 +23,8 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend deps-backend - run: make deps-frontend deps-backend
# xgo build # xgo build
- run: make release - run: make release

View File

@ -24,6 +24,8 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend deps-backend - run: make deps-frontend deps-backend
# xgo build # xgo build
- run: make release - run: make release

View File

@ -26,6 +26,8 @@ jobs:
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend deps-backend - run: make deps-frontend deps-backend
# xgo build # xgo build
- run: make release - run: make release

View File

@ -1,13 +1,14 @@
linters: linters:
enable-all: false
disable-all: true
fast: false
enable: enable:
- bidichk - bidichk
# - deadcode # deprecated - https://github.com/golangci/golangci-lint/issues/1841
- depguard - depguard
- dupl - dupl
- errcheck - errcheck
- forbidigo - forbidigo
- gocritic - gocritic
# - gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time.
- gofmt - gofmt
- gofumpt - gofumpt
- gosimple - gosimple
@ -17,20 +18,18 @@ linters:
- nolintlint - nolintlint
- revive - revive
- staticcheck - staticcheck
# - structcheck # deprecated - https://github.com/golangci/golangci-lint/issues/1841
- stylecheck - stylecheck
- typecheck - typecheck
- unconvert - unconvert
- unused - unused
# - varcheck # deprecated - https://github.com/golangci/golangci-lint/issues/1841
- wastedassign - wastedassign
enable-all: false
disable-all: true
fast: false
run: run:
timeout: 10m timeout: 10m
output:
sort-results: true
linters-settings: linters-settings:
stylecheck: stylecheck:
checks: ["all", "-ST1005", "-ST1003"] checks: ["all", "-ST1005", "-ST1003"]
@ -47,27 +46,37 @@ linters-settings:
errorCode: 1 errorCode: 1
warningCode: 1 warningCode: 1
rules: rules:
- name: atomic
- name: bare-return
- name: blank-imports - name: blank-imports
- name: constant-logical-expr
- name: context-as-argument - name: context-as-argument
- name: context-keys-type - name: context-keys-type
- name: dot-imports - name: dot-imports
- name: duplicated-imports
- name: empty-lines
- name: error-naming
- name: error-return - name: error-return
- name: error-strings - name: error-strings
- name: error-naming - name: errorf
- name: exported - name: exported
- name: identical-branches
- name: if-return - name: if-return
- name: increment-decrement - name: increment-decrement
- name: var-naming - name: indent-error-flow
- name: var-declaration - name: modifies-value-receiver
- name: package-comments - name: package-comments
- name: range - name: range
- name: receiver-naming - name: receiver-naming
- name: redefines-builtin-id
- name: string-of-int
- name: superfluous-else
- name: time-naming - name: time-naming
- name: unconditional-recursion
- name: unexported-return - name: unexported-return
- name: indent-error-flow - name: unreachable-code
- name: errorf - name: var-declaration
- name: duplicated-imports - name: var-naming
- name: modifies-value-receiver
gofumpt: gofumpt:
extra-rules: true extra-rules: true
depguard: depguard:
@ -93,8 +102,8 @@ issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
exclude-dirs: [node_modules, public, web_src] exclude-dirs: [node_modules, public, web_src]
exclude-case-sensitive: true
exclude-rules: exclude-rules:
# Exclude some linters from running on tests files.
- path: _test\.go - path: _test\.go
linters: linters:
- gocyclo - gocyclo
@ -112,19 +121,19 @@ issues:
- path: cmd - path: cmd
linters: linters:
- forbidigo - forbidigo
- linters: - text: "webhook"
linters:
- dupl - dupl
text: "webhook" - text: "`ID' should not be capitalized"
- linters: linters:
- gocritic - gocritic
text: "`ID' should not be capitalized" - text: "swagger"
- linters: linters:
- unused - unused
- deadcode - deadcode
text: "swagger" - text: "argument x is overwritten before first use"
- linters: linters:
- staticcheck - staticcheck
text: "argument x is overwritten before first use"
- text: "commentFormatting: put a space between `//` and comment text" - text: "commentFormatting: put a space between `//` and comment text"
linters: linters:
- gocritic - gocritic

5223
CHANGELOG-archived.md Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,7 @@ EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-che
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
@ -143,9 +143,9 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN
GO_DIRS := build cmd models modules routers services tests GO_DIRS := build cmd models modules routers services tests
WEB_DIRS := web_src/js web_src/css 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 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 EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
GO_SOURCES := $(wildcard *.go) GO_SOURCES := $(wildcard *.go)
@ -397,11 +397,11 @@ lint-md: node_modules
.PHONY: lint-spell .PHONY: lint-spell
lint-spell: lint-spell:
@go run $(MISSPELL_PACKAGE) -error $(SPELLCHECK_FILES) @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -error $(SPELLCHECK_FILES)
.PHONY: lint-spell-fix .PHONY: lint-spell-fix
lint-spell-fix: lint-spell-fix:
@go run $(MISSPELL_PACKAGE) -w $(SPELLCHECK_FILES) @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -w $(SPELLCHECK_FILES)
.PHONY: lint-go .PHONY: lint-go
lint-go: lint-go:
@ -778,7 +778,7 @@ generate-backend: $(TAGS_PREREQ) generate-go
.PHONY: generate-go .PHONY: generate-go
generate-go: $(TAGS_PREREQ) generate-go: $(TAGS_PREREQ)
@echo "Running go generate..." @echo "Running go generate..."
@CC= GOOS= GOARCH= $(GO) generate -tags '$(TAGS)' ./... @CC= GOOS= GOARCH= CGO_ENABLED=0 $(GO) generate -tags '$(TAGS)' ./...
.PHONY: security-check .PHONY: security-check
security-check: security-check:
@ -908,8 +908,9 @@ webpack: $(WEBPACK_DEST)
$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json
@$(MAKE) -s node-check node_modules @$(MAKE) -s node-check node_modules
rm -rf $(WEBPACK_DEST_ENTRIES) @rm -rf $(WEBPACK_DEST_ENTRIES)
npx webpack @echo "Running webpack..."
@BROWSERSLIST_IGNORE_OLD_DATA=true npx webpack
@touch $(WEBPACK_DEST) @touch $(WEBPACK_DEST)
.PHONY: svg .PHONY: svg

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"text/tabwriter" "text/tabwriter"
@ -91,7 +92,7 @@ func runListAuth(c *cli.Context) error {
func runDeleteAuth(c *cli.Context) error { func runDeleteAuth(c *cli.Context) error {
if !c.IsSet("id") { if !c.IsSet("id") {
return fmt.Errorf("--id flag is missing") return errors.New("--id flag is missing")
} }
ctx, cancel := installSignals() ctx, cancel := installSignals()

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
"net/url" "net/url"
@ -193,7 +194,7 @@ func runAddOauth(c *cli.Context) error {
func runUpdateOauth(c *cli.Context) error { func runUpdateOauth(c *cli.Context) error {
if !c.IsSet("id") { if !c.IsSet("id") {
return fmt.Errorf("--id flag is missing") return errors.New("--id flag is missing")
} }
ctx, cancel := installSignals() ctx, cancel := installSignals()

View File

@ -5,7 +5,6 @@ package cmd
import ( import (
"errors" "errors"
"fmt"
"strings" "strings"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
@ -166,7 +165,7 @@ func runAddSMTP(c *cli.Context) error {
func runUpdateSMTP(c *cli.Context) error { func runUpdateSMTP(c *cli.Context) error {
if !c.IsSet("id") { if !c.IsSet("id") {
return fmt.Errorf("--id flag is missing") return errors.New("--id flag is missing")
} }
ctx, cancel := installSignals() ctx, cancel := installSignals()

View File

@ -35,7 +35,7 @@ var microcmdUserChangePassword = &cli.Command{
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "must-change-password", Name: "must-change-password",
Usage: "User must change password", Usage: "User must change password (can be disabled by --must-change-password=false)",
Value: true, Value: true,
}, },
}, },

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
@ -48,7 +49,7 @@ var microcmdUserCreate = &cli.Command{
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "must-change-password", Name: "must-change-password",
Usage: "Set to false to prevent forcing the user to change their password after initial login", Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
DisableDefaultText: true, DisableDefaultText: true,
}, },
&cli.IntFlag{ &cli.IntFlag{
@ -91,11 +92,16 @@ func runCreateUser(c *cli.Context) error {
_, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n") _, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
} }
ctx, cancel := installSignals() ctx := c.Context
defer cancel() if !setting.IsInTesting {
// FIXME: need to refactor the "installSignals/initDB" related code later
if err := initDB(ctx); err != nil { // it doesn't make sense to call it in (almost) every command action function
return err var cancel context.CancelFunc
ctx, cancel = installSignals()
defer cancel()
if err := initDB(ctx); err != nil {
return err
}
} }
var password string var password string
@ -123,8 +129,8 @@ func runCreateUser(c *cli.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("IsTableNotEmpty: %w", err) return fmt.Errorf("IsTableNotEmpty: %w", err)
} }
if !hasUserRecord && isAdmin { if !hasUserRecord {
// if this is the first admin being created, don't force to change password (keep the old behavior) // if this is the first one being created, don't force to change password (keep the old behavior)
mustChangePassword = false mustChangePassword = false
} }
} }

View File

@ -0,0 +1,44 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"fmt"
"strings"
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestAdminUserCreate(t *testing.T) {
app := NewMainApp(AppVersion{})
reset := func() {
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
}
type createCheck struct{ IsAdmin, MustChangePassword bool }
createUser := func(name, args string) createCheck {
assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
return createCheck{u.IsAdmin, u.MustChangePassword}
}
reset()
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
reset()
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
reset()
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
}

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
@ -42,7 +43,7 @@ var microcmdUserDelete = &cli.Command{
func runDeleteUser(c *cli.Context) error { func runDeleteUser(c *cli.Context) error {
if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { 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() ctx, cancel := installSignals()

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
@ -42,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{
func runGenerateAccessToken(c *cli.Context) error { func runGenerateAccessToken(c *cli.Context) error {
if !c.IsSet("username") { 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() ctx, cancel := installSignals()
@ -68,7 +69,7 @@ func runGenerateAccessToken(c *cli.Context) error {
return err return err
} }
if exist { 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 // make sure the scopes are valid

View File

@ -87,6 +87,10 @@ var CmdDump = &cli.Command{
Name: "skip-index", Name: "skip-index",
Usage: "Skip bleve index data", Usage: "Skip bleve index data",
}, },
&cli.BoolFlag{
Name: "skip-db",
Usage: "Skip database",
},
&cli.StringFlag{ &cli.StringFlag{
Name: "type", Name: "type",
Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")), 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 ctx.Bool("skip-db") {
if _, err := os.Stat(tmpDir); os.IsNotExist(err) { // Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere.
fatal("Path does not exist: %s", tmpDir) dumper.GlobalExcludeAbsPath(setting.Database.Path)
} log.Info("Skipping database")
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 { } else {
log.Info("Dumping database...") tmpDir := ctx.String("tempdir")
} if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
fatal("Path does not exist: %s", tmpDir)
}
if err := db.DumpDatabase(dbDump.Name(), targetDBType); err != nil { dbDump, err := os.CreateTemp(tmpDir, "gitea-db.sql")
fatal("Failed to dump database: %v", err) 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)
}
}()
if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil { targetDBType := ctx.String("database")
fatal("Failed to include gitea-db.sql: %v", err) 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 = 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) log.Info("Adding custom configuration file from %s", setting.CustomConf)

View File

@ -157,9 +157,9 @@ func runViewDo(c *cli.Context) error {
} }
if len(matchedAssetFiles) == 0 { 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 { } 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) data, err := matchedAssetFiles[0].fs.ReadFile(matchedAssetFiles[0].name)
@ -180,7 +180,7 @@ func runExtractDo(c *cli.Context) error {
} }
if c.NArg() == 0 { 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 := "." destdir := "."

View File

@ -465,7 +465,7 @@ func hookPrintResult(output, isCreate bool, branch, url string) {
fmt.Fprintf(os.Stderr, " %s\n", url) fmt.Fprintf(os.Stderr, " %s\n", url)
} }
fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "")
os.Stderr.Sync() _ = os.Stderr.Sync()
} }
func pushOptions() map[string]string { func pushOptions() map[string]string {

View File

@ -112,13 +112,18 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context)
} }
} }
func NewMainApp(version, versionExtra string) *cli.App { type AppVersion struct {
Version string
Extra string
}
func NewMainApp(appVer AppVersion) *cli.App {
app := cli.NewApp() app := cli.NewApp()
app.Name = "Gitea" app.Name = "Gitea"
app.HelpName = "gitea" app.HelpName = "gitea"
app.Usage = "A painless self-hosted Git service" app.Usage = "A painless self-hosted Git service"
app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.` app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
app.Version = version + versionExtra app.Version = appVer.Version + appVer.Extra
app.EnableBashCompletion = true app.EnableBashCompletion = true
// these sub-commands need to use config file // these sub-commands need to use config file

View File

@ -28,7 +28,7 @@ func makePathOutput(workPath, customPath, customConf string) string {
} }
func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App { func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App {
app := NewMainApp("version", "version-extra") app := NewMainApp(AppVersion{})
testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction} testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction}
prepareSubcommandWithConfig(testCmd, appGlobalFlags()) prepareSubcommandWithConfig(testCmd, appGlobalFlags())
app.Commands = append(app.Commands, testCmd) app.Commands = append(app.Commands, testCmd)

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
@ -249,7 +250,7 @@ func runAddFileLogger(c *cli.Context) error {
if c.IsSet("filename") { if c.IsSet("filename") {
vals["filename"] = c.String("filename") vals["filename"] = c.String("filename")
} else { } 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") { if c.IsSet("rotate") {
vals["rotate"] = c.Bool("rotate") vals["rotate"] = c.Bool("rotate")

View File

@ -17,7 +17,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/google/go-github/v57/github" "github.com/google/go-github/v61/github"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )

View File

@ -1231,7 +1231,8 @@ LEVEL = Info
;DEFAULT_THEME = gitea-auto ;DEFAULT_THEME = gitea-auto
;; ;;
;; All available themes. Allow users select personalized themes regardless of the value of `DEFAULT_THEME`. ;; 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. ;; All available reactions users can choose on issues/prs and comments.
;; Values can be emoji alias (:smile:) or a unicode emoji. ;; Values can be emoji alias (:smile:) or a unicode emoji.
@ -1455,7 +1456,7 @@ LEVEL = Info
;; Batch size to send for batched queues ;; Batch size to send for batched queues
;BATCH_LENGTH = 20 ;BATCH_LENGTH = 20
;; ;;
;; Connection string for redis queues this will store the redis or redis-cluster connection string. ;; Connection string for redis queues this will store the redis (or Redis cluster) connection string.
;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb ;; When `TYPE` is `persistable-channel`, this provides a directory for the underlying leveldb
;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`. ;; or additional options of the form `leveldb://path/to/db?option=value&....`, and will override `DATADIR`.
;CONN_STR = "redis://127.0.0.1:6379/0" ;CONN_STR = "redis://127.0.0.1:6379/0"
@ -1557,8 +1558,8 @@ LEVEL = Info
;; email = use the username part of the email attribute ;; email = use the username part of the email attribute
;; Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria: ;; Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria:
;; - diacritics are removed ;; - diacritics are removed
;; - the characters in the set `['´\x60]` are removed ;; - the characters in the set ['´`] are removed
;; - the characters in the set `[\s~+]` are replaced with `-` ;; - the characters in the set [\s~+] are replaced with "-"
;USERNAME = nickname ;USERNAME = nickname
;; ;;
;; Update avatar if available from oauth2 provider. ;; Update avatar if available from oauth2 provider.
@ -1739,9 +1740,8 @@ LEVEL = Info
;; For "memory" only, GC interval in seconds, default is 60 ;; For "memory" only, GC interval in seconds, default is 60
;INTERVAL = 60 ;INTERVAL = 60
;; ;;
;; For "redis", "redis-cluster" and "memcache", connection host address ;; For "redis" and "memcache", connection host address
;; redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` ;; redis: `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 a Redis cluster)
;; redis-cluster: `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s`
;; memcache: `127.0.0.1:11211` ;; memcache: `127.0.0.1:11211`
;; twoqueue: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` ;; twoqueue: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000`
;HOST = ;HOST =
@ -1771,15 +1771,14 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ;;
;; Either "memory", "file", "redis", "redis-cluster", "db", "mysql", "couchbase", "memcache" or "postgres" ;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
;; Default is "memory". "db" will reuse the configuration in [database] ;; Default is "memory". "db" will reuse the configuration in [database]
;PROVIDER = memory ;PROVIDER = memory
;; ;;
;; Provider config options ;; Provider config options
;; memory: doesn't have any config yet ;; memory: doesn't have any config yet
;; file: session file path, e.g. `data/sessions` ;; file: session file path, e.g. `data/sessions`
;; redis: `redis://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` ;; redis: `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 a Redis cluster)
;; redis-cluster: `redis+cluster://127.0.0.1:6379/0?pool_size=100&idle_timeout=180s`
;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table` ;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
;PROVIDER_CONFIG = data/sessions ; Relative paths will be made absolute against _`AppWorkPath`_. ;PROVIDER_CONFIG = data/sessions ; Relative paths will be made absolute against _`AppWorkPath`_.
;; ;;
@ -2377,22 +2376,6 @@ LEVEL = Info
;; Enable issue by repository metrics; default is false ;; Enable issue by repository metrics; default is false
;ENABLED_ISSUE_BY_REPOSITORY = 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] ;[migrations]

View File

@ -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. - `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. - `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. - `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. - `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. - `THEMES`: **_empty_**: All available themes by `{CustomPath}/public/assets/css/theme-*.css`. Allow users select personalized themes.
regardless of the value of `DEFAULT_THEME`.
- `MAX_DISPLAY_FILE_SIZE`: **8388608**: Max size of files to be displayed (default is 8MiB) - `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 - `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 - `REACTIONS`: All available reactions users can choose on issues/prs and comments
@ -493,7 +492,7 @@ Configuration at `[queue]` will set defaults for queues with overrides for indiv
- `DATADIR`: **queues/common**: Base DataDir for storing level queues. `DATADIR` for individual queues can be set in `queue.name` sections. Relative paths will be made absolute against `%(APP_DATA_PATH)s`. - `DATADIR`: **queues/common**: Base DataDir for storing level queues. `DATADIR` for individual queues can be set in `queue.name` sections. Relative paths will be made absolute against `%(APP_DATA_PATH)s`.
- `LENGTH`: **100000**: Maximal queue size before channel queues block - `LENGTH`: **100000**: Maximal queue size before channel queues block
- `BATCH_LENGTH`: **20**: Batch data before passing to the handler - `BATCH_LENGTH`: **20**: Batch data before passing to the handler
- `CONN_STR`: **redis://127.0.0.1:6379/0**: Connection string for the redis queue type. For `redis-cluster` use `redis+cluster://127.0.0.1:6379/0`. Options can be set using query params. Similarly, LevelDB options can also be set using: **leveldb://relative/path?option=value** or **leveldb:///absolute/path?option=value**, and will override `DATADIR` - `CONN_STR`: **redis://127.0.0.1:6379/0**: Connection string for the redis queue type. If you're running a Redis cluster, use `redis+cluster://127.0.0.1:6379/0`. Options can be set using query params. Similarly, LevelDB options can also be set using: **leveldb://relative/path?option=value** or **leveldb:///absolute/path?option=value**, and will override `DATADIR`
- `QUEUE_NAME`: **_queue**: The suffix for default redis and disk queue name. Individual queues will default to **`name`**`QUEUE_NAME` but can be overridden in the specific `queue.name` section. - `QUEUE_NAME`: **_queue**: The suffix for default redis and disk queue name. Individual queues will default to **`name`**`QUEUE_NAME` but can be overridden in the specific `queue.name` section.
- `SET_NAME`: **_unique**: The suffix that will be added to the default redis and disk queue `set` name for unique queues. Individual queues will default to **`name`**`QUEUE_NAME`_`SET_NAME`_ but can be overridden in the specific `queue.name` section. - `SET_NAME`: **_unique**: The suffix that will be added to the default redis and disk queue `set` name for unique queues. Individual queues will default to **`name`**`QUEUE_NAME`_`SET_NAME`_ but can be overridden in the specific `queue.name` section.
- `MAX_WORKERS`: **(dynamic)**: Maximum number of worker go-routines for the queue. Default value is "CpuNum/2" clipped to between 1 and 10. - `MAX_WORKERS`: **(dynamic)**: Maximum number of worker go-routines for the queue. Default value is "CpuNum/2" clipped to between 1 and 10.
@ -613,7 +612,7 @@ And the following unique queues:
- `email` - use the username part of the email attribute - `email` - use the username part of the email attribute
- Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria: - Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria:
- diacritics are removed - diacritics are removed
- the characters in the set `['´\x60]` are removed - the characters in the set ```['´`]``` are removed
- the characters in the set `[\s~+]` are replaced with `-` - the characters in the set `[\s~+]` are replaced with `-`
- `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login. - `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login.
- `ACCOUNT_LINKING`: **login**: How to handle if an account / email already exists: - `ACCOUNT_LINKING`: **login**: How to handle if an account / email already exists:
@ -778,11 +777,11 @@ and
## Cache (`cache`) ## Cache (`cache`)
- `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, `redis-cluster`, `twoqueue` or `memcache`. (`twoqueue` represents a size limited LRU cache.) - `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, `twoqueue` or `memcache`. (`twoqueue` represents a size limited LRU cache.)
- `INTERVAL`: **60**: Garbage Collection interval (sec), for memory and twoqueue cache only. - `INTERVAL`: **60**: Garbage Collection interval (sec), for memory and twoqueue cache only.
- `HOST`: **_empty_**: Connection string for `redis`, `redis-cluster` and `memcache`. For `twoqueue` sets configuration for the queue. - `HOST`: **_empty_**: Connection string for `redis` and `memcache`. For `twoqueue` sets configuration for the queue.
- Redis: `redis://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` - Redis: `redis://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s`
- Redis-cluster `redis+cluster://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s` - For a Redis cluster: `redis+cluster://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s`
- Memcache: `127.0.0.1:9090;127.0.0.1:9091` - Memcache: `127.0.0.1:9090;127.0.0.1:9091`
- TwoQueue LRU cache: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` representing the maximum number of objects stored in the cache. - TwoQueue LRU cache: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` representing the maximum number of objects stored in the cache.
- `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to -1 disables caching. - `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to -1 disables caching.
@ -794,7 +793,7 @@ and
## Session (`session`) ## Session (`session`)
- `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]` - `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]`
- `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_. - `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_.
- `COOKIE_SECURE`:**_empty_**: `true` or `false`. Enable this to force using HTTPS for all session access. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL. - `COOKIE_SECURE`:**_empty_**: `true` or `false`. Enable this to force using HTTPS for all session access. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL.
- `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID. - `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID.
@ -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 - `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`) ## Migrations (`migrations`)
- `MAX_ATTEMPTS`: **3**: Max attempts per http/https request on migrations. - `MAX_ATTEMPTS`: **3**: Max attempts per http/https request on migrations.
@ -1331,7 +1322,7 @@ Defaultly every storage has their default base path like below
| actions_log | actions_log/ | | actions_log | actions_log/ |
| actions_artifacts | actions_artifacts/ | | actions_artifacts | actions_artifacts/ |
And bucket, basepath or `SERVE_DIRECT` could be special or overrided, if you want to use a different you can: And bucket, basepath or `SERVE_DIRECT` could be special or overridden, if you want to use a different you can:
```ini ```ini
[storage.actions_log] [storage.actions_log]

View File

@ -212,10 +212,9 @@ menu:
- `SITEMAP_PAGING_NUM`: **20**: 在单个子SiteMap中显示的项数。 - `SITEMAP_PAGING_NUM`: **20**: 在单个子SiteMap中显示的项数。
- `GRAPH_MAX_COMMIT_NUM`: **100**: 提交图中显示的最大commit数量。 - `GRAPH_MAX_COMMIT_NUM`: **100**: 提交图中显示的最大commit数量。
- `CODE_COMMENT_LINES`: **4**: 在代码评论中能够显示的最大代码行数。 - `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`页面中。 - `SHOW_USER_EMAIL`: **true**: 用户的电子邮件是否应该显示在`Explore Users`页面中。
- `THEMES`: **gitea-auto,gitea-light,gitea-dark**: 所有可用的主题。允许用户选择个性化的主题, - `THEMES`: **_empty_**: 所有可用的主题(由 `{CustomPath}/public/assets/css/theme-*.css` 提供)。允许用户选择个性化的主题,
而不受DEFAULT_THEME 值的影响。
- `MAX_DISPLAY_FILE_SIZE`: **8388608**: 能够显示文件的最大大小默认为8MiB - `MAX_DISPLAY_FILE_SIZE`: **8388608**: 能够显示文件的最大大小默认为8MiB
- `REACTIONS`: 用户可以在问题Issue、Pull RequestPR以及评论中选择的所有可选的反应。 - `REACTIONS`: 用户可以在问题Issue、Pull RequestPR以及评论中选择的所有可选的反应。
这些值可以是表情符号别名(例如::smile:或Unicode表情符号。 这些值可以是表情符号别名(例如::smile:或Unicode表情符号。
@ -1128,15 +1127,6 @@ ALLOW_DATA_URI_IMAGES = true
- `DEFAULT_UI_LOCATION`:在 UI 上的默认时间位置,以便我们可以在 UI 上显示正确的用户时间。例如Asia/Shanghai - `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`) ## 迁移 (`migrations`)
- `MAX_ATTEMPTS`**3**:每次 http/https 请求的最大尝试次数(用于迁移)。 - `MAX_ATTEMPTS`**3**:每次 http/https 请求的最大尝试次数(用于迁移)。

View File

@ -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`. 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". 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). Community themes are listed in [gitea/awesome-gitea#themes](https://gitea.com/gitea/awesome-gitea#themes).

View File

@ -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 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 vs built-in SSH
SSHD is the built-in SSH server on most Unix systems. SSHD is the built-in SSH server on most Unix systems.

View File

@ -182,17 +182,6 @@ Gitea不提供内置的Pages服务器。您需要一个专用的域名来提供
使用 [Fail2Ban](administration/fail2ban-setup.md) 监视并阻止基于日志模式的自动登录尝试或其他恶意行为。 使用 [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 vs 内建SSH
SSHD是大多数Unix系统上内建的SSH服务器。 SSHD是大多数Unix系统上内建的SSH服务器。

View File

@ -58,7 +58,7 @@ The repository now gets mirrored periodically to the remote repository. You can
To set up a mirror from Gitea to GitHub, you need to follow these steps: To set up a mirror from Gitea to GitHub, you need to follow these steps:
1. Create a [GitHub personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with the *public_repo* box checked. Also check the **workflow** checkbox in case your repo using act for continuous integration. 1. Create a [GitHub personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with the *public_repo* box checked. Also check the **workflow** checkbox in case your repo uses GitHub Actions for continuous integration.
2. Create a repository with that name on GitHub. Unlike Gitea, GitHub does not support creating repositories by pushing to the remote. You can also use an existing remote repo if it has the same commit history as your Gitea repo. 2. Create a repository with that name on GitHub. Unlike Gitea, GitHub does not support creating repositories by pushing to the remote. You can also use an existing remote repo if it has the same commit history as your Gitea repo.
3. In the settings of your Gitea repo, fill in the **Git Remote Repository URL**: `https://github.com/<your_github_group>/<your_github_project>.git`. 3. In the settings of your Gitea repo, fill in the **Git Remote Repository URL**: `https://github.com/<your_github_group>/<your_github_project>.git`.
4. Fill in the **Authorization** fields with your GitHub username and the personal access token as **Password**. 4. Fill in the **Authorization** fields with your GitHub username and the personal access token as **Password**.
@ -91,10 +91,10 @@ The repository pushes shortly thereafter. To force a push, select the **Synchron
### Mirror an existing ssh repository ### Mirror an existing ssh repository
Currently gitea supports no ssh push mirrors. You can work around this by adding a `post-receive` hook to your gitea repository that pushes manually. Currently Gitea supports no ssh push mirrors. You can work around this by adding a `post-receive` hook to your Gitea repository that pushes manually.
1. Make sure the user running gitea has access to the git repo you are trying to mirror to from shell. 1. Make sure the user running Gitea has access to the git repo you are trying to mirror to from shell.
2. On the Webinterface at the repository settings > git hooks add a post-receive hook for the mirror. I.e. 2. On the web interface at the repository settings > git hooks add a post-receive hook for the mirror. I.e.
``` ```
#!/usr/bin/env bash #!/usr/bin/env bash

7
go.mod
View File

@ -16,7 +16,7 @@ require (
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/NYTimes/gziphandler v1.1.1 github.com/ProtonMail/go-crypto v1.0.0
github.com/PuerkitoBio/goquery v1.9.1 github.com/PuerkitoBio/goquery v1.9.1
github.com/alecthomas/chroma/v2 v2.13.0 github.com/alecthomas/chroma/v2 v2.13.0
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
@ -54,7 +54,7 @@ require (
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/go-github/v57 v57.0.0 github.com/google/go-github/v61 v61.0.0
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.1.2 github.com/gorilla/feeds v1.1.2
@ -67,7 +67,7 @@ require (
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 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/klauspost/cpuid/v2 v2.2.7
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/markbates/goth v1.79.0 github.com/markbates/goth v1.79.0
@ -136,7 +136,6 @@ require (
github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/RoaringBitmap/roaring v1.9.0 // indirect github.com/RoaringBitmap/roaring v1.9.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect

10
go.sum
View File

@ -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.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 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 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 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
@ -396,8 +394,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs= github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=
github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw= github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
@ -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.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.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= 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.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 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 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.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=

View File

@ -42,7 +42,7 @@ func main() {
log.GetManager().Close() log.GetManager().Close()
os.Exit(code) os.Exit(code)
} }
app := cmd.NewMainApp(Version, formatBuiltWith()) app := cmd.NewMainApp(cmd.AppVersion{Version: Version, Extra: formatBuiltWith()})
_ = cmd.RunMainApp(app, os.Args...) // all errors should have been handled by the RunMainApp _ = cmd.RunMainApp(app, os.Args...) // all errors should have been handled by the RunMainApp
log.GetManager().Close() log.GetManager().Close()
} }

View File

@ -74,6 +74,13 @@ func (run *ActionRun) Link() string {
return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index) return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index)
} }
func (run *ActionRun) WorkflowLink() string {
if run.Repo == nil {
return ""
}
return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), run.WorkflowID)
}
// RefLink return the url of run's ref // RefLink return the url of run's ref
func (run *ActionRun) RefLink() string { func (run *ActionRun) RefLink() string {
refName := git.RefName(run.Ref) refName := git.RefName(run.Ref)
@ -98,13 +105,10 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error {
return nil return nil
} }
if run.Repo == nil { if err := run.LoadRepo(ctx); err != nil {
repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) return err
if err != nil {
return err
}
run.Repo = repo
} }
if err := run.Repo.LoadAttributes(ctx); err != nil { if err := run.Repo.LoadAttributes(ctx); err != nil {
return err return err
} }
@ -120,6 +124,19 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error {
return nil 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 { func (run *ActionRun) Duration() time.Duration {
return calculateDuration(run.Started, run.Stopped, run.Status) + run.PreviousDuration return calculateDuration(run.Started, run.Stopped, run.Status) + run.PreviousDuration
} }
@ -146,6 +163,10 @@ func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, err
return nil, fmt.Errorf("event %s is not a pull request event", run.Event) return nil, fmt.Errorf("event %s is not a pull request event", run.Event)
} }
func (run *ActionRun) IsSchedule() bool {
return run.ScheduleID > 0
}
func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
_, err := db.GetEngine(ctx).ID(repo.ID). _, err := db.GetEngine(ctx).ID(repo.ID).
SetExpr("num_action_runs", SetExpr("num_action_runs",
@ -241,11 +262,11 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
// InsertRun inserts a run // InsertRun inserts a run
func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error {
ctx, commiter, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return err return err
} }
defer commiter.Close() defer committer.Close()
index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID)
if err != nil { if err != nil {
@ -310,7 +331,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
} }
} }
return commiter.Commit() return committer.Commit()
} }
func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) { func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) {

View File

@ -270,7 +270,7 @@ func CountRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) {
// Only affect action runners were a owner ID is set, as actions runners // Only affect action runners were a owner ID is set, as actions runners
// could also be created on a repository. // could also be created on a repository.
return db.GetEngine(ctx).Table("action_runner"). 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). Where("`action_runner`.owner_id != ?", 0).
And(builder.IsNull{"`user`.id"}). And(builder.IsNull{"`user`.id"}).
Count(new(ActionRunner)) Count(new(ActionRunner))
@ -279,7 +279,7 @@ func CountRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) {
func FixRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) { func FixRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) {
subQuery := builder.Select("`action_runner`.id"). subQuery := builder.Select("`action_runner`.id").
From("`action_runner`"). 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}). Where(builder.Neq{"`action_runner`.owner_id": 0}).
And(builder.IsNull{"`user`.id"}) And(builder.IsNull{"`user`.id"})
b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`") b := builder.Delete(builder.In("id", subQuery)).From("`action_runner`")
@ -289,3 +289,25 @@ func FixRunnersWithoutBelongingOwner(ctx context.Context) (int64, error) {
} }
return res.RowsAffected() 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()
}

View File

@ -216,11 +216,11 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
} }
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) { func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
ctx, commiter, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
defer commiter.Close() defer committer.Close()
e := db.GetEngine(ctx) e := db.GetEngine(ctx)
@ -322,7 +322,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
task.Job = job task.Job = job
if err := commiter.Commit(); err != nil { if err := committer.Commit(); err != nil {
return nil, false, err return nil, false, err
} }
@ -347,11 +347,11 @@ func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionT
stepStates[v.Id] = v stepStates[v.Id] = v
} }
ctx, commiter, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer commiter.Close() defer committer.Close()
e := db.GetEngine(ctx) e := db.GetEngine(ctx)
@ -412,7 +412,7 @@ func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionT
} }
} }
if err := commiter.Commit(); err != nil { if err := committer.Commit(); err != nil {
return nil, err return nil, err
} }

View File

@ -13,7 +13,7 @@ import (
// ActionTasksVersion // ActionTasksVersion
// If both ownerID and repoID is zero, its scope is global. // If both ownerID and repoID is zero, its scope is global.
// If ownerID is not zero and repoID is zero, its scope is org (there is no user-level runner currrently). // If ownerID is not zero and repoID is zero, its scope is org (there is no user-level runner currently).
// If ownerID is zero and repoID is not zero, its scope is repo. // If ownerID is zero and repoID is not zero, its scope is repo.
type ActionTasksVersion struct { type ActionTasksVersion struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
@ -73,11 +73,11 @@ func increaseTasksVersionByScope(ctx context.Context, ownerID, repoID int64) err
} }
func IncreaseTaskVersion(ctx context.Context, ownerID, repoID int64) error { func IncreaseTaskVersion(ctx context.Context, ownerID, repoID int64) error {
ctx, commiter, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return err return err
} }
defer commiter.Close() defer committer.Close()
// 1. increase global // 1. increase global
if err := increaseTasksVersionByScope(ctx, 0, 0); err != nil { if err := increaseTasksVersionByScope(ctx, 0, 0); err != nil {
@ -101,5 +101,5 @@ func IncreaseTaskVersion(ctx context.Context, ownerID, repoID int64) error {
} }
} }
return commiter.Commit() return committer.Commit()
} }

View File

@ -92,6 +92,11 @@ func DeleteVariable(ctx context.Context, id int64) error {
func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) { func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) {
variables := map[string]string{} variables := map[string]string{}
if err := run.LoadRepo(ctx); err != nil {
log.Error("LoadRepo: %v", err)
return nil, err
}
// Global // Global
globalVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{}) globalVariables, err := db.Find[ActionVariable](ctx, FindVariablesOpts{})
if err != nil { if err != nil {

View File

@ -110,7 +110,6 @@ func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerific
Reason: "gpg.error.no_committer_account", Reason: "gpg.error.no_committer_account",
} }
} }
} }
} }

View File

@ -8,6 +8,7 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/base32" "encoding/base32"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
@ -294,7 +295,7 @@ func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOp
return nil, err return nil, err
} }
if app.UID != opts.UserID { if app.UID != opts.UserID {
return nil, fmt.Errorf("UID mismatch") return nil, errors.New("UID mismatch")
} }
builtinApps := BuiltinApplications() builtinApps := BuiltinApplications()
if _, builtin := builtinApps[app.ClientID]; builtin { if _, builtin := builtinApps[app.ClientID]; builtin {

View File

@ -13,8 +13,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
//////////////////// Application
func TestOAuth2Application_GenerateClientSecret(t *testing.T) { func TestOAuth2Application_GenerateClientSecret(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})

View File

@ -227,7 +227,6 @@ func NamesToBean(names ...string) ([]any, error) {
// Need to map provided names to beans... // Need to map provided names to beans...
beanMap := make(map[string]any) beanMap := make(map[string]any)
for _, bean := range tables { for _, bean := range tables {
beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean
beanMap[strings.ToLower(x.TableName(bean))] = bean beanMap[strings.ToLower(x.TableName(bean))] = bean
beanMap[strings.ToLower(x.TableName(bean, true))] = bean beanMap[strings.ToLower(x.TableName(bean, true))] = bean

View File

@ -1,7 +1,7 @@
- -
id: 1 id: 1
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 2 issue_id: 2
index: 2 index: 2
head_repo_id: 1 head_repo_id: 1
@ -16,7 +16,7 @@
- -
id: 2 id: 2
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 3 issue_id: 3
index: 3 index: 3
head_repo_id: 1 head_repo_id: 1
@ -29,7 +29,7 @@
- -
id: 3 id: 3
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 8 issue_id: 8
index: 1 index: 1
head_repo_id: 11 head_repo_id: 11
@ -42,7 +42,7 @@
- -
id: 4 id: 4
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 9 issue_id: 9
index: 1 index: 1
head_repo_id: 48 head_repo_id: 48
@ -55,7 +55,7 @@
- -
id: 5 # this PR is outdated (one commit behind branch1 ) id: 5 # this PR is outdated (one commit behind branch1 )
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 11 issue_id: 11
index: 5 index: 5
head_repo_id: 1 head_repo_id: 1
@ -68,7 +68,7 @@
- -
id: 6 id: 6
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 12 issue_id: 12
index: 2 index: 2
head_repo_id: 3 head_repo_id: 3
@ -81,7 +81,7 @@
- -
id: 7 id: 7
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 19 issue_id: 19
index: 1 index: 1
head_repo_id: 58 head_repo_id: 58
@ -94,7 +94,7 @@
- -
id: 8 id: 8
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 20 issue_id: 20
index: 1 index: 1
head_repo_id: 23 head_repo_id: 23
@ -103,7 +103,7 @@
- -
id: 9 id: 9
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 21 issue_id: 21
index: 1 index: 1
head_repo_id: 60 head_repo_id: 60
@ -112,7 +112,7 @@
- -
id: 10 id: 10
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 22 issue_id: 22
index: 1 index: 1
head_repo_id: 61 head_repo_id: 61

View File

@ -397,36 +397,16 @@ func GetLatestCommitStatusForRepoCommitIDs(ctx context.Context, repoID int64, co
// FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts // FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts
func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) { func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) {
type result struct {
Index int64
SHA string
}
getBase := func() *xorm.Session {
return db.GetEngine(ctx).Table(&CommitStatus{}).Where("repo_id = ?", repoID)
}
start := timeutil.TimeStampNow().AddDuration(-before) start := timeutil.TimeStampNow().AddDuration(-before)
results := make([]result, 0, 10)
sess := getBase().And("updated_unix >= ?", start). var contexts []string
Select("max( `index` ) as `index`, sha"). if err := db.GetEngine(ctx).Table("commit_status").
GroupBy("context_hash, sha").OrderBy("max( `index` ) desc") Where("repo_id = ?", repoID).And("updated_unix >= ?", start).
Cols("context").Distinct().Find(&contexts); err != nil {
err := sess.Find(&results)
if err != nil {
return nil, err return nil, err
} }
contexts := make([]string, 0, len(results)) return contexts, nil
if len(results) == 0 {
return contexts, nil
}
conds := make([]builder.Cond, 0, len(results))
for _, result := range results {
conds = append(conds, builder.Eq{"`index`": result.Index, "sha": result.SHA})
}
return contexts, getBase().And(builder.Or(conds...)).Select("context").Find(&contexts)
} }
// NewCommitStatusOptions holds options for creating a CommitStatus // NewCommitStatusOptions holds options for creating a CommitStatus

View File

@ -5,11 +5,15 @@ package git_test
import ( import (
"testing" "testing"
"time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -175,3 +179,55 @@ func Test_CalcCommitStatus(t *testing.T) {
assert.Equal(t, kase.expected, git_model.CalcCommitStatus(kase.statuses)) assert.Equal(t, kase.expected, git_model.CalcCommitStatus(kase.statuses))
} }
} }
func TestFindRepoRecentCommitStatusContexts(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo2)
assert.NoError(t, err)
defer gitRepo.Close()
commit, err := gitRepo.GetBranchCommit(repo2.DefaultBranch)
assert.NoError(t, err)
defer func() {
_, err := db.DeleteByBean(db.DefaultContext, &git_model.CommitStatus{
RepoID: repo2.ID,
CreatorID: user2.ID,
SHA: commit.ID.String(),
})
assert.NoError(t, err)
}()
err = git_model.NewCommitStatus(db.DefaultContext, git_model.NewCommitStatusOptions{
Repo: repo2,
Creator: user2,
SHA: commit.ID,
CommitStatus: &git_model.CommitStatus{
State: structs.CommitStatusFailure,
TargetURL: "https://example.com/tests/",
Context: "compliance/lint-backend",
},
})
assert.NoError(t, err)
err = git_model.NewCommitStatus(db.DefaultContext, git_model.NewCommitStatusOptions{
Repo: repo2,
Creator: user2,
SHA: commit.ID,
CommitStatus: &git_model.CommitStatus{
State: structs.CommitStatusSuccess,
TargetURL: "https://example.com/tests/",
Context: "compliance/lint-backend",
},
})
assert.NoError(t, err)
contexts, err := git_model.FindRepoRecentCommitStatusContexts(db.DefaultContext, repo2.ID, time.Hour)
assert.NoError(t, err)
if assert.Len(t, contexts, 1) {
assert.Equal(t, "compliance/lint-backend", contexts[0])
}
}

View File

@ -5,7 +5,7 @@ package git
import ( import (
"context" "context"
"fmt" "errors"
"strings" "strings"
"time" "time"
@ -148,7 +148,7 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor
} }
if !force && u.ID != lock.OwnerID { 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 { if _, err := db.GetEngine(dbCtx).ID(id).Delete(new(LFSLock)); err != nil {

View File

@ -34,7 +34,7 @@ func TestXRef_AddCrossReferences(t *testing.T) {
// Comment on PR to reopen issue #1 // Comment on PR to reopen issue #1
content = fmt.Sprintf("content2, reopens #%d", itarget.Index) content = fmt.Sprintf("content2, reopens #%d", itarget.Index)
c := testCreateComment(t, 1, 2, pr.ID, content) c := testCreateComment(t, 2, pr.ID, content)
ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: c.ID}) ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: c.ID})
assert.Equal(t, issues_model.CommentTypeCommentRef, ref.Type) assert.Equal(t, issues_model.CommentTypeCommentRef, ref.Type)
assert.Equal(t, pr.RepoID, ref.RefRepoID) assert.Equal(t, pr.RepoID, ref.RefRepoID)
@ -104,18 +104,18 @@ func TestXRef_ResolveCrossReferences(t *testing.T) {
pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index)) pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index))
rp := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i1.ID, RefIssueID: pr.Issue.ID, RefCommentID: 0}) rp := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i1.ID, RefIssueID: pr.Issue.ID, RefCommentID: 0})
c1 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i2.Index)) c1 := testCreateComment(t, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i2.Index))
r1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c1.ID}) r1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c1.ID})
// Must be ignored // Must be ignored
c2 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("mentions #%d", i2.Index)) c2 := testCreateComment(t, 2, pr.Issue.ID, fmt.Sprintf("mentions #%d", i2.Index))
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c2.ID}) unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c2.ID})
// Must be superseded by c4/r4 // Must be superseded by c4/r4
c3 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("reopens #%d", i3.Index)) c3 := testCreateComment(t, 2, pr.Issue.ID, fmt.Sprintf("reopens #%d", i3.Index))
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c3.ID}) unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c3.ID})
c4 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i3.Index)) c4 := testCreateComment(t, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i3.Index))
r4 := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c4.ID}) r4 := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c4.ID})
refs, err := pr.ResolveCrossReferences(db.DefaultContext) refs, err := pr.ResolveCrossReferences(db.DefaultContext)
@ -168,7 +168,7 @@ func testCreatePR(t *testing.T, repo, doer int64, title, content string) *issues
return pr return pr
} }
func testCreateComment(t *testing.T, repo, doer, issue int64, content string) *issues_model.Comment { func testCreateComment(t *testing.T, doer, issue int64, content string) *issues_model.Comment {
d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}) d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer})
i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue}) i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue})
c := &issues_model.Comment{Type: issues_model.CommentTypeComment, PosterID: doer, Poster: d, IssueID: issue, Issue: i, Content: content} c := &issues_model.Comment{Type: issues_model.CommentTypeComment, PosterID: doer, Poster: d, IssueID: issue, Issue: i, Content: content}

View File

@ -807,7 +807,7 @@ func UpdateAllowEdits(ctx context.Context, pr *PullRequest) error {
// Mergeable returns if the pullrequest is mergeable. // Mergeable returns if the pullrequest is mergeable.
func (pr *PullRequest) Mergeable(ctx context.Context) bool { func (pr *PullRequest) Mergeable(ctx context.Context) bool {
// If a pull request isn't mergable if it's: // If a pull request isn't mergeable if it's:
// - Being conflict checked. // - Being conflict checked.
// - Has a conflict. // - Has a conflict.
// - Received a error while being conflict checked. // - Received a error while being conflict checked.

View File

@ -62,11 +62,13 @@ func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission,
return true 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 return false
} }
prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, p.Units[0].RepoID, branch) prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, branch)
if err != nil { if err != nil {
return false return false
} }

View File

@ -345,11 +345,9 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error
return nil, err return nil, err
} }
} }
} else if opts.ReviewerTeam != nil { } else if opts.ReviewerTeam != nil {
review.Type = ReviewTypeRequest review.Type = ReviewTypeRequest
review.ReviewerTeamID = opts.ReviewerTeam.ID review.ReviewerTeamID = opts.ReviewerTeam.ID
} else { } else {
return nil, fmt.Errorf("provide either reviewer or reviewer team") return nil, fmt.Errorf("provide either reviewer or reviewer team")
} }

View File

@ -187,8 +187,8 @@ func AddTime(ctx context.Context, user *user_model.User, issue *Issue, amount in
Issue: issue, Issue: issue,
Repo: issue.Repo, Repo: issue.Repo,
Doer: user, Doer: user,
// Content before v1.21 did store the formated string instead of seconds, // Content before v1.21 did store the formatted string instead of seconds,
// so use "|" as delimeter to mark the new format // so use "|" as delimiter to mark the new format
Content: fmt.Sprintf("|%d", amount), Content: fmt.Sprintf("|%d", amount),
Type: CommentTypeAddTimeManual, Type: CommentTypeAddTimeManual,
TimeID: t.ID, TimeID: t.ID,
@ -267,8 +267,8 @@ func DeleteIssueUserTimes(ctx context.Context, issue *Issue, user *user_model.Us
Issue: issue, Issue: issue,
Repo: issue.Repo, Repo: issue.Repo,
Doer: user, Doer: user,
// Content before v1.21 did store the formated string instead of seconds, // Content before v1.21 did store the formatted string instead of seconds,
// so use "|" as delimeter to mark the new format // so use "|" as delimiter to mark the new format
Content: fmt.Sprintf("|%d", removedTime), Content: fmt.Sprintf("|%d", removedTime),
Type: CommentTypeDeleteTimeManual, Type: CommentTypeDeleteTimeManual,
}); err != nil { }); err != nil {
@ -298,8 +298,8 @@ func DeleteTime(ctx context.Context, t *TrackedTime) error {
Issue: t.Issue, Issue: t.Issue,
Repo: t.Issue.Repo, Repo: t.Issue.Repo,
Doer: t.User, Doer: t.User,
// Content before v1.21 did store the formated string instead of seconds, // Content before v1.21 did store the formatted string instead of seconds,
// so use "|" as delimeter to mark the new format // so use "|" as delimiter to mark the new format
Content: fmt.Sprintf("|%d", t.Time), Content: fmt.Sprintf("|%d", t.Time),
Type: CommentTypeDeleteTimeManual, Type: CommentTypeDeleteTimeManual,
}); err != nil { }); err != nil {

View File

@ -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) log.Error("Unable to recreate uniques on table %s. Error: %v", tableName, err)
return err return err
} }
case setting.Database.Type.IsMySQL(): case setting.Database.Type.IsMySQL():
// MySQL will drop all the constraints on the old table // MySQL will drop all the constraints on the old table
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil { 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 return err
} }
sequenceMap[sequence] = sequenceData sequenceMap[sequence] = sequenceData
} }
// CASCADE causes postgres to drop all the constraints on the old table // 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 return err
} }
} }
} }
case setting.Database.Type.IsMSSQL(): case setting.Database.Type.IsMSSQL():
// MSSQL will drop all the constraints on the old table // MSSQL will drop all the constraints on the old table
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil { 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) log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err)
return err return err
} }
default: default:
log.Fatal("Unrecognized DB") log.Fatal("Unrecognized DB")
} }

View File

@ -574,15 +574,22 @@ var migrations = []Migration{
// v293 -> v294 // v293 -> v294
NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency), NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
// Gitea 1.22.0 ends at 294 // Gitea 1.22.0-rc0 ends at 294
// v294 -> v295 // v294 -> v295
NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue), NewMigration("Add unique index for project issue table", v1_22.AddUniqueIndexForProjectIssue),
// v295 -> v296 // v295 -> v296
NewMigration("Add commit status summary table", v1_23.AddCommitStatusSummary), NewMigration("Add commit status summary table", v1_22.AddCommitStatusSummary),
// v296 -> v297 // v296 -> v297
NewMigration("Add missing field of commit status summary table", v1_23.AddCommitStatusSummary2), NewMigration("Add missing field of commit status summary table", v1_22.AddCommitStatusSummary2),
// v297 -> v298 // v297 -> v298
NewMigration("Add everyone_access_mode for repo_unit", v1_22.AddRepoUnitEveryoneAccessMode),
// v298 -> v299
NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable),
// Gitea 1.22.0-rc1 ends at 299
// v299 -> v300
NewMigration("Add metadata column for comment table", v1_23.AddCommentMetaDataColumn), NewMigration("Add metadata column for comment table", v1_23.AddCommentMetaDataColumn),
} }

View File

@ -262,7 +262,6 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
for _, u := range units { for _, u := range units {
var found bool var found bool
for _, team := range teams { for _, team := range teams {
var teamU []*TeamUnit var teamU []*TeamUnit
var unitEnabled bool var unitEnabled bool
err = sess.Where("team_id = ?", team.ID).Find(&teamU) err = sess.Where("team_id = ?", team.ID).Find(&teamU)
@ -331,12 +330,11 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error {
} }
if !protectedBranch.EnableApprovalsWhitelist { if !protectedBranch.EnableApprovalsWhitelist {
perm, err := getUserRepoPermission(sess, baseRepo, reviewer) perm, err := getUserRepoPermission(sess, baseRepo, reviewer)
if err != nil { if err != nil {
return false, err return false, err
} }
if perm.UnitsMode == nil { if len(perm.UnitsMode) == 0 {
for _, u := range perm.Units { for _, u := range perm.Units {
if u.Type == UnitTypeCode { if u.Type == UnitTypeCode {
return AccessModeWrite <= perm.AccessMode, nil return AccessModeWrite <= perm.AccessMode, nil

View File

@ -43,11 +43,6 @@ func RemigrateU2FCredentials(x *xorm.Engine) error {
if err != nil { if err != nil {
return err 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: case schemas.MSSQL:
// This column has an index on it. I could write all of the code to attempt to change the index OR // 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. // I could just use recreate table.

View File

@ -4,4 +4,4 @@
package v1_17 //nolint package v1_17 //nolint
// This migration added non-ideal indices to the action table which on larger datasets slowed things down // This migration added non-ideal indices to the action table which on larger datasets slowed things down
// it has been superceded by v218.go // it has been superseded by v218.go

View File

@ -9,9 +9,9 @@ import (
// AddConfidentialColumnToOAuth2ApplicationTable: add ConfidentialClient column, setting existing rows to true // AddConfidentialColumnToOAuth2ApplicationTable: add ConfidentialClient column, setting existing rows to true
func AddConfidentialClientColumnToOAuth2ApplicationTable(x *xorm.Engine) error { func AddConfidentialClientColumnToOAuth2ApplicationTable(x *xorm.Engine) error {
type OAuth2Application struct { type oauth2Application struct {
ID int64
ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"` ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"`
} }
return x.Sync(new(oauth2Application))
return x.Sync(new(OAuth2Application))
} }

View File

@ -13,12 +13,12 @@ import (
func Test_AddConfidentialClientColumnToOAuth2ApplicationTable(t *testing.T) { func Test_AddConfidentialClientColumnToOAuth2ApplicationTable(t *testing.T) {
// premigration // premigration
type OAuth2Application struct { type oauth2Application struct {
ID int64 ID int64
} }
// Prepare and load the testing database // Prepare and load the testing database
x, deferable := base.PrepareTestEnv(t, 0, new(OAuth2Application)) x, deferable := base.PrepareTestEnv(t, 0, new(oauth2Application))
defer deferable() defer deferable()
if x == nil || t.Failed() { if x == nil || t.Failed() {
return return
@ -36,7 +36,7 @@ func Test_AddConfidentialClientColumnToOAuth2ApplicationTable(t *testing.T) {
} }
got := []ExpectedOAuth2Application{} 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 return
} }

View File

@ -104,7 +104,7 @@ func ChangeContainerMetadataMultiArch(x *xorm.Engine) error {
// Convert to new metadata format // Convert to new metadata format
new := &MetadataNew{ newMetadata := &MetadataNew{
Type: old.Type, Type: old.Type,
IsTagged: old.IsTagged, IsTagged: old.IsTagged,
Platform: old.Platform, Platform: old.Platform,
@ -119,7 +119,7 @@ func ChangeContainerMetadataMultiArch(x *xorm.Engine) error {
Manifests: manifests, Manifests: manifests,
} }
metadataJSON, err := json.Marshal(new) metadataJSON, err := json.Marshal(newMetadata)
if err != nil { if err != nil {
return err return err
} }

View File

@ -53,7 +53,7 @@ func expandHashReferencesToSha256(x *xorm.Engine) error {
if setting.Database.Type.IsMySQL() { if setting.Database.Type.IsMySQL() {
_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1])) _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` MODIFY COLUMN `%s` VARCHAR(64)", alts[0], alts[1]))
} else if setting.Database.Type.IsMSSQL() { } 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 { } else {
_, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1])) _, err = db.Exec(fmt.Sprintf("ALTER TABLE `%s` ALTER COLUMN `%s` TYPE VARCHAR(64)", alts[0], alts[1]))
} }

View File

@ -1,7 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved. // Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package v1_23 //nolint package v1_22 //nolint
import ( import (
"fmt" "fmt"

View File

@ -1,7 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved. // Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package v1_23 //nolint package v1_22 //nolint
import ( import (
"slices" "slices"

View File

@ -1,7 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved. // Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package v1_23 //nolint package v1_22 //nolint
import "xorm.io/xorm" import "xorm.io/xorm"

View File

@ -1,7 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved. // Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package v1_23 //nolint package v1_22 //nolint
import "xorm.io/xorm" import "xorm.io/xorm"

View File

@ -0,0 +1,17 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //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{})
}

View File

@ -0,0 +1,10 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import "xorm.io/xorm"
func DropWronglyCreatedTable(x *xorm.Engine) error {
return x.DropTables("o_auth2_application")
}

View File

@ -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 { 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) return fmt.Errorf("couldn't add in scratch_hash and scratch_salt: %w", err)
} }
} }
} }

View File

@ -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 { 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) return fmt.Errorf("couldn't add in sha1, token_hash, token_salt and token_last_eight: %w", err)
} }
} }
} }

View File

@ -118,7 +118,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error
// Remove watches from all users and now unaccessible repos // Remove watches from all users and now unaccessible repos
for _, user := range t.Members { 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 { if err != nil {
return err return err
} else if has { } 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 { 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 return err
} }
if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil { if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil {

View File

@ -291,15 +291,15 @@ func TestAccessibleReposEnv_CountRepos(t *testing.T) {
func TestAccessibleReposEnv_RepoIDs(t *testing.T) { func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
testSuccess := func(userID, _, pageSize int64, expectedRepoIDs []int64) { testSuccess := func(userID int64, expectedRepoIDs []int64) {
env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID)
assert.NoError(t, err) assert.NoError(t, err)
repoIDs, err := env.RepoIDs(1, 100) repoIDs, err := env.RepoIDs(1, 100)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedRepoIDs, repoIDs) assert.Equal(t, expectedRepoIDs, repoIDs)
} }
testSuccess(2, 1, 100, []int64{3, 5, 32}) testSuccess(2, []int64{3, 5, 32})
testSuccess(4, 0, 100, []int64{3, 32}) testSuccess(4, []int64{3, 32})
} }
func TestAccessibleReposEnv_Repos(t *testing.T) { func TestAccessibleReposEnv_Repos(t *testing.T) {

View File

@ -130,11 +130,11 @@ func (t *Team) GetUnitsMap() map[string]string {
m := make(map[string]string) m := make(map[string]string)
if t.AccessMode >= perm.AccessModeAdmin { if t.AccessMode >= perm.AccessModeAdmin {
for _, u := range unit.Units { for _, u := range unit.Units {
m[u.NameKey] = t.AccessMode.String() m[u.NameKey] = t.AccessMode.ToString()
} }
} else { } else {
for _, u := range t.Units { for _, u := range t.Units {
m[u.Unit().NameKey] = u.AccessMode.String() m[u.Unit().NameKey] = u.AccessMode.ToString()
} }
} }
return m return m
@ -226,9 +226,8 @@ func GetTeamIDsByNames(ctx context.Context, orgID int64, names []string, ignoreN
if err != nil { if err != nil {
if ignoreNonExistent { if ignoreNonExistent {
continue continue
} else {
return nil, err
} }
return nil, err
} }
ids = append(ids, u.ID) ids = append(ids, u.ID)
} }

View File

@ -287,9 +287,10 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
// SearchVersions gets all versions of packages matching the search options // SearchVersions gets all versions of packages matching the search options
func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
sess := db.GetEngine(ctx). sess := db.GetEngine(ctx).
Where(opts.ToConds()). Select("package_version.*").
Table("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) 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 // SearchLatestVersions gets the latest version of every package matching the search options
func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
cond := opts.ToConds(). in := builder.
And(builder.Expr("pv2.id IS NULL")) Select("MAX(package_version.id)").
From("package_version").
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))") InnerJoin("package", "package.id = package_version.package_id").
if opts.IsInternal.Has() { Where(opts.ToConds()).
joinCond = joinCond.And(builder.Eq{"pv2.is_internal": opts.IsInternal.Value()}) GroupBy("package_version.package_id")
}
sess := db.GetEngine(ctx). sess := db.GetEngine(ctx).
Select("package_version.*").
Table("package_version"). Table("package_version").
Join("LEFT", "package_version pv2", joinCond).
Join("INNER", "package", "package.id = package_version.package_id"). Join("INNER", "package", "package.id = package_version.package_id").
Where(cond) Where(builder.In("package_version.id", in))
opts.configureOrderBy(sess) opts.configureOrderBy(sess)

View File

@ -63,13 +63,11 @@ func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re
} }
func maxAccessMode(modes ...perm.AccessMode) perm.AccessMode { func maxAccessMode(modes ...perm.AccessMode) perm.AccessMode {
max := perm.AccessModeNone maxMode := perm.AccessModeNone
for _, mode := range modes { for _, mode := range modes {
if mode > max { maxMode = max(maxMode, mode)
max = mode
}
} }
return max return maxMode
} }
type userAccess struct { type userAccess struct {

View File

@ -79,17 +79,17 @@ func TestHasAccess(t *testing.T) {
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.True(t, repo2.IsPrivate) 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.NoError(t, err)
assert.True(t, has) 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) 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) 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) assert.NoError(t, err)
} }

View File

@ -6,6 +6,7 @@ package access
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
@ -14,13 +15,17 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "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 // Permission contains all the permissions related variables to a repository for a user
type Permission struct { type Permission struct {
AccessMode perm_model.AccessMode 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. // 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 return p.AccessMode >= perm_model.AccessModeAdmin
} }
// HasAccess returns true if the current user has at least read access to any unit of this repository // HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
func (p *Permission) HasAccess() bool { // It doesn't count the "everyone access mode".
if p.UnitsMode == nil { func (p *Permission) HasAnyUnitAccess() bool {
return p.AccessMode >= perm_model.AccessModeRead 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) HasAnyUnitAccessOrEveryoneAccess() bool {
func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode { for _, v := range p.everyoneAccessMode {
if p.UnitsMode == nil { if v >= perm_model.AccessModeRead {
for _, u := range p.Units { return true
if u.Type == unitType {
return p.AccessMode
}
} }
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 // CanAccess returns true if user has mode access to the unit of the repository
@ -103,8 +142,8 @@ func (p *Permission) CanWriteIssuesOrPulls(isPull bool) bool {
} }
func (p *Permission) ReadableUnitTypes() []unit.Type { func (p *Permission) ReadableUnitTypes() []unit.Type {
types := make([]unit.Type, 0, len(p.Units)) types := make([]unit.Type, 0, len(p.units))
for _, u := range p.Units { for _, u := range p.units {
if p.CanRead(u.Type) { if p.CanRead(u.Type) {
types = append(types, u.Type) types = append(types, u.Type)
} }
@ -114,21 +153,21 @@ func (p *Permission) ReadableUnitTypes() []unit.Type {
func (p *Permission) LogString() string { func (p *Permission) LogString() string {
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [ " 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 := "" config := ""
if unit.Config != nil { if u.Config != nil {
configBytes, err := unit.Config.ToDB() configBytes, err := u.Config.ToDB()
config = string(configBytes) config = string(configBytes)
if err != nil { if err != nil {
config = err.Error() config = err.Error()
} }
} }
format += "\nUnits[%d]: ID: %d RepoID: %d Type: %s Config: %s" 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" format += "\nUnitMode[%-v]: %-v"
args = append(args, key.LogString(), value.LogString()) args = append(args, key.LogString(), value.LogString())
} }
@ -136,23 +175,35 @@ func (p *Permission) LogString() string {
return fmt.Sprintf(format, args...) return fmt.Sprintf(format, args...)
} }
// GetUserRepoPermission returns the user permissions to the repository func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) {
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (Permission, error) { if user == nil || user.ID <= 0 {
var perm Permission return
if log.IsTrace() {
defer func() {
if user == nil {
log.Trace("Permission Loaded for anonymous user in %-v:\nPermissions: %-+v",
repo,
perm)
return
}
log.Trace("Permission Loaded for %-v in %-v:\nPermissions: %-+v",
user,
repo,
perm)
}()
} }
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)
}
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. // anonymous user visit private repo.
// TODO: anonymous user visit public unit of private repo??? // TODO: anonymous user visit public unit of private repo???
@ -162,7 +213,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
} }
var isCollaborator bool var isCollaborator bool
var err error
if user != nil { if user != nil {
isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID) isCollaborator, err = repo_model.IsCollaborator(ctx, repo.ID, user.ID)
if err != nil { if err != nil {
@ -170,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 return perm, err
} }
@ -181,12 +231,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
return perm, nil return perm, nil
} }
if err := repo.LoadUnits(ctx); err != nil {
return perm, err
}
perm.Units = repo.Units
// anonymous visit public repo // anonymous visit public repo
if user == nil { if user == nil {
perm.AccessMode = perm_model.AccessModeRead perm.AccessMode = perm_model.AccessModeRead
@ -205,19 +249,16 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
return perm, err return perm, err
} }
if err := repo.LoadOwner(ctx); err != nil {
return perm, err
}
if !repo.Owner.IsOrganization() { if !repo.Owner.IsOrganization() {
return perm, nil return perm, nil
} }
perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode) perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
// Collaborators on organization // Collaborators on organization
if isCollaborator { if isCollaborator {
for _, u := range repo.Units { for _, u := range repo.Units {
perm.UnitsMode[u.Type] = perm.AccessMode perm.unitsMode[u.Type] = perm.AccessMode
} }
} }
@ -231,7 +272,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
for _, team := range teams { for _, team := range teams {
if team.AccessMode >= perm_model.AccessModeAdmin { if team.AccessMode >= perm_model.AccessModeAdmin {
perm.AccessMode = perm_model.AccessModeOwner perm.AccessMode = perm_model.AccessModeOwner
perm.UnitsMode = nil perm.unitsMode = nil
return perm, nil return perm, nil
} }
} }
@ -240,25 +281,25 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
var found bool var found bool
for _, team := range teams { for _, team := range teams {
if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist { if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist {
perm.UnitsMode[u.Type] = max(perm.UnitsMode[u.Type], teamMode) perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode)
found = true found = true
} }
} }
// for a public repo on an organization, a non-restricted user has read permission on non-team defined units. // 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 !found && !repo.IsPrivate && !user.IsRestricted {
if _, ok := perm.UnitsMode[u.Type]; !ok { if _, ok := perm.unitsMode[u.Type]; !ok {
perm.UnitsMode[u.Type] = perm_model.AccessModeRead perm.unitsMode[u.Type] = perm_model.AccessModeRead
} }
} }
} }
// remove no permission units // remove no permission units
perm.Units = make([]*repo_model.RepoUnit, 0, len(repo.Units)) perm.units = make([]*repo_model.RepoUnit, 0, len(repo.Units))
for t := range perm.UnitsMode { for t := range perm.unitsMode {
for _, u := range repo.Units { for _, u := range repo.Units {
if u.Type == t { if u.Type == t {
perm.Units = append(perm.Units, u) perm.units = append(perm.units, u)
} }
} }
} }
@ -340,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. // 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) { func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.Repository, _ bool) (bool, error) {
if user.IsOrganization() { 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) perm, err := GetUserRepoPermission(ctx, repo, user)
if err != nil { if err != nil {
@ -350,8 +391,8 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.
perm.CanAccessAny(perm_model.AccessModeRead, unit.TypePullRequests), nil perm.CanAccessAny(perm_model.AccessModeRead, unit.TypePullRequests), nil
} }
// HasAccess returns true if user has access to repo // HasAnyUnitAccess see the comment of "perm.HasAnyUnitAccess"
func HasAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (bool, error) { func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (bool, error) {
var user *user_model.User var user *user_model.User
var err error var err error
if userID > 0 { if userID > 0 {
@ -364,7 +405,7 @@ func HasAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (
if err != nil { if err != nil {
return false, err return false, err
} }
return perm.HasAccess(), nil return perm.HasAnyUnitAccess(), nil
} }
// getUsersWithAccessMode returns users that have at least given access mode to the repository. // getUsersWithAccessMode returns users that have at least given access mode to the repository.

View File

@ -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")
}

View File

@ -5,25 +5,25 @@ package perm
import ( import (
"fmt" "fmt"
"slices"
"code.gitea.io/gitea/modules/util"
) )
// AccessMode specifies the users access mode // AccessMode specifies the users access mode
type AccessMode int type AccessMode int
const ( const (
// AccessModeNone no access AccessModeNone AccessMode = iota // 0: no access
AccessModeNone AccessMode = iota // 0
// AccessModeRead read access AccessModeRead // 1: read access
AccessModeRead // 1 AccessModeWrite // 2: write access
// AccessModeWrite write access AccessModeAdmin // 3: admin access
AccessModeWrite // 2 AccessModeOwner // 4: owner access
// AccessModeAdmin admin access
AccessModeAdmin // 3
// AccessModeOwner owner access
AccessModeOwner // 4
) )
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 { switch mode {
case AccessModeRead: case AccessModeRead:
return "read" return "read"
@ -39,19 +39,24 @@ func (mode AccessMode) String() string {
} }
func (mode AccessMode) LogString() 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. // 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 { switch permission {
case "read": case "read":
return AccessModeRead m = AccessModeRead
case "write": case "write":
return AccessModeWrite m = AccessModeWrite
case "admin": case "admin":
return AccessModeAdmin m = AccessModeAdmin
default: 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)
} }

View File

@ -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"))
}

View File

@ -110,13 +110,11 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error {
var items []string var items []string
switch project.BoardType { switch project.BoardType {
case BoardTypeBugTriage: case BoardTypeBugTriage:
items = setting.Project.ProjectBoardBugTriageType items = setting.Project.ProjectBoardBugTriageType
case BoardTypeBasicKanban: case BoardTypeBasicKanban:
items = setting.Project.ProjectBoardBasicKanbanType items = setting.Project.ProjectBoardBasicKanbanType
case BoardTypeNone: case BoardTypeNone:
fallthrough fallthrough
default: default:

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -41,11 +42,12 @@ func (err ErrUnitTypeNotExist) Unwrap() error {
// RepoUnit describes all units of a repository // RepoUnit describes all units of a repository
type RepoUnit struct { //revive:disable-line:exported type RepoUnit struct { //revive:disable-line:exported
ID int64 ID int64
RepoID int64 `xorm:"INDEX(s)"` RepoID int64 `xorm:"INDEX(s)"`
Type unit.Type `xorm:"INDEX(s)"` Type unit.Type `xorm:"INDEX(s)"`
Config convert.Conversion `xorm:"TEXT"` Config convert.Conversion `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
EveryoneAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
} }
func init() { func init() {

View File

@ -130,7 +130,10 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
// and just waste 1 unit is cheaper than re-allocate memory once. // and just waste 1 unit is cheaper than re-allocate memory once.
users := make([]*user_model.User, 0, len(uniqueUserIDs)+1) users := make([]*user_model.User, 0, len(uniqueUserIDs)+1)
if len(userIDs) > 0 { if len(userIDs) > 0 {
if err = e.In("id", uniqueUserIDs.Values()).OrderBy(user_model.GetOrderByName()).Find(&users); err != nil { if err = e.In("id", uniqueUserIDs.Values()).
Where(builder.Eq{"`user`.is_active": true}).
OrderBy(user_model.GetOrderByName()).
Find(&users); err != nil {
return nil, err return nil, err
} }
} }
@ -152,7 +155,8 @@ func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64)
return nil, err return nil, err
} }
cond := builder.And(builder.Neq{"`user`.id": posterID}) cond := builder.And(builder.Neq{"`user`.id": posterID}).
And(builder.Eq{"`user`.is_active": true})
if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate { if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate {
// This a private repository: // This a private repository:
@ -170,7 +174,6 @@ func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64)
// the owner of a private repo needs to be explicitly added. // the owner of a private repo needs to be explicitly added.
cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID}) cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID})
} }
} else { } else {
// This is a "public" repository: // This is a "public" repository:
// Any user that has read access, is a watcher or organization member can be requested to review // Any user that has read access, is a watcher or organization member can be requested to review

View File

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -25,8 +26,17 @@ func TestRepoAssignees(t *testing.T) {
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21}) repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21})
users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21) users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, users, 4) if assert.Len(t, users, 4) {
assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID}) assert.ElementsMatch(t, []int64{10, 15, 16, 18}, []int64{users[0].ID, users[1].ID, users[2].ID, users[3].ID})
}
// do not return deactivated users
assert.NoError(t, user_model.UpdateUserCols(db.DefaultContext, &user_model.User{ID: 15, IsActive: false}, "is_active"))
users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21)
assert.NoError(t, err)
if assert.Len(t, users, 3) {
assert.NotContains(t, []int64{users[0].ID, users[1].ID, users[2].ID}, 15)
}
} }
func TestRepoGetReviewers(t *testing.T) { func TestRepoGetReviewers(t *testing.T) {
@ -38,17 +48,19 @@ func TestRepoGetReviewers(t *testing.T) {
ctx := db.DefaultContext ctx := db.DefaultContext
reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2) reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, reviewers, 4) if assert.Len(t, reviewers, 3) {
assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID})
}
// should include doer if doer is not PR poster. // should include doer if doer is not PR poster.
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2) reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, reviewers, 4) assert.Len(t, reviewers, 3)
// should not include PR poster, if PR poster would be otherwise eligible // should not include PR poster, if PR poster would be otherwise eligible
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4) reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, reviewers, 3) assert.Len(t, reviewers, 2)
// test private user repo // test private user repo
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})

View File

@ -5,6 +5,7 @@ package models
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"code.gitea.io/gitea/models/db" "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 { func TestRepositoryReadyForTransfer(status repo_model.RepositoryStatus) error {
switch status { switch status {
case repo_model.RepositoryBeingMigrated: 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: case repo_model.RepositoryPendingTransfer:
return ErrRepoTransferInProgress{} return ErrRepoTransferInProgress{}
} }

View File

@ -191,16 +191,13 @@ type Unit struct {
NameKey string NameKey string
URI string URI string
DescKey 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. 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 // IsLessThan compares order of two units
func (u Unit) IsLessThan(unit Unit) bool { func (u Unit) IsLessThan(unit Unit) bool {
if (u.Type == TypeExternalTracker || u.Type == TypeExternalWiki) && unit.Type != TypeExternalTracker && unit.Type != TypeExternalWiki { return u.Priority < unit.Priority
return false
}
return u.Idx < unit.Idx
} }
// MaxPerm returns the max perms of this unit // MaxPerm returns the max perms of this unit
@ -236,7 +233,7 @@ var (
"repo.ext_issues", "repo.ext_issues",
"/issues", "/issues",
"repo.ext_issues.desc", "repo.ext_issues.desc",
1, 101,
perm.AccessModeRead, perm.AccessModeRead,
} }
@ -272,7 +269,7 @@ var (
"repo.ext_wiki", "repo.ext_wiki",
"/wiki", "/wiki",
"repo.ext_wiki.desc", "repo.ext_wiki.desc",
4, 102,
perm.AccessModeRead, perm.AccessModeRead,
} }

View File

@ -6,7 +6,6 @@ package unittest
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -16,7 +15,9 @@ import (
"code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
@ -45,6 +46,14 @@ func fatalTestError(fmtStr string, args ...any) {
// InitSettings initializes config provider and load common settings for tests // InitSettings initializes config provider and load common settings for tests
func InitSettings() { func InitSettings() {
setting.IsInTesting = true
log.OsExiter = func(code int) {
if code != 0 {
// non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details
panic(fmt.Errorf("non-zero exit code during testing: %d", code))
}
os.Exit(0)
}
if setting.CustomConf == "" { if setting.CustomConf == "" {
setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini") setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
_ = os.Remove(setting.CustomConf) _ = os.Remove(setting.CustomConf)
@ -53,7 +62,7 @@ func InitSettings() {
setting.LoadCommonSettings() setting.LoadCommonSettings()
if err := setting.PrepareAppDataPath(); err != nil { if err := setting.PrepareAppDataPath(); err != nil {
log.Fatalf("Can not prepare APP_DATA_PATH: %v", err) log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
} }
// register the dummy hash algorithm function used in the test fixtures // register the dummy hash algorithm function used in the test fixtures
_ = hash.Register("dummy", hash.NewDummyHasher) _ = hash.Register("dummy", hash.NewDummyHasher)
@ -106,6 +115,7 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) {
fatalTestError("Error creating test engine: %v\n", err) fatalTestError("Error creating test engine: %v\n", err)
} }
setting.IsInTesting = true
setting.AppURL = "https://try.gitea.io/" setting.AppURL = "https://try.gitea.io/"
setting.RunUser = "runuser" setting.RunUser = "runuser"
setting.SSH.User = "sshuser" setting.SSH.User = "sshuser"
@ -148,6 +158,9 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) {
config.SetDynGetter(system.NewDatabaseDynKeyGetter()) config.SetDynGetter(system.NewDatabaseDynKeyGetter())
if err = cache.Init(); err != nil {
fatalTestError("cache.Init: %v\n", err)
}
if err = storage.Init(); err != nil { if err = storage.Init(); err != nil {
fatalTestError("storage.Init: %v\n", err) fatalTestError("storage.Init: %v\n", err)
} }

View File

@ -501,19 +501,19 @@ func GetUserSalt() (string, error) {
// Note: The set of characters here can safely expand without a breaking change, // Note: The set of characters here can safely expand without a breaking change,
// but characters removed from this set can cause user account linking to break // but characters removed from this set can cause user account linking to break
var ( var (
customCharsReplacement = strings.NewReplacer("Æ", "AE") customCharsReplacement = strings.NewReplacer("Æ", "AE")
removeCharsRE = regexp.MustCompile(`['´\x60]`) removeCharsRE = regexp.MustCompile("['`´]")
removeDiacriticsTransform = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) transformDiacritics = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`) replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
) )
// normalizeUserName returns a string with single-quotes and diacritics // NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters.
// removed, and any other non-supported username characters replaced with // It returns a string with the single-quotes removed, and any other non-supported username characters are replaced with a `-` character
// a `-` character
func NormalizeUserName(s string) (string, error) { func NormalizeUserName(s string) (string, error) {
strDiacriticsRemoved, n, err := transform.String(removeDiacriticsTransform, customCharsReplacement.Replace(s)) s, _, _ = strings.Cut(s, "@")
strDiacriticsRemoved, n, err := transform.String(transformDiacritics, customCharsReplacement.Replace(s))
if err != nil { if err != nil {
return "", fmt.Errorf("Failed to normalize character `%v` in provided username `%v`", s[n], s) return "", fmt.Errorf("failed to normalize the string of provided username %q at position %d", s, n)
} }
return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
} }
@ -988,9 +988,8 @@ func GetUserIDsByNames(ctx context.Context, names []string, ignoreNonExistent bo
if err != nil { if err != nil {
if ignoreNonExistent { if ignoreNonExistent {
continue continue
} else {
return nil, err
} }
return nil, err
} }
ids = append(ids, u.ID) ids = append(ids, u.ID)
} }

View File

@ -5,8 +5,8 @@ package user_test
import ( import (
"context" "context"
"crypto/rand"
"fmt" "fmt"
"math/rand"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -506,15 +506,16 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
Expected string Expected string
IsNormalizedValid bool IsNormalizedValid bool
}{ }{
{"test", "test", true}, {"name@example.com", "name", true},
{"test'`´name", "testname", true},
{"Sinéad.O'Connor", "Sinead.OConnor", true}, {"Sinéad.O'Connor", "Sinead.OConnor", true},
{"Æsir", "AEsir", true}, {"Æsir", "AEsir", true},
// \u00e9\u0065\u0301 {"éé", "ee", true}, // \u00e9\u0065\u0301
{"éé", "ee", true},
{"Awareness Hub", "Awareness-Hub", true}, {"Awareness Hub", "Awareness-Hub", true},
{"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters {"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters
{".bad.", ".bad.", false}, {".bad.", ".bad.", false},
{"new😀user", "new😀user", false}, // No plans to support {"new😀user", "new😀user", false}, // No plans to support
{`"quoted"`, `"quoted"`, false}, // No plans to support
} }
for _, testCase := range testCases { for _, testCase := range testCases {
normalizedName, err := user_model.NormalizeUserName(testCase.Input) normalizedName, err := user_model.NormalizeUserName(testCase.Input)

View File

@ -208,14 +208,14 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueAssign,
webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueLabel,
webhook_module.HookEventIssueMilestone: webhook_module.HookEventIssueMilestone:
return matchIssuesEvent(commit, payload.(*api.IssuePayload), evt) return matchIssuesEvent(payload.(*api.IssuePayload), evt)
case // issue_comment case // issue_comment
webhook_module.HookEventIssueComment, webhook_module.HookEventIssueComment,
// `pull_request_comment` is same as `issue_comment` // `pull_request_comment` is same as `issue_comment`
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
webhook_module.HookEventPullRequestComment: webhook_module.HookEventPullRequestComment:
return matchIssueCommentEvent(commit, payload.(*api.IssueCommentPayload), evt) return matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt)
case // pull_request case // pull_request
webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequest,
@ -229,19 +229,19 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
case // pull_request_review case // pull_request_review
webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewApproved,
webhook_module.HookEventPullRequestReviewRejected: webhook_module.HookEventPullRequestReviewRejected:
return matchPullRequestReviewEvent(commit, payload.(*api.PullRequestPayload), evt) return matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt)
case // pull_request_review_comment case // pull_request_review_comment
webhook_module.HookEventPullRequestReviewComment: webhook_module.HookEventPullRequestReviewComment:
return matchPullRequestReviewCommentEvent(commit, payload.(*api.PullRequestPayload), evt) return matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt)
case // release case // release
webhook_module.HookEventRelease: webhook_module.HookEventRelease:
return matchReleaseEvent(commit, payload.(*api.ReleasePayload), evt) return matchReleaseEvent(payload.(*api.ReleasePayload), evt)
case // registry_package case // registry_package
webhook_module.HookEventPackage: webhook_module.HookEventPackage:
return matchPackageEvent(commit, payload.(*api.PackagePayload), evt) return matchPackageEvent(payload.(*api.PackagePayload), evt)
default: default:
log.Warn("unsupported event %q", triggedEvent) log.Warn("unsupported event %q", triggedEvent)
@ -347,7 +347,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
return matchTimes == len(evt.Acts()) return matchTimes == len(evt.Acts())
} }
func matchIssuesEvent(commit *git.Commit, issuePayload *api.IssuePayload, evt *jobparser.Event) bool { func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool {
// with no special filter parameters // with no special filter parameters
if len(evt.Acts()) == 0 { if len(evt.Acts()) == 0 {
return true return true
@ -495,7 +495,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
return activityTypeMatched && matchTimes == len(evt.Acts()) return activityTypeMatched && matchTimes == len(evt.Acts())
} }
func matchIssueCommentEvent(commit *git.Commit, issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool { func matchIssueCommentEvent(issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool {
// with no special filter parameters // with no special filter parameters
if len(evt.Acts()) == 0 { if len(evt.Acts()) == 0 {
return true return true
@ -527,7 +527,7 @@ func matchIssueCommentEvent(commit *git.Commit, issueCommentPayload *api.IssueCo
return matchTimes == len(evt.Acts()) return matchTimes == len(evt.Acts())
} }
func matchPullRequestReviewEvent(commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool { func matchPullRequestReviewEvent(prPayload *api.PullRequestPayload, evt *jobparser.Event) bool {
// with no special filter parameters // with no special filter parameters
if len(evt.Acts()) == 0 { if len(evt.Acts()) == 0 {
return true return true
@ -576,7 +576,7 @@ func matchPullRequestReviewEvent(commit *git.Commit, prPayload *api.PullRequestP
return matchTimes == len(evt.Acts()) return matchTimes == len(evt.Acts())
} }
func matchPullRequestReviewCommentEvent(commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool { func matchPullRequestReviewCommentEvent(prPayload *api.PullRequestPayload, evt *jobparser.Event) bool {
// with no special filter parameters // with no special filter parameters
if len(evt.Acts()) == 0 { if len(evt.Acts()) == 0 {
return true return true
@ -625,7 +625,7 @@ func matchPullRequestReviewCommentEvent(commit *git.Commit, prPayload *api.PullR
return matchTimes == len(evt.Acts()) return matchTimes == len(evt.Acts())
} }
func matchReleaseEvent(commit *git.Commit, payload *api.ReleasePayload, evt *jobparser.Event) bool { func matchReleaseEvent(payload *api.ReleasePayload, evt *jobparser.Event) bool {
// with no special filter parameters // with no special filter parameters
if len(evt.Acts()) == 0 { if len(evt.Acts()) == 0 {
return true return true
@ -662,7 +662,7 @@ func matchReleaseEvent(commit *git.Commit, payload *api.ReleasePayload, evt *job
return matchTimes == len(evt.Acts()) return matchTimes == len(evt.Acts())
} }
func matchPackageEvent(commit *git.Commit, payload *api.PackagePayload, evt *jobparser.Event) bool { func matchPackageEvent(payload *api.PackagePayload, evt *jobparser.Event) bool {
// with no special filter parameters // with no special filter parameters
if len(evt.Acts()) == 0 { if len(evt.Acts()) == 0 {
return true return true

View File

@ -63,16 +63,16 @@ func NewComplexity() {
func setupComplexity(values []string) { func setupComplexity(values []string) {
if len(values) != 1 || values[0] != "off" { if len(values) != 1 || values[0] != "off" {
for _, val := range values { for _, val := range values {
if complex, ok := charComplexities[val]; ok { if complexity, ok := charComplexities[val]; ok {
validChars += complex.ValidChars validChars += complexity.ValidChars
requiredList = append(requiredList, complex) requiredList = append(requiredList, complexity)
} }
} }
if len(requiredList) == 0 { if len(requiredList) == 0 {
// No valid character classes found; use all classes as default // No valid character classes found; use all classes as default
for _, complex := range charComplexities { for _, complexity := range charComplexities {
validChars += complex.ValidChars validChars += complexity.ValidChars
requiredList = append(requiredList, complex) requiredList = append(requiredList, complexity)
} }
} }
} }

View File

@ -4,9 +4,8 @@
package pwn package pwn
import ( import (
"math/rand" "math/rand/v2"
"net/http" "net/http"
"os"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -18,11 +17,6 @@ var client = New(WithHTTP(&http.Client{
Timeout: time.Second * 2, Timeout: time.Second * 2,
})) }))
func TestMain(m *testing.M) {
rand.Seed(time.Now().Unix())
os.Exit(m.Run())
}
func TestPassword(t *testing.T) { func TestPassword(t *testing.T) {
// Check input error // Check input error
_, err := client.CheckPassword("", false) _, err := client.CheckPassword("", false)
@ -81,24 +75,24 @@ func testPassword() string {
// Set special character // Set special character
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
random := rand.Intn(len(specialCharSet)) random := rand.IntN(len(specialCharSet))
password.WriteString(string(specialCharSet[random])) password.WriteString(string(specialCharSet[random]))
} }
// Set numeric // Set numeric
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
random := rand.Intn(len(numberSet)) random := rand.IntN(len(numberSet))
password.WriteString(string(numberSet[random])) password.WriteString(string(numberSet[random]))
} }
// Set uppercase // Set uppercase
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
random := rand.Intn(len(upperCharSet)) random := rand.IntN(len(upperCharSet))
password.WriteString(string(upperCharSet[random])) password.WriteString(string(upperCharSet[random]))
} }
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
random := rand.Intn(len(allCharSet)) random := rand.IntN(len(allCharSet))
password.WriteString(string(allCharSet[random])) password.WriteString(string(allCharSet[random]))
} }
inRune := []rune(password.String()) inRune := []rune(password.String())

View File

@ -307,10 +307,10 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu
// Deal with the binary hash // Deal with the binary hash
idx = 0 idx = 0
len := objectFormat.FullLength() / 2 length := objectFormat.FullLength() / 2
for idx < len { for idx < length {
var read int var read int
read, err = rd.Read(shaBuf[idx:len]) read, err = rd.Read(shaBuf[idx:length])
n += read n += read
if err != nil { if err != nil {
return mode, fname, sha, n, err return mode, fname, sha, n, err

View File

@ -468,7 +468,7 @@ func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) {
_, _ = rd.Discard(1) _, _ = rd.Discard(1)
} }
for { for {
modifier, err := rd.ReadSlice('\x00') modifier, err := rd.ReadString('\x00')
if err != nil { if err != nil {
if err != io.EOF { if err != io.EOF {
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)

View File

@ -29,7 +29,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
var revs map[string]*Commit var revs map[string]*Commit
if commit.repo.LastCommitCache != nil { if commit.repo.LastCommitCache != nil {
var unHitPaths []string var unHitPaths []string
revs, unHitPaths, err = getLastCommitForPathsByCache(ctx, commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache) revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -97,7 +97,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
return commitsInfo, treeCommit, nil return commitsInfo, treeCommit, nil
} }
func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) { func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) {
var unHitEntryPaths []string var unHitEntryPaths []string
results := make(map[string]*Commit) results := make(map[string]*Commit)
for _, p := range paths { for _, p := range paths {

View File

@ -49,9 +49,8 @@ readLoop:
if len(line) > 0 && line[0] == ' ' { if len(line) > 0 && line[0] == ' ' {
_, _ = signatureSB.Write(line[1:]) _, _ = signatureSB.Write(line[1:])
continue continue
} else {
pgpsig = false
} }
pgpsig = false
} }
if !message { if !message {

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