mirror of
https://github.com/go-gitea/gitea.git
synced 2024-12-24 22:45:11 +00:00
Merge branch 'main' into lunny/add_last_commit_when
This commit is contained in:
commit
54b22555ea
@ -1,5 +1,5 @@
|
||||
# Build stage
|
||||
FROM docker.io/library/golang:1.23-alpine3.20 AS build-env
|
||||
FROM docker.io/library/golang:1.23-alpine3.21 AS build-env
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-direct}
|
||||
@ -41,7 +41,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
|
||||
/go/src/code.gitea.io/gitea/environment-to-ini
|
||||
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
|
||||
|
||||
FROM docker.io/library/alpine:3.20
|
||||
FROM docker.io/library/alpine:3.21
|
||||
LABEL maintainer="maintainers@gitea.io"
|
||||
|
||||
EXPOSE 22 3000
|
||||
@ -78,7 +78,7 @@ ENV GITEA_CUSTOM=/data/gitea
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["/usr/bin/entrypoint"]
|
||||
CMD ["/bin/s6-svscan", "/etc/s6"]
|
||||
CMD ["/usr/bin/s6-svscan", "/etc/s6"]
|
||||
|
||||
COPY --from=build-env /tmp/local /
|
||||
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Build stage
|
||||
FROM docker.io/library/golang:1.23-alpine3.20 AS build-env
|
||||
FROM docker.io/library/golang:1.23-alpine3.21 AS build-env
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-direct}
|
||||
@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
|
||||
/go/src/code.gitea.io/gitea/environment-to-ini
|
||||
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
|
||||
|
||||
FROM docker.io/library/alpine:3.20
|
||||
FROM docker.io/library/alpine:3.21
|
||||
LABEL maintainer="maintainers@gitea.io"
|
||||
|
||||
EXPOSE 2222 3000
|
||||
|
8
Makefile
8
Makefile
@ -26,17 +26,17 @@ COMMA := ,
|
||||
XGO_VERSION := go-1.23.x
|
||||
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.0.3
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
|
||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.15.3
|
||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.0
|
||||
|
||||
DOCKER_IMAGE ?= gitea/gitea
|
||||
DOCKER_TAG ?= latest
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
@ -51,7 +52,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, fmt.Errorf("runner token %q: %w", token, util.ErrNotExist)
|
||||
return nil, fmt.Errorf(`runner token "%s...": %w`, base.TruncateString(token, 3), util.ErrNotExist)
|
||||
}
|
||||
return &runnerToken, nil
|
||||
}
|
||||
@ -68,19 +69,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string
|
||||
return err
|
||||
}
|
||||
|
||||
// NewRunnerToken creates a new active runner token and invalidate all old tokens
|
||||
// NewRunnerTokenWithValue creates a new active runner token and invalidate all old tokens
|
||||
// ownerID will be ignored and treated as 0 if repoID is non-zero.
|
||||
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
|
||||
func NewRunnerTokenWithValue(ctx context.Context, ownerID, repoID int64, token string) (*ActionRunnerToken, error) {
|
||||
if ownerID != 0 && repoID != 0 {
|
||||
// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally.
|
||||
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
||||
ownerID = 0
|
||||
}
|
||||
|
||||
token, err := util.CryptoRandomString(40)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runnerToken := &ActionRunnerToken{
|
||||
OwnerID: ownerID,
|
||||
RepoID: repoID,
|
||||
@ -95,11 +92,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).Insert(runnerToken)
|
||||
_, err := db.GetEngine(ctx).Insert(runnerToken)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
|
||||
token, err := util.CryptoRandomString(40)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewRunnerTokenWithValue(ctx, ownerID, repoID, token)
|
||||
}
|
||||
|
||||
// GetLatestRunnerToken returns the latest runner token
|
||||
func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
|
||||
if ownerID != 0 && repoID != 0 {
|
||||
|
@ -13,6 +13,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
@ -54,7 +56,8 @@ func TestDumpAuthSource(t *testing.T) {
|
||||
|
||||
sb := new(strings.Builder)
|
||||
|
||||
db.DumpTables([]*schemas.Table{authSourceSchema}, sb)
|
||||
|
||||
// TODO: this test is quite hacky, it should use a low-level "select" (without model processors) but not a database dump
|
||||
engine := db.GetEngine(db.DefaultContext).(*xorm.Engine)
|
||||
require.NoError(t, engine.DumpTables([]*schemas.Table{authSourceSchema}, sb))
|
||||
assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`)
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) {
|
||||
}
|
||||
|
||||
func CheckCollationsDefaultEngine() (*CheckCollationsResult, error) {
|
||||
return CheckCollations(x)
|
||||
return CheckCollations(xormEngine)
|
||||
}
|
||||
|
||||
func alterDatabaseCollation(x *xorm.Engine, collation string) error {
|
||||
|
@ -94,7 +94,7 @@ func GetEngine(ctx context.Context) Engine {
|
||||
if e := getExistingEngine(ctx); e != nil {
|
||||
return e
|
||||
}
|
||||
return x.Context(ctx)
|
||||
return xormEngine.Context(ctx)
|
||||
}
|
||||
|
||||
// getExistingEngine gets an existing db Engine/Statement from this context or returns nil
|
||||
@ -155,7 +155,7 @@ func TxContext(parentCtx context.Context) (*Context, Committer, error) {
|
||||
return newContext(parentCtx, sess), &halfCommitter{committer: sess}, nil
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
sess := xormEngine.NewSession()
|
||||
if err := sess.Begin(); err != nil {
|
||||
_ = sess.Close()
|
||||
return nil, nil, err
|
||||
@ -179,7 +179,7 @@ func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error
|
||||
}
|
||||
|
||||
func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error {
|
||||
sess := x.NewSession()
|
||||
sess := xormEngine.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
@ -322,7 +322,7 @@ func CountByBean(ctx context.Context, bean any) (int64, error) {
|
||||
|
||||
// TableName returns the table name according a bean object
|
||||
func TableName(bean any) string {
|
||||
return x.TableName(bean)
|
||||
return xormEngine.TableName(bean)
|
||||
}
|
||||
|
||||
// InTransaction returns true if the engine is in a transaction otherwise return false
|
||||
|
@ -16,30 +16,30 @@ import (
|
||||
|
||||
// ConvertDatabaseTable converts database and tables from utf8 to utf8mb4 if it's mysql and set ROW_FORMAT=dynamic
|
||||
func ConvertDatabaseTable() error {
|
||||
if x.Dialect().URI().DBType != schemas.MYSQL {
|
||||
if xormEngine.Dialect().URI().DBType != schemas.MYSQL {
|
||||
return nil
|
||||
}
|
||||
|
||||
r, err := CheckCollations(x)
|
||||
r, err := CheckCollations(xormEngine)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = x.Exec(fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET utf8mb4 COLLATE %s", setting.Database.Name, r.ExpectedCollation))
|
||||
_, err = xormEngine.Exec(fmt.Sprintf("ALTER DATABASE `%s` CHARACTER SET utf8mb4 COLLATE %s", setting.Database.Name, r.ExpectedCollation))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tables, err := x.DBMetas()
|
||||
tables, err := xormEngine.DBMetas()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, table := range tables {
|
||||
if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` ROW_FORMAT=dynamic", table.Name)); err != nil {
|
||||
if _, err := xormEngine.Exec(fmt.Sprintf("ALTER TABLE `%s` ROW_FORMAT=dynamic", table.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` CONVERT TO CHARACTER SET utf8mb4 COLLATE %s", table.Name, r.ExpectedCollation)); err != nil {
|
||||
if _, err := xormEngine.Exec(fmt.Sprintf("ALTER TABLE `%s` CONVERT TO CHARACTER SET utf8mb4 COLLATE %s", table.Name, r.ExpectedCollation)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -49,11 +49,11 @@ func ConvertDatabaseTable() error {
|
||||
|
||||
// ConvertVarcharToNVarchar converts database and tables from varchar to nvarchar if it's mssql
|
||||
func ConvertVarcharToNVarchar() error {
|
||||
if x.Dialect().URI().DBType != schemas.MSSQL {
|
||||
if xormEngine.Dialect().URI().DBType != schemas.MSSQL {
|
||||
return nil
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
sess := xormEngine.NewSession()
|
||||
defer sess.Close()
|
||||
res, err := sess.QuerySliceString(`SELECT 'ALTER TABLE ' + OBJECT_NAME(SC.object_id) + ' MODIFY SC.name NVARCHAR(' + CONVERT(VARCHAR(5),SC.max_length) + ')'
|
||||
FROM SYS.columns SC
|
||||
|
@ -8,17 +8,10 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/contexts"
|
||||
"xorm.io/xorm/names"
|
||||
"xorm.io/xorm/schemas"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver
|
||||
@ -27,9 +20,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
x *xorm.Engine
|
||||
tables []any
|
||||
initFuncs []func() error
|
||||
xormEngine *xorm.Engine
|
||||
registeredModels []any
|
||||
registeredInitFuncs []func() error
|
||||
)
|
||||
|
||||
// Engine represents a xorm engine or session.
|
||||
@ -70,167 +63,38 @@ type Engine interface {
|
||||
|
||||
// TableInfo returns table's information via an object
|
||||
func TableInfo(v any) (*schemas.Table, error) {
|
||||
return x.TableInfo(v)
|
||||
return xormEngine.TableInfo(v)
|
||||
}
|
||||
|
||||
// DumpTables dump tables information
|
||||
func DumpTables(tables []*schemas.Table, w io.Writer, tp ...schemas.DBType) error {
|
||||
return x.DumpTables(tables, w, tp...)
|
||||
}
|
||||
|
||||
// RegisterModel registers model, if initfunc provided, it will be invoked after data model sync
|
||||
// RegisterModel registers model, if initFuncs provided, it will be invoked after data model sync
|
||||
func RegisterModel(bean any, initFunc ...func() error) {
|
||||
tables = append(tables, bean)
|
||||
if len(initFuncs) > 0 && initFunc[0] != nil {
|
||||
initFuncs = append(initFuncs, initFunc[0])
|
||||
registeredModels = append(registeredModels, bean)
|
||||
if len(registeredInitFuncs) > 0 && initFunc[0] != nil {
|
||||
registeredInitFuncs = append(registeredInitFuncs, initFunc[0])
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
gonicNames := []string{"SSL", "UID"}
|
||||
for _, name := range gonicNames {
|
||||
names.LintGonicMapper[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// newXORMEngine returns a new XORM engine from the configuration
|
||||
func newXORMEngine() (*xorm.Engine, error) {
|
||||
connStr, err := setting.DBConnStr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var engine *xorm.Engine
|
||||
|
||||
if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 {
|
||||
// OK whilst we sort out our schema issues - create a schema aware postgres
|
||||
registerPostgresSchemaDriver()
|
||||
engine, err = xorm.NewEngine("postgresschema", connStr)
|
||||
} else {
|
||||
engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if setting.Database.Type == "mysql" {
|
||||
engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"})
|
||||
} else if setting.Database.Type == "mssql" {
|
||||
engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"})
|
||||
}
|
||||
engine.SetSchema(setting.Database.Schema)
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// SyncAllTables sync the schemas of all tables, is required by unit test code
|
||||
func SyncAllTables() error {
|
||||
_, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{
|
||||
_, err := xormEngine.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{
|
||||
WarnIfDatabaseColumnMissed: true,
|
||||
}, tables...)
|
||||
}, registeredModels...)
|
||||
return err
|
||||
}
|
||||
|
||||
// InitEngine initializes the xorm.Engine and sets it as db.DefaultContext
|
||||
func InitEngine(ctx context.Context) error {
|
||||
xormEngine, err := newXORMEngine()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "SQLite3 support") {
|
||||
return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
|
||||
}
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
xormEngine.SetMapper(names.GonicMapper{})
|
||||
// WARNING: for serv command, MUST remove the output to os.stdout,
|
||||
// so use log file to instead print to stdout.
|
||||
xormEngine.SetLogger(NewXORMLogger(setting.Database.LogSQL))
|
||||
xormEngine.ShowSQL(setting.Database.LogSQL)
|
||||
xormEngine.SetMaxOpenConns(setting.Database.MaxOpenConns)
|
||||
xormEngine.SetMaxIdleConns(setting.Database.MaxIdleConns)
|
||||
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
||||
xormEngine.SetDefaultContext(ctx)
|
||||
|
||||
if setting.Database.SlowQueryThreshold > 0 {
|
||||
xormEngine.AddHook(&SlowQueryHook{
|
||||
Threshold: setting.Database.SlowQueryThreshold,
|
||||
Logger: log.GetLogger("xorm"),
|
||||
})
|
||||
}
|
||||
|
||||
SetDefaultEngine(ctx, xormEngine)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDefaultEngine sets the default engine for db
|
||||
func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
|
||||
x = eng
|
||||
DefaultContext = &Context{Context: ctx, engine: x}
|
||||
}
|
||||
|
||||
// UnsetDefaultEngine closes and unsets the default engine
|
||||
// We hope the SetDefaultEngine and UnsetDefaultEngine can be paired, but it's impossible now,
|
||||
// there are many calls to InitEngine -> SetDefaultEngine directly to overwrite the `x` and DefaultContext without close
|
||||
// Global database engine related functions are all racy and there is no graceful close right now.
|
||||
func UnsetDefaultEngine() {
|
||||
if x != nil {
|
||||
_ = x.Close()
|
||||
x = nil
|
||||
}
|
||||
DefaultContext = nil
|
||||
}
|
||||
|
||||
// InitEngineWithMigration initializes a new xorm.Engine and sets it as the db.DefaultContext
|
||||
// This function must never call .Sync() if the provided migration function fails.
|
||||
// When called from the "doctor" command, the migration function is a version check
|
||||
// that prevents the doctor from fixing anything in the database if the migration level
|
||||
// is different from the expected value.
|
||||
func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) {
|
||||
if err = InitEngine(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = x.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preprocessDatabaseCollation(x)
|
||||
|
||||
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
|
||||
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
|
||||
//
|
||||
// Installation should only be being re-run if users want to recover an old database.
|
||||
// However, we should think carefully about should we support re-install on an installed instance,
|
||||
// as there may be other problems due to secret reinitialization.
|
||||
if err = migrateFunc(x); err != nil {
|
||||
return fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
|
||||
if err = SyncAllTables(); err != nil {
|
||||
return fmt.Errorf("sync database struct error: %w", err)
|
||||
}
|
||||
|
||||
for _, initFunc := range initFuncs {
|
||||
if err := initFunc(); err != nil {
|
||||
return fmt.Errorf("initFunc failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NamesToBean return a list of beans or an error
|
||||
func NamesToBean(names ...string) ([]any, error) {
|
||||
beans := []any{}
|
||||
if len(names) == 0 {
|
||||
beans = append(beans, tables...)
|
||||
beans = append(beans, registeredModels...)
|
||||
return beans, nil
|
||||
}
|
||||
// Need to map provided names to beans...
|
||||
beanMap := make(map[string]any)
|
||||
for _, bean := range tables {
|
||||
for _, bean := range registeredModels {
|
||||
beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean
|
||||
beanMap[strings.ToLower(x.TableName(bean))] = bean
|
||||
beanMap[strings.ToLower(x.TableName(bean, true))] = bean
|
||||
beanMap[strings.ToLower(xormEngine.TableName(bean))] = bean
|
||||
beanMap[strings.ToLower(xormEngine.TableName(bean, true))] = bean
|
||||
}
|
||||
|
||||
gotBean := make(map[any]bool)
|
||||
@ -247,36 +111,9 @@ func NamesToBean(names ...string) ([]any, error) {
|
||||
return beans, nil
|
||||
}
|
||||
|
||||
// DumpDatabase dumps all data from database according the special database SQL syntax to file system.
|
||||
func DumpDatabase(filePath, dbType string) error {
|
||||
var tbs []*schemas.Table
|
||||
for _, t := range tables {
|
||||
t, err := x.TableInfo(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tbs = append(tbs, t)
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Version int64
|
||||
}
|
||||
t, err := x.TableInfo(&Version{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tbs = append(tbs, t)
|
||||
|
||||
if len(dbType) > 0 {
|
||||
return x.DumpTablesToFile(tbs, filePath, schemas.DBType(dbType))
|
||||
}
|
||||
return x.DumpTablesToFile(tbs, filePath)
|
||||
}
|
||||
|
||||
// MaxBatchInsertSize returns the table's max batch insert size
|
||||
func MaxBatchInsertSize(bean any) int {
|
||||
t, err := x.TableInfo(bean)
|
||||
t, err := xormEngine.TableInfo(bean)
|
||||
if err != nil {
|
||||
return 50
|
||||
}
|
||||
@ -285,18 +122,18 @@ func MaxBatchInsertSize(bean any) int {
|
||||
|
||||
// IsTableNotEmpty returns true if table has at least one record
|
||||
func IsTableNotEmpty(beanOrTableName any) (bool, error) {
|
||||
return x.Table(beanOrTableName).Exist()
|
||||
return xormEngine.Table(beanOrTableName).Exist()
|
||||
}
|
||||
|
||||
// DeleteAllRecords will delete all the records of this table
|
||||
func DeleteAllRecords(tableName string) error {
|
||||
_, err := x.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
|
||||
_, err := xormEngine.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
|
||||
return err
|
||||
}
|
||||
|
||||
// GetMaxID will return max id of the table
|
||||
func GetMaxID(beanOrTableName any) (maxID int64, err error) {
|
||||
_, err = x.Select("MAX(id)").Table(beanOrTableName).Get(&maxID)
|
||||
_, err = xormEngine.Select("MAX(id)").Table(beanOrTableName).Get(&maxID)
|
||||
return maxID, err
|
||||
}
|
||||
|
||||
@ -308,24 +145,3 @@ func SetLogSQL(ctx context.Context, on bool) {
|
||||
sess.Engine().ShowSQL(on)
|
||||
}
|
||||
}
|
||||
|
||||
type SlowQueryHook struct {
|
||||
Threshold time.Duration
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
var _ contexts.Hook = &SlowQueryHook{}
|
||||
|
||||
func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
return c.Ctx, nil
|
||||
}
|
||||
|
||||
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.ExecuteTime >= h.Threshold {
|
||||
// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function
|
||||
// is being displayed (the function that ultimately wants to execute the query in the code)
|
||||
// instead of the function of the slow query hook being called.
|
||||
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
33
models/db/engine_dump.go
Normal file
33
models/db/engine_dump.go
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package db
|
||||
|
||||
import "xorm.io/xorm/schemas"
|
||||
|
||||
// DumpDatabase dumps all data from database according the special database SQL syntax to file system.
|
||||
func DumpDatabase(filePath, dbType string) error {
|
||||
var tbs []*schemas.Table
|
||||
for _, t := range registeredModels {
|
||||
t, err := xormEngine.TableInfo(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tbs = append(tbs, t)
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Version int64
|
||||
}
|
||||
t, err := xormEngine.TableInfo(&Version{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tbs = append(tbs, t)
|
||||
|
||||
if dbType != "" {
|
||||
return xormEngine.DumpTablesToFile(tbs, filePath, schemas.DBType(dbType))
|
||||
}
|
||||
return xormEngine.DumpTablesToFile(tbs, filePath)
|
||||
}
|
34
models/db/engine_hook.go
Normal file
34
models/db/engine_hook.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"xorm.io/xorm/contexts"
|
||||
)
|
||||
|
||||
type SlowQueryHook struct {
|
||||
Threshold time.Duration
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
var _ contexts.Hook = (*SlowQueryHook)(nil)
|
||||
|
||||
func (*SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
return c.Ctx, nil
|
||||
}
|
||||
|
||||
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.ExecuteTime >= h.Threshold {
|
||||
// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function
|
||||
// is being displayed (the function that ultimately wants to execute the query in the code)
|
||||
// instead of the function of the slow query hook being called.
|
||||
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
|
||||
}
|
||||
return nil
|
||||
}
|
140
models/db/engine_init.go
Normal file
140
models/db/engine_init.go
Normal file
@ -0,0 +1,140 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/names"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gonicNames := []string{"SSL", "UID"}
|
||||
for _, name := range gonicNames {
|
||||
names.LintGonicMapper[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// newXORMEngine returns a new XORM engine from the configuration
|
||||
func newXORMEngine() (*xorm.Engine, error) {
|
||||
connStr, err := setting.DBConnStr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var engine *xorm.Engine
|
||||
|
||||
if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 {
|
||||
// OK whilst we sort out our schema issues - create a schema aware postgres
|
||||
registerPostgresSchemaDriver()
|
||||
engine, err = xorm.NewEngine("postgresschema", connStr)
|
||||
} else {
|
||||
engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if setting.Database.Type == "mysql" {
|
||||
engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"})
|
||||
} else if setting.Database.Type == "mssql" {
|
||||
engine.Dialect().SetParams(map[string]string{"DEFAULT_VARCHAR": "nvarchar"})
|
||||
}
|
||||
engine.SetSchema(setting.Database.Schema)
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// InitEngine initializes the xorm.Engine and sets it as db.DefaultContext
|
||||
func InitEngine(ctx context.Context) error {
|
||||
xe, err := newXORMEngine()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "SQLite3 support") {
|
||||
return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
|
||||
}
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
xe.SetMapper(names.GonicMapper{})
|
||||
// WARNING: for serv command, MUST remove the output to os.stdout,
|
||||
// so use log file to instead print to stdout.
|
||||
xe.SetLogger(NewXORMLogger(setting.Database.LogSQL))
|
||||
xe.ShowSQL(setting.Database.LogSQL)
|
||||
xe.SetMaxOpenConns(setting.Database.MaxOpenConns)
|
||||
xe.SetMaxIdleConns(setting.Database.MaxIdleConns)
|
||||
xe.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
||||
xe.SetDefaultContext(ctx)
|
||||
|
||||
if setting.Database.SlowQueryThreshold > 0 {
|
||||
xe.AddHook(&SlowQueryHook{
|
||||
Threshold: setting.Database.SlowQueryThreshold,
|
||||
Logger: log.GetLogger("xorm"),
|
||||
})
|
||||
}
|
||||
|
||||
SetDefaultEngine(ctx, xe)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDefaultEngine sets the default engine for db
|
||||
func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
|
||||
xormEngine = eng
|
||||
DefaultContext = &Context{Context: ctx, engine: xormEngine}
|
||||
}
|
||||
|
||||
// UnsetDefaultEngine closes and unsets the default engine
|
||||
// We hope the SetDefaultEngine and UnsetDefaultEngine can be paired, but it's impossible now,
|
||||
// there are many calls to InitEngine -> SetDefaultEngine directly to overwrite the `xormEngine` and DefaultContext without close
|
||||
// Global database engine related functions are all racy and there is no graceful close right now.
|
||||
func UnsetDefaultEngine() {
|
||||
if xormEngine != nil {
|
||||
_ = xormEngine.Close()
|
||||
xormEngine = nil
|
||||
}
|
||||
DefaultContext = nil
|
||||
}
|
||||
|
||||
// InitEngineWithMigration initializes a new xorm.Engine and sets it as the db.DefaultContext
|
||||
// This function must never call .Sync() if the provided migration function fails.
|
||||
// When called from the "doctor" command, the migration function is a version check
|
||||
// that prevents the doctor from fixing anything in the database if the migration level
|
||||
// is different from the expected value.
|
||||
func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) {
|
||||
if err = InitEngine(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = xormEngine.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preprocessDatabaseCollation(xormEngine)
|
||||
|
||||
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
|
||||
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
|
||||
//
|
||||
// Installation should only be being re-run if users want to recover an old database.
|
||||
// However, we should think carefully about should we support re-install on an installed instance,
|
||||
// as there may be other problems due to secret reinitialization.
|
||||
if err = migrateFunc(xormEngine); err != nil {
|
||||
return fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
|
||||
if err = SyncAllTables(); err != nil {
|
||||
return fmt.Errorf("sync database struct error: %w", err)
|
||||
}
|
||||
|
||||
for _, initFunc := range registeredInitFuncs {
|
||||
if err := initFunc(); err != nil {
|
||||
return fmt.Errorf("initFunc failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -16,7 +16,7 @@ var (
|
||||
// ErrNameEmpty name is empty error
|
||||
ErrNameEmpty = util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument}
|
||||
|
||||
// AlphaDashDotPattern characters prohibited in a user name (anything except A-Za-z0-9_.-)
|
||||
// AlphaDashDotPattern characters prohibited in a username (anything except A-Za-z0-9_.-)
|
||||
AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
|
||||
)
|
||||
|
||||
|
@ -17,11 +17,11 @@ func CountBadSequences(_ context.Context) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
sess := xormEngine.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
var sequences []string
|
||||
schema := x.Dialect().URI().Schema
|
||||
schema := xormEngine.Dialect().URI().Schema
|
||||
|
||||
sess.Engine().SetSchema("")
|
||||
if err := sess.Table("information_schema.sequences").Cols("sequence_name").Where("sequence_name LIKE 'tmp_recreate__%_id_seq%' AND sequence_catalog = ?", setting.Database.Name).Find(&sequences); err != nil {
|
||||
@ -38,7 +38,7 @@ func FixBadSequences(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
sess := x.NewSession()
|
||||
sess := xormEngine.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
|
510
models/error.go
510
models/error.go
@ -1,510 +0,0 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// ErrUserOwnRepos represents a "UserOwnRepos" kind of error.
|
||||
type ErrUserOwnRepos struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrUserOwnRepos checks if an error is a ErrUserOwnRepos.
|
||||
func IsErrUserOwnRepos(err error) bool {
|
||||
_, ok := err.(ErrUserOwnRepos)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserOwnRepos) Error() string {
|
||||
return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// ErrUserHasOrgs represents a "UserHasOrgs" kind of error.
|
||||
type ErrUserHasOrgs struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrUserHasOrgs checks if an error is a ErrUserHasOrgs.
|
||||
func IsErrUserHasOrgs(err error) bool {
|
||||
_, ok := err.(ErrUserHasOrgs)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserHasOrgs) Error() string {
|
||||
return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// ErrUserOwnPackages notifies that the user (still) owns the packages.
|
||||
type ErrUserOwnPackages struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrUserOwnPackages checks if an error is an ErrUserOwnPackages.
|
||||
func IsErrUserOwnPackages(err error) bool {
|
||||
_, ok := err.(ErrUserOwnPackages)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserOwnPackages) Error() string {
|
||||
return fmt.Sprintf("user still has ownership of packages [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// ErrDeleteLastAdminUser represents a "DeleteLastAdminUser" kind of error.
|
||||
type ErrDeleteLastAdminUser struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrDeleteLastAdminUser checks if an error is a ErrDeleteLastAdminUser.
|
||||
func IsErrDeleteLastAdminUser(err error) bool {
|
||||
_, ok := err.(ErrDeleteLastAdminUser)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrDeleteLastAdminUser) Error() string {
|
||||
return fmt.Sprintf("can not delete the last admin user [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// ErrInvalidCloneAddr represents a "InvalidCloneAddr" kind of error.
|
||||
type ErrInvalidCloneAddr struct {
|
||||
Host string
|
||||
IsURLError bool
|
||||
IsInvalidPath bool
|
||||
IsProtocolInvalid bool
|
||||
IsPermissionDenied bool
|
||||
LocalPath bool
|
||||
}
|
||||
|
||||
// IsErrInvalidCloneAddr checks if an error is a ErrInvalidCloneAddr.
|
||||
func IsErrInvalidCloneAddr(err error) bool {
|
||||
_, ok := err.(*ErrInvalidCloneAddr)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrInvalidCloneAddr) Error() string {
|
||||
if err.IsInvalidPath {
|
||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided path is invalid", err.Host)
|
||||
}
|
||||
if err.IsProtocolInvalid {
|
||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url protocol is not allowed", err.Host)
|
||||
}
|
||||
if err.IsPermissionDenied {
|
||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed.", err.Host)
|
||||
}
|
||||
if err.IsURLError {
|
||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url is invalid", err.Host)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed", err.Host)
|
||||
}
|
||||
|
||||
func (err *ErrInvalidCloneAddr) Unwrap() error {
|
||||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
||||
// ErrUpdateTaskNotExist represents a "UpdateTaskNotExist" kind of error.
|
||||
type ErrUpdateTaskNotExist struct {
|
||||
UUID string
|
||||
}
|
||||
|
||||
// IsErrUpdateTaskNotExist checks if an error is a ErrUpdateTaskNotExist.
|
||||
func IsErrUpdateTaskNotExist(err error) bool {
|
||||
_, ok := err.(ErrUpdateTaskNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUpdateTaskNotExist) Error() string {
|
||||
return fmt.Sprintf("update task does not exist [uuid: %s]", err.UUID)
|
||||
}
|
||||
|
||||
func (err ErrUpdateTaskNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrInvalidTagName represents a "InvalidTagName" kind of error.
|
||||
type ErrInvalidTagName struct {
|
||||
TagName string
|
||||
}
|
||||
|
||||
// IsErrInvalidTagName checks if an error is a ErrInvalidTagName.
|
||||
func IsErrInvalidTagName(err error) bool {
|
||||
_, ok := err.(ErrInvalidTagName)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidTagName) Error() string {
|
||||
return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
|
||||
}
|
||||
|
||||
func (err ErrInvalidTagName) Unwrap() error {
|
||||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
||||
// ErrProtectedTagName represents a "ProtectedTagName" kind of error.
|
||||
type ErrProtectedTagName struct {
|
||||
TagName string
|
||||
}
|
||||
|
||||
// IsErrProtectedTagName checks if an error is a ErrProtectedTagName.
|
||||
func IsErrProtectedTagName(err error) bool {
|
||||
_, ok := err.(ErrProtectedTagName)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrProtectedTagName) Error() string {
|
||||
return fmt.Sprintf("release tag name is protected [tag_name: %s]", err.TagName)
|
||||
}
|
||||
|
||||
func (err ErrProtectedTagName) Unwrap() error {
|
||||
return util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// ErrRepoFileAlreadyExists represents a "RepoFileAlreadyExist" kind of error.
|
||||
type ErrRepoFileAlreadyExists struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// IsErrRepoFileAlreadyExists checks if an error is a ErrRepoFileAlreadyExists.
|
||||
func IsErrRepoFileAlreadyExists(err error) bool {
|
||||
_, ok := err.(ErrRepoFileAlreadyExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrRepoFileAlreadyExists) Error() string {
|
||||
return fmt.Sprintf("repository file already exists [path: %s]", err.Path)
|
||||
}
|
||||
|
||||
func (err ErrRepoFileAlreadyExists) Unwrap() error {
|
||||
return util.ErrAlreadyExist
|
||||
}
|
||||
|
||||
// ErrRepoFileDoesNotExist represents a "RepoFileDoesNotExist" kind of error.
|
||||
type ErrRepoFileDoesNotExist struct {
|
||||
Path string
|
||||
Name string
|
||||
}
|
||||
|
||||
// IsErrRepoFileDoesNotExist checks if an error is a ErrRepoDoesNotExist.
|
||||
func IsErrRepoFileDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrRepoFileDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrRepoFileDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("repository file does not exist [path: %s]", err.Path)
|
||||
}
|
||||
|
||||
func (err ErrRepoFileDoesNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrFilenameInvalid represents a "FilenameInvalid" kind of error.
|
||||
type ErrFilenameInvalid struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// IsErrFilenameInvalid checks if an error is an ErrFilenameInvalid.
|
||||
func IsErrFilenameInvalid(err error) bool {
|
||||
_, ok := err.(ErrFilenameInvalid)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrFilenameInvalid) Error() string {
|
||||
return fmt.Sprintf("path contains a malformed path component [path: %s]", err.Path)
|
||||
}
|
||||
|
||||
func (err ErrFilenameInvalid) Unwrap() error {
|
||||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
||||
// ErrUserCannotCommit represents "UserCannotCommit" kind of error.
|
||||
type ErrUserCannotCommit struct {
|
||||
UserName string
|
||||
}
|
||||
|
||||
// IsErrUserCannotCommit checks if an error is an ErrUserCannotCommit.
|
||||
func IsErrUserCannotCommit(err error) bool {
|
||||
_, ok := err.(ErrUserCannotCommit)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserCannotCommit) Error() string {
|
||||
return fmt.Sprintf("user cannot commit to repo [user: %s]", err.UserName)
|
||||
}
|
||||
|
||||
func (err ErrUserCannotCommit) Unwrap() error {
|
||||
return util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// ErrFilePathInvalid represents a "FilePathInvalid" kind of error.
|
||||
type ErrFilePathInvalid struct {
|
||||
Message string
|
||||
Path string
|
||||
Name string
|
||||
Type git.EntryMode
|
||||
}
|
||||
|
||||
// IsErrFilePathInvalid checks if an error is an ErrFilePathInvalid.
|
||||
func IsErrFilePathInvalid(err error) bool {
|
||||
_, ok := err.(ErrFilePathInvalid)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrFilePathInvalid) Error() string {
|
||||
if err.Message != "" {
|
||||
return err.Message
|
||||
}
|
||||
return fmt.Sprintf("path is invalid [path: %s]", err.Path)
|
||||
}
|
||||
|
||||
func (err ErrFilePathInvalid) Unwrap() error {
|
||||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
||||
// ErrFilePathProtected represents a "FilePathProtected" kind of error.
|
||||
type ErrFilePathProtected struct {
|
||||
Message string
|
||||
Path string
|
||||
}
|
||||
|
||||
// IsErrFilePathProtected checks if an error is an ErrFilePathProtected.
|
||||
func IsErrFilePathProtected(err error) bool {
|
||||
_, ok := err.(ErrFilePathProtected)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrFilePathProtected) Error() string {
|
||||
if err.Message != "" {
|
||||
return err.Message
|
||||
}
|
||||
return fmt.Sprintf("path is protected and can not be changed [path: %s]", err.Path)
|
||||
}
|
||||
|
||||
func (err ErrFilePathProtected) Unwrap() error {
|
||||
return util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// ErrDisallowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it.
|
||||
type ErrDisallowedToMerge struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
// IsErrDisallowedToMerge checks if an error is an ErrDisallowedToMerge.
|
||||
func IsErrDisallowedToMerge(err error) bool {
|
||||
_, ok := err.(ErrDisallowedToMerge)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrDisallowedToMerge) Error() string {
|
||||
return fmt.Sprintf("not allowed to merge [reason: %s]", err.Reason)
|
||||
}
|
||||
|
||||
func (err ErrDisallowedToMerge) Unwrap() error {
|
||||
return util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// ErrTagAlreadyExists represents an error that tag with such name already exists.
|
||||
type ErrTagAlreadyExists struct {
|
||||
TagName string
|
||||
}
|
||||
|
||||
// IsErrTagAlreadyExists checks if an error is an ErrTagAlreadyExists.
|
||||
func IsErrTagAlreadyExists(err error) bool {
|
||||
_, ok := err.(ErrTagAlreadyExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrTagAlreadyExists) Error() string {
|
||||
return fmt.Sprintf("tag already exists [name: %s]", err.TagName)
|
||||
}
|
||||
|
||||
func (err ErrTagAlreadyExists) Unwrap() error {
|
||||
return util.ErrAlreadyExist
|
||||
}
|
||||
|
||||
// ErrSHADoesNotMatch represents a "SHADoesNotMatch" kind of error.
|
||||
type ErrSHADoesNotMatch struct {
|
||||
Path string
|
||||
GivenSHA string
|
||||
CurrentSHA string
|
||||
}
|
||||
|
||||
// IsErrSHADoesNotMatch checks if an error is a ErrSHADoesNotMatch.
|
||||
func IsErrSHADoesNotMatch(err error) bool {
|
||||
_, ok := err.(ErrSHADoesNotMatch)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrSHADoesNotMatch) Error() string {
|
||||
return fmt.Sprintf("sha does not match [given: %s, expected: %s]", err.GivenSHA, err.CurrentSHA)
|
||||
}
|
||||
|
||||
// ErrSHANotFound represents a "SHADoesNotMatch" kind of error.
|
||||
type ErrSHANotFound struct {
|
||||
SHA string
|
||||
}
|
||||
|
||||
// IsErrSHANotFound checks if an error is a ErrSHANotFound.
|
||||
func IsErrSHANotFound(err error) bool {
|
||||
_, ok := err.(ErrSHANotFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrSHANotFound) Error() string {
|
||||
return fmt.Sprintf("sha not found [%s]", err.SHA)
|
||||
}
|
||||
|
||||
func (err ErrSHANotFound) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrCommitIDDoesNotMatch represents a "CommitIDDoesNotMatch" kind of error.
|
||||
type ErrCommitIDDoesNotMatch struct {
|
||||
GivenCommitID string
|
||||
CurrentCommitID string
|
||||
}
|
||||
|
||||
// IsErrCommitIDDoesNotMatch checks if an error is a ErrCommitIDDoesNotMatch.
|
||||
func IsErrCommitIDDoesNotMatch(err error) bool {
|
||||
_, ok := err.(ErrCommitIDDoesNotMatch)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrCommitIDDoesNotMatch) Error() string {
|
||||
return fmt.Sprintf("file CommitID does not match [given: %s, expected: %s]", err.GivenCommitID, err.CurrentCommitID)
|
||||
}
|
||||
|
||||
// ErrSHAOrCommitIDNotProvided represents a "SHAOrCommitIDNotProvided" kind of error.
|
||||
type ErrSHAOrCommitIDNotProvided struct{}
|
||||
|
||||
// IsErrSHAOrCommitIDNotProvided checks if an error is a ErrSHAOrCommitIDNotProvided.
|
||||
func IsErrSHAOrCommitIDNotProvided(err error) bool {
|
||||
_, ok := err.(ErrSHAOrCommitIDNotProvided)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrSHAOrCommitIDNotProvided) Error() string {
|
||||
return "a SHA or commit ID must be proved when updating a file"
|
||||
}
|
||||
|
||||
// ErrInvalidMergeStyle represents an error if merging with disabled merge strategy
|
||||
type ErrInvalidMergeStyle struct {
|
||||
ID int64
|
||||
Style repo_model.MergeStyle
|
||||
}
|
||||
|
||||
// IsErrInvalidMergeStyle checks if an error is a ErrInvalidMergeStyle.
|
||||
func IsErrInvalidMergeStyle(err error) bool {
|
||||
_, ok := err.(ErrInvalidMergeStyle)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidMergeStyle) Error() string {
|
||||
return fmt.Sprintf("merge strategy is not allowed or is invalid [repo_id: %d, strategy: %s]",
|
||||
err.ID, err.Style)
|
||||
}
|
||||
|
||||
func (err ErrInvalidMergeStyle) Unwrap() error {
|
||||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
||||
// ErrMergeConflicts represents an error if merging fails with a conflict
|
||||
type ErrMergeConflicts struct {
|
||||
Style repo_model.MergeStyle
|
||||
StdOut string
|
||||
StdErr string
|
||||
Err error
|
||||
}
|
||||
|
||||
// IsErrMergeConflicts checks if an error is a ErrMergeConflicts.
|
||||
func IsErrMergeConflicts(err error) bool {
|
||||
_, ok := err.(ErrMergeConflicts)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrMergeConflicts) Error() string {
|
||||
return fmt.Sprintf("Merge Conflict Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
|
||||
}
|
||||
|
||||
// ErrMergeUnrelatedHistories represents an error if merging fails due to unrelated histories
|
||||
type ErrMergeUnrelatedHistories struct {
|
||||
Style repo_model.MergeStyle
|
||||
StdOut string
|
||||
StdErr string
|
||||
Err error
|
||||
}
|
||||
|
||||
// IsErrMergeUnrelatedHistories checks if an error is a ErrMergeUnrelatedHistories.
|
||||
func IsErrMergeUnrelatedHistories(err error) bool {
|
||||
_, ok := err.(ErrMergeUnrelatedHistories)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrMergeUnrelatedHistories) Error() string {
|
||||
return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
|
||||
}
|
||||
|
||||
// ErrMergeDivergingFastForwardOnly represents an error if a fast-forward-only merge fails because the branches diverge
|
||||
type ErrMergeDivergingFastForwardOnly struct {
|
||||
StdOut string
|
||||
StdErr string
|
||||
Err error
|
||||
}
|
||||
|
||||
// IsErrMergeDivergingFastForwardOnly checks if an error is a ErrMergeDivergingFastForwardOnly.
|
||||
func IsErrMergeDivergingFastForwardOnly(err error) bool {
|
||||
_, ok := err.(ErrMergeDivergingFastForwardOnly)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrMergeDivergingFastForwardOnly) Error() string {
|
||||
return fmt.Sprintf("Merge DivergingFastForwardOnly Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
|
||||
}
|
||||
|
||||
// ErrRebaseConflicts represents an error if rebase fails with a conflict
|
||||
type ErrRebaseConflicts struct {
|
||||
Style repo_model.MergeStyle
|
||||
CommitSHA string
|
||||
StdOut string
|
||||
StdErr string
|
||||
Err error
|
||||
}
|
||||
|
||||
// IsErrRebaseConflicts checks if an error is a ErrRebaseConflicts.
|
||||
func IsErrRebaseConflicts(err error) bool {
|
||||
_, ok := err.(ErrRebaseConflicts)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrRebaseConflicts) Error() string {
|
||||
return fmt.Sprintf("Rebase Error: %v: Whilst Rebasing: %s\n%s\n%s", err.Err, err.CommitSHA, err.StdErr, err.StdOut)
|
||||
}
|
||||
|
||||
// ErrPullRequestHasMerged represents a "PullRequestHasMerged"-error
|
||||
type ErrPullRequestHasMerged struct {
|
||||
ID int64
|
||||
IssueID int64
|
||||
HeadRepoID int64
|
||||
BaseRepoID int64
|
||||
HeadBranch string
|
||||
BaseBranch string
|
||||
}
|
||||
|
||||
// IsErrPullRequestHasMerged checks if an error is a ErrPullRequestHasMerged.
|
||||
func IsErrPullRequestHasMerged(err error) bool {
|
||||
_, ok := err.(ErrPullRequestHasMerged)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Error does pretty-printing :D
|
||||
func (err ErrPullRequestHasMerged) Error() string {
|
||||
return fmt.Sprintf("pull request has merged [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
|
||||
err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
|
||||
}
|
@ -13,9 +13,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/testlogger"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -92,10 +92,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
|
||||
func MainTest(m *testing.M) {
|
||||
testlogger.Init()
|
||||
|
||||
giteaRoot := base.SetupGiteaRoot()
|
||||
if giteaRoot == "" {
|
||||
testlogger.Fatalf("Environment variable $GITEA_ROOT not set\n")
|
||||
}
|
||||
giteaRoot := test.SetupGiteaRoot()
|
||||
giteaBinary := "gitea"
|
||||
if runtime.GOOS == "windows" {
|
||||
giteaBinary += ".exe"
|
||||
|
@ -36,6 +36,21 @@ func init() {
|
||||
db.RegisterModel(new(OrgUser))
|
||||
}
|
||||
|
||||
// ErrUserHasOrgs represents a "UserHasOrgs" kind of error.
|
||||
type ErrUserHasOrgs struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrUserHasOrgs checks if an error is a ErrUserHasOrgs.
|
||||
func IsErrUserHasOrgs(err error) bool {
|
||||
_, ok := err.(ErrUserHasOrgs)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserHasOrgs) Error() string {
|
||||
return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// GetOrganizationCount returns count of membership of organization of the user.
|
||||
func GetOrganizationCount(ctx context.Context, u *user_model.User) (int64, error) {
|
||||
return db.GetEngine(ctx).
|
||||
|
@ -301,6 +301,21 @@ func FindUnreferencedPackages(ctx context.Context) ([]*Package, error) {
|
||||
Find(&ps)
|
||||
}
|
||||
|
||||
// ErrUserOwnPackages notifies that the user (still) owns the packages.
|
||||
type ErrUserOwnPackages struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrUserOwnPackages checks if an error is an ErrUserOwnPackages.
|
||||
func IsErrUserOwnPackages(err error) bool {
|
||||
_, ok := err.(ErrUserOwnPackages)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserOwnPackages) Error() string {
|
||||
return fmt.Sprintf("user still has ownership of packages [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// HasOwnerPackages tests if a user/org has accessible packages
|
||||
func HasOwnerPackages(ctx context.Context, ownerID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
|
@ -37,7 +37,7 @@ type ErrUserDoesNotHaveAccessToRepo struct {
|
||||
RepoName string
|
||||
}
|
||||
|
||||
// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists.
|
||||
// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrUserDoesNotHaveAccessToRepo.
|
||||
func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
|
||||
_, ok := err.(ErrUserDoesNotHaveAccessToRepo)
|
||||
return ok
|
||||
@ -866,6 +866,21 @@ func (repo *Repository) TemplateRepo(ctx context.Context) *Repository {
|
||||
return repo
|
||||
}
|
||||
|
||||
// ErrUserOwnRepos represents a "UserOwnRepos" kind of error.
|
||||
type ErrUserOwnRepos struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrUserOwnRepos checks if an error is a ErrUserOwnRepos.
|
||||
func IsErrUserOwnRepos(err error) bool {
|
||||
_, ok := err.(ErrUserOwnRepos)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserOwnRepos) Error() string {
|
||||
return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
type CountRepositoryOptions struct {
|
||||
OwnerID int64
|
||||
Private optional.Option[bool]
|
||||
|
@ -14,13 +14,13 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/system"
|
||||
"code.gitea.io/gitea/modules/auth/password/hash"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/setting/config"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -206,7 +206,7 @@ func CreateTestEngine(opts FixturesOptions) error {
|
||||
x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate")
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "unknown driver") {
|
||||
return fmt.Errorf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
|
||||
return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -235,5 +235,5 @@ func PrepareTestEnv(t testing.TB) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta")
|
||||
assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath))
|
||||
base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
|
||||
test.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
|
||||
}
|
||||
|
@ -788,6 +788,21 @@ func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, o
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// ErrDeleteLastAdminUser represents a "DeleteLastAdminUser" kind of error.
|
||||
type ErrDeleteLastAdminUser struct {
|
||||
UID int64
|
||||
}
|
||||
|
||||
// IsErrDeleteLastAdminUser checks if an error is a ErrDeleteLastAdminUser.
|
||||
func IsErrDeleteLastAdminUser(err error) bool {
|
||||
_, ok := err.(ErrDeleteLastAdminUser)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrDeleteLastAdminUser) Error() string {
|
||||
return fmt.Sprintf("can not delete the last admin user [uid: %d]", err.UID)
|
||||
}
|
||||
|
||||
// IsLastAdminUser check whether user is the last admin
|
||||
func IsLastAdminUser(ctx context.Context, user *User) bool {
|
||||
if user.IsAdmin && CountUsers(ctx, &CountUserFilter{IsAdmin: optional.Some(true)}) <= 1 {
|
||||
|
@ -1,9 +0,0 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
type (
|
||||
// TplName template relative path type
|
||||
TplName string
|
||||
)
|
@ -13,9 +13,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -189,49 +186,3 @@ func EntryIcon(entry *git.TreeEntry) string {
|
||||
|
||||
return "file"
|
||||
}
|
||||
|
||||
// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
|
||||
func SetupGiteaRoot() string {
|
||||
giteaRoot := os.Getenv("GITEA_ROOT")
|
||||
if giteaRoot == "" {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
giteaRoot = strings.TrimSuffix(filename, "modules/base/tool.go")
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
rel, err := filepath.Rel(giteaRoot, wd)
|
||||
if err != nil && strings.HasPrefix(filepath.ToSlash(rel), "../") {
|
||||
giteaRoot = wd
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(giteaRoot, "gitea")); os.IsNotExist(err) {
|
||||
giteaRoot = ""
|
||||
} else if err := os.Setenv("GITEA_ROOT", giteaRoot); err != nil {
|
||||
giteaRoot = ""
|
||||
}
|
||||
}
|
||||
return giteaRoot
|
||||
}
|
||||
|
||||
// FormatNumberSI format a number
|
||||
func FormatNumberSI(data any) string {
|
||||
var num int64
|
||||
if num1, ok := data.(int64); ok {
|
||||
num = num1
|
||||
} else if num1, ok := data.(int); ok {
|
||||
num = int64(num1)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
if num < 1000 {
|
||||
return fmt.Sprintf("%d", num)
|
||||
} else if num < 1000000 {
|
||||
num2 := float32(num) / float32(1000.0)
|
||||
return fmt.Sprintf("%.1fk", num2)
|
||||
} else if num < 1000000000 {
|
||||
num2 := float32(num) / float32(1000000.0)
|
||||
return fmt.Sprintf("%.1fM", num2)
|
||||
}
|
||||
num2 := float32(num) / float32(1000000000.0)
|
||||
return fmt.Sprintf("%.1fG", num2)
|
||||
}
|
||||
|
@ -169,18 +169,3 @@ func TestInt64sToStrings(t *testing.T) {
|
||||
}
|
||||
|
||||
// TODO: Test EntryIcon
|
||||
|
||||
func TestSetupGiteaRoot(t *testing.T) {
|
||||
t.Setenv("GITEA_ROOT", "test")
|
||||
assert.Equal(t, "test", SetupGiteaRoot())
|
||||
t.Setenv("GITEA_ROOT", "")
|
||||
assert.NotEqual(t, "test", SetupGiteaRoot())
|
||||
}
|
||||
|
||||
func TestFormatNumberSI(t *testing.T) {
|
||||
assert.Equal(t, "125", FormatNumberSI(int(125)))
|
||||
assert.Equal(t, "1.3k", FormatNumberSI(int64(1317)))
|
||||
assert.Equal(t, "21.3M", FormatNumberSI(21317675))
|
||||
assert.Equal(t, "45.7G", FormatNumberSI(45721317675))
|
||||
assert.Equal(t, "", FormatNumberSI("test"))
|
||||
}
|
||||
|
@ -7,10 +7,8 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -32,7 +30,6 @@ type WriteCloserError interface {
|
||||
func ensureValidGitRepository(ctx context.Context, repoPath string) error {
|
||||
stderr := strings.Builder{}
|
||||
err := NewCommand(ctx, "rev-parse").
|
||||
SetDescription(fmt.Sprintf("%s rev-parse [repo_path: %s]", GitExecutable, repoPath)).
|
||||
Run(&RunOpts{
|
||||
Dir: repoPath,
|
||||
Stderr: &stderr,
|
||||
@ -62,13 +59,9 @@ func catFileBatchCheck(ctx context.Context, repoPath string) (WriteCloserError,
|
||||
cancel()
|
||||
}()
|
||||
|
||||
_, filename, line, _ := runtime.Caller(2)
|
||||
filename = strings.TrimPrefix(filename, callerPrefix)
|
||||
|
||||
go func() {
|
||||
stderr := strings.Builder{}
|
||||
err := NewCommand(ctx, "cat-file", "--batch-check").
|
||||
SetDescription(fmt.Sprintf("%s cat-file --batch-check [repo_path: %s] (%s:%d)", GitExecutable, repoPath, filename, line)).
|
||||
Run(&RunOpts{
|
||||
Dir: repoPath,
|
||||
Stdin: batchStdinReader,
|
||||
@ -114,13 +107,9 @@ func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi
|
||||
cancel()
|
||||
}()
|
||||
|
||||
_, filename, line, _ := runtime.Caller(2)
|
||||
filename = strings.TrimPrefix(filename, callerPrefix)
|
||||
|
||||
go func() {
|
||||
stderr := strings.Builder{}
|
||||
err := NewCommand(ctx, "cat-file", "--batch").
|
||||
SetDescription(fmt.Sprintf("%s cat-file --batch [repo_path: %s] (%s:%d)", GitExecutable, repoPath, filename, line)).
|
||||
Run(&RunOpts{
|
||||
Dir: repoPath,
|
||||
Stdin: batchStdinReader,
|
||||
@ -320,13 +309,6 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu
|
||||
return mode, fname, sha, n, err
|
||||
}
|
||||
|
||||
var callerPrefix string
|
||||
|
||||
func init() {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
callerPrefix = strings.TrimSuffix(filename, "modules/git/batch_reader.go")
|
||||
}
|
||||
|
||||
func DiscardFull(rd *bufio.Reader, discard int64) error {
|
||||
if discard > math.MaxInt32 {
|
||||
n, err := rd.Discard(math.MaxInt32)
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
@ -142,9 +141,7 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath
|
||||
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
|
||||
cmd.AddOptionValues("--ignore-revs-file", *ignoreRevsFile)
|
||||
}
|
||||
cmd.AddDynamicArguments(commit.ID.String()).
|
||||
AddDashesAndList(file).
|
||||
SetDescription(fmt.Sprintf("GetBlame [repo_path: %s]", repoPath))
|
||||
cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
|
||||
reader, stdout, err := os.Pipe()
|
||||
if err != nil {
|
||||
if ignoreRevsFile != nil {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@ -43,18 +44,24 @@ type Command struct {
|
||||
prog string
|
||||
args []string
|
||||
parentContext context.Context
|
||||
desc string
|
||||
globalArgsLength int
|
||||
brokenArgs []string
|
||||
}
|
||||
|
||||
func (c *Command) String() string {
|
||||
return c.toString(false)
|
||||
func logArgSanitize(arg string) string {
|
||||
if strings.Contains(arg, "://") && strings.Contains(arg, "@") {
|
||||
return util.SanitizeCredentialURLs(arg)
|
||||
} else if filepath.IsAbs(arg) {
|
||||
base := filepath.Base(arg)
|
||||
dir := filepath.Dir(arg)
|
||||
return filepath.Join(filepath.Base(dir), base)
|
||||
}
|
||||
return arg
|
||||
}
|
||||
|
||||
func (c *Command) toString(sanitizing bool) string {
|
||||
func (c *Command) LogString() string {
|
||||
// WARNING: this function is for debugging purposes only. It's much better than old code (which only joins args with space),
|
||||
// It's impossible to make a simple and 100% correct implementation of argument quoting for different platforms.
|
||||
// It's impossible to make a simple and 100% correct implementation of argument quoting for different platforms here.
|
||||
debugQuote := func(s string) string {
|
||||
if strings.ContainsAny(s, " `'\"\t\r\n") {
|
||||
return fmt.Sprintf("%q", s)
|
||||
@ -63,12 +70,11 @@ func (c *Command) toString(sanitizing bool) string {
|
||||
}
|
||||
a := make([]string, 0, len(c.args)+1)
|
||||
a = append(a, debugQuote(c.prog))
|
||||
for _, arg := range c.args {
|
||||
if sanitizing && (strings.Contains(arg, "://") && strings.Contains(arg, "@")) {
|
||||
a = append(a, debugQuote(util.SanitizeCredentialURLs(arg)))
|
||||
} else {
|
||||
a = append(a, debugQuote(arg))
|
||||
}
|
||||
if c.globalArgsLength > 0 {
|
||||
a = append(a, "...global...")
|
||||
}
|
||||
for i := c.globalArgsLength; i < len(c.args); i++ {
|
||||
a = append(a, debugQuote(logArgSanitize(c.args[i])))
|
||||
}
|
||||
return strings.Join(a, " ")
|
||||
}
|
||||
@ -112,12 +118,6 @@ func (c *Command) SetParentContext(ctx context.Context) *Command {
|
||||
return c
|
||||
}
|
||||
|
||||
// SetDescription sets the description for this command which be returned on c.String()
|
||||
func (c *Command) SetDescription(desc string) *Command {
|
||||
c.desc = desc
|
||||
return c
|
||||
}
|
||||
|
||||
// isSafeArgumentValue checks if the argument is safe to be used as a value (not an option)
|
||||
func isSafeArgumentValue(s string) bool {
|
||||
return s == "" || s[0] != '-'
|
||||
@ -271,8 +271,12 @@ var ErrBrokenCommand = errors.New("git command is broken")
|
||||
|
||||
// Run runs the command with the RunOpts
|
||||
func (c *Command) Run(opts *RunOpts) error {
|
||||
return c.run(1, opts)
|
||||
}
|
||||
|
||||
func (c *Command) run(skip int, opts *RunOpts) error {
|
||||
if len(c.brokenArgs) != 0 {
|
||||
log.Error("git command is broken: %s, broken args: %s", c.String(), strings.Join(c.brokenArgs, " "))
|
||||
log.Error("git command is broken: %s, broken args: %s", c.LogString(), strings.Join(c.brokenArgs, " "))
|
||||
return ErrBrokenCommand
|
||||
}
|
||||
if opts == nil {
|
||||
@ -285,20 +289,14 @@ func (c *Command) Run(opts *RunOpts) error {
|
||||
timeout = defaultCommandExecutionTimeout
|
||||
}
|
||||
|
||||
if len(opts.Dir) == 0 {
|
||||
log.Debug("git.Command.Run: %s", c)
|
||||
} else {
|
||||
log.Debug("git.Command.RunDir(%s): %s", opts.Dir, c)
|
||||
}
|
||||
|
||||
desc := c.desc
|
||||
if desc == "" {
|
||||
if opts.Dir == "" {
|
||||
desc = fmt.Sprintf("git: %s", c.toString(true))
|
||||
} else {
|
||||
desc = fmt.Sprintf("git(dir:%s): %s", opts.Dir, c.toString(true))
|
||||
}
|
||||
var desc string
|
||||
callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */)
|
||||
if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 {
|
||||
callerInfo = callerInfo[pos+1:]
|
||||
}
|
||||
// these logs are for debugging purposes only, so no guarantee of correctness or stability
|
||||
desc = fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), c.LogString())
|
||||
log.Debug("git.Command: %s", desc)
|
||||
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
@ -401,7 +399,7 @@ func IsErrorExitCode(err error, code int) bool {
|
||||
|
||||
// RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr).
|
||||
func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr RunStdError) {
|
||||
stdoutBytes, stderrBytes, err := c.RunStdBytes(opts)
|
||||
stdoutBytes, stderrBytes, err := c.runStdBytes(opts)
|
||||
stdout = util.UnsafeBytesToString(stdoutBytes)
|
||||
stderr = util.UnsafeBytesToString(stderrBytes)
|
||||
if err != nil {
|
||||
@ -413,6 +411,10 @@ func (c *Command) RunStdString(opts *RunOpts) (stdout, stderr string, runErr Run
|
||||
|
||||
// RunStdBytes runs the command with options and returns stdout/stderr as bytes. and store stderr to returned error (err combined with stderr).
|
||||
func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) {
|
||||
return c.runStdBytes(opts)
|
||||
}
|
||||
|
||||
func (c *Command) runStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) {
|
||||
if opts == nil {
|
||||
opts = &RunOpts{}
|
||||
}
|
||||
@ -435,7 +437,7 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS
|
||||
PipelineFunc: opts.PipelineFunc,
|
||||
}
|
||||
|
||||
err := c.Run(newOpts)
|
||||
err := c.run(2, newOpts)
|
||||
stderr = stderrBuf.Bytes()
|
||||
if err != nil {
|
||||
return nil, stderr, &runStdError{err: err, stderr: util.UnsafeBytesToString(stderr)}
|
||||
|
@ -55,8 +55,8 @@ func TestGitArgument(t *testing.T) {
|
||||
|
||||
func TestCommandString(t *testing.T) {
|
||||
cmd := NewCommandContextNoGlobals(context.Background(), "a", "-m msg", "it's a test", `say "hello"`)
|
||||
assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.String())
|
||||
assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString())
|
||||
|
||||
cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/")
|
||||
assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/"`, cmd.toString(true))
|
||||
cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/", "/root/dir-a/dir-b")
|
||||
assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" dir-a/dir-b`, cmd.LogString())
|
||||
}
|
||||
|
@ -5,8 +5,12 @@ package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// GetRemoteAddress returns remote url of git repository in the repoPath with special remote name
|
||||
@ -37,3 +41,61 @@ func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.Git
|
||||
}
|
||||
return giturl.Parse(addr)
|
||||
}
|
||||
|
||||
// ErrInvalidCloneAddr represents a "InvalidCloneAddr" kind of error.
|
||||
type ErrInvalidCloneAddr struct {
|
||||
Host string
|
||||
IsURLError bool
|
||||
IsInvalidPath bool
|
||||
IsProtocolInvalid bool
|
||||
IsPermissionDenied bool
|
||||
LocalPath bool
|
||||
}
|
||||
|
||||
// IsErrInvalidCloneAddr checks if an error is a ErrInvalidCloneAddr.
|
||||
func IsErrInvalidCloneAddr(err error) bool {
|
||||
_, ok := err.(*ErrInvalidCloneAddr)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrInvalidCloneAddr) Error() string {
|
||||
if err.IsInvalidPath {
|
||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided path is invalid", err.Host)
|
||||
}
|
||||
if err.IsProtocolInvalid {
|
||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url protocol is not allowed", err.Host)
|
||||
}
|
||||
if err.IsPermissionDenied {
|
||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed.", err.Host)
|
||||
}
|
||||
if err.IsURLError {
|
||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed: the provided url is invalid", err.Host)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("migration/cloning from '%s' is not allowed", err.Host)
|
||||
}
|
||||
|
||||
func (err *ErrInvalidCloneAddr) Unwrap() error {
|
||||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
||||
// ParseRemoteAddr checks if given remote address is valid,
|
||||
// and returns composed URL with needed username and password.
|
||||
func ParseRemoteAddr(remoteAddr, authUsername, authPassword string) (string, error) {
|
||||
remoteAddr = strings.TrimSpace(remoteAddr)
|
||||
// Remote address can be HTTP/HTTPS/Git URL or local path.
|
||||
if strings.HasPrefix(remoteAddr, "http://") ||
|
||||
strings.HasPrefix(remoteAddr, "https://") ||
|
||||
strings.HasPrefix(remoteAddr, "git://") {
|
||||
u, err := url.Parse(remoteAddr)
|
||||
if err != nil {
|
||||
return "", &ErrInvalidCloneAddr{IsURLError: true, Host: remoteAddr}
|
||||
}
|
||||
if len(authUsername)+len(authPassword) > 0 {
|
||||
u.User = url.UserPassword(authUsername, authPassword)
|
||||
}
|
||||
remoteAddr = u.String()
|
||||
}
|
||||
|
||||
return remoteAddr, nil
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/proxy"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// GPGSettings represents the default GPG settings for this repository
|
||||
@ -160,12 +159,6 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op
|
||||
}
|
||||
cmd.AddDashesAndList(from, to)
|
||||
|
||||
if strings.Contains(from, "://") && strings.Contains(from, "@") {
|
||||
cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, util.SanitizeCredentialURLs(from), to, opts.Shared, opts.Mirror, opts.Depth))
|
||||
} else {
|
||||
cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, from, to, opts.Shared, opts.Mirror, opts.Depth))
|
||||
}
|
||||
|
||||
if opts.Timeout <= 0 {
|
||||
opts.Timeout = -1
|
||||
}
|
||||
@ -213,12 +206,6 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error {
|
||||
}
|
||||
cmd.AddDashesAndList(remoteBranchArgs...)
|
||||
|
||||
if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") {
|
||||
cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, util.SanitizeCredentialURLs(opts.Remote), opts.Force, opts.Mirror))
|
||||
} else {
|
||||
cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, opts.Remote, opts.Force, opts.Mirror))
|
||||
}
|
||||
|
||||
stdout, stderr, err := cmd.RunStdString(&RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath})
|
||||
if err != nil {
|
||||
if strings.Contains(stderr, "non-fast-forward") {
|
||||
|
@ -216,8 +216,6 @@ type CommitsByFileAndRangeOptions struct {
|
||||
|
||||
// CommitsByFileAndRange return the commits according revision file and the page
|
||||
func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) {
|
||||
skip := (opts.Page - 1) * setting.Git.CommitsRangeSize
|
||||
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
defer func() {
|
||||
_ = stdoutReader.Close()
|
||||
@ -226,8 +224,8 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions)
|
||||
go func() {
|
||||
stderr := strings.Builder{}
|
||||
gitCmd := NewCommand(repo.Ctx, "rev-list").
|
||||
AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize*opts.Page).
|
||||
AddOptionFormat("--skip=%d", skip)
|
||||
AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize).
|
||||
AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize)
|
||||
gitCmd.AddDynamicArguments(opts.Revision)
|
||||
|
||||
if opts.Not != "" {
|
||||
|
@ -8,7 +8,11 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepository_GetCommitBranches(t *testing.T) {
|
||||
@ -126,3 +130,21 @@ func TestGetRefCommitID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitsByFileAndRange(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Git.CommitsRangeSize, 2)()
|
||||
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||
bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path)
|
||||
require.NoError(t, err)
|
||||
defer bareRepo1.Close()
|
||||
|
||||
// "foo" has 3 commits in "master" branch
|
||||
commits, err := bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, commits, 2)
|
||||
|
||||
commits, err = bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 2})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, commits, 1)
|
||||
}
|
||||
|
@ -233,72 +233,34 @@ func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int,
|
||||
return numFiles, totalAdditions, totalDeletions, err
|
||||
}
|
||||
|
||||
// GetDiffOrPatch generates either diff or formatted patch data between given revisions
|
||||
func (repo *Repository) GetDiffOrPatch(base, head string, w io.Writer, patch, binary bool) error {
|
||||
if patch {
|
||||
return repo.GetPatch(base, head, w)
|
||||
}
|
||||
if binary {
|
||||
return repo.GetDiffBinary(base, head, w)
|
||||
}
|
||||
return repo.GetDiff(base, head, w)
|
||||
}
|
||||
|
||||
// GetDiff generates and returns patch data between given revisions, optimized for human readability
|
||||
func (repo *Repository) GetDiff(base, head string, w io.Writer) error {
|
||||
func (repo *Repository) GetDiff(compareArg string, w io.Writer) error {
|
||||
stderr := new(bytes.Buffer)
|
||||
err := NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base + "..." + head).
|
||||
return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(compareArg).
|
||||
Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
Stderr: stderr,
|
||||
})
|
||||
if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) {
|
||||
return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base, head).
|
||||
Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDiffBinary generates and returns patch data between given revisions, including binary diffs.
|
||||
func (repo *Repository) GetDiffBinary(base, head string, w io.Writer) error {
|
||||
stderr := new(bytes.Buffer)
|
||||
err := NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base + "..." + head).
|
||||
Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
Stderr: stderr,
|
||||
})
|
||||
if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) {
|
||||
return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base, head).
|
||||
Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
})
|
||||
}
|
||||
return err
|
||||
func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error {
|
||||
return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(compareArg).Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply`
|
||||
func (repo *Repository) GetPatch(base, head string, w io.Writer) error {
|
||||
func (repo *Repository) GetPatch(compareArg string, w io.Writer) error {
|
||||
stderr := new(bytes.Buffer)
|
||||
err := NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(base + "..." + head).
|
||||
return NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg).
|
||||
Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
Stderr: stderr,
|
||||
})
|
||||
if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) {
|
||||
return NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(base, head).
|
||||
Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetFilesChangedBetween returns a list of all files that have been changed between the given commits
|
||||
@ -329,21 +291,6 @@ func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, err
|
||||
return split, err
|
||||
}
|
||||
|
||||
// GetDiffFromMergeBase generates and return patch data from merge base to head
|
||||
func (repo *Repository) GetDiffFromMergeBase(base, head string, w io.Writer) error {
|
||||
stderr := new(bytes.Buffer)
|
||||
err := NewCommand(repo.Ctx, "diff", "-p", "--binary").AddDynamicArguments(base + "..." + head).
|
||||
Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
Stderr: stderr,
|
||||
})
|
||||
if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) {
|
||||
return repo.GetDiffBinary(base, head, w)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadPatchCommit will check if a diff patch exists and return stats
|
||||
func (repo *Repository) ReadPatchCommit(prID int64) (commitSHA string, err error) {
|
||||
// Migrated repositories download patches to "pulls" location
|
||||
|
@ -28,7 +28,7 @@ func TestGetFormatPatch(t *testing.T) {
|
||||
defer repo.Close()
|
||||
|
||||
rd := &bytes.Buffer{}
|
||||
err = repo.GetPatch("8d92fc95^", "8d92fc95", rd)
|
||||
err = repo.GetPatch("8d92fc95^...8d92fc95", rd)
|
||||
if err != nil {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
@ -38,63 +39,32 @@ func OpenWikiRepository(ctx context.Context, repo Repository) (*git.Repository,
|
||||
|
||||
// contextKey is a value for use with context.WithValue.
|
||||
type contextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// RepositoryContextKey is a context key. It is used with context.Value() to get the current Repository for the context
|
||||
var RepositoryContextKey = &contextKey{"repository"}
|
||||
|
||||
// RepositoryFromContext attempts to get the repository from the context
|
||||
func repositoryFromContext(ctx context.Context, repo Repository) *git.Repository {
|
||||
value := ctx.Value(RepositoryContextKey)
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if gitRepo, ok := value.(*git.Repository); ok && gitRepo != nil {
|
||||
if gitRepo.Path == repoPath(repo) {
|
||||
return gitRepo
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
repoPath string
|
||||
}
|
||||
|
||||
// RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it
|
||||
func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) {
|
||||
gitRepo := repositoryFromContext(ctx, repo)
|
||||
if gitRepo != nil {
|
||||
return gitRepo, util.NopCloser{}, nil
|
||||
ds := reqctx.GetRequestDataStore(ctx)
|
||||
if ds != nil {
|
||||
gitRepo, err := RepositoryFromRequestContextOrOpen(ctx, ds, repo)
|
||||
return gitRepo, util.NopCloser{}, err
|
||||
}
|
||||
|
||||
gitRepo, err := OpenRepository(ctx, repo)
|
||||
return gitRepo, gitRepo, err
|
||||
}
|
||||
|
||||
// repositoryFromContextPath attempts to get the repository from the context
|
||||
func repositoryFromContextPath(ctx context.Context, path string) *git.Repository {
|
||||
value := ctx.Value(RepositoryContextKey)
|
||||
if value == nil {
|
||||
return nil
|
||||
// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context
|
||||
// The repo will be automatically closed when the request context is done
|
||||
func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDataStore, repo Repository) (*git.Repository, error) {
|
||||
ck := contextKey{repoPath: repoPath(repo)}
|
||||
if gitRepo, ok := ctx.Value(ck).(*git.Repository); ok {
|
||||
return gitRepo, nil
|
||||
}
|
||||
|
||||
if repo, ok := value.(*git.Repository); ok && repo != nil {
|
||||
if repo.Path == path {
|
||||
return repo
|
||||
}
|
||||
gitRepo, err := git.OpenRepository(ctx, ck.repoPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RepositoryFromContextOrOpenPath attempts to get the repository from the context or just opens it
|
||||
// Deprecated: Use RepositoryFromContextOrOpen instead
|
||||
func RepositoryFromContextOrOpenPath(ctx context.Context, path string) (*git.Repository, io.Closer, error) {
|
||||
gitRepo := repositoryFromContextPath(ctx, path)
|
||||
if gitRepo != nil {
|
||||
return gitRepo, util.NopCloser{}, nil
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, path)
|
||||
return gitRepo, gitRepo, err
|
||||
ds.AddCloser(gitRepo)
|
||||
ds.SetContextValue(ck, gitRepo)
|
||||
return gitRepo, nil
|
||||
}
|
||||
|
@ -14,15 +14,11 @@ import (
|
||||
// WalkReferences walks all the references from the repository
|
||||
// refname is empty, ObjectTag or ObjectBranch. All other values should be treated as equivalent to empty.
|
||||
func WalkReferences(ctx context.Context, repo Repository, walkfn func(sha1, refname string) error) (int, error) {
|
||||
gitRepo := repositoryFromContext(ctx, repo)
|
||||
if gitRepo == nil {
|
||||
var err error
|
||||
gitRepo, err = OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
gitRepo, closer, err := RepositoryFromContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
i := 0
|
||||
iter, err := gitRepo.GoGitRepo().References()
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/gtprof"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -136,7 +137,7 @@ func (g *Manager) doShutdown() {
|
||||
}
|
||||
g.lock.Lock()
|
||||
g.shutdownCtxCancel()
|
||||
atShutdownCtx := pprof.WithLabels(g.hammerCtx, pprof.Labels(LifecyclePProfLabel, "post-shutdown"))
|
||||
atShutdownCtx := pprof.WithLabels(g.hammerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "post-shutdown"))
|
||||
pprof.SetGoroutineLabels(atShutdownCtx)
|
||||
for _, fn := range g.toRunAtShutdown {
|
||||
go fn()
|
||||
@ -167,7 +168,7 @@ func (g *Manager) doHammerTime(d time.Duration) {
|
||||
default:
|
||||
log.Warn("Setting Hammer condition")
|
||||
g.hammerCtxCancel()
|
||||
atHammerCtx := pprof.WithLabels(g.terminateCtx, pprof.Labels(LifecyclePProfLabel, "post-hammer"))
|
||||
atHammerCtx := pprof.WithLabels(g.terminateCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "post-hammer"))
|
||||
pprof.SetGoroutineLabels(atHammerCtx)
|
||||
}
|
||||
g.lock.Unlock()
|
||||
@ -183,7 +184,7 @@ func (g *Manager) doTerminate() {
|
||||
default:
|
||||
log.Warn("Terminating")
|
||||
g.terminateCtxCancel()
|
||||
atTerminateCtx := pprof.WithLabels(g.managerCtx, pprof.Labels(LifecyclePProfLabel, "post-terminate"))
|
||||
atTerminateCtx := pprof.WithLabels(g.managerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "post-terminate"))
|
||||
pprof.SetGoroutineLabels(atTerminateCtx)
|
||||
|
||||
for _, fn := range g.toRunAtTerminate {
|
||||
|
@ -8,6 +8,8 @@ import (
|
||||
"runtime/pprof"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/gtprof"
|
||||
)
|
||||
|
||||
// FIXME: it seems that there is a bug when using systemd Type=notify: the "Install Page" (INSTALL_LOCK=false) doesn't notify properly.
|
||||
@ -22,12 +24,6 @@ const (
|
||||
watchdogMsg systemdNotifyMsg = "WATCHDOG=1"
|
||||
)
|
||||
|
||||
// LifecyclePProfLabel is a label marking manager lifecycle phase
|
||||
// Making it compliant with prometheus key regex https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
|
||||
// would enable someone interested to be able to to continuously gather profiles into pyroscope.
|
||||
// Other labels for pprof (in "modules/process" package) should also follow this rule.
|
||||
const LifecyclePProfLabel = "graceful_lifecycle"
|
||||
|
||||
func statusMsg(msg string) systemdNotifyMsg {
|
||||
return systemdNotifyMsg("STATUS=" + msg)
|
||||
}
|
||||
@ -71,10 +67,10 @@ func (g *Manager) prepare(ctx context.Context) {
|
||||
g.hammerCtx, g.hammerCtxCancel = context.WithCancel(ctx)
|
||||
g.managerCtx, g.managerCtxCancel = context.WithCancel(ctx)
|
||||
|
||||
g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels(LifecyclePProfLabel, "with-terminate"))
|
||||
g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels(LifecyclePProfLabel, "with-shutdown"))
|
||||
g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels(LifecyclePProfLabel, "with-hammer"))
|
||||
g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels(LifecyclePProfLabel, "with-manager"))
|
||||
g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-terminate"))
|
||||
g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-shutdown"))
|
||||
g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-hammer"))
|
||||
g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels(gtprof.LabelGracefulLifecycle, "with-manager"))
|
||||
|
||||
if !g.setStateTransition(stateInit, stateRunning) {
|
||||
panic("invalid graceful manager state: transition from init to running failed")
|
||||
|
25
modules/gtprof/gtprof.go
Normal file
25
modules/gtprof/gtprof.go
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package gtprof
|
||||
|
||||
// This is a Gitea-specific profiling package,
|
||||
// the name is chosen to distinguish it from the standard pprof tool and "GNU gprof"
|
||||
|
||||
// LabelGracefulLifecycle is a label marking manager lifecycle phase
|
||||
// Making it compliant with prometheus key regex https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
|
||||
// would enable someone interested to be able to continuously gather profiles into pyroscope.
|
||||
// Other labels for pprof should also follow this rule.
|
||||
const LabelGracefulLifecycle = "graceful_lifecycle"
|
||||
|
||||
// LabelPid is a label set on goroutines that have a process attached
|
||||
const LabelPid = "pid"
|
||||
|
||||
// LabelPpid is a label set on goroutines that have a process attached
|
||||
const LabelPpid = "ppid"
|
||||
|
||||
// LabelProcessType is a label set on goroutines that have a process attached
|
||||
const LabelProcessType = "process_type"
|
||||
|
||||
// LabelProcessDescription is a label set on goroutines that have a process attached
|
||||
const LabelProcessDescription = "process_description"
|
@ -13,10 +13,9 @@ import (
|
||||
type Event struct {
|
||||
Time time.Time
|
||||
|
||||
GoroutinePid string
|
||||
Caller string
|
||||
Filename string
|
||||
Line int
|
||||
Caller string
|
||||
Filename string
|
||||
Line int
|
||||
|
||||
Level Level
|
||||
|
||||
@ -218,17 +217,16 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms
|
||||
}
|
||||
|
||||
if flags&Lgopid == Lgopid {
|
||||
if event.GoroutinePid != "" {
|
||||
buf = append(buf, '[')
|
||||
if mode.Colorize {
|
||||
buf = append(buf, ColorBytes(FgHiYellow)...)
|
||||
}
|
||||
buf = append(buf, event.GoroutinePid...)
|
||||
if mode.Colorize {
|
||||
buf = append(buf, resetBytes...)
|
||||
}
|
||||
buf = append(buf, ']', ' ')
|
||||
deprecatedGoroutinePid := "no-gopid" // use a dummy value to avoid breaking the log format
|
||||
buf = append(buf, '[')
|
||||
if mode.Colorize {
|
||||
buf = append(buf, ColorBytes(FgHiYellow)...)
|
||||
}
|
||||
buf = append(buf, deprecatedGoroutinePid...)
|
||||
if mode.Colorize {
|
||||
buf = append(buf, resetBytes...)
|
||||
}
|
||||
buf = append(buf, ']', ' ')
|
||||
}
|
||||
buf = append(buf, msg...)
|
||||
|
||||
|
@ -24,34 +24,32 @@ func TestItoa(t *testing.T) {
|
||||
func TestEventFormatTextMessage(t *testing.T) {
|
||||
res := EventFormatTextMessage(&WriterMode{Prefix: "[PREFIX] ", Colorize: false, Flags: Flags{defined: true, flags: 0xffffffff}},
|
||||
&Event{
|
||||
Time: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
|
||||
Caller: "caller",
|
||||
Filename: "filename",
|
||||
Line: 123,
|
||||
GoroutinePid: "pid",
|
||||
Level: ERROR,
|
||||
Stacktrace: "stacktrace",
|
||||
Time: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
|
||||
Caller: "caller",
|
||||
Filename: "filename",
|
||||
Line: 123,
|
||||
Level: ERROR,
|
||||
Stacktrace: "stacktrace",
|
||||
},
|
||||
"msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue),
|
||||
)
|
||||
|
||||
assert.Equal(t, `[PREFIX] 2020/01/02 03:04:05.000000 filename:123:caller [E] [pid] msg format: arg0 arg1
|
||||
assert.Equal(t, `[PREFIX] 2020/01/02 03:04:05.000000 filename:123:caller [E] [no-gopid] msg format: arg0 arg1
|
||||
stacktrace
|
||||
|
||||
`, string(res))
|
||||
|
||||
res = EventFormatTextMessage(&WriterMode{Prefix: "[PREFIX] ", Colorize: true, Flags: Flags{defined: true, flags: 0xffffffff}},
|
||||
&Event{
|
||||
Time: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
|
||||
Caller: "caller",
|
||||
Filename: "filename",
|
||||
Line: 123,
|
||||
GoroutinePid: "pid",
|
||||
Level: ERROR,
|
||||
Stacktrace: "stacktrace",
|
||||
Time: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
|
||||
Caller: "caller",
|
||||
Filename: "filename",
|
||||
Line: 123,
|
||||
Level: ERROR,
|
||||
Stacktrace: "stacktrace",
|
||||
},
|
||||
"msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue),
|
||||
)
|
||||
|
||||
assert.Equal(t, "[PREFIX] \x1b[36m2020/01/02 03:04:05.000000 \x1b[0m\x1b[32mfilename:123:\x1b[32mcaller\x1b[0m \x1b[1;31m[E]\x1b[0m [\x1b[93mpid\x1b[0m] msg format: arg0 \x1b[34marg1\x1b[0m\n\tstacktrace\n\n", string(res))
|
||||
assert.Equal(t, "[PREFIX] \x1b[36m2020/01/02 03:04:05.000000 \x1b[0m\x1b[32mfilename:123:\x1b[32mcaller\x1b[0m \x1b[1;31m[E]\x1b[0m [\x1b[93mno-gopid\x1b[0m] msg format: arg0 \x1b[34marg1\x1b[0m\n\tstacktrace\n\n", string(res))
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ const (
|
||||
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
|
||||
Llevelinitial // Initial character of the provided level in brackets, eg. [I] for info
|
||||
Llevel // Provided level in brackets [INFO]
|
||||
Lgopid // the Goroutine-PID of the context
|
||||
Lgopid // the Goroutine-PID of the context, deprecated and it is always a const value
|
||||
|
||||
Lmedfile = Lshortfile | Llongfile // last 20 characters of the filename
|
||||
LstdFlags = Ldate | Ltime | Lmedfile | Lshortfuncname | Llevelinitial // default
|
||||
|
@ -1,19 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package log
|
||||
|
||||
import "unsafe"
|
||||
|
||||
//go:linkname runtime_getProfLabel runtime/pprof.runtime_getProfLabel
|
||||
func runtime_getProfLabel() unsafe.Pointer //nolint
|
||||
|
||||
type labelMap map[string]string
|
||||
|
||||
func getGoroutineLabels() map[string]string {
|
||||
l := (*labelMap)(runtime_getProfLabel())
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return *l
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime/pprof"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getGoroutineLabels(t *testing.T) {
|
||||
pprof.Do(context.Background(), pprof.Labels(), func(ctx context.Context) {
|
||||
currentLabels := getGoroutineLabels()
|
||||
pprof.ForLabels(ctx, func(key, value string) bool {
|
||||
assert.EqualValues(t, value, currentLabels[key])
|
||||
return true
|
||||
})
|
||||
|
||||
pprof.Do(ctx, pprof.Labels("Test_getGoroutineLabels", "Test_getGoroutineLabels_child1"), func(ctx context.Context) {
|
||||
currentLabels := getGoroutineLabels()
|
||||
pprof.ForLabels(ctx, func(key, value string) bool {
|
||||
assert.EqualValues(t, value, currentLabels[key])
|
||||
return true
|
||||
})
|
||||
if assert.NotNil(t, currentLabels) {
|
||||
assert.EqualValues(t, "Test_getGoroutineLabels_child1", currentLabels["Test_getGoroutineLabels"])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
@ -200,11 +200,6 @@ func (l *LoggerImpl) Log(skip int, level Level, format string, logArgs ...any) {
|
||||
event.Stacktrace = Stack(skip + 1)
|
||||
}
|
||||
|
||||
labels := getGoroutineLabels()
|
||||
if labels != nil {
|
||||
event.GoroutinePid = labels["pid"]
|
||||
}
|
||||
|
||||
// get a simple text message without color
|
||||
msgArgs := make([]any, len(logArgs))
|
||||
copy(msgArgs, logArgs)
|
||||
|
@ -24,7 +24,7 @@ type GlobalVarsType struct {
|
||||
LinkRegex *regexp.Regexp // fast matching a URL link, no any extra validation.
|
||||
}
|
||||
|
||||
var GlobalVars = sync.OnceValue[*GlobalVarsType](func() *GlobalVarsType {
|
||||
var GlobalVars = sync.OnceValue(func() *GlobalVarsType {
|
||||
v := &GlobalVarsType{}
|
||||
v.wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`)
|
||||
v.LinkRegex, _ = xurls.StrictMatchingScheme("https?://")
|
||||
|
@ -42,7 +42,7 @@ type globalVarsType struct {
|
||||
nulCleaner *strings.Replacer
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue[*globalVarsType](func() *globalVarsType {
|
||||
var globalVars = sync.OnceValue(func() *globalVarsType {
|
||||
v := &globalVarsType{}
|
||||
// NOTE: All below regex matching do not perform any extra validation.
|
||||
// Thus a link is produced even if the linked entity does not exist.
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
@ -194,3 +195,21 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
node = node.NextSibling.NextSibling
|
||||
}
|
||||
}
|
||||
|
||||
func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
next := node.NextSibling
|
||||
|
||||
for node != nil && node != next {
|
||||
found, ref := references.FindRenderizableCommitCrossReference(node.Data)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
|
||||
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp)
|
||||
link := createLink(ctx, linkHref, reftext, "commit")
|
||||
|
||||
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
|
||||
node = node.NextSibling.NextSibling
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
@ -16,8 +16,16 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
"golang.org/x/net/html/atom"
|
||||
)
|
||||
|
||||
type RenderIssueIconTitleOptions struct {
|
||||
OwnerName string
|
||||
RepoName string
|
||||
LinkHref string
|
||||
IssueIndex int64
|
||||
}
|
||||
|
||||
func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
return
|
||||
@ -66,6 +74,27 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref *references.RenderizableReference) *html.Node {
|
||||
if DefaultRenderHelperFuncs.RenderRepoIssueIconTitle == nil {
|
||||
return nil
|
||||
}
|
||||
issueIndex, _ := strconv.ParseInt(ref.Issue, 10, 64)
|
||||
h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{
|
||||
OwnerName: ref.Owner,
|
||||
RepoName: ref.Name,
|
||||
LinkHref: linkHref,
|
||||
IssueIndex: issueIndex,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("RenderRepoIssueIconTitle failed: %v", err)
|
||||
return nil
|
||||
}
|
||||
if h == "" {
|
||||
return nil
|
||||
}
|
||||
return &html.Node{Type: html.RawNode, Data: string(ctx.RenderInternal.ProtectSafeAttrs(h))}
|
||||
}
|
||||
|
||||
func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
return
|
||||
@ -76,32 +105,28 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
// old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki
|
||||
crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true"
|
||||
|
||||
var (
|
||||
found bool
|
||||
ref *references.RenderizableReference
|
||||
)
|
||||
var ref *references.RenderizableReference
|
||||
|
||||
next := node.NextSibling
|
||||
|
||||
for node != nil && node != next {
|
||||
_, hasExtTrackFormat := ctx.RenderOptions.Metas["format"]
|
||||
|
||||
// Repos with external issue trackers might still need to reference local PRs
|
||||
// We need to concern with the first one that shows up in the text, whichever it is
|
||||
isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric
|
||||
foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly)
|
||||
refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly)
|
||||
|
||||
switch ctx.RenderOptions.Metas["style"] {
|
||||
case "", IssueNameStyleNumeric:
|
||||
found, ref = foundNumeric, refNumeric
|
||||
ref = refNumeric
|
||||
case IssueNameStyleAlphanumeric:
|
||||
found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
|
||||
ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
|
||||
case IssueNameStyleRegexp:
|
||||
pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
found, ref = references.FindRenderizableReferenceRegexp(node.Data, pattern)
|
||||
ref = references.FindRenderizableReferenceRegexp(node.Data, pattern)
|
||||
}
|
||||
|
||||
// Repos with external issue trackers might still need to reference local PRs
|
||||
@ -109,17 +134,17 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if hasExtTrackFormat && !isNumericStyle && refNumeric != nil {
|
||||
// If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that
|
||||
// Allow a free-pass when non-numeric pattern wasn't found.
|
||||
if found && (ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start) {
|
||||
found = foundNumeric
|
||||
if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start {
|
||||
ref = refNumeric
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
|
||||
if ref == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var link *html.Node
|
||||
reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
|
||||
refText := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
|
||||
if hasExtTrackFormat && !ref.IsPull {
|
||||
ctx.RenderOptions.Metas["index"] = ref.Issue
|
||||
|
||||
@ -129,18 +154,23 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
|
||||
}
|
||||
|
||||
link = createLink(ctx, res, reftext, "ref-issue ref-external-issue")
|
||||
link = createLink(ctx, res, refText, "ref-issue ref-external-issue")
|
||||
} else {
|
||||
// Path determines the type of link that will be rendered. It's unknown at this point whether
|
||||
// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
|
||||
// Gitea will redirect on click as appropriate.
|
||||
issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner)
|
||||
issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name)
|
||||
issuePath := util.Iif(ref.IsPull, "pulls", "issues")
|
||||
if ref.Owner == "" {
|
||||
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], issuePath, ref.Issue), LinkTypeApp)
|
||||
link = createLink(ctx, linkHref, reftext, "ref-issue")
|
||||
} else {
|
||||
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, issuePath, ref.Issue), LinkTypeApp)
|
||||
link = createLink(ctx, linkHref, reftext, "ref-issue")
|
||||
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue), LinkTypeApp)
|
||||
|
||||
// at the moment, only render the issue index in a full line (or simple line) as icon+title
|
||||
// otherwise it would be too noisy for "take #1 as an example" in a sentence
|
||||
if node.Parent.DataAtom == atom.Li && ref.RefLocation.Start < 20 && ref.RefLocation.End == len(node.Data) {
|
||||
link = createIssueLinkContentWithSummary(ctx, linkHref, ref)
|
||||
}
|
||||
if link == nil {
|
||||
link = createLink(ctx, linkHref, refText, "ref-issue")
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,21 +198,3 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
node = node.NextSibling.NextSibling.NextSibling.NextSibling
|
||||
}
|
||||
}
|
||||
|
||||
func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
next := node.NextSibling
|
||||
|
||||
for node != nil && node != next {
|
||||
found, ref := references.FindRenderizableCommitCrossReference(node.Data)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
|
||||
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp)
|
||||
link := createLink(ctx, linkHref, reftext, "commit")
|
||||
|
||||
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
|
||||
node = node.NextSibling.NextSibling
|
||||
}
|
||||
}
|
||||
|
72
modules/markup/html_issue_test.go
Normal file
72
modules/markup/html_issue_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
testModule "code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRender_IssueList(t *testing.T) {
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
markup.Init(&markup.RenderHelperFuncs{
|
||||
RenderRepoIssueIconTitle: func(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (template.HTML, error) {
|
||||
return htmlutil.HTMLFormat("<div>issue #%d</div>", opts.IssueIndex), nil
|
||||
},
|
||||
})
|
||||
|
||||
test := func(input, expected string) {
|
||||
rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{
|
||||
"user": "test-user", "repo": "test-repo",
|
||||
"markupAllowShortIssuePattern": "true",
|
||||
})
|
||||
out, err := markdown.RenderString(rctx, input)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
t.Run("NormalIssueRef", func(t *testing.T) {
|
||||
test(
|
||||
"#12345",
|
||||
`<p><a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a></p>`,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("ListIssueRef", func(t *testing.T) {
|
||||
test(
|
||||
"* #12345",
|
||||
`<ul>
|
||||
<li><div>issue #12345</div></li>
|
||||
</ul>`,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("ListIssueRefNormal", func(t *testing.T) {
|
||||
test(
|
||||
"* foo #12345 bar",
|
||||
`<ul>
|
||||
<li>foo <a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a> bar</li>
|
||||
</ul>`,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("ListTodoIssueRef", func(t *testing.T) {
|
||||
test(
|
||||
"* [ ] #12345",
|
||||
`<ul>
|
||||
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="2"/><div>issue #12345</div></li>
|
||||
</ul>`,
|
||||
)
|
||||
})
|
||||
}
|
@ -17,7 +17,7 @@ import (
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
var reAttrClass = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp {
|
||||
var reAttrClass = sync.OnceValue(func() *regexp.Regexp {
|
||||
// TODO: it isn't a problem at the moment because our HTML contents are always well constructed
|
||||
return regexp.MustCompile(`(<[^>]+)\s+class="([^"]+)"([^>]*>)`)
|
||||
})
|
||||
|
@ -112,7 +112,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
}
|
||||
|
||||
// it is copied from old code, which is quite doubtful whether it is correct
|
||||
var reValidIconName = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp {
|
||||
var reValidIconName = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
|
||||
})
|
||||
|
||||
|
@ -38,6 +38,7 @@ type RenderHelper interface {
|
||||
type RenderHelperFuncs struct {
|
||||
IsUsernameMentionable func(ctx context.Context, username string) bool
|
||||
RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
|
||||
RenderRepoIssueIconTitle func(ctx context.Context, options RenderIssueIconTitleOptions) (template.HTML, error)
|
||||
}
|
||||
|
||||
var DefaultRenderHelperFuncs *RenderHelperFuncs
|
||||
|
@ -43,8 +43,9 @@ var (
|
||||
ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid")
|
||||
|
||||
// https://man.archlinux.org/man/PKGBUILD.5
|
||||
namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`)
|
||||
versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`)
|
||||
namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`)
|
||||
// (epoch:pkgver-pkgrel)
|
||||
versionPattern = regexp.MustCompile(`\A(?:\d:)?[\w.+~]+(?:-[-\w.+~]+)?\z`)
|
||||
)
|
||||
|
||||
type Package struct {
|
||||
|
@ -122,6 +122,14 @@ func TestParsePackageInfo(t *testing.T) {
|
||||
assert.ErrorIs(t, err, ErrInvalidName)
|
||||
})
|
||||
|
||||
t.Run("Regexp", func(t *testing.T) {
|
||||
assert.Regexp(t, versionPattern, "1.2_3~4+5")
|
||||
assert.Regexp(t, versionPattern, "1:2_3~4+5")
|
||||
assert.NotRegexp(t, versionPattern, "a:1.0.0-1")
|
||||
assert.NotRegexp(t, versionPattern, "0.0.1/1-1")
|
||||
assert.NotRegexp(t, versionPattern, "1.0.0 -1")
|
||||
})
|
||||
|
||||
t.Run("InvalidVersion", func(t *testing.T) {
|
||||
data := createPKGINFOContent(packageName, "")
|
||||
|
||||
|
@ -11,6 +11,8 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/gtprof"
|
||||
)
|
||||
|
||||
// TODO: This packages still uses a singleton for the Manager.
|
||||
@ -25,18 +27,6 @@ var (
|
||||
DefaultContext = context.Background()
|
||||
)
|
||||
|
||||
// DescriptionPProfLabel is a label set on goroutines that have a process attached
|
||||
const DescriptionPProfLabel = "process_description"
|
||||
|
||||
// PIDPProfLabel is a label set on goroutines that have a process attached
|
||||
const PIDPProfLabel = "pid"
|
||||
|
||||
// PPIDPProfLabel is a label set on goroutines that have a process attached
|
||||
const PPIDPProfLabel = "ppid"
|
||||
|
||||
// ProcessTypePProfLabel is a label set on goroutines that have a process attached
|
||||
const ProcessTypePProfLabel = "process_type"
|
||||
|
||||
// IDType is a pid type
|
||||
type IDType string
|
||||
|
||||
@ -187,7 +177,12 @@ func (pm *Manager) Add(ctx context.Context, description string, cancel context.C
|
||||
|
||||
Trace(true, pid, description, parentPID, processType)
|
||||
|
||||
pprofCtx := pprof.WithLabels(ctx, pprof.Labels(DescriptionPProfLabel, description, PPIDPProfLabel, string(parentPID), PIDPProfLabel, string(pid), ProcessTypePProfLabel, processType))
|
||||
pprofCtx := pprof.WithLabels(ctx, pprof.Labels(
|
||||
gtprof.LabelProcessDescription, description,
|
||||
gtprof.LabelPpid, string(parentPID),
|
||||
gtprof.LabelPid, string(pid),
|
||||
gtprof.LabelProcessType, processType,
|
||||
))
|
||||
if currentlyRunning {
|
||||
pprof.SetGoroutineLabels(pprofCtx)
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/gtprof"
|
||||
|
||||
"github.com/google/pprof/profile"
|
||||
)
|
||||
|
||||
@ -202,7 +204,7 @@ func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int
|
||||
|
||||
// Add the non-process associated labels from the goroutine sample to the Stack
|
||||
for name, value := range sample.Label {
|
||||
if name == DescriptionPProfLabel || name == PIDPProfLabel || (!flat && name == PPIDPProfLabel) || name == ProcessTypePProfLabel {
|
||||
if name == gtprof.LabelProcessDescription || name == gtprof.LabelPid || (!flat && name == gtprof.LabelPpid) || name == gtprof.LabelProcessType {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -224,7 +226,7 @@ func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int
|
||||
var process *Process
|
||||
|
||||
// Try to get the PID from the goroutine labels
|
||||
if pidvalue, ok := sample.Label[PIDPProfLabel]; ok && len(pidvalue) == 1 {
|
||||
if pidvalue, ok := sample.Label[gtprof.LabelPid]; ok && len(pidvalue) == 1 {
|
||||
pid := IDType(pidvalue[0])
|
||||
|
||||
// Now try to get the process from our map
|
||||
@ -238,20 +240,20 @@ func (pm *Manager) ProcessStacktraces(flat, noSystem bool) ([]*Process, int, int
|
||||
|
||||
// get the parent PID
|
||||
ppid := IDType("")
|
||||
if value, ok := sample.Label[PPIDPProfLabel]; ok && len(value) == 1 {
|
||||
if value, ok := sample.Label[gtprof.LabelPpid]; ok && len(value) == 1 {
|
||||
ppid = IDType(value[0])
|
||||
}
|
||||
|
||||
// format the description
|
||||
description := "(dead process)"
|
||||
if value, ok := sample.Label[DescriptionPProfLabel]; ok && len(value) == 1 {
|
||||
if value, ok := sample.Label[gtprof.LabelProcessDescription]; ok && len(value) == 1 {
|
||||
description = value[0] + " " + description
|
||||
}
|
||||
|
||||
// override the type of the process to "code" but add the old type as a label on the first stack
|
||||
ptype := NoneProcessType
|
||||
if value, ok := sample.Label[ProcessTypePProfLabel]; ok && len(value) == 1 {
|
||||
stack.Labels = append(stack.Labels, &Label{Name: ProcessTypePProfLabel, Value: value[0]})
|
||||
if value, ok := sample.Label[gtprof.LabelProcessType]; ok && len(value) == 1 {
|
||||
stack.Labels = append(stack.Labels, &Label{Name: gtprof.LabelProcessType, Value: value[0]})
|
||||
}
|
||||
process = &Process{
|
||||
PID: pid,
|
||||
|
@ -6,6 +6,7 @@ package queue
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/pprof"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -241,6 +242,9 @@ func NewWorkerPoolQueueWithContext[T any](ctx context.Context, name string, queu
|
||||
w.origHandler = handler
|
||||
w.safeHandler = func(t ...T) (unhandled []T) {
|
||||
defer func() {
|
||||
// FIXME: there is no ctx support in the handler, so process manager is unable to restore the labels
|
||||
// so here we explicitly set the "queue ctx" labels again after the handler is done
|
||||
pprof.SetGoroutineLabels(w.ctxRun)
|
||||
err := recover()
|
||||
if err != nil {
|
||||
log.Error("Recovered from panic in queue %q handler: %v\n%s", name, err, log.Stack(2))
|
||||
|
@ -32,7 +32,7 @@ var (
|
||||
// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
|
||||
issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\")([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$)`)
|
||||
// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
|
||||
issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\')`)
|
||||
issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\'|,)`)
|
||||
// crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
|
||||
// e.g. org/repo#12345
|
||||
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
|
||||
@ -330,22 +330,22 @@ func FindAllIssueReferences(content string) []IssueReference {
|
||||
}
|
||||
|
||||
// FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string.
|
||||
func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) (bool, *RenderizableReference) {
|
||||
func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) *RenderizableReference {
|
||||
var match []int
|
||||
if !crossLinkOnly {
|
||||
match = issueNumericPattern.FindStringSubmatchIndex(content)
|
||||
}
|
||||
if match == nil {
|
||||
if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); match == nil {
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
r := getCrossReference(util.UnsafeStringToBytes(content), match[2], match[3], false, prOnly)
|
||||
if r == nil {
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return true, &RenderizableReference{
|
||||
return &RenderizableReference{
|
||||
Issue: r.issue,
|
||||
Owner: r.owner,
|
||||
Name: r.name,
|
||||
@ -372,15 +372,14 @@ func FindRenderizableCommitCrossReference(content string) (bool, *RenderizableRe
|
||||
}
|
||||
|
||||
// FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string.
|
||||
func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bool, *RenderizableReference) {
|
||||
func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) *RenderizableReference {
|
||||
match := pattern.FindStringSubmatchIndex(content)
|
||||
if len(match) < 4 {
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
action, location := findActionKeywords([]byte(content), match[2])
|
||||
|
||||
return true, &RenderizableReference{
|
||||
return &RenderizableReference{
|
||||
Issue: content[match[2]:match[3]],
|
||||
RefLocation: &RefSpan{Start: match[0], End: match[1]},
|
||||
Action: action,
|
||||
@ -390,15 +389,14 @@ func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bo
|
||||
}
|
||||
|
||||
// FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string.
|
||||
func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) {
|
||||
func FindRenderizableReferenceAlphanumeric(content string) *RenderizableReference {
|
||||
match := issueAlphanumericPattern.FindStringSubmatchIndex(content)
|
||||
if match == nil {
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
action, location := findActionKeywords([]byte(content), match[2])
|
||||
|
||||
return true, &RenderizableReference{
|
||||
return &RenderizableReference{
|
||||
Issue: content[match[2]:match[3]],
|
||||
RefLocation: &RefSpan{Start: match[2], End: match[3]},
|
||||
Action: action,
|
||||
|
@ -249,11 +249,10 @@ func TestFindAllIssueReferences(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, fixture := range alnumFixtures {
|
||||
found, ref := FindRenderizableReferenceAlphanumeric(fixture.input)
|
||||
ref := FindRenderizableReferenceAlphanumeric(fixture.input)
|
||||
if fixture.issue == "" {
|
||||
assert.False(t, found, "Failed to parse: {%s}", fixture.input)
|
||||
assert.Nil(t, ref, "Failed to parse: {%s}", fixture.input)
|
||||
} else {
|
||||
assert.True(t, found, "Failed to parse: {%s}", fixture.input)
|
||||
assert.Equal(t, fixture.issue, ref.Issue, "Failed to parse: {%s}", fixture.input)
|
||||
assert.Equal(t, fixture.refLocation, ref.RefLocation, "Failed to parse: {%s}", fixture.input)
|
||||
assert.Equal(t, fixture.action, ref.Action, "Failed to parse: {%s}", fixture.input)
|
||||
@ -463,6 +462,7 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) {
|
||||
"ABC-123:",
|
||||
"\"ABC-123\"",
|
||||
"'ABC-123'",
|
||||
"ABC-123, unknown PR",
|
||||
}
|
||||
falseTestCases := []string{
|
||||
"RC-08",
|
||||
|
@ -31,12 +31,7 @@ func Test_getLicense(t *testing.T) {
|
||||
|
||||
Copyright (c) 2023 Gitea
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
`,
|
||||
Permission is hereby granted`,
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
@ -53,7 +48,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
if !tt.wantErr(t, err, fmt.Sprintf("GetLicense(%v, %v)", tt.args.name, tt.args.values)) {
|
||||
return
|
||||
}
|
||||
assert.Equalf(t, tt.want, string(got), "GetLicense(%v, %v)", tt.args.name, tt.args.values)
|
||||
assert.Contains(t, string(got), tt.want, "GetLicense(%v, %v)", tt.args.name, tt.args.values)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
123
modules/reqctx/datastore.go
Normal file
123
modules/reqctx/datastore.go
Normal file
@ -0,0 +1,123 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package reqctx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
)
|
||||
|
||||
type ContextDataProvider interface {
|
||||
GetData() ContextData
|
||||
}
|
||||
|
||||
type ContextData map[string]any
|
||||
|
||||
func (ds ContextData) GetData() ContextData {
|
||||
return ds
|
||||
}
|
||||
|
||||
func (ds ContextData) MergeFrom(other ContextData) ContextData {
|
||||
for k, v := range other {
|
||||
ds[k] = v
|
||||
}
|
||||
return ds
|
||||
}
|
||||
|
||||
// RequestDataStore is a short-lived context-related object that is used to store request-specific data.
|
||||
type RequestDataStore interface {
|
||||
GetData() ContextData
|
||||
SetContextValue(k, v any)
|
||||
GetContextValue(key any) any
|
||||
AddCleanUp(f func())
|
||||
AddCloser(c io.Closer)
|
||||
}
|
||||
|
||||
type requestDataStoreKeyType struct{}
|
||||
|
||||
var RequestDataStoreKey requestDataStoreKeyType
|
||||
|
||||
type requestDataStore struct {
|
||||
data ContextData
|
||||
|
||||
mu sync.RWMutex
|
||||
values map[any]any
|
||||
cleanUpFuncs []func()
|
||||
}
|
||||
|
||||
func (r *requestDataStore) GetContextValue(key any) any {
|
||||
if key == RequestDataStoreKey {
|
||||
return r
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.values[key]
|
||||
}
|
||||
|
||||
func (r *requestDataStore) SetContextValue(k, v any) {
|
||||
r.mu.Lock()
|
||||
r.values[k] = v
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetData and the underlying ContextData are not thread-safe, callers should ensure thread-safety.
|
||||
func (r *requestDataStore) GetData() ContextData {
|
||||
if r.data == nil {
|
||||
r.data = make(ContextData)
|
||||
}
|
||||
return r.data
|
||||
}
|
||||
|
||||
func (r *requestDataStore) AddCleanUp(f func()) {
|
||||
r.mu.Lock()
|
||||
r.cleanUpFuncs = append(r.cleanUpFuncs, f)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *requestDataStore) AddCloser(c io.Closer) {
|
||||
r.AddCleanUp(func() { _ = c.Close() })
|
||||
}
|
||||
|
||||
func (r *requestDataStore) cleanUp() {
|
||||
for _, f := range r.cleanUpFuncs {
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
func GetRequestDataStore(ctx context.Context) RequestDataStore {
|
||||
if req, ok := ctx.Value(RequestDataStoreKey).(*requestDataStore); ok {
|
||||
return req
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type requestContext struct {
|
||||
context.Context
|
||||
dataStore *requestDataStore
|
||||
}
|
||||
|
||||
func (c *requestContext) Value(key any) any {
|
||||
if v := c.dataStore.GetContextValue(key); v != nil {
|
||||
return v
|
||||
}
|
||||
return c.Context.Value(key)
|
||||
}
|
||||
|
||||
func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Context, finished func()) {
|
||||
ctx, _, processFinished := process.GetManager().AddTypedContext(parentCtx, profDesc, process.RequestProcessType, true)
|
||||
reqCtx := &requestContext{Context: ctx, dataStore: &requestDataStore{values: make(map[any]any)}}
|
||||
return reqCtx, func() {
|
||||
reqCtx.dataStore.cleanUp()
|
||||
processFinished()
|
||||
}
|
||||
}
|
||||
|
||||
// NewRequestContextForTest creates a new RequestContext for testing purposes
|
||||
// It doesn't add the context to the process manager, nor do cleanup
|
||||
func NewRequestContextForTest(parentCtx context.Context) context.Context {
|
||||
return &requestContext{Context: parentCtx, dataStore: &requestDataStore{values: make(map[any]any)}}
|
||||
}
|
@ -11,6 +11,7 @@ var defaultI18nLangNames = []string{
|
||||
"zh-TW", "繁體中文(台灣)",
|
||||
"de-DE", "Deutsch",
|
||||
"fr-FR", "Français",
|
||||
"ga-IE", "Gaeilge",
|
||||
"nl-NL", "Nederlands",
|
||||
"lv-LV", "Latviešu",
|
||||
"ru-RU", "Русский",
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type normalizeVarsStruct struct {
|
||||
type globalVarsStruct struct {
|
||||
reXMLDoc,
|
||||
reComment,
|
||||
reAttrXMLNs,
|
||||
@ -18,26 +18,23 @@ type normalizeVarsStruct struct {
|
||||
reAttrClassPrefix *regexp.Regexp
|
||||
}
|
||||
|
||||
var (
|
||||
normalizeVars *normalizeVarsStruct
|
||||
normalizeVarsOnce sync.Once
|
||||
)
|
||||
var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
||||
return &globalVarsStruct{
|
||||
reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`),
|
||||
reComment: regexp.MustCompile(`(?s)<!--.*?-->`),
|
||||
|
||||
reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`),
|
||||
reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`),
|
||||
reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`),
|
||||
}
|
||||
})
|
||||
|
||||
// Normalize normalizes the SVG content: set default width/height, remove unnecessary tags/attributes
|
||||
// It's designed to work with valid SVG content. For invalid SVG content, the returned content is not guaranteed.
|
||||
func Normalize(data []byte, size int) []byte {
|
||||
normalizeVarsOnce.Do(func() {
|
||||
normalizeVars = &normalizeVarsStruct{
|
||||
reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`),
|
||||
reComment: regexp.MustCompile(`(?s)<!--.*?-->`),
|
||||
|
||||
reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`),
|
||||
reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`),
|
||||
reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`),
|
||||
}
|
||||
})
|
||||
data = normalizeVars.reXMLDoc.ReplaceAll(data, nil)
|
||||
data = normalizeVars.reComment.ReplaceAll(data, nil)
|
||||
vars := globalVars()
|
||||
data = vars.reXMLDoc.ReplaceAll(data, nil)
|
||||
data = vars.reComment.ReplaceAll(data, nil)
|
||||
|
||||
data = bytes.TrimSpace(data)
|
||||
svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">"))
|
||||
@ -45,9 +42,9 @@ func Normalize(data []byte, size int) []byte {
|
||||
return data
|
||||
}
|
||||
normalized := bytes.Clone(svgTag)
|
||||
normalized = normalizeVars.reAttrXMLNs.ReplaceAll(normalized, nil)
|
||||
normalized = normalizeVars.reAttrSize.ReplaceAll(normalized, nil)
|
||||
normalized = normalizeVars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`))
|
||||
normalized = vars.reAttrXMLNs.ReplaceAll(normalized, nil)
|
||||
normalized = vars.reAttrSize.ReplaceAll(normalized, nil)
|
||||
normalized = vars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`))
|
||||
normalized = bytes.TrimSpace(normalized)
|
||||
normalized = fmt.Appendf(normalized, ` width="%d" height="%d"`, size, size)
|
||||
if !bytes.Contains(normalized, []byte(` class="`)) {
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"html"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -69,7 +68,7 @@ func NewFuncMap() template.FuncMap {
|
||||
// -----------------------------------------------------------------
|
||||
// time / number / format
|
||||
"FileSize": base.FileSize,
|
||||
"CountFmt": base.FormatNumberSI,
|
||||
"CountFmt": countFmt,
|
||||
"Sec2Time": util.SecToTime,
|
||||
|
||||
"TimeEstimateString": timeEstimateString,
|
||||
@ -239,29 +238,8 @@ func iif(condition any, vals ...any) any {
|
||||
}
|
||||
|
||||
func isTemplateTruthy(v any) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rv.Kind() {
|
||||
case reflect.Bool:
|
||||
return rv.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return rv.Int() != 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return rv.Uint() != 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return rv.Float() != 0
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return rv.Complex() != 0
|
||||
case reflect.String, reflect.Slice, reflect.Array, reflect.Map:
|
||||
return rv.Len() > 0
|
||||
case reflect.Struct:
|
||||
return true
|
||||
default:
|
||||
return !rv.IsNil()
|
||||
}
|
||||
truth, _ := template.IsTrue(v)
|
||||
return truth
|
||||
}
|
||||
|
||||
// evalTokens evaluates the expression by tokens and returns the result, see the comment of eval.Expr for details.
|
||||
@ -286,14 +264,6 @@ func userThemeName(user *user_model.User) string {
|
||||
return setting.UI.DefaultTheme
|
||||
}
|
||||
|
||||
func timeEstimateString(timeSec any) string {
|
||||
v, _ := util.ToInt64(timeSec)
|
||||
if v == 0 {
|
||||
return ""
|
||||
}
|
||||
return util.TimeEstimateString(v)
|
||||
}
|
||||
|
||||
// QueryBuild builds a query string from a list of key-value pairs.
|
||||
// It omits the nil and empty strings, but it doesn't omit other zero values,
|
||||
// because the zero value of number types may have a meaning.
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -65,31 +66,12 @@ func TestSanitizeHTML(t *testing.T) {
|
||||
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
|
||||
}
|
||||
|
||||
func TestTemplateTruthy(t *testing.T) {
|
||||
func TestTemplateIif(t *testing.T) {
|
||||
tmpl := template.New("test")
|
||||
tmpl.Funcs(template.FuncMap{"Iif": iif})
|
||||
template.Must(tmpl.Parse(`{{if .Value}}true{{else}}false{{end}}:{{Iif .Value "true" "false"}}`))
|
||||
|
||||
cases := []any{
|
||||
nil, false, true, "", "string", 0, 1,
|
||||
byte(0), byte(1), int64(0), int64(1), float64(0), float64(1),
|
||||
complex(0, 0), complex(1, 0),
|
||||
(chan int)(nil), make(chan int),
|
||||
(func())(nil), func() {},
|
||||
util.ToPointer(0), util.ToPointer(util.ToPointer(0)),
|
||||
util.ToPointer(1), util.ToPointer(util.ToPointer(1)),
|
||||
[0]int{},
|
||||
[1]int{0},
|
||||
[]int(nil),
|
||||
[]int{},
|
||||
[]int{0},
|
||||
map[any]any(nil),
|
||||
map[any]any{},
|
||||
map[any]any{"k": "v"},
|
||||
(*struct{})(nil),
|
||||
struct{}{},
|
||||
util.ToPointer(struct{}{}),
|
||||
}
|
||||
cases := []any{nil, false, true, "", "string", 0, 1}
|
||||
w := &strings.Builder{}
|
||||
truthyCount := 0
|
||||
for i, v := range cases {
|
||||
@ -102,3 +84,37 @@ func TestTemplateTruthy(t *testing.T) {
|
||||
}
|
||||
assert.True(t, truthyCount != 0 && truthyCount != len(cases))
|
||||
}
|
||||
|
||||
func TestTemplateEscape(t *testing.T) {
|
||||
execTmpl := func(code string) string {
|
||||
tmpl := template.New("test")
|
||||
tmpl.Funcs(template.FuncMap{"QueryBuild": QueryBuild, "HTMLFormat": htmlutil.HTMLFormat})
|
||||
template.Must(tmpl.Parse(code))
|
||||
w := &strings.Builder{}
|
||||
assert.NoError(t, tmpl.Execute(w, nil))
|
||||
return w.String()
|
||||
}
|
||||
|
||||
t.Run("Golang URL Escape", func(t *testing.T) {
|
||||
// Golang template considers "href", "*src*", "*uri*", "*url*" (and more) ... attributes as contentTypeURL and does auto-escaping
|
||||
actual := execTmpl(`<a href="?a={{"%"}}"></a>`)
|
||||
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
|
||||
actual = execTmpl(`<a data-xxx-url="?a={{"%"}}"></a>`)
|
||||
assert.Equal(t, `<a data-xxx-url="?a=%25"></a>`, actual)
|
||||
})
|
||||
t.Run("Golang URL No-escape", func(t *testing.T) {
|
||||
// non-URL content isn't auto-escaped
|
||||
actual := execTmpl(`<a data-link="?a={{"%"}}"></a>`)
|
||||
assert.Equal(t, `<a data-link="?a=%"></a>`, actual)
|
||||
})
|
||||
t.Run("QueryBuild", func(t *testing.T) {
|
||||
actual := execTmpl(`<a href="{{QueryBuild "?" "a" "%"}}"></a>`)
|
||||
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
|
||||
actual = execTmpl(`<a href="?{{QueryBuild "a" "%"}}"></a>`)
|
||||
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
|
||||
})
|
||||
t.Run("HTMLFormat", func(t *testing.T) {
|
||||
actual := execTmpl("{{HTMLFormat `<a k=\"%s\">%s</a>` `\"` `<>`}}")
|
||||
assert.Equal(t, `<a k="""><></a>`, actual)
|
||||
})
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ import (
|
||||
|
||||
type TemplateExecutor scopedtmpl.TemplateExecutor
|
||||
|
||||
type TplName string
|
||||
|
||||
type HTMLRender struct {
|
||||
templates atomic.Pointer[scopedtmpl.ScopedTemplate]
|
||||
}
|
||||
@ -40,7 +42,8 @@ var (
|
||||
|
||||
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
|
||||
|
||||
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data any, ctx context.Context) error { //nolint:revive
|
||||
func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ctx context.Context) error { //nolint:revive
|
||||
name := string(tplName)
|
||||
if respWriter, ok := w.(http.ResponseWriter); ok {
|
||||
if respWriter.Header().Get("Content-Type") == "" {
|
||||
respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
37
modules/templates/util_format.go
Normal file
37
modules/templates/util_format.go
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func timeEstimateString(timeSec any) string {
|
||||
v, _ := util.ToInt64(timeSec)
|
||||
if v == 0 {
|
||||
return ""
|
||||
}
|
||||
return util.TimeEstimateString(v)
|
||||
}
|
||||
|
||||
func countFmt(data any) string {
|
||||
// legacy code, not ideal, still used in some places
|
||||
num, err := util.ToInt64(data)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if num < 1000 {
|
||||
return fmt.Sprintf("%d", num)
|
||||
} else if num < 1_000_000 {
|
||||
num2 := float32(num) / 1000.0
|
||||
return fmt.Sprintf("%.1fk", num2)
|
||||
} else if num < 1_000_000_000 {
|
||||
num2 := float32(num) / 1_000_000.0
|
||||
return fmt.Sprintf("%.1fM", num2)
|
||||
}
|
||||
num2 := float32(num) / 1_000_000_000.0
|
||||
return fmt.Sprintf("%.1fG", num2)
|
||||
}
|
18
modules/templates/util_format_test.go
Normal file
18
modules/templates/util_format_test.go
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCountFmt(t *testing.T) {
|
||||
assert.Equal(t, "125", countFmt(125))
|
||||
assert.Equal(t, "1.3k", countFmt(int64(1317)))
|
||||
assert.Equal(t, "21.3M", countFmt(21317675))
|
||||
assert.Equal(t, "45.7G", countFmt(45721317675))
|
||||
assert.Equal(t, "", countFmt("test"))
|
||||
}
|
@ -4,11 +4,16 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// RedirectURL returns the redirect URL of a http response.
|
||||
@ -41,3 +46,19 @@ func MockVariableValue[T any](p *T, v ...T) (reset func()) {
|
||||
}
|
||||
return func() { *p = old }
|
||||
}
|
||||
|
||||
// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
|
||||
func SetupGiteaRoot() string {
|
||||
giteaRoot := os.Getenv("GITEA_ROOT")
|
||||
if giteaRoot != "" {
|
||||
return giteaRoot
|
||||
}
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename)))
|
||||
fixturesDir := filepath.Join(giteaRoot, "models", "fixtures")
|
||||
if exist, _ := util.IsDir(fixturesDir); !exist {
|
||||
panic(fmt.Sprintf("fixtures directory not found: %s", fixturesDir))
|
||||
}
|
||||
_ = os.Setenv("GITEA_ROOT", giteaRoot)
|
||||
return giteaRoot
|
||||
}
|
||||
|
17
modules/test/utils_test.go
Normal file
17
modules/test/utils_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSetupGiteaRoot(t *testing.T) {
|
||||
t.Setenv("GITEA_ROOT", "test")
|
||||
assert.Equal(t, "test", SetupGiteaRoot())
|
||||
t.Setenv("GITEA_ROOT", "")
|
||||
assert.NotEqual(t, "test", SetupGiteaRoot())
|
||||
}
|
13
modules/util/runtime.go
Normal file
13
modules/util/runtime.go
Normal file
@ -0,0 +1,13 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package util
|
||||
|
||||
import "runtime"
|
||||
|
||||
func CallerFuncName(skip int) string {
|
||||
pc := make([]uintptr, 1)
|
||||
runtime.Callers(skip+1, pc)
|
||||
funcName := runtime.FuncForPC(pc[0]).Name()
|
||||
return funcName
|
||||
}
|
32
modules/util/runtime_test.go
Normal file
32
modules/util/runtime_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCallerFuncName(t *testing.T) {
|
||||
s := CallerFuncName(1)
|
||||
assert.Equal(t, "code.gitea.io/gitea/modules/util.TestCallerFuncName", s)
|
||||
}
|
||||
|
||||
func BenchmarkCallerFuncName(b *testing.B) {
|
||||
// BenchmarkCaller/sprintf-12 12744829 95.49 ns/op
|
||||
b.Run("sprintf", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("aaaaaaaaaaaaaaaa %s %s %s", "bbbbbbbbbbbbbbbbbbb", b.Name(), "ccccccccccccccccccccc")
|
||||
}
|
||||
})
|
||||
// BenchmarkCaller/caller-12 10625133 113.6 ns/op
|
||||
// It is almost as fast as fmt.Sprintf
|
||||
b.Run("caller", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
CallerFuncName(1)
|
||||
}
|
||||
})
|
||||
}
|
@ -25,7 +25,7 @@ type timeStrGlobalVarsType struct {
|
||||
// In the future, it could be some configurable options to help users
|
||||
// to convert the working time to different units.
|
||||
|
||||
var timeStrGlobalVars = sync.OnceValue[*timeStrGlobalVarsType](func() *timeStrGlobalVarsType {
|
||||
var timeStrGlobalVars = sync.OnceValue(func() *timeStrGlobalVarsType {
|
||||
v := &timeStrGlobalVarsType{}
|
||||
v.re = regexp.MustCompile(`(?i)(\d+)\s*([hms])`)
|
||||
v.units = []struct {
|
||||
|
@ -242,10 +242,10 @@ func TestReserveLineBreakForTextarea(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOptionalArg(t *testing.T) {
|
||||
foo := func(other any, optArg ...int) int {
|
||||
foo := func(_ any, optArg ...int) int {
|
||||
return OptionalArg(optArg)
|
||||
}
|
||||
bar := func(other any, optArg ...int) int {
|
||||
bar := func(_ any, optArg ...int) int {
|
||||
return OptionalArg(optArg, 42)
|
||||
}
|
||||
assert.Equal(t, 0, foo(nil))
|
||||
|
@ -4,7 +4,6 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
goctx "context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
@ -51,7 +50,6 @@ func (r *responseWriter) WriteHeader(statusCode int) {
|
||||
var (
|
||||
httpReqType = reflect.TypeOf((*http.Request)(nil))
|
||||
respWriterType = reflect.TypeOf((*http.ResponseWriter)(nil)).Elem()
|
||||
cancelFuncType = reflect.TypeOf((*goctx.CancelFunc)(nil)).Elem()
|
||||
)
|
||||
|
||||
// preCheckHandler checks whether the handler is valid, developers could get first-time feedback, all mistakes could be found at startup
|
||||
@ -65,11 +63,8 @@ func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) {
|
||||
if !hasStatusProvider {
|
||||
panic(fmt.Sprintf("handler should have at least one ResponseStatusProvider argument, but got %s", fn.Type()))
|
||||
}
|
||||
if fn.Type().NumOut() != 0 && fn.Type().NumIn() != 1 {
|
||||
panic(fmt.Sprintf("handler should have no return value or only one argument, but got %s", fn.Type()))
|
||||
}
|
||||
if fn.Type().NumOut() == 1 && fn.Type().Out(0) != cancelFuncType {
|
||||
panic(fmt.Sprintf("handler should return a cancel function, but got %s", fn.Type()))
|
||||
if fn.Type().NumOut() != 0 {
|
||||
panic(fmt.Sprintf("handler should have no return value other than registered ones, but got %s", fn.Type()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,16 +100,10 @@ func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect
|
||||
return argsIn
|
||||
}
|
||||
|
||||
func handleResponse(fn reflect.Value, ret []reflect.Value) goctx.CancelFunc {
|
||||
if len(ret) == 1 {
|
||||
if cancelFunc, ok := ret[0].Interface().(goctx.CancelFunc); ok {
|
||||
return cancelFunc
|
||||
}
|
||||
panic(fmt.Sprintf("unsupported return type: %s", ret[0].Type()))
|
||||
} else if len(ret) > 1 {
|
||||
func handleResponse(fn reflect.Value, ret []reflect.Value) {
|
||||
if len(ret) != 0 {
|
||||
panic(fmt.Sprintf("unsupported return values: %s", fn.Type()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasResponseBeenWritten(argsIn []reflect.Value) bool {
|
||||
@ -171,11 +160,8 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
|
||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||
ret := fn.Call(argsIn)
|
||||
|
||||
// handle the return value, and defer the cancel function if there is one
|
||||
cancelFunc := handleResponse(fn, ret)
|
||||
if cancelFunc != nil {
|
||||
defer cancelFunc()
|
||||
}
|
||||
// handle the return value (no-op at the moment)
|
||||
handleResponse(fn, ret)
|
||||
|
||||
// if the response has not been written, call the next handler
|
||||
if next != nil && !hasResponseBeenWritten(argsIn) {
|
||||
|
@ -7,46 +7,21 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// ContextDataStore represents a data store
|
||||
type ContextDataStore interface {
|
||||
GetData() ContextData
|
||||
}
|
||||
|
||||
type ContextData map[string]any
|
||||
|
||||
func (ds ContextData) GetData() ContextData {
|
||||
return ds
|
||||
}
|
||||
|
||||
func (ds ContextData) MergeFrom(other ContextData) ContextData {
|
||||
for k, v := range other {
|
||||
ds[k] = v
|
||||
}
|
||||
return ds
|
||||
}
|
||||
|
||||
const ContextDataKeySignedUser = "SignedUser"
|
||||
|
||||
type contextDataKeyType struct{}
|
||||
|
||||
var contextDataKey contextDataKeyType
|
||||
|
||||
func WithContextData(c context.Context) context.Context {
|
||||
return context.WithValue(c, contextDataKey, make(ContextData, 10))
|
||||
}
|
||||
|
||||
func GetContextData(c context.Context) ContextData {
|
||||
if ds, ok := c.Value(contextDataKey).(ContextData); ok {
|
||||
return ds
|
||||
func GetContextData(c context.Context) reqctx.ContextData {
|
||||
if rc := reqctx.GetRequestDataStore(c); rc != nil {
|
||||
return rc.GetData()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CommonTemplateContextData() ContextData {
|
||||
return ContextData{
|
||||
func CommonTemplateContextData() reqctx.ContextData {
|
||||
return reqctx.ContextData{
|
||||
"IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations,
|
||||
|
||||
"ShowRegistrationButton": setting.Service.ShowRegistrationButton,
|
||||
|
@ -7,11 +7,13 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
)
|
||||
|
||||
// Flash represents a one time data transfer between two requests.
|
||||
type Flash struct {
|
||||
DataStore ContextDataStore
|
||||
DataStore reqctx.RequestDataStore
|
||||
url.Values
|
||||
ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
|
||||
@ -29,12 +30,12 @@ func Bind[T any](_ T) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// SetForm set the form object
|
||||
func SetForm(dataStore middleware.ContextDataStore, obj any) {
|
||||
func SetForm(dataStore reqctx.ContextDataProvider, obj any) {
|
||||
dataStore.GetData()["__form"] = obj
|
||||
}
|
||||
|
||||
// GetForm returns the validate form information
|
||||
func GetForm(dataStore middleware.ContextDataStore) any {
|
||||
func GetForm(dataStore reqctx.RequestDataStore) any {
|
||||
return dataStore.GetData()["__form"]
|
||||
}
|
||||
|
||||
|
31
options/gitignore/AutomationStudio
Normal file
31
options/gitignore/AutomationStudio
Normal file
@ -0,0 +1,31 @@
|
||||
# gitignore template for B&R Automation Studio (AS) 4
|
||||
# website: https://www.br-automation.com/en-us/products/software/automation-software/automation-studio/
|
||||
|
||||
# AS temporary directories
|
||||
Binaries/
|
||||
Diagnosis/
|
||||
Temp/
|
||||
TempObjects/
|
||||
|
||||
# AS transfer files
|
||||
*artransfer.br
|
||||
*arTrsfmode.nv
|
||||
|
||||
# 'ignored' directory
|
||||
ignored/
|
||||
|
||||
# ARNC0ext
|
||||
*arnc0ext.br
|
||||
|
||||
# AS File types
|
||||
*.bak
|
||||
*.isopen
|
||||
*.orig
|
||||
*.log
|
||||
*.asar
|
||||
*.csvlog*
|
||||
*.set
|
||||
!**/Physical/**/*.set
|
||||
|
||||
# RevInfo variables
|
||||
*RevInfo.var
|
28
options/gitignore/Firebase
Normal file
28
options/gitignore/Firebase
Normal file
@ -0,0 +1,28 @@
|
||||
# Firebase build and deployment files
|
||||
/firebase-debug.log
|
||||
/firebase-debug.*.log
|
||||
.firebaserc
|
||||
|
||||
# Firebase Hosting
|
||||
/firebase.json
|
||||
*.cache
|
||||
hosting/.cache
|
||||
|
||||
# Firebase Functions
|
||||
/functions/node_modules/
|
||||
/functions/.env
|
||||
/functions/package-lock.json
|
||||
|
||||
# Firebase Emulators
|
||||
/firebase-*.zip
|
||||
/.firebase/
|
||||
/emulator-ui/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Environment files (local configs)
|
||||
/.env.*
|
42
options/gitignore/Modelica
Normal file
42
options/gitignore/Modelica
Normal file
@ -0,0 +1,42 @@
|
||||
# Modelica - an object-oriented language for modeling of cyber-physical systems
|
||||
# https://modelica.org/
|
||||
# Ignore temporary files, build results, simulation files
|
||||
|
||||
## Modelica-specific files
|
||||
*~
|
||||
*.bak
|
||||
*.bak-mo
|
||||
*.mof
|
||||
\#*\#
|
||||
*.moe
|
||||
*.mol
|
||||
|
||||
## Build artefacts
|
||||
*.exe
|
||||
*.exp
|
||||
*.o
|
||||
*.pyc
|
||||
|
||||
## Simulation files
|
||||
*.mat
|
||||
|
||||
## Package files
|
||||
*.gz
|
||||
*.rar
|
||||
*.tar
|
||||
*.zip
|
||||
|
||||
## Dymola-specific files
|
||||
buildlog.txt
|
||||
dsfinal.txt
|
||||
dsin.txt
|
||||
dslog.txt
|
||||
dsmodel*
|
||||
dsres.txt
|
||||
dymosim*
|
||||
request
|
||||
stat
|
||||
status
|
||||
stop
|
||||
success
|
||||
*.
|
@ -166,3 +166,6 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
@ -26,6 +26,7 @@
|
||||
|
||||
## Bibliography auxiliary files (bibtex/biblatex/biber):
|
||||
*.bbl
|
||||
*.bbl-SAVE-ERROR
|
||||
*.bcf
|
||||
*.blg
|
||||
*-blx.aux
|
||||
|
@ -2,18 +2,18 @@ Elastic License 2.0
|
||||
|
||||
URL: https://www.elastic.co/licensing/elastic-license
|
||||
|
||||
## Acceptance
|
||||
Acceptance
|
||||
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
## Copyright License
|
||||
Copyright License
|
||||
|
||||
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
||||
non-sublicensable, non-transferable license to use, copy, distribute, make
|
||||
available, and prepare derivative works of the software, in each case subject to
|
||||
the limitations and conditions below.
|
||||
|
||||
## Limitations
|
||||
Limitations
|
||||
|
||||
You may not provide the software to third parties as a hosted or managed
|
||||
service, where the service provides users with access to any substantial set of
|
||||
@ -27,7 +27,7 @@ You may not alter, remove, or obscure any licensing, copyright, or other notices
|
||||
of the licensor in the software. Any use of the licensor’s trademarks is subject
|
||||
to applicable law.
|
||||
|
||||
## Patents
|
||||
Patents
|
||||
|
||||
The licensor grants you a license, under any patent claims the licensor can
|
||||
license, or becomes able to license, to make, have made, use, sell, offer for
|
||||
@ -40,7 +40,7 @@ the software granted under these terms ends immediately. If your company makes
|
||||
such a claim, your patent license ends immediately for work on behalf of your
|
||||
company.
|
||||
|
||||
## Notices
|
||||
Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of the software from you
|
||||
also gets a copy of these terms.
|
||||
@ -53,7 +53,7 @@ software prominent notices stating that you have modified the software.
|
||||
These terms do not imply any licenses other than those expressly granted in
|
||||
these terms.
|
||||
|
||||
## Termination
|
||||
Termination
|
||||
|
||||
If you use the software in violation of these terms, such use is not licensed,
|
||||
and your licenses will automatically terminate. If the licensor provides you
|
||||
@ -63,31 +63,31 @@ reinstated retroactively. However, if you violate these terms after such
|
||||
reinstatement, any additional violation of these terms will cause your licenses
|
||||
to terminate automatically and permanently.
|
||||
|
||||
## No Liability
|
||||
No Liability
|
||||
|
||||
*As far as the law allows, the software comes as is, without any warranty or
|
||||
As far as the law allows, the software comes as is, without any warranty or
|
||||
condition, and the licensor will not be liable to you for any damages arising
|
||||
out of these terms or the use or nature of the software, under any kind of
|
||||
legal claim.*
|
||||
legal claim.
|
||||
|
||||
## Definitions
|
||||
Definitions
|
||||
|
||||
The **licensor** is the entity offering these terms, and the **software** is the
|
||||
The licensor is the entity offering these terms, and the software is the
|
||||
software the licensor makes available under these terms, including any portion
|
||||
of it.
|
||||
|
||||
**you** refers to the individual or entity agreeing to these terms.
|
||||
you refers to the individual or entity agreeing to these terms.
|
||||
|
||||
**your company** is any legal entity, sole proprietorship, or other kind of
|
||||
your company is any legal entity, sole proprietorship, or other kind of
|
||||
organization that you work for, plus all organizations that have control over,
|
||||
are under the control of, or are under common control with that
|
||||
organization. **control** means ownership of substantially all the assets of an
|
||||
organization. control means ownership of substantially all the assets of an
|
||||
entity, or the power to direct its management and policies by vote, contract, or
|
||||
otherwise. Control can be direct or indirect.
|
||||
|
||||
**your licenses** are all the licenses granted to you for the software under
|
||||
your licenses are all the licenses granted to you for the software under
|
||||
these terms.
|
||||
|
||||
**use** means anything you do with the software requiring one of your licenses.
|
||||
use means anything you do with the software requiring one of your licenses.
|
||||
|
||||
**trademark** means trademarks, service marks, and similar rights.
|
||||
trademark means trademarks, service marks, and similar rights.
|
||||
|
@ -2,8 +2,17 @@ MIT License
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
@ -3742,6 +3742,7 @@ variables.creation.success=Proměnná „%s“ byla přidána.
|
||||
variables.update.failed=Úprava proměnné se nezdařila.
|
||||
variables.update.success=Proměnná byla upravena.
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Odstraněný projekt
|
||||
type-1.display_name=Samostatný projekt
|
||||
|
@ -3556,6 +3556,7 @@ variables.creation.success=Die Variable „%s“ wurde hinzugefügt.
|
||||
variables.update.failed=Fehler beim Bearbeiten der Variable.
|
||||
variables.update.success=Die Variable wurde bearbeitet.
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Individuelles Projekt
|
||||
type-2.display_name=Repository-Projekt
|
||||
|
@ -3439,6 +3439,7 @@ variables.creation.success=Η μεταβλητή "%s" έχει προστεθε
|
||||
variables.update.failed=Αποτυχία επεξεργασίας μεταβλητής.
|
||||
variables.update.success=Η μεταβλητή έχει τροποποιηθεί.
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Ατομικό Έργο
|
||||
type-2.display_name=Έργο Αποθετηρίου
|
||||
|
@ -1946,8 +1946,8 @@ pulls.delete.title = Delete this pull request?
|
||||
pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived)
|
||||
|
||||
pulls.recently_pushed_new_branches = You pushed on branch <strong>%[1]s</strong> %[2]s
|
||||
pulls.upstream_diverging_prompt_behind_1 = This branch is %d commit behind %s
|
||||
pulls.upstream_diverging_prompt_behind_n = This branch is %d commits behind %s
|
||||
pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[2]s
|
||||
pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s
|
||||
pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes
|
||||
pulls.upstream_diverging_merge = Sync fork
|
||||
|
||||
@ -3722,6 +3722,7 @@ runners.status.active = Active
|
||||
runners.status.offline = Offline
|
||||
runners.version = Version
|
||||
runners.reset_registration_token = Reset registration token
|
||||
runners.reset_registration_token_confirm = Would you like to invalidate the current token and generate a new one?
|
||||
runners.reset_registration_token_success = Runner registration token reset successfully
|
||||
|
||||
runs.all_workflows = All Workflows
|
||||
@ -3773,6 +3774,9 @@ variables.creation.success = The variable "%s" has been added.
|
||||
variables.update.failed = Failed to edit variable.
|
||||
variables.update.success = The variable has been edited.
|
||||
|
||||
logs.always_auto_scroll = Always auto scroll logs
|
||||
logs.always_expand_running = Always expand running logs
|
||||
|
||||
[projects]
|
||||
deleted.display_name = Deleted Project
|
||||
type-1.display_name = Individual Project
|
||||
|
@ -3415,6 +3415,7 @@ variables.creation.success=La variable "%s" ha sido añadida.
|
||||
variables.update.failed=Error al editar la variable.
|
||||
variables.update.success=La variable ha sido editada.
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Proyecto individual
|
||||
type-2.display_name=Proyecto repositorio
|
||||
|
@ -2529,6 +2529,7 @@ runs.commit=کامیت
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
@ -1707,6 +1707,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
@ -3772,6 +3772,7 @@ variables.creation.success=La variable « %s » a été ajoutée.
|
||||
variables.update.failed=Impossible d’éditer la variable.
|
||||
variables.update.success=La variable a bien été modifiée.
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Projet supprimé
|
||||
type-1.display_name=Projet personnel
|
||||
|
@ -1945,8 +1945,6 @@ pulls.delete.title=Scrios an t-iarratas tarraingthe seo?
|
||||
pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann)
|
||||
|
||||
pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse <strong>%[1]s</strong> %[2]s
|
||||
pulls.upstream_diverging_prompt_behind_1=Tá an brainse seo %d tiomantas taobh thiar de %s
|
||||
pulls.upstream_diverging_prompt_behind_n=Tá an brainse seo %d geallta taobh thiar de %s
|
||||
pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s
|
||||
pulls.upstream_diverging_merge=Forc sionc
|
||||
|
||||
@ -3772,6 +3770,7 @@ variables.creation.success=Tá an athróg "%s" curtha leis.
|
||||
variables.update.failed=Theip ar athróg a chur in eagar.
|
||||
variables.update.success=Tá an t-athróg curtha in eagar.
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Tionscadal scriosta
|
||||
type-1.display_name=Tionscadal Aonair
|
||||
|
@ -1615,6 +1615,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
@ -1444,6 +1444,7 @@ variables.creation.success=Variabel "%s" telah ditambahkan.
|
||||
variables.update.failed=Gagal mengedit variabel.
|
||||
variables.update.success=Variabel telah diedit.
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Proyek Individu
|
||||
type-2.display_name=Proyek Repositori
|
||||
|
@ -1342,6 +1342,7 @@ runs.commit=Framlag
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
@ -2807,6 +2807,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
@ -1669,12 +1669,25 @@ issues.delete.title=このイシューを削除しますか?
|
||||
issues.delete.text=本当にこのイシューを削除しますか? (これはすべてのコンテンツを完全に削除します。 保存しておきたい場合は、代わりにクローズすることを検討してください)
|
||||
|
||||
issues.tracker=タイムトラッカー
|
||||
issues.timetracker_timer_start=タイマー開始
|
||||
issues.timetracker_timer_stop=タイマー終了
|
||||
issues.timetracker_timer_discard=タイマー破棄
|
||||
issues.timetracker_timer_manually_add=時間を追加
|
||||
|
||||
issues.time_estimate_set=見積時間を設定
|
||||
issues.time_estimate_display=見積時間: %s
|
||||
issues.change_time_estimate_at=が見積時間を <b>%s</b> に変更 %s
|
||||
issues.remove_time_estimate_at=が見積時間を削除 %s
|
||||
issues.time_estimate_invalid=見積時間のフォーマットが不正です
|
||||
issues.start_tracking_history=が作業を開始 %s
|
||||
issues.tracker_auto_close=タイマーは、このイシューがクローズされると自動的に終了します
|
||||
issues.tracking_already_started=`<a href="%s">別のイシュー</a>で既にタイムトラッキングを開始しています!`
|
||||
issues.stop_tracking_history=が <b>%s</b> の作業を終了 %s
|
||||
issues.cancel_tracking_history=`がタイムトラッキングを中止 %s`
|
||||
issues.del_time=このタイムログを削除
|
||||
issues.add_time_history=が作業時間 <b>%s</b> を追加 %s
|
||||
issues.del_time_history=`が作業時間を削除 %s`
|
||||
issues.add_time_manually=時間の手入力
|
||||
issues.add_time_hours=時間
|
||||
issues.add_time_minutes=分
|
||||
issues.add_time_sum_to_small=時間が入力されていません。
|
||||
@ -1694,15 +1707,15 @@ issues.due_date_form_add=期日の追加
|
||||
issues.due_date_form_edit=変更
|
||||
issues.due_date_form_remove=削除
|
||||
issues.due_date_not_writer=イシューの期日を変更するには、リポジトリへの書き込み権限が必要です。
|
||||
issues.due_date_not_set=期日は未設定です。
|
||||
issues.due_date_not_set=期日は設定されていません。
|
||||
issues.due_date_added=が期日 %s を追加 %s
|
||||
issues.due_date_modified=が期日を %[2]s から %[1]s に変更 %[3]s
|
||||
issues.due_date_remove=が期日 %s を削除 %s
|
||||
issues.due_date_overdue=期日は過ぎています
|
||||
issues.due_date_invalid=期日が正しくないか範囲を超えています。 'yyyy-mm-dd' の形式で入力してください。
|
||||
issues.dependency.title=依存関係
|
||||
issues.dependency.issue_no_dependencies=依存関係が設定されていません。
|
||||
issues.dependency.pr_no_dependencies=依存関係が設定されていません。
|
||||
issues.dependency.issue_no_dependencies=依存関係は設定されていません。
|
||||
issues.dependency.pr_no_dependencies=依存関係は設定されていません。
|
||||
issues.dependency.no_permission_1=%d 個の依存関係への読み取り権限がありません
|
||||
issues.dependency.no_permission_n=%d 個の依存関係への読み取り権限がありません
|
||||
issues.dependency.no_permission.can_remove=この依存関係への読み取り権限はありませんが、この依存関係は削除できます
|
||||
@ -1927,8 +1940,6 @@ pulls.delete.title=このプルリクエストを削除しますか?
|
||||
pulls.delete.text=本当にこのプルリクエストを削除しますか? (これはすべてのコンテンツを完全に削除します。 保存しておきたい場合は、代わりにクローズすることを検討してください)
|
||||
|
||||
pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ <strong>%[1]s</strong> にプッシュしました
|
||||
pulls.upstream_diverging_prompt_behind_1=このブランチは %[2]s よりも %[1]d コミット遅れています
|
||||
pulls.upstream_diverging_prompt_behind_n=このブランチは %[2]s よりも %[1]d コミット遅れています
|
||||
pulls.upstream_diverging_prompt_base_newer=ベースブランチ %s に新しい変更があります
|
||||
pulls.upstream_diverging_merge=フォークを同期
|
||||
|
||||
@ -3751,6 +3762,7 @@ variables.creation.success=変数 "%s" を追加しました。
|
||||
variables.update.failed=変数を更新できませんでした。
|
||||
variables.update.success=変数を更新しました。
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=削除されたプロジェクト
|
||||
type-1.display_name=個人プロジェクト
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user