diff --git a/.drone.yml b/.drone.yml index ad4757cba0259..a0f765532a244 100644 --- a/.drone.yml +++ b/.drone.yml @@ -57,7 +57,7 @@ steps: - name: build-backend-no-gcc pull: always - image: golang:1.12 # this step is kept as the lowest version of golang that we support + image: golang:1.13 # this step is kept as the lowest version of golang that we support environment: GO111MODULE: on GOPROXY: off diff --git a/.stylelintrc b/.stylelintrc index 0e1b38228fe9c..427d89b5bcda7 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -5,6 +5,7 @@ rules: block-closing-brace-empty-line-before: null color-hex-length: null comment-empty-line-before: null + declaration-block-single-line-max-declarations: null declaration-empty-line-before: null indentation: 2 no-descending-specificity: null diff --git a/CHANGELOG.md b/CHANGELOG.md index 351749f946303..89e7601dbc9ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ This changelog goes through all the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.io). +## [1.12.4](https://github.com/go-gitea/gitea/releases/tag/v1.12.4) - 2020-09-02 + +* SECURITY + * Escape provider name in oauth2 provider redirect (#12648) (#12650) + * Escape Email on password reset page (#12610) (#12612) + * When reading expired sessions - expire them (#12686) (#12690) +* ENHANCEMENTS + * StaticRootPath configurable at compile time (#12371) (#12652) +* BUGFIXES + * Fix to show an issue that is related to a deleted issue (#12651) (#12692) + * Expire time acknowledged for cache (#12605) (#12611) + * Fix diff path unquoting (#12554) (#12575) + * Improve HTML escaping helper (#12562) + * models: break out of loop (#12386) (#12561) + * Default empty merger list to those with write permissions (#12535) (#12560) + * Skip SSPI authentication attempts for /api/internal (#12556) (#12559) + * Prevent NPE on commenting on lines with invalidated comments (#12549) (#12550) + * Remove hardcoded ES indexername (#12521) (#12526) + * Fix bug preventing transfer to private organization (#12497) (#12501) + * Keys should not verify revoked email addresses (#12486) (#12495) + * Do not add prefix on http/https submodule links (#12477) (#12479) + * Fix ignored login on compare (#12476) (#12478) + * Fix incorrect error logging in Stats indexer and OAuth2 (#12387) (#12422) + * Upgrade google/go-github to v32.1.0 (#12361) (#12390) + * Render emoji's of Commit message on feed-page (#12373) + * Fix handling of diff on unrelated branches when Git 2.28 used (#12370) + ## [1.12.3](https://github.com/go-gitea/gitea/releases/tag/v1.12.3) - 2020-07-28 * BUGFIXES diff --git a/Makefile b/Makefile index 777b057094cd3..a3948462006c3 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" ) COMMA := , XGO_VERSION := go-1.15.x -MIN_GO_VERSION := 001012000 +MIN_GO_VERSION := 001013000 MIN_NODE_VERSION := 010013000 DOCKER_IMAGE ?= gitea/gitea @@ -155,6 +155,7 @@ help: @echo " - build build everything" @echo " - frontend build frontend files" @echo " - backend build backend files" + @echo " - watch watch everything and continuously rebuild" @echo " - watch-frontend watch frontend files and continuously rebuild" @echo " - watch-backend watch backend files and continuously rebuild" @echo " - clean delete backend and integration files" @@ -186,7 +187,7 @@ help: go-check: $(eval GO_VERSION := $(shell printf "%03d%03d%03d" $(shell go version | grep -Eo '[0-9]+\.[0-9.]+' | tr '.' ' ');)) @if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \ - echo "Gitea requires Go 1.12 or greater to build. You can get it at https://golang.org/dl/"; \ + echo "Gitea requires Go 1.13 or greater to build. You can get it at https://golang.org/dl/"; \ exit 1; \ fi @@ -316,6 +317,10 @@ lint-frontend: node_modules .PHONY: lint-backend lint-backend: golangci-lint revive vet +.PHONY: watch +watch: + bash tools/watch.sh + .PHONY: watch-frontend watch-frontend: node-check $(FOMANTIC_DEST) node_modules rm -rf $(WEBPACK_DEST_ENTRIES) @@ -612,7 +617,7 @@ release-docs: | $(DIST_DIRS) docs .PHONY: docs docs: @hash hugo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ - $(GO) get -u github.com/gohugoio/hugo; \ + curl -sL https://github.com/gohugoio/hugo/releases/download/v0.74.3/hugo_0.74.3_Linux-64bit.tar.gz | tar zxf - -C /tmp && mv /tmp/hugo /usr/bin/hugo && chmod +x /usr/bin/hugo; \ fi cd docs; make trans-copy clean build-offline; diff --git a/README.md b/README.md index e46ae4cd1af7c..163daa85b6b74 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ or if sqlite support is required: The `build` target is split into two sub-targets: -- `make backend` which requires [Go 1.12](https://golang.org/dl/) or greater. +- `make backend` which requires [Go 1.13](https://golang.org/dl/) or greater. - `make frontend` which requires [Node.js 10.13](https://nodejs.org/en/download/) or greater. If pre-built frontend files are present it is possible to only build the backend: diff --git a/build/generate-gitignores.go b/build/generate-gitignores.go index f341c1ec484eb..846bb076365d2 100644 --- a/build/generate-gitignores.go +++ b/build/generate-gitignores.go @@ -21,12 +21,16 @@ import ( func main() { var ( - prefix = "gitea-gitignore" - url = "https://api.github.com/repos/github/gitignore/tarball" - destination = "" + prefix = "gitea-gitignore" + url = "https://api.github.com/repos/github/gitignore/tarball" + githubApiToken = "" + githubUsername = "" + destination = "" ) flag.StringVar(&destination, "dest", "options/gitignore/", "destination for the gitignores") + flag.StringVar(&githubUsername, "username", "", "github username") + flag.StringVar(&githubApiToken, "token", "", "github api token") flag.Parse() file, err := ioutil.TempFile(os.TempDir(), prefix) @@ -37,12 +41,19 @@ func main() { defer util.Remove(file.Name()) - resp, err := http.Get(url) - + req, err := http.NewRequest("GET", url, nil) if err != nil { log.Fatalf("Failed to download archive. %s", err) } + if len(githubApiToken) > 0 && len(githubUsername) > 0 { + req.SetBasicAuth(githubUsername, githubApiToken) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("Failed to download archive. %s", err) + } defer resp.Body.Close() if _, err := io.Copy(file, resp.Body); err != nil { diff --git a/build/generate-licenses.go b/build/generate-licenses.go index 53623e4193c81..9dd13adf9a7bc 100644 --- a/build/generate-licenses.go +++ b/build/generate-licenses.go @@ -21,12 +21,16 @@ import ( func main() { var ( - prefix = "gitea-licenses" - url = "https://api.github.com/repos/spdx/license-list-data/tarball" - destination = "" + prefix = "gitea-licenses" + url = "https://api.github.com/repos/spdx/license-list-data/tarball" + githubApiToken = "" + githubUsername = "" + destination = "" ) flag.StringVar(&destination, "dest", "options/license/", "destination for the licenses") + flag.StringVar(&githubUsername, "username", "", "github username") + flag.StringVar(&githubApiToken, "token", "", "github api token") flag.Parse() file, err := ioutil.TempFile(os.TempDir(), prefix) @@ -37,8 +41,16 @@ func main() { defer util.Remove(file.Name()) - resp, err := http.Get(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Fatalf("Failed to download archive. %s", err) + } + + if len(githubApiToken) > 0 && len(githubUsername) > 0 { + req.SetBasicAuth(githubUsername, githubApiToken) + } + resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatalf("Failed to download archive. %s", err) } diff --git a/cmd/admin.go b/cmd/admin.go index a049f7f2cf17b..9f81f5284dd6d 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -6,6 +6,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -265,6 +266,13 @@ func runChangePassword(c *cli.Context) error { if !pwd.IsComplexEnough(c.String("password")) { return errors.New("Password does not meet complexity requirements") } + pwned, err := pwd.IsPwned(context.Background(), c.String("password")) + if err != nil { + return err + } + if pwned { + return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords") + } uname := c.String("username") user, err := models.GetUserByName(uname) if err != nil { diff --git a/cmd/doctor.go b/cmd/doctor.go index 2a93db27da23a..2ca2bb5e70b6a 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "xorm.io/builder" + "xorm.io/xorm" "github.com/urfave/cli" ) @@ -62,6 +63,27 @@ var CmdDoctor = cli.Command{ Usage: `Name of the log file (default: "doctor.log"). Set to "-" to output to stdout, set to "" to disable`, }, }, + Subcommands: []cli.Command{ + cmdRecreateTable, + }, +} + +var cmdRecreateTable = cli.Command{ + Name: "recreate-table", + Usage: "Recreate tables from XORM definitions and copy the data.", + ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "debug", + Usage: "Print SQL commands sent", + }, + }, + Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns. + +This command will cause Xorm to recreate tables, copying over the data and deleting the old table. + +You should back-up your database before doing this and ensure that your database is up-to-date first.`, + Action: runRecreateTable, } type check struct { @@ -136,6 +158,47 @@ var checklist = []check{ // more checks please append here } +func runRecreateTable(ctx *cli.Context) error { + // Redirect the default golog to here + golog.SetFlags(0) + golog.SetPrefix("") + golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT))) + + setting.NewContext() + setting.InitDBConfig() + + setting.EnableXORMLog = ctx.Bool("debug") + setting.Database.LogSQL = ctx.Bool("debug") + setting.Cfg.Section("log").Key("XORM").SetValue(",") + + setting.NewXORMLogService(!ctx.Bool("debug")) + if err := models.SetEngine(); err != nil { + fmt.Println(err) + fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.") + return nil + } + + args := ctx.Args() + names := make([]string, 0, ctx.NArg()) + for i := 0; i < ctx.NArg(); i++ { + names = append(names, args.Get(i)) + } + + beans, err := models.NamesToBean(names...) + if err != nil { + return err + } + recreateTables := migrations.RecreateTables(beans...) + + return models.NewEngine(context.Background(), func(x *xorm.Engine) error { + if err := migrations.EnsureUpToDate(x); err != nil { + return err + } + return recreateTables(x) + }) + +} + func runDoctor(ctx *cli.Context) error { // Silence the default loggers diff --git a/cmd/dump.go b/cmd/dump.go index c64734122106e..0e41ecb8c7efb 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -186,6 +186,10 @@ func runDump(ctx *cli.Context) error { if _, err := setting.Cfg.Section("log.console").NewKey("STDERR", "true"); err != nil { fatal("Setting console logger to stderr failed: %v", err) } + if !setting.InstallLock { + log.Error("Is '%s' really the right config path?\n", setting.CustomConf) + return fmt.Errorf("gitea is not initialized") + } setting.NewServices() // cannot access session settings otherwise err := models.SetEngine() diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index 3a26f0b3f5b8b..b8e45c954d68e 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -83,6 +83,13 @@ func migrateAttachments(dstStorage storage.ObjectStorage) error { }) } +func migrateLFS(dstStorage storage.ObjectStorage) error { + return models.IterateLFS(func(mo *models.LFSMetaObject) error { + _, err := storage.Copy(dstStorage, mo.RelativePath(), storage.LFS, mo.RelativePath()) + return err + }) +} + func runMigrateStorage(ctx *cli.Context) error { if err := initDB(); err != nil { return err @@ -103,45 +110,50 @@ func runMigrateStorage(ctx *cli.Context) error { return err } + var dstStorage storage.ObjectStorage + var err error + switch ctx.String("store") { + case "local": + p := ctx.String("path") + if p == "" { + log.Fatal("Path must be given when store is loal") + return nil + } + dstStorage, err = storage.NewLocalStorage(p) + case "minio": + dstStorage, err = storage.NewMinioStorage( + context.Background(), + ctx.String("minio-endpoint"), + ctx.String("minio-access-key-id"), + ctx.String("minio-secret-access-key"), + ctx.String("minio-bucket"), + ctx.String("minio-location"), + ctx.String("minio-base-path"), + ctx.Bool("minio-use-ssl"), + ) + default: + return fmt.Errorf("Unsupported attachments store type: %s", ctx.String("store")) + } + + if err != nil { + return err + } + tp := ctx.String("type") switch tp { case "attachments": - var dstStorage storage.ObjectStorage - var err error - switch ctx.String("store") { - case "local": - p := ctx.String("path") - if p == "" { - log.Fatal("Path must be given when store is loal") - return nil - } - dstStorage, err = storage.NewLocalStorage(p) - case "minio": - dstStorage, err = storage.NewMinioStorage( - context.Background(), - ctx.String("minio-endpoint"), - ctx.String("minio-access-key-id"), - ctx.String("minio-secret-access-key"), - ctx.String("minio-bucket"), - ctx.String("minio-location"), - ctx.String("minio-base-path"), - ctx.Bool("minio-use-ssl"), - ) - default: - return fmt.Errorf("Unsupported attachments store type: %s", ctx.String("store")) - } - - if err != nil { + if err := migrateAttachments(dstStorage); err != nil { return err } - if err := migrateAttachments(dstStorage); err != nil { + case "lfs": + if err := migrateLFS(dstStorage); err != nil { return err } - - log.Warn("All files have been copied to the new placement but old files are still on the orignial placement.") - - return nil + default: + return fmt.Errorf("Unsupported storage: %s", ctx.String("type")) } + log.Warn("All files have been copied to the new placement but old files are still on the orignial placement.") + return nil } diff --git a/cmd/serv.go b/cmd/serv.go index 2c035111d8fd7..b6cdab8334c4d 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -218,6 +218,7 @@ func runServ(c *cli.Context) error { os.Setenv(models.EnvPRID, fmt.Sprintf("%d", 0)) os.Setenv(models.EnvIsDeployKey, fmt.Sprintf("%t", results.IsDeployKey)) os.Setenv(models.EnvKeyID, fmt.Sprintf("%d", results.KeyID)) + os.Setenv(models.EnvAppURL, setting.AppURL) //LFS token authentication if verb == lfsAuthenticateVerb { diff --git a/contrib/k8s/gitea.yml b/contrib/k8s/gitea.yml deleted file mode 100644 index c4aed869f7fe7..0000000000000 --- a/contrib/k8s/gitea.yml +++ /dev/null @@ -1,107 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: gitea ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: gitea - namespace: gitea - labels: - app: gitea -spec: - replicas: 1 - template: - metadata: - name: gitea - labels: - app: gitea - spec: - containers: - - name: gitea - image: gitea/gitea:latest - imagePullPolicy: Always - volumeMounts: - - mountPath: "/var/lib/gitea" - name: "root" - - mountPath: "/data" - name: "data" - ports: - - containerPort: 22 - name: ssh - protocol: TCP - - containerPort: 3000 - name: http - protocol: TCP - restartPolicy: Always - volumes: - # Set up a data directory for gitea - # For production usage, you should consider using PV/PVC instead(or simply using storage like NAS) - # For more details, please see https://kubernetes.io/docs/concepts/storage/volumes/ - - name: "root" - hostPath: - # directory location on host - path: "/var/lib/gitea" - # this field is optional - type: Directory - - name: "data" - hostPath: - path: "/data/gitea" - type: Directory - selector: - matchLabels: - app: gitea ---- -# Using cluster mode -apiVersion: v1 -kind: Service -metadata: - name: gitea-web - namespace: gitea - labels: - app: gitea-web -spec: - ports: - - port: 80 - targetPort: 3000 - name: http - selector: - app: gitea ---- -# Using node-port mode -# This mainly open a specific TCP port for SSH usage on each host, -# so you can use a proxy layer to handle it(e.g. slb, nginx) -apiVersion: v1 -kind: Service -metadata: - name: gitea-ssh - namespace: gitea - labels: - app: gitea-ssh -spec: - ports: - - port: 22 - targetPort: 22 - nodePort: 30022 - name: ssh - selector: - app: gitea - type: NodePort ---- -# Ingress is always suitable for HTTP usage, -# we suggest using an proxy layer such as slb to send traffic to different ports. -# Usually 80/443 for web and 22 directly for SSH. -apiVersion: extensions/v1beta1 -kind: Ingress -metadata: - name: gitea - namespace: gitea -spec: - rules: - - host: your-gitea-host.com - http: - paths: - - backend: - serviceName: gitea-web - servicePort: 80 diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index bb65c4f08dc9f..af3418f70c9b0 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -433,7 +433,7 @@ REPO_INDEXER_TYPE = bleve ; Index file used for code search. REPO_INDEXER_PATH = indexers/repos.bleve ; Code indexer connection string, available when `REPO_INDEXER_TYPE` is elasticsearch. i.e. http://elastic:changeme@localhost:9200 -REPO_INDEXER_CONN_STR = +REPO_INDEXER_CONN_STR = ; Code indexer name, available when `REPO_INDEXER_TYPE` is elasticsearch REPO_INDEXER_NAME = gitea_codes @@ -512,6 +512,8 @@ PASSWORD_COMPLEXITY = off PASSWORD_HASH_ALGO = argon2 ; Set false to allow JavaScript to read CSRF cookie CSRF_COOKIE_HTTP_ONLY = true +; Validate against https://haveibeenpwned.com/Passwords to see if a password has been exposed +PASSWORD_CHECK_PWN = false [openid] ; diff --git a/docs/config.yaml b/docs/config.yaml index 3935d04dd3612..3447cf3a1b33c 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -18,8 +18,8 @@ params: description: Git with a cup of tea author: The Gitea Authors website: https://docs.gitea.io - version: 1.12.2 - minGoVersion: 1.12 + version: 1.12.4 + minGoVersion: 1.13 goVersion: 1.15 minNodeVersion: 10.13 diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index f86415c2888a7..7f969add2c902 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -206,12 +206,23 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars. - `ENABLE_GZIP`: **false**: Enables application-level GZIP support. - `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore, organizations, login\]. + - `LFS_START_SERVER`: **false**: Enables git-lfs support. -- `LFS_CONTENT_PATH`: **./data/lfs**: Where to store LFS files. +- `LFS_STORE_TYPE`: **local**: Storage type for lfs, `local` for local disk or `minio` for s3 compatible object storage service. +- `LFS_SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing. +- `LFS_CONTENT_PATH`: **./data/lfs**: Where to store LFS files, only available when `LFS_STORE_TYPE` is `local`. +- `LFS_MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `LFS_STORE_TYPE` is `minio` +- `LFS_MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `LFS_STORE_TYPE` is `minio` +- `LFS_MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `LFS_STORE_TYPE is` `minio` +- `LFS_MINIO_BUCKET`: **gitea**: Minio bucket to store the lfs only available when `LFS_STORE_TYPE` is `minio` +- `LFS_MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `LFS_STORE_TYPE` is `minio` +- `LFS_MINIO_BASE_PATH`: **lfs/**: Minio base path on the bucket only available when `LFS_STORE_TYPE` is `minio` +- `LFS_MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `LFS_STORE_TYPE` is `minio` - `LFS_JWT_SECRET`: **\**: LFS authentication secret, change this a unique string. - `LFS_HTTP_AUTH_EXPIRY`: **20m**: LFS authentication validity period in time.Duration, pushes taking longer than this may fail. - `LFS_MAX_FILE_SIZE`: **0**: Maximum allowed LFS file size in bytes (Set to 0 for no limit). - `LFS_LOCK_PAGING_NUM`: **50**: Maximum number of LFS Locks returned per page. + - `REDIRECT_OTHER_PORT`: **false**: If true and `PROTOCOL` is https, allows redirecting http requests on `PORT_TO_REDIRECT` to the https port Gitea listens on. - `PORT_TO_REDIRECT`: **80**: Port for the http redirection service to listen on. Used when `REDIRECT_OTHER_PORT` is true. - `ENABLE_LETSENCRYPT`: **false**: If enabled you must set `DOMAIN` to valid internet facing domain (ensure DNS is set and port 80 is accessible by letsencrypt validation server). @@ -333,6 +344,7 @@ set name for unique queues. Individual queues will default to - digit - use one or more digits - spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~`` - off - do not check password complexity +- `PASSWORD_CHECK_PWN`: **false**: Check [HaveIBeenPwned](https://haveibeenpwned.com/Passwords) to see if a password has been exposed. ## OpenID (`openid`) diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md index d9a851b50845e..ac6c94dff2f27 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -69,8 +69,18 @@ menu: - `STATIC_CACHE_TIME`: **6h**: 静态资源文件,包括 `custom/`, `public/` 和所有上传的头像的浏览器缓存时间。 - `ENABLE_GZIP`: 启用应用级别的 GZIP 压缩。 - `LANDING_PAGE`: 未登录用户的默认页面,可选 `home` 或 `explore`。 + - `LFS_START_SERVER`: 是否启用 git-lfs 支持. 可以为 `true` 或 `false`, 默认是 `false`。 +- `LFS_STORE_TYPE`: **local**: LFS 的存储类型,`local` 将存储到磁盘,`minio` 将存储到 s3 兼容的对象服务。 +- `LFS_SERVE_DIRECT`: **false**: 允许直接重定向到存储系统。当前,仅 Minio/S3 是支持的。 - `LFS_CONTENT_PATH`: 存放 lfs 命令上传的文件的地方,默认是 `data/lfs`。 +- `LFS_MINIO_ENDPOINT`: **localhost:9000**: Minio 地址,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。 +- `LFS_MINIO_ACCESS_KEY_ID`: Minio accessKeyID,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。 +- `LFS_MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。 +- `LFS_MINIO_BUCKET`: **gitea**: Minio bucket,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。 +- `LFS_MINIO_LOCATION`: **us-east-1**: Minio location ,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。 +- `LFS_MINIO_BASE_PATH`: **lfs/**: Minio base path ,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。 +- `LFS_MINIO_USE_SSL`: **false**: Minio 是否启用 ssl ,仅当 `LFS_STORE_TYPE` 为 `minio` 时有效。 - `LFS_JWT_SECRET`: LFS 认证密钥,改成自己的。 ## Database (`database`) diff --git a/docs/content/doc/advanced/hacking-on-gitea.en-us.md b/docs/content/doc/advanced/hacking-on-gitea.en-us.md index 0c8ab419e4c6c..1d2702a5e8998 100644 --- a/docs/content/doc/advanced/hacking-on-gitea.en-us.md +++ b/docs/content/doc/advanced/hacking-on-gitea.en-us.md @@ -94,14 +94,10 @@ See `make help` for all available `make` targets. Also see [`.drone.yml`](https: ## Building continuously -Both the `frontend` and `backend` targets can be ran continuously when source files change: +To run and continously rebuild when source files change: ````bash -# in your first terminal -make watch-backend - -# in your second terminal -make watch-frontend +make watch ```` On macOS, watching all backend source files may hit the default open files limit which can be increased via `ulimit -n 12288` for the current shell or in your shell startup file for all future shells. diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md index 8e47b0224e0ca..dae530438b52a 100644 --- a/docs/content/doc/features/comparison.en-us.md +++ b/docs/content/doc/features/comparison.en-us.md @@ -84,7 +84,7 @@ _Symbols used in table:_ | Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | Lock Discussion | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Issue Boards | [✓](https://github.com/go-gitea/gitea/pull/8346) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Issue Boards (Kanban) | [✓](https://github.com/go-gitea/gitea/pull/8346) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | | Create new branches from issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | | Issue search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | | Global issue search | [✘](https://github.com/go-gitea/gitea/issues/2434) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | diff --git a/docs/content/doc/help/faq.en-us.md b/docs/content/doc/help/faq.en-us.md index 5752dc9233035..e265793f23f42 100644 --- a/docs/content/doc/help/faq.en-us.md +++ b/docs/content/doc/help/faq.en-us.md @@ -47,7 +47,8 @@ Also see [Support Options]({{< relref "doc/help/seek-help.en-us.md" >}}) * [How can I enable password reset](#how-can-i-enable-password-reset) * [How can a user's password be changed](#how-can-a-user-s-password-be-changed) * [Why is my markdown broken](#why-is-my-markdown-broken) - +* [Errors during upgrade on MySQL: Error 1118: Row size too large.](#upgrade-errors-with-mysql) +* [Why are emoji broken on MySQL](#why-are-emoji-broken-on-mysql) ## Difference between 1.x and 1.x.x downloads Version 1.7.x will be used for this example. @@ -308,3 +309,30 @@ There is no setting for password resets. It is enabled when a [mail service]({{< In Gitea version `1.11` we moved to [goldmark](https://github.com/yuin/goldmark) for markdown rendering, which is [CommonMark](https://commonmark.org/) compliant. If you have markdown that worked as you expected prior to version `1.11` and after upgrading it's not working anymore, please look through the CommonMark spec to see whether the problem is due to a bug or non-compliant syntax. If it is the latter, _usually_ there is a compliant alternative listed in the spec. + +## Upgrade errors with MySQL + +If you are receiving errors on upgrade of Gitea using MySQL that read: + +> `ORM engine initialization failed: migrate: do migrate: Error: 1118: Row size too large...` + +Please run `gitea convert` or run `ALTER TABLE table_name ROW_FORMAT=dynamic;` for each table in the database. + +The underlying problem is that the space allocated for indices by the default row format +is too small. Gitea requires that the `ROWFORMAT` for its tables is `DYNAMIC`. + +If you are receiving an error line containing `Error 1071: Specified key was too long; max key length is 1000 bytes...` +then you are attempting to run Gitea on tables which use the ISAM engine. While this may have worked by chance in previous versions of Gitea, it has never been officially supported and +you must use InnoDB. You should run `ALTER TABLE table_name ENGINE=InnoDB;` for each table in the database. + +## Why Are Emoji Broken On MySQL + +Unfortunately MySQL's `utf8` charset does not completely allow all possible UTF-8 characters, in particular Emoji. +They created a new charset and collation called `utf8mb4` that allows for emoji to be stored but tables which use +the `utf8` charset, and connections which use the `utf8` charset will not use this. + +Please run `gitea convert`, or run `ALTER DATABASE database_name CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;` +for the database_name and run `ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;` +for each table in the database. + +You will also need to change the app.ini database charset to `CHARSET=utf8mb4`. diff --git a/docs/content/doc/usage/command-line.en-us.md b/docs/content/doc/usage/command-line.en-us.md index e458b11ba47ba..49df30edb0276 100644 --- a/docs/content/doc/usage/command-line.en-us.md +++ b/docs/content/doc/usage/command-line.en-us.md @@ -319,6 +319,36 @@ var checklist = []check{ This function will receive a command line context and return a list of details about the problems or error. +##### doctor recreate-table + +Sometimes when there are migrations the old columns and default values may be left +unchanged in the database schema. This may lead to warning such as: + +``` +2020/08/02 11:32:29 ...rm/session_schema.go:360:Sync2() [W] Table user Column keep_activity_private db default is , struct default is 0 +``` + +You can cause Gitea to recreate these tables and copy the old data into the new table +with the defaults set appropriately by using: + +``` +gitea doctor recreate-table user +``` + +You can ask gitea to recreate multiple tables using: + +``` +gitea doctor recreate-table table1 table2 ... +``` + +And if you would like Gitea to recreate all tables simply call: + +``` +gitea doctor recreate-table +``` + +It is highly recommended to back-up your database before running these commands. + #### manager Manage running server operations: diff --git a/docs/content/doc/usage/linked-references.en-us.md b/docs/content/doc/usage/linked-references.en-us.md index d2836f8571994..bbfd1ef64ea2c 100644 --- a/docs/content/doc/usage/linked-references.en-us.md +++ b/docs/content/doc/usage/linked-references.en-us.md @@ -42,7 +42,6 @@ Example: This is also valid for teams and organizations: > [@Documenters](#), we need to plan for this. - > [@CoolCompanyInc](#), this issue concerns us all! Teams will receive mail notifications when appropriate, but whole organizations won't. @@ -123,6 +122,33 @@ The default _keywords_ are: * **Closing**: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved * **Reopening**: reopen, reopens, reopened +## Time tracking in Pull Requests and Commit Messages + +When commit or merging of pull request results in automatic closing of issue +it is possible to also add spent time resolving this issue through commit message. + +To specify spent time on resolving issue you need to specify time in format +`@` after issue number. In one commit message you can specify +multiple fixed issues and spent time for each of them. + +Supported time units (``): + +* `m` - minutes +* `h` - hours +* `d` - days (equals to 8 hours) +* `w` - weeks (equals to 5 days) +* `mo` - months (equals to 4 weeks) + +Numbers to specify time (``) can be also decimal numbers, ex. `@1.5h` would +result in one and half hours. Multiple time units can be combined, ex. `@1h10m` would +mean 1 hour and 10 minutes. + +Example of commit message: + +> Fixed #123 spent @1h, refs #102, fixes #124 @1.5h + +This would result in 1 hour added to issue #123 and 1 and half hours added to issue #124. + ## External Trackers Gitea supports the use of external issue trackers, and references to issues @@ -132,7 +158,6 @@ the pull requests hosted in Gitea. To address this, Gitea allows the use of the `!` marker to identify pull requests. For example: > This is issue [#1234](#), and links to the external tracker. - > This is pull request [!1234](#), and links to a pull request in Gitea. The `!` and `#` can be used interchangeably for issues and pull request _except_ diff --git a/go.mod b/go.mod index ab5c376c6e5cc..38fb7f8390749 100644 --- a/go.mod +++ b/go.mod @@ -18,15 +18,15 @@ require ( gitea.com/macaron/toolbox v0.0.0-20190822013122-05ff0fc766b7 github.com/BurntSushi/toml v0.3.1 github.com/PuerkitoBio/goquery v1.5.1 - github.com/RoaringBitmap/roaring v0.4.23 // indirect github.com/alecthomas/chroma v0.8.0 - github.com/blevesearch/bleve v1.0.7 + github.com/blevesearch/bleve v1.0.10 github.com/couchbase/gomemcached v0.0.0-20191004160342-7b5da2ec40b2 // indirect github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d // indirect github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/dlclark/regexp2 v1.2.1 // indirect github.com/dustin/go-humanize v1.0.0 github.com/editorconfig/editorconfig-core-go/v2 v2.1.1 github.com/emirpasic/gods v1.12.0 @@ -49,7 +49,8 @@ require ( github.com/google/go-github/v32 v32.1.0 github.com/google/uuid v1.1.1 github.com/gorilla/context v1.1.1 - github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-retryablehttp v0.6.7 // indirect + github.com/hashicorp/go-version v0.0.0-00010101000000-000000000000 github.com/huandu/xstrings v1.3.0 github.com/issue9/assert v1.3.2 // indirect github.com/issue9/identicon v1.0.1 @@ -64,7 +65,6 @@ require ( github.com/markbates/goth v1.61.2 github.com/mattn/go-isatty v0.0.12 github.com/mattn/go-sqlite3 v1.14.0 - github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81 github.com/mgechev/revive v1.0.2 github.com/mholt/archiver/v3 v3.3.0 @@ -72,8 +72,8 @@ require ( github.com/minio/minio-go/v7 v7.0.4 github.com/mitchellh/go-homedir v1.1.0 github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc - github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 - github.com/niklasfasching/go-org v0.1.9 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 + github.com/niklasfasching/go-org v1.3.2 github.com/oliamb/cutter v0.2.2 github.com/olivere/elastic/v7 v7.0.9 github.com/pkg/errors v0.9.1 @@ -93,18 +93,19 @@ require ( github.com/unknwon/i18n v0.0.0-20190805065654-5c6446a380b6 github.com/unknwon/paginater v0.0.0-20151104151617-7748a72e0141 github.com/urfave/cli v1.20.0 - github.com/xanzy/go-gitlab v0.31.0 + github.com/xanzy/go-gitlab v0.37.0 github.com/yohcop/openid-go v1.0.0 github.com/yuin/goldmark v1.2.1 github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 github.com/yuin/goldmark-meta v0.0.0-20191126180153-f0638e958b60 + go.jolheiser.com/pwn v0.0.3 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a - golang.org/x/net v0.0.0-20200707034311-ab3426394381 + golang.org/x/net v0.0.0-20200904194848-62affa334b73 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae golang.org/x/text v0.3.3 - golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect - golang.org/x/tools v0.0.0-20200814230902-9882f1d1823d + golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect + golang.org/x/tools v0.0.0-20200825202427-b303f430e36d gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/asn1-ber.v1 v1.0.0-20150924051756-4e86f4367175 // indirect gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df @@ -114,5 +115,7 @@ require ( mvdan.cc/xurls/v2 v2.1.0 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 xorm.io/builder v0.3.7 - xorm.io/xorm v1.0.4 + xorm.io/xorm v1.0.5 ) + +replace github.com/hashicorp/go-version => github.com/6543/go-version v1.2.3 diff --git a/go.sum b/go.sum index 08a1d20ffd191..e4a0a2562179d 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ gitea.com/macaron/toolbox v0.0.0-20190822013122-05ff0fc766b7 h1:N9QFoeNsUXLhl14m gitea.com/macaron/toolbox v0.0.0-20190822013122-05ff0fc766b7/go.mod h1:kgsbFPPS4P+acDYDOPDa3N4IWWOuDJt5/INKRUz7aks= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= +github.com/6543/go-version v1.2.3 h1:uF30BawMhoQLzqBeCwhFcWM6HVxlzMHe/zXbzJeKP+o= +github.com/6543/go-version v1.2.3/go.mod h1:fcfWh4zkneEgGXe8JJptiGwp8l6JgJJgS7oTw6P83So= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -61,8 +63,6 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/RoaringBitmap/roaring v0.4.21 h1:WJ/zIlNX4wQZ9x8Ey33O1UaD9TCTakYsdLFSBcTwH+8= -github.com/RoaringBitmap/roaring v0.4.21/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/RoaringBitmap/roaring v0.4.23 h1:gpyfd12QohbqhFO4NVDUdoPOCXsyahYRQhINmlHxKeo= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= @@ -115,8 +115,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/blevesearch/bleve v1.0.7 h1:4PspZE7XABMSKcVpzAKp0E05Yer1PIYmTWk+1ngNr/c= -github.com/blevesearch/bleve v1.0.7/go.mod h1:3xvmBtaw12Y4C9iA1RTzwWCof5j5HjydjCTiDE2TeE0= +github.com/blevesearch/bleve v1.0.10 h1:DxFXeC+faL+5LVTlljUDpP9eXj3mleiQem3DuSjepqQ= +github.com/blevesearch/bleve v1.0.10/go.mod h1:KHAOH5HuVGn9fo+dN5TkqcA1HcuOQ89goLWVWXZDl8w= github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040 h1:SjYVcfJVZoCfBlg+fkaq2eoZHTf5HaJfaTeTkOtyfHQ= github.com/blevesearch/blevex v0.0.0-20190916190636-152f0fe5c040/go.mod h1:WH+MU2F4T0VmSdaPX+Wu5GYoZBrYWdOZWSjzvYcDmqQ= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= @@ -127,10 +127,14 @@ github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= -github.com/blevesearch/zap/v11 v11.0.7 h1:nnmAOP6eXBkqEa1Srq1eqA5Wmn4w+BZjLdjynNxvd+M= -github.com/blevesearch/zap/v11 v11.0.7/go.mod h1:bJoY56fdU2m/IP4LLz/1h4jY2thBoREvoqbuJ8zhm9k= -github.com/blevesearch/zap/v12 v12.0.7 h1:y8FWSAYkdc4p1dn4YLxNNr1dxXlSUsakJh2Fc/r6cj4= -github.com/blevesearch/zap/v12 v12.0.7/go.mod h1:70DNK4ZN4tb42LubeDbfpp6xnm8g3ROYVvvZ6pEoXD8= +github.com/blevesearch/zap/v11 v11.0.10 h1:zJdl+cnxT0Yt2hA6meG+OIat3oSA4rERfrNX2CSchII= +github.com/blevesearch/zap/v11 v11.0.10/go.mod h1:BdqdgKy6u0Jgw/CqrMfP2Gue/EldcfvB/3eFzrzhIfw= +github.com/blevesearch/zap/v12 v12.0.10 h1:T1/GXNBxC9eetfuMwCM5RLWXeharSMyAdNEdXVtBuHA= +github.com/blevesearch/zap/v12 v12.0.10/go.mod h1:QtKkjpmV/sVFEnKSaIWPXZJAaekL97TrTV3ImhNx+nw= +github.com/blevesearch/zap/v13 v13.0.2 h1:quhI5OVFX33dhPpUW+nLyXGpu7QT8qTgzu6qA/fRRXM= +github.com/blevesearch/zap/v13 v13.0.2/go.mod h1:/9QLKla8/8mloJvQQutPhB+tw6y35urvKeAFeun2JGA= +github.com/blevesearch/zap/v14 v14.0.1 h1:s8KeqX53Vc4eRaziHsnY2bYUE+8IktWqRL9W5H5VDMY= +github.com/blevesearch/zap/v14 v14.0.1/go.mod h1:Y+tUL9TypMca5+96m7iJb2lpcntETXSeDoI5BBX2tvY= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 h1:U/lr3Dgy4WK+hNk4tyD+nuGjpVLPEHuJSFXMw11/HPA= @@ -162,8 +166,8 @@ github.com/couchbase/goutils v0.0.0-20190315194238-f9d42b11473b/go.mod h1:BQwMFl github.com/couchbase/goutils v0.0.0-20191018232750-b49639060d85 h1:0WMIDtuXCKEm4wtAJgAAXa/qtM5O9MariLwgHaRlYmk= github.com/couchbase/goutils v0.0.0-20191018232750-b49639060d85/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= -github.com/couchbase/vellum v1.0.1 h1:qrj9ohvZedvc51S5KzPfJ6P6z0Vqzv7Lx7k3mVc2WOk= -github.com/couchbase/vellum v1.0.1/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4= +github.com/couchbase/vellum v1.0.2 h1:BrbP0NKiyDdndMPec8Jjhy0U47CZ0Lgx3xUC2r9rZqw= +github.com/couchbase/vellum v1.0.2/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4= github.com/couchbaselabs/go-couchbase v0.0.0-20190708161019-23e7ca2ce2b7 h1:1XjEY/gnjQ+AfXef2U6dxCquhiRzkEpxZuWqs+QxTL8= github.com/couchbaselabs/go-couchbase v0.0.0-20190708161019-23e7ca2ce2b7/go.mod h1:mby/05p8HE5yHEAKiIH/555NoblMs7PtW6NrYshDruc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= @@ -194,6 +198,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.2.1 h1:Ff/S0snjr1oZHUNOkvA/gP6KUaMg5vDDl3Qnhjnwgm8= +github.com/dlclark/regexp2 v1.2.1/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= @@ -473,8 +479,8 @@ github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjh github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= -github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.6.7 h1:8/CAEZt/+F7kR7GevNHulKkUjLht3CPmn7egmhieNKo= +github.com/hashicorp/go-retryablehttp v0.6.7/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -657,8 +663,6 @@ github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/ github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk= -github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81 h1:QASJXOGm2RZ5Ardbc86qNFvby9AqkLDibfChMtAg5QM= github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= github.com/mgechev/revive v1.0.2 h1:v0NxxQ7fSFz/u1NQydPo6EGdq7va0J1BtsZmae6kzUg= @@ -701,12 +705,12 @@ github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOl github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc h1:z1PgdCCmYYVL0BoJTUgmAq1p7ca8fzYIPsNyfsN3xAU= github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc/go.mod h1:np1wUFZ6tyoke22qDJZY40URn9Ae51gX7ljIWXN5TJs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY= -github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/niklasfasching/go-org v0.1.9 h1:Toz8WMIt+qJb52uYEk1YD/muLuOOmRt1CfkV+bKVMkI= -github.com/niklasfasching/go-org v0.1.9/go.mod h1:AsLD6X7djzRIz4/RFZu8vwRL0VGjUvGZCCH1Nz0VdrU= +github.com/niklasfasching/go-org v1.3.2 h1:ZKTSd+GdJYkoZl1pBXLR/k7DRiRXnmB96TRiHmHdzwI= +github.com/niklasfasching/go-org v1.3.2/go.mod h1:AsLD6X7djzRIz4/RFZu8vwRL0VGjUvGZCCH1Nz0VdrU= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs= github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= @@ -886,8 +890,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/xanzy/go-gitlab v0.31.0 h1:+nHztQuCXGSMluKe5Q9IRaPdz6tO8O0gMkQ0vqGpiBk= -github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= +github.com/xanzy/go-gitlab v0.37.0 h1:Z/CQkjj5VwbWVYVL7S70kS/TFj5H/pJumV7xbJ0YUQ8= +github.com/xanzy/go-gitlab v0.37.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= @@ -912,10 +916,11 @@ github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= -go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.jolheiser.com/pwn v0.0.3 h1:MQowb3QvCL5r5NmHmCPxw93SdjfgJ0q6rAwYn4i1Hjg= +go.jolheiser.com/pwn v0.0.3/go.mod h1:/j5Dl8ftNqqJ8Dlx3YTrJV1wIR2lWOTyrNU3Qe7rk6I= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.1.1 h1:Sq1fR+0c58RME5EoqKdjkiQAmPjmfHlZOoRI6fTUOcs= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= @@ -973,7 +978,6 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -1008,8 +1012,9 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1068,15 +1073,14 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORK golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1107,11 +1111,10 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200225230052-807dcd883420/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224 h1:azwY/v0y0K4mFHVsg5+UrTgchqALYWpqVo6vL5OmkmI= golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200814230902-9882f1d1823d h1:XZxUC4/ZNKTjrT4/Oc9gCgIYnzPW3/CefdPjsndrVWM= -golang.org/x/tools v0.0.0-20200814230902-9882f1d1823d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1156,7 +1159,6 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0 h1:cJv5/xdbk1NnMPR1VP9+HU6gupuG9MLBoH1r6RHZ2MY= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= @@ -1183,9 +1185,7 @@ gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.44.2/go.mod h1:M3Cogqpuv0QCi3ExAY5V4uOt4qb/R3xZubo9m8lK5wg= gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.60.1 h1:P5y5shSkb0CFe44qEeMBgn8JLow09MP17jlJHanke5g= gopkg.in/ini.v1 v1.60.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.60.2 h1:7i8mqModL63zqi8nQn8Q3+0zvSCZy1AxhBgthKfi4WU= gopkg.in/ini.v1 v1.60.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -1227,5 +1227,5 @@ xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw= xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= xorm.io/xorm v0.8.0/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY= -xorm.io/xorm v1.0.4 h1:UBXA4I3NhiyjXfPqxXUkS2t5hMta9SSPATeMMaZg9oA= -xorm.io/xorm v1.0.4/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4= +xorm.io/xorm v1.0.5 h1:LRr5PfOUb4ODPR63YwbowkNDwcolT2LnkwP/TUaMaB0= +xorm.io/xorm v1.0.5/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4= diff --git a/integrations/api_issue_reaction_test.go b/integrations/api_issue_reaction_test.go index abbc6429fb49a..1906b8d09082c 100644 --- a/integrations/api_issue_reaction_test.go +++ b/integrations/api_issue_reaction_test.go @@ -11,25 +11,11 @@ import ( "time" "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) -func TestAPIAllowedReactions(t *testing.T) { - defer prepareTestEnv(t)() - - a := new(api.GeneralUISettings) - - req := NewRequest(t, "GET", "/api/v1/settings/ui") - resp := MakeRequest(t, req, http.StatusOK) - - DecodeJSON(t, resp, &a) - assert.Len(t, a.AllowedReactions, len(setting.UI.Reactions)) - assert.ElementsMatch(t, setting.UI.Reactions, a.AllowedReactions) -} - func TestAPIIssuesReactions(t *testing.T) { defer prepareTestEnv(t)() diff --git a/integrations/api_repo_test.go b/integrations/api_repo_test.go index 0d0d4a117b372..9d3599102a3b2 100644 --- a/integrations/api_repo_test.go +++ b/integrations/api_repo_test.go @@ -321,7 +321,16 @@ func TestAPIRepoMigrate(t *testing.T) { UID: int(testCase.userID), RepoName: testCase.repoName, }) - session.MakeRequest(t, req, testCase.expectedStatus) + resp := MakeRequest(t, req, NoExpectedStatus) + if resp.Code == http.StatusUnprocessableEntity { + respJSON := map[string]string{} + DecodeJSON(t, resp, &respJSON) + if assert.Equal(t, respJSON["message"], "Remote visit addressed rate limitation.") { + t.Log("test hit github rate limitation") + } + } else { + assert.EqualValues(t, testCase.expectedStatus, resp.Code) + } } } diff --git a/integrations/api_settings_test.go b/integrations/api_settings_test.go new file mode 100644 index 0000000000000..60dbf7a9dc0f3 --- /dev/null +++ b/integrations/api_settings_test.go @@ -0,0 +1,61 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestAPIExposedSettings(t *testing.T) { + defer prepareTestEnv(t)() + + ui := new(api.GeneralUISettings) + req := NewRequest(t, "GET", "/api/v1/settings/ui") + resp := MakeRequest(t, req, http.StatusOK) + + DecodeJSON(t, resp, &ui) + assert.Len(t, ui.AllowedReactions, len(setting.UI.Reactions)) + assert.ElementsMatch(t, setting.UI.Reactions, ui.AllowedReactions) + + apiSettings := new(api.GeneralAPISettings) + req = NewRequest(t, "GET", "/api/v1/settings/api") + resp = MakeRequest(t, req, http.StatusOK) + + DecodeJSON(t, resp, &apiSettings) + assert.EqualValues(t, &api.GeneralAPISettings{ + MaxResponseItems: setting.API.MaxResponseItems, + DefaultPagingNum: setting.API.DefaultPagingNum, + DefaultGitTreesPerPage: setting.API.DefaultGitTreesPerPage, + DefaultMaxBlobSize: setting.API.DefaultMaxBlobSize, + }, apiSettings) + + repo := new(api.GeneralRepoSettings) + req = NewRequest(t, "GET", "/api/v1/settings/repository") + resp = MakeRequest(t, req, http.StatusOK) + + DecodeJSON(t, resp, &repo) + assert.EqualValues(t, &api.GeneralRepoSettings{ + MirrorsDisabled: setting.Repository.DisableMirrors, + HTTPGitDisabled: setting.Repository.DisableHTTPGit, + }, repo) + + attachment := new(api.GeneralAttachmentSettings) + req = NewRequest(t, "GET", "/api/v1/settings/attachment") + resp = MakeRequest(t, req, http.StatusOK) + + DecodeJSON(t, resp, &attachment) + assert.EqualValues(t, &api.GeneralAttachmentSettings{ + Enabled: setting.Attachment.Enabled, + AllowedTypes: setting.Attachment.AllowedTypes, + MaxFiles: setting.Attachment.MaxFiles, + MaxSize: setting.Attachment.MaxSize, + }, attachment) +} diff --git a/integrations/lfs_getobject_test.go b/integrations/lfs_getobject_test.go index 45e94406a598e..431c7ed9e8d42 100644 --- a/integrations/lfs_getobject_test.go +++ b/integrations/lfs_getobject_test.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" "gitea.com/macaron/gzip" gzipp "github.com/klauspost/compress/gzip" @@ -49,8 +50,10 @@ func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string lfsID++ lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject) assert.NoError(t, err) - contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath} - if !contentStore.Exists(lfsMetaObject) { + contentStore := &lfs.ContentStore{ObjectStorage: storage.LFS} + exist, err := contentStore.Exists(lfsMetaObject) + assert.NoError(t, err) + if !exist { err := contentStore.Put(lfsMetaObject, bytes.NewReader(*content)) assert.NoError(t, err) } diff --git a/integrations/migration-test/migration_test.go b/integrations/migration-test/migration_test.go index 976a59a5791d8..940e4738ad389 100644 --- a/integrations/migration-test/migration_test.go +++ b/integrations/migration-test/migration_test.go @@ -258,6 +258,30 @@ func doMigrationTest(t *testing.T, version string) { err = models.NewEngine(context.Background(), wrappedMigrate) assert.NoError(t, err) currentEngine.Close() + + err = models.SetEngine() + assert.NoError(t, err) + + beans, _ := models.NamesToBean() + + err = models.NewEngine(context.Background(), func(x *xorm.Engine) error { + currentEngine = x + return migrations.RecreateTables(beans...)(x) + }) + assert.NoError(t, err) + currentEngine.Close() + + // We do this a second time to ensure that there is not a problem with retained indices + err = models.SetEngine() + assert.NoError(t, err) + + err = models.NewEngine(context.Background(), func(x *xorm.Engine) error { + currentEngine = x + return migrations.RecreateTables(beans...)(x) + }) + assert.NoError(t, err) + + currentEngine.Close() } func TestMigrations(t *testing.T) { diff --git a/integrations/mssql.ini.tmpl b/integrations/mssql.ini.tmpl index a8fbbe7fe5ae3..cfb35941261fc 100644 --- a/integrations/mssql.ini.tmpl +++ b/integrations/mssql.ini.tmpl @@ -14,6 +14,9 @@ ISSUE_INDEXER_PATH = integrations/indexers-mssql/issues.bleve REPO_INDEXER_ENABLED = true REPO_INDEXER_PATH = integrations/indexers-mssql/repos.bleve +[queue.code_indexer] +TYPE = immediate + [repository] ROOT = {{REPO_TEST_DIR}}integrations/gitea-integration-mssql/gitea-repositories diff --git a/integrations/mysql.ini.tmpl b/integrations/mysql.ini.tmpl index 5691311660c89..5211a4693afde 100644 --- a/integrations/mysql.ini.tmpl +++ b/integrations/mysql.ini.tmpl @@ -16,6 +16,9 @@ ISSUE_INDEXER_PATH = integrations/indexers-mysql/issues.bleve REPO_INDEXER_ENABLED = true REPO_INDEXER_PATH = integrations/indexers-mysql/repos.bleve +[queue.code_indexer] +TYPE = immediate + [repository] ROOT = {{REPO_TEST_DIR}}integrations/gitea-integration-mysql/gitea-repositories @@ -33,13 +36,23 @@ ROOT_URL = http://localhost:3001/ DISABLE_SSH = false SSH_LISTEN_HOST = localhost SSH_PORT = 2201 +APP_DATA_PATH = integrations/gitea-integration-mysql/data +BUILTIN_SSH_SERVER_USER = git START_SSH_SERVER = true +OFFLINE_MODE = false + LFS_START_SERVER = true LFS_CONTENT_PATH = integrations/gitea-integration-mysql/datalfs-mysql -OFFLINE_MODE = false LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w -APP_DATA_PATH = integrations/gitea-integration-mysql/data -BUILTIN_SSH_SERVER_USER = git +LFS_STORE_TYPE = minio +LFS_SERVE_DIRECT = false +LFS_MINIO_ENDPOINT = minio:9000 +LFS_MINIO_ACCESS_KEY_ID = 123456 +LFS_MINIO_SECRET_ACCESS_KEY = 12345678 +LFS_MINIO_BUCKET = gitea +LFS_MINIO_LOCATION = us-east-1 +LFS_MINIO_BASE_PATH = lfs/ +LFS_MINIO_USE_SSL = false [attachment] STORE_TYPE = minio diff --git a/integrations/mysql8.ini.tmpl b/integrations/mysql8.ini.tmpl index a135ecb9812b8..ca77babf4b729 100644 --- a/integrations/mysql8.ini.tmpl +++ b/integrations/mysql8.ini.tmpl @@ -14,6 +14,9 @@ ISSUE_INDEXER_PATH = integrations/indexers-mysql8/issues.bleve REPO_INDEXER_ENABLED = true REPO_INDEXER_PATH = integrations/indexers-mysql8/repos.bleve +[queue.code_indexer] +TYPE = immediate + [repository] ROOT = {{REPO_TEST_DIR}}integrations/gitea-integration-mysql8/gitea-repositories diff --git a/integrations/pgsql.ini.tmpl b/integrations/pgsql.ini.tmpl index 4cac2585fbefb..802296cf631a9 100644 --- a/integrations/pgsql.ini.tmpl +++ b/integrations/pgsql.ini.tmpl @@ -15,6 +15,9 @@ ISSUE_INDEXER_PATH = integrations/indexers-pgsql/issues.bleve REPO_INDEXER_ENABLED = true REPO_INDEXER_PATH = integrations/indexers-pgsql/repos.bleve +[queue.code_indexer] +TYPE = immediate + [repository] ROOT = {{REPO_TEST_DIR}}integrations/gitea-integration-pgsql/gitea-repositories diff --git a/integrations/repo_migrate_test.go b/integrations/repo_migrate_test.go index a9970655ef598..5a02b4ba03c09 100644 --- a/integrations/repo_migrate_test.go +++ b/integrations/repo_migrate_test.go @@ -5,15 +5,17 @@ package integrations import ( + "fmt" "net/http" "net/http/httptest" "testing" + "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName string) *httptest.ResponseRecorder { - req := NewRequest(t, "GET", "/repo/migrate") + req := NewRequest(t, "GET", fmt.Sprintf("/repo/migrate?service_type=%d", structs.PlainGitService)) // render plain git migration page resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) @@ -28,8 +30,8 @@ func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName str "clone_addr": cloneAddr, "uid": uid, "repo_name": repoName, - }, - ) + "service": fmt.Sprintf("%d", structs.PlainGitService), + }) resp = session.MakeRequest(t, req, http.StatusFound) return resp diff --git a/integrations/repo_search_test.go b/integrations/repo_search_test.go index 701013735c9b0..6f2ee37460023 100644 --- a/integrations/repo_search_test.go +++ b/integrations/repo_search_test.go @@ -7,7 +7,6 @@ package integrations import ( "net/http" "testing" - "time" "code.gitea.io/gitea/models" code_indexer "code.gitea.io/gitea/modules/indexer/code" @@ -62,14 +61,6 @@ func testSearch(t *testing.T, url string, expected []string) { assert.EqualValues(t, expected, filenames) } -func executeIndexer(t *testing.T, repo *models.Repository, op func(*models.Repository, ...chan<- error)) { - waiter := make(chan error, 1) - op(repo, waiter) - - select { - case err := <-waiter: - assert.NoError(t, err) - case <-time.After(1 * time.Minute): - assert.Fail(t, "Repository indexer took too long") - } +func executeIndexer(t *testing.T, repo *models.Repository, op func(*models.Repository)) { + op(repo) } diff --git a/integrations/sqlite.ini.tmpl b/integrations/sqlite.ini.tmpl index e899328c81fe9..5d54c5f9fab1a 100644 --- a/integrations/sqlite.ini.tmpl +++ b/integrations/sqlite.ini.tmpl @@ -10,6 +10,9 @@ ISSUE_INDEXER_PATH = integrations/indexers-sqlite/issues.bleve REPO_INDEXER_ENABLED = true REPO_INDEXER_PATH = integrations/indexers-sqlite/repos.bleve +[queue.code_indexer] +TYPE = immediate + [repository] ROOT = {{REPO_TEST_DIR}}integrations/gitea-integration-sqlite/gitea-repositories diff --git a/models/error.go b/models/error.go index 13391e5d870bc..24a7c22855d69 100644 --- a/models/error.go +++ b/models/error.go @@ -267,6 +267,21 @@ func (err ErrReachLimitOfRepo) Error() string { return fmt.Sprintf("user has reached maximum limit of repositories [limit: %d]", err.Limit) } +// ErrReachLimitOfPrivateRepo represents an error due to reaching the limit of allowed private repositories for this user. +type ErrReachLimitOfPrivateRepo struct { + Limit int +} + +// IsErrReachLimitOfPrivateRepo checks if an error is a ErrReachLimitOfPrivateRepo. +func IsErrReachLimitOfPrivateRepo(err error) bool { + _, ok := err.(ErrReachLimitOfPrivateRepo) + return ok +} + +func (err ErrReachLimitOfPrivateRepo) Error() string { + return fmt.Sprintf("user has reached maximum limit of private repositories [limit: %d]", err.Limit) +} + // __ __.__ __ .__ // / \ / \__| | _|__| // \ \/\/ / | |/ / | diff --git a/models/helper_environment.go b/models/helper_environment.go index f1c758d65d4a1..8924d0a28331d 100644 --- a/models/helper_environment.go +++ b/models/helper_environment.go @@ -8,6 +8,8 @@ import ( "fmt" "os" "strings" + + "code.gitea.io/gitea/modules/setting" ) // env keys for git hooks need @@ -23,6 +25,7 @@ const ( EnvIsDeployKey = "GITEA_IS_DEPLOY_KEY" EnvPRID = "GITEA_PR_ID" EnvIsInternal = "GITEA_INTERNAL_PUSH" + EnvAppURL = "GITEA_ROOT_URL" ) // InternalPushingEnvironment returns an os environment to switch off hooks on push @@ -62,6 +65,7 @@ func FullPushingEnvironment(author, committer *User, repo *Repository, repoName EnvPusherID+"="+fmt.Sprintf("%d", committer.ID), EnvRepoID+"="+fmt.Sprintf("%d", repo.ID), EnvPRID+"="+fmt.Sprintf("%d", prID), + EnvAppURL+"="+setting.AppURL, "SSH_ORIGINAL_COMMAND=gitea-internal", ) diff --git a/models/issue.go b/models/issue.go index 2912f7e8ef8e2..228316fc3c3ce 100644 --- a/models/issue.go +++ b/models/issue.go @@ -709,6 +709,22 @@ func (issue *Issue) ChangeTitle(doer *User, oldTitle string) (err error) { return sess.Commit() } +// ChangeRef changes the branch of this issue, as the given user. +func (issue *Issue) ChangeRef(doer *User, oldRef string) (err error) { + sess := x.NewSession() + defer sess.Close() + + if err = sess.Begin(); err != nil { + return err + } + + if err = updateIssueCols(sess, issue, "ref"); err != nil { + return fmt.Errorf("updateIssueCols: %v", err) + } + + return sess.Commit() +} + // AddDeletePRBranchComment adds delete branch comment for pull request issue func AddDeletePRBranchComment(doer *User, repo *Repository, issueID int64, branchName string) error { issue, err := getIssueByID(x, issueID) @@ -1978,6 +1994,11 @@ func deleteIssuesByRepoID(sess Engine, repoID int64) (attachmentPaths []string, return } + if _, err = sess.In("dependent_issue_id", deleteCond). + Delete(&Comment{}); err != nil { + return + } + var attachments []*Attachment if err = sess.In("issue_id", deleteCond). Find(&attachments); err != nil { diff --git a/models/issue_milestone.go b/models/issue_milestone.go index f4fba84ec0bc9..5c34834e2a502 100644 --- a/models/issue_milestone.go +++ b/models/issue_milestone.go @@ -7,6 +7,7 @@ package models import ( "fmt" "strings" + "time" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -31,11 +32,14 @@ type Milestone struct { Completeness int // Percentage(1-100). IsOverdue bool `xorm:"-"` - DeadlineString string `xorm:"-"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` DeadlineUnix timeutil.TimeStamp ClosedDateUnix timeutil.TimeStamp + DeadlineString string `xorm:"-"` TotalTrackedTime int64 `xorm:"-"` + TimeSinceUpdate int64 `xorm:"-"` } // BeforeUpdate is invoked from XORM before updating this object. @@ -50,6 +54,9 @@ func (m *Milestone) BeforeUpdate() { // AfterLoad is invoked from XORM after setting the value of a field of // this object. func (m *Milestone) AfterLoad() { + if !m.UpdatedUnix.IsZero() { + m.TimeSinceUpdate = time.Now().Unix() - m.UpdatedUnix.AsTime().Unix() + } m.NumOpenIssues = m.NumIssues - m.NumClosedIssues if m.DeadlineUnix.Year() == 9999 { return diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go index 79ce48c4cd1d2..789f8ebdfc6a6 100644 --- a/models/issue_stopwatch.go +++ b/models/issue_stopwatch.go @@ -201,21 +201,54 @@ func (sw *Stopwatch) APIFormat() (api.StopWatch, error) { if err != nil { return api.StopWatch{}, err } + if err := issue.LoadRepo(); err != nil { + return api.StopWatch{}, err + } return api.StopWatch{ - Created: sw.CreatedUnix.AsTime(), - IssueIndex: issue.Index, + Created: sw.CreatedUnix.AsTime(), + IssueIndex: issue.Index, + IssueTitle: issue.Title, + RepoOwnerName: issue.Repo.OwnerName, + RepoName: issue.Repo.Name, }, nil } // APIFormat convert Stopwatches type to api.StopWatches type func (sws Stopwatches) APIFormat() (api.StopWatches, error) { result := api.StopWatches(make([]api.StopWatch, 0, len(sws))) + + issueCache := make(map[int64]*Issue) + repoCache := make(map[int64]*Repository) + var ( + issue *Issue + repo *Repository + ok bool + err error + ) + for _, sw := range sws { - apiSW, err := sw.APIFormat() - if err != nil { - return nil, err + issue, ok = issueCache[sw.IssueID] + if !ok { + issue, err = GetIssueByID(sw.IssueID) + if err != nil { + return nil, err + } } - result = append(result, apiSW) + repo, ok = repoCache[issue.RepoID] + if !ok { + repo, err = GetRepositoryByID(issue.RepoID) + if err != nil { + return nil, err + } + } + + result = append(result, api.StopWatch{ + Created: sw.CreatedUnix.AsTime(), + IssueIndex: issue.Index, + IssueTitle: issue.Title, + RepoOwnerName: repo.OwnerName, + RepoName: repo.Name, + }) } return result, nil } diff --git a/models/lfs.go b/models/lfs.go index 7a04f799c0f7d..274b32a736758 100644 --- a/models/lfs.go +++ b/models/lfs.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io" + "path" "code.gitea.io/gitea/modules/timeutil" @@ -26,6 +27,15 @@ type LFSMetaObject struct { CreatedUnix timeutil.TimeStamp `xorm:"created"` } +// RelativePath returns the relative path of the lfs object +func (m *LFSMetaObject) RelativePath() string { + if len(m.Oid) < 5 { + return m.Oid + } + + return path.Join(m.Oid[0:2], m.Oid[2:4], m.Oid[4:]) +} + // Pointer returns the string representation of an LFS pointer file func (m *LFSMetaObject) Pointer() string { return fmt.Sprintf("%s\n%s%s\nsize %d\n", LFSMetaFileIdentifier, LFSMetaFileOidPrefix, m.Oid, m.Size) @@ -202,3 +212,25 @@ func LFSAutoAssociate(metas []*LFSMetaObject, user *User, repoID int64) error { return sess.Commit() } + +// IterateLFS iterates lfs object +func IterateLFS(f func(mo *LFSMetaObject) error) error { + var start int + const batchSize = 100 + for { + var mos = make([]*LFSMetaObject, 0, batchSize) + if err := x.Limit(batchSize, start).Find(&mos); err != nil { + return err + } + if len(mos) == 0 { + return nil + } + start += len(mos) + + for _, mo := range mos { + if err := f(mo); err != nil { + return err + } + } + } +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 721b045fdce6c..9b94c49cc06c7 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -7,6 +7,7 @@ package migrations import ( "fmt" + "reflect" "regexp" "strings" @@ -228,6 +229,10 @@ var migrations = []Migration{ NewMigration("Add projects info to repository table", addProjectsInfo), // v147 -> v148 NewMigration("create review for 0 review id code comments", createReviewsForCodeComments), + // v148 -> v149 + NewMigration("remove issue dependency comments who refer to non existing issues", purgeInvalidDependenciesComments), + // v149 -> v150 + NewMigration("Add Created and Updated to Milestone table", addCreatedAndUpdatedToMilestones), } // GetCurrentDBVersion returns the current db version @@ -323,6 +328,221 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t return nil } +// RecreateTables will recreate the tables for the provided beans using the newly provided bean definition and move all data to that new table +// WARNING: YOU MUST PROVIDE THE FULL BEAN DEFINITION +func RecreateTables(beans ...interface{}) func(*xorm.Engine) error { + return func(x *xorm.Engine) error { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + sess = sess.StoreEngine("InnoDB") + for _, bean := range beans { + log.Info("Recreating Table: %s for Bean: %s", x.TableName(bean), reflect.Indirect(reflect.ValueOf(bean)).Type().Name()) + if err := recreateTable(sess, bean); err != nil { + return err + } + } + return sess.Commit() + } +} + +// recreateTable will recreate the table using the newly provided bean definition and move all data to that new table +// WARNING: YOU MUST PROVIDE THE FULL BEAN DEFINITION +// WARNING: YOU MUST COMMIT THE SESSION AT THE END +func recreateTable(sess *xorm.Session, bean interface{}) error { + // TODO: This will not work if there are foreign keys + + tableName := sess.Engine().TableName(bean) + tempTableName := fmt.Sprintf("tmp_recreate__%s", tableName) + + // We need to move the old table away and create a new one with the correct columns + // We will need to do this in stages to prevent data loss + // + // First create the temporary table + if err := sess.Table(tempTableName).CreateTable(bean); err != nil { + log.Error("Unable to create table %s. Error: %v", tempTableName, err) + return err + } + + if err := sess.Table(tempTableName).CreateUniques(bean); err != nil { + log.Error("Unable to create uniques for table %s. Error: %v", tempTableName, err) + return err + } + + if err := sess.Table(tempTableName).CreateIndexes(bean); err != nil { + log.Error("Unable to create indexes for table %s. Error: %v", tempTableName, err) + return err + } + + // Work out the column names from the bean - these are the columns to select from the old table and install into the new table + table, err := sess.Engine().TableInfo(bean) + if err != nil { + log.Error("Unable to get table info. Error: %v", err) + + return err + } + newTableColumns := table.Columns() + if len(newTableColumns) == 0 { + return fmt.Errorf("no columns in new table") + } + hasID := false + for _, column := range newTableColumns { + hasID = hasID || (column.IsPrimaryKey && column.IsAutoIncrement) + } + + if hasID && setting.Database.UseMSSQL { + if _, err := sess.Exec(fmt.Sprintf("SET IDENTITY_INSERT `%s` ON", tempTableName)); err != nil { + log.Error("Unable to set identity insert for table %s. Error: %v", tempTableName, err) + return err + } + } + + sqlStringBuilder := &strings.Builder{} + _, _ = sqlStringBuilder.WriteString("INSERT INTO `") + _, _ = sqlStringBuilder.WriteString(tempTableName) + _, _ = sqlStringBuilder.WriteString("` (`") + _, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name) + _, _ = sqlStringBuilder.WriteString("`") + for _, column := range newTableColumns[1:] { + _, _ = sqlStringBuilder.WriteString(", `") + _, _ = sqlStringBuilder.WriteString(column.Name) + _, _ = sqlStringBuilder.WriteString("`") + } + _, _ = sqlStringBuilder.WriteString(")") + _, _ = sqlStringBuilder.WriteString(" SELECT ") + if newTableColumns[0].Default != "" { + _, _ = sqlStringBuilder.WriteString("COALESCE(`") + _, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name) + _, _ = sqlStringBuilder.WriteString("`, ") + _, _ = sqlStringBuilder.WriteString(newTableColumns[0].Default) + _, _ = sqlStringBuilder.WriteString(")") + } else { + _, _ = sqlStringBuilder.WriteString("`") + _, _ = sqlStringBuilder.WriteString(newTableColumns[0].Name) + _, _ = sqlStringBuilder.WriteString("`") + } + + for _, column := range newTableColumns[1:] { + if column.Default != "" { + _, _ = sqlStringBuilder.WriteString(", COALESCE(`") + _, _ = sqlStringBuilder.WriteString(column.Name) + _, _ = sqlStringBuilder.WriteString("`, ") + _, _ = sqlStringBuilder.WriteString(column.Default) + _, _ = sqlStringBuilder.WriteString(")") + } else { + _, _ = sqlStringBuilder.WriteString(", `") + _, _ = sqlStringBuilder.WriteString(column.Name) + _, _ = sqlStringBuilder.WriteString("`") + } + } + _, _ = sqlStringBuilder.WriteString(" FROM `") + _, _ = sqlStringBuilder.WriteString(tableName) + _, _ = sqlStringBuilder.WriteString("`") + + if _, err := sess.Exec(sqlStringBuilder.String()); err != nil { + log.Error("Unable to set copy data in to temp table %s. Error: %v", tempTableName, err) + return err + } + + if hasID && setting.Database.UseMSSQL { + if _, err := sess.Exec(fmt.Sprintf("SET IDENTITY_INSERT `%s` OFF", tempTableName)); err != nil { + log.Error("Unable to switch off identity insert for table %s. Error: %v", tempTableName, err) + return err + } + } + + switch { + case setting.Database.UseSQLite3: + // SQLite will drop all the constraints on the old table + if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil { + log.Error("Unable to drop old table %s. Error: %v", tableName, err) + return err + } + + if err := sess.Table(tempTableName).DropIndexes(bean); err != nil { + log.Error("Unable to drop indexes on temporary table %s. Error: %v", tempTableName, err) + return err + } + + if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", tempTableName, tableName)); err != nil { + log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err) + return err + } + + if err := sess.Table(tableName).CreateIndexes(bean); err != nil { + log.Error("Unable to recreate indexes on table %s. Error: %v", tableName, err) + return err + } + + if err := sess.Table(tableName).CreateUniques(bean); err != nil { + log.Error("Unable to recreate uniques on table %s. Error: %v", tableName, err) + return err + } + + case setting.Database.UseMySQL: + // MySQL will drop all the constraints on the old table + if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil { + log.Error("Unable to drop old table %s. Error: %v", tableName, err) + return err + } + + // SQLite and MySQL will move all the constraints from the temporary table to the new table + if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", tempTableName, tableName)); err != nil { + log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err) + return err + } + case setting.Database.UsePostgreSQL: + // CASCADE causes postgres to drop all the constraints on the old table + if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s` CASCADE", tableName)); err != nil { + log.Error("Unable to drop old table %s. Error: %v", tableName, err) + return err + } + + // CASCADE causes postgres to move all the constraints from the temporary table to the new table + if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", tempTableName, tableName)); err != nil { + log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err) + return err + } + + var indices []string + schema := sess.Engine().Dialect().URI().Schema + sess.Engine().SetSchema("") + if err := sess.Table("pg_indexes").Cols("indexname").Where("tablename = ? ", tableName).Find(&indices); err != nil { + log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err) + return err + } + sess.Engine().SetSchema(schema) + + for _, index := range indices { + newIndexName := strings.Replace(index, "tmp_recreate__", "", 1) + if _, err := sess.Exec(fmt.Sprintf("ALTER INDEX `%s` RENAME TO `%s`", index, newIndexName)); err != nil { + log.Error("Unable to rename %s to %s. Error: %v", index, newIndexName, err) + return err + } + } + + case setting.Database.UseMSSQL: + // MSSQL will drop all the constraints on the old table + if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil { + log.Error("Unable to drop old table %s. Error: %v", tableName, err) + return err + } + + // MSSQL sp_rename will move all the constraints from the temporary table to the new table + if _, err := sess.Exec(fmt.Sprintf("sp_rename `%s`,`%s`", tempTableName, tableName)); err != nil { + log.Error("Unable to rename %s to %s. Error: %v", tempTableName, tableName, err) + return err + } + + default: + log.Fatal("Unrecognized DB") + } + return nil +} + +// WARNING: YOU MUST COMMIT THE SESSION AT THE END func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...string) (err error) { if tableName == "" || len(columnNames) == 0 { return nil diff --git a/models/migrations/v111.go b/models/migrations/v111.go index 6a94298ddcad7..93831de94a96e 100644 --- a/models/migrations/v111.go +++ b/models/migrations/v111.go @@ -357,21 +357,18 @@ func addBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { return sess.Where("uid=?", reviewer.ID).In("team_id", protectedBranch.ApprovalsWhitelistTeamIDs).Exist(new(TeamUser)) } - sess := x.NewSession() - defer sess.Close() - - if _, err := sess.Exec("UPDATE `protected_branch` SET `enable_whitelist` = ? WHERE enable_whitelist IS NULL", false); err != nil { + if _, err := x.Exec("UPDATE `protected_branch` SET `enable_whitelist` = ? WHERE enable_whitelist IS NULL", false); err != nil { return err } - if _, err := sess.Exec("UPDATE `protected_branch` SET `can_push` = `enable_whitelist`"); err != nil { + if _, err := x.Exec("UPDATE `protected_branch` SET `can_push` = `enable_whitelist`"); err != nil { return err } - if _, err := sess.Exec("UPDATE `protected_branch` SET `enable_approvals_whitelist` = ? WHERE `required_approvals` > ?", true, 0); err != nil { + if _, err := x.Exec("UPDATE `protected_branch` SET `enable_approvals_whitelist` = ? WHERE `required_approvals` > ?", true, 0); err != nil { return err } var pageSize int64 = 20 - qresult, err := sess.QueryInterface("SELECT max(id) as max_id FROM issue") + qresult, err := x.QueryInterface("SELECT max(id) as max_id FROM issue") if err != nil { return err } @@ -383,16 +380,28 @@ func addBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { } totalPages := totalIssues / pageSize - // Find latest review of each user in each pull request, and set official field if appropriate - reviews := []*Review{} - var page int64 - for page = 0; page <= totalPages; page++ { - if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id > ? AND issue_id <= ? AND type in (?, ?) GROUP BY issue_id, reviewer_id)", + var executeBody = func(page, pageSize int64) error { + // Find latest review of each user in each pull request, and set official field if appropriate + reviews := []*Review{} + + if err := x.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id > ? AND issue_id <= ? AND type in (?, ?) GROUP BY issue_id, reviewer_id)", page*pageSize, (page+1)*pageSize, ReviewTypeApprove, ReviewTypeReject). Find(&reviews); err != nil { return err } + if len(reviews) == 0 { + return nil + } + + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + + var updated int for _, review := range reviews { reviewer := new(User) has, err := sess.ID(review.ReviewerID).Get(reviewer) @@ -407,13 +416,24 @@ func addBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { continue } review.Official = official - + updated++ if _, err := sess.ID(review.ID).Cols("official").Update(review); err != nil { return err } } + if updated > 0 { + return sess.Commit() + } + return nil + } + + var page int64 + for page = 0; page <= totalPages; page++ { + if err := executeBody(page, pageSize); err != nil { + return err + } } - return sess.Commit() + return nil } diff --git a/models/migrations/v148.go b/models/migrations/v148.go new file mode 100644 index 0000000000000..35d17f5b2cc06 --- /dev/null +++ b/models/migrations/v148.go @@ -0,0 +1,14 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func purgeInvalidDependenciesComments(x *xorm.Engine) error { + _, err := x.Exec("DELETE FROM comment WHERE dependent_issue_id != 0 AND dependent_issue_id NOT IN (SELECT id FROM issue)") + return err +} diff --git a/models/migrations/v149.go b/models/migrations/v149.go new file mode 100644 index 0000000000000..60c0fae8bcdbf --- /dev/null +++ b/models/migrations/v149.go @@ -0,0 +1,25 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "fmt" + + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func addCreatedAndUpdatedToMilestones(x *xorm.Engine) error { + type Milestone struct { + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + + if err := x.Sync2(new(Milestone)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + return nil +} diff --git a/models/models.go b/models/models.go index e0dd3ed2a4231..32df9bdfd87b3 100644 --- a/models/models.go +++ b/models/models.go @@ -10,6 +10,8 @@ import ( "database/sql" "errors" "fmt" + "reflect" + "strings" "code.gitea.io/gitea/modules/setting" @@ -214,6 +216,36 @@ func NewEngine(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err e return nil } +// NamesToBean return a list of beans or an error +func NamesToBean(names ...string) ([]interface{}, error) { + beans := []interface{}{} + if len(names) == 0 { + beans = append(beans, tables...) + return beans, nil + } + // Need to map provided names to beans... + beanMap := make(map[string]interface{}) + for _, bean := range tables { + + beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean + beanMap[strings.ToLower(x.TableName(bean))] = bean + beanMap[strings.ToLower(x.TableName(bean, true))] = bean + } + + gotBean := make(map[interface{}]bool) + for _, name := range names { + bean, ok := beanMap[strings.ToLower(strings.TrimSpace(name))] + if !ok { + return nil, fmt.Errorf("No table found that matches: %s", name) + } + if !gotBean[bean] { + beans = append(beans, bean) + gotBean[bean] = true + } + } + return beans, nil +} + // Statistic contains the database statistics type Statistic struct { Counter struct { @@ -270,6 +302,17 @@ func DumpDatabase(filePath string, dbType string) error { } 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)) } diff --git a/models/models_test.go b/models/models_test.go index 37e9a352f8af1..2441ad7fb064e 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -21,6 +21,12 @@ func TestDumpDatabase(t *testing.T) { dir, err := ioutil.TempDir(os.TempDir(), "dump") assert.NoError(t, err) + type Version struct { + ID int64 `xorm:"pk autoincr"` + Version int64 + } + assert.NoError(t, x.Sync2(Version{})) + for _, dbName := range setting.SupportedDatabases { dbType := setting.GetDBTypeByName(dbName) assert.NoError(t, DumpDatabase(filepath.Join(dir, dbType+".sql"), dbType)) diff --git a/models/repo.go b/models/repo.go index d9b65769763fa..eceb3eb23882d 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1008,11 +1008,15 @@ func (repo *Repository) CloneLink() (cl *CloneLink) { } // CheckCreateRepository check if could created a repository -func CheckCreateRepository(doer, u *User, name string) error { - if !doer.CanCreateRepo() { +func CheckCreateRepository(doer, u *User, name string, private bool) error { + if !private && !doer.CanCreateRepo() { return ErrReachLimitOfRepo{u.MaxRepoCreation} } + if private && !doer.CanCreatePrivateRepo() { + return ErrReachLimitOfPrivateRepo{u.MaxPrivateRepoCreation} + } + if err := IsUsableRepoName(name); err != nil { return err } @@ -1135,10 +1139,17 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error return fmt.Errorf("updateUser: %v", err) } - if _, err = ctx.e.Incr("num_repos").ID(u.ID).Update(new(User)); err != nil { - return fmt.Errorf("increment user total_repos: %v", err) + if !repo.IsPrivate { + if _, err = ctx.e.Incr("num_repos").ID(u.ID).Update(new(User)); err != nil { + return fmt.Errorf("increment user total_repos: %v", err) + } + u.NumRepos++ + } else { + if _, err = ctx.e.Incr("num_private_repos").ID(u.ID).Update(new(User)); err != nil { + return fmt.Errorf("increment user total_private_repos: %v", err) + } + u.NumPrivateRepos++ } - u.NumRepos++ // Give access to all members in teams with access to all repositories. if u.IsOrganization() { @@ -1305,10 +1316,18 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error } // Update repository count. - if _, err = sess.Exec("UPDATE `user` SET num_repos=num_repos+1 WHERE id=?", newOwner.ID); err != nil { - return fmt.Errorf("increase new owner repository count: %v", err) - } else if _, err = sess.Exec("UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", oldOwner.ID); err != nil { - return fmt.Errorf("decrease old owner repository count: %v", err) + if !repo.IsPrivate { + if _, err = sess.Exec("UPDATE `user` SET num_repos=num_repos+1 WHERE id=?", newOwner.ID); err != nil { + return fmt.Errorf("increase new owner repository count: %v", err) + } else if _, err = sess.Exec("UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", oldOwner.ID); err != nil { + return fmt.Errorf("decrease old owner repository count: %v", err) + } + } else { + if _, err = sess.Exec("UPDATE `user` SET num_private_repos=num_private_repos+1 WHERE id=?", newOwner.ID); err != nil { + return fmt.Errorf("increase new owner repository count: %v", err) + } else if _, err = sess.Exec("UPDATE `user` SET num_private_repos=num_private_repos-1 WHERE id=?", oldOwner.ID); err != nil { + return fmt.Errorf("decrease old owner repository count: %v", err) + } } if err = watchRepo(sess, doer.ID, repo.ID, true); err != nil { @@ -1643,8 +1662,14 @@ func DeleteRepository(doer *User, uid, repoID int64) error { } } - if _, err = sess.Exec("UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", uid); err != nil { - return err + if repo.IsPrivate { + if _, err = sess.Exec("UPDATE `user` SET num_private_repos=num_private_repos-1 WHERE id=?", uid); err != nil { + return err + } + } else { + if _, err = sess.Exec("UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", uid); err != nil { + return err + } } if len(repo.Topics) > 0 { @@ -1998,9 +2023,15 @@ func CheckRepoStats(ctx context.Context) error { // User.NumRepos { "SELECT `user`.id FROM `user` WHERE `user`.num_repos!=(SELECT COUNT(*) FROM `repository` WHERE owner_id=`user`.id)", - "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", + "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=? WHERE private=false", "user count 'num_repos'", }, + // User.NumPrivateRepos + { + "SELECT `user`.id FROM `user` WHERE `user`.num_private_repos!=(SELECT COUNT(*) FROM `repository` WHERE owner_id=`user`.id)", + "UPDATE `user` SET num_private_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=? WHERE private=true", + "user count 'num_private_repos'", + }, // Issue.NumComments { "SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)", diff --git a/models/unit_tests.go b/models/unit_tests.go index 11b7708ffd874..e977f706f848b 100644 --- a/models/unit_tests.go +++ b/models/unit_tests.go @@ -69,6 +69,7 @@ func MainTest(m *testing.M, pathToGiteaRoot string) { } setting.Attachment.Path = filepath.Join(setting.AppDataPath, "attachments") + setting.LFS.ContentPath = filepath.Join(setting.AppDataPath, "lfs") if err = storage.Init(); err != nil { fatalTestError("storage.Init: %v\n", err) } diff --git a/models/user.go b/models/user.go index 2e5d6473bbd7a..88a745ba7be3f 100644 --- a/models/user.go +++ b/models/user.go @@ -133,6 +133,8 @@ type User struct { LastRepoVisibility bool // Maximum repository creation limit, -1 means use global default MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1"` + // Maximum private repository creation limit, -1 means use global default + MaxPrivateRepoCreation int `xorm:"NOT NULL DEFAULT -1"` // Permissions IsActive bool `xorm:"INDEX"` // Activate primary email @@ -149,10 +151,11 @@ type User struct { UseCustomAvatar bool // Counters - NumFollowers int - NumFollowing int `xorm:"NOT NULL DEFAULT 0"` - NumStars int - NumRepos int + NumFollowers int + NumFollowing int `xorm:"NOT NULL DEFAULT 0"` + NumStars int + NumRepos int + NumPrivateRepos int // For organization NumTeams int @@ -188,6 +191,10 @@ func (u *User) BeforeUpdate() { u.MaxRepoCreation = -1 } + if u.MaxPrivateRepoCreation < -1 { + u.MaxPrivateRepoCreation = -1 + } + // Organization does not need email u.Email = strings.ToLower(u.Email) if !u.IsOrganization() { @@ -280,6 +287,14 @@ func (u *User) MaxCreationLimit() int { return u.MaxRepoCreation } +// MaxPrivateCreationLimit returns the number of private repositories a user is allowed to create +func (u *User) MaxPrivateCreationLimit() int { + if u.MaxPrivateRepoCreation <= -1 { + return setting.Repository.MaxPrivateCreationLimit + } + return u.MaxPrivateRepoCreation +} + // CanCreateRepo returns if user login can create a repository // NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revised func (u *User) CanCreateRepo() bool { @@ -295,6 +310,21 @@ func (u *User) CanCreateRepo() bool { return u.NumRepos < u.MaxRepoCreation } +// CanCreatePrivateRepo returns if user login can create a private repository +// NOTE: functions calling this assume a failure due to private repository count limit; if new checks are added, those functions should be revised +func (u *User) CanCreatePrivateRepo() bool { + if u.IsAdmin { + return true + } + if u.MaxPrivateRepoCreation <= -1 { + if setting.Repository.MaxPrivateCreationLimit <= -1 { + return true + } + return u.NumPrivateRepos < setting.Repository.MaxPrivateCreationLimit + } + return u.NumPrivateRepos < u.MaxPrivateRepoCreation +} + // CanCreateOrganization returns true if user can create organisation. func (u *User) CanCreateOrganization() bool { return u.IsAdmin || (u.AllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation) @@ -1005,6 +1035,7 @@ func CreateUser(u *User) (err error) { u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification u.MaxRepoCreation = -1 + u.MaxPrivateRepoCreation = -1 u.Theme = setting.UI.DefaultTheme if _, err = sess.Insert(u); err != nil { diff --git a/modules/auth/admin.go b/modules/auth/admin.go index 9caf81e07ffb3..d626ef9fcb91a 100644 --- a/modules/auth/admin.go +++ b/modules/auth/admin.go @@ -35,6 +35,7 @@ type AdminEditUserForm struct { Website string `binding:"ValidUrl;MaxSize(255)"` Location string `binding:"MaxSize(50)"` MaxRepoCreation int + MaxPrivateRepoCreation int Active bool Admin bool Restricted bool diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index cf3da6df5ed9d..f4c0655fa9762 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -89,6 +89,6 @@ func Prepare(data []byte) (*image.Image, error) { } } - img = resize.Resize(AvatarSize, AvatarSize, img, resize.NearestNeighbor) + img = resize.Resize(AvatarSize, AvatarSize, img, resize.Bilinear) return &img, nil } diff --git a/modules/convert/issue.go b/modules/convert/issue.go index a335f6326b9ed..e89021cbcc072 100644 --- a/modules/convert/issue.go +++ b/modules/convert/issue.go @@ -152,6 +152,8 @@ func ToAPIMilestone(m *models.Milestone) *api.Milestone { Description: m.Content, OpenIssues: m.NumOpenIssues, ClosedIssues: m.NumClosedIssues, + Created: m.CreatedUnix.AsTime(), + Updated: m.UpdatedUnix.AsTimePtr(), } if m.IsClosed { apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr() diff --git a/modules/convert/issue_test.go b/modules/convert/issue_test.go index e5676293f85fa..2f8f56e99a643 100644 --- a/modules/convert/issue_test.go +++ b/modules/convert/issue_test.go @@ -34,6 +34,8 @@ func TestMilestone_APIFormat(t *testing.T) { IsClosed: false, NumOpenIssues: 5, NumClosedIssues: 6, + CreatedUnix: timeutil.TimeStamp(time.Date(1999, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()), + UpdatedUnix: timeutil.TimeStamp(time.Date(1999, time.March, 1, 0, 0, 0, 0, time.UTC).Unix()), DeadlineUnix: timeutil.TimeStamp(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()), } assert.Equal(t, api.Milestone{ @@ -43,6 +45,8 @@ func TestMilestone_APIFormat(t *testing.T) { Description: milestone.Content, OpenIssues: milestone.NumOpenIssues, ClosedIssues: milestone.NumClosedIssues, + Created: milestone.CreatedUnix.AsTime(), + Updated: milestone.UpdatedUnix.AsTimePtr(), Deadline: milestone.DeadlineUnix.AsTimePtr(), }, *ToAPIMilestone(milestone)) } diff --git a/modules/git/command.go b/modules/git/command.go index 1496b0186ed15..d40c0bfa2322b 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -1,4 +1,5 @@ // Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2016 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -130,7 +131,9 @@ func (c *Command) RunInDirTimeoutEnvFullPipelineFunc(env []string, timeout time. } // TODO: verify if this is still needed in golang 1.15 - cmd.Env = append(cmd.Env, "GODEBUG=asyncpreemptoff=1") + if goVersionLessThan115 { + cmd.Env = append(cmd.Env, "GODEBUG=asyncpreemptoff=1") + } cmd.Dir = dir cmd.Stdout = stdout cmd.Stderr = stderr diff --git a/modules/git/commit.go b/modules/git/commit.go index 2ae35c9f58ace..6d2bc2b02cfbb 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -21,7 +21,6 @@ import ( "strings" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/mcuadros/go-version" ) // Commit represents a git commit. @@ -470,7 +469,7 @@ func (c *Commit) GetSubModule(entryname string) (*SubModule, error) { // GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') func (c *Commit) GetBranchName() (string, error) { - binVersion, err := BinVersion() + err := LoadGitVersion() if err != nil { return "", fmt.Errorf("Git version missing: %v", err) } @@ -478,7 +477,7 @@ func (c *Commit) GetBranchName() (string, error) { args := []string{ "name-rev", } - if version.Compare(binVersion, "2.13.0", ">=") { + if CheckGitVersionConstraint(">= 2.13.0") == nil { args = append(args, "--exclude", "refs/tags/*") } args = append(args, "--name-only", "--no-undefined", c.ID.String()) diff --git a/modules/git/git.go b/modules/git/git.go index 1061bdb0d5253..a9ff923cc5bea 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -15,14 +15,9 @@ import ( "code.gitea.io/gitea/modules/process" - "github.com/mcuadros/go-version" + "github.com/hashicorp/go-version" ) -// Version return this package's current version -func Version() string { - return "0.4.2" -} - var ( // Debug enables verbose logging on everything. // This should be false in case Gogs starts in SSH mode. @@ -39,7 +34,10 @@ var ( // DefaultContext is the default context to run git commands in DefaultContext = context.Background() - gitVersion string + gitVersion *version.Version + + // will be checked on Init + goVersionLessThan115 = true ) func log(format string, args ...interface{}) { @@ -55,31 +53,43 @@ func log(format string, args ...interface{}) { } } -// BinVersion returns current Git version from shell. -func BinVersion() (string, error) { - if len(gitVersion) > 0 { - return gitVersion, nil +// LocalVersion returns current Git version from shell. +func LocalVersion() (*version.Version, error) { + if err := LoadGitVersion(); err != nil { + return nil, err + } + return gitVersion, nil +} + +// LoadGitVersion returns current Git version from shell. +func LoadGitVersion() error { + // doesn't need RWMutex because its exec by Init() + if gitVersion != nil { + return nil } stdout, err := NewCommand("version").Run() if err != nil { - return "", err + return err } fields := strings.Fields(stdout) if len(fields) < 3 { - return "", fmt.Errorf("not enough output: %s", stdout) + return fmt.Errorf("not enough output: %s", stdout) } + var versionString string + // Handle special case on Windows. i := strings.Index(fields[2], "windows") if i >= 1 { - gitVersion = fields[2][:i-1] - return gitVersion, nil + versionString = fields[2][:i-1] + } else { + versionString = fields[2] } - gitVersion = fields[2] - return gitVersion, nil + gitVersion, err = version.NewVersion(versionString) + return err } // SetExecutablePath changes the path of git executable and checks the file permission and version. @@ -94,11 +104,17 @@ func SetExecutablePath(path string) error { } GitExecutable = absPath - gitVersion, err := BinVersion() + err = LoadGitVersion() if err != nil { return fmt.Errorf("Git version missing: %v", err) } - if version.Compare(gitVersion, GitVersionRequired, "<") { + + versionRequired, err := version.NewVersion(GitVersionRequired) + if err != nil { + return err + } + + if gitVersion.LessThan(versionRequired) { return fmt.Errorf("Git version not supported. Requires version > %v", GitVersionRequired) } @@ -108,6 +124,20 @@ func SetExecutablePath(path string) error { // Init initializes git module func Init(ctx context.Context) error { DefaultContext = ctx + + // Save current git version on init to gitVersion otherwise it would require an RWMutex + if err := LoadGitVersion(); err != nil { + return err + } + + // Save if the go version used to compile gitea is greater or equal 1.15 + runtimeVersion, err := version.NewVersion(strings.TrimPrefix(runtime.Version(), "go")) + if err != nil { + return err + } + version115, _ := version.NewVersion("1.15") + goVersionLessThan115 = runtimeVersion.LessThan(version115) + // Git requires setting user.name and user.email in order to commit changes - if they're not set just add some defaults for configKey, defaultValue := range map[string]string{"user.name": "Gitea", "user.email": "gitea@fake.local"} { if err := checkAndSetConfig(configKey, defaultValue, false); err != nil { @@ -120,13 +150,13 @@ func Init(ctx context.Context) error { return err } - if version.Compare(gitVersion, "2.10", ">=") { + if CheckGitVersionConstraint(">= 2.10") == nil { if err := checkAndSetConfig("receive.advertisePushOptions", "true", true); err != nil { return err } } - if version.Compare(gitVersion, "2.18", ">=") { + if CheckGitVersionConstraint(">= 2.18") == nil { if err := checkAndSetConfig("core.commitGraph", "true", true); err != nil { return err } @@ -143,6 +173,21 @@ func Init(ctx context.Context) error { return nil } +// CheckGitVersionConstraint check version constrain against local installed git version +func CheckGitVersionConstraint(constraint string) error { + if err := LoadGitVersion(); err != nil { + return err + } + check, err := version.NewConstraint(constraint) + if err != nil { + return err + } + if !check.Check(gitVersion) { + return fmt.Errorf("installed git binary %s does not satisfy version constraint %s", gitVersion.Original(), constraint) + } + return nil +} + func checkAndSetConfig(key, defaultValue string, forceToDefault bool) error { stdout, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key) if err != nil { diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index c10c96f5584b6..7522b0dc82aa1 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -7,8 +7,6 @@ package git import ( "bytes" "fmt" - - "github.com/mcuadros/go-version" ) // CheckAttributeOpts represents the possible options to CheckAttribute @@ -21,7 +19,7 @@ type CheckAttributeOpts struct { // CheckAttribute return the Blame object of file func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) { - binVersion, err := BinVersion() + err := LoadGitVersion() if err != nil { return nil, fmt.Errorf("Git version missing: %v", err) } @@ -42,7 +40,7 @@ func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[ } // git check-attr --cached first appears in git 1.7.8 - if opts.CachedOnly && version.Compare(binVersion, "1.7.8", ">=") { + if opts.CachedOnly && CheckGitVersionConstraint(">= 1.7.8") == nil { cmdArgs = append(cmdArgs, "--cached") } diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 4f04b2d3635dc..45745c808868b 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -14,7 +14,6 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/mcuadros/go-version" ) // GetRefCommitID returns the last commit ID string of given reference (branch or tag). @@ -470,7 +469,7 @@ func (repo *Repository) getCommitsBeforeLimit(id SHA1, num int) (*list.List, err } func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error) { - if version.Compare(gitVersion, "2.7.0", ">=") { + if CheckGitVersionConstraint(">= 2.7.0") == nil { stdout, err := NewCommand("for-each-ref", "--count="+strconv.Itoa(limit), "--format=%(refname:strip=2)", "--contains", commit.ID.String(), BranchPrefix).RunInDir(repo.Path) if err != nil { return nil, err diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go index bfa368b6dfb7d..59b8177401cca 100644 --- a/modules/git/repo_stats.go +++ b/modules/git/repo_stats.go @@ -6,8 +6,9 @@ package git import ( "bufio" - "bytes" + "context" "fmt" + "os" "sort" "strconv" "strings" @@ -49,6 +50,15 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) } stats.CommitCountInAllBranches = c + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return nil, err + } + defer func() { + _ = stdoutReader.Close() + _ = stdoutWriter.Close() + }() + args := []string{"log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%an%n%ae%n", "--date=iso", fmt.Sprintf("--since='%s'", since)} if len(branch) == 0 { args = append(args, "--branches=*") @@ -56,79 +66,88 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) args = append(args, "--first-parent", branch) } - stdout, err = NewCommand(args...).RunInDirBytes(repo.Path) - if err != nil { - return nil, err - } + stderr := new(strings.Builder) + err = NewCommand(args...).RunInDirTimeoutEnvFullPipelineFunc( + nil, -1, repo.Path, + stdoutWriter, stderr, nil, + func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() - scanner := bufio.NewScanner(bytes.NewReader(stdout)) - scanner.Split(bufio.ScanLines) - stats.CommitCount = 0 - stats.Additions = 0 - stats.Deletions = 0 - authors := make(map[string]*CodeActivityAuthor) - files := make(map[string]bool) - var author string - p := 0 - for scanner.Scan() { - l := strings.TrimSpace(scanner.Text()) - if l == "---" { - p = 1 - } else if p == 0 { - continue - } else { - p++ - } - if p > 4 && len(l) == 0 { - continue - } - switch p { - case 1: // Separator - case 2: // Commit sha-1 - stats.CommitCount++ - case 3: // Author - author = l - case 4: // E-mail - email := strings.ToLower(l) - if _, ok := authors[email]; !ok { - authors[email] = &CodeActivityAuthor{ - Name: author, - Email: email, - Commits: 0, + scanner := bufio.NewScanner(stdoutReader) + scanner.Split(bufio.ScanLines) + stats.CommitCount = 0 + stats.Additions = 0 + stats.Deletions = 0 + authors := make(map[string]*CodeActivityAuthor) + files := make(map[string]bool) + var author string + p := 0 + for scanner.Scan() { + l := strings.TrimSpace(scanner.Text()) + if l == "---" { + p = 1 + } else if p == 0 { + continue + } else { + p++ } - } - authors[email].Commits++ - default: // Changed file - if parts := strings.Fields(l); len(parts) >= 3 { - if parts[0] != "-" { - if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil { - stats.Additions += c - } + if p > 4 && len(l) == 0 { + continue } - if parts[1] != "-" { - if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil { - stats.Deletions += c + switch p { + case 1: // Separator + case 2: // Commit sha-1 + stats.CommitCount++ + case 3: // Author + author = l + case 4: // E-mail + email := strings.ToLower(l) + if _, ok := authors[email]; !ok { + authors[email] = &CodeActivityAuthor{ + Name: author, + Email: email, + Commits: 0, + } + } + authors[email].Commits++ + default: // Changed file + if parts := strings.Fields(l); len(parts) >= 3 { + if parts[0] != "-" { + if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil { + stats.Additions += c + } + } + if parts[1] != "-" { + if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil { + stats.Deletions += c + } + } + if _, ok := files[parts[2]]; !ok { + files[parts[2]] = true + } } - } - if _, ok := files[parts[2]]; !ok { - files[parts[2]] = true } } - } - } - a := make([]*CodeActivityAuthor, 0, len(authors)) - for _, v := range authors { - a = append(a, v) + a := make([]*CodeActivityAuthor, 0, len(authors)) + for _, v := range authors { + a = append(a, v) + } + // Sort authors descending depending on commit count + sort.Slice(a, func(i, j int) bool { + return a[i].Commits > a[j].Commits + }) + + stats.AuthorCount = int64(len(authors)) + stats.ChangedFiles = int64(len(files)) + stats.Authors = a + + _ = stdoutReader.Close() + return nil + }) + if err != nil { + return nil, fmt.Errorf("Failed to get GetCodeActivityStats for repository.\nError: %w\nStderr: %s", err, stderr) } - // Sort authors descending depending on commit count - sort.Slice(a, func(i, j int) bool { - return a[i].Commits > a[j].Commits - }) - - stats.AuthorCount = int64(len(authors)) - stats.ChangedFiles = int64(len(files)) - stats.Authors = a return stats, nil } diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 7780e3477ddf0..376a699502bac 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/go-git/go-git/v5/plumbing" - "github.com/mcuadros/go-version" ) // TagPrefix tags prefix path on the repository @@ -239,8 +238,6 @@ func (repo *Repository) GetTags() ([]string, error) { return nil }) - version.Sort(tagNames) - // Reverse order for i := 0; i < len(tagNames)/2; i++ { j := len(tagNames) - i - 1 diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index 8f91f4efaccba..a662aaab4f823 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -11,8 +11,6 @@ import ( "os" "strings" "time" - - "github.com/mcuadros/go-version" ) func (repo *Repository) getTree(id SHA1) (*Tree, error) { @@ -65,7 +63,7 @@ type CommitTreeOpts struct { // CommitTree creates a commit from a given tree id for the user with provided message func (repo *Repository) CommitTree(sig *Signature, tree *Tree, opts CommitTreeOpts) (SHA1, error) { - binVersion, err := BinVersion() + err := LoadGitVersion() if err != nil { return SHA1{}, err } @@ -91,11 +89,11 @@ func (repo *Repository) CommitTree(sig *Signature, tree *Tree, opts CommitTreeOp _, _ = messageBytes.WriteString(opts.Message) _, _ = messageBytes.WriteString("\n") - if version.Compare(binVersion, "1.7.9", ">=") && (opts.KeyID != "" || opts.AlwaysSign) { + if CheckGitVersionConstraint(">= 1.7.9") == nil && (opts.KeyID != "" || opts.AlwaysSign) { cmd.AddArguments(fmt.Sprintf("-S%s", opts.KeyID)) } - if version.Compare(binVersion, "2.0.0", ">=") && opts.NoGPGSign { + if CheckGitVersionConstraint(">= 2.0.0") == nil && opts.NoGPGSign { cmd.AddArguments("--no-gpg-sign") } diff --git a/modules/git/submodule.go b/modules/git/submodule.go index bb094bda5df58..231827f1e989a 100644 --- a/modules/git/submodule.go +++ b/modules/git/submodule.go @@ -39,7 +39,7 @@ func NewSubModuleFile(c *Commit, refURL, refID string) *SubModuleFile { } } -func getRefURL(refURL, urlPrefix, repoFullName string) string { +func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string { if refURL == "" { return "" } @@ -76,7 +76,7 @@ func getRefURL(refURL, urlPrefix, repoFullName string) string { pth = "/" + pth } - if urlPrefixHostname == refHostname { + if urlPrefixHostname == refHostname || refHostname == sshDomain { return urlPrefix + path.Clean(path.Join("/", pth)) } return "http://" + refHostname + pth @@ -102,7 +102,7 @@ func getRefURL(refURL, urlPrefix, repoFullName string) string { return ref.Scheme + "://" + fmt.Sprintf("%v", ref.User) + "@" + ref.Host + ref.Path } return ref.Scheme + "://" + ref.Host + ref.Path - } else if urlPrefixHostname == refHostname { + } else if urlPrefixHostname == refHostname || refHostname == sshDomain { return urlPrefix + path.Clean(path.Join("/", ref.Path)) } else { return "http://" + refHostname + ref.Path @@ -114,8 +114,8 @@ func getRefURL(refURL, urlPrefix, repoFullName string) string { } // RefURL guesses and returns reference URL. -func (sf *SubModuleFile) RefURL(urlPrefix string, repoFullName string) string { - return getRefURL(sf.refURL, urlPrefix, repoFullName) +func (sf *SubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string { + return getRefURL(sf.refURL, urlPrefix, repoFullName, sshDomain) } // RefID returns reference ID. diff --git a/modules/git/submodule_test.go b/modules/git/submodule_test.go index fcb0c6fd671b8..ff8dc579f6d51 100644 --- a/modules/git/submodule_test.go +++ b/modules/git/submodule_test.go @@ -15,27 +15,29 @@ func TestGetRefURL(t *testing.T) { refURL string prefixURL string parentPath string + SSHDomain string expect string }{ - {"git://github.com/user1/repo1", "/", "user1/repo2", "http://github.com/user1/repo1"}, - {"https://localhost/user1/repo1.git", "/", "user1/repo2", "https://localhost/user1/repo1"}, - {"http://localhost/user1/repo1.git", "/", "owner/reponame", "http://localhost/user1/repo1"}, - {"git@github.com:user1/repo1.git", "/", "owner/reponame", "http://github.com/user1/repo1"}, - {"ssh://git@git.zefie.net:2222/zefie/lge_g6_kernel_scripts.git", "/", "zefie/lge_g6_kernel", "http://git.zefie.net/zefie/lge_g6_kernel_scripts"}, - {"git@git.zefie.net:2222/zefie/lge_g6_kernel_scripts.git", "/", "zefie/lge_g6_kernel", "http://git.zefie.net/2222/zefie/lge_g6_kernel_scripts"}, - {"git@try.gitea.io:go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "https://try.gitea.io/go-gitea/gitea"}, - {"ssh://git@try.gitea.io:9999/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "https://try.gitea.io/go-gitea/gitea"}, - {"git://git@try.gitea.io:9999/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "https://try.gitea.io/go-gitea/gitea"}, - {"ssh://git@127.0.0.1:9999/go-gitea/gitea", "https://127.0.0.1:3000/", "go-gitea/sdk", "https://127.0.0.1:3000/go-gitea/gitea"}, - {"https://gitea.com:3000/user1/repo1.git", "https://127.0.0.1:3000/", "user/repo2", "https://gitea.com:3000/user1/repo1"}, - {"https://example.gitea.com/gitea/user1/repo1.git", "https://example.gitea.com/gitea/", "user/repo2", "https://example.gitea.com/gitea/user1/repo1"}, - {"https://username:password@github.com/username/repository.git", "/", "username/repository2", "https://username:password@github.com/username/repository"}, - {"somethingbad", "https://127.0.0.1:3000/go-gitea/gitea", "/", ""}, - {"git@localhost:user/repo", "https://localhost/", "user2/repo1", "https://localhost/user/repo"}, - {"../path/to/repo.git/", "https://localhost/", "user/repo2", "https://localhost/user/path/to/repo.git"}, + {"git://github.com/user1/repo1", "/", "user1/repo2", "", "http://github.com/user1/repo1"}, + {"https://localhost/user1/repo1.git", "/", "user1/repo2", "", "https://localhost/user1/repo1"}, + {"http://localhost/user1/repo1.git", "/", "owner/reponame", "", "http://localhost/user1/repo1"}, + {"git@github.com:user1/repo1.git", "/", "owner/reponame", "", "http://github.com/user1/repo1"}, + {"ssh://git@git.zefie.net:2222/zefie/lge_g6_kernel_scripts.git", "/", "zefie/lge_g6_kernel", "", "http://git.zefie.net/zefie/lge_g6_kernel_scripts"}, + {"git@git.zefie.net:2222/zefie/lge_g6_kernel_scripts.git", "/", "zefie/lge_g6_kernel", "", "http://git.zefie.net/2222/zefie/lge_g6_kernel_scripts"}, + {"git@try.gitea.io:go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"}, + {"ssh://git@try.gitea.io:9999/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"}, + {"git://git@try.gitea.io:9999/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "", "https://try.gitea.io/go-gitea/gitea"}, + {"ssh://git@127.0.0.1:9999/go-gitea/gitea", "https://127.0.0.1:3000/", "go-gitea/sdk", "", "https://127.0.0.1:3000/go-gitea/gitea"}, + {"https://gitea.com:3000/user1/repo1.git", "https://127.0.0.1:3000/", "user/repo2", "", "https://gitea.com:3000/user1/repo1"}, + {"https://example.gitea.com/gitea/user1/repo1.git", "https://example.gitea.com/gitea/", "", "user/repo2", "https://example.gitea.com/gitea/user1/repo1"}, + {"https://username:password@github.com/username/repository.git", "/", "username/repository2", "", "https://username:password@github.com/username/repository"}, + {"somethingbad", "https://127.0.0.1:3000/go-gitea/gitea", "/", "", ""}, + {"git@localhost:user/repo", "https://localhost/", "user2/repo1", "", "https://localhost/user/repo"}, + {"../path/to/repo.git/", "https://localhost/", "user/repo2", "", "https://localhost/user/path/to/repo.git"}, + {"ssh://git@ssh.gitea.io:2222/go-gitea/gitea", "https://try.gitea.io/", "go-gitea/sdk", "ssh.gitea.io", "https://try.gitea.io/go-gitea/gitea"}, } for _, kase := range kases { - assert.EqualValues(t, kase.expect, getRefURL(kase.refURL, kase.prefixURL, kase.parentPath)) + assert.EqualValues(t, kase.expect, getRefURL(kase.refURL, kase.prefixURL, kase.parentPath, kase.SSHDomain)) } } diff --git a/modules/indexer/code/elastic_search.go b/modules/indexer/code/elastic_search.go index 4f690ed8065a6..db36c5e0c4fcd 100644 --- a/modules/indexer/code/elastic_search.go +++ b/modules/indexer/code/elastic_search.go @@ -168,6 +168,11 @@ func (b *ElasticSearchIndexer) init() (bool, error) { } func (b *ElasticSearchIndexer) addUpdate(sha string, update fileUpdate, repo *models.Repository) ([]elastic.BulkableRequest, error) { + // Ignore vendored files in code search + if setting.Indexer.ExcludeVendored && enry.IsVendor(update.Filename) { + return nil, nil + } + stdout, err := git.NewCommand("cat-file", "-s", update.BlobSha). RunInDir(repo.RepoPath()) if err != nil { diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index 468955cd8938b..54563733980c8 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" ) @@ -38,7 +39,7 @@ type SearchResultLanguages struct { Count int } -// Indexer defines an interface to indexer issues contents +// Indexer defines an interface to index and search code contents type Indexer interface { Index(repo *models.Repository, sha string, changes *repoChanges) error Delete(repoID int64) error @@ -67,6 +68,40 @@ func filenameOfIndexerID(indexerID string) string { return indexerID[index+1:] } +// IndexerData represents data stored in the code indexer +type IndexerData struct { + RepoID int64 + IsDelete bool +} + +var ( + indexerQueue queue.Queue +) + +func index(indexer Indexer, repoID int64) error { + repo, err := models.GetRepositoryByID(repoID) + if err != nil { + return err + } + + sha, err := getDefaultBranchSha(repo) + if err != nil { + return err + } + changes, err := getRepoChanges(repo, sha) + if err != nil { + return err + } else if changes == nil { + return nil + } + + if err := indexer.Index(repo, sha, changes); err != nil { + return err + } + + return repo.UpdateIndexerStatus(models.RepoIndexerTypeCode, sha) +} + // Init initialize the repo indexer func Init() { if !setting.Indexer.RepoIndexerEnabled { @@ -74,8 +109,6 @@ func Init() { return } - initQueue(setting.Indexer.UpdateQueueLength) - ctx, cancel := context.WithCancel(context.Background()) graceful.GetManager().RunAtTerminate(ctx, func() { @@ -85,6 +118,46 @@ func Init() { }) waitChannel := make(chan time.Duration) + + // Create the Queue + switch setting.Indexer.RepoType { + case "bleve", "elasticsearch": + handler := func(data ...queue.Data) { + idx, err := indexer.get() + if idx == nil || err != nil { + log.Error("Codes indexer handler: unable to get indexer!") + return + } + + for _, datum := range data { + indexerData, ok := datum.(*IndexerData) + if !ok { + log.Error("Unable to process provided datum: %v - not possible to cast to IndexerData", datum) + continue + } + log.Trace("IndexerData Process: %v %t", indexerData.RepoID, indexerData.IsDelete) + + if indexerData.IsDelete { + if err := indexer.Delete(indexerData.RepoID); err != nil { + log.Error("indexer.Delete: %v", err) + } + } else { + if err := index(indexer, indexerData.RepoID); err != nil { + log.Error("index: %v", err) + continue + } + } + } + } + + indexerQueue = queue.CreateQueue("code_indexer", handler, &IndexerData{}) + if indexerQueue == nil { + log.Fatal("Unable to create codes indexer queue") + } + default: + log.Fatal("Unknown codes indexer type; %s", setting.Indexer.RepoType) + } + go func() { start := time.Now() var ( @@ -139,10 +212,11 @@ func Init() { indexer.set(rIndexer) - go processRepoIndexerOperationQueue(indexer) + // Start processing the queue + go graceful.GetManager().RunWithShutdownFns(indexerQueue.Run) if populate { - go populateRepoIndexer() + go graceful.GetManager().RunWithShutdownContext(populateRepoIndexer) } select { case waitChannel <- time.Since(start): @@ -179,3 +253,77 @@ func Init() { }() } } + +// DeleteRepoFromIndexer remove all of a repository's entries from the indexer +func DeleteRepoFromIndexer(repo *models.Repository) { + indexData := &IndexerData{RepoID: repo.ID, IsDelete: true} + if err := indexerQueue.Push(indexData); err != nil { + log.Error("Delete repo index data %v failed: %v", indexData, err) + } +} + +// UpdateRepoIndexer update a repository's entries in the indexer +func UpdateRepoIndexer(repo *models.Repository) { + indexData := &IndexerData{RepoID: repo.ID} + if err := indexerQueue.Push(indexData); err != nil { + log.Error("Update repo index data %v failed: %v", indexData, err) + } +} + +// populateRepoIndexer populate the repo indexer with pre-existing data. This +// should only be run when the indexer is created for the first time. +func populateRepoIndexer(ctx context.Context) { + log.Info("Populating the repo indexer with existing repositories") + + exist, err := models.IsTableNotEmpty("repository") + if err != nil { + log.Fatal("System error: %v", err) + } else if !exist { + return + } + + // if there is any existing repo indexer metadata in the DB, delete it + // since we are starting afresh. Also, xorm requires deletes to have a + // condition, and we want to delete everything, thus 1=1. + if err := models.DeleteAllRecords("repo_indexer_status"); err != nil { + log.Fatal("System error: %v", err) + } + + var maxRepoID int64 + if maxRepoID, err = models.GetMaxID("repository"); err != nil { + log.Fatal("System error: %v", err) + } + + // start with the maximum existing repo ID and work backwards, so that we + // don't include repos that are created after gitea starts; such repos will + // already be added to the indexer, and we don't need to add them again. + for maxRepoID > 0 { + select { + case <-ctx.Done(): + log.Info("Repository Indexer population shutdown before completion") + return + default: + } + ids, err := models.GetUnindexedRepos(models.RepoIndexerTypeCode, maxRepoID, 0, 50) + if err != nil { + log.Error("populateRepoIndexer: %v", err) + return + } else if len(ids) == 0 { + break + } + for _, id := range ids { + select { + case <-ctx.Done(): + log.Info("Repository Indexer population shutdown before completion") + return + default: + } + if err := indexerQueue.Push(&IndexerData{RepoID: id}); err != nil { + log.Error("indexerQueue.Push: %v", err) + return + } + maxRepoID = id - 1 + } + } + log.Info("Done (re)populating the repo indexer with existing repositories") +} diff --git a/modules/indexer/code/queue.go b/modules/indexer/code/queue.go deleted file mode 100644 index 844003e1fcc1a..0000000000000 --- a/modules/indexer/code/queue.go +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package code - -import ( - "os" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/graceful" - "code.gitea.io/gitea/modules/log" -) - -type repoIndexerOperation struct { - repoID int64 - deleted bool - watchers []chan<- error -} - -var repoIndexerOperationQueue chan repoIndexerOperation - -func initQueue(queueLength int) { - repoIndexerOperationQueue = make(chan repoIndexerOperation, queueLength) -} - -func index(indexer Indexer, repoID int64) error { - repo, err := models.GetRepositoryByID(repoID) - if err != nil { - return err - } - - sha, err := getDefaultBranchSha(repo) - if err != nil { - return err - } - changes, err := getRepoChanges(repo, sha) - if err != nil { - return err - } else if changes == nil { - return nil - } - - if err := indexer.Index(repo, sha, changes); err != nil { - return err - } - - return repo.UpdateIndexerStatus(models.RepoIndexerTypeCode, sha) -} - -func processRepoIndexerOperationQueue(indexer Indexer) { - for { - select { - case op := <-repoIndexerOperationQueue: - var err error - if op.deleted { - if err = indexer.Delete(op.repoID); err != nil { - log.Error("indexer.Delete: %v", err) - } - } else { - if err = index(indexer, op.repoID); err != nil { - log.Error("indexer.Index: %v", err) - } - } - for _, watcher := range op.watchers { - watcher <- err - } - case <-graceful.GetManager().IsShutdown(): - log.Info("PID: %d Repository indexer queue processing stopped", os.Getpid()) - return - } - } -} - -// DeleteRepoFromIndexer remove all of a repository's entries from the indexer -func DeleteRepoFromIndexer(repo *models.Repository, watchers ...chan<- error) { - addOperationToQueue(repoIndexerOperation{repoID: repo.ID, deleted: true, watchers: watchers}) -} - -// UpdateRepoIndexer update a repository's entries in the indexer -func UpdateRepoIndexer(repo *models.Repository, watchers ...chan<- error) { - addOperationToQueue(repoIndexerOperation{repoID: repo.ID, deleted: false, watchers: watchers}) -} - -func addOperationToQueue(op repoIndexerOperation) { - select { - case repoIndexerOperationQueue <- op: - break - default: - go func() { - repoIndexerOperationQueue <- op - }() - } -} - -// populateRepoIndexer populate the repo indexer with pre-existing data. This -// should only be run when the indexer is created for the first time. -func populateRepoIndexer() { - log.Info("Populating the repo indexer with existing repositories") - - isShutdown := graceful.GetManager().IsShutdown() - - exist, err := models.IsTableNotEmpty("repository") - if err != nil { - log.Fatal("System error: %v", err) - } else if !exist { - return - } - - // if there is any existing repo indexer metadata in the DB, delete it - // since we are starting afresh. Also, xorm requires deletes to have a - // condition, and we want to delete everything, thus 1=1. - if err := models.DeleteAllRecords("repo_indexer_status"); err != nil { - log.Fatal("System error: %v", err) - } - - var maxRepoID int64 - if maxRepoID, err = models.GetMaxID("repository"); err != nil { - log.Fatal("System error: %v", err) - } - - // start with the maximum existing repo ID and work backwards, so that we - // don't include repos that are created after gitea starts; such repos will - // already be added to the indexer, and we don't need to add them again. - for maxRepoID > 0 { - select { - case <-isShutdown: - log.Info("Repository Indexer population shutdown before completion") - return - default: - } - ids, err := models.GetUnindexedRepos(models.RepoIndexerTypeCode, maxRepoID, 0, 50) - if err != nil { - log.Error("populateRepoIndexer: %v", err) - return - } else if len(ids) == 0 { - break - } - for _, id := range ids { - select { - case <-isShutdown: - log.Info("Repository Indexer population shutdown before completion") - return - default: - } - repoIndexerOperationQueue <- repoIndexerOperation{ - repoID: id, - deleted: false, - } - maxRepoID = id - 1 - } - } - log.Info("Done (re)populating the repo indexer with existing repositories") -} diff --git a/modules/lfs/content_store.go b/modules/lfs/content_store.go index b0fa77e2550f5..cf0a05d644cef 100644 --- a/modules/lfs/content_store.go +++ b/modules/lfs/content_store.go @@ -10,11 +10,10 @@ import ( "errors" "io" "os" - "path/filepath" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/storage" ) var ( @@ -24,17 +23,15 @@ var ( // ContentStore provides a simple file system based storage. type ContentStore struct { - BasePath string + storage.ObjectStorage } // Get takes a Meta object and retrieves the content from the store, returning // it as an io.Reader. If fromByte > 0, the reader starts from that byte func (s *ContentStore) Get(meta *models.LFSMetaObject, fromByte int64) (io.ReadCloser, error) { - path := filepath.Join(s.BasePath, transformKey(meta.Oid)) - - f, err := os.Open(path) + f, err := s.Open(meta.RelativePath()) if err != nil { - log.Error("Whilst trying to read LFS OID[%s]: Unable to open %s Error: %v", meta.Oid, path, err) + log.Error("Whilst trying to read LFS OID[%s]: Unable to open Error: %v", meta.Oid, err) return nil, err } if fromByte > 0 { @@ -48,82 +45,55 @@ func (s *ContentStore) Get(meta *models.LFSMetaObject, fromByte int64) (io.ReadC // Put takes a Meta object and an io.Reader and writes the content to the store. func (s *ContentStore) Put(meta *models.LFSMetaObject, r io.Reader) error { - path := filepath.Join(s.BasePath, transformKey(meta.Oid)) - tmpPath := path + ".tmp" - - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0750); err != nil { - log.Error("Whilst putting LFS OID[%s]: Unable to create the LFS directory: %s Error: %v", meta.Oid, dir, err) - return err - } - - file, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0640) - if err != nil { - log.Error("Whilst putting LFS OID[%s]: Unable to open temporary file for writing: %s Error: %v", tmpPath, err) - return err - } - defer func() { - if err := util.Remove(tmpPath); err != nil { - log.Warn("Unable to remove temporary path: %s: Error: %v", tmpPath, err) - } - }() - hash := sha256.New() - hw := io.MultiWriter(hash, file) - - written, err := io.Copy(hw, r) + rd := io.TeeReader(r, hash) + p := meta.RelativePath() + written, err := s.Save(p, rd) if err != nil { - log.Error("Whilst putting LFS OID[%s]: Failed to copy to tmpPath: %s Error: %v", meta.Oid, tmpPath, err) - file.Close() + log.Error("Whilst putting LFS OID[%s]: Failed to copy to tmpPath: %s Error: %v", meta.Oid, p, err) return err } - file.Close() if written != meta.Size { + if err := s.Delete(p); err != nil { + log.Error("Cleaning the LFS OID[%s] failed: %v", meta.Oid, err) + } return errSizeMismatch } shaStr := hex.EncodeToString(hash.Sum(nil)) if shaStr != meta.Oid { + if err := s.Delete(p); err != nil { + log.Error("Cleaning the LFS OID[%s] failed: %v", meta.Oid, err) + } return errHashMismatch } - if err := os.Rename(tmpPath, path); err != nil { - log.Error("Whilst putting LFS OID[%s]: Unable to move tmp file to final destination: %s Error: %v", meta.Oid, path, err) - return err - } - return nil } // Exists returns true if the object exists in the content store. -func (s *ContentStore) Exists(meta *models.LFSMetaObject) bool { - path := filepath.Join(s.BasePath, transformKey(meta.Oid)) - if _, err := os.Stat(path); os.IsNotExist(err) { - return false +func (s *ContentStore) Exists(meta *models.LFSMetaObject) (bool, error) { + _, err := s.ObjectStorage.Stat(meta.RelativePath()) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err } - return true + return true, nil } // Verify returns true if the object exists in the content store and size is correct. func (s *ContentStore) Verify(meta *models.LFSMetaObject) (bool, error) { - path := filepath.Join(s.BasePath, transformKey(meta.Oid)) - - fi, err := os.Stat(path) - if os.IsNotExist(err) || err == nil && fi.Size() != meta.Size { + p := meta.RelativePath() + fi, err := s.ObjectStorage.Stat(p) + if os.IsNotExist(err) || (err == nil && fi.Size() != meta.Size) { return false, nil } else if err != nil { - log.Error("Unable stat file: %s for LFS OID[%s] Error: %v", path, meta.Oid, err) + log.Error("Unable stat file: %s for LFS OID[%s] Error: %v", p, meta.Oid, err) return false, err } return true, nil } - -func transformKey(key string) string { - if len(key) < 5 { - return key - } - - return filepath.Join(key[0:2], key[2:4], key[4:]) -} diff --git a/modules/lfs/pointers.go b/modules/lfs/pointers.go index bc27ee37a7fb5..c6fbf090e5164 100644 --- a/modules/lfs/pointers.go +++ b/modules/lfs/pointers.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" ) // ReadPointerFile will return a partially filled LFSMetaObject if the provided reader is a pointer file @@ -53,9 +54,10 @@ func IsPointerFile(buf *[]byte) *models.LFSMetaObject { return nil } - contentStore := &ContentStore{BasePath: setting.LFS.ContentPath} + contentStore := &ContentStore{ObjectStorage: storage.LFS} meta := &models.LFSMetaObject{Oid: oid, Size: size} - if !contentStore.Exists(meta) { + exist, err := contentStore.Exists(meta) + if err != nil || !exist { return nil } @@ -64,6 +66,6 @@ func IsPointerFile(buf *[]byte) *models.LFSMetaObject { // ReadMetaObject will read a models.LFSMetaObject and return a reader func ReadMetaObject(meta *models.LFSMetaObject) (io.ReadCloser, error) { - contentStore := &ContentStore{BasePath: setting.LFS.ContentPath} + contentStore := &ContentStore{ObjectStorage: storage.LFS} return contentStore.Get(meta, 0) } diff --git a/modules/lfs/server.go b/modules/lfs/server.go index f227ebe2eb154..2801f8410cc06 100644 --- a/modules/lfs/server.go +++ b/modules/lfs/server.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" "gitea.com/macaron/macaron" "github.com/dgrijalva/jwt-go" @@ -187,7 +188,7 @@ func getContentHandler(ctx *context.Context) { } } - contentStore := &ContentStore{BasePath: setting.LFS.ContentPath} + contentStore := &ContentStore{ObjectStorage: storage.LFS} content, err := contentStore.Get(meta, fromByte) if err != nil { // Errors are logged in contentStore.Get @@ -288,8 +289,14 @@ func PostHandler(ctx *context.Context) { ctx.Resp.Header().Set("Content-Type", metaMediaType) sentStatus := 202 - contentStore := &ContentStore{BasePath: setting.LFS.ContentPath} - if meta.Existing && contentStore.Exists(meta) { + contentStore := &ContentStore{ObjectStorage: storage.LFS} + exist, err := contentStore.Exists(meta) + if err != nil { + log.Error("Unable to check if LFS OID[%s] exist on %s / %s. Error: %v", rv.Oid, rv.User, rv.Repo, err) + writeStatus(ctx, 500) + return + } + if meta.Existing && exist { sentStatus = 200 } ctx.Resp.WriteHeader(sentStatus) @@ -343,12 +350,20 @@ func BatchHandler(ctx *context.Context) { return } - contentStore := &ContentStore{BasePath: setting.LFS.ContentPath} + contentStore := &ContentStore{ObjectStorage: storage.LFS} meta, err := repository.GetLFSMetaObjectByOid(object.Oid) - if err == nil && contentStore.Exists(meta) { // Object is found and exists - responseObjects = append(responseObjects, Represent(object, meta, true, false)) - continue + if err == nil { // Object is found and exists + exist, err := contentStore.Exists(meta) + if err != nil { + log.Error("Unable to check if LFS OID[%s] exist on %s / %s. Error: %v", object.Oid, object.User, object.Repo, err) + writeStatus(ctx, 500) + return + } + if exist { + responseObjects = append(responseObjects, Represent(object, meta, true, false)) + continue + } } if requireWrite && setting.LFS.MaxFileSize > 0 && object.Size > setting.LFS.MaxFileSize { @@ -360,7 +375,13 @@ func BatchHandler(ctx *context.Context) { // Object is not found meta, err = models.NewLFSMetaObject(&models.LFSMetaObject{Oid: object.Oid, Size: object.Size, RepositoryID: repository.ID}) if err == nil { - responseObjects = append(responseObjects, Represent(object, meta, meta.Existing, !contentStore.Exists(meta))) + exist, err := contentStore.Exists(meta) + if err != nil { + log.Error("Unable to check if LFS OID[%s] exist on %s / %s. Error: %v", object.Oid, object.User, object.Repo, err) + writeStatus(ctx, 500) + return + } + responseObjects = append(responseObjects, Represent(object, meta, meta.Existing, !exist)) } else { log.Error("Unable to write LFS OID[%s] size %d meta object in %v/%v to database. Error: %v", object.Oid, object.Size, object.User, object.Repo, err) } @@ -387,7 +408,7 @@ func PutHandler(ctx *context.Context) { return } - contentStore := &ContentStore{BasePath: setting.LFS.ContentPath} + contentStore := &ContentStore{ObjectStorage: storage.LFS} bodyReader := ctx.Req.Body().ReadCloser() defer bodyReader.Close() if err := contentStore.Put(meta, bodyReader); err != nil { @@ -429,7 +450,7 @@ func VerifyHandler(ctx *context.Context) { return } - contentStore := &ContentStore{BasePath: setting.LFS.ContentPath} + contentStore := &ContentStore{ObjectStorage: storage.LFS} ok, err := contentStore.Verify(meta) if err != nil { // Error will be logged in Verify diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go index 40323912b4276..020a3f592ad85 100644 --- a/modules/markup/orgmode/orgmode_test.go +++ b/modules/markup/orgmode/orgmode_test.go @@ -27,12 +27,12 @@ func TestRender_StandardLinks(t *testing.T) { assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) } - googleRendered := "

\nhttps://google.com/\n

" + googleRendered := "

https://google.com/

" test("[[https://google.com/]]", googleRendered) lnk := util.URLJoin(AppSubURL, "WikiPage") test("[[WikiPage][WikiPage]]", - "

\nWikiPage\n

") + "

WikiPage

") } func TestRender_Images(t *testing.T) { @@ -48,5 +48,5 @@ func TestRender_Images(t *testing.T) { result := util.URLJoin(AppSubURL, url) test("[[file:"+url+"]]", - "

\n\""+result+"\"\n

") + "

\""+result+"\"

") } diff --git a/modules/migrations/gitlab.go b/modules/migrations/gitlab.go index 3cdcef3afa5e2..7e7d88b702d1e 100644 --- a/modules/migrations/gitlab.go +++ b/modules/migrations/gitlab.go @@ -234,10 +234,10 @@ func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) { var perPage = 100 var labels = make([]*base.Label, 0, perPage) for i := 1; ; i++ { - ls, _, err := g.client.Labels.ListLabels(g.repoID, &gitlab.ListLabelsOptions{ + ls, _, err := g.client.Labels.ListLabels(g.repoID, &gitlab.ListLabelsOptions{ListOptions: gitlab.ListOptions{ Page: i, PerPage: perPage, - }, nil, gitlab.WithContext(g.ctx)) + }}, nil, gitlab.WithContext(g.ctx)) if err != nil { return nil, err } diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index 428f9a9544f3c..5cd2b4c060595 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -28,6 +28,7 @@ type Notifier interface { NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string) NotifyIssueClearLabels(doer *models.User, issue *models.Issue) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) + NotifyIssueChangeRef(doer *models.User, issue *models.Issue, oldRef string) NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, addedLabels []*models.Label, removedLabels []*models.Label) diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index b2ce0742b6c16..15d06ec856305 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -102,6 +102,10 @@ func (*NullNotifier) NotifyIssueClearLabels(doer *models.User, issue *models.Iss func (*NullNotifier) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) { } +// NotifyIssueChangeRef places a place holder function +func (*NullNotifier) NotifyIssueChangeRef(doer *models.User, issue *models.Issue, oldTitle string) { +} + // NotifyIssueChangeLabels places a place holder function func (*NullNotifier) NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, addedLabels []*models.Label, removedLabels []*models.Label) { diff --git a/modules/notification/indexer/indexer.go b/modules/notification/indexer/indexer.go index f292d7339b20c..6e848e6318ad5 100644 --- a/modules/notification/indexer/indexer.go +++ b/modules/notification/indexer/indexer.go @@ -148,3 +148,7 @@ func (r *indexerNotifier) NotifyIssueChangeContent(doer *models.User, issue *mod func (r *indexerNotifier) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) { issue_indexer.UpdateIssueIndexer(issue) } + +func (r *indexerNotifier) NotifyIssueChangeRef(doer *models.User, issue *models.Issue, oldRef string) { + issue_indexer.UpdateIssueIndexer(issue) +} diff --git a/modules/notification/notification.go b/modules/notification/notification.go index d17b13b9e53f3..57f1e7c16df81 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -178,6 +178,13 @@ func NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle str } } +// NotifyIssueChangeRef notifies change reference to notifiers +func NotifyIssueChangeRef(doer *models.User, issue *models.Issue, oldRef string) { + for _, notifier := range notifiers { + notifier.NotifyIssueChangeRef(doer, issue, oldRef) + } +} + // NotifyIssueChangeLabels notifies change labels to notifiers func NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, addedLabels []*models.Label, removedLabels []*models.Label) { diff --git a/modules/password/password.go b/modules/password/password.go index 1c4b9c514a459..e1f1f769ec73e 100644 --- a/modules/password/password.go +++ b/modules/password/password.go @@ -6,6 +6,7 @@ package password import ( "bytes" + goContext "context" "crypto/rand" "math/big" "strings" @@ -88,7 +89,7 @@ func IsComplexEnough(pwd string) bool { return true } -// Generate a random password +// Generate a random password func Generate(n int) (string, error) { NewComplexity() buffer := make([]byte, n) @@ -101,7 +102,11 @@ func Generate(n int) (string, error) { } buffer[j] = validChars[rnd.Int64()] } - if IsComplexEnough(string(buffer)) && string(buffer[0]) != " " && string(buffer[n-1]) != " " { + pwned, err := IsPwned(goContext.Background(), string(buffer)) + if err != nil { + return "", err + } + if IsComplexEnough(string(buffer)) && !pwned && string(buffer[0]) != " " && string(buffer[n-1]) != " " { return string(buffer), nil } } diff --git a/modules/password/pwn.go b/modules/password/pwn.go new file mode 100644 index 0000000000000..938524e6dee28 --- /dev/null +++ b/modules/password/pwn.go @@ -0,0 +1,30 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package password + +import ( + "context" + + "code.gitea.io/gitea/modules/setting" + + "go.jolheiser.com/pwn" +) + +// IsPwned checks whether a password has been pwned +// NOTE: This func returns true if it encounters an error under the assumption that you ALWAYS want to check against +// HIBP, so not getting a response should block a password until it can be verified. +func IsPwned(ctx context.Context, password string) (bool, error) { + if !setting.PasswordCheckPwn { + return false, nil + } + + client := pwn.New(pwn.WithContext(ctx)) + count, err := client.CheckPassword(password, true) + if err != nil { + return true, err + } + + return count > 0, nil +} diff --git a/modules/public/public.go b/modules/public/public.go index 008fba9b01885..2dcc530a739a5 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -159,7 +159,7 @@ func (opts *Options) handle(ctx *macaron.Context, log *log.Logger, opt *Options) // Add an Expires header to the static content if opt.ExpiresAfter > 0 { ctx.Resp.Header().Set("Expires", time.Now().Add(opt.ExpiresAfter).UTC().Format(http.TimeFormat)) - tag := GenerateETag(fmt.Sprintf("%d", fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat)) + tag := GenerateETag(fmt.Sprint(fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat)) ctx.Resp.Header().Set("ETag", tag) if ctx.Req.Header.Get("If-None-Match") == tag { ctx.Resp.WriteHeader(304) diff --git a/modules/queue/queue.go b/modules/queue/queue.go index e3c63310bef5c..d08cba35a1ea5 100644 --- a/modules/queue/queue.go +++ b/modules/queue/queue.go @@ -106,7 +106,64 @@ func (*DummyQueue) IsEmpty() bool { return true } -var queuesMap = map[Type]NewQueueFunc{DummyQueueType: NewDummyQueue} +// ImmediateType is the type to execute the function when push +const ImmediateType Type = "immediate" + +// NewImmediate creates a new false queue to execute the function when push +func NewImmediate(handler HandlerFunc, opts, exemplar interface{}) (Queue, error) { + return &Immediate{ + handler: handler, + }, nil +} + +// Immediate represents an direct execution queue +type Immediate struct { + handler HandlerFunc +} + +// Run does nothing +func (*Immediate) Run(_, _ func(context.Context, func())) {} + +// Push fakes a push of data to the queue +func (q *Immediate) Push(data Data) error { + return q.PushFunc(data, nil) +} + +// PushFunc fakes a push of data to the queue with a function. The function is never run. +func (q *Immediate) PushFunc(data Data, f func() error) error { + if f != nil { + if err := f(); err != nil { + return err + } + } + q.handler(data) + return nil +} + +// Has always returns false as this queue never does anything +func (*Immediate) Has(Data) (bool, error) { + return false, nil +} + +// Flush always returns nil +func (*Immediate) Flush(time.Duration) error { + return nil +} + +// FlushWithContext always returns nil +func (*Immediate) FlushWithContext(context.Context) error { + return nil +} + +// IsEmpty asserts that the queue is empty +func (*Immediate) IsEmpty() bool { + return true +} + +var queuesMap = map[Type]NewQueueFunc{ + DummyQueueType: NewDummyQueue, + ImmediateType: NewImmediate, +} // RegisteredTypes provides the list of requested types of queues func RegisteredTypes() []Type { diff --git a/modules/queue/queue_disk_channel_test.go b/modules/queue/queue_disk_channel_test.go index 5049fb58d0a8d..93061bffc6586 100644 --- a/modules/queue/queue_disk_channel_test.go +++ b/modules/queue/queue_disk_channel_test.go @@ -52,7 +52,7 @@ func TestPersistableChannelQueue(t *testing.T) { err = queue.Push(&test1) assert.NoError(t, err) go func() { - err = queue.Push(&test2) + err := queue.Push(&test2) assert.NoError(t, err) }() diff --git a/modules/queue/queue_disk_test.go b/modules/queue/queue_disk_test.go index 789b6c3e64092..edaed49a52396 100644 --- a/modules/queue/queue_disk_test.go +++ b/modules/queue/queue_disk_test.go @@ -65,7 +65,7 @@ func TestLevelQueue(t *testing.T) { err = queue.Push(&test1) assert.NoError(t, err) go func() { - err = queue.Push(&test2) + err := queue.Push(&test2) assert.NoError(t, err) }() diff --git a/modules/references/references.go b/modules/references/references.go index ce08dcc7ab8cc..070c6e566a8e7 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -37,6 +37,8 @@ var ( crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) // spaceTrimmedPattern let's us find the trailing space spaceTrimmedPattern = regexp.MustCompile(`(?:.*[0-9a-zA-Z-_])\s`) + // timeLogPattern matches string for time tracking + timeLogPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@([0-9]+([\.,][0-9]+)?(w|d|m|h))+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`) issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp issueKeywordsOnce sync.Once @@ -62,10 +64,11 @@ const ( // IssueReference contains an unverified cross-reference to a local issue or pull request type IssueReference struct { - Index int64 - Owner string - Name string - Action XRefAction + Index int64 + Owner string + Name string + Action XRefAction + TimeLog string } // RenderizableReference contains an unverified cross-reference to with rendering information @@ -91,16 +94,18 @@ type rawReference struct { issue string refLocation *RefSpan actionLocation *RefSpan + timeLog string } func rawToIssueReferenceList(reflist []*rawReference) []IssueReference { refarr := make([]IssueReference, len(reflist)) for i, r := range reflist { refarr[i] = IssueReference{ - Index: r.index, - Owner: r.owner, - Name: r.name, - Action: r.action, + Index: r.index, + Owner: r.owner, + Name: r.name, + Action: r.action, + TimeLog: r.timeLog, } } return refarr @@ -386,6 +391,38 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference } } + if len(ret) == 0 { + return ret + } + + pos = 0 + + for { + match := timeLogPattern.FindSubmatchIndex(content[pos:]) + if match == nil { + break + } + + timeLogEntry := string(content[match[2]+pos+1 : match[3]+pos]) + + var f *rawReference + for _, ref := range ret { + if ref.refLocation != nil && ref.refLocation.End < match[2]+pos && (f == nil || f.refLocation.End < ref.refLocation.End) { + f = ref + } + } + + pos = match[1] + pos + + if f == nil { + f = ret[0] + } + + if len(f.timeLog) == 0 { + f.timeLog = timeLogEntry + } + } + return ret } diff --git a/modules/references/references_test.go b/modules/references/references_test.go index 48589c1637be5..0c4037f1204af 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -26,6 +26,7 @@ type testResult struct { Action XRefAction RefLocation *RefSpan ActionLocation *RefSpan + TimeLog string } func TestFindAllIssueReferences(t *testing.T) { @@ -34,19 +35,19 @@ func TestFindAllIssueReferences(t *testing.T) { { "Simply closes: #29 yes", []testResult{ - {29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}}, + {29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}, ""}, }, }, { "Simply closes: !29 yes", []testResult{ - {29, "", "", "29", true, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}}, + {29, "", "", "29", true, XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}, ""}, }, }, { " #124 yes, this is a reference.", []testResult{ - {124, "", "", "124", false, XRefActionNone, &RefSpan{Start: 0, End: 4}, nil}, + {124, "", "", "124", false, XRefActionNone, &RefSpan{Start: 0, End: 4}, nil, ""}, }, }, { @@ -60,13 +61,13 @@ func TestFindAllIssueReferences(t *testing.T) { { "This user3/repo4#200 yes.", []testResult{ - {200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil}, + {200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""}, }, }, { "This user3/repo4!200 yes.", []testResult{ - {200, "user3", "repo4", "200", true, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil}, + {200, "user3", "repo4", "200", true, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""}, }, }, { @@ -76,19 +77,19 @@ func TestFindAllIssueReferences(t *testing.T) { { "This [two](/user2/repo1/issues/921) yes.", []testResult{ - {921, "user2", "repo1", "921", false, XRefActionNone, nil, nil}, + {921, "user2", "repo1", "921", false, XRefActionNone, nil, nil, ""}, }, }, { "This [three](/user2/repo1/pulls/922) yes.", []testResult{ - {922, "user2", "repo1", "922", true, XRefActionNone, nil, nil}, + {922, "user2", "repo1", "922", true, XRefActionNone, nil, nil, ""}, }, }, { "This [four](http://gitea.com:3000/user3/repo4/issues/203) yes.", []testResult{ - {203, "user3", "repo4", "203", false, XRefActionNone, nil, nil}, + {203, "user3", "repo4", "203", false, XRefActionNone, nil, nil, ""}, }, }, { @@ -102,49 +103,49 @@ func TestFindAllIssueReferences(t *testing.T) { { "This http://gitea.com:3000/user4/repo5/pulls/202 yes.", []testResult{ - {202, "user4", "repo5", "202", true, XRefActionNone, nil, nil}, + {202, "user4", "repo5", "202", true, XRefActionNone, nil, nil, ""}, }, }, { "This http://GiTeA.COM:3000/user4/repo6/pulls/205 yes.", []testResult{ - {205, "user4", "repo6", "205", true, XRefActionNone, nil, nil}, + {205, "user4", "repo6", "205", true, XRefActionNone, nil, nil, ""}, }, }, { "Reopens #15 yes", []testResult{ - {15, "", "", "15", false, XRefActionReopens, &RefSpan{Start: 8, End: 11}, &RefSpan{Start: 0, End: 7}}, + {15, "", "", "15", false, XRefActionReopens, &RefSpan{Start: 8, End: 11}, &RefSpan{Start: 0, End: 7}, ""}, }, }, { "This closes #20 for you yes", []testResult{ - {20, "", "", "20", false, XRefActionCloses, &RefSpan{Start: 12, End: 15}, &RefSpan{Start: 5, End: 11}}, + {20, "", "", "20", false, XRefActionCloses, &RefSpan{Start: 12, End: 15}, &RefSpan{Start: 5, End: 11}, ""}, }, }, { "Do you fix user6/repo6#300 ? yes", []testResult{ - {300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 11, End: 26}, &RefSpan{Start: 7, End: 10}}, + {300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 11, End: 26}, &RefSpan{Start: 7, End: 10}, ""}, }, }, { "For 999 #1235 no keyword, but yes", []testResult{ - {1235, "", "", "1235", false, XRefActionNone, &RefSpan{Start: 8, End: 13}, nil}, + {1235, "", "", "1235", false, XRefActionNone, &RefSpan{Start: 8, End: 13}, nil, ""}, }, }, { "For [!123] yes", []testResult{ - {123, "", "", "123", true, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil}, + {123, "", "", "123", true, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil, ""}, }, }, { "For (#345) yes", []testResult{ - {345, "", "", "345", false, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil}, + {345, "", "", "345", false, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil, ""}, }, }, { @@ -154,31 +155,39 @@ func TestFindAllIssueReferences(t *testing.T) { { "For #24, and #25. yes; also #26; #27? #28! and #29: should", []testResult{ - {24, "", "", "24", false, XRefActionNone, &RefSpan{Start: 4, End: 7}, nil}, - {25, "", "", "25", false, XRefActionNone, &RefSpan{Start: 13, End: 16}, nil}, - {26, "", "", "26", false, XRefActionNone, &RefSpan{Start: 28, End: 31}, nil}, - {27, "", "", "27", false, XRefActionNone, &RefSpan{Start: 33, End: 36}, nil}, - {28, "", "", "28", false, XRefActionNone, &RefSpan{Start: 38, End: 41}, nil}, - {29, "", "", "29", false, XRefActionNone, &RefSpan{Start: 47, End: 50}, nil}, + {24, "", "", "24", false, XRefActionNone, &RefSpan{Start: 4, End: 7}, nil, ""}, + {25, "", "", "25", false, XRefActionNone, &RefSpan{Start: 13, End: 16}, nil, ""}, + {26, "", "", "26", false, XRefActionNone, &RefSpan{Start: 28, End: 31}, nil, ""}, + {27, "", "", "27", false, XRefActionNone, &RefSpan{Start: 33, End: 36}, nil, ""}, + {28, "", "", "28", false, XRefActionNone, &RefSpan{Start: 38, End: 41}, nil, ""}, + {29, "", "", "29", false, XRefActionNone, &RefSpan{Start: 47, End: 50}, nil, ""}, }, }, { "This user3/repo4#200, yes.", []testResult{ - {200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil}, + {200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil, ""}, }, }, { "Which abc. #9434 same as above", []testResult{ - {9434, "", "", "9434", false, XRefActionNone, &RefSpan{Start: 11, End: 16}, nil}, + {9434, "", "", "9434", false, XRefActionNone, &RefSpan{Start: 11, End: 16}, nil, ""}, }, }, { "This closes #600 and reopens #599", []testResult{ - {600, "", "", "600", false, XRefActionCloses, &RefSpan{Start: 12, End: 16}, &RefSpan{Start: 5, End: 11}}, - {599, "", "", "599", false, XRefActionReopens, &RefSpan{Start: 29, End: 33}, &RefSpan{Start: 21, End: 28}}, + {600, "", "", "600", false, XRefActionCloses, &RefSpan{Start: 12, End: 16}, &RefSpan{Start: 5, End: 11}, ""}, + {599, "", "", "599", false, XRefActionReopens, &RefSpan{Start: 29, End: 33}, &RefSpan{Start: 21, End: 28}, ""}, + }, + }, + { + "This fixes #100 spent @40m and reopens #101, also fixes #102 spent @4h15m", + []testResult{ + {100, "", "", "100", false, XRefActionCloses, &RefSpan{Start: 11, End: 15}, &RefSpan{Start: 5, End: 10}, "40m"}, + {101, "", "", "101", false, XRefActionReopens, &RefSpan{Start: 39, End: 43}, &RefSpan{Start: 31, End: 38}, ""}, + {102, "", "", "102", false, XRefActionCloses, &RefSpan{Start: 56, End: 60}, &RefSpan{Start: 50, End: 55}, "4h15m"}, }, }, } @@ -237,6 +246,7 @@ func testFixtures(t *testing.T, fixtures []testFixture, context string) { issue: e.Issue, refLocation: e.RefLocation, actionLocation: e.ActionLocation, + timeLog: e.TimeLog, } } expref := rawToIssueReferenceList(expraw) @@ -382,25 +392,25 @@ func TestCustomizeCloseKeywords(t *testing.T) { { "Simplemente cierra: #29 yes", []testResult{ - {29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 20, End: 23}, &RefSpan{Start: 12, End: 18}}, + {29, "", "", "29", false, XRefActionCloses, &RefSpan{Start: 20, End: 23}, &RefSpan{Start: 12, End: 18}, ""}, }, }, { "Closes: #123 no, this English.", []testResult{ - {123, "", "", "123", false, XRefActionNone, &RefSpan{Start: 8, End: 12}, nil}, + {123, "", "", "123", false, XRefActionNone, &RefSpan{Start: 8, End: 12}, nil, ""}, }, }, { "Cerró user6/repo6#300 yes", []testResult{ - {300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}}, + {300, "user6", "repo6", "300", false, XRefActionCloses, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}, ""}, }, }, { "Reabre user3/repo4#200 yes", []testResult{ - {200, "user3", "repo4", "200", false, XRefActionReopens, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}}, + {200, "user3", "repo4", "200", false, XRefActionReopens, &RefSpan{Start: 7, End: 22}, &RefSpan{Start: 0, End: 6}, ""}, }, }, } diff --git a/modules/repofiles/action.go b/modules/repofiles/action.go index 464249d19ba48..05e9fc958dd38 100644 --- a/modules/repofiles/action.go +++ b/modules/repofiles/action.go @@ -8,7 +8,10 @@ import ( "encoding/json" "fmt" "html" + "regexp" + "strconv" "strings" + "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" @@ -19,6 +22,16 @@ import ( "code.gitea.io/gitea/modules/setting" ) +const ( + secondsByMinute = float64(time.Minute / time.Second) // seconds in a minute + secondsByHour = 60 * secondsByMinute // seconds in an hour + secondsByDay = 8 * secondsByHour // seconds in a day + secondsByWeek = 5 * secondsByDay // seconds in a week + secondsByMonth = 4 * secondsByWeek // seconds in a month +) + +var reDuration = regexp.MustCompile(`(?i)^(?:(\d+([\.,]\d+)?)(?:mo))?(?:(\d+([\.,]\d+)?)(?:w))?(?:(\d+([\.,]\d+)?)(?:d))?(?:(\d+([\.,]\d+)?)(?:h))?(?:(\d+([\.,]\d+)?)(?:m))?$`) + // getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue // if the provided ref references a non-existent issue. func getIssueFromRef(repo *models.Repository, index int64) (*models.Issue, error) { @@ -32,6 +45,60 @@ func getIssueFromRef(repo *models.Repository, index int64) (*models.Issue, error return issue, nil } +// timeLogToAmount parses time log string and returns amount in seconds +func timeLogToAmount(str string) int64 { + matches := reDuration.FindAllStringSubmatch(str, -1) + if len(matches) == 0 { + return 0 + } + + match := matches[0] + + var a int64 + + // months + if len(match[1]) > 0 { + mo, _ := strconv.ParseFloat(strings.Replace(match[1], ",", ".", 1), 64) + a += int64(mo * secondsByMonth) + } + + // weeks + if len(match[3]) > 0 { + w, _ := strconv.ParseFloat(strings.Replace(match[3], ",", ".", 1), 64) + a += int64(w * secondsByWeek) + } + + // days + if len(match[5]) > 0 { + d, _ := strconv.ParseFloat(strings.Replace(match[5], ",", ".", 1), 64) + a += int64(d * secondsByDay) + } + + // hours + if len(match[7]) > 0 { + h, _ := strconv.ParseFloat(strings.Replace(match[7], ",", ".", 1), 64) + a += int64(h * secondsByHour) + } + + // minutes + if len(match[9]) > 0 { + d, _ := strconv.ParseFloat(strings.Replace(match[9], ",", ".", 1), 64) + a += int64(d * secondsByMinute) + } + + return a +} + +func issueAddTime(issue *models.Issue, doer *models.User, time time.Time, timeLog string) error { + amount := timeLogToAmount(timeLog) + if amount == 0 { + return nil + } + + _, err := models.AddTime(doer, issue, amount, time) + return err +} + func changeIssueStatus(repo *models.Repository, issue *models.Issue, doer *models.User, closed bool) error { stopTimerIfAvailable := func(doer *models.User, issue *models.Issue) error { @@ -139,6 +206,11 @@ func UpdateIssuesCommit(doer *models.User, repo *models.Repository, commits []*r } } close := (ref.Action == references.XRefActionCloses) + if close && len(ref.TimeLog) > 0 { + if err := issueAddTime(refIssue, doer, c.Timestamp, ref.TimeLog); err != nil { + return err + } + } if close != refIssue.IsClosed { if err := changeIssueStatus(refRepo, refIssue, doer, close); err != nil { return err diff --git a/modules/repofiles/temp_repo.go b/modules/repofiles/temp_repo.go index 2b03db8b4a310..ec671a9322cab 100644 --- a/modules/repofiles/temp_repo.go +++ b/modules/repofiles/temp_repo.go @@ -19,8 +19,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/gitdiff" - - "github.com/mcuadros/go-version" ) // TemporaryUploadRepository is a type to wrap our upload repositories as a shallow clone @@ -196,7 +194,7 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models authorSig := author.NewGitSig() committerSig := committer.NewGitSig() - binVersion, err := git.BinVersion() + err := git.LoadGitVersion() if err != nil { return "", fmt.Errorf("Unable to get git version: %v", err) } @@ -218,11 +216,11 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models args := []string{"commit-tree", treeHash, "-p", "HEAD"} // Determine if we should sign - if version.Compare(binVersion, "1.7.9", ">=") { + if git.CheckGitVersionConstraint(">= 1.7.9") == nil { sign, keyID, _ := t.repo.SignCRUDAction(author, t.basePath, "HEAD") if sign { args = append(args, "-S"+keyID) - } else if version.Compare(binVersion, "2.0.0", ">=") { + } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil { args = append(args, "--no-gpg-sign") } } @@ -309,7 +307,7 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) { // CheckAttribute checks the given attribute of the provided files func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...string) (map[string]map[string]string, error) { - binVersion, err := git.BinVersion() + err := git.LoadGitVersion() if err != nil { log.Error("Error retrieving git version: %v", err) return nil, err @@ -321,7 +319,7 @@ func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...str cmdArgs := []string{"check-attr", "-z", attribute} // git check-attr --cached first appears in git 1.7.8 - if version.Compare(binVersion, "1.7.8", ">=") { + if git.CheckGitVersionConstraint(">= 1.7.8") == nil { cmdArgs = append(cmdArgs, "--cached") } cmdArgs = append(cmdArgs, "--") diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index d65f61c8409e2..84a3bcb64ea8d 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/structs" pull_service "code.gitea.io/gitea/services/pull" @@ -433,8 +434,12 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up if err != nil { return nil, err } - contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath} - if !contentStore.Exists(lfsMetaObject) { + contentStore := &lfs.ContentStore{ObjectStorage: storage.LFS} + exist, err := contentStore.Exists(lfsMetaObject) + if err != nil { + return nil, err + } + if !exist { if err := contentStore.Put(lfsMetaObject, strings.NewReader(opts.Content)); err != nil { if _, err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil { return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err) diff --git a/modules/repofiles/upload.go b/modules/repofiles/upload.go index eb1379560dfff..e3ec48ec0f9e7 100644 --- a/modules/repofiles/upload.go +++ b/modules/repofiles/upload.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" ) // UploadRepoFileOptions contains the uploaded repository file options @@ -163,12 +164,16 @@ func UploadRepoFiles(repo *models.Repository, doer *models.User, opts *UploadRep // OK now we can insert the data into the store - there's no way to clean up the store // once it's in there, it's in there. - contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath} + contentStore := &lfs.ContentStore{ObjectStorage: storage.LFS} for _, uploadInfo := range infos { if uploadInfo.lfsMetaObject == nil { continue } - if !contentStore.Exists(uploadInfo.lfsMetaObject) { + exist, err := contentStore.Exists(uploadInfo.lfsMetaObject) + if err != nil { + return cleanUpAfterFailure(&infos, t, err) + } + if !exist { file, err := os.Open(uploadInfo.upload.LocalPath()) if err != nil { return cleanUpAfterFailure(&infos, t, err) diff --git a/modules/repository/create.go b/modules/repository/create.go index 945f4a8cea6c4..abbec05a37b0f 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -23,6 +23,10 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m } } + if len(opts.DefaultBranch) == 0 { + opts.DefaultBranch = setting.Repository.DefaultBranch + } + repo := &models.Repository{ OwnerID: u.ID, Owner: u, diff --git a/modules/repository/init.go b/modules/repository/init.go index 8c8827c511ac6..c2038e18d4ca2 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -19,7 +19,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - "github.com/mcuadros/go-version" "github.com/unknwon/com" ) @@ -122,7 +121,7 @@ func initRepoCommit(tmpPath string, repo *models.Repository, u *models.User, def return fmt.Errorf("git add --all: %v", err) } - binVersion, err := git.BinVersion() + err = git.LoadGitVersion() if err != nil { return fmt.Errorf("Unable to get git version: %v", err) } @@ -132,11 +131,11 @@ func initRepoCommit(tmpPath string, repo *models.Repository, u *models.User, def "-m", "Initial commit", } - if version.Compare(binVersion, "1.7.9", ">=") { + if git.CheckGitVersionConstraint(">= 1.7.9") == nil { sign, keyID, _ := models.SignInitialCommit(tmpPath, u) if sign { args = append(args, "-S"+keyID) - } else if version.Compare(binVersion, "2.0.0", ">=") { + } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil { args = append(args, "--no-gpg-sign") } } diff --git a/modules/setting/git.go b/modules/setting/git.go index f83758f3879c4..9136ea7f17ac2 100644 --- a/modules/setting/git.go +++ b/modules/setting/git.go @@ -9,8 +9,6 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - - version "github.com/mcuadros/go-version" ) var ( @@ -71,20 +69,20 @@ func newGit() { } git.DefaultCommandExecutionTimeout = time.Duration(Git.Timeout.Default) * time.Second - binVersion, err := git.BinVersion() + version, err := git.LocalVersion() if err != nil { log.Fatal("Error retrieving git version: %v", err) } - if version.Compare(binVersion, "2.9", ">=") { + if git.CheckGitVersionConstraint(">= 2.9") == nil { // Explicitly disable credential helper, otherwise Git credentials might leak git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "credential.helper=") } var format = "Git Version: %s" - var args = []interface{}{binVersion} + var args = []interface{}{version.Original()} // Since git wire protocol has been released from git v2.18 - if Git.EnableAutoGitWireProtocol && version.Compare(binVersion, "2.18", ">=") { + if Git.EnableAutoGitWireProtocol && git.CheckGitVersionConstraint(">= 2.18") == nil { git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "protocol.version=2") format += ", Wire Protocol %s Enabled" args = append(args, "Version 2") // for focus color diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go new file mode 100644 index 0000000000000..a740a6d6292a4 --- /dev/null +++ b/modules/setting/lfs.go @@ -0,0 +1,122 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package setting + +import ( + "encoding/base64" + "os" + "path/filepath" + "time" + + "code.gitea.io/gitea/modules/generate" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + + "github.com/unknwon/com" + ini "gopkg.in/ini.v1" +) + +// LFS represents the configuration for Git LFS +var LFS = struct { + StartServer bool `ini:"LFS_START_SERVER"` + ContentPath string `ini:"LFS_CONTENT_PATH"` + JWTSecretBase64 string `ini:"LFS_JWT_SECRET"` + JWTSecretBytes []byte `ini:"-"` + HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"` + MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"` + LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"` + + StoreType string + ServeDirect bool + Minio struct { + Endpoint string + AccessKeyID string + SecretAccessKey string + UseSSL bool + Bucket string + Location string + BasePath string + } +}{ + StoreType: "local", +} + +func newLFSService() { + sec := Cfg.Section("server") + if err := sec.MapTo(&LFS); err != nil { + log.Fatal("Failed to map LFS settings: %v", err) + } + + LFS.ContentPath = sec.Key("LFS_CONTENT_PATH").MustString(filepath.Join(AppDataPath, "lfs")) + if !filepath.IsAbs(LFS.ContentPath) { + LFS.ContentPath = filepath.Join(AppWorkPath, LFS.ContentPath) + } + if LFS.LocksPagingNum == 0 { + LFS.LocksPagingNum = 50 + } + + LFS.HTTPAuthExpiry = sec.Key("LFS_HTTP_AUTH_EXPIRY").MustDuration(20 * time.Minute) + + if LFS.StartServer { + LFS.JWTSecretBytes = make([]byte, 32) + n, err := base64.RawURLEncoding.Decode(LFS.JWTSecretBytes, []byte(LFS.JWTSecretBase64)) + + if err != nil || n != 32 { + LFS.JWTSecretBase64, err = generate.NewJwtSecret() + if err != nil { + log.Fatal("Error generating JWT Secret for custom config: %v", err) + return + } + + // Save secret + cfg := ini.Empty() + if com.IsFile(CustomConf) { + // Keeps custom settings if there is already something. + if err := cfg.Append(CustomConf); err != nil { + log.Error("Failed to load custom conf '%s': %v", CustomConf, err) + } + } + + cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) + + if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil { + log.Fatal("Failed to create '%s': %v", CustomConf, err) + } + if err := cfg.SaveTo(CustomConf); err != nil { + log.Fatal("Error saving generated JWT Secret to custom config: %v", err) + return + } + } + } +} + +func ensureLFSDirectory() { + if LFS.StartServer { + if err := os.MkdirAll(LFS.ContentPath, 0700); err != nil { + log.Fatal("Failed to create '%s': %v", LFS.ContentPath, err) + } + } +} + +// CheckLFSVersion will check lfs version, if not satisfied, then disable it. +func CheckLFSVersion() { + if LFS.StartServer { + //Disable LFS client hooks if installed for the current OS user + //Needs at least git v2.1.2 + + err := git.LoadGitVersion() + if err != nil { + log.Fatal("Error retrieving git version: %v", err) + } + + if git.CheckGitVersionConstraint(">= 2.1.2") != nil { + LFS.StartServer = false + log.Error("LFS server support needs at least Git v2.1.2") + } else { + git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "filter.lfs.required=", + "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=") + } + } +} diff --git a/modules/setting/repository.go b/modules/setting/repository.go index eb1501d7b86eb..6604ae2276441 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -30,6 +30,7 @@ var ( ForcePrivate bool DefaultPrivate string MaxCreationLimit int + MaxPrivateCreationLimit int MirrorQueueLength int PullRequestQueueLength int PreferredLicenses []string @@ -132,6 +133,7 @@ var ( ForcePrivate: false, DefaultPrivate: RepoCreatingLastUserVisibility, MaxCreationLimit: -1, + MaxPrivateCreationLimit: -1, MirrorQueueLength: 1000, PullRequestQueueLength: 1000, PreferredLicenses: []string{"Apache License 2.0,MIT License"}, @@ -242,6 +244,7 @@ func newRepository() { Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool() Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool() Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1) + Repository.MaxPrivateCreationLimit = sec.Key("MAX_PRIVATE_CREATION_LIMIT").MustInt(-1) Repository.DefaultBranch = sec.Key("DEFAULT_BRANCH").MustString("master") RepoRootPath = sec.Key("ROOT").MustString(path.Join(homeDir, "gitea-repositories")) forcePathSeparator(RepoRootPath) diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 5b8aefdaa4caf..7d7eacba6f93b 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -23,12 +23,10 @@ import ( "time" "code.gitea.io/gitea/modules/generate" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/user" shellquote "github.com/kballard/go-shellquote" - version "github.com/mcuadros/go-version" "github.com/unknwon/com" ini "gopkg.in/ini.v1" "strk.kbt.io/projects/go/libravatar" @@ -134,16 +132,6 @@ var ( MinimumKeySizes: map[string]int{"ed25519": 256, "ecdsa": 256, "rsa": 2048, "dsa": 1024}, } - LFS struct { - StartServer bool `ini:"LFS_START_SERVER"` - ContentPath string `ini:"LFS_CONTENT_PATH"` - JWTSecretBase64 string `ini:"LFS_JWT_SECRET"` - JWTSecretBytes []byte `ini:"-"` - HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"` - MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"` - LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"` - } - // Security settings InstallLock bool SecretKey string @@ -158,6 +146,7 @@ var ( OnlyAllowPushIfGiteaEnvironmentSet bool PasswordComplexity []string PasswordHashAlgo string + PasswordCheckPwn bool // UI settings UI = struct { @@ -473,27 +462,6 @@ func createPIDFile(pidPath string) { } } -// CheckLFSVersion will check lfs version, if not satisfied, then disable it. -func CheckLFSVersion() { - if LFS.StartServer { - //Disable LFS client hooks if installed for the current OS user - //Needs at least git v2.1.2 - - binVersion, err := git.BinVersion() - if err != nil { - log.Fatal("Error retrieving git version: %v", err) - } - - if !version.Compare(binVersion, "2.1.2", ">=") { - LFS.StartServer = false - log.Error("LFS server support needs at least Git v2.1.2") - } else { - git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "filter.lfs.required=", - "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=") - } - } -} - // SetCustomPathAndConf will set CustomPath and CustomConf with reference to the // GITEA_CUSTOM environment variable and with provided overrides before stepping // back to the default @@ -723,51 +691,7 @@ func NewContext() { SSH.CreateAuthorizedKeysFile = sec.Key("SSH_CREATE_AUTHORIZED_KEYS_FILE").MustBool(true) SSH.ExposeAnonymous = sec.Key("SSH_EXPOSE_ANONYMOUS").MustBool(false) - sec = Cfg.Section("server") - if err = sec.MapTo(&LFS); err != nil { - log.Fatal("Failed to map LFS settings: %v", err) - } - LFS.ContentPath = sec.Key("LFS_CONTENT_PATH").MustString(filepath.Join(AppDataPath, "lfs")) - if !filepath.IsAbs(LFS.ContentPath) { - LFS.ContentPath = filepath.Join(AppWorkPath, LFS.ContentPath) - } - if LFS.LocksPagingNum == 0 { - LFS.LocksPagingNum = 50 - } - - LFS.HTTPAuthExpiry = sec.Key("LFS_HTTP_AUTH_EXPIRY").MustDuration(20 * time.Minute) - - if LFS.StartServer { - LFS.JWTSecretBytes = make([]byte, 32) - n, err := base64.RawURLEncoding.Decode(LFS.JWTSecretBytes, []byte(LFS.JWTSecretBase64)) - - if err != nil || n != 32 { - LFS.JWTSecretBase64, err = generate.NewJwtSecret() - if err != nil { - log.Fatal("Error generating JWT Secret for custom config: %v", err) - return - } - - // Save secret - cfg := ini.Empty() - if com.IsFile(CustomConf) { - // Keeps custom settings if there is already something. - if err := cfg.Append(CustomConf); err != nil { - log.Error("Failed to load custom conf '%s': %v", CustomConf, err) - } - } - - cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) - - if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil { - log.Fatal("Failed to create '%s': %v", CustomConf, err) - } - if err := cfg.SaveTo(CustomConf); err != nil { - log.Fatal("Error saving generated JWT Secret to custom config: %v", err) - return - } - } - } + newLFSService() if err = Cfg.Section("oauth2").MapTo(&OAuth2); err != nil { log.Fatal("Failed to OAuth2 settings: %v", err) @@ -821,6 +745,7 @@ func NewContext() { OnlyAllowPushIfGiteaEnvironmentSet = sec.Key("ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET").MustBool(true) PasswordHashAlgo = sec.Key("PASSWORD_HASH_ALGO").MustString("argon2") CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true) + PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false) InternalToken = loadInternalToken(sec) @@ -1087,14 +1012,6 @@ func loadOrGenerateInternalToken(sec *ini.Section) string { return token } -func ensureLFSDirectory() { - if LFS.StartServer { - if err := os.MkdirAll(LFS.ContentPath, 0700); err != nil { - log.Fatal("Failed to create '%s': %v", LFS.ContentPath, err) - } - } -} - // NewServices initializes the services func NewServices() { InitDBConfig() diff --git a/modules/storage/local.go b/modules/storage/local.go index 4c830211d96cd..a937a831f157d 100644 --- a/modules/storage/local.go +++ b/modules/storage/local.go @@ -34,7 +34,7 @@ func NewLocalStorage(bucket string) (*LocalStorage, error) { } // Open a file -func (l *LocalStorage) Open(path string) (io.ReadCloser, error) { +func (l *LocalStorage) Open(path string) (Object, error) { return os.Open(filepath.Join(l.dir, path)) } @@ -58,6 +58,11 @@ func (l *LocalStorage) Save(path string, r io.Reader) (int64, error) { return io.Copy(f, r) } +// Stat returns the info of the file +func (l *LocalStorage) Stat(path string) (ObjectInfo, error) { + return os.Stat(filepath.Join(l.dir, path)) +} + // Delete delete a file func (l *LocalStorage) Delete(path string) error { p := filepath.Join(l.dir, path) diff --git a/modules/storage/minio.go b/modules/storage/minio.go index 77d24e6b732de..30751755f3e31 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -8,6 +8,7 @@ import ( "context" "io" "net/url" + "os" "path" "strings" "time" @@ -62,7 +63,7 @@ func (m *MinioStorage) buildMinioPath(p string) string { } // Open open a file -func (m *MinioStorage) Open(path string) (io.ReadCloser, error) { +func (m *MinioStorage) Open(path string) (Object, error) { var opts = minio.GetObjectOptions{} object, err := m.client.GetObject(m.ctx, m.bucket, m.buildMinioPath(path), opts) if err != nil { @@ -87,6 +88,41 @@ func (m *MinioStorage) Save(path string, r io.Reader) (int64, error) { return uploadInfo.Size, nil } +type minioFileInfo struct { + minio.ObjectInfo +} + +func (m minioFileInfo) Name() string { + return m.ObjectInfo.Key +} + +func (m minioFileInfo) Size() int64 { + return m.ObjectInfo.Size +} + +func (m minioFileInfo) ModTime() time.Time { + return m.LastModified +} + +// Stat returns the stat information of the object +func (m *MinioStorage) Stat(path string) (ObjectInfo, error) { + info, err := m.client.StatObject( + m.ctx, + m.bucket, + m.buildMinioPath(path), + minio.StatObjectOptions{}, + ) + if err != nil { + if errResp, ok := err.(minio.ErrorResponse); ok { + if errResp.Code == "NoSuchKey" { + return nil, os.ErrNotExist + } + } + return nil, err + } + return &minioFileInfo{info}, nil +} + // Delete delete a file func (m *MinioStorage) Delete(path string) error { return m.client.RemoveObject(m.ctx, m.bucket, m.buildMinioPath(path), minio.RemoveObjectOptions{}) diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 8528ebc5cb31b..e355d2459f50b 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "net/url" + "time" "code.gitea.io/gitea/modules/setting" ) @@ -19,10 +20,24 @@ var ( ErrURLNotSupported = errors.New("url method not supported") ) +// Object represents the object on the storage +type Object interface { + io.ReadCloser + io.Seeker +} + +// ObjectInfo represents the object info on the storage +type ObjectInfo interface { + Name() string + Size() int64 + ModTime() time.Time +} + // ObjectStorage represents an object storage to handle a bucket and files type ObjectStorage interface { + Open(path string) (Object, error) Save(path string, r io.Reader) (int64, error) - Open(path string) (io.ReadCloser, error) + Stat(path string) (ObjectInfo, error) Delete(path string) error URL(path, name string) (*url.URL, error) } @@ -41,10 +56,21 @@ func Copy(dstStorage ObjectStorage, dstPath string, srcStorage ObjectStorage, sr var ( // Attachments represents attachments storage Attachments ObjectStorage + + // LFS represents lfs storage + LFS ObjectStorage ) // Init init the stoarge func Init() error { + if err := initAttachments(); err != nil { + return err + } + + return initLFS() +} + +func initAttachments() error { var err error switch setting.Attachment.StoreType { case "local": @@ -71,3 +97,31 @@ func Init() error { return nil } + +func initLFS() error { + var err error + switch setting.LFS.StoreType { + case "local": + LFS, err = NewLocalStorage(setting.LFS.ContentPath) + case "minio": + minio := setting.LFS.Minio + LFS, err = NewMinioStorage( + context.Background(), + minio.Endpoint, + minio.AccessKeyID, + minio.SecretAccessKey, + minio.Bucket, + minio.Location, + minio.BasePath, + minio.UseSSL, + ) + default: + return fmt.Errorf("Unsupported LFS store type: %s", setting.LFS.StoreType) + } + + if err != nil { + return err + } + + return nil +} diff --git a/modules/structs/issue_milestone.go b/modules/structs/issue_milestone.go index ec940c26049df..ace783ebbcfe6 100644 --- a/modules/structs/issue_milestone.go +++ b/modules/structs/issue_milestone.go @@ -17,6 +17,10 @@ type Milestone struct { OpenIssues int `json:"open_issues"` ClosedIssues int `json:"closed_issues"` // swagger:strfmt date-time + Created time.Time `json:"created_at"` + // swagger:strfmt date-time + Updated *time.Time `json:"updated_at"` + // swagger:strfmt date-time Closed *time.Time `json:"closed_at"` // swagger:strfmt date-time Deadline *time.Time `json:"due_on"` diff --git a/modules/structs/issue_stopwatch.go b/modules/structs/issue_stopwatch.go index 10510e36efbd7..8599e072731f5 100644 --- a/modules/structs/issue_stopwatch.go +++ b/modules/structs/issue_stopwatch.go @@ -11,8 +11,11 @@ import ( // StopWatch represent a running stopwatch type StopWatch struct { // swagger:strfmt date-time - Created time.Time `json:"created"` - IssueIndex int64 `json:"issue_index"` + Created time.Time `json:"created"` + IssueIndex int64 `json:"issue_index"` + IssueTitle string `json:"issue_title"` + RepoOwnerName string `json:"repo_owner_name"` + RepoName string `json:"repo_name"` } // StopWatches represent a list of stopwatches diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 808d2ffbc8ed0..f751c00789b7e 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -5,6 +5,7 @@ package structs import ( + "strings" "time" ) @@ -205,17 +206,7 @@ const ( // Name represents the service type's name // WARNNING: the name have to be equal to that on goth's library func (gt GitServiceType) Name() string { - switch gt { - case GithubService: - return "github" - case GiteaService: - return "gitea" - case GitlabService: - return "gitlab" - case GogsService: - return "gogs" - } - return "" + return strings.ToLower(gt.Title()) } // Title represents the service type's proper title diff --git a/modules/structs/settings.go b/modules/structs/settings.go index 4a6e0ce5a84b9..6874d6705bf40 100644 --- a/modules/structs/settings.go +++ b/modules/structs/settings.go @@ -14,3 +14,19 @@ type GeneralRepoSettings struct { type GeneralUISettings struct { AllowedReactions []string `json:"allowed_reactions"` } + +// GeneralAPISettings contains global api settings exposed by it +type GeneralAPISettings struct { + MaxResponseItems int `json:"max_response_items"` + DefaultPagingNum int `json:"default_paging_num"` + DefaultGitTreesPerPage int `json:"default_git_trees_per_page"` + DefaultMaxBlobSize int64 `json:"default_max_blob_size"` +} + +// GeneralAttachmentSettings contains global Attachment settings exposed by API +type GeneralAttachmentSettings struct { + Enabled bool `json:"enabled"` + AllowedTypes string `json:"allowed_types"` + MaxSize int64 `json:"max_size"` + MaxFiles int `json:"max_files"` +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index f86287f10bef9..9037af8991986 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -468,13 +468,23 @@ func NewTextFuncMap() []texttmpl.FuncMap { var widthRe = regexp.MustCompile(`width="[0-9]+?"`) var heightRe = regexp.MustCompile(`height="[0-9]+?"`) -// SVG render icons -func SVG(icon string, size int) template.HTML { +// SVG render icons - arguments icon name (string), size (int), class (string) +func SVG(icon string, others ...interface{}) template.HTML { + var size = others[0].(int) + + class := "" + if len(others) > 1 && others[1].(string) != "" { + class = others[1].(string) + } + if svgStr, ok := svg.SVGs[icon]; ok { if size != 16 { svgStr = widthRe.ReplaceAllString(svgStr, fmt.Sprintf(`width="%d"`, size)) svgStr = heightRe.ReplaceAllString(svgStr, fmt.Sprintf(`height="%d"`, size)) } + if class != "" { + svgStr = strings.Replace(svgStr, `class="`, fmt.Sprintf(`class="%s `, class), 1) + } return template.HTML(svgStr) } return template.HTML("") @@ -684,7 +694,7 @@ func ActionContent2Commits(act Actioner) *repository.PushCommits { // DiffTypeToStr returns diff type name func DiffTypeToStr(diffType int) string { diffTypes := map[int]string{ - 1: "add", 2: "modify", 3: "del", 4: "rename", + 1: "add", 2: "modify", 3: "del", 4: "rename", 5: "copy", } return diffTypes[diffType] } diff --git a/modules/webhook/dingtalk.go b/modules/webhook/dingtalk.go index 4e0e52451abb1..a9032db046fea 100644 --- a/modules/webhook/dingtalk.go +++ b/modules/webhook/dingtalk.go @@ -21,19 +21,24 @@ type ( DingtalkPayload dingtalk.Payload ) +var ( + _ PayloadConvertor = &DingtalkPayload{} +) + // SetSecret sets the dingtalk secret -func (p *DingtalkPayload) SetSecret(_ string) {} +func (d *DingtalkPayload) SetSecret(_ string) {} // JSONPayload Marshals the DingtalkPayload to json -func (p *DingtalkPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(p, "", " ") +func (d *DingtalkPayload) JSONPayload() ([]byte, error) { + data, err := json.MarshalIndent(d, "", " ") if err != nil { return []byte{}, err } return data, nil } -func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) { +// Create implements PayloadConvertor Create method +func (d *DingtalkPayload) Create(p *api.CreatePayload) (api.Payloader, error) { // created tag/branch refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -50,7 +55,8 @@ func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) { }, nil } -func getDingtalkDeletePayload(p *api.DeletePayload) (*DingtalkPayload, error) { +// Delete implements PayloadConvertor Delete method +func (d *DingtalkPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { // created tag/branch refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -67,7 +73,8 @@ func getDingtalkDeletePayload(p *api.DeletePayload) (*DingtalkPayload, error) { }, nil } -func getDingtalkForkPayload(p *api.ForkPayload) (*DingtalkPayload, error) { +// Fork implements PayloadConvertor Fork method +func (d *DingtalkPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return &DingtalkPayload{ @@ -82,7 +89,8 @@ func getDingtalkForkPayload(p *api.ForkPayload) (*DingtalkPayload, error) { }, nil } -func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) { +// Push implements PayloadConvertor Push method +func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) { var ( branchName = git.RefEndName(p.Ref) commitDesc string @@ -131,7 +139,8 @@ func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) { }, nil } -func getDingtalkIssuesPayload(p *api.IssuePayload) (*DingtalkPayload, error) { +// Issue implements PayloadConvertor Issue method +func (d *DingtalkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true) return &DingtalkPayload{ @@ -147,7 +156,8 @@ func getDingtalkIssuesPayload(p *api.IssuePayload) (*DingtalkPayload, error) { }, nil } -func getDingtalkIssueCommentPayload(p *api.IssueCommentPayload) (*DingtalkPayload, error) { +// IssueComment implements PayloadConvertor IssueComment method +func (d *DingtalkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true) return &DingtalkPayload{ @@ -162,7 +172,8 @@ func getDingtalkIssueCommentPayload(p *api.IssueCommentPayload) (*DingtalkPayloa }, nil } -func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, error) { +// PullRequest implements PayloadConvertor PullRequest method +func (d *DingtalkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true) return &DingtalkPayload{ @@ -178,7 +189,8 @@ func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, }, nil } -func getDingtalkPullRequestApprovalPayload(p *api.PullRequestPayload, event models.HookEventType) (*DingtalkPayload, error) { +// Review implements PayloadConvertor Review method +func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event models.HookEventType) (api.Payloader, error) { var text, title string switch p.Action { case api.HookIssueReviewed: @@ -204,7 +216,8 @@ func getDingtalkPullRequestApprovalPayload(p *api.PullRequestPayload, event mode }, nil } -func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, error) { +// Repository implements PayloadConvertor Repository method +func (d *DingtalkPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { var title, url string switch p.Action { case api.HookRepoCreated: @@ -235,7 +248,8 @@ func getDingtalkRepositoryPayload(p *api.RepositoryPayload) (*DingtalkPayload, e return nil, nil } -func getDingtalkReleasePayload(p *api.ReleasePayload) (*DingtalkPayload, error) { +// Release implements PayloadConvertor Release method +func (d *DingtalkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true) return &DingtalkPayload{ @@ -251,36 +265,6 @@ func getDingtalkReleasePayload(p *api.ReleasePayload) (*DingtalkPayload, error) } // GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload -func GetDingtalkPayload(p api.Payloader, event models.HookEventType, meta string) (*DingtalkPayload, error) { - s := new(DingtalkPayload) - - switch event { - case models.HookEventCreate: - return getDingtalkCreatePayload(p.(*api.CreatePayload)) - case models.HookEventDelete: - return getDingtalkDeletePayload(p.(*api.DeletePayload)) - case models.HookEventFork: - return getDingtalkForkPayload(p.(*api.ForkPayload)) - case models.HookEventIssues, models.HookEventIssueAssign, models.HookEventIssueLabel, models.HookEventIssueMilestone: - return getDingtalkIssuesPayload(p.(*api.IssuePayload)) - case models.HookEventIssueComment, models.HookEventPullRequestComment: - pl, ok := p.(*api.IssueCommentPayload) - if ok { - return getDingtalkIssueCommentPayload(pl) - } - return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload)) - case models.HookEventPush: - return getDingtalkPushPayload(p.(*api.PushPayload)) - case models.HookEventPullRequest, models.HookEventPullRequestAssign, models.HookEventPullRequestLabel, - models.HookEventPullRequestMilestone, models.HookEventPullRequestSync: - return getDingtalkPullRequestPayload(p.(*api.PullRequestPayload)) - case models.HookEventPullRequestReviewApproved, models.HookEventPullRequestReviewRejected, models.HookEventPullRequestReviewComment: - return getDingtalkPullRequestApprovalPayload(p.(*api.PullRequestPayload), event) - case models.HookEventRepository: - return getDingtalkRepositoryPayload(p.(*api.RepositoryPayload)) - case models.HookEventRelease: - return getDingtalkReleasePayload(p.(*api.ReleasePayload)) - } - - return s, nil +func GetDingtalkPayload(p api.Payloader, event models.HookEventType, meta string) (api.Payloader, error) { + return convertPayloader(new(DingtalkPayload), p, event) } diff --git a/modules/webhook/dingtalk_test.go b/modules/webhook/dingtalk_test.go index 4cb7a913e6a84..e5aa0fca36abe 100644 --- a/modules/webhook/dingtalk_test.go +++ b/modules/webhook/dingtalk_test.go @@ -14,18 +14,18 @@ import ( func TestGetDingTalkIssuesPayload(t *testing.T) { p := issueTestPayload() - + d := new(DingtalkPayload) p.Action = api.HookIssueOpened - pl, err := getDingtalkIssuesPayload(p) + pl, err := d.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "#2 crash", pl.ActionCard.Title) - assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\n", pl.ActionCard.Text) + assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title) + assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1\r\n\r\n", pl.(*DingtalkPayload).ActionCard.Text) p.Action = api.HookIssueClosed - pl, err = getDingtalkIssuesPayload(p) + pl, err = d.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "#2 crash", pl.ActionCard.Title) - assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1\r\n\r\n", pl.ActionCard.Text) + assert.Equal(t, "#2 crash", pl.(*DingtalkPayload).ActionCard.Title) + assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1\r\n\r\n", pl.(*DingtalkPayload).ActionCard.Text) } diff --git a/modules/webhook/discord.go b/modules/webhook/discord.go index 761129d8d9e72..530e7adbda4de 100644 --- a/modules/webhook/discord.go +++ b/modules/webhook/discord.go @@ -97,25 +97,30 @@ var ( ) // SetSecret sets the discord secret -func (p *DiscordPayload) SetSecret(_ string) {} +func (d *DiscordPayload) SetSecret(_ string) {} // JSONPayload Marshals the DiscordPayload to json -func (p *DiscordPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(p, "", " ") +func (d *DiscordPayload) JSONPayload() ([]byte, error) { + data, err := json.MarshalIndent(d, "", " ") if err != nil { return []byte{}, err } return data, nil } -func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordPayload, error) { +var ( + _ PayloadConvertor = &DiscordPayload{} +) + +// Create implements PayloadConvertor Create method +func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) { // created tag/branch refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, + Username: d.Username, + AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ { Title: title, @@ -131,14 +136,15 @@ func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordP }, nil } -func getDiscordDeletePayload(p *api.DeletePayload, meta *DiscordMeta) (*DiscordPayload, error) { +// Delete implements PayloadConvertor Delete method +func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { // deleted tag/branch refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, + Username: d.Username, + AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ { Title: title, @@ -154,13 +160,13 @@ func getDiscordDeletePayload(p *api.DeletePayload, meta *DiscordMeta) (*DiscordP }, nil } -func getDiscordForkPayload(p *api.ForkPayload, meta *DiscordMeta) (*DiscordPayload, error) { - // fork +// Fork implements PayloadConvertor Fork method +func (d *DiscordPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, + Username: d.Username, + AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ { Title: title, @@ -176,7 +182,8 @@ func getDiscordForkPayload(p *api.ForkPayload, meta *DiscordMeta) (*DiscordPaylo }, nil } -func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPayload, error) { +// Push implements PayloadConvertor Push method +func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) { var ( branchName = git.RefEndName(p.Ref) commitDesc string @@ -208,8 +215,8 @@ func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPaylo } return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, + Username: d.Username, + AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ { Title: title, @@ -226,12 +233,13 @@ func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPaylo }, nil } -func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPayload, error) { +// Issue implements PayloadConvertor Issue method +func (d *DiscordPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { text, _, attachmentText, color := getIssuesPayloadInfo(p, noneLinkFormatter, false) return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, + Username: d.Username, + AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ { Title: text, @@ -248,12 +256,13 @@ func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPa }, nil } -func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, discord *DiscordMeta) (*DiscordPayload, error) { +// IssueComment implements PayloadConvertor IssueComment method +func (d *DiscordPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { text, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false) return &DiscordPayload{ - Username: discord.Username, - AvatarURL: discord.IconURL, + Username: d.Username, + AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ { Title: text, @@ -270,12 +279,13 @@ func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, discord *DiscordM }, nil } -func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) (*DiscordPayload, error) { +// PullRequest implements PayloadConvertor PullRequest method +func (d *DiscordPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { text, _, attachmentText, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false) return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, + Username: d.Username, + AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ { Title: text, @@ -292,7 +302,8 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) }, nil } -func getDiscordPullRequestApprovalPayload(p *api.PullRequestPayload, meta *DiscordMeta, event models.HookEventType) (*DiscordPayload, error) { +// Review implements PayloadConvertor Review method +func (d *DiscordPayload) Review(p *api.PullRequestPayload, event models.HookEventType) (api.Payloader, error) { var text, title string var color int switch p.Action { @@ -318,8 +329,8 @@ func getDiscordPullRequestApprovalPayload(p *api.PullRequestPayload, meta *Disco } return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, + Username: d.Username, + AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ { Title: title, @@ -336,7 +347,8 @@ func getDiscordPullRequestApprovalPayload(p *api.PullRequestPayload, meta *Disco }, nil } -func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*DiscordPayload, error) { +// Repository implements PayloadConvertor Repository method +func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { var title, url string var color int switch p.Action { @@ -350,8 +362,8 @@ func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (* } return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, + Username: d.Username, + AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ { Title: title, @@ -367,12 +379,13 @@ func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (* }, nil } -func getDiscordReleasePayload(p *api.ReleasePayload, meta *DiscordMeta) (*DiscordPayload, error) { +// Release implements PayloadConvertor Release method +func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { text, color := getReleasePayloadInfo(p, noneLinkFormatter, false) return &DiscordPayload{ - Username: meta.Username, - AvatarURL: meta.IconURL, + Username: d.Username, + AvatarURL: d.AvatarURL, Embeds: []DiscordEmbed{ { Title: text, @@ -390,47 +403,20 @@ func getDiscordReleasePayload(p *api.ReleasePayload, meta *DiscordMeta) (*Discor } // GetDiscordPayload converts a discord webhook into a DiscordPayload -func GetDiscordPayload(p api.Payloader, event models.HookEventType, meta string) (*DiscordPayload, error) { +func GetDiscordPayload(p api.Payloader, event models.HookEventType, meta string) (api.Payloader, error) { s := new(DiscordPayload) discord := &DiscordMeta{} if err := json.Unmarshal([]byte(meta), &discord); err != nil { return s, errors.New("GetDiscordPayload meta json:" + err.Error()) } + s.Username = discord.Username + s.AvatarURL = discord.IconURL - switch event { - case models.HookEventCreate: - return getDiscordCreatePayload(p.(*api.CreatePayload), discord) - case models.HookEventDelete: - return getDiscordDeletePayload(p.(*api.DeletePayload), discord) - case models.HookEventFork: - return getDiscordForkPayload(p.(*api.ForkPayload), discord) - case models.HookEventIssues, models.HookEventIssueAssign, models.HookEventIssueLabel, models.HookEventIssueMilestone: - return getDiscordIssuesPayload(p.(*api.IssuePayload), discord) - case models.HookEventIssueComment, models.HookEventPullRequestComment: - pl, ok := p.(*api.IssueCommentPayload) - if ok { - return getDiscordIssueCommentPayload(pl, discord) - } - return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord) - case models.HookEventPush: - return getDiscordPushPayload(p.(*api.PushPayload), discord) - case models.HookEventPullRequest, models.HookEventPullRequestAssign, models.HookEventPullRequestLabel, - models.HookEventPullRequestMilestone, models.HookEventPullRequestSync: - return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord) - case models.HookEventPullRequestReviewRejected, models.HookEventPullRequestReviewApproved, models.HookEventPullRequestReviewComment: - return getDiscordPullRequestApprovalPayload(p.(*api.PullRequestPayload), discord, event) - case models.HookEventRepository: - return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord) - case models.HookEventRelease: - return getDiscordReleasePayload(p.(*api.ReleasePayload), discord) - } - - return s, nil + return convertPayloader(s, p, event) } func parseHookPullRequestEventType(event models.HookEventType) (string, error) { - switch event { case models.HookEventPullRequestReviewApproved: diff --git a/modules/webhook/feishu.go b/modules/webhook/feishu.go index 4beda9014c54d..8e60dbba1359b 100644 --- a/modules/webhook/feishu.go +++ b/modules/webhook/feishu.go @@ -23,18 +23,23 @@ type ( ) // SetSecret sets the Feishu secret -func (p *FeishuPayload) SetSecret(_ string) {} +func (f *FeishuPayload) SetSecret(_ string) {} // JSONPayload Marshals the FeishuPayload to json -func (p *FeishuPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(p, "", " ") +func (f *FeishuPayload) JSONPayload() ([]byte, error) { + data, err := json.MarshalIndent(f, "", " ") if err != nil { return []byte{}, err } return data, nil } -func getFeishuCreatePayload(p *api.CreatePayload) (*FeishuPayload, error) { +var ( + _ PayloadConvertor = &FeishuPayload{} +) + +// Create implements PayloadConvertor Create method +func (f *FeishuPayload) Create(p *api.CreatePayload) (api.Payloader, error) { // created tag/branch refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -45,7 +50,8 @@ func getFeishuCreatePayload(p *api.CreatePayload) (*FeishuPayload, error) { }, nil } -func getFeishuDeletePayload(p *api.DeletePayload) (*FeishuPayload, error) { +// Delete implements PayloadConvertor Delete method +func (f *FeishuPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { // created tag/branch refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -56,7 +62,8 @@ func getFeishuDeletePayload(p *api.DeletePayload) (*FeishuPayload, error) { }, nil } -func getFeishuForkPayload(p *api.ForkPayload) (*FeishuPayload, error) { +// Fork implements PayloadConvertor Fork method +func (f *FeishuPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return &FeishuPayload{ @@ -65,7 +72,8 @@ func getFeishuForkPayload(p *api.ForkPayload) (*FeishuPayload, error) { }, nil } -func getFeishuPushPayload(p *api.PushPayload) (*FeishuPayload, error) { +// Push implements PayloadConvertor Push method +func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) { var ( branchName = git.RefEndName(p.Ref) commitDesc string @@ -94,7 +102,8 @@ func getFeishuPushPayload(p *api.PushPayload) (*FeishuPayload, error) { }, nil } -func getFeishuIssuesPayload(p *api.IssuePayload) (*FeishuPayload, error) { +// Issue implements PayloadConvertor Issue method +func (f *FeishuPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true) return &FeishuPayload{ @@ -103,7 +112,8 @@ func getFeishuIssuesPayload(p *api.IssuePayload) (*FeishuPayload, error) { }, nil } -func getFeishuIssueCommentPayload(p *api.IssueCommentPayload) (*FeishuPayload, error) { +// IssueComment implements PayloadConvertor IssueComment method +func (f *FeishuPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true) return &FeishuPayload{ @@ -112,7 +122,8 @@ func getFeishuIssueCommentPayload(p *api.IssueCommentPayload) (*FeishuPayload, e }, nil } -func getFeishuPullRequestPayload(p *api.PullRequestPayload) (*FeishuPayload, error) { +// PullRequest implements PayloadConvertor PullRequest method +func (f *FeishuPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true) return &FeishuPayload{ @@ -121,7 +132,8 @@ func getFeishuPullRequestPayload(p *api.PullRequestPayload) (*FeishuPayload, err }, nil } -func getFeishuPullRequestApprovalPayload(p *api.PullRequestPayload, event models.HookEventType) (*FeishuPayload, error) { +// Review implements PayloadConvertor Review method +func (f *FeishuPayload) Review(p *api.PullRequestPayload, event models.HookEventType) (api.Payloader, error) { var text, title string switch p.Action { case api.HookIssueSynchronized: @@ -141,7 +153,8 @@ func getFeishuPullRequestApprovalPayload(p *api.PullRequestPayload, event models }, nil } -func getFeishuRepositoryPayload(p *api.RepositoryPayload) (*FeishuPayload, error) { +// Repository implements PayloadConvertor Repository method +func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { var title string switch p.Action { case api.HookRepoCreated: @@ -161,7 +174,8 @@ func getFeishuRepositoryPayload(p *api.RepositoryPayload) (*FeishuPayload, error return nil, nil } -func getFeishuReleasePayload(p *api.ReleasePayload) (*FeishuPayload, error) { +// Release implements PayloadConvertor Release method +func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true) return &FeishuPayload{ @@ -171,35 +185,6 @@ func getFeishuReleasePayload(p *api.ReleasePayload) (*FeishuPayload, error) { } // GetFeishuPayload converts a ding talk webhook into a FeishuPayload -func GetFeishuPayload(p api.Payloader, event models.HookEventType, meta string) (*FeishuPayload, error) { - s := new(FeishuPayload) - - switch event { - case models.HookEventCreate: - return getFeishuCreatePayload(p.(*api.CreatePayload)) - case models.HookEventDelete: - return getFeishuDeletePayload(p.(*api.DeletePayload)) - case models.HookEventFork: - return getFeishuForkPayload(p.(*api.ForkPayload)) - case models.HookEventIssues: - return getFeishuIssuesPayload(p.(*api.IssuePayload)) - case models.HookEventIssueComment, models.HookEventPullRequestComment: - pl, ok := p.(*api.IssueCommentPayload) - if ok { - return getFeishuIssueCommentPayload(pl) - } - return getFeishuPullRequestPayload(p.(*api.PullRequestPayload)) - case models.HookEventPush: - return getFeishuPushPayload(p.(*api.PushPayload)) - case models.HookEventPullRequest: - return getFeishuPullRequestPayload(p.(*api.PullRequestPayload)) - case models.HookEventPullRequestReviewApproved, models.HookEventPullRequestReviewRejected: - return getFeishuPullRequestApprovalPayload(p.(*api.PullRequestPayload), event) - case models.HookEventRepository: - return getFeishuRepositoryPayload(p.(*api.RepositoryPayload)) - case models.HookEventRelease: - return getFeishuReleasePayload(p.(*api.ReleasePayload)) - } - - return s, nil +func GetFeishuPayload(p api.Payloader, event models.HookEventType, meta string) (api.Payloader, error) { + return convertPayloader(new(FeishuPayload), p, event) } diff --git a/modules/webhook/matrix.go b/modules/webhook/matrix.go index d6309000a863b..063147198ae62 100644 --- a/modules/webhook/matrix.go +++ b/modules/webhook/matrix.go @@ -51,14 +51,18 @@ type MatrixPayloadUnsafe struct { AccessToken string `json:"access_token"` } +var ( + _ PayloadConvertor = &MatrixPayloadUnsafe{} +) + // safePayload "converts" a unsafe payload to a safe payload -func (p *MatrixPayloadUnsafe) safePayload() *MatrixPayloadSafe { +func (m *MatrixPayloadUnsafe) safePayload() *MatrixPayloadSafe { return &MatrixPayloadSafe{ - Body: p.Body, - MsgType: p.MsgType, - Format: p.Format, - FormattedBody: p.FormattedBody, - Commits: p.Commits, + Body: m.Body, + MsgType: m.MsgType, + Format: m.Format, + FormattedBody: m.FormattedBody, + Commits: m.Commits, } } @@ -72,11 +76,11 @@ type MatrixPayloadSafe struct { } // SetSecret sets the Matrix secret -func (p *MatrixPayloadUnsafe) SetSecret(_ string) {} +func (m *MatrixPayloadUnsafe) SetSecret(_ string) {} // JSONPayload Marshals the MatrixPayloadUnsafe to json -func (p *MatrixPayloadUnsafe) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(p, "", " ") +func (m *MatrixPayloadUnsafe) JSONPayload() ([]byte, error) { + data, err := json.MarshalIndent(m, "", " ") if err != nil { return []byte{}, err } @@ -101,51 +105,56 @@ func MatrixLinkToRef(repoURL, ref string) string { } } -func getMatrixCreatePayload(p *api.CreatePayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) { +// Create implements PayloadConvertor Create method +func (m *MatrixPayloadUnsafe) Create(p *api.CreatePayload) (api.Payloader, error) { repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref) text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName) - return getMatrixPayloadUnsafe(text, nil, matrix), nil + return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil } -// getMatrixDeletePayload composes Matrix payload for delete a branch or tag. -func getMatrixDeletePayload(p *api.DeletePayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) { +// Delete composes Matrix payload for delete a branch or tag. +func (m *MatrixPayloadUnsafe) Delete(p *api.DeletePayload) (api.Payloader, error) { refName := git.RefEndName(p.Ref) repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName) - return getMatrixPayloadUnsafe(text, nil, matrix), nil + return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil } -// getMatrixForkPayload composes Matrix payload for forked by a repository. -func getMatrixForkPayload(p *api.ForkPayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) { +// Fork composes Matrix payload for forked by a repository. +func (m *MatrixPayloadUnsafe) Fork(p *api.ForkPayload) (api.Payloader, error) { baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink) - return getMatrixPayloadUnsafe(text, nil, matrix), nil + return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil } -func getMatrixIssuesPayload(p *api.IssuePayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) { +// Issue implements PayloadConvertor Issue method +func (m *MatrixPayloadUnsafe) Issue(p *api.IssuePayload) (api.Payloader, error) { text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true) - return getMatrixPayloadUnsafe(text, nil, matrix), nil + return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil } -func getMatrixIssueCommentPayload(p *api.IssueCommentPayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) { +// IssueComment implements PayloadConvertor IssueComment method +func (m *MatrixPayloadUnsafe) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true) - return getMatrixPayloadUnsafe(text, nil, matrix), nil + return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil } -func getMatrixReleasePayload(p *api.ReleasePayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) { +// Release implements PayloadConvertor Release method +func (m *MatrixPayloadUnsafe) Release(p *api.ReleasePayload) (api.Payloader, error) { text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true) - return getMatrixPayloadUnsafe(text, nil, matrix), nil + return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil } -func getMatrixPushPayload(p *api.PushPayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) { +// Push implements PayloadConvertor Push method +func (m *MatrixPayloadUnsafe) Push(p *api.PushPayload) (api.Payloader, error) { var commitDesc string if len(p.Commits) == 1 { @@ -168,16 +177,18 @@ func getMatrixPushPayload(p *api.PushPayload, matrix *MatrixMeta) (*MatrixPayloa } - return getMatrixPayloadUnsafe(text, p.Commits, matrix), nil + return getMatrixPayloadUnsafe(text, p.Commits, m.AccessToken, m.MsgType), nil } -func getMatrixPullRequestPayload(p *api.PullRequestPayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) { +// PullRequest implements PayloadConvertor PullRequest method +func (m *MatrixPayloadUnsafe) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true) - return getMatrixPayloadUnsafe(text, nil, matrix), nil + return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil } -func getMatrixPullRequestApprovalPayload(p *api.PullRequestPayload, matrix *MatrixMeta, event models.HookEventType) (*MatrixPayloadUnsafe, error) { +// Review implements PayloadConvertor Review method +func (m *MatrixPayloadUnsafe) Review(p *api.PullRequestPayload, event models.HookEventType) (api.Payloader, error) { senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index) @@ -194,10 +205,11 @@ func getMatrixPullRequestApprovalPayload(p *api.PullRequestPayload, matrix *Matr text = fmt.Sprintf("[%s] Pull request review %s: [%s](%s) by %s", repoLink, action, title, titleLink, senderLink) } - return getMatrixPayloadUnsafe(text, nil, matrix), nil + return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil } -func getMatrixRepositoryPayload(p *api.RepositoryPayload, matrix *MatrixMeta) (*MatrixPayloadUnsafe, error) { +// Repository implements PayloadConvertor Repository method +func (m *MatrixPayloadUnsafe) Repository(p *api.RepositoryPayload) (api.Payloader, error) { senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) var text string @@ -209,11 +221,11 @@ func getMatrixRepositoryPayload(p *api.RepositoryPayload, matrix *MatrixMeta) (* text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink) } - return getMatrixPayloadUnsafe(text, nil, matrix), nil + return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil } // GetMatrixPayload converts a Matrix webhook into a MatrixPayloadUnsafe -func GetMatrixPayload(p api.Payloader, event models.HookEventType, meta string) (*MatrixPayloadUnsafe, error) { +func GetMatrixPayload(p api.Payloader, event models.HookEventType, meta string) (api.Payloader, error) { s := new(MatrixPayloadUnsafe) matrix := &MatrixMeta{} @@ -221,44 +233,19 @@ func GetMatrixPayload(p api.Payloader, event models.HookEventType, meta string) return s, errors.New("GetMatrixPayload meta json:" + err.Error()) } - switch event { - case models.HookEventCreate: - return getMatrixCreatePayload(p.(*api.CreatePayload), matrix) - case models.HookEventDelete: - return getMatrixDeletePayload(p.(*api.DeletePayload), matrix) - case models.HookEventFork: - return getMatrixForkPayload(p.(*api.ForkPayload), matrix) - case models.HookEventIssues, models.HookEventIssueAssign, models.HookEventIssueLabel, models.HookEventIssueMilestone: - return getMatrixIssuesPayload(p.(*api.IssuePayload), matrix) - case models.HookEventIssueComment, models.HookEventPullRequestComment: - pl, ok := p.(*api.IssueCommentPayload) - if ok { - return getMatrixIssueCommentPayload(pl, matrix) - } - return getMatrixPullRequestPayload(p.(*api.PullRequestPayload), matrix) - case models.HookEventPush: - return getMatrixPushPayload(p.(*api.PushPayload), matrix) - case models.HookEventPullRequest, models.HookEventPullRequestAssign, models.HookEventPullRequestLabel, - models.HookEventPullRequestMilestone, models.HookEventPullRequestSync: - return getMatrixPullRequestPayload(p.(*api.PullRequestPayload), matrix) - case models.HookEventPullRequestReviewRejected, models.HookEventPullRequestReviewApproved, models.HookEventPullRequestReviewComment: - return getMatrixPullRequestApprovalPayload(p.(*api.PullRequestPayload), matrix, event) - case models.HookEventRepository: - return getMatrixRepositoryPayload(p.(*api.RepositoryPayload), matrix) - case models.HookEventRelease: - return getMatrixReleasePayload(p.(*api.ReleasePayload), matrix) - } + s.AccessToken = matrix.AccessToken + s.MsgType = messageTypeText[matrix.MessageType] - return s, nil + return convertPayloader(s, p, event) } -func getMatrixPayloadUnsafe(text string, commits []*api.PayloadCommit, matrix *MatrixMeta) *MatrixPayloadUnsafe { +func getMatrixPayloadUnsafe(text string, commits []*api.PayloadCommit, accessToken, msgType string) *MatrixPayloadUnsafe { p := MatrixPayloadUnsafe{} - p.AccessToken = matrix.AccessToken + p.AccessToken = accessToken p.FormattedBody = text p.Body = getMessageBody(text) p.Format = "org.matrix.custom.html" - p.MsgType = messageTypeText[matrix.MessageType] + p.MsgType = msgType p.Commits = commits return &p } diff --git a/modules/webhook/matrix_test.go b/modules/webhook/matrix_test.go index 3d1c660126c39..771146f2f30fe 100644 --- a/modules/webhook/matrix_test.go +++ b/modules/webhook/matrix_test.go @@ -16,73 +16,69 @@ import ( func TestMatrixIssuesPayloadOpened(t *testing.T) { p := issueTestPayload() - sl := &MatrixMeta{} + m := new(MatrixPayloadUnsafe) p.Action = api.HookIssueOpened - pl, err := getMatrixIssuesPayload(p, sl) + pl, err := m.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) - assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1", pl.FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) + assert.Equal(t, "[test/repo] Issue opened: #2 crash by user1", pl.(*MatrixPayloadUnsafe).FormattedBody) p.Action = api.HookIssueClosed - pl, err = getMatrixIssuesPayload(p, sl) + pl, err = m.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) - assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) + assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1", pl.(*MatrixPayloadUnsafe).FormattedBody) } func TestMatrixIssueCommentPayload(t *testing.T) { p := issueCommentTestPayload() + m := new(MatrixPayloadUnsafe) - sl := &MatrixMeta{} - - pl, err := getMatrixIssueCommentPayload(p, sl) + pl, err := m.IssueComment(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.Body) - assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1", pl.FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) + assert.Equal(t, "[test/repo] New comment on issue #2 crash by user1", pl.(*MatrixPayloadUnsafe).FormattedBody) } func TestMatrixPullRequestCommentPayload(t *testing.T) { p := pullRequestCommentTestPayload() + m := new(MatrixPayloadUnsafe) - sl := &MatrixMeta{} - - pl, err := getMatrixIssueCommentPayload(p, sl) + pl, err := m.IssueComment(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#2 Fix bug](http://localhost:3000/test/repo/pulls/2) by [user1](https://try.gitea.io/user1)", pl.Body) - assert.Equal(t, "[test/repo] New comment on pull request #2 Fix bug by user1", pl.FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#2 Fix bug](http://localhost:3000/test/repo/pulls/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) + assert.Equal(t, "[test/repo] New comment on pull request #2 Fix bug by user1", pl.(*MatrixPayloadUnsafe).FormattedBody) } func TestMatrixReleasePayload(t *testing.T) { p := pullReleaseTestPayload() + m := new(MatrixPayloadUnsafe) - sl := &MatrixMeta{} - - pl, err := getMatrixReleasePayload(p, sl) + pl, err := m.Release(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/src/v1.0) by [user1](https://try.gitea.io/user1)", pl.Body) - assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/src/v1.0) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) + assert.Equal(t, "[test/repo] Release created: v1.0 by user1", pl.(*MatrixPayloadUnsafe).FormattedBody) } func TestMatrixPullRequestPayload(t *testing.T) { p := pullRequestTestPayload() + m := new(MatrixPayloadUnsafe) - sl := &MatrixMeta{} - - pl, err := getMatrixPullRequestPayload(p, sl) + pl, err := m.PullRequest(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#2 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.Body) - assert.Equal(t, "[test/repo] Pull request opened: #2 Fix bug by user1", pl.FormattedBody) + assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#2 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body) + assert.Equal(t, "[test/repo] Pull request opened: #2 Fix bug by user1", pl.(*MatrixPayloadUnsafe).FormattedBody) } func TestMatrixHookRequest(t *testing.T) { diff --git a/modules/webhook/msteams.go b/modules/webhook/msteams.go index e7ec396f2941c..80998d9f81e49 100644 --- a/modules/webhook/msteams.go +++ b/modules/webhook/msteams.go @@ -56,18 +56,23 @@ type ( ) // SetSecret sets the MSTeams secret -func (p *MSTeamsPayload) SetSecret(_ string) {} +func (m *MSTeamsPayload) SetSecret(_ string) {} // JSONPayload Marshals the MSTeamsPayload to json -func (p *MSTeamsPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(p, "", " ") +func (m *MSTeamsPayload) JSONPayload() ([]byte, error) { + data, err := json.MarshalIndent(m, "", " ") if err != nil { return []byte{}, err } return data, nil } -func getMSTeamsCreatePayload(p *api.CreatePayload) (*MSTeamsPayload, error) { +var ( + _ PayloadConvertor = &MSTeamsPayload{} +) + +// Create implements PayloadConvertor Create method +func (m *MSTeamsPayload) Create(p *api.CreatePayload) (api.Payloader, error) { // created tag/branch refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName) @@ -110,7 +115,8 @@ func getMSTeamsCreatePayload(p *api.CreatePayload) (*MSTeamsPayload, error) { }, nil } -func getMSTeamsDeletePayload(p *api.DeletePayload) (*MSTeamsPayload, error) { +// Delete implements PayloadConvertor Delete method +func (m *MSTeamsPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { // deleted tag/branch refName := git.RefEndName(p.Ref) title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName) @@ -153,8 +159,8 @@ func getMSTeamsDeletePayload(p *api.DeletePayload) (*MSTeamsPayload, error) { }, nil } -func getMSTeamsForkPayload(p *api.ForkPayload) (*MSTeamsPayload, error) { - // fork +// Fork implements PayloadConvertor Fork method +func (m *MSTeamsPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName) return &MSTeamsPayload{ @@ -195,7 +201,8 @@ func getMSTeamsForkPayload(p *api.ForkPayload) (*MSTeamsPayload, error) { }, nil } -func getMSTeamsPushPayload(p *api.PushPayload) (*MSTeamsPayload, error) { +// Push implements PayloadConvertor Push method +func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) { var ( branchName = git.RefEndName(p.Ref) commitDesc string @@ -265,7 +272,8 @@ func getMSTeamsPushPayload(p *api.PushPayload) (*MSTeamsPayload, error) { }, nil } -func getMSTeamsIssuesPayload(p *api.IssuePayload) (*MSTeamsPayload, error) { +// Issue implements PayloadConvertor Issue method +func (m *MSTeamsPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { text, _, attachmentText, color := getIssuesPayloadInfo(p, noneLinkFormatter, false) return &MSTeamsPayload{ @@ -307,7 +315,8 @@ func getMSTeamsIssuesPayload(p *api.IssuePayload) (*MSTeamsPayload, error) { }, nil } -func getMSTeamsIssueCommentPayload(p *api.IssueCommentPayload) (*MSTeamsPayload, error) { +// IssueComment implements PayloadConvertor IssueComment method +func (m *MSTeamsPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { text, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false) return &MSTeamsPayload{ @@ -349,7 +358,8 @@ func getMSTeamsIssueCommentPayload(p *api.IssueCommentPayload) (*MSTeamsPayload, }, nil } -func getMSTeamsPullRequestPayload(p *api.PullRequestPayload) (*MSTeamsPayload, error) { +// PullRequest implements PayloadConvertor PullRequest method +func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { text, _, attachmentText, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false) return &MSTeamsPayload{ @@ -391,7 +401,8 @@ func getMSTeamsPullRequestPayload(p *api.PullRequestPayload) (*MSTeamsPayload, e }, nil } -func getMSTeamsPullRequestApprovalPayload(p *api.PullRequestPayload, event models.HookEventType) (*MSTeamsPayload, error) { +// Review implements PayloadConvertor Review method +func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event models.HookEventType) (api.Payloader, error) { var text, title string var color int switch p.Action { @@ -455,7 +466,8 @@ func getMSTeamsPullRequestApprovalPayload(p *api.PullRequestPayload, event model }, nil } -func getMSTeamsRepositoryPayload(p *api.RepositoryPayload) (*MSTeamsPayload, error) { +// Repository implements PayloadConvertor Repository method +func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { var title, url string var color int switch p.Action { @@ -502,7 +514,8 @@ func getMSTeamsRepositoryPayload(p *api.RepositoryPayload) (*MSTeamsPayload, err }, nil } -func getMSTeamsReleasePayload(p *api.ReleasePayload) (*MSTeamsPayload, error) { +// Release implements PayloadConvertor Release method +func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { text, color := getReleasePayloadInfo(p, noneLinkFormatter, false) return &MSTeamsPayload{ @@ -545,36 +558,6 @@ func getMSTeamsReleasePayload(p *api.ReleasePayload) (*MSTeamsPayload, error) { } // GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload -func GetMSTeamsPayload(p api.Payloader, event models.HookEventType, meta string) (*MSTeamsPayload, error) { - s := new(MSTeamsPayload) - - switch event { - case models.HookEventCreate: - return getMSTeamsCreatePayload(p.(*api.CreatePayload)) - case models.HookEventDelete: - return getMSTeamsDeletePayload(p.(*api.DeletePayload)) - case models.HookEventFork: - return getMSTeamsForkPayload(p.(*api.ForkPayload)) - case models.HookEventIssues, models.HookEventIssueAssign, models.HookEventIssueLabel, models.HookEventIssueMilestone: - return getMSTeamsIssuesPayload(p.(*api.IssuePayload)) - case models.HookEventIssueComment, models.HookEventPullRequestComment: - pl, ok := p.(*api.IssueCommentPayload) - if ok { - return getMSTeamsIssueCommentPayload(pl) - } - return getMSTeamsPullRequestPayload(p.(*api.PullRequestPayload)) - case models.HookEventPush: - return getMSTeamsPushPayload(p.(*api.PushPayload)) - case models.HookEventPullRequest, models.HookEventPullRequestAssign, models.HookEventPullRequestLabel, - models.HookEventPullRequestMilestone, models.HookEventPullRequestSync: - return getMSTeamsPullRequestPayload(p.(*api.PullRequestPayload)) - case models.HookEventPullRequestReviewRejected, models.HookEventPullRequestReviewApproved, models.HookEventPullRequestReviewComment: - return getMSTeamsPullRequestApprovalPayload(p.(*api.PullRequestPayload), event) - case models.HookEventRepository: - return getMSTeamsRepositoryPayload(p.(*api.RepositoryPayload)) - case models.HookEventRelease: - return getMSTeamsReleasePayload(p.(*api.ReleasePayload)) - } - - return s, nil +func GetMSTeamsPayload(p api.Payloader, event models.HookEventType, meta string) (api.Payloader, error) { + return convertPayloader(new(MSTeamsPayload), p, event) } diff --git a/modules/webhook/payloader.go b/modules/webhook/payloader.go new file mode 100644 index 0000000000000..f1cdaf65956d4 --- /dev/null +++ b/modules/webhook/payloader.go @@ -0,0 +1,56 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package webhook + +import ( + "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" +) + +// PayloadConvertor defines the interface to convert system webhook payload to external payload +type PayloadConvertor interface { + api.Payloader + Create(*api.CreatePayload) (api.Payloader, error) + Delete(*api.DeletePayload) (api.Payloader, error) + Fork(*api.ForkPayload) (api.Payloader, error) + Issue(*api.IssuePayload) (api.Payloader, error) + IssueComment(*api.IssueCommentPayload) (api.Payloader, error) + Push(*api.PushPayload) (api.Payloader, error) + PullRequest(*api.PullRequestPayload) (api.Payloader, error) + Review(*api.PullRequestPayload, models.HookEventType) (api.Payloader, error) + Repository(*api.RepositoryPayload) (api.Payloader, error) + Release(*api.ReleasePayload) (api.Payloader, error) +} + +func convertPayloader(s PayloadConvertor, p api.Payloader, event models.HookEventType) (api.Payloader, error) { + switch event { + case models.HookEventCreate: + return s.Create(p.(*api.CreatePayload)) + case models.HookEventDelete: + return s.Delete(p.(*api.DeletePayload)) + case models.HookEventFork: + return s.Fork(p.(*api.ForkPayload)) + case models.HookEventIssues, models.HookEventIssueAssign, models.HookEventIssueLabel, models.HookEventIssueMilestone: + return s.Issue(p.(*api.IssuePayload)) + case models.HookEventIssueComment, models.HookEventPullRequestComment: + pl, ok := p.(*api.IssueCommentPayload) + if ok { + return s.IssueComment(pl) + } + return s.PullRequest(p.(*api.PullRequestPayload)) + case models.HookEventPush: + return s.Push(p.(*api.PushPayload)) + case models.HookEventPullRequest, models.HookEventPullRequestAssign, models.HookEventPullRequestLabel, + models.HookEventPullRequestMilestone, models.HookEventPullRequestSync: + return s.PullRequest(p.(*api.PullRequestPayload)) + case models.HookEventPullRequestReviewApproved, models.HookEventPullRequestReviewRejected, models.HookEventPullRequestReviewComment: + return s.Review(p.(*api.PullRequestPayload), event) + case models.HookEventRepository: + return s.Repository(p.(*api.RepositoryPayload)) + case models.HookEventRelease: + return s.Release(p.(*api.ReleasePayload)) + } + return s, nil +} diff --git a/modules/webhook/slack.go b/modules/webhook/slack.go index 4177bd1250e92..4c0b50ecab6d2 100644 --- a/modules/webhook/slack.go +++ b/modules/webhook/slack.go @@ -38,6 +38,7 @@ func GetSlackHook(w *models.Webhook) *SlackMeta { type SlackPayload struct { Channel string `json:"channel"` Text string `json:"text"` + Color string `json:"-"` Username string `json:"username"` IconURL string `json:"icon_url"` UnfurlLinks int `json:"unfurl_links"` @@ -55,11 +56,11 @@ type SlackAttachment struct { } // SetSecret sets the slack secret -func (p *SlackPayload) SetSecret(_ string) {} +func (s *SlackPayload) SetSecret(_ string) {} // JSONPayload Marshals the SlackPayload to json -func (p *SlackPayload) JSONPayload() ([]byte, error) { - data, err := json.MarshalIndent(p, "", " ") +func (s *SlackPayload) JSONPayload() ([]byte, error) { + data, err := json.MarshalIndent(s, "", " ") if err != nil { return []byte{}, err } @@ -98,53 +99,59 @@ func SlackLinkToRef(repoURL, ref string) string { return SlackLinkFormatter(url, refName) } -func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayload, error) { +var ( + _ PayloadConvertor = &SlackPayload{} +) + +// Create implements PayloadConvertor Create method +func (s *SlackPayload) Create(p *api.CreatePayload) (api.Payloader, error) { repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref) text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName) return &SlackPayload{ - Channel: slack.Channel, + Channel: s.Channel, Text: text, - Username: slack.Username, - IconURL: slack.IconURL, + Username: s.Username, + IconURL: s.IconURL, }, nil } -// getSlackDeletePayload composes Slack payload for delete a branch or tag. -func getSlackDeletePayload(p *api.DeletePayload, slack *SlackMeta) (*SlackPayload, error) { +// Delete composes Slack payload for delete a branch or tag. +func (s *SlackPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { refName := git.RefEndName(p.Ref) repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName) return &SlackPayload{ - Channel: slack.Channel, + Channel: s.Channel, Text: text, - Username: slack.Username, - IconURL: slack.IconURL, + Username: s.Username, + IconURL: s.IconURL, }, nil } -// getSlackForkPayload composes Slack payload for forked by a repository. -func getSlackForkPayload(p *api.ForkPayload, slack *SlackMeta) (*SlackPayload, error) { +// Fork composes Slack payload for forked by a repository. +func (s *SlackPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { baseLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) forkLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink) return &SlackPayload{ - Channel: slack.Channel, + Channel: s.Channel, Text: text, - Username: slack.Username, - IconURL: slack.IconURL, + Username: s.Username, + IconURL: s.IconURL, }, nil } -func getSlackIssuesPayload(p *api.IssuePayload, slack *SlackMeta) (*SlackPayload, error) { +// Issue implements PayloadConvertor Issue method +func (s *SlackPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { text, issueTitle, attachmentText, color := getIssuesPayloadInfo(p, SlackLinkFormatter, true) pl := &SlackPayload{ - Channel: slack.Channel, + Channel: s.Channel, Text: text, - Username: slack.Username, - IconURL: slack.IconURL, + Username: s.Username, + IconURL: s.IconURL, } if attachmentText != "" { attachmentText = SlackTextFormatter(attachmentText) @@ -160,14 +167,15 @@ func getSlackIssuesPayload(p *api.IssuePayload, slack *SlackMeta) (*SlackPayload return pl, nil } -func getSlackIssueCommentPayload(p *api.IssueCommentPayload, slack *SlackMeta) (*SlackPayload, error) { +// IssueComment implements PayloadConvertor IssueComment method +func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { text, issueTitle, color := getIssueCommentPayloadInfo(p, SlackLinkFormatter, true) return &SlackPayload{ - Channel: slack.Channel, + Channel: s.Channel, Text: text, - Username: slack.Username, - IconURL: slack.IconURL, + Username: s.Username, + IconURL: s.IconURL, Attachments: []SlackAttachment{{ Color: fmt.Sprintf("%x", color), Title: issueTitle, @@ -177,18 +185,20 @@ func getSlackIssueCommentPayload(p *api.IssueCommentPayload, slack *SlackMeta) ( }, nil } -func getSlackReleasePayload(p *api.ReleasePayload, slack *SlackMeta) (*SlackPayload, error) { +// Release implements PayloadConvertor Release method +func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { text, _ := getReleasePayloadInfo(p, SlackLinkFormatter, true) return &SlackPayload{ - Channel: slack.Channel, + Channel: s.Channel, Text: text, - Username: slack.Username, - IconURL: slack.IconURL, + Username: s.Username, + IconURL: s.IconURL, }, nil } -func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) { +// Push implements PayloadConvertor Push method +func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) { // n new commits var ( commitDesc string @@ -221,12 +231,12 @@ func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, e } return &SlackPayload{ - Channel: slack.Channel, + Channel: s.Channel, Text: text, - Username: slack.Username, - IconURL: slack.IconURL, + Username: s.Username, + IconURL: s.IconURL, Attachments: []SlackAttachment{{ - Color: slack.Color, + Color: s.Color, Title: p.Repo.HTMLURL, TitleLink: p.Repo.HTMLURL, Text: attachmentText, @@ -234,14 +244,15 @@ func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, e }, nil } -func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*SlackPayload, error) { +// PullRequest implements PayloadConvertor PullRequest method +func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { text, issueTitle, attachmentText, color := getPullRequestPayloadInfo(p, SlackLinkFormatter, true) pl := &SlackPayload{ - Channel: slack.Channel, + Channel: s.Channel, Text: text, - Username: slack.Username, - IconURL: slack.IconURL, + Username: s.Username, + IconURL: s.IconURL, } if attachmentText != "" { attachmentText = SlackTextFormatter(p.PullRequest.Body) @@ -257,7 +268,8 @@ func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*S return pl, nil } -func getSlackPullRequestApprovalPayload(p *api.PullRequestPayload, slack *SlackMeta, event models.HookEventType) (*SlackPayload, error) { +// Review implements PayloadConvertor Review method +func (s *SlackPayload) Review(p *api.PullRequestPayload, event models.HookEventType) (api.Payloader, error) { senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title) titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index) @@ -275,14 +287,15 @@ func getSlackPullRequestApprovalPayload(p *api.PullRequestPayload, slack *SlackM } return &SlackPayload{ - Channel: slack.Channel, + Channel: s.Channel, Text: text, - Username: slack.Username, - IconURL: slack.IconURL, + Username: s.Username, + IconURL: s.IconURL, }, nil } -func getSlackRepositoryPayload(p *api.RepositoryPayload, slack *SlackMeta) (*SlackPayload, error) { +// Repository implements PayloadConvertor Repository method +func (s *SlackPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName) var text string @@ -295,15 +308,15 @@ func getSlackRepositoryPayload(p *api.RepositoryPayload, slack *SlackMeta) (*Sla } return &SlackPayload{ - Channel: slack.Channel, + Channel: s.Channel, Text: text, - Username: slack.Username, - IconURL: slack.IconURL, + Username: s.Username, + IconURL: s.IconURL, }, nil } // GetSlackPayload converts a slack webhook into a SlackPayload -func GetSlackPayload(p api.Payloader, event models.HookEventType, meta string) (*SlackPayload, error) { +func GetSlackPayload(p api.Payloader, event models.HookEventType, meta string) (api.Payloader, error) { s := new(SlackPayload) slack := &SlackMeta{} @@ -311,33 +324,10 @@ func GetSlackPayload(p api.Payloader, event models.HookEventType, meta string) ( return s, errors.New("GetSlackPayload meta json:" + err.Error()) } - switch event { - case models.HookEventCreate: - return getSlackCreatePayload(p.(*api.CreatePayload), slack) - case models.HookEventDelete: - return getSlackDeletePayload(p.(*api.DeletePayload), slack) - case models.HookEventFork: - return getSlackForkPayload(p.(*api.ForkPayload), slack) - case models.HookEventIssues, models.HookEventIssueAssign, models.HookEventIssueLabel, models.HookEventIssueMilestone: - return getSlackIssuesPayload(p.(*api.IssuePayload), slack) - case models.HookEventIssueComment, models.HookEventPullRequestComment: - pl, ok := p.(*api.IssueCommentPayload) - if ok { - return getSlackIssueCommentPayload(pl, slack) - } - return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack) - case models.HookEventPush: - return getSlackPushPayload(p.(*api.PushPayload), slack) - case models.HookEventPullRequest, models.HookEventPullRequestAssign, models.HookEventPullRequestLabel, - models.HookEventPullRequestMilestone, models.HookEventPullRequestSync: - return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack) - case models.HookEventPullRequestReviewRejected, models.HookEventPullRequestReviewApproved, models.HookEventPullRequestReviewComment: - return getSlackPullRequestApprovalPayload(p.(*api.PullRequestPayload), slack, event) - case models.HookEventRepository: - return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack) - case models.HookEventRelease: - return getSlackReleasePayload(p.(*api.ReleasePayload), slack) - } + s.Channel = slack.Channel + s.Username = slack.Username + s.IconURL = slack.IconURL + s.Color = slack.Color - return s, nil + return convertPayloader(s, p, event) } diff --git a/modules/webhook/slack_test.go b/modules/webhook/slack_test.go index 15503434f83fc..20de80bd656d8 100644 --- a/modules/webhook/slack_test.go +++ b/modules/webhook/slack_test.go @@ -14,75 +14,67 @@ import ( func TestSlackIssuesPayloadOpened(t *testing.T) { p := issueTestPayload() - sl := &SlackMeta{ - Username: p.Sender.UserName, - } - p.Action = api.HookIssueOpened - pl, err := getSlackIssuesPayload(p, sl) + + s := new(SlackPayload) + s.Username = p.Sender.UserName + + pl, err := s.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[] Issue opened: by ", pl.Text) + assert.Equal(t, "[] Issue opened: by ", pl.(*SlackPayload).Text) p.Action = api.HookIssueClosed - pl, err = getSlackIssuesPayload(p, sl) + pl, err = s.Issue(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[] Issue closed: by ", pl.Text) + assert.Equal(t, "[] Issue closed: by ", pl.(*SlackPayload).Text) } func TestSlackIssueCommentPayload(t *testing.T) { p := issueCommentTestPayload() + s := new(SlackPayload) + s.Username = p.Sender.UserName - sl := &SlackMeta{ - Username: p.Sender.UserName, - } - - pl, err := getSlackIssueCommentPayload(p, sl) + pl, err := s.IssueComment(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[] New comment on issue by ", pl.Text) + assert.Equal(t, "[] New comment on issue by ", pl.(*SlackPayload).Text) } func TestSlackPullRequestCommentPayload(t *testing.T) { p := pullRequestCommentTestPayload() + s := new(SlackPayload) + s.Username = p.Sender.UserName - sl := &SlackMeta{ - Username: p.Sender.UserName, - } - - pl, err := getSlackIssueCommentPayload(p, sl) + pl, err := s.IssueComment(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[] New comment on pull request by ", pl.Text) + assert.Equal(t, "[] New comment on pull request by ", pl.(*SlackPayload).Text) } func TestSlackReleasePayload(t *testing.T) { p := pullReleaseTestPayload() + s := new(SlackPayload) + s.Username = p.Sender.UserName - sl := &SlackMeta{ - Username: p.Sender.UserName, - } - - pl, err := getSlackReleasePayload(p, sl) + pl, err := s.Release(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[] Release created: by ", pl.Text) + assert.Equal(t, "[] Release created: by ", pl.(*SlackPayload).Text) } func TestSlackPullRequestPayload(t *testing.T) { p := pullRequestTestPayload() + s := new(SlackPayload) + s.Username = p.Sender.UserName - sl := &SlackMeta{ - Username: p.Sender.UserName, - } - - pl, err := getSlackPullRequestPayload(p, sl) + pl, err := s.PullRequest(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[] Pull request opened: by ", pl.Text) + assert.Equal(t, "[] Pull request opened: by ", pl.(*SlackPayload).Text) } diff --git a/modules/webhook/telegram.go b/modules/webhook/telegram.go index 6d2f804a70237..84fc210042c66 100644 --- a/modules/webhook/telegram.go +++ b/modules/webhook/telegram.go @@ -40,22 +40,27 @@ func GetTelegramHook(w *models.Webhook) *TelegramMeta { return s } +var ( + _ PayloadConvertor = &TelegramPayload{} +) + // SetSecret sets the telegram secret -func (p *TelegramPayload) SetSecret(_ string) {} +func (t *TelegramPayload) SetSecret(_ string) {} // JSONPayload Marshals the TelegramPayload to json -func (p *TelegramPayload) JSONPayload() ([]byte, error) { - p.ParseMode = "HTML" - p.DisableWebPreview = true - p.Message = markup.Sanitize(p.Message) - data, err := json.MarshalIndent(p, "", " ") +func (t *TelegramPayload) JSONPayload() ([]byte, error) { + t.ParseMode = "HTML" + t.DisableWebPreview = true + t.Message = markup.Sanitize(t.Message) + data, err := json.MarshalIndent(t, "", " ") if err != nil { return []byte{}, err } return data, nil } -func getTelegramCreatePayload(p *api.CreatePayload) (*TelegramPayload, error) { +// Create implements PayloadConvertor Create method +func (t *TelegramPayload) Create(p *api.CreatePayload) (api.Payloader, error) { // created tag/branch refName := git.RefEndName(p.Ref) title := fmt.Sprintf(`[%s] %s %s created`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType, @@ -66,7 +71,8 @@ func getTelegramCreatePayload(p *api.CreatePayload) (*TelegramPayload, error) { }, nil } -func getTelegramDeletePayload(p *api.DeletePayload) (*TelegramPayload, error) { +// Delete implements PayloadConvertor Delete method +func (t *TelegramPayload) Delete(p *api.DeletePayload) (api.Payloader, error) { // created tag/branch refName := git.RefEndName(p.Ref) title := fmt.Sprintf(`[%s] %s %s deleted`, p.Repo.HTMLURL, p.Repo.FullName, p.RefType, @@ -77,7 +83,8 @@ func getTelegramDeletePayload(p *api.DeletePayload) (*TelegramPayload, error) { }, nil } -func getTelegramForkPayload(p *api.ForkPayload) (*TelegramPayload, error) { +// Fork implements PayloadConvertor Fork method +func (t *TelegramPayload) Fork(p *api.ForkPayload) (api.Payloader, error) { title := fmt.Sprintf(`%s is forked to %s`, p.Forkee.FullName, p.Repo.HTMLURL, p.Repo.FullName) return &TelegramPayload{ @@ -85,7 +92,8 @@ func getTelegramForkPayload(p *api.ForkPayload) (*TelegramPayload, error) { }, nil } -func getTelegramPushPayload(p *api.PushPayload) (*TelegramPayload, error) { +// Push implements PayloadConvertor Push method +func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) { var ( branchName = git.RefEndName(p.Ref) commitDesc string @@ -124,7 +132,8 @@ func getTelegramPushPayload(p *api.PushPayload) (*TelegramPayload, error) { }, nil } -func getTelegramIssuesPayload(p *api.IssuePayload) (*TelegramPayload, error) { +// Issue implements PayloadConvertor Issue method +func (t *TelegramPayload) Issue(p *api.IssuePayload) (api.Payloader, error) { text, _, attachmentText, _ := getIssuesPayloadInfo(p, htmlLinkFormatter, true) return &TelegramPayload{ @@ -132,7 +141,8 @@ func getTelegramIssuesPayload(p *api.IssuePayload) (*TelegramPayload, error) { }, nil } -func getTelegramIssueCommentPayload(p *api.IssueCommentPayload) (*TelegramPayload, error) { +// IssueComment implements PayloadConvertor IssueComment method +func (t *TelegramPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) { text, _, _ := getIssueCommentPayloadInfo(p, htmlLinkFormatter, true) return &TelegramPayload{ @@ -140,7 +150,8 @@ func getTelegramIssueCommentPayload(p *api.IssueCommentPayload) (*TelegramPayloa }, nil } -func getTelegramPullRequestPayload(p *api.PullRequestPayload) (*TelegramPayload, error) { +// PullRequest implements PayloadConvertor PullRequest method +func (t *TelegramPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) { text, _, attachmentText, _ := getPullRequestPayloadInfo(p, htmlLinkFormatter, true) return &TelegramPayload{ @@ -148,7 +159,8 @@ func getTelegramPullRequestPayload(p *api.PullRequestPayload) (*TelegramPayload, }, nil } -func getTelegramPullRequestApprovalPayload(p *api.PullRequestPayload, event models.HookEventType) (*TelegramPayload, error) { +// Review implements PayloadConvertor Review method +func (t *TelegramPayload) Review(p *api.PullRequestPayload, event models.HookEventType) (api.Payloader, error) { var text, attachmentText string switch p.Action { case api.HookIssueReviewed: @@ -167,7 +179,8 @@ func getTelegramPullRequestApprovalPayload(p *api.PullRequestPayload, event mode }, nil } -func getTelegramRepositoryPayload(p *api.RepositoryPayload) (*TelegramPayload, error) { +// Repository implements PayloadConvertor Repository method +func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) { var title string switch p.Action { case api.HookRepoCreated: @@ -184,7 +197,8 @@ func getTelegramRepositoryPayload(p *api.RepositoryPayload) (*TelegramPayload, e return nil, nil } -func getTelegramReleasePayload(p *api.ReleasePayload) (*TelegramPayload, error) { +// Release implements PayloadConvertor Release method +func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error) { text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true) return &TelegramPayload{ @@ -193,36 +207,6 @@ func getTelegramReleasePayload(p *api.ReleasePayload) (*TelegramPayload, error) } // GetTelegramPayload converts a telegram webhook into a TelegramPayload -func GetTelegramPayload(p api.Payloader, event models.HookEventType, meta string) (*TelegramPayload, error) { - s := new(TelegramPayload) - - switch event { - case models.HookEventCreate: - return getTelegramCreatePayload(p.(*api.CreatePayload)) - case models.HookEventDelete: - return getTelegramDeletePayload(p.(*api.DeletePayload)) - case models.HookEventFork: - return getTelegramForkPayload(p.(*api.ForkPayload)) - case models.HookEventIssues, models.HookEventIssueAssign, models.HookEventIssueLabel, models.HookEventIssueMilestone: - return getTelegramIssuesPayload(p.(*api.IssuePayload)) - case models.HookEventIssueComment, models.HookEventPullRequestComment: - pl, ok := p.(*api.IssueCommentPayload) - if ok { - return getTelegramIssueCommentPayload(pl) - } - return getTelegramPullRequestPayload(p.(*api.PullRequestPayload)) - case models.HookEventPush: - return getTelegramPushPayload(p.(*api.PushPayload)) - case models.HookEventPullRequest, models.HookEventPullRequestAssign, models.HookEventPullRequestLabel, - models.HookEventPullRequestMilestone, models.HookEventPullRequestSync: - return getTelegramPullRequestPayload(p.(*api.PullRequestPayload)) - case models.HookEventPullRequestReviewRejected, models.HookEventPullRequestReviewApproved, models.HookEventPullRequestReviewComment: - return getTelegramPullRequestApprovalPayload(p.(*api.PullRequestPayload), event) - case models.HookEventRepository: - return getTelegramRepositoryPayload(p.(*api.RepositoryPayload)) - case models.HookEventRelease: - return getTelegramReleasePayload(p.(*api.ReleasePayload)) - } - - return s, nil +func GetTelegramPayload(p api.Payloader, event models.HookEventType, meta string) (api.Payloader, error) { + return convertPayloader(new(TelegramPayload), p, event) } diff --git a/modules/webhook/telegram_test.go b/modules/webhook/telegram_test.go index 1686188b9dda9..0e909343a86c6 100644 --- a/modules/webhook/telegram_test.go +++ b/modules/webhook/telegram_test.go @@ -16,9 +16,9 @@ func TestGetTelegramIssuesPayload(t *testing.T) { p := issueTestPayload() p.Action = api.HookIssueClosed - pl, err := getTelegramIssuesPayload(p) + pl, err := new(TelegramPayload).Issue(p) require.NoError(t, err) require.NotNil(t, pl) - assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1\n\n", pl.Message) + assert.Equal(t, "[test/repo] Issue closed: #2 crash by user1\n\n", pl.(*TelegramPayload).Message) } diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 6a64d6272c851..1d1b18889ad37 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -832,7 +832,6 @@ editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již není editor.file_changed_while_editing=Obsah souboru byl změněn od doby, kdy jste začaly s úpravou. Klikněte zde, abyste je zobrazili, nebo potvrďte změny ještě jednou pro jejich přepsání. editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři. editor.commit_empty_file_header=Potvrďte prázdný soubor -editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. Pokračovat? editor.no_changes_to_show=Žádné změny k zobrazení. editor.fail_to_update_file=Vytvoření nebo změna souboru „%s“ skončila chybou: %v editor.push_rejected_no_message=Změna byla serverem zamítnuta bez zprávy. Prosím, zkontrolujte háčky Gitu. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 12878eba9799a..33e15102a4307 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -21,10 +21,12 @@ signed_in_as=Angemeldet als enable_javascript=Diese Webseite funktioniert besser mit JavaScript. toc=Inhaltsverzeichnis licenses=Lizenzen +return_to_gitea=Zurück zu Gitea username=Benutzername email=E-Mail-Adresse password=Passwort +access_token=Zugangs-Token re_type=Passwort erneut eingeben captcha=CAPTCHA twofa=Zwei-Faktor-Authentifizierung @@ -369,6 +371,7 @@ enterred_invalid_owner_name=Der Name des neuen Besitzers ist ungültig. enterred_invalid_password=Das eingegebene Passwort ist falsch. user_not_exist=Dieser Benutzer ist nicht vorhanden. team_not_exist=Dieses Team existiert nicht. +last_org_owner=Du kannst den letzten Benutzer nicht aus dem 'Besitzer'-Team entfernen. Es muss mindestens einen Besitzer in einer Organisation geben. cannot_add_org_to_team=Eine Organisation kann nicht als Teammitglied hinzugefügt werden. invalid_ssh_key=Dein SSH-Key kann nicht überprüft werden: %s @@ -704,6 +707,10 @@ form.name_reserved=Der Repository-Name „%s“ ist reserviert. form.name_pattern_not_allowed='%s' ist nicht erlaubt für Repository-Namen. need_auth=Authentifizierung zum Klonen benötigt +migrate_options=Migrationsoptionen +migrate_service=Migrationsdienst +migrate_options_mirror_helper=Dieses Repository wird ein Mirror sein +migrate_options_mirror_disabled=Dein Administrator hat neue Mirrors deaktiviert. migrate_items=Migrationselemente migrate_items_wiki=Wiki migrate_items_milestones=Meilensteine @@ -719,6 +726,7 @@ migrate.permission_denied=Du hast keine Berechtigung zum Importieren lokaler Rep migrate.invalid_local_path=Der lokale Pfad ist ungültig, existiert nicht oder ist kein Ordner. migrate.failed=Fehler bei der Migration: %v migrate.lfs_mirror_unsupported=Spiegeln von LFS-Objekten wird nicht unterstützt - nutze stattdessen 'git lfs fetch --all' und 'git lfs push --all'. +migrate.migrate_items_options=Authentifizierung wird benötigt, um Elemente aus einem Dienst zu migrieren, der sie unterstützt. migrated_from=Migriert von %[2]s migrated_from_fake=Migriert von %[1]s migrate.migrating=Migriere von %s ... @@ -751,6 +759,7 @@ code=Code code.desc=Zugriff auf Quellcode, Dateien, Commits und Branches. branch=Branch tree=Struktur +clear_ref=`Aktuelle Referenz löschen` filter_branch_and_tag=Branch oder Tag filtern branches=Branches tags=Tags @@ -824,7 +833,7 @@ editor.file_deleting_no_longer_exists=Die Datei '%s' existiert in diesem Reposit editor.file_changed_while_editing=Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. Hier klicken, um die Änderungen anzusehen, oder Änderungen erneut comitten, um sie zu überschreiben. editor.file_already_exists=Eine Datei mit dem Namen „%s“ ist bereits in diesem Repository vorhanden. editor.commit_empty_file_header=Leere Datei committen -editor.commit_empty_file_text=Die Datei, die du gerade commitest ist leer! Fortfahren? +editor.commit_empty_file_text=Die Datei, die du commiten willst, ist leer. Fortfahren? editor.no_changes_to_show=Keine Änderungen vorhanden. editor.fail_to_update_file=Fehler beim Ändern/Erstellen der Datei „%s“. Fehler: %v editor.push_rejected_no_message=Die Änderung wurde vom Server ohne Nachricht abgelehnt. Bitte überprüfe die githooks. @@ -1018,6 +1027,7 @@ issues.poster=Ersteller issues.collaborator=Mitarbeiter issues.owner=Besitzer issues.re_request_review=Review erneut anfordern +issues.is_stale=Seit diesem Review gab es Änderungen an diesem PR issues.remove_request_review=Review-Anfrage entfernen issues.remove_request_review_block=Review-Anfrage kann nicht entfernt werden issues.sign_in_require_desc=Anmelden, um an der Diskussion teilzunehmen. @@ -1231,6 +1241,7 @@ milestones.new=Neuer Meilenstein milestones.open_tab=%d offen milestones.close_tab=%d geschlossen milestones.closed=Geschlossen %s +milestones.update_ago=Vor %s aktualisiert milestones.no_due_date=Kein Fälligkeitsdatum milestones.open=Öffnen milestones.close=Schließen @@ -1270,6 +1281,7 @@ signing.wont_sign.basesigned=Der Merge Commit wird nicht signiert werden, da der signing.wont_sign.headsigned=Der Merge Commit wird nicht signiert werden, da der Head-Commit nicht signiert ist signing.wont_sign.commitssigned=Der Merge Commit wird nicht signiert werden, da alle zugehörigen Commits nicht signiert sind signing.wont_sign.approved=Der Merge Commit wird nicht signiert werden, da der Pull Request nicht genehmigt wurde +signing.wont_sign.not_signed_in=Du bist nicht eingeloggt ext_wiki=Externes Wiki ext_wiki.desc=Verweis auf externes Wiki. @@ -1427,7 +1439,7 @@ settings.convert_notices_1=Dieser Vorgang wandelt das Mirror-Repository in ein n settings.convert_confirm=Repository umwandeln settings.convert_succeed=Das Mirror-Repository wurde erfolgreich in ein normales Repository umgewandelt. settings.convert_fork=In ein normales Repository umwandeln -settings.convert_fork_desc=Sie können diesen Fork in ein normales Repository umwandeln. Dies kann nicht rückgängig gemacht werden. +settings.convert_fork_desc=Du kannst diesen Fork in ein normales Repository umwandeln. Dies kann nicht rückgängig gemacht werden. settings.convert_fork_notices_1=Dieser Vorgang konvertiert den Fork in ein normales Repository und kann nicht rückgängig gemacht werden. settings.convert_fork_confirm=Repository umwandeln settings.convert_fork_succeed=Der Fork wurde in ein normales Repository konvertiert. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 94d7ab27fb1b5..96226f9ef3168 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -300,6 +300,8 @@ authorization_failed = Authorization failed authorization_failed_desc = The authorization failed because we detected an invalid request. Please contact the maintainer of the app you've tried to authorize. disable_forgot_password_mail = Account recovery is disabled. Please contact your site administrator. sspi_auth_failed = SSPI authentication failed +password_pwned = The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password. +password_pwned_err = Could not complete request to HaveIBeenPwned [mail] activate_account = Please activate your account @@ -704,6 +706,7 @@ archive.issue.nocomment = This repo is archived. You cannot comment on issues. archive.pull.nocomment = This repo is archived. You cannot comment on pull requests. form.reach_limit_of_creation = You have already reached your limit of %d repositories. +form.reach_limit_of_private_creation = You have already reached your limit of %d private repositories. form.name_reserved = The repository name '%s' is reserved. form.name_pattern_not_allowed = The pattern '%s' is not allowed in a repository name. @@ -718,6 +721,7 @@ migrate_items_milestones = Milestones migrate_items_labels = Labels migrate_items_issues = Issues migrate_items_pullrequests = Pull Requests +migrate_items_merge_requests = Merge Requests migrate_items_releases = Releases migrate_repo = Migrate Repository migrate.clone_address = Migrate / Clone From URL @@ -727,11 +731,15 @@ migrate.permission_denied = You are not allowed to import local repositories. migrate.invalid_local_path = "The local path is invalid. It does not exist or is not a directory." migrate.failed = Migration failed: %v migrate.lfs_mirror_unsupported = Mirroring LFS objects is not supported - use 'git lfs fetch --all' and 'git lfs push --all' instead. -migrate.migrate_items_options = Authentication is needed to migrate items from a service that supports them. +migrate.migrate_items_options = Access Token is required to migrate additional items migrated_from = Migrated from %[2]s migrated_from_fake = Migrated From %[1]s +migrate.migrate = Migrate From %s migrate.migrating = Migrating from %s ... migrate.migrating_failed = Migrating from %s failed. +migrate.github.description = Migrating data from Github.com or Github Enterprise. +migrate.git.description = Migrating or Mirroring git data from Git services +migrate.gitlab.description = Migrating data from GitLab.com or Self-Hosted gitlab server. mirror_from = mirror of forked_from = forked from @@ -760,6 +768,7 @@ code = Code code.desc = Access source code, files, commits and branches. branch = Branch tree = Tree +clear_ref = `Clear current reference` filter_branch_and_tag = Filter branch or tag branches = Branches tags = Tags @@ -833,7 +842,7 @@ editor.file_deleting_no_longer_exists = The file being deleted, '%s', no longer editor.file_changed_while_editing = The file contents have changed since you started editing. Click here to see them or Commit Changes again to overwrite them. editor.file_already_exists = A file named '%s' already exists in this repository. editor.commit_empty_file_header = Commit an empty file -editor.commit_empty_file_text = The file you're about commit is empty. Proceed? +editor.commit_empty_file_text = The file you're about to commit is empty. Proceed? editor.no_changes_to_show = There are no changes to show. editor.fail_to_update_file = Failed to update/create file '%s' with error: %v editor.push_rejected_no_message = The change was rejected by the server without a message. Please check githooks. @@ -1027,6 +1036,7 @@ issues.poster = Poster issues.collaborator = Collaborator issues.owner = Owner issues.re_request_review=Re-request review +issues.is_stale = There have been changes to this PR since this review issues.remove_request_review=Remove review request issues.remove_request_review_block=Can't remove review request issues.sign_in_require_desc = Sign in to join this conversation. @@ -1240,6 +1250,7 @@ milestones.new = New Milestone milestones.open_tab = %d Open milestones.close_tab = %d Closed milestones.closed = Closed %s +milestones.update_ago = Updated %s ago milestones.no_due_date = No due date milestones.open = Open milestones.close = Close @@ -1990,6 +2001,7 @@ users.admin = Admin users.restricted = Restricted users.2fa = 2FA users.repos = Repos +users.privaterepos = Private Repos users.created = Created users.last_login = Last Sign-In users.never_login = Never Signed-In @@ -2004,6 +2016,8 @@ users.update_profile_success = The user account has been updated. users.edit_account = Edit User Account users.max_repo_creation = Maximum Number of Repositories users.max_repo_creation_desc = (Enter -1 to use the global default limit.) +users.max_private_repo_creation = Maximum Number of Private Repositories +users.max_private_repo_creation_desc = (Enter -1 to use the global default limit.) users.is_activated = User Account Is Activated users.prohibit_login = Disable Sign-In users.is_admin = Is Administrator diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index fe7e1bd9c8f9b..3133ee8906d0b 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -299,6 +299,8 @@ authorize_title=¿Autorizar a "%s" a acceder a su cuenta? authorization_failed=Autorización fallida authorization_failed_desc=La autorización ha fallado porque hemos detectado una solicitud no válida. Por favor, póngase en contacto con el mantenedor de la aplicación que ha intentado autorizar. sspi_auth_failed=Fallo en la autenticación SSPI +password_pwned=La contraseña que eligió está en una lista de contraseñas robadas previamente expuestas en violaciones de datos públicos. Por favor, inténtalo de nuevo con una contraseña diferente. +password_pwned_err=No se pudo completar la solicitud a HaveIBeenPwned [mail] activate_account=Por favor, active su cuenta @@ -759,6 +761,7 @@ code=Código code.desc=Acceder código fuente, archivos, commits, y ramas. branch=Rama tree=Árbol +clear_ref=`Borrar referencia actual` filter_branch_and_tag=Filtrar por rama o etiqueta branches=Ramas tags=Etiquetas @@ -1026,6 +1029,7 @@ issues.poster=Autor issues.collaborator=Colaborador issues.owner=Propietario issues.re_request_review=Solicitar revisión de nuevo +issues.is_stale=Ha habido cambios en este PR desde esta revisión issues.remove_request_review=Eliminar solicitud de revisión issues.remove_request_review_block=No se puede eliminar la solicitud de revisión issues.sign_in_require_desc=Inicie sesión para unirse a esta conversación. @@ -1239,6 +1243,7 @@ milestones.new=Nuevo hito milestones.open_tab=%d abiertas milestones.close_tab=%d cerradas milestones.closed=Cerrada %s +milestones.update_ago=Actualizado hace %s milestones.no_due_date=Sin fecha límite milestones.open=Abrir milestones.close=Cerrar diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 26fdf21ca7d98..8e0c8b509a2af 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -787,7 +787,6 @@ editor.file_deleting_no_longer_exists=فایل آماده حذف می‌شود ' editor.file_changed_while_editing=محتوای پرونده تغییر میکند از زمانی که شما شروع به ویرایش می‌کنید.اینجا کلیک کنید تا ببنید آن را یا یا کامیت تغییرات را دوباره اعمال کنید تا روی آن بازنویسی شود. editor.file_already_exists=فایلی با نام %s از قبل در مخزن موجود است. editor.commit_empty_file_header=کامیت کردن یک پرونده خالی -editor.commit_empty_file_text=فایل کامیت شده شما تقریبا خالیست. پردازش شود؟ editor.no_changes_to_show=تغییری برای نمایش وجود ندارد. editor.fail_to_update_file=خطا در ساخت/به‌روزرسانی پرونده %s. خطای رخ داده: %v editor.add_subdir=افزودن پوشه… diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 39c50d5b3a945..c94da8c7242da 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -796,7 +796,6 @@ editor.file_deleting_no_longer_exists=Le fichier en cours de suppression, '%s', editor.file_changed_while_editing=Le contenu du fichier a changé depuis que vous avez commencé à éditer. Cliquez ici pour voir les changements ou soumettez de nouveau pour les écraser. editor.file_already_exists=Un fichier nommé '%s' existe déjà dans ce dépôt. editor.commit_empty_file_header=Commiter un fichier vide -editor.commit_empty_file_text=Le fichier que vous souhaitez commiter est vide. Continuer ? editor.no_changes_to_show=Il n’y a aucun changement à afficher. editor.fail_to_update_file=Échec lors de la mise à jour/création du fichier '%s' avec l’erreur : %v editor.push_rejected_no_message=La modification a été rejetée par le serveur sans message. Veuillez vérifier les githooks. diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 41433ef11b13b..5402598c7204d 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -788,7 +788,6 @@ editor.file_deleting_no_longer_exists=Il file che si sta per eliminare, '%s', no editor.file_changed_while_editing=I contenuti di questo file hanno subito dei cambiamenti da quando hai iniziato la modifica. Clicca qui per visualizzarli o Committa nuovamente i Cambiamenti per sovrascriverli. editor.file_already_exists=Un file di nome '%s' esiste già in questo repository. editor.commit_empty_file_header=Commit di un file vuoto -editor.commit_empty_file_text=Il file di cui stai facendo un commit è vuoto. Procedere? editor.no_changes_to_show=Non ci sono cambiamenti da mostrare. editor.fail_to_update_file=Errore durante l'aggiornamento/ creazione del file '%s' con errore: %v editor.push_rejected_no_message=La modifica è stata rifiutata dal server senza un messaggio. Controlla githooks. diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 98ace8b0de7d0..5582915cf7ce6 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -832,7 +832,6 @@ editor.file_deleting_no_longer_exists=削除しようとしたファイル '%s' editor.file_changed_while_editing=あなたが編集を開始したあと、ファイルの内容が変更されました。 ここをクリックして何が変更されたか確認するか、もう一度"変更をコミット"をクリックして上書きします。 editor.file_already_exists=ファイル '%s' は、このリポジトリに既に存在します。 editor.commit_empty_file_header=空ファイルのコミット -editor.commit_empty_file_text=コミットしようとしているファイルは空です。 続行しますか? editor.no_changes_to_show=表示する変更箇所はありません。 editor.fail_to_update_file=ファイル '%s' を作成または変更できませんでした: %v editor.push_rejected_no_message=サーバーがメッセージを出さずに変更を拒否しました。 Gitフックを確認してください。 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 149ef34f4f068..700ac305c0d8f 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -815,7 +815,6 @@ editor.file_deleting_no_longer_exists=Fails '%s', ko dzēšat, vairs neeksistē editor.file_changed_while_editing=Faila saturs ir mainījies kopš sākāt to labot. Noklikšķiniet šeit, lai apskatītu, vai Nosūtiet izmaiņas atkārtoti, lai pārrakstītu. editor.file_already_exists=Fails ar nosaukumu '%s' šajā repozitorijā jau eksistē. editor.commit_empty_file_header=Iesūtīt tukšu failu -editor.commit_empty_file_text=Fails, ko vēlaties iesūtīt, ir tukšs. Vai turpināt? editor.no_changes_to_show=Nav izmaiņu, ko rādīt. editor.fail_to_update_file=Neizdevās izmainīt/izveidot failu '%s', kļūda: %v editor.push_rejected_no_message=Izmaiņu iesūtīšana tika noraidīta, bet serveris neatgrieza paziņojumu. Pārbaudiet git āķus šim repozitorijam. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 428463a2a99df..2fd26659447d7 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -802,7 +802,6 @@ editor.file_deleting_no_longer_exists=Het bestand dat wordt verwijderd, '%s', be editor.file_changed_while_editing=De bestandsinhoud is veranderd sinds je bent begonnen met bewerken. Klik hier om ze te zien, of commit de veranderingen opnieuw om ze te overschrijven. editor.file_already_exists=Een bestand met de naam '%s' bestaat al in deze repository. editor.commit_empty_file_header=Commit een leeg bestand -editor.commit_empty_file_text=Het bestand dat u wilt committen is leeg. Doorgaan? editor.no_changes_to_show=Er zijn geen wijzigingen om weer te geven. editor.fail_to_update_file=Update/maken van bestand '%s' is mislukt: %v editor.push_rejected_no_message=De wijziging is afgewezen door de server zonder bericht. Controleer githooks. @@ -1746,6 +1745,7 @@ users.local=Lokaal users.auth_login_name=Authenticatie-loginnaam users.update_profile_success=Het gebruikersaccount is bijgewerkt. users.edit_account=Wijzig gebruikers account +users.max_repo_creation = Maximum aantal repositories users.max_repo_creation_desc=(Zet op -1 om de globale limiet te gebruiken) users.is_activated=Gebruikersaccount is geactiveerd users.prohibit_login=Inloggen uitschakelen @@ -2101,4 +2101,3 @@ error.no_gpg_keys_found=Geen bekende sleutel gevonden voor deze handtekening in error.not_signed_commit=Geen ondertekende commit [units] - diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index ccfbdbb568184..75064b8df176e 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -802,7 +802,6 @@ editor.file_deleting_no_longer_exists=Usuwany plik '%s' już nie istnieje w tym editor.file_changed_while_editing=Zawartość pliku zmieniła się, odkąd rozpoczęto jego edycję. Kliknij tutaj, aby zobaczyć zmiany, lub ponownie Zatwierdź zmiany, aby je nadpisać. editor.file_already_exists=Plik o nazwie '%s' już istnieje w tym repozytorium. editor.commit_empty_file_header=Commituj pusty plik -editor.commit_empty_file_text=Plik, który zamierzasz commitować, jest pusty. Kontynuować? editor.no_changes_to_show=Brak zmian do pokazania. editor.fail_to_update_file=Tworzenie/aktualizacja pliku '%s' nie powiodła się z błędem: %v editor.push_rejected_no_message=Zmiana została odrzucona przez serwer bez wiadomości. Sprawdź githooki. diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index e99ee920191fa..83812f761a892 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -203,7 +203,7 @@ my_repos=Repositórios show_more_repos=Mostrar mais repositórios… collaborative_repos=Repositórios colaborativos my_orgs=Minhas organizações -my_mirrors=Meus espelhamentos +my_mirrors=Meus espelhos view_home=Ver %s search_repos=Encontre um repositório… filter=Outros filtros diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 1fd47b7fcfd11..42c09fdf28ca9 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -299,6 +299,8 @@ authorize_title=Autorizar o acesso de "%s" à sua conta? authorization_failed=A autorização falhou authorization_failed_desc=A autorização falhou porque encontrámos um pedido inválido. Entre em contacto com o responsável pela aplicação que tentou autorizar. sspi_auth_failed=Falhou a autenticação SSPI +password_pwned=A senha utilizada está numa lista de senhas roubadas anteriormente expostas em fugas de dados públicas. Tente novamente com uma senha diferente. +password_pwned_err=Não foi possível completar o pedido ao HaveIBeenPwned [mail] activate_account=Por favor, ponha a sua conta em funcionamento @@ -759,6 +761,7 @@ code=Código code.desc=Aceder ao código fonte, ficheiros, cometimentos e ramos. branch=Ramo tree=Árvore +clear_ref=`Apagar a referência vigente` filter_branch_and_tag=Filtrar ramo ou etiqueta branches=Ramos tags=Etiquetas @@ -771,7 +774,7 @@ org_labels_desc_manage=gerir milestones=Etapas commits=Cometimentos -commit=Cometer +commit=Cometimento releases=Lançamentos file_raw=Em bruto file_history=Histórico @@ -1026,6 +1029,7 @@ issues.poster=Autor issues.collaborator=Colaborador(a) issues.owner=Proprietário(a) issues.re_request_review=Voltar a solicitar revisão +issues.is_stale=Houve alterações neste pedido de integração posteriormente a esta revisão issues.remove_request_review=Remover solicitação de revisão issues.remove_request_review_block=Não é possível remover a solicitação de revisão issues.sign_in_require_desc=Inicie a sessão para participar neste diálogo. @@ -1239,6 +1243,7 @@ milestones.new=Nova etapa milestones.open_tab=%d abertas milestones.close_tab=%d fechadas milestones.closed=Encerrada %s +milestones.update_ago=Modificada há %s milestones.no_due_date=Sem data limite milestones.open=Abrir milestones.close=Fechar diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index c43790721c048..4c56207678d41 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -124,7 +124,7 @@ sqlite_helper=Путь к файлу базы данных SQLite3.
Введ err_empty_db_path=Путь к базе данных SQLite3 не может быть пустым. no_admin_and_disable_registration=Вы не можете отключить регистрацию до создания учётной записи администратора. err_empty_admin_password=Пароль администратора не может быть пустым. -err_empty_admin_email=Email администратора не может быть пустым. +err_empty_admin_email=Адрес электронной почты администратора не может быть пустым. err_admin_name_is_reserved=Неверное имя администратора, это имя зарезервировано err_admin_name_pattern_not_allowed=Неверное имя администратора, имя попадает под зарезервированный шаблон err_admin_name_is_invalid=Неверное имя администратора @@ -150,10 +150,10 @@ log_root_path=Путь к журналу log_root_path_helper=Файлы журнала будут записываться в этот каталог. optional_title=Расширенные настройки -email_title=Настройки Email +email_title=Настройки электронной почты smtp_host=Узел SMTP -smtp_from=Отправлять Email от имени -smtp_from_helper=Адрес электронной почты, который будет использоваться Gitea. Введите обычный адрес электронной почты или используйте формат «Имя» . +smtp_from=Отправить эл. почту как +smtp_from_helper=Адрес электронной почты, который будет использоваться Gitea. Введите обычный адрес электронной почты или используйте формат "Имя" . mailer_user=SMTP логин mailer_password=SMTP пароль register_confirm=Требовать подтверждение по электронной почте для регистрации @@ -202,7 +202,7 @@ no_reply_address=Скрытый почтовый домен no_reply_address_helper=Доменное имя для пользователей со скрытым адресом электронной почты. Например, имя пользователя 'joe' будет зарегистрировано в Git как 'joe@noreply.example.org' если скрытый домен электронной почты установлен как 'noreply.example.org'. [home] -uname_holder=Имя пользователя / Email +uname_holder=Имя пользователя / Адрес эл. почты password_holder=Пароль switch_dashboard_context=Переключить контекст панели управления my_repos=Репозитории @@ -290,7 +290,7 @@ openid_register_title=Создать новый аккаунт openid_register_desc=Выбранный OpenID URI неизвестен. Свяжите с новой учетной записью здесь. openid_signin_desc=Введите свой OpenID URI. Например: https://anne.me, bob.openid.org.cn или gnusocial.net/carry. disable_forgot_password_mail=Восстановление аккаунта отключено. Пожалуйста, свяжитесь с администратором сайта. -email_domain_blacklisted=С данным email регистрация невозможна. +email_domain_blacklisted=С данным адресом электронной почты регистрация невозможна. authorize_application=Авторизация приложения authorize_redirect_notice=Вы будете перенаправлены на %s, если вы авторизуете это приложение. authorize_application_created_by=Это приложение было создано %s. @@ -299,6 +299,8 @@ authorize_title=Разрешить "%s" доступ к вашей учётно authorization_failed=Ошибка авторизации authorization_failed_desc=Ошибка авторизации, обнаружен неверный запрос. Пожалуйста, свяжитесь с автором приложения, которое вы пытались авторизовать. sspi_auth_failed=SSPI аутентификация не удалась +password_pwned=Выбранный вами пароль находится в списке украденных паролей ранее выставленных в публичных нарушениях данных. Повторите попытку с другим паролем. +password_pwned_err=Не удалось завершить запрос к HaveIBeenPwned [mail] activate_account=Пожалуйста активируйте свой аккаунт @@ -342,7 +344,7 @@ git_ref_name_error=` должно быть правильным ссылочны size_error=` должен быть размер %s.` min_size_error=«должен содержать по крайней мере %s символов.» max_size_error=` должен содержать максимум %s символов.` -email_error=«не является адресом электронной почты.» +email_error=`не является адресом электронной почты.` url_error=` не является допустимым URL-адресом.` include_error=` должен содержать '%s'.` glob_pattern_error=` неверный glob шаблон: %s.` @@ -460,7 +462,7 @@ change_password_success=Ваш пароль был изменён. С этого password_change_disabled=Нелокальные аккаунты не могут изменить пароль через Gitea. emails=Email адреса -manage_emails=Управление Email адресами +manage_emails=Управление адресами электронной почты manage_themes=Выберите тему по умолчанию manage_openid=Управление OpenID email_desc=Ваш основной адрес электронной почты будет использован для уведомлений и других операций. @@ -474,7 +476,7 @@ activations_pending=Ожидает активации delete_email=Удалить email_deletion=Удалить адрес электронной почты email_deletion_desc=Адрес электронной почты и вся связанная с ним информация будет удалена из вашего аккаунта. Коммиты, сделанные от имени этого адреса электронной почты, не будут изменены. Продолжить? -email_deletion_success=Ваш Email адрес был удален. +email_deletion_success=Ваш адрес электронной почты был удалён. theme_update_success=Тема была изменена. theme_update_error=Выбранная тема не существует. openid_deletion=Удалить OpenID URI @@ -625,7 +627,7 @@ delete_account_title=Удалить аккаунт delete_account_desc=Вы уверены, что хотите навсегда удалить этот аккаунт? email_notifications.enable=Включить почтовые уведомления -email_notifications.onmention=Только уведомлять по почте при упоминании +email_notifications.onmention=Посылать письмо на эл. почту только при упоминании email_notifications.disable=Отключить почтовые уведомления email_notifications.submit=Установить настройки электронной почты @@ -689,8 +691,8 @@ desc.archived=Архивировано template.items=Элементы шаблона template.git_content=Содержимое Git (Ветвь По Умолчанию) -template.git_hooks=Git хуки -template.git_hooks_tooltip=В настоящее время вы не можете изменить или удалить Git хуки после добавления. Выберите, только если вы доверяете репозиторию шаблона. +template.git_hooks=Git hook'и +template.git_hooks_tooltip=В настоящее время вы не можете изменить или удалить Git hook'и после добавления. Выберите, только если вы доверяете репозиторию шаблона. template.webhooks=Веб-хуки template.topics=Темы template.avatar=Аватар @@ -759,6 +761,7 @@ code=Код code.desc=Исходный код, файлы, коммиты и ветки. branch=ветка tree=Дерево +clear_ref=`Удалить текущую ссылку` filter_branch_and_tag=Фильтр по ветке или тегу branches=Ветки tags=Теги @@ -832,7 +835,7 @@ editor.file_deleting_no_longer_exists=Удаляемый файл '%s' боль editor.file_changed_while_editing=Содержимое файла изменилось с момента начала редактирования. Нажмите здесь, чтобы увидеть, что было изменено, или Зафиксировать изменения снова, чтобы заменить их. editor.file_already_exists=Файл с именем '%s' уже существует в репозитории. editor.commit_empty_file_header=Закоммитить пустой файл -editor.commit_empty_file_text=Файл, который вы собираетесь зафиксировать, пуст. Продолжать? +editor.commit_empty_file_text=Файл, который в коммите, пуст. Продолжить? editor.no_changes_to_show=Нет изменений. editor.fail_to_update_file=Не удалось обновить/создать файл «%s» из-за ошибки: %v editor.push_rejected_no_message=Сервер отклонил изменение без сообщения. Пожалуйста, проверьте githooks. @@ -1026,6 +1029,7 @@ issues.poster=Автор issues.collaborator=Соавтор issues.owner=Владелец issues.re_request_review=Повторить запрос на отзыв +issues.is_stale=Со времени этого обзора в этот PR были внесены некоторые изменения issues.remove_request_review=Удалить запрос на отзыв issues.remove_request_review_block=Невозможно удалить запрос на отзыв issues.sign_in_require_desc=Войдите, чтобы присоединиться к обсуждению. @@ -1175,7 +1179,7 @@ pulls.merged_title_desc=слито %[1]d коммит(ов) из %[2]s%s на %s %s` pulls.tab_conversation=Обсуждение pulls.tab_commits=Коммиты -pulls.tab_files=Измененные файлы +pulls.tab_files=Изменённые файлы pulls.reopen_to_merge=Пожалуйста, переоткройте этот Pull Request для выполнения слияния. pulls.cant_reopen_deleted_branch=Этот запрос на слияние не может быть открыт заново, потому что ветка была удалена. pulls.merged=Слито @@ -1236,9 +1240,10 @@ pulls.closed_at=`закрыл этот запрос на слияние %[2]s` milestones.new=Новый этап -milestones.open_tab=%d открыты +milestones.open_tab=%d открыто(ы) milestones.close_tab=%d закрыто(ы) milestones.closed=Закрыт %s +milestones.update_ago=Обновлено %s назад milestones.no_due_date=Срок не указан milestones.open=Открыть milestones.close=Закрыть @@ -1385,14 +1390,14 @@ settings.collaboration.write=Запись settings.collaboration.read=Просмотр settings.collaboration.owner=Владелец settings.collaboration.undefined=Не определено -settings.hooks=Автоматическое обновление -settings.githooks=Git хуки +settings.hooks=Веб-хуки +settings.githooks=Git Hook'и settings.basic_settings=Основные параметры settings.mirror_settings=Настройки зеркалирования settings.sync_mirror=Синхронизировать settings.mirror_sync_in_progress=Синхронизируются репозитории-зеркала. Подождите минуту и обновите страницу. settings.email_notifications.enable=Включить почтовые уведомления -settings.email_notifications.onmention=Посылать email только при упоминании +settings.email_notifications.onmention=Посылать письмо на эл. почту только при упоминании settings.email_notifications.disable=Отключить почтовые уведомления settings.email_notifications.submit=Установить настройки электронной почты settings.site=Сайт @@ -1481,26 +1486,26 @@ settings.search_team=Поиск команды… settings.change_team_permission_tip=Разрешение команды установлено на странице настройки команды и не может быть изменено для каждого репозитория settings.delete_team_tip=Эта команда имеет доступ ко всем репозиториям и не может быть удалена settings.remove_team_success=Доступ команды к репозиторию был удалён. -settings.add_webhook=Добавить Webhook -settings.add_webhook.invalid_channel_name=Имя канала не может быть пустым или состоять только из символа #. -settings.hooks_desc=Webhooks позволяют внешним службам получать уведомления при возникновении определенных событий на Gitea. При возникновении указанных событий мы отправим запрос POST на каждый заданный вами URL. Узнать больше можно в нашем руководстве по webhooks. -settings.webhook_deletion=Удалить Webhook +settings.add_webhook=Добавить Вебхук +settings.add_webhook.invalid_channel_name=Название канала вебхука не может быть пустым или состоять только из символа #. +settings.hooks_desc=Вебхуки позволяют внешним службам получать уведомления при возникновении определенных событий на Gitea. При возникновении указанных событий мы отправим запрос POST на каждый заданный вами URL. Узнать больше можно в нашем руководстве по вебхукам. +settings.webhook_deletion=Удалить вебхук settings.webhook_deletion_desc=Удаление этого веб-хука приведет к удалению всей связанной с ним информации, включая историю. Хотите продолжить? -settings.webhook_deletion_success=Webhook был удалён. +settings.webhook_deletion_success=Вебхук был удалён. settings.webhook.test_delivery=Проверить доставку settings.webhook.test_delivery_desc=Отправить тестовое событие для тестирования настройки веб-хука. -settings.webhook.test_delivery_success=Тест веб-хука была добавлен в очередь доставки. Это может занять несколько секунд, прежде чем он отобразится в истории доставки. +settings.webhook.test_delivery_success=Тест веб-хука был добавлен в очередь доставки. Это может занять несколько секунд, прежде чем он отобразится в истории доставки. settings.webhook.request=Запрос settings.webhook.response=Ответ settings.webhook.headers=Заголовки settings.webhook.payload=Содержимое settings.webhook.body=Тело ответа -settings.githooks_desc=Git-хуки предоставляются Git самим по себе, вы можете изменять файлы поддерживаемых хуков из списка ниже чтобы выполнять внешние операции. -settings.githook_edit_desc=Если хук не активен, будет подставлен пример содержимого. Пустое значение в этом поле приведет к отключению хука. +settings.githooks_desc=Git hook'и предоставляются Git самим по себе, вы можете изменять файлы поддерживаемых hook'ов из списка ниже чтобы выполнять внешние операции. +settings.githook_edit_desc=Если хук не активен, будет подставлен пример содержимого. Пустое значение в этом поле приведёт к отключению хука. settings.githook_name=Название Hook'a -settings.githook_content=Перехватить содержание +settings.githook_content=Содержание hook'а settings.update_githook=Обновить Hook -settings.add_webhook_desc=Gitea будет оправлять POST запросы на указанный URL адрес, с информацией о происходящих событиях. Подробности на странице инструкции по использованию webhooks. +settings.add_webhook_desc=Gitea будет оправлять POST запросы на указанный URL адрес, с информацией о происходящих событиях. Подробности на странице инструкции по использованию вебхуков. settings.payload_url=URL обработчика settings.http_method=Метод HTTP settings.content_type=Тип содержимого @@ -1510,7 +1515,7 @@ settings.slack_icon_url=URL иконки settings.discord_username=Имя пользователя settings.discord_icon_url=URL иконки settings.slack_color=Цвет -settings.event_desc=На какие события этот webhook должен срабатывать? +settings.event_desc=На какие события этот веб-хук должен срабатывать? settings.event_push_only=Просто push событие settings.event_send_everything=Все события settings.event_choose=Позвольте мне выбрать то, что нужно. @@ -1556,12 +1561,13 @@ settings.event_pull_request_sync_desc=Запрос на слияние синх settings.branch_filter=Фильтр веток settings.branch_filter_desc=Белый список ветвей для событий Push, создания ветвей и удаления ветвей, указанных в виде глобуса. Если пусто или *, сообщается о событиях для всех филиалов. Смотрите github.com/gobwas/glob документацию по синтаксису. Примеры: master, {master,release*}. settings.active=Активный -settings.active_helper=Информация о происходящих событиях будет отправляться на URL-адрес этого webhook'а. -settings.add_hook_success=Webhook был добавлен. +settings.active_helper=Информация о происходящих событиях будет отправляться на URL-адрес этого вебхука. +settings.add_hook_success=Вебхук был добавлен. +settings.update_webhook=Обновление вебхука settings.update_hook_success=Вебхук был обновлён. -settings.delete_webhook=Удалить Webhook +settings.delete_webhook=Удалить вебхук settings.recent_deliveries=Недавние рассылки -settings.hook_type=Тип перехватчика +settings.hook_type=Тип hook'а settings.add_slack_hook_desc=Добавить интеграцию с Slack в ваш репозиторий. settings.slack_token=Slack токен settings.slack_domain=Домен @@ -1569,9 +1575,9 @@ settings.slack_channel=Канал settings.add_discord_hook_desc=Добавить уведомления о событиях через Discord. settings.add_dingtalk_hook_desc=Добавить интеграцию с Dingtalk в ваш репозиторий. settings.add_telegram_hook_desc=Добавить интеграцию с Telegram в ваш репозиторий. -settings.add_matrix_hook_desc=Интеграция Matrix в ваш репозиторий. +settings.add_matrix_hook_desc=Добавить интеграцию Matrix в ваш репозиторий. settings.add_msteams_hook_desc=Добавить интеграцию с Microsoft Teams в ваш репозиторий. -settings.add_feishu_hook_desc=Интегрировать Feishu в ваш репозиторий. +settings.add_feishu_hook_desc=Добавить интеграцию Feishu в ваш репозиторий. settings.deploy_keys=Ключи развертывания settings.add_deploy_key=Добавить ключ развертывания settings.deploy_key_desc=Ключи развёртывания доступны только для чтения. Это не то же самое что и SSH-ключи аккаунта. @@ -1835,7 +1841,7 @@ settings.delete_prompt=Это действие БЕЗВОЗВРАТНО< settings.confirm_delete_account=Подтвердить удаление settings.delete_org_title=Удалить организацию settings.delete_org_desc=Эта организация будет безвозвратно удалена. Продолжить? -settings.hooks_desc=Добавьте webhooks, который будет вызываться для всех репозиториев под этой организации. +settings.hooks_desc=Добавьте вебхуки, которые будет вызываться для всех репозиториев под этой организации. settings.labels_desc=Добавьте метки, которые могут быть использованы в задачах для всех репозиториев этой организации. @@ -1902,7 +1908,7 @@ repositories=Репозитории hooks=Стандартные Веб-хуки systemhooks=Системные вебхуки authentication=Авторизация -emails=Email пользователей +emails=Адреса эл. почты пользователей config=Конфигурация notices=Системные уведомления monitor=Мониторинг @@ -1913,7 +1919,7 @@ total=Всего: %d dashboard.statistic=Статистика dashboard.operations=Операции dashboard.system_status=Статус системного монитора -dashboard.statistic_info=В базе данных Gitea записано %d пользователей, %d организаций, %d публичных ключей, %d репозиториев, %d подписок на репозитории, %d добавлений в избранное, %d действий, %d доступов, %d задач, %d комментариев, %d социальных учетных записей, %d подписок на пользователей, %d зеркал, %d релизов, %d источников входа, %d веб-хуков, %d этапов, %d меток, %d задач хуков, %d команд, %d задач по обновлению, %d присоединенных файлов. +dashboard.statistic_info=В базе данных Gitea записано %d пользователей, %d организаций, %d публичных ключей, %d репозиториев, %d подписок на репозитории, %d добавлений в избранное, %d действий, %d доступов, %d задач, %d комментариев, %d социальных учетных записей, %d подписок на пользователей, %d зеркал, %d релизов, %d источников входа, %d вебхуков, %d этапов, %d меток, %d задач hook'ов, %d команд, %d задач по обновлению, %d присоединённых файлов. dashboard.operation_name=Имя операции dashboard.operation_switch=Переключить dashboard.operation_run=Запуск @@ -1946,7 +1952,7 @@ dashboard.update_migration_poster_id=Обновить ID плакатов миг dashboard.git_gc_repos=Выполнить сборку мусора для всех репозиториев dashboard.resync_all_sshkeys=Обновить файл '.ssh/authorized_keys' с SSH ключами Gitea. dashboard.resync_all_sshkeys.desc=(Не требуется для встроенного SSH сервера.) -dashboard.resync_all_hooks=Повторная синхронизация хуков pre-receive, update и post-receive во всех репозиториях. +dashboard.resync_all_hooks=Повторная синхронизация hook'ов pre-receive, update и post-receive во всех репозиториях. dashboard.reinit_missing_repos=Переинициализировать все отсутствующие Git репозитории, для которых существуют записи dashboard.sync_external_users=Синхронизировать данные внешних пользователей dashboard.server_uptime=Время непрерывной работы сервера @@ -2006,8 +2012,8 @@ users.is_activated=Эта учетная запись активирована users.prohibit_login=Этой учетной записи запрещён вход в систему users.is_admin=У этой учетной записи есть права администратора users.is_restricted=Ограничен -users.allow_git_hook=Эта учетная запись имеет разрешение на создание Git-хуков -users.allow_git_hook_tooltip=Git Hooks выполняются от пользователь Gitea операционной системы и будет иметь одинаковый уровень доступа к хосту +users.allow_git_hook=Эта учётная запись имеет разрешение на создание Git hook'ов +users.allow_git_hook_tooltip=Hook'и Git выполняются от имени пользователя ОС, который запустил Gitea, и имеют тот же уровень доступа к хосту users.allow_import_local=Пользователь имеет право импортировать локальные репозитории users.allow_create_organization=Эта учетная запись имеет разрешения на создание организаций users.update_profile=Обновить профиль учетной записи @@ -2016,11 +2022,11 @@ users.still_own_repo=На вашем аккаунте все еще остает users.still_has_org=Эта учетная запись все еще является членом одной или более организаций. Для продолжения, покиньте или удалите организации. users.deletion_success=Учётная запись успешно удалена. -emails.email_manage_panel=Управление Email пользователя +emails.email_manage_panel=Управление эл. почтой пользователя emails.primary=Первичный emails.activated=Активирован emails.filter_sort.email=Эл. почта -emails.filter_sort.email_reverse=Email (обратный) +emails.filter_sort.email_reverse=Эл. почта (обратный) emails.filter_sort.name=Имя пользователя emails.filter_sort.name_reverse=Имя пользователя (обратное) emails.updated=Email обновлён @@ -2045,11 +2051,11 @@ repos.forks=Форки repos.issues=Задачи repos.size=Размер -hooks.desc=Webhooks автоматически делает HTTP-POST запросы на сервер, когда вызываются определенные события Gitea. Webhooks, определенные здесь, по умолчанию и будут скопированы во все новые репозитории. Подробнее читайте в руководстве по webhooks. +hooks.desc=Вебхуки автоматически делают HTTP-POST запросы на сервер, когда вызываются определенные события Gitea. Вебхуки, определённые здесь, по умолчанию и будут скопированы во все новые репозитории. Подробнее читайте в руководстве по вебхукам. hooks.add_webhook=Добавить стандартный Веб-хук hooks.update_webhook=Обновить стандартный Веб-хук -systemhooks.desc=Webhooks автоматически делает HTTP-POST запросы на сервер, когда вызываются определенные события Gitea. Определенные вебхуки будут действовать на всех репозиториях системы, поэтому пожалуйста, учитывайте любые последствия для производительности. Подробнее читайте в руководстве по вебхукам. +systemhooks.desc=Вебхуки автоматически делают HTTP-POST запросы на сервер, когда вызываются определённые события Gitea. Определённые вебхуки будут действовать на всех репозиториях системы, поэтому пожалуйста, учитывайте любые последствия для производительности. Подробнее читайте в руководстве по вебхукам. systemhooks.add_webhook=Добавить системный вебхук systemhooks.update_webhook=Обновить системный вебхук @@ -2075,7 +2081,7 @@ auths.attribute_username=Атрибут Username auths.attribute_username_placeholder=Оставьте пустым, чтобы использовать имя пользователя для регистрации. auths.attribute_name=Атрибут First Name auths.attribute_surname=Атрибут Surname -auths.attribute_mail=Атрибут Email +auths.attribute_mail=Атрибут электронной почты auths.attribute_ssh_public_key=Атрибут Открытый SSH ключ auths.attributes_in_bind=Извлекать атрибуты в контексте Bind DN auths.allow_deactivate_all=Разрешить пустой результат поиска для отключения всех пользователей @@ -2129,7 +2135,7 @@ auths.tip.openid_connect=Используйте OpenID Connect Discovery URL (Klicka här för att se ändringarna eller commita ändringarna igen för att skriva över dem. editor.file_already_exists=En fil vid namn '%s' finns redan i denna utvecklingskatalog. editor.commit_empty_file_header=Committa en tom fil -editor.commit_empty_file_text=Filen du vill committa är tom. Vill du fortsätta? editor.no_changes_to_show=Det finns inga ändringar att visa. editor.fail_to_update_file=Uppdateringen/skapandet av filen '%s' misslyckades med felet: %v editor.push_rejected_no_message=Ändringarna avvisades av servern utan något meddelande. Kontrollera githookarna. diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 84b50b4125900..b4d4b73d4cb80 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -371,6 +371,7 @@ enterred_invalid_owner_name=Yeni sahip ismi hatalı. enterred_invalid_password=Girdiğiniz parola hatalı. user_not_exist=Böyle bir kullanıcı yok. team_not_exist=Böyle bir takım bulunmuyor. +last_org_owner=Son kullanıcıyı 'sahipler' takımından çıkaramazsınız. Bir organizasyonun en az bir sahibi olmalıdır. cannot_add_org_to_team=Organizasyon, takım üyesi olarak eklenemez. invalid_ssh_key=SSH anahtarınız doğrulanamıyor: %s @@ -831,7 +832,7 @@ editor.file_deleting_no_longer_exists=Silinen '%s' dosyası bu depoda artık yer editor.file_changed_while_editing=Düzenlemeye başladığınızdan beri dosya içeriği değişti. Görmek için burayı tıklayın veya üzerine yazmak için değişiklikleri yine de işleyin. editor.file_already_exists=Bu depoda '%s' isimli bir dosya zaten mevcut. editor.commit_empty_file_header=Boş bir dosya işle -editor.commit_empty_file_text=İşleme yaptığınız dosya boş. Devam edilsin mi? +editor.commit_empty_file_text=İşlemek üzere olduğunuz dosya boş. Devam edilsin mi? editor.no_changes_to_show=Gösterilecek değişiklik yok. editor.fail_to_update_file=Şu hata ile '%s' dosyasını güncelleme/oluşturma başarısız oldu: %v editor.push_rejected_no_message=Değişiklik, bir ileti olmadan sunucu tarafından reddedildi. Githooks'u kontrol edin. @@ -1025,6 +1026,7 @@ issues.poster=Poster issues.collaborator=Katkıcı issues.owner=Sahibi issues.re_request_review=İncelemeyi yeniden iste +issues.is_stale=Bu incelemeden bu yana bu istekte değişiklikler oldu issues.remove_request_review=İnceleme isteğini kaldır issues.remove_request_review_block=İnceleme isteği kaldırılamadı issues.sign_in_require_desc=Bu konuşmaya katılmak için oturum aç. @@ -1238,6 +1240,7 @@ milestones.new=Yeni Kilometre Taşı milestones.open_tab=%d Açık milestones.close_tab=%d Kapalı milestones.closed=Kapalı %s +milestones.update_ago=%s önce güncellendi milestones.no_due_date=Bitiş tarihi yok milestones.open=Aç milestones.close=Kapat diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index f24947e751cf7..b774f48bf2b84 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -786,7 +786,6 @@ editor.file_deleting_no_longer_exists=Видалений файл '%s' біль editor.file_changed_while_editing=Зміст файлу змінився з моменту початку редагування. Натисніть тут , щоб переглянути що було змінено, або закомітьте зміни ще раз, щоб переписати їх. editor.file_already_exists=Файл з назвою "%s" уже існує у цьому репозиторію. editor.commit_empty_file_header=Закомітити порожній файл -editor.commit_empty_file_text=Файл, який ви збираєтеся закомітити, порожній. Продовжити? editor.no_changes_to_show=Нема змін для показу. editor.fail_to_update_file=Не вдалося оновити/створити файл '%s' через помилку: %v editor.push_rejected_no_message=Зміна була відхилена сервером без повідомлення. Будь ласка, перевірте git-хуки. diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 7cd269173d6e2..aedfaa7a71837 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -991,7 +991,7 @@ issues.action_assignee_no_select=未指派 issues.opened_by=由 %[3]s 于 %[1]s创建 pulls.merged_by=由 %[3]s 于 %[1]s 合并 pulls.merged_by_fake=由 %[2]s 于 %[1]s 合并 -issues.closed_by=按 %[3]s 关闭%[1]s +issues.closed_by=%[3]s 关闭于 %[1]s issues.opened_by_fake=由 %[2]s 于 %[1]s创建 issues.closed_by_fake=通过 %[2]s 关闭 %[1]s issues.previous=上一页 @@ -1026,6 +1026,7 @@ issues.poster=发布者 issues.collaborator=协作者 issues.owner=所有者 issues.re_request_review=再次请求审核 +issues.is_stale=此评审之后代码有更新 issues.remove_request_review=移除审核请求 issues.remove_request_review_block=无法移除审核请求 issues.sign_in_require_desc=登录 并参与到对话中。 @@ -1097,7 +1098,7 @@ issues.error_modifying_due_date=未能修改到期时间。 issues.error_removing_due_date=未能删除到期时间。 issues.push_commit_1=已于 %[2]s 推送了 %[1]d 提交 issues.push_commits_n=已于 %[2]s 推送了 %[1]d 提交 -issues.force_push_codes="强制从 %[2]s 推送 %[1]s 到 %[4]s %[6]s" +issues.force_push_codes=`强制从 %[2]s 推送 %[1]s 到 %[4]s %[6]s` issues.due_date_form=yyyy年mm月dd日 issues.due_date_form_add=添加到期时间 issues.due_date_form_edit=编辑 @@ -1239,6 +1240,7 @@ milestones.new=新的里程碑 milestones.open_tab=%d 开启中 milestones.close_tab=%d 已关闭 milestones.closed=于 %s关闭 +milestones.update_ago=更新于 %s 前 milestones.no_due_date=暂无截止日期 milestones.open=开启中 milestones.close=关闭 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 27e720fc42ed2..1a05a4f162dc8 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1,5 +1,5 @@ home=首頁 -dashboard=儀表板 +dashboard=資訊主頁 explore=探索 help=說明 sign_in=登入 @@ -14,10 +14,10 @@ powered_by=技術提供: %s page=頁面 template=樣板 language=語言 -notifications=訊息 +notifications=通知 create_new=建立... -user_profile_and_more=設定檔和設置... -signed_in_as=已登入用戶 +user_profile_and_more=個人資料和設定... +signed_in_as=已登入 enable_javascript=本網站在啟用 JavaScript 的情況下可以運作的更好。 licenses=授權條款 return_to_gitea=返回 Gitea @@ -25,10 +25,11 @@ return_to_gitea=返回 Gitea username=用戶名稱 email=電子郵件地址 password=密碼 +access_token=Access Token re_type=再次輸入密碼 captcha=驗證碼 twofa=兩步驟驗證 -twofa_scratch=兩步驟驗證備用碼 +twofa_scratch=兩步驟驗證備用驗證碼 passcode=驗證碼 u2f_insert_key=插入安全金鑰 @@ -40,6 +41,7 @@ u2f_unsupported_browser=你的瀏覽器不支援 U2F 安全金鑰。 u2f_error_1=發生未知錯誤,請再試一次。 u2f_error_2=請確認使用正確,加密的 (https://) URL。 u2f_error_3=伺服器無法執行您的請求。 +u2f_error_4=此請求不允許使用這個安全金鑰。請確保該金鑰尚未註冊。 u2f_error_5=在成功讀取金鑰之前已逾時,請重新載入以重試。 u2f_reload=重新載入 @@ -49,13 +51,15 @@ mirror=鏡像 new_repo=新增儲存庫 new_migrate=遷移外部儲存庫 new_mirror=新鏡像 -new_fork=Fork 新的儲存庫 +new_fork=新增儲存庫 fork new_org=新增組織 +new_project=新增專案 +new_project_board=新增專案看板 manage_org=管理組織 admin_panel=網站管理 account_settings=帳號設定 settings=設定 -your_profile=個人訊息 +your_profile=個人資料 your_starred=標記星號 your_settings=設定 @@ -63,7 +67,7 @@ all=所有 sources=來源 mirrors=鏡像 collaborative=協同者 -forks=複製列表 +forks=Fork activities=活動 pull_requests=合併請求 @@ -79,8 +83,17 @@ loading=載入中… [error] occurred=發生錯誤 +report_message=如果你確定這是一個 Gitea 的 bug,請去 GitHub 搜尋相關的問題,如果有需要你也可以開一個新的問題 [startpage] +install=安裝容易 +install_desc=簡單地用執行檔來架設平台,或是使用 Docker,你也可以從套件管理員安裝。 +platform=跨平台 +platform_desc=Gitea 可以在所有能編譯 Go 語言的平台上執行: Windows, macOS, Linux, ARM 等等。挑一個您喜歡的吧! +lightweight=輕量級 +lightweight_desc=一片便宜的 Raspberry Pi 就可以滿足 Gitea 的最低需求。節省您的機器資源! +license=開放原始碼 +license_desc=取得 code.gitea.io/gitea !成為一名貢獻者和我們一起讓 Gitea 更好,快點加入我們吧! [install] install=安裝頁面 @@ -134,7 +147,7 @@ mailer_user=SMTP 帳號 mailer_password=SMTP 密碼 register_confirm=要求註冊時確認電子郵件 mail_notify=啟用郵件通知 -server_service_title=伺服器和其他服務設定 +server_service_title=伺服器和第三方服務設定 offline_mode=啟用本地模式 offline_mode_popup=停用其他服務並在本地提供所有資源。 disable_gravatar=停用 Gravatar @@ -177,7 +190,7 @@ no_reply_address=隱藏電子郵件域名 [home] uname_holder=使用者名稱或電子郵件地址 password_holder=密碼 -switch_dashboard_context=切換儀表板帳號 +switch_dashboard_context=切換資訊主頁帳戶 my_repos=儲存庫 show_more_repos=顯示更多儲存庫... collaborative_repos=參與協作的儲存庫 @@ -187,9 +200,17 @@ view_home=訪問 %s search_repos=搜尋儲存庫... filter=其他篩選條件 +show_archived=已封存 +show_both_archived_unarchived=顯示已封存和未封存 +show_only_archived=只顯示已封存 +show_only_unarchived=只顯示未封存 +show_private=私有 +show_both_private_public=顯示公開和私有 +show_only_private=只顯示私有 +show_only_public=只顯示公開 -issues.in_your_repos=屬於該用戶儲存庫的 +issues.in_your_repos=在您的儲存庫中 [explore] repos=儲存庫 @@ -215,6 +236,7 @@ confirmation_mail_sent_prompt=一封新的確認郵件已發送至 %s。 must_change_password=更新您的密碼 allow_password_change=要求使用者更改密碼 (推薦) active_your_account=啟用您的帳戶 +account_activated=帳戶已啟用 prohibit_login=禁止登入 prohibit_login_desc=您的帳號被禁止登入,請聯繫網站管理員 resent_limit_prompt=抱歉,您請求發送驗證電子郵件太過頻繁,請等待 3 分鐘後再試一次。 @@ -225,11 +247,11 @@ send_reset_mail=發送帳號救援郵件 invalid_code=您的確認代碼無效或已過期。 non_local_account=非本機帳號無法透過 Gitea 的網頁介面更改密碼 verify=驗證 -scratch_code=備用碼 -use_scratch_code=使用備用碼 -twofa_scratch_used=你已經使用了你的備用碼。你將會被轉到兩步驟驗證設定頁面以便移除你已註冊設備或重新產生新的備用碼。 -twofa_passcode_incorrect=你的驗證碼不正確。如果您遺失設備,請使用您的備用碼登入。 -twofa_scratch_token_incorrect=您的備用碼不正確 +scratch_code=備用驗證碼 +use_scratch_code=使用備用驗證碼 +twofa_scratch_used=您已經用掉了備用驗證碼。您已被重新導向到兩步驟驗證設定頁面以便移除你已註冊設備或重新產生新的備用驗證碼。 +twofa_passcode_incorrect=你的驗證碼不正確。如果您遺失設備,請使用您的備用驗證碼登入。 +twofa_scratch_token_incorrect=您的備用驗證碼不正確 login_userpass=登入 login_openid=OpenID oauth_signup_tab=註冊新帳號 @@ -244,6 +266,12 @@ openid_connect_desc=所選的 OpenID URI 未知。在這裡連結一個新帳戶 openid_register_title=建立新帳戶 openid_register_desc=所選的 OpenID URI 未知。在這裡連結一個新帳戶。 openid_signin_desc=輸入您的 OpenID URI。例如: https://anne.me、bob.openid.org.cn 或 gnusocial.net/carry。 +disable_forgot_password_mail=已停用帳號救援功能。請與網站管理員聯絡。 +authorize_application=授權應用程式 +authorize_redirect_notice=如果您授權此應用程式,您將會被重新導向至 %s。 +authorize_application_created_by=此應用程式是由 %s 建立的。 +authorize_application_description=如果您允許,它將能夠讀取和修改您的所有帳戶資訊,包括私有儲存庫和組織。 +authorize_title=授權「%s」存取您的帳戶? [mail] activate_account=請啟用您的帳戶 @@ -305,6 +333,8 @@ enterred_invalid_repo_name=輸入的儲存庫名稱不正確。 enterred_invalid_owner_name=新的擁有者名稱無效。 enterred_invalid_password=輸入的密碼不正確。 user_not_exist=該用戶名不存在 +team_not_exist=團隊不存在 +last_org_owner=你不能從'擁有者'團隊中刪除最後一個使用者。每個組織中至少要有一個擁有者。 cannot_add_org_to_team=組織不能被新增為團隊成員。 invalid_ssh_key=無法驗證您的 SSH 密鑰:%s @@ -323,11 +353,13 @@ change_avatar=更改大頭貼... join_on=加入於 repositories=儲存庫列表 activity=公開活動 -followers=關註者 +followers=關注者 starred=已收藏 -following=關註中 +projects=專案 +following=關注中 follow=關注 unfollow=取消關注 +heatmap.loading=正在載入熱點圖... user_bio=個人簡介 form.name_reserved=用戶名 '%s' 是被保留的。 @@ -335,7 +367,7 @@ form.name_pattern_not_allowed=使用者名稱不可包含字元 '%s'。 form.name_chars_not_allowed=使用者名稱 '%s' 包含無效字元。 [settings] -profile=個人訊息 +profile=個人資料 account=帳號 password=修改密碼 security=安全性 @@ -352,19 +384,22 @@ organization=組織 uid=用戶 ID u2f=安全密鑰 -public_profile=公開訊息 +public_profile=公開的個人資料 +profile_desc=您的電子信箱將被用於通知提醒和其他操作。 full_name=自定義名稱 website=個人網站 location=所在地區 update_theme=更新佈景主題 -update_profile=更新訊息 -update_profile_success=您的個人資料已被更新 +update_profile=更新個人資料 +update_profile_success=已更新您的個人資料。 change_username=您的使用者名稱已更改。 change_username_prompt=注意:使用者名更改也會更改您的帳戶的 URL。 continue=繼續操作 cancel=取消操作 language=語言 ui=佈景主題 +keep_activity_private=在個人資料頁面隱藏最近活動 +keep_activity_private_popup=讓最近的活動只有你和管理員看得到 lookup_avatar_by_mail=以電子信箱查詢大頭貼 federated_avatar_lookup=Federated Avatar 查詢 @@ -377,7 +412,7 @@ uploaded_avatar_is_too_big=上傳的檔案大小超過了最大限制 update_avatar_success=您的大頭貼已更新 change_password=更新密碼 -old_password=當前密碼 +old_password=目前的密碼 new_password=新的密碼 retype_new_password=重新輸入新的密碼 password_incorrect=輸入的密碼不正確! @@ -388,10 +423,14 @@ emails=電子郵件地址 manage_emails=管理電子郵件地址 manage_themes=選擇預設佈景主題 manage_openid=管理 OpenID 位址 -email_desc=您的主要邮箱地址将被用于通知提醒和其它操作。 +email_desc=您的主要電子信箱將被用於通知提醒和其他操作。 theme_desc=這將是您在整個網站上的預設佈景主題。 primary=主要 +activated=已啟用 +requires_activation=需要啟動 primary_email=設為主要 +activate_email=寄出啟用信 +activations_pending=等待啟用中 delete_email=移除 email_deletion=移除電子郵件地址 email_deletion_desc=電子郵件地址和相關資訊將從您的帳戶中刪除,此電子郵件地址所提交的 Git 將保持不變,繼續執行? @@ -407,6 +446,7 @@ add_email=新增電子郵件地址 add_openid=新增 OpenID URI add_email_confirmation_sent=一封新的確認郵件已發送至 '%s',請檢查您的收件匣並在 %s 內確認您的電郵地址。 add_email_success=該電子郵件地址已添加。 +email_preference_set_success=已套用郵件偏好設定 add_openid_success=該 OpenID 已添加。 keep_email_private=隱藏電子郵件地址 keep_email_private_popup=您的電子郵件地址將對其他使用者隱藏。 @@ -438,21 +478,39 @@ can_read_info=讀取 can_write_info=寫入 key_state_desc=該金鑰在 7 天內被使用過 token_state_desc=此 token 在過去七天內曾經被使用過 -show_openid=在設定檔顯示 -hide_openid=從設定檔隱藏 +show_openid=在個人資料顯示 +hide_openid=從個人資料隱藏 ssh_disabled=已停用 SSH manage_social=管理關聯社交帳戶 unbind=解除連結 -manage_access_token=管理訪問權杖 +manage_access_token=管理 Access Token generate_new_token=生成新的令牌 +tokens_desc=這些 Token 透過 Gitea API 獲得存取你帳戶的權限。 +new_token_desc=使用 token 的應用程式擁有完全存取您帳戶的權限。 token_name=令牌名稱 generate_token=生成令牌 -delete_token=删除令牌 -access_token_deletion=刪除訪問權杖 - +delete_token=刪除 +access_token_deletion=刪除 Access Token +access_token_deletion_desc=刪除 token 後,使用此 token 的應用程式將無法再存取您的帳戶。繼續嗎? +delete_token_success=已刪除 token。使用此 token 的應用程式無法再存取您的帳戶。 + +manage_oauth2_applications=管理 OAuth2 應用程式 +edit_oauth2_application=編輯 OAuth2 應用程式 +oauth2_applications_desc=OAuth2 應用程式讓您的第三方應用程式安全地驗證此 Gitea 中的使用者。 +remove_oauth2_application=刪除 OAuth2 應用程式 +remove_oauth2_application_desc=刪除 OAuth2 應用程式將會撤銷所有已簽署的 access token 存取權。繼續嗎? +remove_oauth2_application_success=已刪除應用程式。 +create_oauth2_application=新增 OAuth2 應用程式 +create_oauth2_application_button=建立應用程式 +create_oauth2_application_success=您已成功新增一個 OAuth2 應用程式。 +update_oauth2_application_success=您已成功更新了 OAuth2 應用程式。 oauth2_application_name=應用程式名稱 +oauth2_select_type=適用哪種程式類別? +oauth2_type_web=Web (例如 Node.JS, Tomacat, Go) +oauth2_type_native=原生應用程式 (Mobile, Desktop, Browser) +oauth2_redirect_uri=重新導向 URI save_application=儲存 oauth2_client_id=客戶端 ID oauth2_client_secret=客戶端密鑰 @@ -460,22 +518,32 @@ oauth2_regenerate_secret=重新產生密鑰 oauth2_regenerate_secret_hint=遺失您的密鑰? oauth2_client_secret_hint=請備份您的祕鑰。祕鑰在您離開這個頁面後將不會再顯示。 oauth2_application_edit=編輯 +oauth2_application_create_description=OAuth2 應用程式讓您的第三方應用程式可以存取此 Gitea 上的帳戶。 +oauth2_application_remove_description=刪除 OAuth2 應用會拒絕它存取此 Gitea 上已授權的帳戶。繼續嗎? +authorized_oauth2_applications=已授權的 OAuth2 應用程式 +authorized_oauth2_applications_description=您已授權給這些第三方應用程式存取您個人 Gitea 帳戶。請對不再需要的應用程式撤銷存取權。 +revoke_oauth2_grant=撤銷存取權 +revoke_oauth2_grant_description=撤銷此第三方應用程式的存取權,此應用程式就無法再存取您的資料?您確定嗎? +revoke_oauth2_grant_success=您已成功撤銷存取權 twofa_is_enrolled=您的帳號已經啟用兩步驟驗證。 twofa_not_enrolled=您的帳號目前尚未啟用兩步驟驗證。 twofa_disable=停用兩步驟驗證 -twofa_scratch_token_regenerate=重新產生備用碼 +twofa_scratch_token_regenerate=重新產生備用驗證碼 +twofa_scratch_token_regenerated=您的備用驗證碼是 %s。請將它保存到一個安全的地方。 twofa_enroll=啟用兩步驟驗證 twofa_disable_note=如有需要,您可以停用兩步驟驗證。 twofa_disable_desc=關閉兩步驟驗證會使您的帳號安全性降低,繼續執行? -regenerate_scratch_token_desc=如果您遺失了臨時令牌或已經使用它登錄,您可以在此處重置。 +regenerate_scratch_token_desc=如果您遺失了備用驗證碼或已經使用它登入,您可以在此重新設定。 twofa_disabled=兩步驟驗證已經被關閉。 scan_this_image=使用您的授權應用程式來掃瞄圖片: or_enter_secret=或者輸入密碼: %s then_enter_passcode=然後輸入應用程序中顯示的驗證碼: passcode_invalid=無效的驗證碼,請重試。 +twofa_enrolled=您的帳戶已經啟用了兩步驟驗證。請將備用驗證碼 (%s) 保存到一個安全的地方,它只會顯示這麼一次! +u2f_require_twofa=您的帳戶必須啟用兩步驟驗證以使用安全金鑰。 u2f_register_key=新增安全密鑰 u2f_nickname=暱稱 u2f_press_button=按下安全密鑰上的密碼進行註冊。 @@ -483,7 +551,7 @@ u2f_delete_key=移除安全密鑰 u2f_delete_key_desc=如果刪除安全金鑰,將不能再使用它登入。確定要刪除嗎? manage_account_links=管理已連結的帳號 -manage_account_links_desc=這些外部帳號與您的 Gitea 帳號相關聯。 +manage_account_links_desc=這些外部帳戶已連結到您的 Gitea 帳戶。 account_links_not_available=目前沒有連結到您的 Gitea 帳號的外部帳號 remove_account_link=刪除連結的帳號 remove_account_link_success=已取消連結帳號。 @@ -491,12 +559,16 @@ remove_account_link_success=已取消連結帳號。 orgs_none=您尚未成為任一組織的成員。 repos_none=您不擁有任何存儲庫 -delete_account=刪除當前帳戶 +delete_account=刪除您的帳戶 delete_prompt=此動作將永久刪除您的帳號,而且無法復原。 confirm_delete_account=確認刪除帳戶 delete_account_title=刪除使用者帳號 delete_account_desc=您是否確定要永久刪除此帳號? +email_notifications.enable=啟用郵件通知 +email_notifications.onmention=只在被提到時傳送郵件通知 +email_notifications.disable=關閉郵件通知 +email_notifications.submit=套用郵件偏好設定 [repo] owner=擁有者 @@ -507,9 +579,14 @@ template=範本 template_select=選擇範本 template_helper=將儲存庫設為範本 template_description=儲存庫範本讓使用者可新增相同目錄結構、檔案以及設定的儲存庫。 -visibility=可見度 -fork_repo=複製儲存庫 -fork_from=複製自 +visibility=瀏覽權限 +visibility_description=只有組織擁有者或有權限的組織成員才能看到。 +visibility_helper=將儲存庫設為私有 +visibility_helper_forced=您的網站管理員強制新的存儲庫必需設定為私有。 +visibility_fork_helper=(修改本值將會影響所有 fork 儲存庫) +fork_repo=Fork 儲存庫 +fork_from=Fork 自 +fork_visibility_helper=無法更改 fork 儲存庫的瀏覽權限。 use_template=使用此範本 repo_desc=儲存庫描述 repo_lang=儲存庫語言 @@ -522,19 +599,26 @@ auto_init=初始化儲存庫(建立 .gitignore、授權條款和讀我檔案 create_repo=建立儲存庫 default_branch=默認分支 mirror_prune=裁減 -mirror_interval_invalid=鏡像周期無效 +mirror_interval_invalid=鏡像週期無效 mirror_address=由 URL 複製 mirror_last_synced=上次同步 watchers=關注者 stargazers=稱讚者 -forks=複製儲存庫 +forks=Fork pick_reaction=選擇你的表情反應 reactions_more=再多添加 %d個 language_other=其它 +desc.private=私有 +desc.public=公開 +desc.private_template=私有樣板 +desc.archived=已封存 +template.git_hooks=Git Hook +template.webhooks=Webhook template.avatar=大頭貼 +archive.title=此存儲庫已封存,您能瀏覽檔案及複製此存儲庫,但不能推送、建立問題及拉取請求。 form.reach_limit_of_creation=您已經達到了儲存庫 %d 的上限。 form.name_reserved=儲存庫名稱 '%s' 是預留的。 @@ -550,8 +634,9 @@ migrate.invalid_local_path=無效的本地路徑,該路徑不存在或不是 migrate.failed=遷移失敗:%v mirror_from=镜像来自 -forked_from=複製自 -fork_from_self=您無法複製您的儲存庫! +forked_from=fork 自 +fork_from_self=您無法 fork 已經擁有的儲存庫。 +fork_guest_user=登入並 fork 這個儲存庫。 copy_link=複製連結 copy_link_success=已複製連結 copy_link_error=請按下 ⌘-C 或 Ctrl-C 複製 @@ -560,7 +645,7 @@ unwatch=取消關注 watch=關注 unstar=取消收藏 star=收藏 -fork=複製 +fork=Fork download_archive=下载此儲存庫 no_desc=暫無描述 @@ -577,6 +662,7 @@ branches=分支列表 tags=標籤 issues=問題 pulls=合併請求 +project_board=專案 labels=標籤 milestones=里程碑 @@ -589,8 +675,11 @@ file_view_raw=查看原始文件 file_permalink=永久連結 file_too_large=檔案太大,無法顯示。 video_not_supported_in_browser=您的瀏覽器不支援使用 HTML5 播放影片。 +audio_not_supported_in_browser=您的瀏覽器不支援 HTML5 'audio' 標籤 stored_lfs=儲存到到 Git LFS commit_graph=提交線圖 +line=行 +lines=行 editor.new_file=新增文件 editor.upload_file=上傳文件 @@ -599,12 +688,14 @@ editor.preview_changes=預覽更改 editor.cannot_edit_non_text_files=網站介面不能編輯二進位檔案 editor.edit_this_file=編輯文件 editor.must_be_on_a_branch=你必須在一個分支或提出對此檔的更改。 +editor.fork_before_edit=如果你想要對這個檔案進行或提出修改,請先 fork 這個儲存庫。 editor.delete_this_file=刪除檔案 editor.file_delete_success=文件 %s 已刪除。 editor.name_your_file=命名您的檔案... editor.or=或 editor.cancel_lower=取消 editor.commit_changes=提交更改嗎? +editor.add_tmpl=新增「」 editor.add=新增 '%s' editor.update=更新 '%s' editor.delete=刪除 '%s' @@ -613,10 +704,12 @@ editor.create_new_branch=建立 新的分支 為此提交和開 editor.new_branch_name_desc=新的分支名稱... editor.cancel=取消 editor.filename_cannot_be_empty=檔案名稱不能為空。 +editor.filename_is_invalid=檔名無效:%s editor.branch_already_exists='%s' 已存在於此存儲庫。 editor.no_changes_to_show=沒有可以顯示的變更。 editor.fail_to_update_file=上傳/建立檔案 '%s' 失敗, 錯誤訊息: %v editor.unable_to_upload_files=上傳檔案失敗到 '%s', 錯誤訊息: %v +editor.upload_file_is_locked=檔案「%s」已被 %s 鎖定 editor.upload_files_to_dir=上傳檔案到 '%s' commits.commits=次程式碼提交 @@ -633,25 +726,62 @@ commits.gpg_key_id=GPG 金鑰 ID ext_issues=外部問題 +projects=專案 +projects.desc=在專案看板中管理問題和 pull。 +projects.create=建立專案 +projects.title=標題 +projects.new=新增專案 +projects.create_success=已建立專案 '%s'。 +projects.deletion=刪除專案 +projects.deletion_desc=刪除專案會從所有相關的問題移除它。是否繼續? +projects.deletion_success=專案已被刪除。 +projects.edit=編輯專案 +projects.edit_subheader=專案可用來組織問題和追蹤進度。 +projects.modify=更新專案 +projects.edit_success=專案 '%s' 已被更新。 +projects.type.none=無 +projects.type.basic_kanban=基本看板 +projects.type.bug_triage=Bug 檢傷分類 +projects.template.desc=專案範本 +projects.template.desc_helper=選擇一個專案範本以開始 +projects.type.uncategorized=未分類 +projects.board.edit=編輯看板 +projects.board.edit_title=新看板名稱 +projects.board.new_title=新看板名稱 +projects.board.new_submit=送出 +projects.board.new=新增看板 +projects.board.delete=刪除看板 +projects.board.deletion_desc=刪除專案看板會將相關的問題移動到 '未分類'。是否繼續? +projects.open=開啟 +projects.close=關閉 issues.filter_milestones=篩選里程碑 issues.filter_projects=篩選專案 issues.filter_labels=篩選標籤 issues.filter_reviewers=篩選審查者 -issues.new=建立問題 +issues.new=新增問題 +issues.new.title_empty=標題不可為空 issues.new.labels=標籤 +issues.new.add_labels_title=套用標籤 issues.new.no_label=未選擇標籤 issues.new.clear_labels=清除已選取標籤 +issues.new.projects=專案 +issues.new.add_project_title=設定專案 +issues.new.clear_projects=清除已選取專案 +issues.new.no_projects=未選擇專案 +issues.new.open_projects=開放中的專案 +issues.new.closed_projects=已關閉的專案 issues.new.milestone=里程碑 +issues.new.add_milestone_title=設定里程碑 issues.new.no_milestone=未選擇里程碑 issues.new.clear_milestone=清除已選取里程碑 -issues.new.open_milestone=開啟中的里程碑 +issues.new.open_milestone=開放中的里程碑 issues.new.closed_milestone=已關閉的里程碑 issues.new.assignees=指派成員 issues.new.clear_assignees=取消指派成員 issues.no_ref=未指定分支或標籤 issues.create=建立問題 -issues.new_label=建立標籤 +issues.new_label=新增標籤 issues.new_label_placeholder=標籤名稱 issues.new_label_desc_placeholder=描述 issues.create_label=建立標籤 @@ -660,13 +790,17 @@ issues.label_templates.helper=選擇一個標籤集 issues.label_templates.use=使用標籤集 issues.label_templates.fail_to_load_file=載入標籤範本檔案 '%s' 失敗: %v issues.add_milestone_at=`新增至%s 里程碑 %s` +issues.add_project_at=`將此加入到 %s 專案 %s` issues.change_milestone_at=`%[3]s 修改了里程碑 %[1]s%[2]s` +issues.change_project_at=`將專案從 %s 修改為 %s %s` issues.remove_milestone_at=`從里程碑 %[2]s 刪除 %[1]s` -issues.deleted_milestone=`(已刪除)` +issues.remove_project_at=`將此從 %s 專案中移除 %s` +issues.deleted_milestone=`(已刪除)` +issues.deleted_project=`(已刪除)` issues.self_assign_at=將 %s 指派給自己 issues.add_assignee_at=`被%s %s指派` issues.delete_branch_at=`刪除分支 %s %s` -issues.open_tab=%d 個開啓中 +issues.open_tab=%d 個開放中 issues.close_tab=%d 個已關閉 issues.filter_label=標籤篩選 issues.filter_label_no_select=所有標籤 @@ -690,9 +824,9 @@ issues.filter_sort.nearduedate=截止日期由近到遠 issues.filter_sort.farduedate=截止日期由遠到近 issues.filter_sort.moststars=最多收藏 issues.filter_sort.feweststars=最少收藏 -issues.filter_sort.mostforks=最多複製 -issues.filter_sort.fewestforks=最少複製 -issues.action_open=開啟 +issues.filter_sort.mostforks=最多 fork +issues.filter_sort.fewestforks=最少 fork +issues.action_open=開放 issues.action_close=關閉 issues.action_label=標籤 issues.action_milestone=里程碑 @@ -703,7 +837,7 @@ issues.opened_by=由 %[3]s 於 %[1]s建立 issues.opened_by_fake=由 %[2]s 於 %[1]s建立 issues.previous=上一頁 issues.next=下一頁 -issues.open_title=開啟中 +issues.open_title=開放中 issues.closed_title=已關閉 issues.num_comments=%d 條評論 issues.commented_at=` 評論 %s` @@ -715,13 +849,21 @@ issues.context.delete=刪除 issues.no_content=尚未有任何內容 issues.close_issue=關閉 issues.close_comment_issue=評論並關閉 -issues.reopen_issue=重新開啟 -issues.reopen_comment_issue=重新開啟並評論 +issues.reopen_issue=重新開放 +issues.reopen_comment_issue=重新開放並評論 issues.create_comment=評論 -issues.commit_ref_at=`在代碼提交 %[2]s 中引用了該問題` +issues.closed_at=`關閉了這個問題 %[2]s` +issues.reopened_at=`重新開放了這個問題 %[2]s` +issues.commit_ref_at=`在代碼提交 %[2]s 中引用了這個問題` +issues.ref_issue_from=`引用了這個問題 %[4]s %[2]s` +issues.ref_pull_from=`引用了這個合併請求 %[4]s %[2]s` +issues.ref_closing_from=`引用了合併請求 %[4]s 將關閉這個問題 %[2]s` +issues.ref_reopening_from=`引用了合併請求 %[4]s 將重新開放這個問題 %[2]s` +issues.ref_closed_from=`關閉了這個問題 %[4]s %[2]s` +issues.ref_reopened_from=`重新開放了這個問題 %[4]s %[2]s` issues.poster=發佈者 -issues.collaborator=協同者 -issues.owner=所有者 +issues.collaborator=協作者 +issues.owner=擁有者 issues.sign_in_require_desc= 登入 才能加入這對話。 issues.edit=編輯 issues.cancel=取消 @@ -730,22 +872,31 @@ issues.label_title=標籤名稱 issues.label_description=標籤描述 issues.label_color=標籤顏色 issues.label_count=%d 個標籤 -issues.label_open_issues=%d 個開啓的問題 +issues.label_open_issues=%d 個開放中的問題 issues.label_edit=編輯 issues.label_delete=刪除 issues.label_modify=編輯標籤 issues.label_deletion=刪除標籤 issues.label_deletion_desc=刪除標籤會將其從所有問題中刪除,繼續? issues.label_deletion_success=標籤已刪除。 -issues.label.filter_sort.alphabetically=按字母顺序排序 +issues.label.filter_sort.alphabetically=按字母順序排序 issues.label.filter_sort.reverse_alphabetically=按字母反向排序 issues.label.filter_sort.by_size=檔案由小到大 issues.label.filter_sort.reverse_by_size=檔案由大到小 issues.num_participants=%d 參與者 -issues.attachment.open_tab=`在新的標籤頁中查看 '%s'` +issues.attachment.open_tab=`在新分頁中查看 '%s'` issues.attachment.download=`點擊下載 '%s'` issues.subscribe=訂閱 issues.unsubscribe=取消訂閱 +issues.lock=鎖定對話 +issues.unlock=解鎖對話 +issues.lock_duplicate=問題無法被鎖定兩次。 +issues.unlock_error=無法解鎖未被鎖定的問題。 +issues.unlock_comment=解鎖這個對話 %s +issues.lock_confirm=鎖定 +issues.unlock_confirm=解除鎖定 +issues.lock.reason=鎖定原因 +issues.comment_on_locked=您無法在已鎖定的問題上留言。 issues.tracker=時間追蹤 issues.start_tracking_short=開始 issues.start_tracking=開始時間追蹤 @@ -776,6 +927,7 @@ issues.due_date_added=已新增截止日期 %s %s issues.due_date_modified=已將截止日期修改為 %s ,原截止日期: %s %s issues.due_date_remove=已移除截止日期 %s %s issues.due_date_overdue=逾期 +issues.dependency.cancel=取消 pulls.new=建立合併請求 pulls.compare_changes=建立合併請求 @@ -796,18 +948,20 @@ pulls.merge_pull_request=合併請求 pulls.rebase_merge_pull_request=Rebase 合併 pulls.squash_merge_pull_request=Squash 合併 -milestones.new=新的里程碑 -milestones.open_tab=%d 開啟中 -milestones.close_tab=%d 已關閉 +milestones.new=新增里程碑 +milestones.open_tab=%d 個開放中 +milestones.close_tab=%d 個已關閉 milestones.closed=於 %s關閉 milestones.no_due_date=暫無截止日期 milestones.open=開啟 milestones.close=關閉 +milestones.new_subheader=里程碑可用來組織問題和追蹤進度。 milestones.create=建立里程碑 milestones.title=標題 milestones.desc=描述 milestones.due_date=截止日期(可選) milestones.clear=清除 +milestones.create_success=已建立里程碑 '%s'。 milestones.edit=編輯里程碑 milestones.cancel=取消 milestones.modify=更新里程碑 @@ -821,6 +975,7 @@ milestones.filter_sort.most_complete=完成度由高到低 milestones.filter_sort.most_issues=問題由多到少 milestones.filter_sort.least_issues=問題由少到多 +signing.wont_sign.not_signed_in=你還沒有登入 ext_wiki=外部 Wiki ext_wiki.desc=連結外部 Wiki。 @@ -875,9 +1030,9 @@ activity.title.issues_n=%d Issues activity.title.issues_closed_by=%[2]s 關閉了 %[1]s activity.title.issues_created_by=%[2]s 建立了 %[1]s activity.closed_issue_label=已關閉 -activity.new_issues_count_1=建立問題 -activity.new_issues_count_n=建立問題 -activity.new_issue_label=已開啟 +activity.new_issues_count_1=新增問題 +activity.new_issues_count_n=新增問題 +activity.new_issue_label=已開放 activity.title.unresolved_conv_1=%d 未解決的對話 activity.title.unresolved_conv_n=%d 未解決的對話 activity.unresolved_conv_desc=這些最近更改的問題和合併請求尚未解決。 @@ -891,13 +1046,14 @@ search=搜尋 search.search_repo=搜尋儲存庫 search.results=在 %s 中搜尋 "%s" 的结果 -settings=儲存庫設定 +settings=設定 settings.desc=設定是您可以管理儲存庫設定的地方 settings.options=儲存庫 settings.collaboration=協作者 settings.collaboration.admin=管理員 settings.collaboration.write=可寫權限 settings.collaboration.read=可讀權限 +settings.collaboration.owner=擁有者 settings.collaboration.undefined=未定義 settings.hooks=管理 Webhooks settings.githooks=管理 Git Hooks @@ -905,6 +1061,10 @@ settings.basic_settings=基本設定 settings.mirror_settings=鏡像設定 settings.sync_mirror=現在同步 settings.mirror_sync_in_progress=鏡像同步正在進行中。 請稍後再回來看看。 +settings.email_notifications.enable=啟用郵件通知 +settings.email_notifications.onmention=只在被提到時傳送郵件通知 +settings.email_notifications.disable=關閉郵件通知 +settings.email_notifications.submit=套用郵件偏好設定 settings.site=網站 settings.update_settings=更新儲存庫設定 settings.advanced_settings=進階設定 @@ -919,6 +1079,7 @@ settings.tracker_url_format=外部問題管理系統的 URL 格式 settings.tracker_issue_style.numeric=數字 settings.tracker_issue_style.alphanumeric=字母及數字 settings.enable_timetracker=啟用時間追蹤 +settings.projects_desc=啟用儲存庫專案 settings.admin_settings=管理員設定 settings.danger_zone=危險操作區 settings.new_owner_has_same_repo=新的儲存庫擁有者已經存在同名儲存庫! @@ -926,13 +1087,20 @@ settings.convert=轉換為普通儲存庫 settings.convert_desc=您可以將此鏡像轉成普通儲存庫。此動作不可恢復。 settings.convert_confirm=轉換儲存庫 settings.convert_succeed=鏡像儲存庫已成功轉換為一般儲存庫。 +settings.convert_fork=轉換成普通儲存庫 +settings.convert_fork_desc=您可以將此 fork 轉換成普通儲存庫。此動作不可還原。 +settings.convert_fork_notices_1=此操作會將此 fork 轉換成普通儲存庫。此動作不可還原。 +settings.convert_fork_confirm=轉換儲存庫 +settings.convert_fork_succeed=此 fork 已轉換成普通儲存庫。 settings.transfer=轉移儲存庫所有權 settings.wiki_delete=刪除 Wiki 資料 settings.confirm_wiki_delete=刪除 Wiki 資料 settings.wiki_deletion_success=已刪除儲存庫的 Wiki 資料。 settings.delete=刪除本儲存庫 settings.delete_notices_1=- 此操作 不可以 被回滾。 +settings.delete_notices_fork_1=- 在此儲存庫刪除後,它的 fork 將會變成獨立儲存庫。 settings.deletion_success=這個儲存庫已被刪除。 +settings.update_settings_success=已更新儲存庫的設定。 settings.transfer_owner=新擁有者 settings.confirm_delete=刪除儲存庫 settings.add_collaborator=增加協作者 @@ -969,7 +1137,8 @@ settings.event_choose=自訂事件... settings.event_create=建立 settings.event_create_desc=建立分支或標籤 settings.event_delete=刪除 -settings.event_fork=複製 +settings.event_fork=Fork +settings.event_fork_desc=儲存庫已被 fork。 settings.event_push=推送 settings.event_repository=儲存庫 settings.event_issues=問題 @@ -1002,6 +1171,8 @@ settings.add_protected_branch=啟用保護 settings.delete_protected_branch=停用保護 settings.choose_branch=選擇一個分支... settings.edit_protected_branch=編輯 +settings.matrix.access_token=Access Token +settings.matrix.message_type=訊息類型 settings.archive.button=封存儲存庫 settings.archive.header=封存本儲存庫 settings.archive.success=此儲存庫已被封存 @@ -1064,6 +1235,7 @@ branch.deletion_success=分支 '%s' 已被刪除。 branch.deletion_failed=刪除分支 '%s' 失敗。 branch.create_branch=建立分支 %s branch.create_from=從 '%s' +branch.create_success=已建立分支 '%s'。 branch.branch_already_exists=分支 '%s' 已存在此儲存庫 branch.deleted_by=刪除人: %s branch.restore_failed=還原分支 %s 失敗 @@ -1087,6 +1259,7 @@ org_desc=組織描述 team_name=團隊名稱 team_desc=團隊描述 team_permission_desc=權限 +team_unit_disabled=(已停用) form.name_reserved=組織名稱 '%s' 是被保留的。 form.name_pattern_not_allowed=儲存庫名稱無法使用 "%s"。 @@ -1097,13 +1270,18 @@ settings.options=組織 settings.full_name=組織全名 settings.website=官方網站 settings.location=所在地區 +settings.repoadminchangeteam=儲存庫管理者可增加與移除團隊權限 +settings.visibility=瀏覽權限 +settings.visibility.public=公開 +settings.visibility.limited=被限制的(只有登入的使用者才能看到) +settings.visibility.private=私有(只有組織成員才能看到) settings.update_settings=更新組織設定 settings.update_setting_success=組織設定已更新。 settings.change_orgname_prompt=注意:修改組織名稱將會同時修改對應的 URL。 settings.update_avatar_success=已更新組織的大頭貼。 settings.delete=刪除組織 -settings.delete_account=刪除當前組織 +settings.delete_account=刪除這個組織 settings.confirm_delete_account=確認刪除組織 settings.delete_org_title=刪除組織 settings.hooks_desc=新增 webhooks 將觸發在這個組織下 全部的儲存庫 。 @@ -1115,7 +1293,7 @@ members.public_helper=隱藏 members.private=隱藏 members.private_helper=顯示 members.member_role=成員角色: -members.owner=管理員 +members.owner=擁有者 members.member=普通成員 members.remove=移除成員 members.leave=離開組織 @@ -1131,6 +1309,7 @@ teams.write_access_helper=成員可以查看和推送到團隊儲存庫。 teams.admin_access=管理員權限 teams.no_desc=該團隊暫無描述 teams.settings=團隊設定 +teams.owners_permission_desc=擁有者對 所有儲存庫 具有完整權限,且對組織具有 管理員權限。 teams.members=團隊成員 teams.update_settings=更新團隊設定 teams.delete_team=刪除團隊 @@ -1142,12 +1321,15 @@ teams.search_repo_placeholder=搜尋儲存庫... teams.add_nonexistent_repo=您嘗試新增到團隊的儲存庫不存在,請先建立儲存庫! [admin] -dashboard=儀表板 +dashboard=資訊主頁 users=使用者帳號 -organizations=組織管理 -repositories=儲存庫管理 +organizations=組織 +repositories=儲存庫 +hooks=預設 Webhook +systemhooks=系統 Webhook authentication=認證來源 -config=應用設定管理 +emails=使用者電子信箱 +config=組態 notices=系統提示管理 monitor=應用監控面版 first_page=首頁 @@ -1157,46 +1339,47 @@ total=總計:%d dashboard.statistic=摘要 dashboard.operations=維護操作 dashboard.system_status=系統狀態 -dashboard.statistic_info=Gitea 資料庫統計:%d 位使用者,%d 個組織,%d 個公鑰,%d 個儲存庫,%d 個儲存庫關注,%d 個讚,%d 次行為,%d 條權限記錄,%d 個問題,%d 次評論,%d 個社交帳號,%d 個用戶關註,%d 個鏡像,%d 個版本發佈,%d 個登錄源,%d 個 Webhook ,%d 個里程碑,%d 個標籤,%d 個 Hook 任務,%d 個團隊,%d 個更新任務,%d 個附件。 +dashboard.statistic_info=Gitea 資料庫統計:%d 位使用者,%d 個組織,%d 個公鑰,%d 個儲存庫,%d 個儲存庫關注,%d 個讚,%d 次行為,%d 條權限記錄,%d 個問題,%d 次評論,%d 個社交帳號,%d 個用戶關注,%d 個鏡像,%d 個版本發佈,%d 個登錄源,%d 個 Webhook ,%d 個里程碑,%d 個標籤,%d 個 Hook 任務,%d 個團隊,%d 個更新任務,%d 個附件。 dashboard.operation_name=操作名稱 dashboard.operation_switch=開關 dashboard.operation_run=執行 dashboard.clean_unbind_oauth=清理未綁定OAuth的連結 dashboard.clean_unbind_oauth_success=所有未綁定 OAuth 的連結已刪除。 +dashboard.delete_inactive_accounts=刪除所有未啟用帳戶 dashboard.delete_repo_archives=刪除所有儲存庫存檔 dashboard.delete_missing_repos=刪除所有遺失 Git 檔案的儲存庫記錄 -dashboard.git_gc_repos=對儲存庫進行垃圾回收 +dashboard.git_gc_repos=對所有儲存庫進行垃圾回收 dashboard.reinit_missing_repos=重新初始化所有遺失具已存在記錄的Git 儲存庫 dashboard.sync_external_users=同步外部使用者資料 dashboard.server_uptime=服務執行時間 -dashboard.current_goroutine=當前 Goroutines 數量 -dashboard.current_memory_usage=當前內存使用量 -dashboard.total_memory_allocated=所有被分配的內存 -dashboard.memory_obtained=內存佔用量 +dashboard.current_goroutine=目前的 Goroutines 數量 +dashboard.current_memory_usage=目前記憶體使用量 +dashboard.total_memory_allocated=所有被分配的記憶體 +dashboard.memory_obtained=記憶體佔用量 dashboard.pointer_lookup_times=指針查找次數 dashboard.memory_allocate_times=記憶體分配次數 dashboard.memory_free_times=記憶體釋放次數 -dashboard.current_heap_usage=當前 Heap 內存使用量 -dashboard.heap_memory_obtained=Heap 內存佔用量 -dashboard.heap_memory_idle=Heap 內存空閒量 -dashboard.heap_memory_in_use=正在使用的 Heap 內存 -dashboard.heap_memory_released=被釋放的 Heap 內存 +dashboard.current_heap_usage=目前 Heap 記憶體使用量 +dashboard.heap_memory_obtained=Heap 記憶體佔用量 +dashboard.heap_memory_idle=Heap 記憶體閒置量 +dashboard.heap_memory_in_use=正在使用的 Heap 記憶體 +dashboard.heap_memory_released=被釋放的 Heap 記憶體 dashboard.heap_objects=Heap 對象數量 dashboard.bootstrap_stack_usage=啟動 Stack 使用量 -dashboard.stack_memory_obtained=被分配的 Stack 內存 -dashboard.mspan_structures_usage=MSpan 結構內存使用量 -dashboard.mspan_structures_obtained=被分配的 MSpan 結構內存 -dashboard.mcache_structures_usage=MCache 結構內存使用量 -dashboard.mcache_structures_obtained=被分配的 MCache 結構內存 -dashboard.profiling_bucket_hash_table_obtained=被分配的剖析哈希表內存 -dashboard.gc_metadata_obtained=被分配的垃圾收集元資料內存 -dashboard.other_system_allocation_obtained=其它被分配的系統內存 -dashboard.next_gc_recycle=下次垃圾收集內存回收量 -dashboard.last_gc_time=距離上次垃圾收集時間 -dashboard.total_gc_time=垃圾收集執行時間總量 -dashboard.total_gc_pause=垃圾收集暫停時間總量 -dashboard.last_gc_pause=上次垃圾收集暫停時間 -dashboard.gc_times=垃圾收集執行次數 +dashboard.stack_memory_obtained=被分配的 Stack 記憶體 +dashboard.mspan_structures_usage=MSpan 結構使用量 +dashboard.mspan_structures_obtained=被分配的 MSpan 結構 +dashboard.mcache_structures_usage=MCache 結構記使用量 +dashboard.mcache_structures_obtained=被分配的 MCache 結構 +dashboard.profiling_bucket_hash_table_obtained=被分配的剖析雜湊表 +dashboard.gc_metadata_obtained=被分配 GC Metadata +dashboard.other_system_allocation_obtained=其它被分配的系統記憶體 +dashboard.next_gc_recycle=下次 GC 記憶體回收量 +dashboard.last_gc_time=距離上次 GC 時間 +dashboard.total_gc_time=總 GC 暫停時間 +dashboard.total_gc_pause=總 GC 暫停時間 +dashboard.last_gc_pause=上次 GC 暫停時間 +dashboard.gc_times=GC 執行次數 users.user_manage_panel=帳號管理 users.new_account=建立新帳號 @@ -1213,6 +1396,7 @@ users.edit=編輯 users.auth_source=認證源 users.local=本地 users.password_helper=密碼留空則不修改。 +users.update_profile_success=已更新帳號。 users.edit_account=編輯帳號 users.max_repo_creation_desc=(設定 -1 使用全域預設限制) users.is_activated=該使用者帳號已被啟用 @@ -1225,10 +1409,14 @@ users.delete_account=刪除帳號 users.still_own_repo=這個使用者還擁有一個或更多的儲存庫。請先刪除或是轉移這些儲存庫。 users.deletion_success=使用者帳號已被刪除。 +emails.email_manage_panel=使用者電子信箱管理 +emails.primary=主要 +emails.activated=已啟用 emails.filter_sort.email=電子信箱 emails.filter_sort.email_reverse=電子信箱(倒序) emails.filter_sort.name=使用者名稱 emails.filter_sort.name_reverse=使用者名稱(倒序) +emails.duplicate_active=此信箱已被其他使用者使用 orgs.org_manage_panel=組織管理 orgs.name=組織名稱 @@ -1237,12 +1425,12 @@ orgs.members=成員數 orgs.new_orga=新增組織 repos.repo_manage_panel=儲存庫管理 -repos.owner=所有者 +repos.owner=擁有者 repos.name=儲存庫名稱 -repos.private=私有庫 -repos.watches=關註數 +repos.private=私有 +repos.watches=關注數 repos.stars=讚好數 -repos.forks=複製 +repos.forks=Fork 數 repos.issues=問題數 repos.size=大小 @@ -1282,13 +1470,13 @@ auths.enable_tls=啟用 TLS 加密 auths.skip_tls_verify=忽略 TLS 驗證 auths.pam_service_name=PAM 服務名稱 auths.oauth2_provider=OAuth2 提供者 -auths.oauth2_clientID=用戶端 ID (金鑰) -auths.oauth2_clientSecret=用戶端金鑰 +auths.oauth2_clientID=客戶端 ID (金鑰) +auths.oauth2_clientSecret=客戶端密鑰 auths.openIdConnectAutoDiscoveryURL=OpenID 連接自動探索 URL auths.oauth2_use_custom_url=使用自定義 URL 而不是預設 URL auths.oauth2_tokenURL=Token URL auths.oauth2_authURL=授權 URL -auths.oauth2_profileURL=個人訊息 URL +auths.oauth2_profileURL=個人資料 URL auths.oauth2_emailURL=電子郵件 URL auths.enable_auto_register=允許授權用戶自動註冊 auths.tips=幫助提示 @@ -1308,7 +1496,7 @@ auths.update=更新驗證來源 auths.delete=刪除驗證來源 auths.delete_auth_title=刪除認證來源 -config.server_config=伺服器設定 +config.server_config=伺服器組態 config.app_name=網站標題 config.app_ver=Gitea 版本 config.app_url=Gitea 基本 URL @@ -1325,7 +1513,7 @@ config.log_file_root_path=日誌路徑 config.script_type=腳本類型 config.reverse_auth_user=反向代理認證 -config.ssh_config=SSH 設定 +config.ssh_config=SSH 組態 config.ssh_enabled=已啟用 config.ssh_start_builtin_server=使用內建的伺服器 config.ssh_domain=伺服器域名 @@ -1337,8 +1525,9 @@ config.ssh_keygen_path=金鑰產生 (' ssh-keygen ') 路徑 config.ssh_minimum_key_size_check=金鑰最小大小檢查 config.ssh_minimum_key_sizes=金鑰最小大小 +config.lfs_config=LFS 組態 -config.db_config=資料庫設定 +config.db_config=資料庫組態 config.db_type=資料庫類型 config.db_host=主機地址 config.db_name=資料庫名稱 @@ -1346,7 +1535,7 @@ config.db_user=使用者名稱 config.db_ssl_mode=SSL config.db_path=資料庫路徑 -config.service_config=服務設定 +config.service_config=服務組態 config.register_email_confirm=要求註冊時確認電子郵件 config.disable_register=關閉註冊功能 config.enable_openid_signup=啟用 OpenID 註冊 @@ -1362,13 +1551,14 @@ config.default_allow_create_organization=預設允許新增組織 config.enable_timetracking=啟用時間追蹤 config.default_enable_timetracking=預設啟用時間追蹤 config.no_reply_address=隱藏電子郵件域名 +config.default_visibility_organization=新組織的預設瀏覽權限 -config.webhook_config=Webhook 設定 -config.queue_length=隊列長度 +config.webhook_config=Webhook 組態 +config.queue_length=佇列長度 config.deliver_timeout=推送超時 config.skip_tls_verify=略過 TLS 驗證 -config.mailer_config=SMTP 設定 +config.mailer_config=SMTP 組態 config.mailer_enabled=啟用服務 config.mailer_disable_helo=禁用 HELO 操作 config.mailer_name=發送者名稱 @@ -1381,30 +1571,30 @@ config.send_test_mail=傳送測試郵件 config.test_mail_failed=傳送測試郵件到 '%s' 時失敗:'%v" config.test_mail_sent=測試郵件已發送到 '%s' -config.oauth_config=社交帳號設定 +config.oauth_config=OAuth 組態 config.oauth_enabled=啟用服務 -config.cache_config=Cache 設定 +config.cache_config=Cache 組態 config.cache_adapter=Cache 適配器 -config.cache_interval=Cache 周期 +config.cache_interval=Cache 週期 config.cache_conn=Cache 連接字符串 -config.session_config=Session 設定 +config.session_config=Session 組態 config.session_provider=Session 提供者 config.provider_config=提供者設定 config.cookie_name=Cookie 名稱 config.enable_set_cookie=啟用設定 Cookie -config.gc_interval_time=垃圾收集周期 -config.session_life_time=Session 生命周期 +config.gc_interval_time=GC 週期 +config.session_life_time=Session 生命週期 config.https_only=僅限 HTTPS -config.cookie_life_time=Cookie 生命周期 +config.cookie_life_time=Cookie 生命週期 -config.picture_config=圖片和大頭貼設定 +config.picture_config=圖片和大頭貼組態 config.picture_service=圖片服務 config.disable_gravatar=停用 Gravatar config.enable_federated_avatar=啟用 Federated Avatars -config.git_config=Git 設定 +config.git_config=Git 組態 config.git_disable_diff_highlight=禁用比較語法高亮 config.git_max_diff_lines=Max Diff 線 (對於單個檔) config.git_max_diff_line_characters=最大比較的字元 (單行) @@ -1416,8 +1606,10 @@ config.git_clone_timeout=複製操作超時 config.git_pull_timeout=操作超時 config.git_gc_timeout=GC 操作超時 -config.log_config=日誌設定 +config.log_config=日誌組態 config.log_mode=日誌模式 +config.disabled_logger=已停用 +config.access_log_template=範本 monitor.cron=Cron 任務 monitor.name=任務名稱 @@ -1425,11 +1617,17 @@ monitor.schedule=任務安排 monitor.next=下次執行時間 monitor.previous=上次執行時間 monitor.execute_times=執行時間 -monitor.process=執行中進程 -monitor.desc=進程描述 +monitor.process=執行中的處理程序 +monitor.desc=描述 monitor.start=開始時間 monitor.execute_time=已執行時間 +monitor.queues=佇列 +monitor.queue=佇列: %s +monitor.queue.name=名稱 +monitor.queue.type=類型 +monitor.queue.settings.submit=更新設定 +monitor.queue.settings.changed=已更新設定 notices.system_notice_list=系統提示管理 @@ -1442,6 +1640,7 @@ notices.delete_selected=刪除選取項 notices.delete_all=刪除所有提示 notices.type=提示類型 notices.type_1=儲存庫 +notices.type_2=任務 notices.desc=描述 notices.op=操作 notices.delete_success=已刪除系統提示。 @@ -1451,13 +1650,14 @@ create_repo=建立了儲存庫 %s rename_repo=重新命名儲存庫 %[1]s%[3]s create_issue=`建立了問題 %s#%[2]s` close_issue=`已關閉問題 %s#%[2]s` -reopen_issue=`已重新開啟問題 %s#%[2]s` +reopen_issue=`已重新開放問題 %s#%[2]s` create_pull_request=`建立了合併請求 %s#%[2]s` close_pull_request=`已關閉合併請求 %s#%[2]s` reopen_pull_request=`已重新開啟合併請求 %s#%[2]s` comment_issue=`評論了問題 %s#%[2]s` merge_pull_request=`合併了合併請求 %s#%[2]s` transfer_repo=將儲存庫 %s 轉移至 %s +compare_branch=比較 compare_commits=比較 %d 提交 [tool] @@ -1489,7 +1689,7 @@ file_too_big=檔案大小({{filesize}} MB) 超過了最大允許大小({{maxFile remove_file=移除文件 [notification] -notifications=訊息 +notifications=通知 unread=未讀 read=已讀 no_unread=沒有未讀通知 diff --git a/public/img/svg/gitea-git.svg b/public/img/svg/gitea-git.svg new file mode 100644 index 0000000000000..c716b1b283e20 --- /dev/null +++ b/public/img/svg/gitea-git.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/svg/gitea-github.svg b/public/img/svg/gitea-github.svg new file mode 100644 index 0000000000000..0ed1d44b55927 --- /dev/null +++ b/public/img/svg/gitea-github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/svg/gitea-gitlab.svg b/public/img/svg/gitea-gitlab.svg new file mode 100644 index 0000000000000..a72a378234b8e --- /dev/null +++ b/public/img/svg/gitea-gitlab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/admin/admin.go b/routers/admin/admin.go index f43c1e69c592d..5dbc321e9db2e 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -243,7 +243,9 @@ func Config(ctx *context.Context) { ctx.Data["DisableRouterLog"] = setting.DisableRouterLog ctx.Data["RunUser"] = setting.RunUser ctx.Data["RunMode"] = strings.Title(macaron.Env) - ctx.Data["GitVersion"], _ = git.BinVersion() + if version, err := git.LocalVersion(); err == nil { + ctx.Data["GitVersion"] = version.Original() + } ctx.Data["RepoRootPath"] = setting.RepoRootPath ctx.Data["CustomRootPath"] = setting.CustomPath ctx.Data["StaticRootPath"] = setting.StaticRootPath diff --git a/routers/admin/users.go b/routers/admin/users.go index a28db2b445312..c1519b68f283c 100644 --- a/routers/admin/users.go +++ b/routers/admin/users.go @@ -108,6 +108,17 @@ func NewUserPost(ctx *context.Context, form auth.AdminCreateUserForm) { ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserNew, &form) return } + pwned, err := password.IsPwned(ctx.Req.Context(), form.Password) + if pwned { + ctx.Data["Err_Password"] = true + errMsg := ctx.Tr("auth.password_pwned") + if err != nil { + log.Error(err.Error()) + errMsg = ctx.Tr("auth.password_pwned_err") + } + ctx.RenderWithErr(errMsg, tplUserNew, &form) + return + } u.MustChangePassword = form.MustChangePassword } if err := models.CreateUser(u); err != nil { @@ -224,6 +235,17 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) { ctx.RenderWithErr(password.BuildComplexityError(ctx), tplUserEdit, &form) return } + pwned, err := password.IsPwned(ctx.Req.Context(), form.Password) + if pwned { + ctx.Data["Err_Password"] = true + errMsg := ctx.Tr("auth.password_pwned") + if err != nil { + log.Error(err.Error()) + errMsg = ctx.Tr("auth.password_pwned_err") + } + ctx.RenderWithErr(errMsg, tplUserNew, &form) + return + } if u.Salt, err = models.GetUserSalt(); err != nil { ctx.ServerError("UpdateUser", err) return @@ -237,6 +259,7 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) { u.Website = form.Website u.Location = form.Location u.MaxRepoCreation = form.MaxRepoCreation + u.MaxPrivateRepoCreation = form.MaxPrivateRepoCreation u.IsActive = form.Active u.IsAdmin = form.Admin u.IsRestricted = form.Restricted diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index a8a573eaf0759..dc095f3a1351a 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -87,6 +87,15 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) { ctx.Error(http.StatusBadRequest, "PasswordComplexity", err) return } + pwned, err := password.IsPwned(ctx.Req.Context(), form.Password) + if pwned { + if err != nil { + log.Error(err.Error()) + } + ctx.Data["Err_Password"] = true + ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned")) + return + } if err := models.CreateUser(u); err != nil { if models.IsErrUserAlreadyExist(err) || models.IsErrEmailAlreadyUsed(err) || @@ -151,7 +160,15 @@ func EditUser(ctx *context.APIContext, form api.EditUserOption) { ctx.Error(http.StatusBadRequest, "PasswordComplexity", err) return } - var err error + pwned, err := password.IsPwned(ctx.Req.Context(), form.Password) + if pwned { + if err != nil { + log.Error(err.Error()) + } + ctx.Data["Err_Password"] = true + ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned")) + return + } if u.Salt, err = models.GetUserSalt(); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateUser", err) return diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index ab7ef6d6f714a..5f472f3518820 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -521,6 +521,8 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/markdown/raw", misc.MarkdownRaw) m.Group("/settings", func() { m.Get("/ui", settings.GetGeneralUISettings) + m.Get("/api", settings.GetGeneralAPISettings) + m.Get("/attachment", settings.GetGeneralAttachmentSettings) m.Get("/repository", settings.GetGeneralRepoSettings) }) diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index a43fdb074e2e9..bc85fec630208 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -87,7 +87,7 @@ func GetArchive(ctx *context.APIContext) { // required: true // - name: archive // in: path - // description: archive to download, consisting of a git reference and archive + // description: the git reference for download with attached archive format (e.g. master.zip) // type: string // required: true // responses: diff --git a/routers/api/v1/settings/settings.go b/routers/api/v1/settings/settings.go index 8403a51d3eaa8..29210751d66fd 100644 --- a/routers/api/v1/settings/settings.go +++ b/routers/api/v1/settings/settings.go @@ -27,6 +27,24 @@ func GetGeneralUISettings(ctx *context.APIContext) { }) } +// GetGeneralAPISettings returns instance's global settings for api +func GetGeneralAPISettings(ctx *context.APIContext) { + // swagger:operation GET /settings/api settings getGeneralAPISettings + // --- + // summary: Get instance's global settings for api + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/GeneralAPISettings" + ctx.JSON(http.StatusOK, api.GeneralAPISettings{ + MaxResponseItems: setting.API.MaxResponseItems, + DefaultPagingNum: setting.API.DefaultPagingNum, + DefaultGitTreesPerPage: setting.API.DefaultGitTreesPerPage, + DefaultMaxBlobSize: setting.API.DefaultMaxBlobSize, + }) +} + // GetGeneralRepoSettings returns instance's global settings for repositories func GetGeneralRepoSettings(ctx *context.APIContext) { // swagger:operation GET /settings/repository settings getGeneralRepositorySettings @@ -42,3 +60,21 @@ func GetGeneralRepoSettings(ctx *context.APIContext) { HTTPGitDisabled: setting.Repository.DisableHTTPGit, }) } + +// GetGeneralAttachmentSettings returns instance's global settings for Attachment +func GetGeneralAttachmentSettings(ctx *context.APIContext) { + // swagger:operation GET /settings/Attachment settings getGeneralAttachmentSettings + // --- + // summary: Get instance's global settings for Attachment + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/GeneralAttachmentSettings" + ctx.JSON(http.StatusOK, api.GeneralAttachmentSettings{ + Enabled: setting.Attachment.Enabled, + AllowedTypes: setting.Attachment.AllowedTypes, + MaxFiles: setting.Attachment.MaxFiles, + MaxSize: setting.Attachment.MaxSize, + }) +} diff --git a/routers/api/v1/swagger/settings.go b/routers/api/v1/swagger/settings.go index 45266e51df6b3..4bf153cb9c523 100644 --- a/routers/api/v1/swagger/settings.go +++ b/routers/api/v1/swagger/settings.go @@ -19,3 +19,17 @@ type swaggerResponseGeneralUISettings struct { // in:body Body api.GeneralUISettings `json:"body"` } + +// GeneralAPISettings +// swagger:response GeneralAPISettings +type swaggerResponseGeneralAPISettings struct { + // in:body + Body api.GeneralAPISettings `json:"body"` +} + +// GeneralAttachmentSettings +// swagger:response GeneralAttachmentSettings +type swaggerResponseGeneralAttachmentSettings struct { + // in:body + Body api.GeneralAttachmentSettings `json:"body"` +} diff --git a/routers/repo/http.go b/routers/repo/http.go index d943cb2ae9d5e..0d5aa2e3e262e 100644 --- a/routers/repo/http.go +++ b/routers/repo/http.go @@ -268,6 +268,7 @@ func HTTP(ctx *context.Context) { models.EnvPusherName + "=" + authUser.Name, models.EnvPusherID + fmt.Sprintf("=%d", authUser.ID), models.EnvIsDeployKey + "=false", + models.EnvAppURL + "=" + setting.AppURL, } if !authUser.KeepEmailPrivate { diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 4bbc355027b57..4fc83719fd0d8 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -1079,8 +1079,10 @@ func ViewIssue(ctx *context.Context) { } } else if comment.Type == models.CommentTypeRemoveDependency || comment.Type == models.CommentTypeAddDependency { if err = comment.LoadDepIssueDetails(); err != nil { - ctx.ServerError("LoadDepIssueDetails", err) - return + if !models.IsErrIssueNotExist(err) { + ctx.ServerError("LoadDepIssueDetails", err) + return + } } } else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview { comment.RenderedContent = string(markdown.Render([]byte(comment.Content), ctx.Repo.RepoLink, @@ -1242,7 +1244,7 @@ func ViewIssue(ctx *context.Context) { ctx.Data["Participants"] = participants ctx.Data["NumParticipants"] = len(participants) ctx.Data["Issue"] = issue - ctx.Data["ReadOnly"] = true + ctx.Data["ReadOnly"] = false ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string) ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID) ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) @@ -1342,6 +1344,30 @@ func UpdateIssueTitle(ctx *context.Context) { }) } +// UpdateIssueRef change issue's ref (branch) +func UpdateIssueRef(ctx *context.Context) { + issue := GetActionIssue(ctx) + if ctx.Written() { + return + } + + if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull { + ctx.Error(403) + return + } + + ref := ctx.QueryTrim("ref") + + if err := issue_service.ChangeIssueRef(issue, ctx.User, ref); err != nil { + ctx.ServerError("ChangeRef", err) + return + } + + ctx.JSON(200, map[string]interface{}{ + "ref": ref, + }) +} + // UpdateIssueContent change issue's content func UpdateIssueContent(ctx *context.Context) { issue := GetActionIssue(ctx) diff --git a/routers/repo/lfs.go b/routers/repo/lfs.go index 8aff89dd6a3c0..481a3e57ad986 100644 --- a/routers/repo/lfs.go +++ b/routers/repo/lfs.go @@ -28,12 +28,12 @@ import ( "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" - "github.com/mcuadros/go-version" "github.com/unknwon/com" ) @@ -541,7 +541,7 @@ func LFSPointerFiles(ctx *context.Context) { return } ctx.Data["PageIsSettingsLFS"] = true - binVersion, err := git.BinVersion() + err := git.LoadGitVersion() if err != nil { log.Fatal("Error retrieving git version: %v", err) } @@ -586,7 +586,7 @@ func LFSPointerFiles(ctx *context.Context) { go createPointerResultsFromCatFileBatch(catFileBatchReader, &wg, pointerChan, ctx.Repo.Repository, ctx.User) go pipeline.CatFileBatch(shasToBatchReader, catFileBatchWriter, &wg, basePath) go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg) - if !version.Compare(binVersion, "2.6.0", ">=") { + if git.CheckGitVersionConstraint(">= 2.6.0") != nil { revListReader, revListWriter := io.Pipe() shasToCheckReader, shasToCheckWriter := io.Pipe() wg.Add(2) @@ -620,7 +620,7 @@ type pointerResult struct { func createPointerResultsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pointerChan chan<- pointerResult, repo *models.Repository, user *models.User) { defer wg.Done() defer catFileBatchReader.Close() - contentStore := lfs.ContentStore{BasePath: setting.LFS.ContentPath} + contentStore := lfs.ContentStore{ObjectStorage: storage.LFS} bufferedReader := bufio.NewReader(catFileBatchReader) buf := make([]byte, 1025) @@ -674,7 +674,11 @@ func createPointerResultsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg result.InRepo = true } - result.Exists = contentStore.Exists(pointer) + result.Exists, err = contentStore.Exists(pointer) + if err != nil { + _ = catFileBatchReader.CloseWithError(err) + break + } if result.Exists { if !result.InRepo { diff --git a/routers/repo/migrate.go b/routers/repo/migrate.go new file mode 100644 index 0000000000000..497f2ce36f840 --- /dev/null +++ b/routers/repo/migrate.go @@ -0,0 +1,173 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repo + +import ( + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/migrations" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/task" + "code.gitea.io/gitea/modules/util" +) + +const ( + tplMigrate base.TplName = "repo/migrate/migrate" +) + +// Migrate render migration of repository page +func Migrate(ctx *context.Context) { + ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...) + serviceType := ctx.QueryInt("service_type") + if serviceType == 0 { + ctx.HTML(200, tplMigrate) + return + } + + ctx.Data["Title"] = ctx.Tr("new_migrate") + ctx.Data["private"] = getRepoPrivate(ctx) + ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate + ctx.Data["DisableMirrors"] = setting.Repository.DisableMirrors + ctx.Data["mirror"] = ctx.Query("mirror") == "1" + ctx.Data["wiki"] = ctx.Query("wiki") == "1" + ctx.Data["milestones"] = ctx.Query("milestones") == "1" + ctx.Data["labels"] = ctx.Query("labels") == "1" + ctx.Data["issues"] = ctx.Query("issues") == "1" + ctx.Data["pull_requests"] = ctx.Query("pull_requests") == "1" + ctx.Data["releases"] = ctx.Query("releases") == "1" + ctx.Data["LFSActive"] = setting.LFS.StartServer + // Plain git should be first + ctx.Data["service"] = structs.GitServiceType(serviceType) + + ctxUser := checkContextUser(ctx, ctx.QueryInt64("org")) + if ctx.Written() { + return + } + ctx.Data["ContextUser"] = ctxUser + + ctx.HTML(200, base.TplName("repo/migrate/"+structs.GitServiceType(serviceType).Name())) +} + +func handleMigrateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form *auth.MigrateRepoForm) { + switch { + case migrations.IsRateLimitError(err): + ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form) + case migrations.IsTwoFactorAuthError(err): + ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form) + case models.IsErrReachLimitOfRepo(err): + ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form) + case models.IsErrRepoAlreadyExist(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) + case models.IsErrNameReserved(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form) + case models.IsErrNamePatternNotAllowed(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form) + default: + remoteAddr, _ := form.ParseRemoteAddr(owner) + err = util.URLSanitizedError(err, remoteAddr) + if strings.Contains(err.Error(), "Authentication failed") || + strings.Contains(err.Error(), "Bad credentials") || + strings.Contains(err.Error(), "could not read Username") { + ctx.Data["Err_Auth"] = true + ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form) + } else if strings.Contains(err.Error(), "fatal:") { + ctx.Data["Err_CloneAddr"] = true + ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form) + } else { + ctx.ServerError(name, err) + } + } +} + +// MigratePost response for migrating from external git repository +func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { + ctx.Data["Title"] = ctx.Tr("new_migrate") + // Plain git should be first + ctx.Data["service"] = form.Service + ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...) + + ctxUser := checkContextUser(ctx, form.UID) + if ctx.Written() { + return + } + ctx.Data["ContextUser"] = ctxUser + + if ctx.HasError() { + ctx.HTML(200, tplMigrate) + return + } + + remoteAddr, err := form.ParseRemoteAddr(ctx.User) + if err != nil { + if models.IsErrInvalidCloneAddr(err) { + ctx.Data["Err_CloneAddr"] = true + addrErr := err.(models.ErrInvalidCloneAddr) + switch { + case addrErr.IsURLError: + ctx.RenderWithErr(ctx.Tr("form.url_error"), tplMigrate, &form) + case addrErr.IsPermissionDenied: + ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplMigrate, &form) + case addrErr.IsInvalidPath: + ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplMigrate, &form) + default: + ctx.ServerError("Unknown error", err) + } + } else { + ctx.ServerError("ParseRemoteAddr", err) + } + return + } + + var opts = migrations.MigrateOptions{ + OriginalURL: form.CloneAddr, + GitServiceType: structs.GitServiceType(form.Service), + CloneAddr: remoteAddr, + RepoName: form.RepoName, + Description: form.Description, + Private: form.Private || setting.Repository.ForcePrivate, + Mirror: form.Mirror && !setting.Repository.DisableMirrors, + AuthUsername: form.AuthUsername, + AuthPassword: form.AuthPassword, + AuthToken: form.AuthToken, + Wiki: form.Wiki, + Issues: form.Issues, + Milestones: form.Milestones, + Labels: form.Labels, + Comments: form.Issues || form.PullRequests, + PullRequests: form.PullRequests, + Releases: form.Releases, + } + if opts.Mirror { + opts.Issues = false + opts.Milestones = false + opts.Labels = false + opts.Comments = false + opts.PullRequests = false + opts.Releases = false + } + + err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName) + if err != nil { + handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) + return + } + + err = task.MigrateRepository(ctx.User, ctxUser, opts) + if err == nil { + ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + opts.RepoName) + return + } + + handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) +} diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 5fc081a6f615d..603791d4df0ab 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -17,19 +17,14 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/migrations" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/task" - "code.gitea.io/gitea/modules/util" repo_service "code.gitea.io/gitea/services/repository" "github.com/unknwon/com" ) const ( - tplCreate base.TplName = "repo/create" - tplMigrate base.TplName = "repo/migrate" + tplCreate base.TplName = "repo/create" ) // MustBeNotEmpty render when a repo is a empty git dir @@ -151,8 +146,10 @@ func Create(ctx *context.Context) { } } - if !ctx.User.CanCreateRepo() { + if ctx.Data["private"] == false && !ctx.User.CanCreateRepo() { ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", ctx.User.MaxCreationLimit()), tplCreate, nil) + } else if ctx.Data["private"] == true && !ctx.User.CanCreatePrivateRepo() { + ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_private_creation", ctx.User.MaxPrivateCreationLimit()), tplCreate, nil) } else { ctx.HTML(200, tplCreate) } @@ -162,6 +159,8 @@ func handleCreateError(ctx *context.Context, owner *models.User, err error, name switch { case models.IsErrReachLimitOfRepo(err): ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form) + case models.IsErrReachLimitOfPrivateRepo(err): + ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_private_creation", owner.MaxPrivateCreationLimit()), tpl, form) case models.IsErrRepoAlreadyExist(err): ctx.Data["Err_RepoName"] = true ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) @@ -254,149 +253,6 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form) } -// Migrate render migration of repository page -func Migrate(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("new_migrate") - ctx.Data["private"] = getRepoPrivate(ctx) - ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate - ctx.Data["DisableMirrors"] = setting.Repository.DisableMirrors - ctx.Data["mirror"] = ctx.Query("mirror") == "1" - ctx.Data["wiki"] = ctx.Query("wiki") == "1" - ctx.Data["milestones"] = ctx.Query("milestones") == "1" - ctx.Data["labels"] = ctx.Query("labels") == "1" - ctx.Data["issues"] = ctx.Query("issues") == "1" - ctx.Data["pull_requests"] = ctx.Query("pull_requests") == "1" - ctx.Data["releases"] = ctx.Query("releases") == "1" - ctx.Data["LFSActive"] = setting.LFS.StartServer - // Plain git should be first - ctx.Data["service"] = structs.PlainGitService - ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...) - - ctxUser := checkContextUser(ctx, ctx.QueryInt64("org")) - if ctx.Written() { - return - } - ctx.Data["ContextUser"] = ctxUser - - ctx.HTML(200, tplMigrate) -} - -func handleMigrateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form *auth.MigrateRepoForm) { - switch { - case migrations.IsRateLimitError(err): - ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form) - case migrations.IsTwoFactorAuthError(err): - ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form) - case models.IsErrReachLimitOfRepo(err): - ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form) - case models.IsErrRepoAlreadyExist(err): - ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) - case models.IsErrNameReserved(err): - ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form) - case models.IsErrNamePatternNotAllowed(err): - ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form) - default: - remoteAddr, _ := form.ParseRemoteAddr(owner) - err = util.URLSanitizedError(err, remoteAddr) - if strings.Contains(err.Error(), "Authentication failed") || - strings.Contains(err.Error(), "Bad credentials") || - strings.Contains(err.Error(), "could not read Username") { - ctx.Data["Err_Auth"] = true - ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form) - } else if strings.Contains(err.Error(), "fatal:") { - ctx.Data["Err_CloneAddr"] = true - ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form) - } else { - ctx.ServerError(name, err) - } - } -} - -// MigratePost response for migrating from external git repository -func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { - ctx.Data["Title"] = ctx.Tr("new_migrate") - // Plain git should be first - ctx.Data["service"] = structs.PlainGitService - ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...) - - ctxUser := checkContextUser(ctx, form.UID) - if ctx.Written() { - return - } - ctx.Data["ContextUser"] = ctxUser - - if ctx.HasError() { - ctx.HTML(200, tplMigrate) - return - } - - remoteAddr, err := form.ParseRemoteAddr(ctx.User) - if err != nil { - if models.IsErrInvalidCloneAddr(err) { - ctx.Data["Err_CloneAddr"] = true - addrErr := err.(models.ErrInvalidCloneAddr) - switch { - case addrErr.IsURLError: - ctx.RenderWithErr(ctx.Tr("form.url_error"), tplMigrate, &form) - case addrErr.IsPermissionDenied: - ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplMigrate, &form) - case addrErr.IsInvalidPath: - ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplMigrate, &form) - default: - ctx.ServerError("Unknown error", err) - } - } else { - ctx.ServerError("ParseRemoteAddr", err) - } - return - } - - var opts = migrations.MigrateOptions{ - OriginalURL: form.CloneAddr, - GitServiceType: structs.GitServiceType(form.Service), - CloneAddr: remoteAddr, - RepoName: form.RepoName, - Description: form.Description, - Private: form.Private || setting.Repository.ForcePrivate, - Mirror: form.Mirror && !setting.Repository.DisableMirrors, - AuthUsername: form.AuthUsername, - AuthPassword: form.AuthPassword, - AuthToken: form.AuthToken, - Wiki: form.Wiki, - Issues: form.Issues, - Milestones: form.Milestones, - Labels: form.Labels, - Comments: true, - PullRequests: form.PullRequests, - Releases: form.Releases, - } - if opts.Mirror { - opts.Issues = false - opts.Milestones = false - opts.Labels = false - opts.Comments = false - opts.PullRequests = false - opts.Releases = false - } - - err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName) - if err != nil { - handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) - return - } - - err = task.MigrateRepository(ctx.User, ctxUser, opts) - if err == nil { - ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + opts.RepoName) - return - } - - handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) -} - // Action response for actions to a repository func Action(ctx *context.Context) { var err error diff --git a/routers/repo/view.go b/routers/repo/view.go index 3074ab7aaea08..3e2a57415db7d 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -34,7 +34,7 @@ const ( tplRepoHome base.TplName = "repo/home" tplWatchers base.TplName = "repo/watchers" tplForks base.TplName = "repo/forks" - tplMigrating base.TplName = "repo/migrating" + tplMigrating base.TplName = "repo/migrate/migrating" ) type namedBlob struct { @@ -365,6 +365,8 @@ func renderDirectory(ctx *context.Context, treeLink string) { ctx.Data["CanAddFile"] = !ctx.Repo.Repository.IsArchived ctx.Data["CanUploadFile"] = setting.Repository.Upload.Enabled && !ctx.Repo.Repository.IsArchived } + + ctx.Data["SSHDomain"] = setting.SSH.Domain } func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink string) { diff --git a/routers/routes/routes.go b/routers/routes/routes.go index bdb82db6f5042..779e8614b3b1c 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -733,6 +733,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/title", repo.UpdateIssueTitle) m.Post("/content", repo.UpdateIssueContent) m.Post("/watch", repo.IssueWatch) + m.Post("/ref", repo.UpdateIssueRef) m.Group("/dependency", func() { m.Post("/add", repo.AddDependency) m.Post("/delete", repo.RemoveDependency) diff --git a/routers/user/auth.go b/routers/user/auth.go index 4e6ac9c87f88b..96a73c9dd4632 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -1110,6 +1110,17 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo ctx.RenderWithErr(password.BuildComplexityError(ctx), tplSignUp, &form) return } + pwned, err := password.IsPwned(ctx.Req.Context(), form.Password) + if pwned { + errMsg := ctx.Tr("auth.password_pwned") + if err != nil { + log.Error(err.Error()) + errMsg = ctx.Tr("auth.password_pwned_err") + } + ctx.Data["Err_Password"] = true + ctx.RenderWithErr(errMsg, tplSignUp, &form) + return + } u := &models.User{ Name: form.UserName, @@ -1409,6 +1420,16 @@ func ResetPasswdPost(ctx *context.Context) { ctx.Data["Err_Password"] = true ctx.RenderWithErr(password.BuildComplexityError(ctx), tplResetPassword, nil) return + } else if pwned, err := password.IsPwned(ctx.Req.Context(), passwd); pwned || err != nil { + errMsg := ctx.Tr("auth.password_pwned") + if err != nil { + log.Error(err.Error()) + errMsg = ctx.Tr("auth.password_pwned_err") + } + ctx.Data["IsResetForm"] = true + ctx.Data["Err_Password"] = true + ctx.RenderWithErr(errMsg, tplResetPassword, nil) + return } // Handle two-factor @@ -1443,7 +1464,6 @@ func ResetPasswdPost(ctx *context.Context) { } } } - var err error if u.Rands, err = models.GetUserSalt(); err != nil { ctx.ServerError("UpdateUser", err) diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go index 27f0bf1c86b41..99e20177bc986 100644 --- a/routers/user/setting/account.go +++ b/routers/user/setting/account.go @@ -54,6 +54,13 @@ func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) { ctx.Flash.Error(ctx.Tr("form.password_not_match")) } else if !password.IsComplexEnough(form.Password) { ctx.Flash.Error(password.BuildComplexityError(ctx)) + } else if pwned, err := password.IsPwned(ctx.Req.Context(), form.Password); pwned || err != nil { + errMsg := ctx.Tr("auth.password_pwned") + if err != nil { + log.Error(err.Error()) + errMsg = ctx.Tr("auth.password_pwned_err") + } + ctx.Flash.Error(errMsg) } else { var err error if ctx.User.Salt, err = models.GetUserSalt(); err != nil { diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 538f613b04588..4cea5dd9a0b18 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -53,6 +53,7 @@ const ( DiffFileChange DiffFileDel DiffFileRename + DiffFileCopy ) // DiffLineExpandDirection represents the DiffLineSection expand direction @@ -481,7 +482,46 @@ func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*D } line := linebuf.String() - if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") || len(line) == 0 { + if strings.HasPrefix(line, "--- ") { + if line[4] == '"' { + fmt.Sscanf(line[4:], "%q", &curFile.OldName) + } else { + curFile.OldName = line[4:] + if strings.Contains(curFile.OldName, " ") { + // Git adds a terminal \t if there is a space in the name + curFile.OldName = curFile.OldName[:len(curFile.OldName)-1] + } + } + if curFile.OldName[0:2] == "a/" { + curFile.OldName = curFile.OldName[2:] + } + continue + } else if strings.HasPrefix(line, "+++ ") { + if line[4] == '"' { + fmt.Sscanf(line[4:], "%q", &curFile.Name) + } else { + curFile.Name = line[4:] + if strings.Contains(curFile.Name, " ") { + // Git adds a terminal \t if there is a space in the name + curFile.Name = curFile.Name[:len(curFile.Name)-1] + } + } + if curFile.Name[0:2] == "b/" { + curFile.Name = curFile.Name[2:] + } + curFile.IsRenamed = (curFile.Name != curFile.OldName) && !(curFile.IsCreated || curFile.IsDeleted) + if curFile.IsDeleted { + curFile.Name = curFile.OldName + curFile.OldName = "" + } else if curFile.IsCreated { + curFile.OldName = "" + } + continue + } else if len(line) == 0 { + continue + } + + if strings.HasPrefix(line, "+++") || strings.HasPrefix(line, "---") || len(line) == 0 { continue } @@ -569,36 +609,10 @@ func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*D break } - // Note: In case file name is surrounded by double quotes (it happens only in git-shell). - // e.g. diff --git "a/xxx" "b/xxx" - var a string - var b string - - rd := strings.NewReader(line[len(cmdDiffHead):]) - char, _ := rd.ReadByte() - _ = rd.UnreadByte() - if char == '"' { - fmt.Fscanf(rd, "%q ", &a) - } else { - fmt.Fscanf(rd, "%s ", &a) - } - char, _ = rd.ReadByte() - _ = rd.UnreadByte() - if char == '"' { - fmt.Fscanf(rd, "%q", &b) - } else { - fmt.Fscanf(rd, "%s", &b) - } - a = a[2:] - b = b[2:] - curFile = &DiffFile{ - Name: b, - OldName: a, - Index: len(diff.Files) + 1, - Type: DiffFileChange, - Sections: make([]*DiffSection, 0, 10), - IsRenamed: a != b, + Index: len(diff.Files) + 1, + Type: DiffFileChange, + Sections: make([]*DiffSection, 0, 10), } diff.Files = append(diff.Files, curFile) curFileLinesCount = 0 @@ -607,6 +621,7 @@ func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*D curFileLFSPrefix = false // Check file diff type and is submodule. + loop: for { line, err := input.ReadString('\n') if err != nil { @@ -617,23 +632,67 @@ func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*D } } - switch { - case strings.HasPrefix(line, "new file"): - curFile.Type = DiffFileAdd - curFile.IsCreated = true - case strings.HasPrefix(line, "deleted"): - curFile.Type = DiffFileDel - curFile.IsDeleted = true - case strings.HasPrefix(line, "index"): - curFile.Type = DiffFileChange - case strings.HasPrefix(line, "similarity index 100%"): - curFile.Type = DiffFileRename - } - if curFile.Type > 0 { - if strings.HasSuffix(line, " 160000\n") { - curFile.IsSubmodule = true + if curFile.Type != DiffFileRename { + switch { + case strings.HasPrefix(line, "new file"): + curFile.Type = DiffFileAdd + curFile.IsCreated = true + case strings.HasPrefix(line, "deleted"): + curFile.Type = DiffFileDel + curFile.IsDeleted = true + case strings.HasPrefix(line, "index"): + curFile.Type = DiffFileChange + case strings.HasPrefix(line, "similarity index 100%"): + curFile.Type = DiffFileRename + } + if curFile.Type > 0 && curFile.Type != DiffFileRename { + if strings.HasSuffix(line, " 160000\n") { + curFile.IsSubmodule = true + } + break + } + } else { + switch { + case strings.HasPrefix(line, "rename from "): + if line[12] == '"' { + fmt.Sscanf(line[12:], "%q", &curFile.OldName) + } else { + curFile.OldName = line[12:] + curFile.OldName = curFile.OldName[:len(curFile.OldName)-1] + } + case strings.HasPrefix(line, "rename to "): + if line[10] == '"' { + fmt.Sscanf(line[10:], "%q", &curFile.Name) + } else { + curFile.Name = line[10:] + curFile.Name = curFile.Name[:len(curFile.Name)-1] + } + curFile.IsRenamed = true + break loop + case strings.HasPrefix(line, "copy from "): + if line[10] == '"' { + fmt.Sscanf(line[10:], "%q", &curFile.OldName) + } else { + curFile.OldName = line[10:] + curFile.OldName = curFile.OldName[:len(curFile.OldName)-1] + } + case strings.HasPrefix(line, "copy to "): + if line[8] == '"' { + fmt.Sscanf(line[8:], "%q", &curFile.Name) + } else { + curFile.Name = line[8:] + curFile.Name = curFile.Name[:len(curFile.Name)-1] + } + curFile.IsRenamed = true + curFile.Type = DiffFileCopy + break loop + default: + if strings.HasSuffix(line, " 160000\n") { + curFile.IsSubmodule = true + } else { + break loop + } } - break } } } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 9826ece2dc5c9..ad1d186e732b0 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -6,6 +6,7 @@ package gitdiff import ( + "encoding/json" "fmt" "html/template" "strings" @@ -14,11 +15,9 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" - - "gopkg.in/ini.v1" - dmp "github.com/sergi/go-diff/diffmatchpatch" "github.com/stretchr/testify/assert" + "gopkg.in/ini.v1" ) func assertEqual(t *testing.T, s1 string, s2 template.HTML) { @@ -77,7 +76,145 @@ func TestDiffToHTML(t *testing.T) { }, DiffLineAdd)) } -func TestParsePatch(t *testing.T) { +func TestParsePatch_singlefile(t *testing.T) { + type testcase struct { + name string + gitdiff string + wantErr bool + addition int + deletion int + oldFilename string + filename string + } + + tests := []testcase{ + { + name: "readme.md2readme.md", + gitdiff: `diff --git "a/README.md" "b/README.md" +--- a/README.md ++++ b/README.md +@@ -1,3 +1,6 @@ + # gitea-github-migrator ++ ++ Build Status +- Latest Release + Docker Pulls ++ cut off ++ cut off +`, + addition: 4, + deletion: 1, + filename: "README.md", + }, + { + name: "A \\ B", + gitdiff: `diff --git "a/A \\ B" "b/A \\ B" +--- "a/A \\ B" ++++ "b/A \\ B" +@@ -1,3 +1,6 @@ + # gitea-github-migrator ++ ++ Build Status +- Latest Release + Docker Pulls ++ cut off ++ cut off`, + addition: 4, + deletion: 1, + filename: "A \\ B", + }, + { + name: "really weird filename", + gitdiff: `diff --git a/a b/file b/a a/file b/a b/file b/a a/file +index d2186f1..f5c8ed2 100644 +--- a/a b/file b/a a/file ++++ b/a b/file b/a a/file +@@ -1,3 +1,2 @@ + Create a weird file. + +-and what does diff do here? +\ No newline at end of file`, + addition: 0, + deletion: 1, + filename: "a b/file b/a a/file", + oldFilename: "a b/file b/a a/file", + }, + { + name: "delete file with blanks", + gitdiff: `diff --git a/file with blanks b/file with blanks +deleted file mode 100644 +index 898651a..0000000 +--- a/file with blanks ++++ /dev/null +@@ -1,5 +0,0 @@ +-a blank file +- +-has a couple o line +- +-the 5th line is the last +`, + addition: 0, + deletion: 5, + filename: "file with blanks", + }, + { + name: "rename a—as", + gitdiff: `diff --git "a/\360\243\220\265b\342\200\240vs" "b/a\342\200\224as" +similarity index 100% +rename from "\360\243\220\265b\342\200\240vs" +rename to "a\342\200\224as" +`, + addition: 0, + deletion: 0, + oldFilename: "𣐵b†vs", + filename: "a—as", + }, + { + name: "rename with spaces", + gitdiff: `diff --git a/a b/file b/a a/file b/a b/a a/file b/b file +similarity index 100% +rename from a b/file b/a a/file +rename to a b/a a/file b/b file +`, + oldFilename: "a b/file b/a a/file", + filename: "a b/a a/file b/b file", + }, + } + + for _, testcase := range tests { + t.Run(testcase.name, func(t *testing.T) { + got, err := ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(testcase.gitdiff)) + if (err != nil) != testcase.wantErr { + t.Errorf("ParsePatch() error = %v, wantErr %v", err, testcase.wantErr) + return + } + gotMarshaled, _ := json.MarshalIndent(got, " ", " ") + if got.NumFiles != 1 { + t.Errorf("ParsePath() did not receive 1 file:\n%s", string(gotMarshaled)) + return + } + if got.TotalAddition != testcase.addition { + t.Errorf("ParsePath() does not have correct totalAddition %d, wanted %d", got.TotalAddition, testcase.addition) + } + if got.TotalDeletion != testcase.deletion { + t.Errorf("ParsePath() did not have correct totalDeletion %d, wanted %d", got.TotalDeletion, testcase.deletion) + } + file := got.Files[0] + if file.Addition != testcase.addition { + t.Errorf("ParsePath() does not have correct file addition %d, wanted %d", file.Addition, testcase.addition) + } + if file.Deletion != testcase.deletion { + t.Errorf("ParsePath() did not have correct file deletion %d, wanted %d", file.Deletion, testcase.deletion) + } + if file.OldName != testcase.oldFilename { + t.Errorf("ParsePath() did not have correct OldName %s, wanted %s", file.OldName, testcase.oldFilename) + } + if file.Name != testcase.filename { + t.Errorf("ParsePath() did not have correct Name %s, wanted %s", file.Name, testcase.filename) + } + }) + } + var diff = `diff --git "a/README.md" "b/README.md" --- a/README.md +++ b/README.md diff --git a/services/issue/issue.go b/services/issue/issue.go index 64d69119b7622..0f90a2bcd05ab 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -42,6 +42,20 @@ func ChangeTitle(issue *models.Issue, doer *models.User, title string) (err erro return nil } +// ChangeIssueRef changes the branch of this issue, as the given user. +func ChangeIssueRef(issue *models.Issue, doer *models.User, ref string) error { + oldRef := issue.Ref + issue.Ref = ref + + if err := issue.ChangeRef(doer, oldRef); err != nil { + return err + } + + notification.NotifyIssueChangeRef(doer, issue, oldRef) + + return nil +} + // UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s) // Deleting is done the GitHub way (quote from their api documentation): // https://developer.github.com/v3/issues/#edit-an-issue diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go index 75054b690f869..da794ea585be0 100644 --- a/services/mirror/mirror.go +++ b/services/mirror/mirror.go @@ -23,7 +23,6 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - "github.com/mcuadros/go-version" "github.com/unknwon/com" ) @@ -43,11 +42,11 @@ func readAddress(m *models.Mirror) { func remoteAddress(repoPath string) (string, error) { var cmd *git.Command - binVersion, err := git.BinVersion() + err := git.LoadGitVersion() if err != nil { return "", err } - if version.Compare(binVersion, "2.7", ">=") { + if git.CheckGitVersionConstraint(">= 2.7") == nil { cmd = git.NewCommand("remote", "get-url", "origin") } else { cmd = git.NewCommand("config", "--get", "remote.origin.url") diff --git a/services/pull/merge.go b/services/pull/merge.go index 27689384a58aa..b430a9080e37d 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -25,8 +25,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" issue_service "code.gitea.io/gitea/services/issue" - - "github.com/mcuadros/go-version" ) // Merge merges pull request to base repository. @@ -113,9 +111,9 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor // rawMerge perform the merge operation without changing any pull information in database func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.MergeStyle, message string) (string, error) { - binVersion, err := git.BinVersion() + err := git.LoadGitVersion() if err != nil { - log.Error("git.BinVersion: %v", err) + log.Error("git.LoadGitVersion: %v", err) return "", fmt.Errorf("Unable to get git version: %v", err) } @@ -157,7 +155,7 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge } var gitConfigCommand func() *git.Command - if version.Compare(binVersion, "1.8.0", ">=") { + if git.CheckGitVersionConstraint(">= 1.8.0") == nil { gitConfigCommand = func() *git.Command { return git.NewCommand("config", "--local") } @@ -213,11 +211,11 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge // Determine if we should sign signArg := "" - if version.Compare(binVersion, "1.7.9", ">=") { + if git.CheckGitVersionConstraint(">= 1.7.9") == nil { sign, keyID, _ := pr.SignMerge(doer, tmpBasePath, "HEAD", trackingBranch) if sign { signArg = "-S" + keyID - } else if version.Compare(binVersion, "2.0.0", ">=") { + } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil { signArg = "--no-gpg-sign" } } diff --git a/services/repository/transfer_test.go b/services/repository/transfer_test.go index 9468e1ced2972..0750da8d50b0c 100644 --- a/services/repository/transfer_test.go +++ b/services/repository/transfer_test.go @@ -32,6 +32,8 @@ func TestTransferOwnership(t *testing.T) { doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) repo.Owner = models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User) + assert.EqualValues(t, 3, repo.OwnerID) + assert.NoError(t, TransferOwnership(doer, doer, repo, nil)) transferredRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 042c09954a2c3..55b133bbbc953 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -63,6 +63,12 @@

{{.i18n.Tr "admin.users.max_repo_creation_desc"}}

+
+ + +

{{.i18n.Tr "admin.users.max_private_repo_creation_desc"}}

+
+
diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index d6dd7d5c03969..63a4ea7d3c81d 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -27,6 +27,7 @@ {{.i18n.Tr "admin.users.restricted"}} {{.i18n.Tr "admin.users.2fa"}} {{.i18n.Tr "admin.users.repos"}} + {{.i18n.Tr "admin.users.privaterepos"}} {{.i18n.Tr "admin.users.created"}} {{.i18n.Tr "admin.users.last_login"}} @@ -46,6 +47,7 @@ {{.NumRepos}} + {{.NumPrivateRepos}} {{.CreatedUnix.FormatShort}} {{if .LastLoginUnix}} {{.LastLoginUnix.FormatShort}} diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index d2eedef49c94e..74025bd5304f1 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -95,9 +95,9 @@ {{if .LatestPullRequest.HasMerged}} {{svg "octicon-git-merge" 16}} {{$.i18n.Tr "repo.pulls.merged"}} {{else if .LatestPullRequest.Issue.IsClosed}} - {{svg "octicon-issue-closed" 16}} {{$.i18n.Tr "repo.issues.closed_title"}} + {{svg "octicon-git-pull-request" 16}} {{$.i18n.Tr "repo.issues.closed_title"}} {{else}} - {{svg "octicon-issue-opened" 16}} {{$.i18n.Tr "repo.issues.open_title"}} + {{svg "octicon-git-pull-request" 16}} {{$.i18n.Tr "repo.issues.open_title"}} {{end}} {{end}} diff --git a/templates/repo/issue/branch_selector_field.tmpl b/templates/repo/issue/branch_selector_field.tmpl index 4f80c13e52df1..69d99b3441b2d 100644 --- a/templates/repo/issue/branch_selector_field.tmpl +++ b/templates/repo/issue/branch_selector_field.tmpl @@ -1,5 +1,10 @@ {{if and (not .Issue.IsPull) (not .PageIsComparePull)}} + +
+ {{$.CsrfTokenHtml}} +
+
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl index bee1cee65bbdc..1fb4906e45f97 100644 --- a/templates/repo/issue/milestones.tmpl +++ b/templates/repo/issue/milestones.tmpl @@ -65,6 +65,7 @@ {{svg "octicon-issue-opened" 16}} {{$.i18n.Tr "repo.issues.open_tab" .NumOpenIssues}} {{svg "octicon-issue-closed" 16}} {{$.i18n.Tr "repo.issues.close_tab" .NumClosedIssues}} {{if .TotalTrackedTime}}{{svg "octicon-clock" 16}} {{.TotalTrackedTime|Sec2Time}}{{end}} + {{if .UpdatedUnix}}{{svg "octicon-clock" 16}} {{$.i18n.Tr "repo.milestones.update_ago" (.TimeSinceUpdate|Sec2Time)}}{{end}} {{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index b9254adeab1c9..2a5198f9622f1 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -27,7 +27,7 @@
{{if .Stale}} - + {{end}} diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index 459c44e78da36..ea63dd0737b52 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -19,7 +19,7 @@ {{if .HasMerged}}
{{svg "octicon-git-merge" 16}} {{.i18n.Tr "repo.pulls.merged"}}
{{else if .Issue.IsClosed}} -
{{svg "octicon-issue-closed" 16}} {{.i18n.Tr "repo.issues.closed_title"}}
+
{{if .Issue.IsPull}}{{svg "octicon-git-pull-request" 16}}{{else}}{{svg "octicon-issue-closed" 16}}{{end}} {{.i18n.Tr "repo.issues.closed_title"}}
{{else if .Issue.IsPull}}
{{svg "octicon-git-pull-request" 16}} {{.i18n.Tr "repo.issues.open_title"}}
{{else}} diff --git a/templates/repo/migrate/git.tmpl b/templates/repo/migrate/git.tmpl new file mode 100644 index 0000000000000..34a1c7bd0d1e3 --- /dev/null +++ b/templates/repo/migrate/git.tmpl @@ -0,0 +1,103 @@ +{{template "base/head" .}} +
+
+
+
+ {{.CsrfTokenHtml}} +

+ {{.i18n.Tr "repo.migrate.migrate" .service.Title}} + +

+
+ {{template "base/alert" .}} +
+ + + + {{.i18n.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{.i18n.Tr "repo.migrate.clone_local_path"}}{{end}} + {{if .LFSActive}}
{{.i18n.Tr "repo.migrate.lfs_mirror_unsupported"}}{{end}} +
+
+
+ + +
+ +
+ + +
+ +
+ +
+ {{if .DisableMirrors}} + + + {{else}} + + + {{end}} +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ +
+ {{if .IsForcedPrivate}} + + + {{else}} + + + {{end}} +
+
+
+ + +
+ +
+ + + {{.i18n.Tr "cancel"}} +
+
+
+
+
+
+{{template "base/footer" .}} diff --git a/templates/repo/migrate.tmpl b/templates/repo/migrate/github.tmpl similarity index 83% rename from templates/repo/migrate.tmpl rename to templates/repo/migrate/github.tmpl index d5a31a680095b..cf84ad39e0b33 100644 --- a/templates/repo/migrate.tmpl +++ b/templates/repo/migrate/github.tmpl @@ -5,7 +5,8 @@
{{.CsrfTokenHtml}}

- {{.i18n.Tr "new_migrate"}} + {{.i18n.Tr "repo.migrate.migrate" .service.Title}} +

{{template "base/alert" .}} @@ -18,31 +19,10 @@
-
- - -
-
- - -
- -
- - -
+ {{svg "octicon-question" 16}}
diff --git a/templates/repo/migrate/gitlab.tmpl b/templates/repo/migrate/gitlab.tmpl new file mode 100644 index 0000000000000..427a1e05dfdfd --- /dev/null +++ b/templates/repo/migrate/gitlab.tmpl @@ -0,0 +1,137 @@ +{{template "base/head" .}} +
+
+
+ + {{.CsrfTokenHtml}} +

+ {{.i18n.Tr "repo.migrate.migrate" .service.Title}} + +

+
+ {{template "base/alert" .}} +
+ + + + {{.i18n.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{.i18n.Tr "repo.migrate.clone_local_path"}}{{end}} + {{if .LFSActive}}
{{.i18n.Tr "repo.migrate.lfs_mirror_unsupported"}}{{end}} +
+
+ +
+ + + {{svg "octicon-question" 16}} +
+ +
+ +
+ {{if .DisableMirrors}} + + + {{else}} + + + {{end}} +
+
+ + {{.i18n.Tr "repo.migrate.migrate_items_options"}} +
+
+ +
+ + +
+
+ + +
+
+
+ +
+ + +
+
+ + +
+
+
+ +
+ + +
+
+ + +
+
+
+ +
+ +
+ + +
+ +
+ + +
+
+ +
+ {{if .IsForcedPrivate}} + + + {{else}} + + + {{end}} +
+
+
+ + +
+ +
+ + + {{.i18n.Tr "cancel"}} +
+
+ +
+
+
+{{template "base/footer" .}} diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl new file mode 100644 index 0000000000000..1521620b0ee8b --- /dev/null +++ b/templates/repo/migrate/migrate.tmpl @@ -0,0 +1,23 @@ +{{template "base/head" .}} +
+
+
+
+ {{range .Services}} +
+ + {{svg (Printf "gitea-%s" .Name) 184}} + +
+ {{.Title}} +
+ {{(Printf "repo.migrate.%s.description" .Name) | $.i18n.Tr }} +
+
+
+ {{end}} +
+
+
+
+{{template "base/footer" .}} diff --git a/templates/repo/migrating.tmpl b/templates/repo/migrate/migrating.tmpl similarity index 100% rename from templates/repo/migrating.tmpl rename to templates/repo/migrate/migrating.tmpl diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 782331aad70d7..26f66d2cb597f 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -1,12 +1,8 @@

-
+
{{if .ReadmeInList}} - {{if .FileIsSymlink}} - - {{else}} - - {{end}} + {{svg "octicon-book" 16 "mr-3"}} {{.FileName}} {{else}}
@@ -26,8 +22,8 @@
{{end}} {{if .LFSLock}} -
- +
+ {{svg "octicon-lock" 16 "mr-2"}} {{.LFSLockOwner}}
{{end}} @@ -35,7 +31,7 @@ {{end}}
{{if not .ReadmeInList}} -
+
{{.i18n.Tr "repo.file_raw"}} diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 2e70f4ff84b17..3b469f4fca053 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -47,7 +47,7 @@ {{if $entry.IsSubModule}} {{svg "octicon-file-submodule" 16}} - {{$refURL := $commit.RefURL AppUrl $.Repository.FullName}} + {{$refURL := $commit.RefURL AppUrl $.Repository.FullName $.SSHDomain}} {{if $refURL}} {{$entry.Name}}@{{ShortSha $commit.RefID}} {{else}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 4d6333ac4e8c7..661005be245d0 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2075,7 +2075,7 @@ }, { "type": "string", - "description": "archive to download, consisting of a git reference and archive", + "description": "the git reference for download with attached archive format (e.g. master.zip)", "name": "archive", "in": "path", "required": true @@ -8672,6 +8672,40 @@ } } }, + "/settings/Attachment": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "settings" + ], + "summary": "Get instance's global settings for Attachment", + "operationId": "getGeneralAttachmentSettings", + "responses": { + "200": { + "$ref": "#/responses/GeneralAttachmentSettings" + } + } + } + }, + "/settings/api": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "settings" + ], + "summary": "Get instance's global settings for api", + "operationId": "getGeneralAPISettings", + "responses": { + "200": { + "$ref": "#/responses/GeneralAPISettings" + } + } + } + }, "/settings/repository": { "get": { "produces": [ @@ -12977,6 +13011,58 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "GeneralAPISettings": { + "description": "GeneralAPISettings contains global api settings exposed by it", + "type": "object", + "properties": { + "default_git_trees_per_page": { + "type": "integer", + "format": "int64", + "x-go-name": "DefaultGitTreesPerPage" + }, + "default_max_blob_size": { + "type": "integer", + "format": "int64", + "x-go-name": "DefaultMaxBlobSize" + }, + "default_paging_num": { + "type": "integer", + "format": "int64", + "x-go-name": "DefaultPagingNum" + }, + "max_response_items": { + "type": "integer", + "format": "int64", + "x-go-name": "MaxResponseItems" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "GeneralAttachmentSettings": { + "description": "GeneralAttachmentSettings contains global Attachment settings exposed by API", + "type": "object", + "properties": { + "allowed_types": { + "type": "string", + "x-go-name": "AllowedTypes" + }, + "enabled": { + "type": "boolean", + "x-go-name": "Enabled" + }, + "max_files": { + "type": "integer", + "format": "int64", + "x-go-name": "MaxFiles" + }, + "max_size": { + "type": "integer", + "format": "int64", + "x-go-name": "MaxSize" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "GeneralRepoSettings": { "description": "GeneralRepoSettings contains global repository settings exposed by API", "type": "object", @@ -13527,6 +13613,11 @@ "format": "int64", "x-go-name": "ClosedIssues" }, + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Created" + }, "description": { "type": "string", "x-go-name": "Description" @@ -13552,6 +13643,11 @@ "title": { "type": "string", "x-go-name": "Title" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -14612,6 +14708,18 @@ "type": "integer", "format": "int64", "x-go-name": "IssueIndex" + }, + "issue_title": { + "type": "string", + "x-go-name": "IssueTitle" + }, + "repo_name": { + "type": "string", + "x-go-name": "RepoName" + }, + "repo_owner_name": { + "type": "string", + "x-go-name": "RepoOwnerName" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -15197,6 +15305,18 @@ } } }, + "GeneralAPISettings": { + "description": "GeneralAPISettings", + "schema": { + "$ref": "#/definitions/GeneralAPISettings" + } + }, + "GeneralAttachmentSettings": { + "description": "GeneralAttachmentSettings", + "schema": { + "$ref": "#/definitions/GeneralAttachmentSettings" + } + }, "GeneralRepoSettings": { "description": "GeneralRepoSettings", "schema": { diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl index ca055e9d87efa..ce4a97a36f856 100644 --- a/templates/user/dashboard/repolist.tmpl +++ b/templates/user/dashboard/repolist.tmpl @@ -25,14 +25,6 @@

{{.i18n.Tr "home.my_repos"}} ${reposTotalCount} - {{if or (not .ContextUser.IsOrganization) .IsOrganizationOwner}} - - {{end}}