diff --git a/cmd/convert.go b/cmd/convert.go index 23a3d8dbe9..e2ffd403ac 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -27,10 +27,10 @@ func runConvert(ctx *cli.Context) error { return err } - log.Trace("AppPath: %s", setting.AppPath) - log.Trace("AppWorkPath: %s", setting.AppWorkPath) - log.Trace("Custom path: %s", setting.CustomPath) - log.Trace("Log path: %s", setting.LogRootPath) + log.Info("AppPath: %s", setting.AppPath) + log.Info("AppWorkPath: %s", setting.AppWorkPath) + log.Info("Custom path: %s", setting.CustomPath) + log.Info("Log path: %s", setting.LogRootPath) setting.InitDBConfig() if !setting.Database.UseMySQL { diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go index cea640b534..69813e3c87 100644 --- a/cmd/dump_repo.go +++ b/cmd/dump_repo.go @@ -69,7 +69,7 @@ var CmdDumpRepository = cli.Command{ cli.StringFlag{ Name: "units", Value: "", - Usage: `Which items will be migrated, one or more units should be separated as comma. + Usage: `Which items will be migrated, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, }, }, @@ -80,10 +80,10 @@ func runDumpRepository(ctx *cli.Context) error { return err } - log.Trace("AppPath: %s", setting.AppPath) - log.Trace("AppWorkPath: %s", setting.AppWorkPath) - log.Trace("Custom path: %s", setting.CustomPath) - log.Trace("Log path: %s", setting.LogRootPath) + log.Info("AppPath: %s", setting.AppPath) + log.Info("AppWorkPath: %s", setting.AppWorkPath) + log.Info("Custom path: %s", setting.CustomPath) + log.Info("Log path: %s", setting.LogRootPath) setting.InitDBConfig() var ( diff --git a/cmd/hook.go b/cmd/hook.go index def3b636eb..2fbbfb4d21 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -179,7 +179,7 @@ Gitea or set your environment appropriately.`, "") GitObjectDirectory: os.Getenv(private.GitObjectDirectory), GitQuarantinePath: os.Getenv(private.GitQuarantinePath), GitPushOptions: pushOptions(), - ProtectedBranchID: prID, + PullRequestID: prID, IsDeployKey: isDeployKey, } @@ -221,8 +221,8 @@ Gitea or set your environment appropriately.`, "") total++ lastline++ - // If the ref is a branch, check if it's protected - if strings.HasPrefix(refFullName, git.BranchPrefix) { + // If the ref is a branch or tag, check if it's protected + if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { oldCommitIDs[count] = oldCommitID newCommitIDs[count] = newCommitID refFullNames[count] = refFullName @@ -230,7 +230,7 @@ Gitea or set your environment appropriately.`, "") fmt.Fprintf(out, "*") if count >= hookBatchSize { - fmt.Fprintf(out, " Checking %d branches\n", count) + fmt.Fprintf(out, " Checking %d references\n", count) hookOptions.OldCommitIDs = oldCommitIDs hookOptions.NewCommitIDs = newCommitIDs @@ -261,7 +261,7 @@ Gitea or set your environment appropriately.`, "") hookOptions.NewCommitIDs = newCommitIDs[:count] hookOptions.RefFullNames = refFullNames[:count] - fmt.Fprintf(out, " Checking %d branches\n", count) + fmt.Fprintf(out, " Checking %d references\n", count) statusCode, msg := private.HookPreReceive(username, reponame, hookOptions) switch statusCode { diff --git a/cmd/migrate.go b/cmd/migrate.go index 2428925887..23bc97b0c4 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -28,10 +28,10 @@ func runMigrate(ctx *cli.Context) error { return err } - log.Trace("AppPath: %s", setting.AppPath) - log.Trace("AppWorkPath: %s", setting.AppWorkPath) - log.Trace("Custom path: %s", setting.CustomPath) - log.Trace("Log path: %s", setting.LogRootPath) + log.Info("AppPath: %s", setting.AppPath) + log.Info("AppWorkPath: %s", setting.AppWorkPath) + log.Info("Custom path: %s", setting.CustomPath) + log.Info("Log path: %s", setting.LogRootPath) setting.InitDBConfig() if err := models.NewEngine(context.Background(), migrations.Migrate); err != nil { diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index 871baed92d..8ff84d690b 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -110,10 +110,10 @@ func runMigrateStorage(ctx *cli.Context) error { return err } - log.Trace("AppPath: %s", setting.AppPath) - log.Trace("AppWorkPath: %s", setting.AppWorkPath) - log.Trace("Custom path: %s", setting.CustomPath) - log.Trace("Log path: %s", setting.LogRootPath) + log.Info("AppPath: %s", setting.AppPath) + log.Info("AppWorkPath: %s", setting.AppWorkPath) + log.Info("Custom path: %s", setting.CustomPath) + log.Info("Log path: %s", setting.LogRootPath) setting.InitDBConfig() if err := models.NewEngine(context.Background(), migrations.Migrate); err != nil { diff --git a/cmd/web.go b/cmd/web.go index 0ba14ae706..6953e7c64f 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -47,6 +47,14 @@ and it takes care of all the other things for you`, Value: setting.PIDFile, Usage: "Custom pid file path", }, + cli.BoolFlag{ + Name: "quiet, q", + Usage: "Only display Fatal logging errors until logging is set-up", + }, + cli.BoolFlag{ + Name: "verbose", + Usage: "Set initial logging to TRACE level until logging is properly set-up", + }, }, } @@ -71,6 +79,14 @@ func runHTTPRedirector() { } func runWeb(ctx *cli.Context) error { + if ctx.Bool("verbose") { + _ = log.DelLogger("console") + log.NewLogger(0, "console", "console", fmt.Sprintf(`{"level": "trace", "colorize": %t, "stacktraceLevel": "none"}`, log.CanColorStdout)) + } else if ctx.Bool("quiet") { + _ = log.DelLogger("console") + log.NewLogger(0, "console", "console", fmt.Sprintf(`{"level": "fatal", "colorize": %t, "stacktraceLevel": "none"}`, log.CanColorStdout)) + } + managerCtx, cancel := context.WithCancel(context.Background()) graceful.InitManager(managerCtx) defer cancel() diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go index 9ce84f762c..902c9ea623 100644 --- a/contrib/pr/checkout.go +++ b/contrib/pr/checkout.go @@ -26,6 +26,7 @@ import ( "time" "code.gitea.io/gitea/models" + gitea_git "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/setting" @@ -79,7 +80,7 @@ func runPR() { setting.RunUser = curUser.Username log.Printf("[PR] Loading fixtures data ...\n") - setting.CheckLFSVersion() + gitea_git.CheckLFSVersion() //models.LoadConfigs() /* setting.Database.Type = "sqlite3" diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 38a27509f7..e7fe9206ed 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -651,9 +651,15 @@ PATH = ;DEFAULT_ALLOW_CREATE_ORGANIZATION = true ;; ;; Either "public", "limited" or "private", default is "public" -;; Limited is for signed user only -;; Private is only for member of the organization -;; Public is for everyone +;; Limited is for users visible only to signed users +;; Private is for users visible only to members of their organizations +;; Public is for users visible for everyone +;DEFAULT_USER_VISIBILITY = public +;; +;; Either "public", "limited" or "private", default is "public" +;; Limited is for organizations visible only to signed users +;; Private is for organizations visible only to members of the organization +;; Public is for organizations visible to everyone ;DEFAULT_ORG_VISIBILITY = public ;; ;; Default value for DefaultOrgMemberVisible @@ -705,6 +711,8 @@ PATH = ;; ;; Minimum amount of time a user must exist before comments are kept when the user is deleted. ;USER_DELETE_WITH_COMMENTS_MAX_TIME = 0 +;; Valid site url schemes for user profiles +;VALID_SITE_URL_SCHEMES=http,https ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2048,6 +2056,16 @@ PATH = ;; storage type ;STORAGE_TYPE = local +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; settings for repository archives, will override storage setting +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[storage.repo-archive] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; storage type +;STORAGE_TYPE = local + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; lfs storage will override storage 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 a33407d15a..21359dcab1 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -512,6 +512,7 @@ relation to port exhaustion. - `SHOW_MILESTONES_DASHBOARD_PAGE`: **true** Enable this to show the milestones dashboard page - a view of all the user's milestones - `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created - `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it +- `DEFAULT_USER_VISIBILITY`: **public**: Set default visibility mode for users, either "public", "limited" or "private". - `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private". - `DEFAULT_ORG_MEMBER_VISIBLE`: **false** True will make the membership of the users visible when added to the organisation. - `ALLOW_ONLY_INTERNAL_REGISTRATION`: **false** Set to true to force registration only via gitea. @@ -519,6 +520,7 @@ relation to port exhaustion. - `NO_REPLY_ADDRESS`: **noreply.DOMAIN** Value for the domain part of the user's email address in the git log if user has set KeepEmailPrivate to true. DOMAIN resolves to the value in server.DOMAIN. The user's email will be replaced with a concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. - `USER_DELETE_WITH_COMMENTS_MAX_TIME`: **0** Minimum amount of time a user must exist before comments are kept when the user is deleted. +- `VALID_SITE_URL_SCHEMES`: **http, https**: Valid site url schemes for user profiles ### Service - Expore (`service.explore`) @@ -907,13 +909,17 @@ Gitea supports customizing the sanitization policy for rendered HTML. The exampl ELEMENT = span ALLOW_ATTR = class REGEXP = ^\s*((math(\s+|$)|inline(\s+|$)|display(\s+|$)))+ +ALLOW_DATA_URI_IMAGES = true ``` - `ELEMENT`: The element this policy applies to. Must be non-empty. - `ALLOW_ATTR`: The attribute this policy allows. Must be non-empty. - `REGEXP`: A regex to match the contents of the attribute against. Must be present but may be empty for unconditional whitelisting of this attribute. + - `ALLOW_DATA_URI_IMAGES`: **false** Allow data uri images (``). Multiple sanitisation rules can be defined by adding unique subsections, e.g. `[markup.sanitizer.TeX-2]`. +To apply a sanitisation rules only for a specify external renderer they must use the renderer name, e.g. `[markup.sanitizer.asciidoc.rule-1]`. +If the rule is defined above the renderer ini section or the name does not match a renderer it is applied to every renderer. ## Time (`time`) @@ -991,6 +997,23 @@ MINIO_USE_SSL = false And used by `[attachment]`, `[lfs]` and etc. as `STORAGE_TYPE`. +## Repository Archive Storage (`storage.repo-archive`) + +Configuration for repository archive storage. It will inherit from default `[storage]` or +`[storage.xxx]` when set `STORAGE_TYPE` to `xxx`. The default of `PATH` +is `data/repo-archive` and the default of `MINIO_BASE_PATH` is `repo-archive/`. + +- `STORAGE_TYPE`: **local**: Storage type for repo archive, `local` for local disk or `minio` for s3 compatible object storage service or other name defined with `[storage.xxx]` +- `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. +- `PATH`: **./data/repo-archive**: Where to store archive files, only available when `STORAGE_TYPE` is `local`. +- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio` +- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio` +- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey to connect only available when `STORAGE_TYPE is` `minio` +- `MINIO_BUCKET`: **gitea**: Minio bucket to store the lfs only available when `STORAGE_TYPE` is `minio` +- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket only available when `STORAGE_TYPE` is `minio` +- `MINIO_BASE_PATH`: **repo-archive/**: Minio base path on the bucket only available when `STORAGE_TYPE` is `minio` +- `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio` + ## Other (`other`) - `SHOW_FOOTER_BRANDING`: **false**: Show Gitea branding in the footer. 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 79cfd94cc7..2303a631d5 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -382,6 +382,21 @@ MINIO_USE_SSL = false 然后你在 `[attachment]`, `[lfs]` 等中可以把这个名字用作 `STORAGE_TYPE` 的值。 +## Repository Archive Storage (`storage.repo-archive`) + +Repository archive 的存储配置。 如果 `STORAGE_TYPE` 为空,则此配置将从 `[storage]` 继承。如果不为 `local` 或者 `minio` 而为 `xxx`, 则从 `[storage.xxx]` 继承。当继承时, `PATH` 默认为 `data/repo-archive`,`MINIO_BASE_PATH` 默认为 `repo-archive/`。 + +- `STORAGE_TYPE`: **local**: Repository archive 的存储类型,`local` 将存储到磁盘,`minio` 将存储到 s3 兼容的对象服务。 +- `SERVE_DIRECT`: **false**: 允许直接重定向到存储系统。当前,仅 Minio/S3 是支持的。 +- `PATH`: 存放 Repository archive 上传的文件的地方,默认是 `data/repo-archive`。 +- `MINIO_ENDPOINT`: **localhost:9000**: Minio 地址,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_SECRET_ACCESS_KEY`: Minio secretAccessKey,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_BUCKET`: **gitea**: Minio bucket,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_LOCATION`: **us-east-1**: Minio location ,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_BASE_PATH`: **repo-archive/**: Minio base path ,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 +- `MINIO_USE_SSL`: **false**: Minio 是否启用 ssl ,仅当 `STORAGE_TYPE` 为 `minio` 时有效。 + ## Other (`other`) - `SHOW_FOOTER_BRANDING`: 为真则在页面底部显示Gitea的字样。 diff --git a/docs/content/doc/advanced/external-renderers.en-us.md b/docs/content/doc/advanced/external-renderers.en-us.md index 71fabc529d..c0109b8014 100644 --- a/docs/content/doc/advanced/external-renderers.en-us.md +++ b/docs/content/doc/advanced/external-renderers.en-us.md @@ -64,8 +64,8 @@ IS_INPUT_FILE = false [markup.jupyter] ENABLED = true FILE_EXTENSIONS = .ipynb -RENDER_COMMAND = "jupyter nbconvert --stdout --to html --template basic " -IS_INPUT_FILE = true +RENDER_COMMAND = "jupyter nbconvert --stdin --stdout --to html --template basic" +IS_INPUT_FILE = false [markup.restructuredtext] ENABLED = true @@ -90,15 +90,50 @@ FILE_EXTENSIONS = .md,.markdown RENDER_COMMAND = pandoc -f markdown -t html --katex ``` -You must define `ELEMENT`, `ALLOW_ATTR`, and `REGEXP` in each section. +You must define `ELEMENT` and `ALLOW_ATTR` in each section. To define multiple entries, add a unique alphanumeric suffix (e.g., `[markup.sanitizer.1]` and `[markup.sanitizer.something]`). +To apply a sanitisation rules only for a specify external renderer they must use the renderer name, e.g. `[markup.sanitizer.asciidoc.rule-1]`, `[markup.sanitizer..rule-1]`. + +**Note**: If the rule is defined above the renderer ini section or the name does not match a renderer it is applied to every renderer. + Once your configuration changes have been made, restart Gitea to have changes take effect. **Note**: Prior to Gitea 1.12 there was a single `markup.sanitiser` section with keys that were redefined for multiple rules, however, there were significant problems with this method of configuration necessitating configuration through multiple sections. +### Example: Office DOCX + +Display Office DOCX files with [`pandoc`](https://pandoc.org/): +```ini +[markup.docx] +ENABLED = true +FILE_EXTENSIONS = .docx +RENDER_COMMAND = "pandoc --from docx --to html --self-contained --template /path/to/basic.html" + +[markup.sanitizer.docx.img] +ALLOW_DATA_URI_IMAGES = true +``` + +The template file has the following content: +``` +$body$ +``` + +### Example: Jupyter Notebook + +Display Jupyter Notebook files with [`nbconvert`](https://github.com/jupyter/nbconvert): +```ini +[markup.jupyter] +ENABLED = true +FILE_EXTENSIONS = .ipynb +RENDER_COMMAND = "jupyter-nbconvert --stdin --stdout --to html --template basic" + +[markup.sanitizer.jupyter.img] +ALLOW_DATA_URI_IMAGES = true +``` + ## Customizing CSS The external renderer is specified in the .ini in the format `[markup.XXXXX]` and the HTML supplied by your external renderer will be wrapped in a `
` with classes `markup` and `XXXXX`. The `markup` class provides out of the box styling (as does `markdown` if `XXXXX` is `markdown`). Otherwise you can use these classes to specifically target the contents of your rendered HTML. diff --git a/docs/content/doc/advanced/protected-tags.en-us.md b/docs/content/doc/advanced/protected-tags.en-us.md new file mode 100644 index 0000000000..36e6e16975 --- /dev/null +++ b/docs/content/doc/advanced/protected-tags.en-us.md @@ -0,0 +1,57 @@ +--- +date: "2021-05-14T00:00:00-00:00" +title: "Protected tags" +slug: "protected-tags" +weight: 45 +toc: false +draft: false +menu: + sidebar: + parent: "advanced" + name: "Protected tags" + weight: 45 + identifier: "protected-tags" +--- + +# Protected tags + +Protected tags allow control over who has permission to create or update git tags. Each rule allows you to match either an individual tag name, or use an appropriate pattern to control multiple tags at once. + +**Table of Contents** + +{{< toc >}} + +## Setting up protected tags + +To protect a tag, you need to follow these steps: + +1. Go to the repository’s **Settings** > **Tags** page. +1. Type a pattern to match a name. You can use a single name, a [glob pattern](https://pkg.go.dev/github.com/gobwas/glob#Compile) or a regular expression. +1. Choose the allowed users and/or teams. If you leave these fields empty noone is allowed to create or modify this tag. +1. Select **Save** to save the configuration. + +## Pattern protected tags + +The pattern uses [glob](https://pkg.go.dev/github.com/gobwas/glob#Compile) or regular expressions to match a tag name. For regular expressions you need to enclose the pattern in slashes. + +Examples: + +| Type | Pattern Protected Tag | Possible Matching Tags | +| ----- | ------------------------ | --------------------------------------- | +| Glob | `v*` | `v`, `v-1`, `version2` | +| Glob | `v[0-9]` | `v0`, `v1` up to `v9` | +| Glob | `*-release` | `2.1-release`, `final-release` | +| Glob | `gitea` | only `gitea` | +| Glob | `*gitea*` | `gitea`, `2.1-gitea`, `1_gitea-release` | +| Glob | `{v,rel}-*` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` | +| Glob | `*` | matches all possible tag names | +| Regex | `/\Av/` | `v`, `v-1`, `version2` | +| Regex | `/\Av[0-9]\z/` | `v0`, `v1` up to `v9` | +| Regex | `/\Av\d+\.\d+\.\d+\z/` | `v1.0.17`, `v2.1.0` | +| Regex | `/\Av\d+(\.\d+){0,2}\z/` | `v1`, `v2.1`, `v1.2.34` | +| Regex | `/-release\z/` | `2.1-release`, `final-release` | +| Regex | `/gitea/` | `gitea`, `2.1-gitea`, `1_gitea-release` | +| Regex | `/\Agitea\z/` | only `gitea` | +| Regex | `/^gitea$/` | only `gitea` | +| Regex | `/\A(v\|rel)-/` | `v-`, `v-1`, `v-final`, `rel-`, `rel-x` | +| Regex | `/.+/` | matches all possible tag names | diff --git a/docs/content/doc/developers/hacking-on-gitea.en-us.md b/docs/content/doc/developers/hacking-on-gitea.en-us.md index 360a46d20f..96db0a01bb 100644 --- a/docs/content/doc/developers/hacking-on-gitea.en-us.md +++ b/docs/content/doc/developers/hacking-on-gitea.en-us.md @@ -73,6 +73,8 @@ One of these three distributions of Make will run on Windows: - The binary is called `mingw32-make.exe` instead of `make.exe`. Add the `bin` folder to `PATH`. - [Chocolatey package](https://chocolatey.org/packages/make). Run `choco install make` +**Note**: If you are attempting to build using make with Windows Command Prompt, you may run into issues. The above prompts (git bash, or mingw) are recommended, however if you only have command prompt (or potentially powershell) you can set environment variables using the [set](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/set_1) command, e.g. `set TAGS=bindata`. + ## Downloading and cloning the Gitea source code The recommended method of obtaining the source code is by using `git clone`. diff --git a/docs/content/doc/usage/command-line.en-us.md b/docs/content/doc/usage/command-line.en-us.md index 40933a7b33..0bc8d70fdb 100644 --- a/docs/content/doc/usage/command-line.en-us.md +++ b/docs/content/doc/usage/command-line.en-us.md @@ -46,6 +46,8 @@ Starts the server: - `--port number`, `-p number`: Port number. Optional. (default: 3000). Overrides configuration file. - `--install-port number`: Port number to run the install page on. Optional. (default: 3000). Overrides configuration file. - `--pid path`, `-P path`: Pidfile path. Optional. + - `--quiet`, `-q`: Only emit Fatal logs on the console for logs emitted before logging set up. + - `--verbose`: Emit tracing logs on the console for logs emitted before logging is set-up. - Examples: - `gitea web` - `gitea web --port 80` diff --git a/docs/content/doc/usage/reverse-proxies.en-us.md b/docs/content/doc/usage/reverse-proxies.en-us.md index e2fdb1d2b7..b339048d68 100644 --- a/docs/content/doc/usage/reverse-proxies.en-us.md +++ b/docs/content/doc/usage/reverse-proxies.en-us.md @@ -221,6 +221,9 @@ If you wish to run Gitea with IIS. You will need to setup IIS with URL Rewrite a ```xml + + + diff --git a/integrations/api_pull_review_test.go b/integrations/api_pull_review_test.go index ebe8539a82..bcc0cbffcb 100644 --- a/integrations/api_pull_review_test.go +++ b/integrations/api_pull_review_test.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models" api "code.gitea.io/gitea/modules/structs" + jsoniter "github.com/json-iterator/go" "github.com/stretchr/testify/assert" ) @@ -139,6 +140,59 @@ func TestAPIPullReview(t *testing.T) { req = NewRequestf(t, http.MethodDelete, "/api/v1/repos/%s/%s/pulls/%d/reviews/%d?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, review.ID, token) resp = session.MakeRequest(t, req, http.StatusNoContent) + // test CreatePullReview Comment without body but with comments + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{ + // Body: "", + Event: "COMMENT", + Comments: []api.CreatePullReviewComment{{ + Path: "README.md", + Body: "first new line", + OldLineNum: 0, + NewLineNum: 1, + }, { + Path: "README.md", + Body: "first old line", + OldLineNum: 1, + NewLineNum: 0, + }, + }, + }) + var commentReview api.PullReview + + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &commentReview) + assert.EqualValues(t, "COMMENT", commentReview.State) + assert.EqualValues(t, 2, commentReview.CodeCommentsCount) + assert.EqualValues(t, "", commentReview.Body) + assert.EqualValues(t, false, commentReview.Dismissed) + + // test CreatePullReview Comment with body but without comments + commentBody := "This is a body of the comment." + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{ + Body: commentBody, + Event: "COMMENT", + Comments: []api.CreatePullReviewComment{}, + }) + + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &commentReview) + assert.EqualValues(t, "COMMENT", commentReview.State) + assert.EqualValues(t, 0, commentReview.CodeCommentsCount) + assert.EqualValues(t, commentBody, commentReview.Body) + assert.EqualValues(t, false, commentReview.Dismissed) + + // test CreatePullReview Comment without body and no comments + req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.CreatePullReviewOptions{ + Body: "", + Event: "COMMENT", + Comments: []api.CreatePullReviewComment{}, + }) + resp = session.MakeRequest(t, req, http.StatusUnprocessableEntity) + errMap := make(map[string]interface{}) + json := jsoniter.ConfigCompatibleWithStandardLibrary + json.Unmarshal(resp.Body.Bytes(), &errMap) + assert.EqualValues(t, "review event COMMENT requires a body or a comment", errMap["message"].(string)) + // test get review requests // to make it simple, use same api with get review pullIssue12 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 12}).(*models.Issue) diff --git a/integrations/api_repo_tags_test.go b/integrations/api_repo_tags_test.go index 1bd9fa6168..0bf54d3a95 100644 --- a/integrations/api_repo_tags_test.go +++ b/integrations/api_repo_tags_test.go @@ -39,7 +39,7 @@ func TestAPIRepoTags(t *testing.T) { assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.zip", tags[0].ZipballURL) assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.tar.gz", tags[0].TarballURL) - newTag := createNewTagUsingAPI(t, session, token, user.Name, repoName, "awesome-tag", "", "nice!\nand some text") + newTag := createNewTagUsingAPI(t, session, token, user.Name, repoName, "gitea/22", "", "nice!\nand some text") resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &tags) assert.Len(t, tags, 2) @@ -51,6 +51,20 @@ func TestAPIRepoTags(t *testing.T) { assert.EqualValues(t, newTag.Commit.SHA, tag.Commit.SHA) } } + + // get created tag + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/tags/%s?token=%s", user.Name, repoName, newTag.Name, token) + resp = session.MakeRequest(t, req, http.StatusOK) + var tag *api.Tag + DecodeJSON(t, resp, &tag) + assert.EqualValues(t, newTag, tag) + + // delete tag + delReq := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/tags/%s?token=%s", user.Name, repoName, newTag.Name, token) + resp = session.MakeRequest(t, delReq, http.StatusNoContent) + + // check if it's gone + resp = session.MakeRequest(t, req, http.StatusNotFound) } func createNewTagUsingAPI(t *testing.T, session *TestSession, token string, ownerName, repoName, name, target, msg string) *api.Tag { diff --git a/integrations/api_user_heatmap_test.go b/integrations/api_user_heatmap_test.go index 105d39e9ae..a0f0552a17 100644 --- a/integrations/api_user_heatmap_test.go +++ b/integrations/api_user_heatmap_test.go @@ -26,7 +26,7 @@ func TestUserHeatmap(t *testing.T) { var heatmap []*models.UserHeatmapData DecodeJSON(t, resp, &heatmap) var dummyheatmap []*models.UserHeatmapData - dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1603152000, Contributions: 1}) + dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1603227600, Contributions: 1}) assert.Equal(t, dummyheatmap, heatmap) } diff --git a/integrations/api_user_search_test.go b/integrations/api_user_search_test.go index c5295fbba5..f7349827e5 100644 --- a/integrations/api_user_search_test.go +++ b/integrations/api_user_search_test.go @@ -59,3 +59,34 @@ func TestAPIUserSearchNotLoggedIn(t *testing.T) { } } } + +func TestAPIUserSearchAdminLoggedInUserHidden(t *testing.T) { + defer prepareTestEnv(t)() + adminUsername := "user1" + session := loginUser(t, adminUsername) + token := getTokenForLoggedInUser(t, session) + query := "user31" + req := NewRequestf(t, "GET", "/api/v1/users/search?token=%s&q=%s", token, query) + req.SetBasicAuth(token, "x-oauth-basic") + resp := session.MakeRequest(t, req, http.StatusOK) + + var results SearchResults + DecodeJSON(t, resp, &results) + assert.NotEmpty(t, results.Data) + for _, user := range results.Data { + assert.Contains(t, user.UserName, query) + assert.NotEmpty(t, user.Email) + assert.EqualValues(t, "private", user.Visibility) + } +} + +func TestAPIUserSearchNotLoggedInUserHidden(t *testing.T) { + defer prepareTestEnv(t)() + query := "user31" + req := NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query) + resp := MakeRequest(t, req, http.StatusOK) + + var results SearchResults + DecodeJSON(t, resp, &results) + assert.Empty(t, results.Data) +} diff --git a/integrations/git_test.go b/integrations/git_test.go index 13a60076a7..a9848eaa4c 100644 --- a/integrations/git_test.go +++ b/integrations/git_test.go @@ -143,7 +143,7 @@ func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) { t.Run("LFS", func(t *testing.T) { defer PrintCurrentTest(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -213,7 +213,7 @@ func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS s resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Length) - setting.CheckLFSVersion() + git.CheckLFSVersion() if setting.LFS.StartServer { req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS)) resp := session.MakeRequest(t, req, http.StatusOK) @@ -255,7 +255,7 @@ func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Length) - setting.CheckLFSVersion() + git.CheckLFSVersion() if setting.LFS.StartServer { req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS)) resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) diff --git a/integrations/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive b/integrations/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive new file mode 100644 index 0000000000..0f13243bfd --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/repo49.git/refs/heads/test/archive @@ -0,0 +1 @@ +aacbdfe9e1c4b47f60abe81849045fa4e96f1d75 diff --git a/integrations/integration_test.go b/integrations/integration_test.go index d755977d1a..8a008ac621 100644 --- a/integrations/integration_test.go +++ b/integrations/integration_test.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/queue" @@ -162,7 +163,7 @@ func initIntegrationTest() { setting.SetCustomPathAndConf("", "", "") setting.NewContext() util.RemoveAll(models.LocalCopyPath()) - setting.CheckLFSVersion() + git.CheckLFSVersion() setting.InitDBConfig() if err := storage.Init(); err != nil { fmt.Printf("Init storage failed: %v", err) diff --git a/integrations/lfs_getobject_test.go b/integrations/lfs_getobject_test.go index 337a93567a..c99500f469 100644 --- a/integrations/lfs_getobject_test.go +++ b/integrations/lfs_getobject_test.go @@ -13,6 +13,7 @@ import ( "testing" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers/web" @@ -81,7 +82,7 @@ func checkResponseTestContentEncoding(t *testing.T, content *[]byte, resp *httpt func TestGetLFSSmall(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -94,7 +95,7 @@ func TestGetLFSSmall(t *testing.T) { func TestGetLFSLarge(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -110,7 +111,7 @@ func TestGetLFSLarge(t *testing.T) { func TestGetLFSGzip(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -131,7 +132,7 @@ func TestGetLFSGzip(t *testing.T) { func TestGetLFSZip(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -154,7 +155,7 @@ func TestGetLFSZip(t *testing.T) { func TestGetLFSRangeNo(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return @@ -167,7 +168,7 @@ func TestGetLFSRangeNo(t *testing.T) { func TestGetLFSRange(t *testing.T) { defer prepareTestEnv(t)() - setting.CheckLFSVersion() + git.CheckLFSVersion() if !setting.LFS.StartServer { t.Skip() return diff --git a/integrations/migration-test/migration_test.go b/integrations/migration-test/migration_test.go index 852c0b737c..209ff5a058 100644 --- a/integrations/migration-test/migration_test.go +++ b/integrations/migration-test/migration_test.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/models/migrations" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -61,7 +62,7 @@ func initMigrationTest(t *testing.T) func() { assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, util.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) - setting.CheckLFSVersion() + git.CheckLFSVersion() setting.InitDBConfig() setting.NewLogServices(true) return deferFn diff --git a/integrations/mirror_pull_test.go b/integrations/mirror_pull_test.go index 0e4da74fcf..3908f35557 100644 --- a/integrations/mirror_pull_test.go +++ b/integrations/mirror_pull_test.go @@ -59,7 +59,9 @@ func TestMirrorPull(t *testing.T) { assert.NoError(t, release_service.CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.2", Target: "master", Title: "v0.2 is released", diff --git a/integrations/repo_tag_test.go b/integrations/repo_tag_test.go new file mode 100644 index 0000000000..eb3f2b47fb --- /dev/null +++ b/integrations/repo_tag_test.go @@ -0,0 +1,74 @@ +// Copyright 2021 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 ( + "io/ioutil" + "net/url" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/release" + + "github.com/stretchr/testify/assert" +) + +func TestCreateNewTagProtected(t *testing.T) { + defer prepareTestEnv(t)() + + repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) + owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User) + + t.Run("API", func(t *testing.T) { + defer PrintCurrentTest(t)() + + err := release.CreateNewTag(owner, repo, "master", "v-1", "first tag") + assert.NoError(t, err) + + err = models.InsertProtectedTag(&models.ProtectedTag{ + RepoID: repo.ID, + NamePattern: "v-*", + }) + assert.NoError(t, err) + err = models.InsertProtectedTag(&models.ProtectedTag{ + RepoID: repo.ID, + NamePattern: "v-1.1", + AllowlistUserIDs: []int64{repo.OwnerID}, + }) + assert.NoError(t, err) + + err = release.CreateNewTag(owner, repo, "master", "v-2", "second tag") + assert.Error(t, err) + assert.True(t, models.IsErrProtectedTagName(err)) + + err = release.CreateNewTag(owner, repo, "master", "v-1.1", "third tag") + assert.NoError(t, err) + }) + + t.Run("Git", func(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + username := "user2" + httpContext := NewAPITestContext(t, username, "repo1") + + dstPath, err := ioutil.TempDir("", httpContext.Reponame) + assert.NoError(t, err) + defer util.RemoveAll(dstPath) + + u.Path = httpContext.GitPath() + u.User = url.UserPassword(username, userPassword) + + doGitClone(dstPath, u)(t) + + _, err = git.NewCommand("tag", "v-2").RunInDir(dstPath) + assert.NoError(t, err) + + _, err = git.NewCommand("push", "--tags").RunInDir(dstPath) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Tag v-2 is protected") + }) + }) +} diff --git a/models/branches.go b/models/branches.go index 1ac1fa49e5..d346f19578 100644 --- a/models/branches.go +++ b/models/branches.go @@ -362,11 +362,7 @@ func (repo *Repository) GetBranchProtection(branchName string) (*ProtectedBranch } // IsProtectedBranch checks if branch is protected -func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool, error) { - if doer == nil { - return true, nil - } - +func (repo *Repository) IsProtectedBranch(branchName string) (bool, error) { protectedBranch := &ProtectedBranch{ RepoID: repo.ID, BranchName: branchName, @@ -379,27 +375,6 @@ func (repo *Repository) IsProtectedBranch(branchName string, doer *User) (bool, return has, nil } -// IsProtectedBranchForPush checks if branch is protected for push -func (repo *Repository) IsProtectedBranchForPush(branchName string, doer *User) (bool, error) { - if doer == nil { - return true, nil - } - - protectedBranch := &ProtectedBranch{ - RepoID: repo.ID, - BranchName: branchName, - } - - has, err := x.Get(protectedBranch) - if err != nil { - return true, err - } else if has { - return !protectedBranch.CanUserPush(doer.ID), nil - } - - return false, nil -} - // updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with // the users from newWhitelist which have explicit read or write access to the repo. func updateApprovalWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { diff --git a/models/error.go b/models/error.go index 501bf86869..513effdb02 100644 --- a/models/error.go +++ b/models/error.go @@ -985,6 +985,21 @@ func (err ErrInvalidTagName) Error() string { return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName) } +// ErrProtectedTagName represents a "ProtectedTagName" kind of error. +type ErrProtectedTagName struct { + TagName string +} + +// IsErrProtectedTagName checks if an error is a ErrProtectedTagName. +func IsErrProtectedTagName(err error) bool { + _, ok := err.(ErrProtectedTagName) + return ok +} + +func (err ErrProtectedTagName) Error() string { + return fmt.Sprintf("release tag name is protected [tag_name: %s]", err.TagName) +} + // ErrRepoFileAlreadyExists represents a "RepoFileAlreadyExist" kind of error. type ErrRepoFileAlreadyExists struct { Path string diff --git a/models/fixtures/action.yml b/models/fixtures/action.yml index 14cfd90423..e3f3d2a971 100644 --- a/models/fixtures/action.yml +++ b/models/fixtures/action.yml @@ -32,3 +32,27 @@ repo_id: 22 is_private: true created_unix: 1603267920 + +- id: 5 + user_id: 10 + op_type: 1 # create repo + act_user_id: 10 + repo_id: 6 + is_private: true + created_unix: 1603010100 + +- id: 6 + user_id: 10 + op_type: 1 # create repo + act_user_id: 10 + repo_id: 7 + is_private: true + created_unix: 1603011300 + +- id: 7 + user_id: 10 + op_type: 1 # create repo + act_user_id: 10 + repo_id: 8 + is_private: false + created_unix: 1603011540 # grouped with id:7 diff --git a/models/fixtures/repo_archiver.yml b/models/fixtures/repo_archiver.yml new file mode 100644 index 0000000000..ca780a73aa --- /dev/null +++ b/models/fixtures/repo_archiver.yml @@ -0,0 +1 @@ +[] # empty diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index d903a7942f..850ee4041d 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -508,7 +508,6 @@ num_repos: 0 is_active: true - - id: 30 lower_name: user30 @@ -525,3 +524,20 @@ avatar_email: user30@example.com num_repos: 2 is_active: true + +- + id: 31 + lower_name: user31 + name: user31 + full_name: "user31" + email: user31@example.com + passwd_hash_algo: argon2 + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password + type: 0 # individual + salt: ZogKvWdyEx + is_admin: false + visibility: 2 + avatar: avatar31 + avatar_email: user31@example.com + num_repos: 0 + is_active: true diff --git a/models/login_source.go b/models/login_source.go index 098b48a8cd..359b562b65 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -856,7 +856,11 @@ func UserSignIn(username, password string) (*User, error) { return authUser, nil } - log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err) + if IsErrUserNotExist(err) { + log.Debug("Failed to login '%s' via '%s': %v", username, source.Name, err) + } else { + log.Warn("Failed to login '%s' via '%s': %v", username, source.Name, err) + } } return nil, ErrUserNotExist{user.ID, user.Name, 0} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 880f55092d..978ba6368f 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -319,6 +319,10 @@ var migrations = []Migration{ NewMigration("Create PushMirror table", createPushMirrorTable), // v184 -> v185 NewMigration("Rename Task errors to message", renameTaskErrorsToMessage), + // v185 -> v186 + NewMigration("Add new table repo_archiver", addRepoArchiver), + // v186 -> v187 + NewMigration("Create protected tag table", createProtectedTagTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/migrations_test.go b/models/migrations/migrations_test.go index 641d972b8b..26066580d8 100644 --- a/models/migrations/migrations_test.go +++ b/models/migrations/migrations_test.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -55,7 +56,7 @@ func TestMain(m *testing.M) { setting.SetCustomPathAndConf("", "", "") setting.NewContext() - setting.CheckLFSVersion() + git.CheckLFSVersion() setting.InitDBConfig() setting.NewLogServices(true) diff --git a/models/migrations/v181.go b/models/migrations/v181.go index 6ba4edc155..65045593ad 100644 --- a/models/migrations/v181.go +++ b/models/migrations/v181.go @@ -1,3 +1,4 @@ +// Copyright 2021 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. diff --git a/models/migrations/v185.go b/models/migrations/v185.go new file mode 100644 index 0000000000..0969948897 --- /dev/null +++ b/models/migrations/v185.go @@ -0,0 +1,22 @@ +// Copyright 2021 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 addRepoArchiver(x *xorm.Engine) error { + // RepoArchiver represents all archivers + type RepoArchiver struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"index unique(s)"` + Type int `xorm:"unique(s)"` + Status int + CommitID string `xorm:"VARCHAR(40) unique(s)"` + CreatedUnix int64 `xorm:"INDEX NOT NULL created"` + } + return x.Sync2(new(RepoArchiver)) +} diff --git a/models/migrations/v186.go b/models/migrations/v186.go new file mode 100644 index 0000000000..eb6ec7118c --- /dev/null +++ b/models/migrations/v186.go @@ -0,0 +1,26 @@ +// Copyright 2021 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 ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func createProtectedTagTable(x *xorm.Engine) error { + type ProtectedTag struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 + NamePattern string + AllowlistUserIDs []int64 `xorm:"JSON TEXT"` + AllowlistTeamIDs []int64 `xorm:"JSON TEXT"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + return x.Sync2(new(ProtectedTag)) +} diff --git a/models/models.go b/models/models.go index c325fd3811..610933d327 100644 --- a/models/models.go +++ b/models/models.go @@ -136,6 +136,8 @@ func init() { new(RepoTransfer), new(IssueIndex), new(PushMirror), + new(RepoArchiver), + new(ProtectedTag), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/org.go b/models/org.go index 7f9e3cce5b..073b26c2f8 100644 --- a/models/org.go +++ b/models/org.go @@ -455,22 +455,22 @@ func getOwnedOrgsByUserID(sess *xorm.Session, userID int64) ([]*User, error) { Find(&orgs) } -// HasOrgVisible tells if the given user can see the given org -func HasOrgVisible(org, user *User) bool { - return hasOrgVisible(x, org, user) +// HasOrgOrUserVisible tells if the given user can see the given org or user +func HasOrgOrUserVisible(org, user *User) bool { + return hasOrgOrUserVisible(x, org, user) } -func hasOrgVisible(e Engine, org, user *User) bool { +func hasOrgOrUserVisible(e Engine, orgOrUser, user *User) bool { // Not SignedUser if user == nil { - return org.Visibility == structs.VisibleTypePublic + return orgOrUser.Visibility == structs.VisibleTypePublic } - if user.IsAdmin { + if user.IsAdmin || orgOrUser.ID == user.ID { return true } - if (org.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !org.hasMemberWithUserID(e, user.ID) { + if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !orgOrUser.hasMemberWithUserID(e, user.ID) { return false } return true @@ -483,7 +483,7 @@ func HasOrgsVisible(orgs []*User, user *User) bool { } for _, org := range orgs { - if HasOrgVisible(org, user) { + if HasOrgOrUserVisible(org, user) { return true } } diff --git a/models/org_test.go b/models/org_test.go index bed7a6eb86..e494e502dd 100644 --- a/models/org_test.go +++ b/models/org_test.go @@ -586,9 +586,9 @@ func TestHasOrgVisibleTypePublic(t *testing.T) { assert.NoError(t, CreateOrganization(org, owner)) org = AssertExistsAndLoadBean(t, &User{Name: org.Name, Type: UserTypeOrganization}).(*User) - test1 := HasOrgVisible(org, owner) - test2 := HasOrgVisible(org, user3) - test3 := HasOrgVisible(org, nil) + test1 := HasOrgOrUserVisible(org, owner) + test2 := HasOrgOrUserVisible(org, user3) + test3 := HasOrgOrUserVisible(org, nil) assert.True(t, test1) // owner of org assert.True(t, test2) // user not a part of org assert.True(t, test3) // logged out user @@ -609,9 +609,9 @@ func TestHasOrgVisibleTypeLimited(t *testing.T) { assert.NoError(t, CreateOrganization(org, owner)) org = AssertExistsAndLoadBean(t, &User{Name: org.Name, Type: UserTypeOrganization}).(*User) - test1 := HasOrgVisible(org, owner) - test2 := HasOrgVisible(org, user3) - test3 := HasOrgVisible(org, nil) + test1 := HasOrgOrUserVisible(org, owner) + test2 := HasOrgOrUserVisible(org, user3) + test3 := HasOrgOrUserVisible(org, nil) assert.True(t, test1) // owner of org assert.True(t, test2) // user not a part of org assert.False(t, test3) // logged out user @@ -632,9 +632,9 @@ func TestHasOrgVisibleTypePrivate(t *testing.T) { assert.NoError(t, CreateOrganization(org, owner)) org = AssertExistsAndLoadBean(t, &User{Name: org.Name, Type: UserTypeOrganization}).(*User) - test1 := HasOrgVisible(org, owner) - test2 := HasOrgVisible(org, user3) - test3 := HasOrgVisible(org, nil) + test1 := HasOrgOrUserVisible(org, owner) + test2 := HasOrgOrUserVisible(org, user3) + test3 := HasOrgOrUserVisible(org, nil) assert.True(t, test1) // owner of org assert.False(t, test2) // user not a part of org assert.False(t, test3) // logged out user diff --git a/models/protected_tag.go b/models/protected_tag.go new file mode 100644 index 0000000000..88f20dd29a --- /dev/null +++ b/models/protected_tag.go @@ -0,0 +1,131 @@ +// Copyright 2021 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 models + +import ( + "regexp" + "strings" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/gobwas/glob" +) + +// ProtectedTag struct +type ProtectedTag struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 + NamePattern string + RegexPattern *regexp.Regexp `xorm:"-"` + GlobPattern glob.Glob `xorm:"-"` + AllowlistUserIDs []int64 `xorm:"JSON TEXT"` + AllowlistTeamIDs []int64 `xorm:"JSON TEXT"` + + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` +} + +// InsertProtectedTag inserts a protected tag to database +func InsertProtectedTag(pt *ProtectedTag) error { + _, err := x.Insert(pt) + return err +} + +// UpdateProtectedTag updates the protected tag +func UpdateProtectedTag(pt *ProtectedTag) error { + _, err := x.ID(pt.ID).AllCols().Update(pt) + return err +} + +// DeleteProtectedTag deletes a protected tag by ID +func DeleteProtectedTag(pt *ProtectedTag) error { + _, err := x.ID(pt.ID).Delete(&ProtectedTag{}) + return err +} + +// EnsureCompiledPattern ensures the glob pattern is compiled +func (pt *ProtectedTag) EnsureCompiledPattern() error { + if pt.RegexPattern != nil || pt.GlobPattern != nil { + return nil + } + + var err error + if len(pt.NamePattern) >= 2 && strings.HasPrefix(pt.NamePattern, "/") && strings.HasSuffix(pt.NamePattern, "/") { + pt.RegexPattern, err = regexp.Compile(pt.NamePattern[1 : len(pt.NamePattern)-1]) + } else { + pt.GlobPattern, err = glob.Compile(pt.NamePattern) + } + return err +} + +// IsUserAllowed returns true if the user is allowed to modify the tag +func (pt *ProtectedTag) IsUserAllowed(userID int64) (bool, error) { + if base.Int64sContains(pt.AllowlistUserIDs, userID) { + return true, nil + } + + if len(pt.AllowlistTeamIDs) == 0 { + return false, nil + } + + in, err := IsUserInTeams(userID, pt.AllowlistTeamIDs) + if err != nil { + return false, err + } + return in, nil +} + +// GetProtectedTags gets all protected tags of the repository +func (repo *Repository) GetProtectedTags() ([]*ProtectedTag, error) { + tags := make([]*ProtectedTag, 0) + return tags, x.Find(&tags, &ProtectedTag{RepoID: repo.ID}) +} + +// GetProtectedTagByID gets the protected tag with the specific id +func GetProtectedTagByID(id int64) (*ProtectedTag, error) { + tag := new(ProtectedTag) + has, err := x.ID(id).Get(tag) + if err != nil { + return nil, err + } + if !has { + return nil, nil + } + return tag, nil +} + +// IsUserAllowedToControlTag checks if a user can control the specific tag. +// It returns true if the tag name is not protected or the user is allowed to control it. +func IsUserAllowedToControlTag(tags []*ProtectedTag, tagName string, userID int64) (bool, error) { + isAllowed := true + for _, tag := range tags { + err := tag.EnsureCompiledPattern() + if err != nil { + return false, err + } + + if !tag.matchString(tagName) { + continue + } + + isAllowed, err = tag.IsUserAllowed(userID) + if err != nil { + return false, err + } + if isAllowed { + break + } + } + + return isAllowed, nil +} + +func (pt *ProtectedTag) matchString(name string) bool { + if pt.RegexPattern != nil { + return pt.RegexPattern.MatchString(name) + } + return pt.GlobPattern.Match(name) +} diff --git a/models/protected_tag_test.go b/models/protected_tag_test.go new file mode 100644 index 0000000000..3dc895c69f --- /dev/null +++ b/models/protected_tag_test.go @@ -0,0 +1,162 @@ +// Copyright 2021 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 models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsUserAllowed(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + pt := &ProtectedTag{} + allowed, err := pt.IsUserAllowed(1) + assert.NoError(t, err) + assert.False(t, allowed) + + pt = &ProtectedTag{ + AllowlistUserIDs: []int64{1}, + } + allowed, err = pt.IsUserAllowed(1) + assert.NoError(t, err) + assert.True(t, allowed) + + allowed, err = pt.IsUserAllowed(2) + assert.NoError(t, err) + assert.False(t, allowed) + + pt = &ProtectedTag{ + AllowlistTeamIDs: []int64{1}, + } + allowed, err = pt.IsUserAllowed(1) + assert.NoError(t, err) + assert.False(t, allowed) + + allowed, err = pt.IsUserAllowed(2) + assert.NoError(t, err) + assert.True(t, allowed) + + pt = &ProtectedTag{ + AllowlistUserIDs: []int64{1}, + AllowlistTeamIDs: []int64{1}, + } + allowed, err = pt.IsUserAllowed(1) + assert.NoError(t, err) + assert.True(t, allowed) + + allowed, err = pt.IsUserAllowed(2) + assert.NoError(t, err) + assert.True(t, allowed) +} + +func TestIsUserAllowedToControlTag(t *testing.T) { + cases := []struct { + name string + userid int64 + allowed bool + }{ + { + name: "test", + userid: 1, + allowed: true, + }, + { + name: "test", + userid: 3, + allowed: true, + }, + { + name: "gitea", + userid: 1, + allowed: true, + }, + { + name: "gitea", + userid: 3, + allowed: false, + }, + { + name: "test-gitea", + userid: 1, + allowed: true, + }, + { + name: "test-gitea", + userid: 3, + allowed: false, + }, + { + name: "gitea-test", + userid: 1, + allowed: true, + }, + { + name: "gitea-test", + userid: 3, + allowed: true, + }, + { + name: "v-1", + userid: 1, + allowed: false, + }, + { + name: "v-1", + userid: 2, + allowed: true, + }, + { + name: "release", + userid: 1, + allowed: false, + }, + } + + t.Run("Glob", func(t *testing.T) { + protectedTags := []*ProtectedTag{ + { + NamePattern: `*gitea`, + AllowlistUserIDs: []int64{1}, + }, + { + NamePattern: `v-*`, + AllowlistUserIDs: []int64{2}, + }, + { + NamePattern: "release", + }, + } + + for n, c := range cases { + isAllowed, err := IsUserAllowedToControlTag(protectedTags, c.name, c.userid) + assert.NoError(t, err) + assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n) + } + }) + + t.Run("Regex", func(t *testing.T) { + protectedTags := []*ProtectedTag{ + { + NamePattern: `/gitea\z/`, + AllowlistUserIDs: []int64{1}, + }, + { + NamePattern: `/\Av-/`, + AllowlistUserIDs: []int64{2}, + }, + { + NamePattern: "/release/", + }, + } + + for n, c := range cases { + isAllowed, err := IsUserAllowedToControlTag(protectedTags, c.name, c.userid) + assert.NoError(t, err) + assert.Equal(t, c.allowed, isAllowed, "case %d: error should match", n) + } + }) +} diff --git a/models/repo.go b/models/repo.go index dc4e03a28a..92d8427fab 100644 --- a/models/repo.go +++ b/models/repo.go @@ -585,8 +585,7 @@ func (repo *Repository) getReviewers(e Engine, doerID, posterID int64) ([]*User, var users []*User - if repo.IsPrivate || - (repo.Owner.IsOrganization() && repo.Owner.Visibility == api.VisibleTypePrivate) { + if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate { // This a private repository: // Anyone who can read the repository is a requestable reviewer if err := e. @@ -1498,6 +1497,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error { &Mirror{RepoID: repoID}, &Notification{RepoID: repoID}, &ProtectedBranch{RepoID: repoID}, + &ProtectedTag{RepoID: repoID}, &PullRequest{BaseRepoID: repoID}, &PushMirror{RepoID: repoID}, &Release{RepoID: repoID}, @@ -1587,6 +1587,22 @@ func DeleteRepository(doer *User, uid, repoID int64) error { return err } + // Remove archives + var archives []*RepoArchiver + if err = sess.Where("repo_id=?", repoID).Find(&archives); err != nil { + return err + } + + for _, v := range archives { + v.Repo = repo + p, _ := v.RelativePath() + removeStorageWithNotice(sess, storage.RepoArchives, "Delete repo archive file", p) + } + + if _, err := sess.Delete(&RepoArchiver{RepoID: repoID}); err != nil { + return err + } + if repo.NumForks > 0 { if _, err = sess.Exec("UPDATE `repository` SET fork_id=0,is_fork=? WHERE fork_id=?", false, repo.ID); err != nil { log.Error("reset 'fork_id' and 'is_fork': %v", err) @@ -1768,64 +1784,45 @@ func DeleteRepositoryArchives(ctx context.Context) error { func DeleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration) error { log.Trace("Doing: ArchiveCleanup") - if err := x.Where("id > 0").Iterate(new(Repository), func(idx int, bean interface{}) error { - return deleteOldRepositoryArchives(ctx, olderThan, idx, bean) - }); err != nil { - log.Trace("Error: ArchiveClean: %v", err) - return err + for { + var archivers []RepoArchiver + err := x.Where("created_unix < ?", time.Now().Add(-olderThan).Unix()). + Asc("created_unix"). + Limit(100). + Find(&archivers) + if err != nil { + log.Trace("Error: ArchiveClean: %v", err) + return err + } + + for _, archiver := range archivers { + if err := deleteOldRepoArchiver(ctx, &archiver); err != nil { + return err + } + } + if len(archivers) < 100 { + break + } } log.Trace("Finished: ArchiveCleanup") return nil } -func deleteOldRepositoryArchives(ctx context.Context, olderThan time.Duration, idx int, bean interface{}) error { - repo := bean.(*Repository) - basePath := filepath.Join(repo.RepoPath(), "archives") +var delRepoArchiver = new(RepoArchiver) - for _, ty := range []string{"zip", "targz"} { - select { - case <-ctx.Done(): - return ErrCancelledf("before deleting old repository archives with filetype %s for %s", ty, repo.FullName()) - default: - } - - path := filepath.Join(basePath, ty) - file, err := os.Open(path) - if err != nil { - if !os.IsNotExist(err) { - log.Warn("Unable to open directory %s: %v", path, err) - return err - } - - // If the directory doesn't exist, that's okay. - continue - } - - files, err := file.Readdir(0) - file.Close() - if err != nil { - log.Warn("Unable to read directory %s: %v", path, err) - return err - } - - minimumOldestTime := time.Now().Add(-olderThan) - for _, info := range files { - if info.ModTime().Before(minimumOldestTime) && !info.IsDir() { - select { - case <-ctx.Done(): - return ErrCancelledf("before deleting old repository archive file %s with filetype %s for %s", info.Name(), ty, repo.FullName()) - default: - } - toDelete := filepath.Join(path, info.Name()) - // This is a best-effort purge, so we do not check error codes to confirm removal. - if err = util.Remove(toDelete); err != nil { - log.Trace("Unable to delete %s, but proceeding: %v", toDelete, err) - } - } - } +func deleteOldRepoArchiver(ctx context.Context, archiver *RepoArchiver) error { + p, err := archiver.RelativePath() + if err != nil { + return err + } + _, err = x.ID(archiver.ID).Delete(delRepoArchiver) + if err != nil { + return err + } + if err := storage.RepoArchives.Delete(p); err != nil { + log.Error("delete repo archive file failed: %v", err) } - return nil } diff --git a/models/repo_archiver.go b/models/repo_archiver.go new file mode 100644 index 0000000000..833a22ee13 --- /dev/null +++ b/models/repo_archiver.go @@ -0,0 +1,86 @@ +// Copyright 2021 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 models + +import ( + "fmt" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/timeutil" +) + +// RepoArchiverStatus represents repo archive status +type RepoArchiverStatus int + +// enumerate all repo archive statuses +const ( + RepoArchiverGenerating = iota // the archiver is generating + RepoArchiverReady // it's ready +) + +// RepoArchiver represents all archivers +type RepoArchiver struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"index unique(s)"` + Repo *Repository `xorm:"-"` + Type git.ArchiveType `xorm:"unique(s)"` + Status RepoArchiverStatus + CommitID string `xorm:"VARCHAR(40) unique(s)"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"` +} + +// LoadRepo loads repository +func (archiver *RepoArchiver) LoadRepo() (*Repository, error) { + if archiver.Repo != nil { + return archiver.Repo, nil + } + + var repo Repository + has, err := x.ID(archiver.RepoID).Get(&repo) + if err != nil { + return nil, err + } + if !has { + return nil, ErrRepoNotExist{ + ID: archiver.RepoID, + } + } + return &repo, nil +} + +// RelativePath returns relative path +func (archiver *RepoArchiver) RelativePath() (string, error) { + repo, err := archiver.LoadRepo() + if err != nil { + return "", err + } + + return fmt.Sprintf("%s/%s/%s.%s", repo.FullName(), archiver.CommitID[:2], archiver.CommitID, archiver.Type.String()), nil +} + +// GetRepoArchiver get an archiver +func GetRepoArchiver(ctx DBContext, repoID int64, tp git.ArchiveType, commitID string) (*RepoArchiver, error) { + var archiver RepoArchiver + has, err := ctx.e.Where("repo_id=?", repoID).And("`type`=?", tp).And("commit_id=?", commitID).Get(&archiver) + if err != nil { + return nil, err + } + if has { + return &archiver, nil + } + return nil, nil +} + +// AddRepoArchiver adds an archiver +func AddRepoArchiver(ctx DBContext, archiver *RepoArchiver) error { + _, err := ctx.e.Insert(archiver) + return err +} + +// UpdateRepoArchiverStatus updates archiver's status +func UpdateRepoArchiverStatus(ctx DBContext, archiver *RepoArchiver) error { + _, err := ctx.e.ID(archiver.ID).Cols("status").Update(archiver) + return err +} diff --git a/models/repo_permission.go b/models/repo_permission.go index 138613b2e9..4f043a58cc 100644 --- a/models/repo_permission.go +++ b/models/repo_permission.go @@ -176,9 +176,9 @@ func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permiss return } - // Prevent strangers from checking out public repo of private orginization - // Allow user if they are collaborator of a repo within a private orginization but not a member of the orginization itself - if repo.Owner.IsOrganization() && !hasOrgVisible(e, repo.Owner, user) && !isCollaborator { + // Prevent strangers from checking out public repo of private orginization/users + // Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself + if !hasOrgOrUserVisible(e, repo.Owner, user) && !isCollaborator { perm.AccessMode = AccessModeNone return } diff --git a/models/unit_tests.go b/models/unit_tests.go index 5a145fa2c0..f8d6819333 100644 --- a/models/unit_tests.go +++ b/models/unit_tests.go @@ -74,6 +74,8 @@ func MainTest(m *testing.M, pathToGiteaRoot string) { setting.RepoAvatar.Storage.Path = filepath.Join(setting.AppDataPath, "repo-avatars") + setting.RepoArchive.Storage.Path = filepath.Join(setting.AppDataPath, "repo-archive") + if err = storage.Init(); err != nil { fatalTestError("storage.Init: %v\n", err) } diff --git a/models/user.go b/models/user.go index 5998341422..221c840a7f 100644 --- a/models/user.go +++ b/models/user.go @@ -432,6 +432,62 @@ func (u *User) IsPasswordSet() bool { return len(u.Passwd) != 0 } +// IsVisibleToUser check if viewer is able to see user profile +func (u *User) IsVisibleToUser(viewer *User) bool { + return u.isVisibleToUser(x, viewer) +} + +func (u *User) isVisibleToUser(e Engine, viewer *User) bool { + if viewer != nil && viewer.IsAdmin { + return true + } + + switch u.Visibility { + case structs.VisibleTypePublic: + return true + case structs.VisibleTypeLimited: + if viewer == nil || viewer.IsRestricted { + return false + } + return true + case structs.VisibleTypePrivate: + if viewer == nil || viewer.IsRestricted { + return false + } + + // If they follow - they see each over + follower := IsFollowing(u.ID, viewer.ID) + if follower { + return true + } + + // Now we need to check if they in some organization together + count, err := x.Table("team_user"). + Where( + builder.And( + builder.Eq{"uid": viewer.ID}, + builder.Or( + builder.Eq{"org_id": u.ID}, + builder.In("org_id", + builder.Select("org_id"). + From("team_user", "t2"). + Where(builder.Eq{"uid": u.ID}))))). + Count(new(TeamUser)) + if err != nil { + return false + } + + if count < 0 { + // No common organization + return false + } + + // they are in an organization together + return true + } + return false +} + // IsOrganization returns true if user is actually a organization. func (u *User) IsOrganization() bool { return u.Type == UserTypeOrganization @@ -796,8 +852,13 @@ func IsUsableUsername(name string) error { return isUsableName(reservedUsernames, reservedUserPatterns, name) } +// CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation +type CreateUserOverwriteOptions struct { + Visibility structs.VisibleType +} + // CreateUser creates record of a new user. -func CreateUser(u *User) (err error) { +func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { if err = IsUsableUsername(u.Name); err != nil { return err } @@ -831,8 +892,6 @@ func CreateUser(u *User) (err error) { return ErrEmailAlreadyUsed{u.Email} } - u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate - u.LowerName = strings.ToLower(u.Name) u.AvatarEmail = u.Email if u.Rands, err = GetUserSalt(); err != nil { @@ -841,10 +900,18 @@ func CreateUser(u *User) (err error) { if err = u.SetPassword(u.Passwd); err != nil { return err } + + // set system defaults + u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate + u.Visibility = setting.Service.DefaultUserVisibilityMode u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification u.MaxRepoCreation = -1 u.Theme = setting.UI.DefaultTheme + // overwrite defaults if set + if len(overwriteDefault) != 0 && overwriteDefault[0] != nil { + u.Visibility = overwriteDefault[0].Visibility + } if _, err = sess.Insert(u); err != nil { return err @@ -1527,10 +1594,9 @@ func (opts *SearchUserOptions) toConds() builder.Cond { cond = cond.And(keywordCond) } + // If visibility filtered if len(opts.Visible) > 0 { cond = cond.And(builder.In("visibility", opts.Visible)) - } else { - cond = cond.And(builder.In("visibility", structs.VisibleTypePublic)) } if opts.Actor != nil { @@ -1543,16 +1609,27 @@ func (opts *SearchUserOptions) toConds() builder.Cond { exprCond = builder.Expr("org_user.org_id = \"user\".id") } - var accessCond builder.Cond - if !opts.Actor.IsRestricted { - accessCond = builder.Or( - builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))), - builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) - } else { - // restricted users only see orgs they are a member of - accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}))) + // If Admin - they see all users! + if !opts.Actor.IsAdmin { + // Force visiblity for privacy + var accessCond builder.Cond + if !opts.Actor.IsRestricted { + accessCond = builder.Or( + builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))), + builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) + } else { + // restricted users only see orgs they are a member of + accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}))) + } + // Don't forget about self + accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID}) + cond = cond.And(accessCond) } - cond = cond.And(accessCond) + + } else { + // Force visiblity for privacy + // Not logged in - only public users + cond = cond.And(builder.In("visibility", structs.VisibleTypePublic)) } if opts.UID > 0 { diff --git a/models/user_heatmap.go b/models/user_heatmap.go index 0e2767212e..306bd1819b 100644 --- a/models/user_heatmap.go +++ b/models/user_heatmap.go @@ -32,17 +32,14 @@ func getUserHeatmapData(user *User, team *Team, doer *User) ([]*UserHeatmapData, return hdata, nil } - var groupBy string + // Group by 15 minute intervals which will allow the client to accurately shift the timestamp to their timezone. + // The interval is based on the fact that there are timezones such as UTC +5:30 and UTC +12:45. + groupBy := "created_unix / 900 * 900" groupByName := "timestamp" // We need this extra case because mssql doesn't allow grouping by alias switch { - case setting.Database.UseSQLite3: - groupBy = "strftime('%s', strftime('%Y-%m-%d', created_unix, 'unixepoch'))" case setting.Database.UseMySQL: - groupBy = "UNIX_TIMESTAMP(DATE(FROM_UNIXTIME(created_unix)))" - case setting.Database.UsePostgreSQL: - groupBy = "extract(epoch from date_trunc('day', to_timestamp(created_unix)))" + groupBy = "created_unix DIV 900 * 900" case setting.Database.UseMSSQL: - groupBy = "datediff(SECOND, '19700101', dateadd(DAY, 0, datediff(day, 0, dateadd(s, created_unix, '19700101'))))" groupByName = groupBy } diff --git a/models/user_heatmap_test.go b/models/user_heatmap_test.go index 31e78a19cc..b2aaea6499 100644 --- a/models/user_heatmap_test.go +++ b/models/user_heatmap_test.go @@ -19,12 +19,20 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { CountResult int JSONResult string }{ - {2, 2, 1, `[{"timestamp":1603152000,"contributions":1}]`}, // self looks at action in private repo - {2, 1, 1, `[{"timestamp":1603152000,"contributions":1}]`}, // admin looks at action in private repo - {2, 3, 0, `[]`}, // other user looks at action in private repo - {2, 0, 0, `[]`}, // nobody looks at action in private repo - {16, 15, 1, `[{"timestamp":1603238400,"contributions":1}]`}, // collaborator looks at action in private repo - {3, 3, 0, `[]`}, // no action action not performed by target user + // self looks at action in private repo + {2, 2, 1, `[{"timestamp":1603227600,"contributions":1}]`}, + // admin looks at action in private repo + {2, 1, 1, `[{"timestamp":1603227600,"contributions":1}]`}, + // other user looks at action in private repo + {2, 3, 0, `[]`}, + // nobody looks at action in private repo + {2, 0, 0, `[]`}, + // collaborator looks at action in private repo + {16, 15, 1, `[{"timestamp":1603267200,"contributions":1}]`}, + // no action action not performed by target user + {3, 3, 0, `[]`}, + // multiple actions performed with two grouped together + {10, 10, 3, `[{"timestamp":1603009800,"contributions":1},{"timestamp":1603010700,"contributions":2}]`}, } // Prepare assert.NoError(t, PrepareTestDatabase()) @@ -51,9 +59,13 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { // Get the heatmap and compare heatmap, err := GetUserHeatmapDataByUser(user, doer) + var contributions int + for _, hm := range heatmap { + contributions += int(hm.Contributions) + } assert.NoError(t, err) - assert.Len(t, heatmap, len(actions), "invalid action count: did the test data became too old?") - assert.Len(t, heatmap, tc.CountResult, fmt.Sprintf("testcase %d", i)) + assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?") + assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase %d", i)) // Test JSON rendering json := jsoniter.ConfigCompatibleWithStandardLibrary diff --git a/modules/context/context.go b/modules/context/context.go index 7b3fd2899a..64f8b12084 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -380,6 +380,21 @@ func (ctx *Context) ServeFile(file string, names ...string) { http.ServeFile(ctx.Resp, ctx.Req, file) } +// ServeStream serves file via io stream +func (ctx *Context) ServeStream(rd io.Reader, name string) { + ctx.Resp.Header().Set("Content-Description", "File Transfer") + ctx.Resp.Header().Set("Content-Type", "application/octet-stream") + ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+name) + ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary") + ctx.Resp.Header().Set("Expires", "0") + ctx.Resp.Header().Set("Cache-Control", "must-revalidate") + ctx.Resp.Header().Set("Pragma", "public") + _, err := io.Copy(ctx.Resp, rd) + if err != nil { + ctx.ServerError("Download file failed", err) + } +} + // Error returned an error to web browser func (ctx *Context) Error(status int, contents ...string) { var v = http.StatusText(status) diff --git a/modules/convert/user.go b/modules/convert/user.go index 07a4efd41a..164ffb71fd 100644 --- a/modules/convert/user.go +++ b/modules/convert/user.go @@ -62,10 +62,14 @@ func toUser(user *models.User, signed, authed bool) *api.User { Following: user.NumFollowing, StarredRepos: user.NumStars, } + + result.Visibility = user.Visibility.String() + // hide primary email if API caller is anonymous or user keep email private if signed && (!user.KeepEmailPrivate || authed) { result.Email = user.Email } + // only site admin will get these information and possibly user himself if authed { result.IsAdmin = user.IsAdmin @@ -76,3 +80,18 @@ func toUser(user *models.User, signed, authed bool) *api.User { } return result } + +// User2UserSettings return UserSettings based on a user +func User2UserSettings(user *models.User) api.UserSettings { + return api.UserSettings{ + FullName: user.FullName, + Website: user.Website, + Location: user.Location, + Language: user.Language, + Description: user.Description, + Theme: user.Theme, + HideEmail: user.KeepEmailPrivate, + HideActivity: user.KeepActivityPrivate, + DiffViewStyle: user.DiffViewStyle, + } +} diff --git a/modules/convert/user_test.go b/modules/convert/user_test.go index 7837910ffe..679c4f9894 100644 --- a/modules/convert/user_test.go +++ b/modules/convert/user_test.go @@ -8,6 +8,7 @@ import ( "testing" "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) @@ -27,4 +28,11 @@ func TestUser_ToUser(t *testing.T) { apiUser = toUser(user1, false, false) assert.False(t, apiUser.IsAdmin) + assert.EqualValues(t, api.VisibleTypePublic.String(), apiUser.Visibility) + + user31 := models.AssertExistsAndLoadBean(t, &models.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate}).(*models.User) + + apiUser = toUser(user31, true, true) + assert.False(t, apiUser.IsAdmin) + assert.EqualValues(t, api.VisibleTypePrivate.String(), apiUser.Visibility) } diff --git a/modules/doctor/checkOldArchives.go b/modules/doctor/checkOldArchives.go new file mode 100644 index 0000000000..a4e2ffbd1f --- /dev/null +++ b/modules/doctor/checkOldArchives.go @@ -0,0 +1,59 @@ +// Copyright 2021 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 doctor + +import ( + "os" + "path/filepath" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" +) + +func checkOldArchives(logger log.Logger, autofix bool) error { + numRepos := 0 + numReposUpdated := 0 + err := iterateRepositories(func(repo *models.Repository) error { + if repo.IsEmpty { + return nil + } + + p := filepath.Join(repo.RepoPath(), "archives") + isDir, err := util.IsDir(p) + if err != nil { + log.Warn("check if %s is directory failed: %v", p, err) + } + if isDir { + numRepos++ + if autofix { + if err := os.RemoveAll(p); err == nil { + numReposUpdated++ + } else { + log.Warn("remove %s failed: %v", p, err) + } + } + } + return nil + }) + + if autofix { + logger.Info("%d / %d old archives in repository deleted", numReposUpdated, numRepos) + } else { + logger.Info("%d old archives in repository need to be deleted", numRepos) + } + + return err +} + +func init() { + Register(&Check{ + Title: "Check old archives", + Name: "check-old-archives", + IsDefault: false, + Run: checkOldArchives, + Priority: 7, + }) +} diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 678b184708..bdf82bde89 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -12,6 +12,8 @@ import ( "strconv" "strings" + "code.gitea.io/gitea/modules/log" + "github.com/djherbis/buffer" "github.com/djherbis/nio/v3" ) @@ -99,7 +101,7 @@ func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err er } idx := strings.IndexByte(typ, ' ') if idx < 0 { - log("missing space typ: %s", typ) + log.Debug("missing space typ: %s", typ) err = ErrNotExist{ID: string(sha)} return } @@ -230,7 +232,7 @@ func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fn } idx := bytes.IndexByte(readBytes, ' ') if idx < 0 { - log("missing space in readBytes ParseTreeLine: %s", readBytes) + log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes) err = &ErrNotExist{} return diff --git a/modules/git/blob.go b/modules/git/blob.go index 732356e5b2..5831bc3735 100644 --- a/modules/git/blob.go +++ b/modules/git/blob.go @@ -34,7 +34,7 @@ func (b *Blob) GetBlobContent() (string, error) { return string(buf), nil } -// GetBlobLineCount gets line count of lob as raw text +// GetBlobLineCount gets line count of the blob func (b *Blob) GetBlobLineCount() (int, error) { reader, err := b.DataAsync() if err != nil { @@ -42,10 +42,14 @@ func (b *Blob) GetBlobLineCount() (int, error) { } defer reader.Close() buf := make([]byte, 32*1024) - count := 0 + count := 1 lineSep := []byte{'\n'} + + c, err := reader.Read(buf) + if c == 0 && err == io.EOF { + return 0, nil + } for { - c, err := reader.Read(buf) count += bytes.Count(buf[:c], lineSep) switch { case err == io.EOF: @@ -53,6 +57,7 @@ func (b *Blob) GetBlobLineCount() (int, error) { case err != nil: return count, err } + c, err = reader.Read(buf) } } diff --git a/modules/git/blob_nogogit.go b/modules/git/blob_nogogit.go index cdaeb636a9..5b42920ebe 100644 --- a/modules/git/blob_nogogit.go +++ b/modules/git/blob_nogogit.go @@ -12,6 +12,8 @@ import ( "io" "io/ioutil" "math" + + "code.gitea.io/gitea/modules/log" ) // Blob represents a Git object. @@ -69,12 +71,12 @@ func (b *Blob) Size() int64 { defer cancel() _, err := wr.Write([]byte(b.ID.String() + "\n")) if err != nil { - log("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) + log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) return 0 } _, _, b.size, err = ReadBatchLine(rd) if err != nil { - log("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) + log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) return 0 } diff --git a/modules/git/command.go b/modules/git/command.go index ef78464d5f..d83c42fdc2 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -15,6 +15,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" ) @@ -22,8 +23,8 @@ var ( // GlobalCommandArgs global command args for external package setting GlobalCommandArgs []string - // DefaultCommandExecutionTimeout default command execution timeout duration - DefaultCommandExecutionTimeout = 360 * time.Second + // defaultCommandExecutionTimeout default command execution timeout duration + defaultCommandExecutionTimeout = 360 * time.Second ) // DefaultLocale is the default LC_ALL to run git commands in. @@ -110,13 +111,13 @@ func (c *Command) RunInDirTimeoutEnvFullPipeline(env []string, timeout time.Dura // it pipes stdout and stderr to given io.Writer and passes in an io.Reader as stdin. Between cmd.Start and cmd.Wait the passed in function is run. func (c *Command) RunInDirTimeoutEnvFullPipelineFunc(env []string, timeout time.Duration, dir string, stdout, stderr io.Writer, stdin io.Reader, fn func(context.Context, context.CancelFunc) error) error { if timeout == -1 { - timeout = DefaultCommandExecutionTimeout + timeout = defaultCommandExecutionTimeout } if len(dir) == 0 { - log(c.String()) + log.Debug("%s", c) } else { - log("%s: %v", dir, c) + log.Debug("%s: %v", dir, c) } ctx, cancel := context.WithTimeout(c.parentContext, timeout) @@ -197,9 +198,12 @@ func (c *Command) RunInDirTimeoutEnv(env []string, timeout time.Duration, dir st if err := c.RunInDirTimeoutEnvPipeline(env, timeout, dir, stdout, stderr); err != nil { return nil, ConcatenateError(err, stderr.String()) } - - if stdout.Len() > 0 { - log("stdout:\n%s", stdout.Bytes()[:1024]) + if stdout.Len() > 0 && log.IsTrace() { + tracelen := stdout.Len() + if tracelen > 1024 { + tracelen = 1024 + } + log.Trace("Stdout:\n %s", stdout.Bytes()[:tracelen]) } return stdout.Bytes(), nil } diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index 2283510d96..060ecba261 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -12,6 +12,8 @@ import ( "io" "path" "sort" + + "code.gitea.io/gitea/modules/log" ) // GetCommitsInfo gets information of all commits that are corresponding to these entries @@ -78,7 +80,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath commitsInfo[i].SubModuleFile = subModuleFile } } else { - log("missing commit for %s", entry.Name()) + log.Debug("missing commit for %s", entry.Name()) } } diff --git a/modules/git/diff.go b/modules/git/diff.go index 5da53568e5..20f25c1bee 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -15,6 +15,7 @@ import ( "strconv" "strings" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" ) @@ -113,7 +114,7 @@ func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHu righHunk, _ = strconv.Atoi(rightRange[1]) } } else { - log("Parse line number failed: %v", diffhunk) + log.Debug("Parse line number failed: %v", diffhunk) rightLine = leftLine righHunk = leftHunk } diff --git a/modules/git/error.go b/modules/git/error.go index 85a938a7da..387dd724e5 100644 --- a/modules/git/error.go +++ b/modules/git/error.go @@ -159,3 +159,20 @@ func (err *ErrPushRejected) GenerateMessage() { } err.Message = strings.TrimSpace(messageBuilder.String()) } + +// ErrMoreThanOne represents an error if pull request fails when there are more than one sources (branch, tag) with the same name +type ErrMoreThanOne struct { + StdOut string + StdErr string + Err error +} + +// IsErrMoreThanOne checks if an error is a ErrMoreThanOne +func IsErrMoreThanOne(err error) bool { + _, ok := err.(*ErrMoreThanOne) + return ok +} + +func (err *ErrMoreThanOne) Error() string { + return fmt.Sprintf("ErrMoreThanOne Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) +} diff --git a/modules/git/git.go b/modules/git/git.go index 6b15138a5c..ef6ec0c2bf 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -14,14 +14,12 @@ import ( "time" "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/setting" "github.com/hashicorp/go-version" ) var ( - // Debug enables verbose logging on everything. - // This should be false in case Gogs starts in SSH mode. - Debug = false // Prefix the log prefix Prefix = "[git-module] " // GitVersionRequired is the minimum Git version required @@ -41,19 +39,6 @@ var ( goVersionLessThan115 = true ) -func log(format string, args ...interface{}) { - if !Debug { - return - } - - fmt.Print(Prefix) - if len(args) == 0 { - fmt.Println(format) - } else { - fmt.Printf(format+"\n", args...) - } -} - // LocalVersion returns current Git version from shell. func LocalVersion() (*version.Version, error) { if err := LoadGitVersion(); err != nil { @@ -122,10 +107,42 @@ func SetExecutablePath(path string) error { return nil } +// VersionInfo returns git version information +func VersionInfo() string { + var format = "Git Version: %s" + var args = []interface{}{gitVersion.Original()} + // Since git wire protocol has been released from git v2.18 + if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { + format += ", Wire Protocol %s Enabled" + args = append(args, "Version 2") // for focus color + } + + return fmt.Sprintf(format, args...) +} + // Init initializes git module func Init(ctx context.Context) error { DefaultContext = ctx + defaultCommandExecutionTimeout = time.Duration(setting.Git.Timeout.Default) * time.Second + + if err := SetExecutablePath(setting.Git.Path); err != nil { + return err + } + + // force cleanup args + GlobalCommandArgs = []string{} + + if CheckGitVersionAtLeast("2.9") == nil { + // Explicitly disable credential helper, otherwise Git credentials might leak + GlobalCommandArgs = append(GlobalCommandArgs, "-c", "credential.helper=") + } + + // Since git wire protocol has been released from git v2.18 + if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { + GlobalCommandArgs = append(GlobalCommandArgs, "-c", "protocol.version=2") + } + // Save current git version on init to gitVersion otherwise it would require an RWMutex if err := LoadGitVersion(); err != nil { return err diff --git a/modules/git/git_test.go b/modules/git/git_test.go index 27951d639b..c62a55badc 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -9,6 +9,8 @@ import ( "fmt" "os" "testing" + + "code.gitea.io/gitea/modules/log" ) func fatalTestError(fmtStr string, args ...interface{}) { @@ -17,6 +19,8 @@ func fatalTestError(fmtStr string, args ...interface{}) { } func TestMain(m *testing.M) { + _ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`) + if err := Init(context.Background()); err != nil { fatalTestError("Init failed: %v", err) } diff --git a/modules/git/hook.go b/modules/git/hook.go index c23fbf8aa1..7007d23be2 100644 --- a/modules/git/hook.go +++ b/modules/git/hook.go @@ -1,4 +1,5 @@ // Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2021 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. @@ -12,6 +13,7 @@ import ( "path/filepath" "strings" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) @@ -126,11 +128,11 @@ const ( // SetUpdateHook writes given content to update hook of the repository. func SetUpdateHook(repoPath, content string) (err error) { - log("Setting update hook: %s", repoPath) + log.Debug("Setting update hook: %s", repoPath) hookPath := path.Join(repoPath, HookPathUpdate) isExist, err := util.IsExist(hookPath) if err != nil { - log("Unable to check if %s exists. Error: %v", hookPath, err) + log.Debug("Unable to check if %s exists. Error: %v", hookPath, err) return err } if isExist { diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index 37a59e1fa8..e2d296641f 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -7,6 +7,8 @@ package git import ( "crypto/sha256" "fmt" + + "code.gitea.io/gitea/modules/log" ) // Cache represents a caching interface @@ -24,6 +26,6 @@ func (c *LastCommitCache) getCacheKey(repoPath, ref, entryPath string) string { // Put put the last commit id with commit and entry path func (c *LastCommitCache) Put(ref, entryPath, commitID string) error { - log("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID) + log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID) return c.cache.Put(c.getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl()) } diff --git a/modules/git/last_commit_cache_gogit.go b/modules/git/last_commit_cache_gogit.go index 16fb1c988c..b8e0db46a9 100644 --- a/modules/git/last_commit_cache_gogit.go +++ b/modules/git/last_commit_cache_gogit.go @@ -10,6 +10,8 @@ import ( "context" "path" + "code.gitea.io/gitea/modules/log" + "github.com/go-git/go-git/v5/plumbing/object" cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph" ) @@ -41,9 +43,9 @@ func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl func() int64, func (c *LastCommitCache) Get(ref, entryPath string) (interface{}, error) { v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath)) if vs, ok := v.(string); ok { - log("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs) + log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs) if commit, ok := c.commitCache[vs]; ok { - log("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs) + log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs) return commit, nil } id, err := c.repo.ConvertToSHA1(vs) diff --git a/modules/git/last_commit_cache_nogogit.go b/modules/git/last_commit_cache_nogogit.go index 84c8ee132c..ff9f9ff1cf 100644 --- a/modules/git/last_commit_cache_nogogit.go +++ b/modules/git/last_commit_cache_nogogit.go @@ -10,6 +10,8 @@ import ( "bufio" "context" "path" + + "code.gitea.io/gitea/modules/log" ) // LastCommitCache represents a cache to store last commit @@ -39,9 +41,9 @@ func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl func() int64, func (c *LastCommitCache) Get(ref, entryPath string, wr WriteCloserError, rd *bufio.Reader) (interface{}, error) { v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath)) if vs, ok := v.(string); ok { - log("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs) + log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs) if commit, ok := c.commitCache[vs]; ok { - log("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs) + log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs) return commit, nil } id, err := c.repo.ConvertToSHA1(vs) diff --git a/modules/git/lfs.go b/modules/git/lfs.go new file mode 100644 index 0000000000..79049c9824 --- /dev/null +++ b/modules/git/lfs.go @@ -0,0 +1,37 @@ +// Copyright 2021 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 git + +import ( + "sync" + + logger "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +var once sync.Once + +// CheckLFSVersion will check lfs version, if not satisfied, then disable it. +func CheckLFSVersion() { + if setting.LFS.StartServer { + //Disable LFS client hooks if installed for the current OS user + //Needs at least git v2.1.2 + + err := LoadGitVersion() + if err != nil { + logger.Fatal("Error retrieving git version: %v", err) + } + + if CheckGitVersionAtLeast("2.1.2") != nil { + setting.LFS.StartServer = false + logger.Error("LFS server support needs at least Git v2.1.2") + } else { + once.Do(func() { + GlobalCommandArgs = append(GlobalCommandArgs, "-c", "filter.lfs.required=", + "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=") + }) + } + } +} diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_nogogit.go index b45b31f239..667111ec4a 100644 --- a/modules/git/parse_nogogit.go +++ b/modules/git/parse_nogogit.go @@ -13,6 +13,8 @@ import ( "io" "strconv" "strings" + + "code.gitea.io/gitea/modules/log" ) // ParseTreeEntries parses the output of a `git ls-tree -l` command. @@ -120,7 +122,7 @@ loop: case "40000": entry.entryMode = EntryModeTree default: - log("Unknown mode: %v", string(mode)) + log.Debug("Unknown mode: %v", string(mode)) return nil, fmt.Errorf("unknown mode: %v", string(mode)) } diff --git a/modules/git/repo.go b/modules/git/repo.go index e06cd43935..43f329f448 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -225,6 +225,13 @@ func Push(repoPath string, opts PushOptions) error { } err.GenerateMessage() return err + } else if strings.Contains(errbuf.String(), "matches more than one") { + err := &ErrMoreThanOne{ + StdOut: outbuf.String(), + StdErr: errbuf.String(), + Err: err, + } + return err } } diff --git a/modules/git/commit_archive.go b/modules/git/repo_archive.go similarity index 60% rename from modules/git/commit_archive.go rename to modules/git/repo_archive.go index d075ba0911..07003aa6b2 100644 --- a/modules/git/commit_archive.go +++ b/modules/git/repo_archive.go @@ -8,6 +8,7 @@ package git import ( "context" "fmt" + "io" "path/filepath" "strings" ) @@ -33,32 +34,28 @@ func (a ArchiveType) String() string { return "unknown" } -// CreateArchiveOpts represents options for creating an archive -type CreateArchiveOpts struct { - Format ArchiveType - Prefix bool -} - // CreateArchive create archive content to the target path -func (c *Commit) CreateArchive(ctx context.Context, target string, opts CreateArchiveOpts) error { - if opts.Format.String() == "unknown" { - return fmt.Errorf("unknown format: %v", opts.Format) +func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, target io.Writer, usePrefix bool, commitID string) error { + if format.String() == "unknown" { + return fmt.Errorf("unknown format: %v", format) } args := []string{ "archive", } - if opts.Prefix { - args = append(args, "--prefix="+filepath.Base(strings.TrimSuffix(c.repo.Path, ".git"))+"/") + if usePrefix { + args = append(args, "--prefix="+filepath.Base(strings.TrimSuffix(repo.Path, ".git"))+"/") } args = append(args, - "--format="+opts.Format.String(), - "-o", - target, - c.ID.String(), + "--format="+format.String(), + commitID, ) - _, err := NewCommandContext(ctx, args...).RunInDir(c.repo.Path) - return err + var stderr strings.Builder + err := NewCommandContext(ctx, args...).RunInDirPipeline(repo.Path, target, &stderr) + if err != nil { + return ConcatenateError(err, stderr.String()) + } + return nil } diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index c7d6019d77..1675967d18 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -12,6 +12,8 @@ import ( "context" "errors" "path/filepath" + + "code.gitea.io/gitea/modules/log" ) // Repository represents a Git repository. @@ -54,7 +56,7 @@ func OpenRepository(repoPath string) (*Repository, error) { // CatFileBatch obtains a CatFileBatch for this repository func (repo *Repository) CatFileBatch() (WriteCloserError, *bufio.Reader, func()) { if repo.batchCancel == nil || repo.batchReader.Buffered() > 0 { - log("Opening temporary cat file batch for: %s", repo.Path) + log.Debug("Opening temporary cat file batch for: %s", repo.Path) return CatFileBatch(repo.Path) } return repo.batchWriter, repo.batchReader, func() {} @@ -63,7 +65,7 @@ func (repo *Repository) CatFileBatch() (WriteCloserError, *bufio.Reader, func()) // CatFileBatchCheck obtains a CatFileBatchCheck for this repository func (repo *Repository) CatFileBatchCheck() (WriteCloserError, *bufio.Reader, func()) { if repo.checkCancel == nil || repo.checkReader.Buffered() > 0 { - log("Opening temporary cat file batch-check: %s", repo.Path) + log.Debug("Opening temporary cat file batch-check: %s", repo.Path) return CatFileBatchCheck(repo.Path) } return repo.checkWriter, repo.checkReader, func() {} diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go index dd34e48899..7d10b8ba0f 100644 --- a/modules/git/repo_branch_nogogit.go +++ b/modules/git/repo_branch_nogogit.go @@ -12,6 +12,8 @@ import ( "bytes" "io" "strings" + + "code.gitea.io/gitea/modules/log" ) // IsObjectExist returns true if given reference exists in the repository. @@ -24,7 +26,7 @@ func (repo *Repository) IsObjectExist(name string) bool { defer cancel() _, err := wr.Write([]byte(name + "\n")) if err != nil { - log("Error writing to CatFileBatchCheck %v", err) + log.Debug("Error writing to CatFileBatchCheck %v", err) return false } sha, _, _, err := ReadBatchLine(rd) @@ -41,7 +43,7 @@ func (repo *Repository) IsReferenceExist(name string) bool { defer cancel() _, err := wr.Write([]byte(name + "\n")) if err != nil { - log("Error writing to CatFileBatchCheck %v", err) + log.Debug("Error writing to CatFileBatchCheck %v", err) return false } _, _, _, err = ReadBatchLine(rd) diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 664a7445dd..815aa141e5 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -12,6 +12,8 @@ import ( "io/ioutil" "strconv" "strings" + + "code.gitea.io/gitea/modules/setting" ) // GetBranchCommitID returns last commit ID string of given branch. @@ -85,12 +87,6 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { return commits.Front().Value.(*Commit), nil } -// CommitsRangeSize the default commits range size -var CommitsRangeSize = 50 - -// BranchesRangeSize the default branches range size -var BranchesRangeSize = 20 - func (repo *Repository) commitsByRange(id SHA1, page, pageSize int) (*list.List, error) { stdout, err := NewCommand("log", id.String(), "--skip="+strconv.Itoa((page-1)*pageSize), "--max-count="+strconv.Itoa(pageSize), prettyLogFormat).RunInDirBytes(repo.Path) @@ -206,7 +202,7 @@ func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) { // CommitsByFileAndRange return the commits according revison file and the page func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) (*list.List, error) { - skip := (page - 1) * CommitsRangeSize + skip := (page - 1) * setting.Git.CommitsRangeSize stdoutReader, stdoutWriter := io.Pipe() defer func() { @@ -216,7 +212,7 @@ func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) ( go func() { stderr := strings.Builder{} err := NewCommand("log", revision, "--follow", - "--max-count="+strconv.Itoa(CommitsRangeSize*page), + "--max-count="+strconv.Itoa(setting.Git.CommitsRangeSize*page), prettyLogFormat, "--", file). RunInDirPipeline(repo.Path, stdoutWriter, &stderr) if err != nil { @@ -247,7 +243,7 @@ func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) ( // CommitsByFileAndRangeNoFollow return the commits according revison file and the page func (repo *Repository) CommitsByFileAndRangeNoFollow(revision, file string, page int) (*list.List, error) { stdout, err := NewCommand("log", revision, "--skip="+strconv.Itoa((page-1)*50), - "--max-count="+strconv.Itoa(CommitsRangeSize), prettyLogFormat, "--", file).RunInDirBytes(repo.Path) + "--max-count="+strconv.Itoa(setting.Git.CommitsRangeSize), prettyLogFormat, "--", file).RunInDirBytes(repo.Path) if err != nil { return nil, err } diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index d00c1bfc67..afd5166f1d 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -12,6 +12,8 @@ import ( "io" "io/ioutil" "strings" + + "code.gitea.io/gitea/modules/log" ) // ResolveReference resolves a name to a reference @@ -110,7 +112,7 @@ func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id SHA1) (*Co return commit, nil default: - log("Unknown typ: %s", typ) + log.Debug("Unknown typ: %s", typ) _, err = rd.Discard(int(size) + 1) if err != nil { return nil, err diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go index 46b084cf01..1684f21d16 100644 --- a/modules/git/repo_language_stats_nogogit.go +++ b/modules/git/repo_language_stats_nogogit.go @@ -13,6 +13,7 @@ import ( "math" "code.gitea.io/gitea/modules/analyze" + "code.gitea.io/gitea/modules/log" "github.com/go-enry/go-enry/v2" ) @@ -34,19 +35,19 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } shaBytes, typ, size, err := ReadBatchLine(batchReader) if typ != "commit" { - log("Unable to get commit for: %s. Err: %v", commitID, err) + log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, ErrNotExist{commitID, ""} } sha, err := NewIDFromString(string(shaBytes)) if err != nil { - log("Unable to get commit for: %s. Err: %v", commitID, err) + log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, ErrNotExist{commitID, ""} } commit, err := CommitFromReader(repo, sha, io.LimitReader(batchReader, size)) if err != nil { - log("Unable to get commit for: %s. Err: %v", commitID, err) + log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, err } if _, err = batchReader.Discard(1); err != nil { @@ -79,7 +80,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } _, _, size, err := ReadBatchLine(batchReader) if err != nil { - log("Error reading blob: %s Err: %v", f.ID.String(), err) + log.Debug("Error reading blob: %s Err: %v", f.ID.String(), err) return nil, err } diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 59ab702096..d91c3ca979 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -8,6 +8,8 @@ package git import ( "fmt" "strings" + + "code.gitea.io/gitea/modules/log" ) // TagPrefix tags prefix path on the repository @@ -33,7 +35,7 @@ func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error func (repo *Repository) getTag(tagID SHA1, name string) (*Tag, error) { t, ok := repo.tagCache.Get(tagID.String()) if ok { - log("Hit cache: %s", tagID) + log.Debug("Hit cache: %s", tagID) tagClone := *t.(*Tag) tagClone.Name = name // This is necessary because lightweight tags may have same id return &tagClone, nil diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index e22e9d5b32..568035fbb7 100644 --- a/modules/highlight/highlight.go +++ b/modules/highlight/highlight.go @@ -33,7 +33,7 @@ var ( once sync.Once - cache *lru.ARCCache + cache *lru.TwoQueueCache ) // NewContext loads custom highlight map from local config @@ -45,7 +45,7 @@ func NewContext() { } // The size 512 is simply a conservative rule of thumb - c, err := lru.NewARC(512) + c, err := lru.New2Q(512) if err != nil { panic(fmt.Sprintf("failed to initialize LRU cache for highlighter: %s", err)) } diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go index 6572b0ee1e..8a4df89511 100644 --- a/modules/markup/csv/csv.go +++ b/modules/markup/csv/csv.go @@ -10,6 +10,7 @@ import ( "html" "io" "io/ioutil" + "regexp" "strconv" "code.gitea.io/gitea/modules/csv" @@ -38,6 +39,15 @@ func (Renderer) Extensions() []string { return []string{".csv", ".tsv"} } +// SanitizerRules implements markup.Renderer +func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { + return []setting.MarkupSanitizerRule{ + {Element: "table", AllowAttr: "class", Regexp: regexp.MustCompile(`data-table`)}, + {Element: "th", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)}, + {Element: "td", AllowAttr: "class", Regexp: regexp.MustCompile(`line-num`)}, + } +} + func writeField(w io.Writer, element, class, field string) error { if _, err := io.WriteString(w, "<"); err != nil { return err diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go index 62814c9914..c849f505e7 100644 --- a/modules/markup/external/external.go +++ b/modules/markup/external/external.go @@ -30,7 +30,7 @@ func RegisterRenderers() { // Renderer implements markup.Renderer for external tools type Renderer struct { - setting.MarkupRenderer + *setting.MarkupRenderer } // Name returns the external tool name @@ -48,6 +48,11 @@ func (p *Renderer) Extensions() []string { return p.FileExtensions } +// SanitizerRules implements markup.Renderer +func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule { + return p.MarkupSanitizerRules +} + func envMark(envName string) string { if runtime.GOOS == "windows" { return "%" + envName + "%" diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 4cdd5798c8..8c3d2b5395 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -112,7 +112,7 @@ func TestRender_links(t *testing.T) { defaultCustom := setting.Markdown.CustomURLSchemes setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"} - ReplaceSanitizer() + InitializeSanitizer() CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) test( @@ -192,7 +192,7 @@ func TestRender_links(t *testing.T) { // Restore previous settings setting.Markdown.CustomURLSchemes = defaultCustom - ReplaceSanitizer() + InitializeSanitizer() CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 87fae2a23b..cac2a180fa 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -199,7 +199,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) } _ = lw.Close() }() - buf := markup.SanitizeReader(rd) + buf := markup.SanitizeReader(rd, "") _, err := io.Copy(output, buf) return err } @@ -215,7 +215,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error if log.IsDebug() { log.Debug("Panic in markdown: %v\n%s", err, string(log.Stack(2))) } - ret := markup.SanitizeReader(input) + ret := markup.SanitizeReader(input, "") _, err = io.Copy(output, ret) if err != nil { log.Error("SanitizeReader failed: %v", err) @@ -249,6 +249,11 @@ func (Renderer) Extensions() []string { return setting.Markdown.FileExtensions } +// SanitizerRules implements markup.Renderer +func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { + return []setting.MarkupSanitizerRule{} +} + // Render implements markup.Renderer func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { return render(ctx, input, output) diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index 96e67f90cf..7e9f1f45c5 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -11,9 +11,13 @@ import ( "io" "strings" + "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "github.com/alecthomas/chroma" + "github.com/alecthomas/chroma/lexers" "github.com/niklasfasching/go-org/org" ) @@ -38,9 +42,55 @@ func (Renderer) Extensions() []string { return []string{".org"} } +// SanitizerRules implements markup.Renderer +func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { + return []setting.MarkupSanitizerRule{} +} + // Render renders orgmode rawbytes to HTML func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { htmlWriter := org.NewHTMLWriter() + htmlWriter.HighlightCodeBlock = func(source, lang string, inline bool) string { + var w strings.Builder + if _, err := w.WriteString(`
`); err != nil {
+			return ""
+		}
+
+		lexer := lexers.Get(lang)
+		if lexer == nil && lang == "" {
+			lexer = lexers.Analyse(source)
+			if lexer == nil {
+				lexer = lexers.Fallback
+			}
+			lang = strings.ToLower(lexer.Config().Name)
+		}
+
+		if lexer == nil {
+			// include language-x class as part of commonmark spec
+			if _, err := w.WriteString(``); err != nil {
+				return ""
+			}
+			if _, err := w.WriteString(html.EscapeString(source)); err != nil {
+				return ""
+			}
+		} else {
+			// include language-x class as part of commonmark spec
+			if _, err := w.WriteString(``); err != nil {
+				return ""
+			}
+			lexer = chroma.Coalesce(lexer)
+
+			if _, err := w.WriteString(highlight.Code(lexer.Config().Filenames[0], source)); err != nil {
+				return ""
+			}
+		}
+
+		if _, err := w.WriteString("
"); err != nil { + return "" + } + + return w.String() + } w := &Writer{ HTMLWriter: htmlWriter, diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index d60c8ad710..04619caee3 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -81,6 +81,7 @@ type Renderer interface { Name() string // markup format name Extensions() []string NeedPostProcess() bool + SanitizerRules() []setting.MarkupSanitizerRule Render(ctx *RenderContext, input io.Reader, output io.Writer) error } @@ -136,37 +137,32 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr _ = pw.Close() }() - if renderer.NeedPostProcess() { - pr2, pw2 := io.Pipe() - defer func() { - _ = pr2.Close() - _ = pw2.Close() - }() + pr2, pw2 := io.Pipe() + defer func() { + _ = pr2.Close() + _ = pw2.Close() + }() - wg.Add(1) - go func() { - buf := SanitizeReader(pr2) - _, err = io.Copy(output, buf) - _ = pr2.Close() - wg.Done() - }() + wg.Add(1) + go func() { + buf := SanitizeReader(pr2, renderer.Name()) + _, err = io.Copy(output, buf) + _ = pr2.Close() + wg.Done() + }() - wg.Add(1) - go func() { + wg.Add(1) + go func() { + if renderer.NeedPostProcess() { err = PostProcess(ctx, pr, pw2) - _ = pr.Close() - _ = pw2.Close() - wg.Done() - }() - } else { - wg.Add(1) - go func() { - buf := SanitizeReader(pr) - _, err = io.Copy(output, buf) - _ = pr.Close() - wg.Done() - }() - } + } else { + _, err = io.Copy(pw2, pr) + } + _ = pr.Close() + _ = pw2.Close() + wg.Done() + }() + if err1 := renderer.Render(ctx, input, pw); err1 != nil { return err1 } diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 5611bd06ad..9342d65de5 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -19,8 +19,9 @@ import ( // Sanitizer is a protection wrapper of *bluemonday.Policy which does not allow // any modification to the underlying policies once it's been created. type Sanitizer struct { - policy *bluemonday.Policy - init sync.Once + defaultPolicy *bluemonday.Policy + rendererPolicies map[string]*bluemonday.Policy + init sync.Once } var sanitizer = &Sanitizer{} @@ -30,47 +31,57 @@ var sanitizer = &Sanitizer{} // entire application lifecycle. func NewSanitizer() { sanitizer.init.Do(func() { - ReplaceSanitizer() + InitializeSanitizer() }) } -// ReplaceSanitizer replaces the current sanitizer to account for changes in settings -func ReplaceSanitizer() { - sanitizer.policy = bluemonday.UGCPolicy() +// InitializeSanitizer (re)initializes the current sanitizer to account for changes in settings +func InitializeSanitizer() { + sanitizer.rendererPolicies = map[string]*bluemonday.Policy{} + sanitizer.defaultPolicy = createDefaultPolicy() + + for name, renderer := range renderers { + sanitizerRules := renderer.SanitizerRules() + if len(sanitizerRules) > 0 { + policy := createDefaultPolicy() + addSanitizerRules(policy, sanitizerRules) + sanitizer.rendererPolicies[name] = policy + } + } +} + +func createDefaultPolicy() *bluemonday.Policy { + policy := bluemonday.UGCPolicy() // For Chroma markdown plugin - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^is-loading$`)).OnElements("pre") - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^is-loading$`)).OnElements("pre") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code") // Checkboxes - sanitizer.policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") - sanitizer.policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input") + policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") + policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input") // Custom URL-Schemes if len(setting.Markdown.CustomURLSchemes) > 0 { - sanitizer.policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...) + policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...) } // Allow classes for anchors - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue`)).OnElements("a") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue`)).OnElements("a") // Allow classes for task lists - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li") // Allow icons - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i") // Allow unlabelled labels - sanitizer.policy.AllowNoAttrs().OnElements("label") + policy.AllowNoAttrs().OnElements("label") // Allow classes for emojis - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") // Allow icons, emojis, chroma syntax and keyword markup on span - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") - - // Allow data tables - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`data-table`)).OnElements("table") - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`line-num`)).OnElements("th", "td") + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") // Allow generally safe attributes generalSafeAttrs := []string{"abbr", "accept", "accept-charset", @@ -101,18 +112,29 @@ func ReplaceSanitizer() { "abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "wbr", } - sanitizer.policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...) + policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...) - sanitizer.policy.AllowAttrs("itemscope", "itemtype").OnElements("div") + policy.AllowAttrs("itemscope", "itemtype").OnElements("div") // FIXME: Need to handle longdesc in img but there is no easy way to do it // Custom keyword markup - for _, rule := range setting.ExternalSanitizerRules { - if rule.Regexp != nil { - sanitizer.policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element) - } else { - sanitizer.policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element) + addSanitizerRules(policy, setting.ExternalSanitizerRules) + + return policy +} + +func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) { + for _, rule := range rules { + if rule.AllowDataURIImages { + policy.AllowDataURIImages() + } + if rule.Element != "" { + if rule.Regexp != nil { + policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element) + } else { + policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element) + } } } } @@ -120,11 +142,15 @@ func ReplaceSanitizer() { // Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist. func Sanitize(s string) string { NewSanitizer() - return sanitizer.policy.Sanitize(s) + return sanitizer.defaultPolicy.Sanitize(s) } // SanitizeReader sanitizes a Reader -func SanitizeReader(r io.Reader) *bytes.Buffer { +func SanitizeReader(r io.Reader, renderer string) *bytes.Buffer { NewSanitizer() - return sanitizer.policy.SanitizeReader(r) + policy, exist := sanitizer.rendererPolicies[renderer] + if !exist { + policy = sanitizer.defaultPolicy + } + return policy.SanitizeReader(r) } diff --git a/modules/private/hook.go b/modules/private/hook.go index cb8fe25708..82dcaf3fc9 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -54,7 +54,7 @@ type HookOptions struct { GitAlternativeObjectDirectories string GitQuarantinePath string GitPushOptions GitPushOptions - ProtectedBranchID int64 + PullRequestID int64 IsDeployKey bool } diff --git a/modules/private/serv.go b/modules/private/serv.go index e077b00ccc..659af6dff5 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -58,7 +58,6 @@ type ServCommandResults struct { // ErrServCommand is an error returned from ServCommmand. type ErrServCommand struct { Results ServCommandResults - Type string Err string StatusCode int } diff --git a/modules/references/references.go b/modules/references/references.go index 106e66b47b..ef859abcc7 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -5,6 +5,7 @@ package references import ( + "bytes" "net/url" "regexp" "strconv" @@ -14,6 +15,8 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/mdstripper" "code.gitea.io/gitea/modules/setting" + + "github.com/yuin/goldmark/util" ) var ( @@ -321,7 +324,7 @@ func FindRenderizableReferenceNumeric(content string, prOnly bool) (bool, *Rende return false, nil } } - r := getCrossReference([]byte(content), match[2], match[3], false, prOnly) + r := getCrossReference(util.StringToReadOnlyBytes(content), match[2], match[3], false, prOnly) if r == nil { return false, nil } @@ -465,18 +468,17 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference } func getCrossReference(content []byte, start, end int, fromLink bool, prOnly bool) *rawReference { - refid := string(content[start:end]) - sep := strings.IndexAny(refid, "#!") + sep := bytes.IndexAny(content[start:end], "#!") if sep < 0 { return nil } - isPull := refid[sep] == '!' + isPull := content[start+sep] == '!' if prOnly && !isPull { return nil } - repo := refid[:sep] - issue := refid[sep+1:] - index, err := strconv.ParseInt(issue, 10, 64) + repo := string(content[start : start+sep]) + issue := string(content[start+sep+1 : end]) + index, err := strconv.ParseInt(string(issue), 10, 64) if err != nil { return nil } diff --git a/modules/setting/git.go b/modules/setting/git.go index 308d94894b..7383996cb9 100644 --- a/modules/setting/git.go +++ b/modules/setting/git.go @@ -7,7 +7,6 @@ package setting import ( "time" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" ) @@ -19,8 +18,8 @@ var ( MaxGitDiffLines int MaxGitDiffLineCharacters int MaxGitDiffFiles int - CommitsRangeSize int - BranchesRangeSize int + CommitsRangeSize int // CommitsRangeSize the default commits range size + BranchesRangeSize int // BranchesRangeSize the default branches range size VerbosePush bool VerbosePushDelay time.Duration GCArgs []string `ini:"GC_ARGS" delim:" "` @@ -54,7 +53,7 @@ var ( Pull int GC int `ini:"GC"` }{ - Default: int(git.DefaultCommandExecutionTimeout / time.Second), + Default: 360, Migrate: 600, Mirror: 300, Clone: 300, @@ -68,35 +67,4 @@ func newGit() { if err := Cfg.Section("git").MapTo(&Git); err != nil { log.Fatal("Failed to map Git settings: %v", err) } - if err := git.SetExecutablePath(Git.Path); err != nil { - log.Fatal("Failed to initialize Git settings: %v", err) - } - git.DefaultCommandExecutionTimeout = time.Duration(Git.Timeout.Default) * time.Second - - version, err := git.LocalVersion() - if err != nil { - log.Fatal("Error retrieving git version: %v", err) - } - - // force cleanup args - git.GlobalCommandArgs = []string{} - - if git.CheckGitVersionAtLeast("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{}{version.Original()} - // Since git wire protocol has been released from git v2.18 - if Git.EnableAutoGitWireProtocol && git.CheckGitVersionAtLeast("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 - } - - git.CommitsRangeSize = Git.CommitsRangeSize - git.BranchesRangeSize = Git.BranchesRangeSize - - log.Info(format, args...) } diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index 8b9224b86a..a4bbd3c3ff 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -9,7 +9,6 @@ import ( "time" "code.gitea.io/gitea/modules/generate" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" ini "gopkg.in/ini.v1" @@ -67,24 +66,3 @@ func newLFSService() { } } } - -// 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.CheckGitVersionAtLeast("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/markup.go b/modules/setting/markup.go index 43df4ce442..31ec1dd2eb 100644 --- a/modules/setting/markup.go +++ b/modules/setting/markup.go @@ -15,31 +15,34 @@ import ( // ExternalMarkupRenderers represents the external markup renderers var ( - ExternalMarkupRenderers []MarkupRenderer + ExternalMarkupRenderers []*MarkupRenderer ExternalSanitizerRules []MarkupSanitizerRule ) // MarkupRenderer defines the external parser configured in ini type MarkupRenderer struct { - Enabled bool - MarkupName string - Command string - FileExtensions []string - IsInputFile bool - NeedPostProcess bool + Enabled bool + MarkupName string + Command string + FileExtensions []string + IsInputFile bool + NeedPostProcess bool + MarkupSanitizerRules []MarkupSanitizerRule } // MarkupSanitizerRule defines the policy for whitelisting attributes on // certain elements. type MarkupSanitizerRule struct { - Element string - AllowAttr string - Regexp *regexp.Regexp + Element string + AllowAttr string + Regexp *regexp.Regexp + AllowDataURIImages bool } func newMarkup() { - ExternalMarkupRenderers = make([]MarkupRenderer, 0, 10) + ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10) ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10) + for _, sec := range Cfg.Section("markup").ChildSections() { name := strings.TrimPrefix(sec.Name(), "markup.") if name == "" { @@ -56,50 +59,62 @@ func newMarkup() { } func newMarkupSanitizer(name string, sec *ini.Section) { - haveElement := sec.HasKey("ELEMENT") - haveAttr := sec.HasKey("ALLOW_ATTR") - haveRegexp := sec.HasKey("REGEXP") + rule, ok := createMarkupSanitizerRule(name, sec) + if ok { + if strings.HasPrefix(name, "sanitizer.") { + names := strings.SplitN(strings.TrimPrefix(name, "sanitizer."), ".", 2) + name = names[0] + } + for _, renderer := range ExternalMarkupRenderers { + if name == renderer.MarkupName { + renderer.MarkupSanitizerRules = append(renderer.MarkupSanitizerRules, rule) + return + } + } + ExternalSanitizerRules = append(ExternalSanitizerRules, rule) + } +} - if !haveElement && !haveAttr && !haveRegexp { - log.Warn("Skipping empty section: markup.%s.", name) - return +func createMarkupSanitizerRule(name string, sec *ini.Section) (MarkupSanitizerRule, bool) { + var rule MarkupSanitizerRule + + ok := false + if sec.HasKey("ALLOW_DATA_URI_IMAGES") { + rule.AllowDataURIImages = sec.Key("ALLOW_DATA_URI_IMAGES").MustBool(false) + ok = true } - if !haveElement || !haveAttr || !haveRegexp { - log.Error("Missing required keys from markup.%s. Must have all three of ELEMENT, ALLOW_ATTR, and REGEXP defined!", name) - return - } + if sec.HasKey("ELEMENT") || sec.HasKey("ALLOW_ATTR") { + rule.Element = sec.Key("ELEMENT").Value() + rule.AllowAttr = sec.Key("ALLOW_ATTR").Value() - elements := sec.Key("ELEMENT").Value() - allowAttrs := sec.Key("ALLOW_ATTR").Value() - regexpStr := sec.Key("REGEXP").Value() - - if regexpStr == "" { - rule := MarkupSanitizerRule{ - Element: elements, - AllowAttr: allowAttrs, - Regexp: nil, + if rule.Element == "" || rule.AllowAttr == "" { + log.Error("Missing required values from markup.%s. Must have ELEMENT and ALLOW_ATTR defined!", name) + return rule, false } - ExternalSanitizerRules = append(ExternalSanitizerRules, rule) - return + regexpStr := sec.Key("REGEXP").Value() + if regexpStr != "" { + // Validate when parsing the config that this is a valid regular + // expression. Then we can use regexp.MustCompile(...) later. + compiled, err := regexp.Compile(regexpStr) + if err != nil { + log.Error("In markup.%s: REGEXP (%s) failed to compile: %v", name, regexpStr, err) + return rule, false + } + + rule.Regexp = compiled + } + + ok = true } - // Validate when parsing the config that this is a valid regular - // expression. Then we can use regexp.MustCompile(...) later. - compiled, err := regexp.Compile(regexpStr) - if err != nil { - log.Error("In module.%s: REGEXP (%s) at definition %d failed to compile: %v", regexpStr, name, err) - return + if !ok { + log.Error("Missing required keys from markup.%s. Must have ELEMENT and ALLOW_ATTR or ALLOW_DATA_URI_IMAGES defined!", name) + return rule, false } - rule := MarkupSanitizerRule{ - Element: elements, - AllowAttr: allowAttrs, - Regexp: compiled, - } - - ExternalSanitizerRules = append(ExternalSanitizerRules, rule) + return rule, true } func newMarkupRenderer(name string, sec *ini.Section) { @@ -126,7 +141,7 @@ func newMarkupRenderer(name string, sec *ini.Section) { return } - ExternalMarkupRenderers = append(ExternalMarkupRenderers, MarkupRenderer{ + ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{ Enabled: sec.Key("ENABLED").MustBool(false), MarkupName: name, FileExtensions: exts, diff --git a/modules/setting/repository.go b/modules/setting/repository.go index a7666895e1..c2a6357d94 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -251,6 +251,10 @@ var ( } RepoRootPath string ScriptType = "bash" + + RepoArchive = struct { + Storage + }{} ) func newRepository() { @@ -328,4 +332,6 @@ func newRepository() { if !filepath.IsAbs(Repository.Upload.TempPath) { Repository.Upload.TempPath = path.Join(AppWorkPath, Repository.Upload.TempPath) } + + RepoArchive.Storage = getStorage("repo-archive", "", nil) } diff --git a/modules/setting/service.go b/modules/setting/service.go index 41e834e8e6..3f689212f3 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -6,6 +6,7 @@ package setting import ( "regexp" + "strings" "time" "code.gitea.io/gitea/modules/log" @@ -14,6 +15,8 @@ import ( // Service settings var Service struct { + DefaultUserVisibility string + DefaultUserVisibilityMode structs.VisibleType DefaultOrgVisibility string DefaultOrgVisibilityMode structs.VisibleType ActiveCodeLives int @@ -55,6 +58,7 @@ var Service struct { AutoWatchOnChanges bool DefaultOrgMemberVisible bool UserDeleteWithCommentsMaxTime time.Duration + ValidSiteURLSchemes []string // OpenID settings EnableOpenIDSignIn bool @@ -116,10 +120,22 @@ func newService() { Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true) Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true) Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false) + Service.DefaultUserVisibility = sec.Key("DEFAULT_USER_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) + Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility] Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0) + sec.Key("VALID_SITE_URL_SCHEMES").MustString("http,https") + Service.ValidSiteURLSchemes = sec.Key("VALID_SITE_URL_SCHEMES").Strings(",") + schemes := make([]string, len(Service.ValidSiteURLSchemes)) + for _, scheme := range Service.ValidSiteURLSchemes { + scheme = strings.ToLower(strings.TrimSpace(scheme)) + if scheme != "" { + schemes = append(schemes, scheme) + } + } + Service.ValidSiteURLSchemes = schemes if err := Cfg.Section("service.explore").MapTo(&Service.Explore); err != nil { log.Fatal("Failed to map service.explore settings: %v", err) diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 020101430d..de167e288a 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -469,7 +469,8 @@ func getWorkPath(appPath string) string { func init() { IsWindows = runtime.GOOS == "windows" // We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically - log.NewLogger(0, "console", "console", fmt.Sprintf(`{"level": "trace", "colorize": %t, "stacktraceLevel": "none"}`, log.CanColorStdout)) + // By default set this logger at Info - we'll change it later but we need to start with something. + log.NewLogger(0, "console", "console", fmt.Sprintf(`{"level": "info", "colorize": %t, "stacktraceLevel": "none"}`, log.CanColorStdout)) var err error if AppPath, err = getAppPath(); err != nil { @@ -1158,6 +1159,19 @@ func CreateOrAppendToCustomConf(callback func(cfg *ini.File)) { if err := cfg.SaveTo(CustomConf); err != nil { log.Fatal("error saving to custom config: %v", err) } + + // Change permissions to be more restrictive + fi, err := os.Stat(CustomConf) + if err != nil { + log.Error("Failed to determine current conf file permissions: %v", err) + return + } + + if fi.Mode().Perm() > 0o600 { + if err = os.Chmod(CustomConf, 0o600); err != nil { + log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.") + } + } } // NewServices initializes the services diff --git a/modules/setting/storage.go b/modules/setting/storage.go index 3ab08d8d2a..075152db59 100644 --- a/modules/setting/storage.go +++ b/modules/setting/storage.go @@ -43,6 +43,10 @@ func getStorage(name, typ string, targetSec *ini.Section) Storage { sec.Key("MINIO_LOCATION").MustString("us-east-1") sec.Key("MINIO_USE_SSL").MustBool(false) + if targetSec == nil { + targetSec, _ = Cfg.NewSection(name) + } + var storage Storage storage.Section = targetSec storage.Type = typ diff --git a/modules/storage/storage.go b/modules/storage/storage.go index 984f154db4..b3708908f8 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -114,6 +114,9 @@ var ( Avatars ObjectStorage // RepoAvatars represents repository avatars storage RepoAvatars ObjectStorage + + // RepoArchives represents repository archives storage + RepoArchives ObjectStorage ) // Init init the stoarge @@ -130,7 +133,11 @@ func Init() error { return err } - return initLFS() + if err := initLFS(); err != nil { + return err + } + + return initRepoArchives() } // NewStorage takes a storage type and some config and returns an ObjectStorage or an error @@ -169,3 +176,9 @@ func initRepoAvatars() (err error) { RepoAvatars, err = NewStorage(setting.RepoAvatar.Storage.Type, &setting.RepoAvatar.Storage) return } + +func initRepoArchives() (err error) { + log.Info("Initialising Repository Archive storage with type: %s", setting.RepoArchive.Storage.Type) + RepoArchives, err = NewStorage(setting.RepoArchive.Storage.Type, &setting.RepoArchive.Storage) + return +} diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go index 5da4e9608b..facf16a395 100644 --- a/modules/structs/admin_user.go +++ b/modules/structs/admin_user.go @@ -19,6 +19,7 @@ type CreateUserOption struct { Password string `json:"password" binding:"Required;MaxSize(255)"` MustChangePassword *bool `json:"must_change_password"` SendNotify bool `json:"send_notify"` + Visibility string `json:"visibility" binding:"In(,public,limited,private)"` } // EditUserOption edit user options @@ -43,4 +44,5 @@ type EditUserOption struct { ProhibitLogin *bool `json:"prohibit_login"` AllowCreateOrganization *bool `json:"allow_create_organization"` Restricted *bool `json:"restricted"` + Visibility string `json:"visibility" binding:"In(,public,limited,private)"` } diff --git a/modules/structs/user.go b/modules/structs/user.go index de2e68c2a2..a3c8f0c32a 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -43,6 +43,8 @@ type User struct { Website string `json:"website"` // the user's description Description string `json:"description"` + // User visibility level option: public, limited, private + Visibility string `json:"visibility"` // user counts Followers int `json:"followers_count"` @@ -60,3 +62,33 @@ func (u User) MarshalJSON() ([]byte, error) { CompatUserName string `json:"username"` }{shadow(u), u.UserName}) } + +// UserSettings represents user settings +// swagger:model +type UserSettings struct { + FullName string `json:"full_name"` + Website string `json:"website"` + Description string `json:"description"` + Location string `json:"location"` + Language string `json:"language"` + Theme string `json:"theme"` + DiffViewStyle string `json:"diff_view_style"` + // Privacy + HideEmail bool `json:"hide_email"` + HideActivity bool `json:"hide_activity"` +} + +// UserSettingsOptions represents options to change user settings +// swagger:model +type UserSettingsOptions struct { + FullName *string `json:"full_name" binding:"MaxSize(100)"` + Website *string `json:"website" binding:"OmitEmpty;ValidUrl;MaxSize(255)"` + Description *string `json:"description" binding:"MaxSize(255)"` + Location *string `json:"location" binding:"MaxSize(50)"` + Language *string `json:"language"` + Theme *string `json:"theme"` + DiffViewStyle *string `json:"diff_view_style"` + // Privacy + HideEmail *bool `json:"hide_email"` + HideActivity *bool `json:"hide_activity"` +} diff --git a/modules/validation/binding.go b/modules/validation/binding.go index 5cfd994d2d..5d5c64611f 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -19,6 +19,9 @@ const ( // ErrGlobPattern is returned when glob pattern is invalid ErrGlobPattern = "GlobPattern" + + // ErrRegexPattern is returned when a regex pattern is invalid + ErrRegexPattern = "RegexPattern" ) var ( @@ -52,7 +55,10 @@ func CheckGitRefAdditionalRulesValid(name string) bool { func AddBindingRules() { addGitRefNameBindingRule() addValidURLBindingRule() + addValidSiteURLBindingRule() addGlobPatternRule() + addRegexPatternRule() + addGlobOrRegexPatternRule() } func addGitRefNameBindingRule() { @@ -97,22 +103,78 @@ func addValidURLBindingRule() { }) } +func addValidSiteURLBindingRule() { + // URL validation rule + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return strings.HasPrefix(rule, "ValidSiteUrl") + }, + IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + str := fmt.Sprintf("%v", val) + if len(str) != 0 && !IsValidSiteURL(str) { + errs.Add([]string{name}, binding.ERR_URL, "Url") + return false, errs + } + + return true, errs + }, + }) +} + func addGlobPatternRule() { binding.AddRule(&binding.Rule{ IsMatch: func(rule string) bool { return rule == "GlobPattern" }, + IsValid: globPatternValidator, + }) +} + +func globPatternValidator(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + str := fmt.Sprintf("%v", val) + + if len(str) != 0 { + if _, err := glob.Compile(str); err != nil { + errs.Add([]string{name}, ErrGlobPattern, err.Error()) + return false, errs + } + } + + return true, errs +} + +func addRegexPatternRule() { + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return rule == "RegexPattern" + }, + IsValid: regexPatternValidator, + }) +} + +func regexPatternValidator(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { + str := fmt.Sprintf("%v", val) + + if _, err := regexp.Compile(str); err != nil { + errs.Add([]string{name}, ErrRegexPattern, err.Error()) + return false, errs + } + + return true, errs +} + +func addGlobOrRegexPatternRule() { + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return rule == "GlobOrRegexPattern" + }, IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) { - str := fmt.Sprintf("%v", val) + str := strings.TrimSpace(fmt.Sprintf("%v", val)) - if len(str) != 0 { - if _, err := glob.Compile(str); err != nil { - errs.Add([]string{name}, ErrGlobPattern, err.Error()) - return false, errs - } + if len(str) >= 2 && strings.HasPrefix(str, "/") && strings.HasSuffix(str, "/") { + return regexPatternValidator(errs, name, str[1:len(str)-1]) } - - return true, errs + return globPatternValidator(errs, name, val) }, }) } diff --git a/modules/validation/binding_test.go b/modules/validation/binding_test.go index e0daba89e5..d3b4e686ae 100644 --- a/modules/validation/binding_test.go +++ b/modules/validation/binding_test.go @@ -26,9 +26,10 @@ type ( } TestForm struct { - BranchName string `form:"BranchName" binding:"GitRefName"` - URL string `form:"ValidUrl" binding:"ValidUrl"` - GlobPattern string `form:"GlobPattern" binding:"GlobPattern"` + BranchName string `form:"BranchName" binding:"GitRefName"` + URL string `form:"ValidUrl" binding:"ValidUrl"` + GlobPattern string `form:"GlobPattern" binding:"GlobPattern"` + RegexPattern string `form:"RegexPattern" binding:"RegexPattern"` } ) diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index c22e667a2e..343261aac5 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -52,6 +52,25 @@ func IsValidURL(uri string) bool { return true } +// IsValidSiteURL checks if URL is valid +func IsValidSiteURL(uri string) bool { + u, err := url.ParseRequestURI(uri) + if err != nil { + return false + } + + if !validPort(portOnly(u.Host)) { + return false + } + + for _, scheme := range setting.Service.ValidSiteURLSchemes { + if scheme == u.Scheme { + return true + } + } + return false +} + // IsAPIURL checks if URL is current Gitea instance API URL func IsAPIURL(uri string) bool { return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(setting.AppURL+"api")) diff --git a/modules/validation/regex_pattern_test.go b/modules/validation/regex_pattern_test.go new file mode 100644 index 0000000000..afe1bcf425 --- /dev/null +++ b/modules/validation/regex_pattern_test.go @@ -0,0 +1,60 @@ +// Copyright 2021 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 validation + +import ( + "regexp" + "testing" + + "gitea.com/go-chi/binding" +) + +func getRegexPatternErrorString(pattern string) string { + if _, err := regexp.Compile(pattern); err != nil { + return err.Error() + } + return "" +} + +var regexValidationTestCases = []validationTestCase{ + { + description: "Empty regex pattern", + data: TestForm{ + RegexPattern: "", + }, + expectedErrors: binding.Errors{}, + }, + { + description: "Valid regex", + data: TestForm{ + RegexPattern: `(\d{1,3})+`, + }, + expectedErrors: binding.Errors{}, + }, + + { + description: "Invalid regex", + data: TestForm{ + RegexPattern: "[a-", + }, + expectedErrors: binding.Errors{ + binding.Error{ + FieldNames: []string{"RegexPattern"}, + Classification: ErrRegexPattern, + Message: getRegexPatternErrorString("[a-"), + }, + }, + }, +} + +func Test_RegexPatternValidation(t *testing.T) { + AddBindingRules() + + for _, testCase := range regexValidationTestCases { + t.Run(testCase.description, func(t *testing.T) { + performValidationTest(t, testCase) + }) + } +} diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index cd418c9792..cbdb29b812 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -135,6 +135,8 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field)) case validation.ErrGlobPattern: data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message) + case validation.ErrRegexPattern: + data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message) default: data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification } diff --git a/options/license/CC-BY-2.5-AU b/options/license/CC-BY-2.5-AU new file mode 100644 index 0000000000..23b8800919 --- /dev/null +++ b/options/license/CC-BY-2.5-AU @@ -0,0 +1,112 @@ +Creative Commons Attribution 2.5 Australia + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENCE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. + +Licence + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENCE ("CCPL" OR "LICENCE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORISED UNDER THIS LICENCE AND/OR APPLICABLE LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENCE. THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. + + 1. Definitions + + a. "Collective Work" means a work, such as a periodical issue, anthology or encyclopaedia, in which the Work in its entirety in unmodified form, along with a number of other contributions, constituting separate and independent works in themselves, are assembled into a collective whole. A work that constitutes a Collective Work will not be considered a Derivative Work (as defined below) for the purposes of this Licence. + + b. "Derivative Work" means a work that reproduces a substantial part of the Work, or of the Work and other pre-existing works protected by copyright, or that is an adaptation of a Work that is a literary, dramatic, musical or artistic work. Derivative Works include a translation, musical arrangement, dramatisation, motion picture version, sound recording, art reproduction, abridgment, condensation, or any other form in which a work may be adapted, except that a work that constitutes a Collective Work will not be considered a Derivative Work for the purpose of this Licence. For the avoidance of doubt, where the Work is a musical composition or sound recording, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered a Derivative Work for the purpose of this Licence. + + c. "Licensor" means the individual or entity that offers the Work under the terms of this Licence. + + d. "Moral rights law" means laws under which an individual who creates a work protected by copyright has rights of integrity of authorship of the work, rights of attribution of authorship of the work, rights not to have authorship of the work falsely attributed, or rights of a similar or analogous nature in the work anywhere in the world. + + e. "Original Author" means the individual or entity who created the Work. + + f. "Work" means the work or other subject-matter protected by copyright that is offered under the terms of this Licence, which may include (without limitation) a literary, dramatic, musical or artistic work, a sound recording or cinematograph film, a published edition of a literary, dramatic, musical or artistic work or a television or sound broadcast. + + g. "You" means an individual or entity exercising rights under this Licence who has not previously violated the terms of this Licence with respect to the Work, or who has received express permission from the Licensor to exercise rights under this Licence despite a previous violation. + + h. "Licence Elements" means the following high-level licence attributes as selected by Licensor and indicated in the title of this Licence: Attribution, NonCommercial, NoDerivatives, ShareAlike. + +2. Fair Dealing and Other Rights. Nothing in this Licence excludes or modifies, or is intended to exclude or modify, (including by reducing, limiting, or restricting) the rights of You or others to use the Work arising from fair dealings or other limitations on the rights of the copyright owner or the Original Author under copyright law, moral rights law or other applicable laws. + +3. Licence Grant. Subject to the terms and conditions of this Licence, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) licence to exercise the rights in the Work as stated below: + + a. to reproduce the Work, to incorporate the Work into one or more Collective Works, and to reproduce the Work as incorporated in the Collective Works; + + b. to create and reproduce Derivative Works; + + c. to publish, communicate to the public, distribute copies or records of, exhibit or display publicly, perform publicly and perform publicly by means of a digital audio transmission the Work including as incorporated in Collective Works; + + d. to publish, communicate to the public, distribute copies or records of, exhibit or display publicly, perform publicly, and perform publicly by means of a digital audio transmission Derivative Works; + + e. For the avoidance of doubt, where the Work is a musical composition: + + i. Performance Royalties Under Blanket Licences. Licensor will not collect, whether individually or via a performance rights society, royalties for Your communication to the public, broadcast, public performance or public digital performance (e.g. webcast) of the Work. + + ii. Mechanical Rights and Statutory Royalties. Licensor will not collect, whether individually or via a music rights agency, designated agent or a music publisher, royalties for any record You create from the Work ("cover version") and distribute, subject to the compulsory licence created by 17 USC Section 115 of the US Copyright Act (or an equivalent statutory licence under the Australian Copyright Act or in other jurisdictions). + + + f. Webcasting Rights and Statutory Royalties. For the avoidance of doubt, where the Work is a sound recording, Licensor will not collect, whether individually or via a performance-rights society, royalties for Your public digital performance (e.g. webcast) of the Work, subject to the compulsory licence created by 17 USC Section 114 of the US Copyright Act (or the equivalent in other jurisdictions). + +The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. All rights not expressly granted by Licensor under this Licence are hereby reserved. + +4. Restrictions. The licence granted in Section 3 above is expressly made subject to and limited by the following restrictions: + + a. You may publish, communicate to the public, distribute, publicly exhibit or display, publicly perform, or publicly digitally perform the Work only under the terms of this Licence, and You must include a copy of, or the Uniform Resource Identifier for, this Licence with every copy or record of the Work You publish, communicate to the public, distribute, publicly exhibit or display, publicly perform or publicly digitally perform. You may not offer or impose any terms on the Work that exclude, alter or restrict the terms of this Licence or the recipients' exercise of the rights granted hereunder. You may not sublicense the Work. You must keep intact all notices that refer to this Licence and to the disclaimer of representations and warranties. You may not publish, communicate to the public, distribute, publicly exhibit or display, publicly perform, or publicly digitally perform the Work with any technological measures that control access or use of the Work in a manner inconsistent with the terms of this Licence. The above applies to the Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Work itself to be made subject to the terms of this Licence. If You create a Collective Work, upon notice from any Licensor You must, to the extent practicable, remove from the Collective Work any credit as required by Section 4(b), as requested. If You create a Derivative Work, upon notice from any Licensor You must, to the extent practicable, remove from the Derivative Work any credit as required by Section 4(b), as requested. + + b. If you publish, communicate to the public, distribute, publicly exhibit or display, publicly perform, or publicly digitally perform the Work or any Derivative Works or Collective Works, You must keep intact all copyright notices for the Work. You must also give clear and reasonably prominent credit to (i) the Original Author (by name or pseudonym if applicable), if the name or pseudonym is supplied; and (ii) if another party or parties (eg a sponsor institute, publishing entity or journal) is designated for attribution in the copyright notice, terms of service or other reasonable means associated with the Work, such party or parties. If applicable, that credit must be given in the particular way made known by the Original Author and otherwise as reasonable to the medium or means You are utilizing, by conveying the identity of the Original Author and the other designated party or parties (if applicable); the title of the Work if supplied; to the extent reasonably practicable, the Uniform Resource Identifier, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and in the case of a Derivative Work, a credit identifying the use of the Work in the Derivative Work (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). Such credit may be implemented in any reasonable manner; provided, however, that in the case of a Derivative Work or Collective Work, at a minimum such credit will appear where any other comparable authorship credit appears and in a manner at least as prominent as such other comparable authorship credit. + + c. False attribution prohibited. Except as otherwise agreed in writing by the Licensor, if You publish, communicate to the public, distribute, publicly exhibit or display, publicly perform, or publicly digitally perform the Work or any Derivative Works or Collective Works in accordance with this Licence, You must not falsely attribute the Work to someone other than the Original Author. + + d. Prejudice to honour or reputation prohibited. Except as otherwise agreed in writing by the Licensor, if you publish, communicate to the public, distribute, publicly exhibit or display, publicly perform, or publicly digitally perform the Work or any Derivative Works or Collective Works, You must not do anything that results in a material distortion of, the mutilation of, or a material alteration to, the Work that is prejudicial to the Original Author's honour or reputation, and You must not do anything else in relation to the Work that is prejudicial to the Original Author's honour or reputation. + +5. Disclaimer. + +EXCEPT AS EXPRESSLY STATED IN THIS LICENCE OR OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, AND TO THE FULL EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR OFFERS THE WORK "AS-IS" AND MAKES NO REPRESENTATIONS, WARRANTIES OR CONDITIONS OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, ANY REPRESENTATIONS, WARRANTIES OR CONDITIONS REGARDING THE CONTENTS OR ACCURACY OF THE WORK, OR OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, THE ABSENCE OF LATENT OR OTHER DEFECTS, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. + +6. Limitation on Liability. + +TO THE FULL EXTENT PERMITTED BY APPLICABLE LAW, AND EXCEPT FOR ANY LIABILITY ARISING FROM CONTRARY MUTUAL AGREEMENT AS REFERRED TO IN SECTION 5, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) FOR ANY LOSS OR DAMAGE WHATSOEVER, INCLUDING (WITHOUT LIMITATION) LOSS OF PRODUCTION OR OPERATION TIME, LOSS, DAMAGE OR CORRUPTION OF DATA OR RECORDS; OR LOSS OF ANTICIPATED SAVINGS, OPPORTUNITY, REVENUE, PROFIT OR GOODWILL, OR OTHER ECONOMIC LOSS; OR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF OR IN CONNECTION WITH THIS LICENCE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +If applicable legislation implies warranties or conditions, or imposes obligations or liability on the Licensor in respect of this Licence that cannot be wholly or partly excluded, restricted or modified, the Licensor's liability is limited, to the full extent permitted by the applicable legislation, at its option, to: + + a. in the case of goods, any one or more of the following: + + i. the replacement of the goods or the supply of equivalent goods; + + ii. the repair of the goods; + + iii. the payment of the cost of replacing the goods or of acquiring equivalent goods; + + iv. the payment of the cost of having the goods repaired; or + + b. in the case of services: + + i. the supplying of the services again; or + + ii. the payment of the cost of having the services supplied again. + +7. Termination. + + a. This Licence and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this Licence. Individuals or entities who have received Derivative Works or Collective Works from You under this Licence, however, will not have their licences terminated provided such individuals or entities remain in full compliance with those licences. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this Licence. + + b. Subject to the above terms and conditions, the licence granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different licence terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this Licence (or any other licence that has been, or is required to be, granted under the terms of this Licence), and this Licence will continue in full force and effect unless terminated as stated above. + +8. Miscellaneous. + + a. Each time You publish, communicate to the public, distribute or publicly digitally perform the Work or a Collective Work, the Licensor offers to the recipient a licence to the Work on the same terms and conditions as the licence granted to You under this Licence. + + b. Each time You publish, communicate to the public, distribute or publicly digitally perform a Derivative Work, Licensor offers to the recipient a licence to the original Work on the same terms and conditions as the licence granted to You under this Licence. + + c. If any provision of this Licence is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Licence, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + + d. No term or provision of this Licence shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. + + e. This Licence constitutes the entire agreement between the parties with respect to the Work licensed here. To the full extent permitted by applicable law, there are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This Licence may not be modified without the mutual written agreement of the Licensor and You. + + f. The construction, validity and performance of this Licence shall be governed by the laws in force in New South Wales, Australia. + +Creative Commons is not a party to this Licence, and, to the full extent permitted by applicable law, makes no representation or warranty whatsoever in connection with the Work. To the full extent permitted by applicable law, Creative Commons will not be liable to You or any party on any legal theory (including, without limitation, negligence) for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this licence. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. + +Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, neither party will use the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. + +Creative Commons may be contacted at https://creativecommons.org/. diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini index ebfc59d166..834c88e177 100644 --- a/options/locale/locale_bg-BG.ini +++ b/options/locale/locale_bg-BG.ini @@ -259,12 +259,19 @@ authorization_failed=Оторизацията беше неуспешна sspi_auth_failed=SSPI удостоверяването беше неуспешно [mail] + activate_account=Моля активирайте Вашия профил + activate_email=Провери адрес на ел. поща -reset_password=Възстановете акаунта си -register_success=Успешна регистрация + register_notify=Добре дошли в Gitea +reset_password=Възстановете акаунта си + +register_success=Успешна регистрация + + + diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 576cf0615e..8fbdfd4a55 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -314,12 +314,19 @@ password_pwned=Heslo, které jste zvolili, je na %s, + activate_account=Bitte aktiviere dein Konto +activate_account.title=%s, bitte aktiviere dein Konto +activate_account.test_1=Hallo %[1]s, danke für deine Registrierung bei %[2]! +activate_account.test_2=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto zu aktivieren: + activate_email=Bestätige deine E-Mail-Adresse -reset_password=Stelle dein Konto wieder her -register_success=Registrierung erfolgreich +activate_email.title=%s, bitte verifiziere deine E-Mail-Adresse +activate_email.text=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto zu aktivieren: + register_notify=Willkommen bei Gitea +register_notify.title=%[1]s, willkommen bei %[2]s +register_notify.text_1=dies ist deine Bestätigungs-E-Mail für %s! +register_notify.text_2=Du kannst dich jetzt mit dem Benutzernamen "%s" anmelden. +register_notify.text_3=Wenn dieser Account von dir erstellt wurde, musst du zuerst dein Passwort setzen. + +reset_password=Stelle dein Konto wieder her +reset_password.title=%s, du hast um Wiederherstellung deines Kontos gebeten +reset_password.text=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto wiederherzustellen: + +register_success=Registrierung erfolgreich + +issue_assigned.pull=@%[1]s hat dich im Repository %[3]s dem Pull Request %[2]s zugewiesen. +issue_assigned.issue=@%[1]s hat dich im Repository %[3]s dem Issue %[2]s zugewiesen. + +issue.x_mentioned_you=@%s hat dich erwähnt: +issue.action.force_push=%[1]s hat %[3]s mit %[4]s auf %[2]s überschrieben. +issue.action.push_1=@%[1]s hat einen Commit auf %[2]s gepusht +issue.action.push_n=@%[1]s hat %[3]d Commits auf %[2]s gepusht +issue.action.close=@%[1]s hat #%[2]d geschlossen. +issue.action.reopen=@%[1]s hat #%[2]d wieder geöffnet. +issue.action.merge=@%[1]s hat #%[2]d in %[3]s gemergt. +issue.action.approve=@%[1]s hat diesen Pull-Request approved. +issue.action.reject=@%[1]s hat Änderungen auf diesem Pull-Request angefordert. +issue.action.review=@%[1]s hat diesen Pull-Request kommentiert. +issue.action.review_dismissed=@%[1]s hat das letzte Review von %[2]s für diesen Pull Request verworfen. +issue.action.ready_for_review=@%[1]s hat diesen Pull Request zum Review freigegeben. +issue.action.new=@%[1]s hat #%[2]d geöffnet. +issue.in_tree_path=In %s: release.new.subject=Release %s in %s erschienen +release.new.text=@%[1]s hat %[2]s in %[3]s released +release.title=Titel: %s +release.note=Anmerkung: +release.downloads=Downloads: +release.download.zip=Quellcode (ZIP Datei) +release.download.targz=Quellcode (TAR.GZ Datei) repo.transfer.subject_to=%s möchte "%s" an %s übertragen repo.transfer.subject_to_you=%s möchte dir "%s" übertragen repo.transfer.to_you=dir +repo.transfer.body=Um es anzunehmen oder abzulehnen, öffne %s, oder ignoriere es einfach. repo.collaborator.added.subject=%s hat dich zu %s hinzugefügt +repo.collaborator.added.text=Du wurdest als Mitarbeiter für folgendes Repository hinzugefügt: [modal] yes=Ja @@ -830,6 +875,7 @@ migrate.gitlab.description=Migriere Daten von GitLab.com oder einem selbst gehos migrate.gitea.description=Migriere Daten von Gitea.com oder einem selbst gehostetem Gitea Server. migrate.gogs.description=Migriere Daten von notabug.org oder einem anderen, selbst gehosteten Gogs Server. migrate.migrating_git=Git Daten werden migriert +migrate.migrating_topics=Themen werden migriert migrate.migrating_milestones=Meilensteine werden migriert migrate.migrating_labels=Labels werden migriert migrate.migrating_releases=Releases werden migriert @@ -1557,6 +1603,8 @@ settings.hooks=Webhooks settings.githooks=Git-Hooks settings.basic_settings=Grundeinstellungen settings.mirror_settings=Mirror-Einstellungen +settings.mirror_settings.docs=Richte dein Projekt so ein, dass Änderungen automatisch in ein anderes Repository gepusht, oder aus einem anderen Repository gepullt werden. Branches, tags und commits werden dann automatisch synchronisiert. Wie kann ich ein Repository spiegeln? (Englisch) +settings.mirror_settings.mirrored_repository=Gespiegeltes Repository settings.mirror_settings.direction=Richtung settings.mirror_settings.direction.pull=Pull settings.mirror_settings.direction.push=Push diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ded76272b6..e0ece8f9f0 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -83,6 +83,7 @@ add = Add add_all = Add All remove = Remove remove_all = Remove All +edit = Edit write = Write preview = Preview @@ -302,7 +303,8 @@ openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new openid_register_title = Create new account openid_register_desc = The chosen OpenID URI is unknown. Associate it with a new account here. openid_signin_desc = Enter your OpenID URI. For example: https://anne.me, bob.openid.org.cn or gnusocial.net/carry. -disable_forgot_password_mail = Account recovery is disabled. Please contact your site administrator. +disable_forgot_password_mail = Account recovery is disabled because no email is set up. Please contact your site administrator. +disable_forgot_password_mail_admin = Account recovery is only available when email is set up. Please set up email to enable account recovery. email_domain_blacklisted = You cannot register with your email address. authorize_application = Authorize Application authorize_redirect_notice = You will be redirected to %s if you authorize this application. @@ -311,7 +313,6 @@ authorize_application_description = If you grant the access, it will be able to authorize_title = Authorize "%s" to access your account? 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 @@ -348,16 +349,16 @@ issue_assigned.issue = @%[1]s assigned you to the issue %[2]s in repository %[3] issue.x_mentioned_you = @%s mentioned you: issue.action.force_push = %[1]s force-pushed the %[2]s from %[3]s to %[4]s. issue.action.push_1 = @%[1]s pushed 1 commit to %[2]s -issue.action.push_n = @%[1]s pushed %[3]d commits to %s: %[2]s +issue.action.push_n = @%[1]s pushed %[3]d commits to %[2]s issue.action.close = @%[1]s closed #%[2]d. issue.action.reopen = @%[1]s reopened #%[2]d. -issue.action.merge = @%[1]s merged #%[2]d into #%[3]s. +issue.action.merge = @%[1]s merged #%[2]d into %[3]s. issue.action.approve = @%[1]s approved this pull request. issue.action.reject = @%[1]s requested changes on this pull request. issue.action.review = @%[1]s commented on this pull request. issue.action.review_dismissed = @%[1]s dismissed last review from %[2]s for this pull request. issue.action.ready_for_review = @%[1]s marked this pull request ready for review. -issue.action.new = Created #%[2]d. +issue.action.new = @%[1]s created #%[2]d. issue.in_tree_path = In %s: release.new.subject = %s in %s released @@ -415,6 +416,7 @@ email_error = ` is not a valid email address.` url_error = ` is not a valid URL.` include_error = ` must contain substring '%s'.` glob_pattern_error = ` glob pattern is invalid: %s.` +regex_pattern_error = ` regex pattern is invalid: %s.` unknown_error = Unknown error: captcha_incorrect = The CAPTCHA code is incorrect. password_not_match = The passwords do not match. @@ -722,6 +724,14 @@ email_notifications.onmention = Only Email on Mention email_notifications.disable = Disable Email Notifications email_notifications.submit = Set Email Preference +visibility = User visibility +visibility.public = Public +visibility.public_tooltip = Visible to all users +visibility.limited = Limited +visibility.limited_tooltip = Visible to logged in users only +visibility.private = Private +visibility.private_tooltip = Visible only to organization members + [repo] new_repo_helper = A repository contains all project files, including revision history. Already have it elsewhere? Migrate repository. owner = Owner @@ -1802,7 +1812,7 @@ settings.event_pull_request_review_desc = Pull request approved, rejected, or re settings.event_pull_request_sync = Pull Request Synchronized settings.event_pull_request_sync_desc = Pull request synchronized. settings.branch_filter = Branch filter -settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or *, events for all branches are reported. See github.com/gobwas/glob documentation for syntax. Examples: master, {master,release*}. +settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or *, events for all branches are reported. See github.com/gobwas/glob documentation for syntax. Examples: master, {master,release*}. settings.active = Active settings.active_helper = Information about triggered events will be sent to this webhook URL. settings.add_hook_success = The webhook has been added. @@ -1872,7 +1882,7 @@ settings.dismiss_stale_approvals_desc = When new commits that change the content settings.require_signed_commits = Require Signed Commits settings.require_signed_commits_desc = Reject pushes to this branch if they are unsigned or unverifiable. settings.protect_protected_file_patterns = Protected file patterns (separated using semicolon '\;'): -settings.protect_protected_file_patterns_desc = Protected files that are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon ('\;'). See github.com/gobwas/glob documentation for pattern syntax. Examples: .drone.yml, /docs/**/*.txt. +settings.protect_protected_file_patterns_desc = Protected files that are not allowed to be changed directly even if user has rights to add, edit, or delete files in this branch. Multiple patterns can be separated using semicolon ('\;'). See github.com/gobwas/glob documentation for pattern syntax. Examples: .drone.yml, /docs/**/*.txt. settings.add_protected_branch = Enable protection settings.delete_protected_branch = Disable protection settings.update_protect_branch_success = Branch protection for branch '%s' has been updated. @@ -1891,6 +1901,16 @@ settings.choose_branch = Choose a branch… settings.no_protected_branch = There are no protected branches. settings.edit_protected_branch = Edit settings.protected_branch_required_approvals_min = Required approvals cannot be negative. +settings.tags = Tags +settings.tags.protection = Tag Protection +settings.tags.protection.pattern = Tag Pattern +settings.tags.protection.allowed = Allowed +settings.tags.protection.allowed.users = Allowed users +settings.tags.protection.allowed.teams = Allowed teams +settings.tags.protection.allowed.noone = No One +settings.tags.protection.create = Protect Tag +settings.tags.protection.none = There are no protected tags. +settings.tags.protection.pattern.description = You can use a single name or a glob pattern or regular expression to match multiple tags. Read more in the protected tags guide. settings.bot_token = Bot Token settings.chat_id = Chat ID settings.matrix.homeserver_url = Homeserver URL @@ -1904,6 +1924,7 @@ settings.archive.success = The repo was successfully archived. settings.archive.error = An error occurred while trying to archive the repo. See the log for more details. settings.archive.error_ismirror = You cannot archive a mirrored repo. settings.archive.branchsettings_unavailable = Branch settings are not available if the repo is archived. +settings.archive.tagsettings_unavailable = Tag settings are not available if the repo is archived. settings.unarchive.button = Un-Archive Repo settings.unarchive.header = Un-Archive This Repo settings.unarchive.text = Un-Archiving the repo will restore its ability to receive commits and pushes, as well as new issues and pull-requests. @@ -2018,6 +2039,7 @@ release.deletion_tag_desc = Will delete this tag from repository. Repository con release.deletion_tag_success = The tag has been deleted. release.tag_name_already_exist = A release with this tag name already exists. release.tag_name_invalid = The tag name is not valid. +release.tag_name_protected = The tag name is protected. release.tag_already_exist = This tag name already exists. release.downloads = Downloads release.download_count = Downloads: %s diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 2ece1992e6..017b83a9b3 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -316,19 +316,64 @@ password_pwned=La contraseña que eligió está en una establezca su contraseña primero. + +reset_password=Recupere su cuenta +reset_password.title=%s, has solicitado recuperar tu cuenta +reset_password.text=Haga clic en el siguiente enlace para recuperar su cuenta dentro de %s: + +register_success=Registro completado + +issue_assigned.pull=@%[1]s le asignó al pull request %[2]s en el repositorio %[3]s. +issue_assigned.issue=@%[1]s le asignó a la incidencia %[2]s en el repositorio %[3]s. + +issue.x_mentioned_you=@%s te mencionó: +issue.action.force_push=%[1]s empujó a la fuerza el %[2]s de %[3]s a %[4]s. +issue.action.push_1=@%[1]s hizo 1 commit al %[2]s +issue.action.push_n=@%[1]s push %[3]d commits a %[2]s +issue.action.close=@%[1]s cerró #%[2]d. +issue.action.reopen=@%[1]s reabrió #%[2]d. +issue.action.merge=@%[1]s fusionó #%[2]d en %[3]s. +issue.action.approve=@%[1]s aprobó este pull request. +issue.action.reject=@%[1]s solicitó cambios en este pull request. +issue.action.review=@%[1]s comentó en este pull request. +issue.action.review_dismissed=@%[1]s descartó la última revisión de %[2]s para este pull request. +issue.action.ready_for_review=@%[1]s marcó este pull request listo para ser revisado. +issue.action.new=@%[1]s creó #%[2]d. +issue.in_tree_path=En %s: release.new.subject=%s en %s publicado +release.new.text=@%[1]s lanzó %[2]s en %[3]s +release.title=Título: %s +release.note=Nota: +release.downloads=Descargas: +release.download.zip=Código fuente (ZIP) +release.download.targz=Código fuente (TAR.GZ) repo.transfer.subject_to=%s desea transferir "%s" a %s repo.transfer.subject_to_you=%s desea transferir "%s" a usted repo.transfer.to_you=usted +repo.transfer.body=Para aceptarlo o rechazarlo, visita %s o simplemente ignórelo. repo.collaborator.added.subject=%s le añadió en %s +repo.collaborator.added.text=Has sido añadido como colaborador del repositorio: [modal] yes=Sí @@ -726,6 +771,7 @@ mirror_prune_desc=Eliminar referencias de seguimiento de remotes obsoletas mirror_interval=Intervalo de réplica (Las unidades de tiempo válidas son 'h', 'm', 's'). Pone 0 para deshabilitar la sincronización automática. mirror_interval_invalid=El intervalo de réplica no es válido. mirror_address=Clonar desde URL +mirror_address_desc=Ponga cualquier credencial requerida en la sección de Autorización. mirror_address_url_invalid=La url proporcionada no es válida. Debe escapar correctamente de todos los componentes de la url. mirror_address_protocol_invalid=La url proporcionada no es válida. Sólo las ubicaciones http(s):// o git:// pueden ser replicadas desde. mirror_lfs=Almacenamiento de archivos grande (LFS) @@ -734,6 +780,8 @@ mirror_lfs_endpoint=Punto final de LFS mirror_lfs_endpoint_desc=Sync intentará usar la url del clon para determinar el servidor LFS. También puede especificar un punto final personalizado si los datos LFS del repositorio se almacenan en otro lugar. mirror_last_synced=Sincronizado por última vez mirror_password_placeholder=(Sin cambios) +mirror_password_blank_placeholder=(Indefinido) +mirror_password_help=Cambie el nombre de usario para eliminar una contraseña almacenada. watchers=Seguidores stargazers=Fans forks=Forks @@ -820,11 +868,19 @@ migrated_from_fake=Migrado desde %[1]s migrate.migrate=Migrar desde %s migrate.migrating=Migrando desde %s... migrate.migrating_failed=La migración desde %s ha fallado. +migrate.migrating_failed.error=Error: %s migrate.github.description=Migrar datos de Github.com o Github Enterprise. migrate.git.description=Migrar o replicar de datos de git desde los servicios de Git migrate.gitlab.description=Migrar datos de GitLab.com o servidor gitlab autoalojado. migrate.gitea.description=Migrando datos de Gitea.com o servidor Gitea autoalojado. migrate.gogs.description=Migrando datos de notabug.org u otro servidor de Gogs autoalojado. +migrate.migrating_git=Migrando datos de Git +migrate.migrating_topics=Migrando Temas +migrate.migrating_milestones=Migrando Hitos +migrate.migrating_labels=Migrando etiquetas +migrate.migrating_releases=Migrando Lanzamientos +migrate.migrating_issues=Migrando Incidencías +migrate.migrating_pulls=Migrando Pull Requests mirror_from=réplica de forked_from=forkeado de @@ -1318,7 +1374,10 @@ pulls.manually_merged_as=El Pull Request se ha fusionado manualmente como Comience el título con %s para prevenir que el pull request se fusione accidentalmente.` +pulls.cannot_merge_work_in_progress=Este pull request está marcado como un trabajo en curso. pulls.still_in_progress=¿Aún en curso? +pulls.add_prefix=Añadir prefijo %s +pulls.remove_prefix=Eliminar prefijo %s pulls.data_broken=Este pull request está rota debido a que falta información del fork. pulls.files_conflicted=Este pull request tiene cambios en conflicto con la rama de destino. pulls.is_checking=La comprobación de conflicto de fusión está en progreso. Inténtalo de nuevo en unos momentos. @@ -1545,10 +1604,14 @@ settings.githooks=Git Hooks settings.basic_settings=Configuración Básica settings.mirror_settings=Configuración de réplica settings.mirror_settings.docs=Configure su proyecto para insertar y/o extraer automáticamente los cambios hacia/desde otro repositorio. Las ramas, etiquetas y commits se sincronizarán automáticamente. ¿Cómo replico los repositorios? +settings.mirror_settings.mirrored_repository=Repositorio Replicado settings.mirror_settings.direction=Dirección settings.mirror_settings.direction.pull=Pull +settings.mirror_settings.direction.push=Push settings.mirror_settings.last_update=Última actualización +settings.mirror_settings.push_mirror.none=No hay Réplicas de Push configurados settings.mirror_settings.push_mirror.remote_url=URL del repositorio remoto de Git +settings.mirror_settings.push_mirror.add=Añadir Réplica de Push settings.sync_mirror=Sincronizar ahora settings.mirror_sync_in_progress=La sincronización del repositorio replicado está en curso. Vuelva a intentarlo más tarde. settings.email_notifications.enable=Habilitar las notificaciones por correo electrónico diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index ce9e429e31..e97a3b0229 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -299,12 +299,19 @@ authorization_failed_desc=تاییدیه ناموفق بود. لذا ما درخ sspi_auth_failed=SSPI عدم احراز هویت [mail] + activate_account=لطفا حساب خود را فعال کنید + activate_email=نشانی ایمیل خود را تایید کنید -reset_password=حساب خود را دوباره فعال کنید -register_success=ثبت‌نام با موفقیت انجام شد + register_notify=به گیتی یا گیت‌گو خوش آمدید +reset_password=حساب خود را دوباره فعال کنید + +register_success=ثبت‌نام با موفقیت انجام شد + + + diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 7d3db4c28c..10b6bbad99 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -263,12 +263,19 @@ authorization_failed_desc=Käyttöoikeuden varmistus epäonnistui virheellisen p sspi_auth_failed=SSPI todennus epäonnistui [mail] + activate_account=Ole hyvä ja aktivoi tilisi + activate_email=Vahvista sähköpostiosoitteesi -reset_password=Palauta käyttäjätili -register_success=Rekisteröinti onnistui + register_notify=Tervetuloa Giteaan +reset_password=Palauta käyttäjätili + +register_success=Rekisteröinti onnistui + + + diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index b96e0d56eb..493c950581 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -91,8 +91,10 @@ loading=Chargement… step1=Étape 1: step2=Étape 2: +error=Erreur error404=La page que vous essayez d'atteindre n'existe pas ou vous n'êtes pas autorisé à la voir. +never=Jamais [error] occurred=Une erreur est survenue @@ -314,19 +316,37 @@ password_pwned=Le mot de passe que vous avez choisi est sur une déterminer le serveur LFS. Vous pouvez également spécifier un point d'accès personnalisé si les données LFS du dépôt sont stockées ailleurs. mirror_last_synced=Dernière synchronisation +mirror_password_placeholder=(Aucune modification) +mirror_password_blank_placeholder=(Non défini) +mirror_password_help=Modifiez le nom d'utilisateur pour effacer un mot de passe enregistré. watchers=Observateurs stargazers=Fans forks=Bifurcations @@ -781,6 +805,7 @@ form.reach_limit_of_creation_n=Vous avez déjà atteint la limite de %d dépôts form.name_reserved=Le dépôt "%s" a un nom réservé. form.name_pattern_not_allowed="%s" n'est pas autorisé dans un nom de dépôt. +need_auth=Autorisation migrate_options=Options de migration migrate_service=Service de migration migrate_options_mirror_helper=Ce dépôt sera un miroir @@ -814,11 +839,19 @@ migrated_from_fake=Migré de %[1]s migrate.migrate=Migrer depuis %s migrate.migrating=Migration de %s ... migrate.migrating_failed=La migration de %s a échoué. +migrate.migrating_failed.error=Erreur: %s migrate.github.description=Migration de données depuis Github.com ou Github Enterprise. migrate.git.description=Migration ou Miroir des données git depuis des services Git migrate.gitlab.description=Migration des données depuis GitLab.com ou d'un serveur gitlab hébergé. migrate.gitea.description=Migration des données depuis Gitea.com ou un serveur Gitea hébergé soi-même. migrate.gogs.description=Migration de données depuis notabug.org ou un autre serveur Gogs auto-hébergé. +migrate.migrating_git=Migration des données Git +migrate.migrating_topics=Migration des sujets +migrate.migrating_milestones=Migration des jalons +migrate.migrating_labels=Migration des étiquettes +migrate.migrating_releases=Migration des versions +migrate.migrating_issues=Migration des tickets +migrate.migrating_pulls=Migration des demandes d'ajout mirror_from=miroir de forked_from=bifurqué depuis @@ -851,6 +884,7 @@ branch=Branche tree=Aborescence clear_ref=`Effacer la référence actuelle` filter_branch_and_tag=Filtrer une branche ou un tag +find_tag=Rechercher un tag branches=Branches tags=Tags issues=Tickets @@ -1281,6 +1315,8 @@ issues.review.resolved_by=marquer cette conversation comme résolue issues.assignee.error=Tous les assignés n'ont pas été ajoutés en raison d'une erreur inattendue. issues.reference_issue.body=Corps +compare.compare_base=base +compare.compare_head=comparer pulls.desc=Activer les demandes de fusion et la revue de code. pulls.new=Nouvelle demande d'ajout @@ -1309,6 +1345,7 @@ pulls.manually_merged_as=La demande d'ajout a été fusionnée manuellement en t pulls.is_closed=La demande de fusion a été fermée. pulls.has_merged=La pull request a été fusionnée. pulls.title_wip_desc=`Préfixer le titre par %s pour empêcher cette demande d'ajout d'être fusionnée par erreur.` +pulls.remove_prefix=Enlever le préfixe %s pulls.data_broken=Cette demande de fusion est impossible par manque d'informations de bifurcation. pulls.files_conflicted=Cette demande d'ajout contient des modifications en conflit avec la branche ciblée. pulls.is_checking=Vérification des conflits de fusion en cours. Réessayez dans quelques instants. @@ -1534,6 +1571,9 @@ settings.hooks=Déclencheurs Web settings.githooks=Déclencheurs Git settings.basic_settings=Paramètres de base settings.mirror_settings=Réglages Miroir +settings.mirror_settings.direction=Direction +settings.mirror_settings.last_update=Dernière mise à jour +settings.mirror_settings.push_mirror.remote_url=URL du dépôt distant Git settings.sync_mirror=Synchroniser maintenant settings.mirror_sync_in_progress=La synchronisation est en cours. Revenez dans une minute. settings.email_notifications.enable=Activer les notifications par e-mail @@ -1542,6 +1582,7 @@ settings.email_notifications.disable=Désactiver les notifications par e-mail settings.email_notifications.submit=Définir la préférence e-mail settings.site=Site Web settings.update_settings=Valider +settings.branches.update_default_branch=Changer la Branche par Défaut settings.advanced_settings=Paramètres avancés settings.wiki_desc=Activer le wiki du dépôt settings.use_internal_wiki=Utiliser le wiki interne @@ -1904,6 +1945,7 @@ diff.image.overlay=Superposition releases.desc=Suivi des versions et des téléchargements. release.releases=Versions release.detail=Détails de la version +release.tags=Tags release.new_release=Nouvelle version release.draft=Brouillon release.prerelease=Pré-publication @@ -2096,8 +2138,12 @@ dashboard.operation_switch=Basculer dashboard.operation_run=Exécuter dashboard.clean_unbind_oauth=Effacer les connexions OAuth associées dashboard.clean_unbind_oauth_success=Toutes les connexions OAuth associées ont été supprimées. +dashboard.cron.finished=Tâche planifiée : %[1]s a terminé +dashboard.delete_repo_archives=Supprimer toutes les archives des dépôts (ZIP, TAR.GZ, etc..) +dashboard.delete_repo_archives.started=Tâche de suppression de toutes les archives de dépôts démarrée. dashboard.delete_missing_repos=Supprimer tous les dépôts dont les fichiers Git sont manquants dashboard.delete_generated_repository_avatars=Supprimer les avatars de dépôt générés +dashboard.repo_health_check=Vérifier l'état de santé de tous les dépôts dashboard.check_repo_stats=Voir les statistiques de tous les dépôts dashboard.archive_cleanup=Supprimer les archives des vieux dépôts dashboard.git_gc_repos=Collecter les déchets des dépôts @@ -2134,6 +2180,7 @@ dashboard.total_gc_time=Pause GC dashboard.total_gc_pause=Pause GC dashboard.last_gc_pause=Dernière Pause GC dashboard.gc_times=Nombres de GC +dashboard.delete_old_actions=Supprimer toutes les anciennes actions de la base de données users.user_manage_panel=Gestion du compte utilisateur users.new_account=Créer un compte @@ -2170,6 +2217,7 @@ users.delete_account=Supprimer cet utilisateur users.still_own_repo=Cet utilisateur possède un ou plusieurs dépôts. Veuillez les supprimer ou les transférer à un autre utilisateur. users.still_has_org=Cet utilisateur est membre d'une organisation. Veuillez le retirer de toutes les organisations dont il est membre au préalable. users.deletion_success=Le compte a été supprimé. +users.reset_2fa=Réinitialiser l'authentification à deux facteurs emails.email_manage_panel=Gestion des courriels des utilisateurs emails.primary=Principale @@ -2191,6 +2239,7 @@ orgs.members=Membres orgs.new_orga=Nouvelle organisation repos.repo_manage_panel=Gestion des dépôts +repos.unadopted=Dépôts non adoptés repos.owner=Propriétaire repos.name=Nom repos.private=Privé @@ -2376,6 +2425,7 @@ config.mailer_use_sendmail=Utiliser Sendmail config.mailer_sendmail_path=Chemin d’accès à Sendmail config.mailer_sendmail_args=Arguments supplémentaires pour Sendmail config.mailer_sendmail_timeout=Délai d’attente de Sendmail +config.test_email_placeholder=E-mail (ex: test@example.com) config.send_test_mail=Envoyer un e-mail de test config.test_mail_failed=Impossible d'envoyer un e-mail de test à '%s' : %v config.test_mail_sent=Un e-mail de test à été envoyé à '%s'. @@ -2503,6 +2553,7 @@ notices.delete_selected=Supprimé les éléments sélectionnés notices.delete_all=Supprimer toutes les notifications notices.type=Type notices.type_1=Dépôt +notices.type_2=Tâche notices.desc=Description notices.op=Opération notices.delete_success=Les informations systèmes ont été supprimées. diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 0669c679ee..953d5ab8ac 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -19,6 +19,9 @@ create_new=Létrehozás… user_profile_and_more=Profil és beállítások... signed_in_as=Bejelentkezve mint enable_javascript=Ez az oldal jobban működik JavaScript-tel. +toc=Tartalomjegyzék +licenses=Licencek +return_to_gitea=Vissza a Gitea-hoz username=Felhasználónév email=E-mail cím @@ -50,6 +53,8 @@ new_migrate=Új migráció new_mirror=Új tükör new_fork=Új másolat new_org=Új szervezet +new_project=Új projekt +new_project_board=Új projekt tábla manage_org=Szervezetek kezelése admin_panel=Rendszergazdai felület account_settings=Fiók beállítások @@ -70,6 +75,7 @@ issues=Hibajegyek milestones=Mérföldkövek cancel=Mégse +save=Mentés add=Hozzáadás add_all=Összes hozzáadása remove=Eltávolítás @@ -79,6 +85,8 @@ write=Írás preview=Előnézet loading=Betöltés… +step1=1. lépés: +step2=2. lépés: error404=Az elérni kívánt oldal vagy nem létezik, vagy nincs jogosultsága a megtekintéséhez. @@ -91,6 +99,7 @@ report_message=Ha biztos benne, hogy ez egy Gitea hiba, keressen a problémára app_desc=Fájdalommentes, saját gépre telepíthető Git szolgáltatás install=Könnyen telepíthető platform=Keresztplatformos +platform_desc=A Gitea minden platformon fut, ahol a Go fordíthat: Windows, macOS, Linux, ARM, stb. Válassza azt, amelyet szereti! lightweight=Könnyűsúlyú license=Nyílt forráskódú @@ -202,7 +211,12 @@ my_mirrors=Tükreim view_home=Nézet %s search_repos=Tároló keresés… +show_archived=Archivált +show_private=Privát +show_both_private_public=Publikus és privát mutatása +show_only_private=Csak privát mutatása +show_only_public=Csak publikus mutatása issues.in_your_repos=A tárolóidban @@ -225,6 +239,7 @@ register_helper_msg=Van már felhasználói fiókja? Jelentkezzen be! social_register_helper_msg=Van már felhasználói fiókja? Csatlakoztassa most! disable_register_prompt=Regisztráció le van tiltva. Kérjük, lépjen kapcsolatba az oldal adminisztrátorával. disable_register_mail=Ki van kapcsolva a visszaigazoló e-mail küldése a regisztrációnál. +remember_me=Eszköz megjegyzése forgot_password_title=Elfelejtett jelszó forgot_password=Elfelejtette a jelszavát? sign_up_now=Szeretne bejelentkezni? Regisztráljon most. @@ -277,12 +292,19 @@ authorization_failed=Az engedélyezés nem sikerült sspi_auth_failed=SSPI hitelesítés sikertelen [mail] + activate_account=Kérjük aktiválja a fiókját + activate_email=E-mail cím megerősítése -reset_password=Fiókjának visszaállítása -register_success=Sikeres regisztráció + register_notify=A Gitea üdvözli +reset_password=Fiókjának visszaállítása + +register_success=Sikeres regisztráció + + + @@ -367,6 +389,7 @@ repositories=Tárolók activity=Nyilvános tevékenységek followers=Követők starred=Csillagozott tárolók +projects=Projektek following=Követve follow=Követés unfollow=Követés törlése @@ -410,6 +433,7 @@ continue=Folytatás cancel=Mégsem language=Nyelv ui=Téma +privacy=Adatvédelem lookup_avatar_by_mail=Avatar mutatása email cím alapján federated_avatar_lookup=Összevont profilkép keresés @@ -473,6 +497,7 @@ subkeys=Alkulcsok key_id=Kulcs ID key_name=Kulcs neve key_content=Tartalom +principal_content=Tartalom add_key_success=A SSH kulcsod sikeresen hozzáadva: '%s' add_gpg_key_success=A GPG kulcsod sikeresen hozzáadva: '%s' delete_key=Eltávolítás @@ -504,6 +529,7 @@ new_token_desc=A tokent használó alkalmazásoknak teljes hozzáférése van a token_name=Token neve generate_token=Token generálása generate_token_success=Új token létrehozva. Másold le most, mivel többször nem fog megjelenni. +generate_token_name_duplicate=A %s nevet már használja egy alkalmazás. Válassz kérlek más nevet. delete_token=Törlés access_token_deletion=Hozzáférési Token Törlése access_token_deletion_desc=Egy token törlésével visszavonja a hozzáférést a fiókjához az ezt használó alkalmazásoktól. Folytatja? @@ -541,6 +567,7 @@ twofa_is_enrolled=A fiókja jelenleg használ kétlépcsős hit twofa_not_enrolled=A fiókja jelenleg nem használ kétlépcsős hitelesítést. twofa_disable=Kétlépcsős hitelesítés letiltása twofa_scratch_token_regenerate=Kaparós kód újragenerálása +twofa_enroll=Kétlépcsős hitelesítés használata twofa_disable_note=A kétlépcsős azonosítás szükség esetén letiltható. twofa_disable_desc=A kétlépcsős hitelesítés letiltása a fiókot kevésbé biztonságossá teszi. Folytatható? twofa_disabled=Kétlépcsős hitelesítés letiltva. @@ -616,9 +643,17 @@ reactions_more=és további %d language_other=Egyéb +desc.private=Privát +desc.public=Nyilvános +desc.private_template=Privát sablon +desc.public_template=Sablon +desc.internal=Belső +desc.archived=Archivált template.items=Sablon elemek template.git_content=Git tartalom (alapértelmezett branch) +template.git_hooks=Git Hook-ok +template.webhooks=Webhook-ok template.topics=Témák template.avatar=Avatar template.issue_labels=Hibajegy címkék @@ -679,11 +714,14 @@ tags=Címkék issues=Hibajegyek pulls=Egyesítési kérések labels=Címkék +org_labels_desc_manage=kezelés milestones=Mérföldkövek commits=Commit-ok commit=Commit +release=Kiadás releases=Kiadások +tag=Címke file_raw=Nyers file_history=Előzmények file_view_raw=Nyers fájl megtekintése @@ -694,6 +732,7 @@ audio_not_supported_in_browser=A böngésző nem támogatja a HTML5 audio tag-et stored_lfs=Git LFS-el eltárolva symbolic_link=Szimbolikus hivatkozás commit_graph=Commit gráf +commit_graph.hide_pr_refs=Pull request-ek elrejtése normal_view=Normál nézet line=sor lines=sor @@ -756,12 +795,17 @@ ext_issues.desc=Külső hibakövető csatlakoztatás. issues.desc=Hibajelentések, feladatok és mérföldkövek elrendezése. +issues.filter_milestones=Mérföldkövek szűrése +issues.filter_labels=Címkék szűrése issues.new=Új hibajegy issues.new.title_empty=A cím nem lehet üres issues.new.labels=Címkék +issues.new.add_labels_title=Címke alkalmazása issues.new.no_label=Nincs címke issues.new.clear_labels=Címkék kiürítése +issues.new.no_items=Nincsenek elemek issues.new.milestone=Mérföldkő +issues.new.add_milestone_title=Mérföldkő beállítása issues.new.no_milestone=Nincs mérföldkő issues.new.clear_milestone=Mérföldkő eltávolítása issues.new.open_milestone=Nyitott mérföldkövek @@ -769,6 +813,8 @@ issues.new.closed_milestone=Lezárt mérföldkövek issues.new.assignees=Megbízottak issues.new.clear_assignees=Megbízottak eltávolítása issues.new.no_assignees=Nincsenek megbízottak +issues.new.no_reviewers=Nincs véleményező +issues.new.add_reviewer_title=Véleményezés kérése issues.no_ref=Nincsen ág/címke megadva issues.create=Hibajegy létrehozása issues.new_label=Új címke @@ -844,6 +890,7 @@ issues.commit_ref_at=`hivatkozott erre a hibajegyre egy commit-ból Jelentkezz be hogy csatlakozz a beszélgetéshez. issues.edit=Szerkesztés issues.cancel=Mégsem @@ -1153,6 +1200,8 @@ settings.add_team_duplicate=A csapat már rendelkezik a tárolóval settings.add_team_success=A csapatnak most van hozzáférése a tárolóhoz. settings.remove_team_success=A csapat hozzáférése a tárolóhoz törölve lett. settings.add_webhook=Webhook hozzáadása +settings.webhook_deletion=Webhook eltávolítása +settings.webhook_deletion_success=A webhook el lett távolítva. settings.webhook.test_delivery=Küldés Kipróbálása settings.webhook.request=Kérés settings.webhook.response=Válasz @@ -1253,6 +1302,7 @@ diff.show_unified_view=Egyesített nézet diff.stats_desc=%d fájl változott, egészen pontosan %d új sor hozzáadva és %d régi sor törölve diff.bin=BINáris diff.view_file=Fájl megtekintése +diff.file_byte_size=Méret diff.file_suppressed=A különbségek nem kerülnek megjelenítésre, mivel a fájl túl nagy diff.too_many_files=Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott diff.comment.placeholder=Hozzászólás létrehozása @@ -1276,6 +1326,7 @@ release.cancel=Mégse release.publish=Kiadás közzététele release.save_draft=Piszkozat mentése release.deletion_success=A kiadás törölve. +release.deletion_tag_success=A cimke törölve lett. release.tag_name_invalid=Ez a címkenév érvénytelen. release.downloads=Letöltések release.download_count=Letöltések: %s @@ -1650,6 +1701,7 @@ config.session_life_time=Munkamenet Élettartama config.https_only=Csak HTTPS config.cookie_life_time=Süti Élettartam +config.picture_config=Kép és Avatár Konfiguráció config.picture_service=Kép Szolgáltatás config.disable_gravatar=Gravatar Kikapcsolása config.enable_federated_avatar=Összevont profilkép lekérés engedélyezése @@ -1670,12 +1722,14 @@ config.log_config=Naplózási Beállítások config.log_mode=Naplózási Módja config.disabled_logger=Letiltva config.access_log_template=Sablon +config.xorm_log_sql=SQL naplózása monitor.cron=Ütemezett Feladatok monitor.name=Név monitor.schedule=Ütemezés monitor.next=Legközelebb monitor.previous=Legutóbb +monitor.execute_times=Végrehajtások monitor.process=Futó Folyamatok monitor.desc=Leírás monitor.start=Kezdés Időpontja @@ -1699,6 +1753,7 @@ notices.delete_selected=Kiválasztottak Törlése notices.delete_all=Minden Értesítés Törlése notices.type=Típus notices.type_1=Tároló +notices.type_2=Feladat notices.desc=Leírás notices.op=Op. notices.delete_success=A rendszer-értesítések törölve lettek. @@ -1717,6 +1772,7 @@ merge_pull_request=`végrehajtott egy egyesítési kérést: %s delete_tag=címke %[2]s törölve innen: %[3]s delete_branch=ág %[2]s törölve innen: %[3]s +compare_branch=Összehasonlítás compare_commits=%d commit összehasonlítása compare_commits_general=Commitok összehasonlítása diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index d96dea88fb..e7d2113452 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -19,6 +19,7 @@ create_new=Buat… user_profile_and_more=Profil dan Pengaturan… signed_in_as=Masuk sebagai enable_javascript=Situs web ini bekerja lebih baik dengan JavaScript. +toc=Daftar Isi username=Nama Pengguna email=Alamat Email @@ -70,6 +71,7 @@ issues=Masalah milestones=Tonggak cancel=Batal +save=Simpan add=Tambah add_all=Tambah Semua remove=Buang @@ -279,12 +281,19 @@ authorization_failed_desc=Otorisasi gagal oleh karena kami mendeteksi permintaan sspi_auth_failed=Autentikasi SSPI gagal [mail] + activate_account=Silakan aktifkan akun anda + activate_email=Verifikasi alamat surel anda -reset_password=Pulihkan akun Anda -register_success=Pendaftaran berhasil + register_notify=Selamat Datang di Gitea +reset_password=Pulihkan akun Anda + +register_success=Pendaftaran berhasil + + + diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index f9d0a869cf..a59531b2b6 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -314,12 +314,19 @@ password_pwned=La password che hai scelto è in una lista 存在しないか、閲覧が許可されていません。 +never=無し [error] occurred=エラーが発生しました @@ -314,12 +316,19 @@ password_pwned=あなたが選択したパスワードは、過去の情報漏 password_pwned_err=HaveIBeenPwnedへのリクエストを完了できませんでした [mail] + activate_account=あなたのアカウントをアクティベートしてください。 + activate_email=メール アドレスを確認します -reset_password=アカウントを回復 -register_success=登録が完了しました + register_notify=Giteaへようこそ +reset_password=アカウントを回復 + +register_success=登録が完了しました + + + release.new.subject=%[2]s の %[1]s がリリースされました repo.transfer.subject_to=%s が "%s" を %s に移転しようとしています @@ -724,6 +733,7 @@ mirror_prune_desc=不要になった古いリモートトラッキング参照 mirror_interval=ミラー間隔 (有効な時間の単位は'h'、'm'、's')。 自動的な同期を無効にする場合は0。 mirror_interval_invalid=ミラー間隔が不正です。 mirror_address=クローンするURL +mirror_address_desc=必要な資格情報は「認証」セクションに設定してください。 mirror_address_url_invalid=入力したURLは無効です。 URLの構成要素はすべて正しくエスケープする必要があります。 mirror_address_protocol_invalid=入力したURLは無効です。 ミラーできるのは、http(s):// または git:// の場所からだけです。 mirror_lfs=Large File Storage (LFS) @@ -786,6 +796,7 @@ form.reach_limit_of_creation_n=すでにあなたが作成できるリポジト form.name_reserved=リポジトリ名 '%s' は予約されています。 form.name_pattern_not_allowed='%s' の形式はリポジトリ名に使用できません。 +need_auth=認証 migrate_options=移行オプション migrate_service=移行するサービス migrate_options_mirror_helper=このリポジトリをミラーにする @@ -819,11 +830,19 @@ migrated_from_fake=%[1]sから移行 migrate.migrate=%s からの移行 migrate.migrating=%s から移行しています ... migrate.migrating_failed=%s からの移行が失敗しました。 +migrate.migrating_failed.error=エラー: %s migrate.github.description=Github.com または Github Enterprise からデータを移行します。 migrate.git.description=Gitサービスからgitデータを移行またはミラーを作成します migrate.gitlab.description=GitLab.com またはセルフホストのgitlabサーバーからデータを移行します。 migrate.gitea.description=Gitea.comまたはセルフホストのGiteaサーバーからデータを移行します。 migrate.gogs.description=notabug.org や、他のセルフホストのGogsサーバーからデータを移行します。 +migrate.migrating_git=Gitデータ移行中 +migrate.migrating_topics=トピック移行中 +migrate.migrating_milestones=マイルストーン移行中 +migrate.migrating_labels=ラベル移行中 +migrate.migrating_releases=リリース移行中 +migrate.migrating_issues=課題移行中 +migrate.migrating_pulls=プルリクエスト移行中 mirror_from=ミラー元 forked_from=フォーク元 @@ -1546,6 +1565,15 @@ settings.hooks=Webhook settings.githooks=Gitフック settings.basic_settings=基本設定 settings.mirror_settings=ミラー設定 +settings.mirror_settings.docs=他のリポジトリへの自動的なプッシュ/プルを行うよう、プロジェクトを設定します。 ブランチ、タグ、コミットが自動的に同期されます。 リポジトリをミラーするには? +settings.mirror_settings.mirrored_repository=同期するリポジトリ +settings.mirror_settings.direction=方向 +settings.mirror_settings.direction.pull=プル +settings.mirror_settings.direction.push=プッシュ +settings.mirror_settings.last_update=最終更新 +settings.mirror_settings.push_mirror.none=プッシュミラーは設定されていません +settings.mirror_settings.push_mirror.remote_url=リモートGitリポジトリのURL +settings.mirror_settings.push_mirror.add=プッシュミラーを追加 settings.sync_mirror=今すぐ同期 settings.mirror_sync_in_progress=ミラー同期を実行しています。 しばらくあとでまた確認してください。 settings.email_notifications.enable=メール通知有効 @@ -1611,6 +1639,7 @@ settings.transfer_form_title=確認のためリポジトリ名を入力: settings.transfer_in_progress=現在進行中の転送があります。このリポジトリを別のユーザーに転送したい場合はキャンセルしてください。 settings.transfer_notices_1=- 個人ユーザーに移転すると、あなたはリポジトリへのアクセス権を失います。 settings.transfer_notices_2=- あなたが所有(または共同で所有)している組織に移転すると、リポジトリへのアクセス権は維持されます。 +settings.transfer_notices_3=- プライベートリポジトリを個人ユーザーに移転した場合は、最低限そのユーザーが読み取り権限を持つよう設定されます (必要に応じて権限が変更されます)。 settings.transfer_owner=新しいオーナー settings.transfer_perform=転送を実行 settings.transfer_started=このリポジトリは転送のためにマークされており、「%s」からの確認を待っています diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index acf7efb478..e0b195d5e8 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -50,6 +50,8 @@ new_migrate=새 마이그레이션 new_mirror=새로운 미러 new_fork=새 저장소 포크 new_org=새로운 조직 +new_project=새 프로젝트 +new_project_board=새 프로젝트 보드 manage_org=조직 관리 admin_panel=사이트 관리 account_settings=계정 설정 @@ -70,6 +72,7 @@ issues=이슈들 milestones=마일스톤 cancel=취소 +save=저장 add=추가 add_all=모두 추가 remove=삭제 @@ -83,6 +86,7 @@ loading=불러오는 중... [error] +occurred=오류가 발생했습니다 [startpage] app_desc=편리한 설치형 Git 서비스 @@ -268,12 +272,19 @@ authorization_failed=인증 실패 sspi_auth_failed=SSPI 인증 실패 [mail] + activate_account=계정을 활성화하세요 + activate_email=이메일 주소 확인 -reset_password=계정 복구 -register_success=등록 완료 + register_notify=Gitea에 오신것을 환영합니다! +reset_password=계정 복구 + +register_success=등록 완료 + + + diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index f4db35671a..2fad43b543 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -314,12 +314,19 @@ password_pwned=Ievadītā parole ir bestaat niet of u bent niet gemachtigd om het te bekijken. @@ -202,6 +206,7 @@ default_enable_timetracking=Tijdregistratie standaard inschakelen default_enable_timetracking_popup=Tijdsregistratie voor nieuwe repositories standaard inschakelen. no_reply_address=Verborgen e-maildomein no_reply_address_helper=Domeinnaam voor gebruikers met een verborgen e-mailadres. Bijvoorbeeld zal de gebruikersnaam 'joe' in Git worden geregistreerd als 'joe@noreply.example.org' als het verborgen email domein is ingesteld op 'noreply.example.org'. +password_algorithm=Wachtwoord Hash Algoritme [home] uname_holder=Gebruikersnaam of e-mailadres @@ -215,6 +220,7 @@ my_mirrors=Mijn kopieën view_home=Bekijk %s search_repos=Zoek een repository… filter=Andere filters +filter_by_team_repositories=Filter op team repositories show_archived=Gearchiveerd show_both_archived_unarchived=Toont zowel gearchiveerd als niet-gearchiveerd @@ -303,15 +309,26 @@ password_pwned=Het gekozen wachtwoord staat op een GitHub 搜尋相關的問題,如果有需要你也可以開一個新的問題 [startpage] +app_desc=一套極易架設的 Git 服務 install=安裝容易 install_desc=簡單地執行您平台的二進位檔,或是使用 Docker,你也可以從套件管理員安裝。 platform=跨平台 @@ -262,10 +263,10 @@ forgot_password_title=忘記密碼 forgot_password=忘記密碼? sign_up_now=還沒有帳戶?馬上註冊。 sign_up_successful=帳戶已成功建立。 -confirmation_mail_sent_prompt=一封新的確認信已發送至 %s。請檢查您的收件匣,並在 %s 內完成註冊作業。 +confirmation_mail_sent_prompt=新的確認信已發送至 %s。請在 %s內檢查您的收件匣並完成註冊作業。 must_change_password=更新您的密碼 allow_password_change=要求使用者更改密碼 (推薦) -reset_password_mail_sent_prompt=一封確認信已發送至 %s。請檢查您的收件匣,並在 %s 內完成帳戶救援作業。 +reset_password_mail_sent_prompt=確認信已發送至 %s。請在 %s內檢查您的收件匣並完成帳戶救援作業。 active_your_account=啟用您的帳戶 account_activated=帳戶已啟用 prohibit_login=禁止登入 @@ -315,12 +316,19 @@ password_pwned=您選擇的密碼已被列於 git.CommitsRangeSize { - listOptions.PageSize = git.CommitsRangeSize + if listOptions.PageSize > setting.Git.CommitsRangeSize { + listOptions.PageSize = setting.Git.CommitsRangeSize } sha := ctx.Query("sha") diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 39a60df33f..e6427ea4f4 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -18,6 +18,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/common" + "code.gitea.io/gitea/routers/web/repo" ) // GetRawFile get a file by path on a repository @@ -126,7 +127,7 @@ func GetArchive(ctx *context.APIContext) { ctx.Repo.GitRepo = gitRepo defer gitRepo.Close() - common.Download(ctx.Context) + repo.Download(ctx.Context) } // GetEditorconfig get editor config of a repository diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 35414e0a80..323904f45c 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -307,7 +307,7 @@ func CreatePullReview(ctx *context.APIContext) { } // determine review type - reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body) + reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(opts.Comments) > 0) if isWrong { return } @@ -429,7 +429,7 @@ func SubmitPullReview(ctx *context.APIContext) { } // determine review type - reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body) + reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body, len(review.Comments) > 0) if isWrong { return } @@ -463,12 +463,15 @@ func SubmitPullReview(ctx *context.APIContext) { } // preparePullReviewType return ReviewType and false or nil and true if an error happen -func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, event api.ReviewStateType, body string) (models.ReviewType, bool) { +func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, event api.ReviewStateType, body string, hasComments bool) (models.ReviewType, bool) { if err := pr.LoadIssue(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadIssue", err) return -1, true } + needsBody := true + hasBody := len(strings.TrimSpace(body)) > 0 + var reviewType models.ReviewType switch event { case api.ReviewStateApproved: @@ -478,6 +481,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even return -1, true } reviewType = models.ReviewTypeApprove + needsBody = false case api.ReviewStateRequestChanges: // can not reject your own PR @@ -489,13 +493,19 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even case api.ReviewStateComment: reviewType = models.ReviewTypeComment + needsBody = false + // if there is no body we need to ensure that there are comments + if !hasBody && !hasComments { + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body or a comment", event)) + return -1, true + } default: reviewType = models.ReviewTypePending } - // reject reviews with empty body if not approve type - if reviewType != models.ReviewTypeApprove && len(strings.TrimSpace(body)) == 0 { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s need body", event)) + // reject reviews with empty body if a body is required for this call + if needsBody && !hasBody { + ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body", event)) return -1, true } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 7a3160fa99..35d3490510 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -375,8 +375,8 @@ func CreateOrgRepo(ctx *context.APIContext) { return } - if !models.HasOrgVisible(org, ctx.User) { - ctx.NotFound("HasOrgVisible", nil) + if !models.HasOrgOrUserVisible(org, ctx.User) { + ctx.NotFound("HasOrgOrUserVisible", nil) return } diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index 51ba43ea89..c95fb63f85 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -64,9 +64,9 @@ func ListTags(ctx *context.APIContext) { ctx.JSON(http.StatusOK, &apiTags) } -// GetTag get the tag of a repository. -func GetTag(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/git/tags/{sha} repository GetTag +// GetAnnotatedTag get the tag of a repository. +func GetAnnotatedTag(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/git/tags/{sha} repository GetAnnotatedTag // --- // summary: Gets the tag object of an annotated tag (not lightweight tags) // produces: @@ -100,21 +100,21 @@ func GetTag(ctx *context.APIContext) { } if tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha); err != nil { - ctx.Error(http.StatusBadRequest, "GetTag", err) + ctx.Error(http.StatusBadRequest, "GetAnnotatedTag", err) } else { commit, err := tag.Commit() if err != nil { - ctx.Error(http.StatusBadRequest, "GetTag", err) + ctx.Error(http.StatusBadRequest, "GetAnnotatedTag", err) } ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx.Repo.Repository, tag, commit)) } } -// DeleteTag delete a specific tag of in a repository by name -func DeleteTag(ctx *context.APIContext) { - // swagger:operation DELETE /repos/{owner}/{repo}/tags/{tag} repository repoDeleteTag +// GetTag get the tag of a repository +func GetTag(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/tags/{tag} repository repoGetTag // --- - // summary: Delete a repository's tag by name + // summary: Get the tag of a repository by tag name // produces: // - application/json // parameters: @@ -130,37 +130,22 @@ func DeleteTag(ctx *context.APIContext) { // required: true // - name: tag // in: path - // description: name of tag to delete + // description: name of tag // type: string // required: true // responses: - // "204": - // "$ref": "#/responses/empty" + // "200": + // "$ref": "#/responses/Tag" // "404": // "$ref": "#/responses/notFound" - // "409": - // "$ref": "#/responses/conflict" + tagName := ctx.Params("*") - tag, err := models.GetRelease(ctx.Repo.Repository.ID, ctx.Params("tag")) + tag, err := ctx.Repo.GitRepo.GetTag(tagName) if err != nil { - if models.IsErrReleaseNotExist(err) { - ctx.NotFound() - return - } - ctx.Error(http.StatusInternalServerError, "GetRelease", err) + ctx.NotFound(tagName) return } - - if !tag.IsTag { - ctx.Error(http.StatusConflict, "IsTag", errors.New("a tag attached to a release cannot be deleted directly")) - return - } - - if err = releaseservice.DeleteReleaseByID(tag.ID, ctx.User, true); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err) - } - - ctx.Status(http.StatusNoContent) + ctx.JSON(http.StatusOK, convert.ToTag(ctx.Repo.Repository, tag)) } // CreateTag create a new git tag in a repository @@ -187,7 +172,7 @@ func CreateTag(ctx *context.APIContext) { // "$ref": "#/definitions/CreateTagOption" // responses: // "200": - // "$ref": "#/responses/AnnotatedTag" + // "$ref": "#/responses/Tag" // "404": // "$ref": "#/responses/notFound" // "409": @@ -221,3 +206,57 @@ func CreateTag(ctx *context.APIContext) { } ctx.JSON(http.StatusCreated, convert.ToTag(ctx.Repo.Repository, tag)) } + +// DeleteTag delete a specific tag of in a repository by name +func DeleteTag(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/tags/{tag} repository repoDeleteTag + // --- + // summary: Delete a repository's tag by name + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: tag + // in: path + // description: name of tag to delete + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/conflict" + tagName := ctx.Params("*") + + tag, err := models.GetRelease(ctx.Repo.Repository.ID, tagName) + if err != nil { + if models.IsErrReleaseNotExist(err) { + ctx.NotFound() + return + } + ctx.Error(http.StatusInternalServerError, "GetRelease", err) + return + } + + if !tag.IsTag { + ctx.Error(http.StatusConflict, "IsTag", errors.New("a tag attached to a release cannot be deleted directly")) + return + } + + if err = releaseservice.DeleteReleaseByID(tag.ID, ctx.User, true); err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err) + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 11158fb86d..b5f34e86a3 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -161,4 +161,7 @@ type swaggerParameterBodies struct { // in:body CreateTagOption api.CreateTagOption + + // in:body + UserSettingsOptions api.UserSettingsOptions } diff --git a/routers/api/v1/swagger/user.go b/routers/api/v1/swagger/user.go index a2df40e4cf..a4d5201236 100644 --- a/routers/api/v1/swagger/user.go +++ b/routers/api/v1/swagger/user.go @@ -42,3 +42,10 @@ type swaggerResponseUserHeatmapData struct { // in:body Body []models.UserHeatmapData `json:"body"` } + +// UserSettings +// swagger:response UserSettings +type swaggerResponseUserSettings struct { + // in:body + Body []api.UserSettings `json:"body"` +} diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go index fcdac257ed..a3500e0ee6 100644 --- a/routers/api/v1/user/helper.go +++ b/routers/api/v1/user/helper.go @@ -17,7 +17,7 @@ func GetUserByParamsName(ctx *context.APIContext, name string) *models.User { user, err := models.GetUserByName(username) if err != nil { if models.IsErrUserNotExist(err) { - if redirectUserID, err := models.LookupUserRedirect(username); err == nil { + if redirectUserID, err2 := models.LookupUserRedirect(username); err2 == nil { context.RedirectToUser(ctx.Context, username, redirectUserID) } else { ctx.NotFound("GetUserByName", err) diff --git a/routers/api/v1/user/settings.go b/routers/api/v1/user/settings.go new file mode 100644 index 0000000000..b4548e7443 --- /dev/null +++ b/routers/api/v1/user/settings.go @@ -0,0 +1,83 @@ +// Copyright 2021 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 user + +import ( + "net/http" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" +) + +// GetUserSettings returns user settings +func GetUserSettings(ctx *context.APIContext) { + // swagger:operation GET /user/settings user getUserSettings + // --- + // summary: Get user settings + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/UserSettings" + ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.User)) +} + +// UpdateUserSettings returns user settings +func UpdateUserSettings(ctx *context.APIContext) { + // swagger:operation PATCH /user/settings user updateUserSettings + // --- + // summary: Update user settings + // parameters: + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UserSettingsOptions" + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/UserSettings" + + form := web.GetForm(ctx).(*api.UserSettingsOptions) + + if form.FullName != nil { + ctx.User.FullName = *form.FullName + } + if form.Description != nil { + ctx.User.Description = *form.Description + } + if form.Website != nil { + ctx.User.Website = *form.Website + } + if form.Location != nil { + ctx.User.Location = *form.Location + } + if form.Language != nil { + ctx.User.Language = *form.Language + } + if form.Theme != nil { + ctx.User.Theme = *form.Theme + } + if form.DiffViewStyle != nil { + ctx.User.DiffViewStyle = *form.DiffViewStyle + } + + if form.HideEmail != nil { + ctx.User.KeepEmailPrivate = *form.HideEmail + } + if form.HideActivity != nil { + ctx.User.KeepActivityPrivate = *form.HideActivity + } + + if err := models.UpdateUser(ctx.User); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, convert.User2UserSettings(ctx.User)) +} diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 4adae532fd..ac543d597d 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -57,6 +57,7 @@ func Search(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) opts := &models.SearchUserOptions{ + Actor: ctx.User, Keyword: strings.Trim(ctx.Query("q"), " "), UID: ctx.QueryInt64("uid"), Type: models.UserTypeIndividual, @@ -102,10 +103,16 @@ func GetInfo(ctx *context.APIContext) { // "$ref": "#/responses/notFound" u := GetUserByParams(ctx) + if ctx.Written() { return } + if !u.IsVisibleToUser(ctx.User) { + // fake ErrUserNotExist error message to not leak information about existence + ctx.NotFound("GetUserByName", models.ErrUserNotExist{Name: ctx.Params(":username")}) + return + } ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.User)) } diff --git a/routers/common/repo.go b/routers/common/repo.go index c61b5ec57f..22403da097 100644 --- a/routers/common/repo.go +++ b/routers/common/repo.go @@ -7,7 +7,6 @@ package common import ( "fmt" "io" - "net/http" "path" "path/filepath" "strings" @@ -19,7 +18,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/typesniffer" - "code.gitea.io/gitea/services/archiver" ) // ServeBlob download a git.Blob @@ -41,30 +39,6 @@ func ServeBlob(ctx *context.Context, blob *git.Blob) error { return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc) } -// Download an archive of a repository -func Download(ctx *context.Context) { - uri := ctx.Params("*") - aReq := archiver.DeriveRequestFrom(ctx, uri) - - if aReq == nil { - ctx.Error(http.StatusNotFound) - return - } - - downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName() - complete := aReq.IsComplete() - if !complete { - aReq = archiver.ArchiveRepository(aReq) - complete = aReq.WaitForCompletion(ctx) - } - - if complete { - ctx.ServeFile(aReq.GetArchivePath(), downloadName) - } else { - ctx.Error(http.StatusNotFound) - } -} - // ServeData download file from io.Reader func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error { buf := make([]byte, 1024) diff --git a/routers/init.go b/routers/init.go index 4c28a95395..05dbe4bd66 100644 --- a/routers/init.go +++ b/routers/init.go @@ -33,6 +33,7 @@ import ( "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/private" web_routers "code.gitea.io/gitea/routers/web" + "code.gitea.io/gitea/services/archiver" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/mailer" mirror_service "code.gitea.io/gitea/services/mirror" @@ -41,16 +42,6 @@ import ( "code.gitea.io/gitea/services/webhook" ) -func checkRunMode() { - switch setting.RunMode { - case "dev", "test": - git.Debug = true - default: - git.Debug = false - } - log.Info("Run Mode: %s", strings.Title(setting.RunMode)) -} - // NewServices init new services func NewServices() { setting.NewServices() @@ -63,6 +54,9 @@ func NewServices() { mailer.NewContext() _ = cache.NewContext() notification.NewContext() + if err := archiver.Init(); err != nil { + log.Fatal("archiver init failed: %v", err) + } } // GlobalInit is for global configuration reload-able. @@ -75,12 +69,14 @@ func GlobalInit(ctx context.Context) { if err := git.Init(ctx); err != nil { log.Fatal("Git module init failed: %v", err) } - setting.CheckLFSVersion() - log.Trace("AppPath: %s", setting.AppPath) - log.Trace("AppWorkPath: %s", setting.AppWorkPath) - log.Trace("Custom path: %s", setting.CustomPath) - log.Trace("Log path: %s", setting.LogRootPath) - checkRunMode() + log.Info(git.VersionInfo()) + + git.CheckLFSVersion() + log.Info("AppPath: %s", setting.AppPath) + log.Info("AppWorkPath: %s", setting.AppWorkPath) + log.Info("Custom path: %s", setting.CustomPath) + log.Info("Log path: %s", setting.LogRootPath) + log.Info("Run Mode: %s", strings.Title(setting.RunMode)) // Setup i18n translation.InitLocales() diff --git a/routers/install/setting.go b/routers/install/setting.go index 53d166ba1d..7b9b7bd8c2 100644 --- a/routers/install/setting.go +++ b/routers/install/setting.go @@ -18,11 +18,11 @@ import ( func PreloadSettings(ctx context.Context) bool { setting.NewContext() if !setting.InstallLock { - log.Trace("AppPath: %s", setting.AppPath) - log.Trace("AppWorkPath: %s", setting.AppWorkPath) - log.Trace("Custom path: %s", setting.CustomPath) - log.Trace("Log path: %s", setting.LogRootPath) - log.Trace("Preparing to run install page") + log.Info("AppPath: %s", setting.AppPath) + log.Info("AppWorkPath: %s", setting.AppWorkPath) + log.Info("Custom path: %s", setting.CustomPath) + log.Info("Log path: %s", setting.LogRootPath) + log.Info("Preparing to run install page") translation.InitLocales() if setting.EnableSQLite3 { log.Info("SQLite3 Supported") diff --git a/routers/private/hook.go b/routers/private/hook.go index 83c3f21b8f..9f5579b6ae 100644 --- a/routers/private/hook.go +++ b/routers/private/hook.go @@ -124,8 +124,8 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) if err != nil { log.Error("Unable to get repository: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -133,8 +133,8 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { gitRepo, err := git.OpenRepository(repo.RepoPath()) if err != nil { log.Error("Unable to get git repository for: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -155,215 +155,248 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { private.GitQuarantinePath+"="+opts.GitQuarantinePath) } + protectedTags, err := repo.GetProtectedTags() + if err != nil { + log.Error("Unable to get protected tags for %-v Error: %v", repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + // Iterate across the provided old commit IDs for i := range opts.OldCommitIDs { oldCommitID := opts.OldCommitIDs[i] newCommitID := opts.NewCommitIDs[i] refFullName := opts.RefFullNames[i] - branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) - if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA { - log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), - }) - return - } + if strings.HasPrefix(refFullName, git.BranchPrefix) { + branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) + if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA { + log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), + }) + return + } - protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName) - if err != nil { - log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - // Allow pushes to non-protected branches - if protectBranch == nil || !protectBranch.IsProtected() { - continue - } - - // This ref is a protected branch. - // - // First of all we need to enforce absolutely: - // - // 1. Detect and prevent deletion of the branch - if newCommitID == git.EmptySHA { - log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from deletion", branchName), - }) - return - } - - // 2. Disallow force pushes to protected branches - if git.EmptySHA != oldCommitID { - output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env) + protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName) if err != nil { - log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Fail to detect force push: %v", err), - }) - return - } else if len(output) > 0 { - log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from force push", branchName), - }) - return - - } - } - - // 3. Enforce require signed commits - if protectBranch.RequireSignedCommits { - err := verifyCommits(oldCommitID, newCommitID, gitRepo, env) - if err != nil { - if !isErrUnverifiedCommit(err) { - log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err), - }) - return - } - unverifiedCommit := err.(*errUnverifiedCommit).sha - log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), - }) - return - } - } - - // Now there are several tests which can be overridden: - // - // 4. Check protected file patterns - this is overridable from the UI - changedProtectedfiles := false - protectedFilePath := "" - - globs := protectBranch.GetProtectedFilePatterns() - if len(globs) > 0 { - _, err := pull_service.CheckFileProtection(oldCommitID, newCommitID, globs, 1, env, gitRepo) - if err != nil { - if !models.IsErrFilePathProtected(err) { - log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), - }) - return - } - - changedProtectedfiles = true - protectedFilePath = err.(models.ErrFilePathProtected).Path - } - } - - // 5. Check if the doer is allowed to push - canPush := false - if opts.IsDeployKey { - canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) - } else { - canPush = !changedProtectedfiles && protectBranch.CanUserPush(opts.UserID) - } - - // 6. If we're not allowed to push directly - if !canPush { - // Is this is a merge from the UI/API? - if opts.ProtectedBranchID == 0 { - // 6a. If we're not merging from the UI/API then there are two ways we got here: - // - // We are changing a protected file and we're not allowed to do that - if changedProtectedfiles { - log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), - }) - return - } - - // Or we're simply not able to push to this protected branch - log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", opts.UserID, branchName, repo) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("Not allowed to push to protected branch %s", branchName), - }) - return - } - // 6b. Merge (from UI or API) - - // Get the PR, user and permissions for the user in the repository - pr, err := models.GetPullRequestByID(opts.ProtectedBranchID) - if err != nil { - log.Error("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.ProtectedBranchID, err), - }) - return - } - user, err := models.GetUserByID(opts.UserID) - if err != nil { - log.Error("Unable to get User id %d Error: %v", opts.UserID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err), - }) - return - } - perm, err := models.GetUserRepoPermission(repo, user) - if err != nil { - log.Error("Unable to get Repo permission of repo %s/%s of User %s", repo.OwnerName, repo.Name, user.Name, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err), + log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } - // Now check if the user is allowed to merge PRs for this repository - allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, user) - if err != nil { - log.Error("Error calculating if allowed to merge: %v", err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Error calculating if allowed to merge: %v", err), - }) - return - } - - if !allowedMerge { - log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", opts.UserID, branchName, repo, pr.Index) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("Not allowed to push to protected branch %s", branchName), - }) - return - } - - // If we're an admin for the repository we can ignore status checks, reviews and override protected files - if perm.IsAdmin() { + // Allow pushes to non-protected branches + if protectBranch == nil || !protectBranch.IsProtected() { continue } - // Now if we're not an admin - we can't overwrite protected files so fail now - if changedProtectedfiles { - log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + // This ref is a protected branch. + // + // First of all we need to enforce absolutely: + // + // 1. Detect and prevent deletion of the branch + if newCommitID == git.EmptySHA { + log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from deletion", branchName), }) return } - // Check all status checks and reviews are ok - if err := pull_service.CheckPRReadyToMerge(pr, true); err != nil { - if models.IsErrNotAllowedToMerge(err) { - log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", opts.UserID, branchName, repo, pr.Index, err.Error()) - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.ProtectedBranchID, err.Error()), + // 2. Disallow force pushes to protected branches + if git.EmptySHA != oldCommitID { + output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env) + if err != nil { + log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Fail to detect force push: %v", err), + }) + return + } else if len(output) > 0 { + log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from force push", branchName), + }) + return + + } + } + + // 3. Enforce require signed commits + if protectBranch.RequireSignedCommits { + err := verifyCommits(oldCommitID, newCommitID, gitRepo, env) + if err != nil { + if !isErrUnverifiedCommit(err) { + log.Error("Unable to check commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to check commits from %s to %s: %v", oldCommitID, newCommitID, err), + }) + return + } + unverifiedCommit := err.(*errUnverifiedCommit).sha + log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), }) return } - log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", opts.UserID, branchName, repo, pr.Index, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.ProtectedBranchID, err), + } + + // Now there are several tests which can be overridden: + // + // 4. Check protected file patterns - this is overridable from the UI + changedProtectedfiles := false + protectedFilePath := "" + + globs := protectBranch.GetProtectedFilePatterns() + if len(globs) > 0 { + _, err := pull_service.CheckFileProtection(oldCommitID, newCommitID, globs, 1, env, gitRepo) + if err != nil { + if !models.IsErrFilePathProtected(err) { + log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err), + }) + return + } + + changedProtectedfiles = true + protectedFilePath = err.(models.ErrFilePathProtected).Path + } + } + + // 5. Check if the doer is allowed to push + canPush := false + if opts.IsDeployKey { + canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys) + } else { + canPush = !changedProtectedfiles && protectBranch.CanUserPush(opts.UserID) + } + + // 6. If we're not allowed to push directly + if !canPush { + // Is this is a merge from the UI/API? + if opts.PullRequestID == 0 { + // 6a. If we're not merging from the UI/API then there are two ways we got here: + // + // We are changing a protected file and we're not allowed to do that + if changedProtectedfiles { + log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + }) + return + } + + // Or we're simply not able to push to this protected branch + log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", opts.UserID, branchName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), + }) + return + } + // 6b. Merge (from UI or API) + + // Get the PR, user and permissions for the user in the repository + pr, err := models.GetPullRequestByID(opts.PullRequestID) + if err != nil { + log.Error("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get PullRequest %d Error: %v", opts.PullRequestID, err), + }) + return + } + user, err := models.GetUserByID(opts.UserID) + if err != nil { + log.Error("Unable to get User id %d Error: %v", opts.UserID, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get User id %d Error: %v", opts.UserID, err), + }) + return + } + perm, err := models.GetUserRepoPermission(repo, user) + if err != nil { + log.Error("Unable to get Repo permission of repo %s/%s of User %s", repo.OwnerName, repo.Name, user.Name, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", repo.OwnerName, repo.Name, user.Name, err), + }) + return + } + + // Now check if the user is allowed to merge PRs for this repository + allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, perm, user) + if err != nil { + log.Error("Error calculating if allowed to merge: %v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Error calculating if allowed to merge: %v", err), + }) + return + } + + if !allowedMerge { + log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", opts.UserID, branchName, repo, pr.Index) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), + }) + return + } + + // If we're an admin for the repository we can ignore status checks, reviews and override protected files + if perm.IsAdmin() { + continue + } + + // Now if we're not an admin - we can't overwrite protected files so fail now + if changedProtectedfiles { + log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), + }) + return + } + + // Check all status checks and reviews are ok + if err := pull_service.CheckPRReadyToMerge(pr, true); err != nil { + if models.IsErrNotAllowedToMerge(err) { + log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", opts.UserID, branchName, repo, pr.Index, err.Error()) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, opts.PullRequestID, err.Error()), + }) + return + } + log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", opts.UserID, branchName, repo, pr.Index, err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", opts.PullRequestID, err), + }) + return + } + } + } else if strings.HasPrefix(refFullName, git.TagPrefix) { + tagName := strings.TrimPrefix(refFullName, git.TagPrefix) + + isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, tagName, opts.UserID) + if err != nil { + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } + if !isAllowed { + log.Warn("Forbidden: Tag %s in %-v is protected", tagName, repo) + ctx.JSON(http.StatusForbidden, private.Response{ + Err: fmt.Sprintf("Tag %s is protected", tagName), + }) + return + } + } else { + log.Error("Unexpected ref: %s", refFullName) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unexpected ref: %s", refFullName), + }) } } @@ -549,8 +582,8 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) { repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) if err != nil { log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), }) return } @@ -561,16 +594,16 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) { repo.DefaultBranch = branch gitRepo, err := git.OpenRepository(repo.RepoPath()) if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Failed to get git repository: %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to get git repository: %s/%s Error: %v", ownerName, repoName, err), }) return } if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { if !git.IsErrUnsupportedVersion(err) { gitRepo.Close() - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), }) return } @@ -578,10 +611,10 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) { gitRepo.Close() if err := repo.UpdateDefaultBranch(); err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "Err": fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), }) return } - ctx.PlainText(200, []byte("success")) + ctx.PlainText(http.StatusOK, []byte("success")) } diff --git a/routers/private/key.go b/routers/private/key.go index b90faa22a4..2bb319083f 100644 --- a/routers/private/key.go +++ b/routers/private/key.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/timeutil" ) @@ -18,8 +19,8 @@ func UpdatePublicKeyInRepo(ctx *context.PrivateContext) { keyID := ctx.ParamsInt64(":id") repoID := ctx.ParamsInt64(":repoid") if err := models.UpdatePublicKeyUpdated(keyID); err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -27,18 +28,18 @@ func UpdatePublicKeyInRepo(ctx *context.PrivateContext) { deployKey, err := models.GetDeployKeyByRepo(keyID, repoID) if err != nil { if models.IsErrDeployKeyNotExist(err) { - ctx.PlainText(200, []byte("success")) + ctx.PlainText(http.StatusOK, []byte("success")) return } - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } deployKey.UpdatedUnix = timeutil.TimeStampNow() if err = models.UpdateDeployKeyCols(deployKey, "updated_unix"); err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -53,8 +54,8 @@ func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) { publicKey, err := models.SearchPublicKeyByContent(content) if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } diff --git a/routers/private/mail.go b/routers/private/mail.go index cda442ea04..d7bd5155f6 100644 --- a/routers/private/mail.go +++ b/routers/private/mail.go @@ -23,8 +23,8 @@ import ( // It doesn't wait before each message will be processed func SendEmail(ctx *context.PrivateContext) { if setting.MailService == nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": "Mail service is not enabled.", + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: "Mail service is not enabled.", }) return } @@ -35,8 +35,8 @@ func SendEmail(ctx *context.PrivateContext) { json := jsoniter.ConfigCompatibleWithStandardLibrary if err := json.NewDecoder(rd).Decode(&mail); err != nil { log.Error("%v", err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err, + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -48,8 +48,8 @@ func SendEmail(ctx *context.PrivateContext) { if err != nil { err := fmt.Sprintf("Failed to get user information: %v", err) log.Error(err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err, + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err, }) return } @@ -68,8 +68,8 @@ func SendEmail(ctx *context.PrivateContext) { if err != nil { err := fmt.Sprintf("Failed to find users: %v", err) log.Error(err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err, + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err, }) return } diff --git a/routers/private/manager.go b/routers/private/manager.go index 1ccb184363..7d010f3f81 100644 --- a/routers/private/manager.go +++ b/routers/private/manager.go @@ -30,15 +30,15 @@ func FlushQueues(ctx *context.PrivateContext) { log.Error("Flushing request timed-out with error: %v", err) } }() - ctx.JSON(http.StatusAccepted, map[string]interface{}{ - "err": "Flushing", + ctx.JSON(http.StatusAccepted, private.Response{ + Err: "Flushing", }) return } err := queue.GetManager().FlushAll(ctx, opts.Timeout) if err != nil { - ctx.JSON(http.StatusRequestTimeout, map[string]interface{}{ - "err": fmt.Sprintf("%v", err), + ctx.JSON(http.StatusRequestTimeout, private.Response{ + Err: fmt.Sprintf("%v", err), }) } ctx.PlainText(http.StatusOK, []byte("success")) @@ -59,8 +59,8 @@ func ResumeLogging(ctx *context.PrivateContext) { // ReleaseReopenLogging releases and reopens logging files func ReleaseReopenLogging(ctx *context.PrivateContext) { if err := log.ReleaseReopen(); err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Error during release and reopen: %v", err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Error during release and reopen: %v", err), }) return } @@ -73,8 +73,8 @@ func RemoveLogger(ctx *context.PrivateContext) { name := ctx.Params("name") ok, err := log.GetLogger(group).DelLogger(name) if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Failed to remove logger: %s %s %v", group, name, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to remove logger: %s %s %v", group, name, err), }) return } @@ -134,8 +134,8 @@ func AddLogger(ctx *context.PrivateContext) { byteConfig, err := json.Marshal(opts.Config) if err != nil { log.Error("Failed to marshal log configuration: %v %v", opts.Config, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Failed to marshal log configuration: %v %v", opts.Config, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to marshal log configuration: %v %v", opts.Config, err), }) return } @@ -143,8 +143,8 @@ func AddLogger(ctx *context.PrivateContext) { if err := log.NewNamedLogger(opts.Group, bufferLen, opts.Name, opts.Mode, config); err != nil { log.Error("Failed to create new named logger: %s %v", config, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": fmt.Sprintf("Failed to create new named logger: %s %v", config, err), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Failed to create new named logger: %s %v", config, err), }) return } diff --git a/routers/private/manager_windows.go b/routers/private/manager_windows.go index 244dbbe4df..f6c9b7ec8f 100644 --- a/routers/private/manager_windows.go +++ b/routers/private/manager_windows.go @@ -11,12 +11,13 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/private" ) // Restart is not implemented for Windows based servers as they can't fork func Restart(ctx *context.PrivateContext) { - ctx.JSON(http.StatusNotImplemented, map[string]interface{}{ - "err": "windows servers cannot be gracefully restarted - shutdown and restart manually", + ctx.JSON(http.StatusNotImplemented, private.Response{ + Err: "windows servers cannot be gracefully restarted - shutdown and restart manually", }) } diff --git a/routers/private/restore_repo.go b/routers/private/restore_repo.go index df787e1b33..36d17dd95c 100644 --- a/routers/private/restore_repo.go +++ b/routers/private/restore_repo.go @@ -6,9 +6,11 @@ package private import ( "io/ioutil" + "net/http" myCtx "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/migrations" + "code.gitea.io/gitea/modules/private" jsoniter "github.com/json-iterator/go" ) @@ -17,8 +19,8 @@ func RestoreRepo(ctx *myCtx.PrivateContext) { json := jsoniter.ConfigCompatibleWithStandardLibrary bs, err := ioutil.ReadAll(ctx.Req.Body) if err != nil { - ctx.JSON(500, map[string]string{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -29,8 +31,8 @@ func RestoreRepo(ctx *myCtx.PrivateContext) { Units []string }{} if err = json.Unmarshal(bs, ¶ms); err != nil { - ctx.JSON(500, map[string]string{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -42,10 +44,10 @@ func RestoreRepo(ctx *myCtx.PrivateContext) { params.RepoName, params.Units, ); err != nil { - ctx.JSON(500, map[string]string{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) } else { - ctx.Status(200) + ctx.Status(http.StatusOK) } } diff --git a/routers/private/serv.go b/routers/private/serv.go index 1461194e7f..6e39790eb5 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -23,8 +23,8 @@ import ( func ServNoCommand(ctx *context.PrivateContext) { keyID := ctx.ParamsInt64(":keyid") if keyID <= 0 { - ctx.JSON(http.StatusBadRequest, map[string]interface{}{ - "err": fmt.Sprintf("Bad key id: %d", keyID), + ctx.JSON(http.StatusBadRequest, private.Response{ + Err: fmt.Sprintf("Bad key id: %d", keyID), }) } results := private.KeyAndOwner{} @@ -32,14 +32,14 @@ func ServNoCommand(ctx *context.PrivateContext) { key, err := models.GetPublicKeyByID(keyID) if err != nil { if models.IsErrKeyNotExist(err) { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "err": fmt.Sprintf("Cannot find key: %d", keyID), + ctx.JSON(http.StatusUnauthorized, private.Response{ + Err: fmt.Sprintf("Cannot find key: %d", keyID), }) return } log.Error("Unable to get public key: %d Error: %v", keyID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } @@ -49,20 +49,20 @@ func ServNoCommand(ctx *context.PrivateContext) { user, err := models.GetUserByID(key.OwnerID) if err != nil { if models.IsErrUserNotExist(err) { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "err": fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), + ctx.JSON(http.StatusUnauthorized, private.Response{ + Err: fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), }) return } log.Error("Unable to get owner with id: %d for public key: %d Error: %v", key.OwnerID, keyID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), }) return } if !user.IsActive || user.ProhibitLogin { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": "Your account is disabled.", + ctx.JSON(http.StatusForbidden, private.Response{ + Err: "Your account is disabled.", }) return } @@ -106,18 +106,16 @@ func ServCommand(ctx *context.PrivateContext) { owner, err := models.GetUserByName(results.OwnerName) if err != nil { log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err), }) return } if !owner.IsOrganization() && !owner.IsActive { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "results": results, - "type": "ForbiddenError", - "err": "Repository cannot be accessed, you could retry it later", + ctx.JSON(http.StatusForbidden, private.ErrServCommand{ + Results: results, + Err: "Repository cannot be accessed, you could retry it later", }) return } @@ -132,20 +130,18 @@ func ServCommand(ctx *context.PrivateContext) { if "git-upload-pack" == verb { // User is fetching/cloning a non-existent repository log.Error("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) - ctx.JSON(http.StatusNotFound, map[string]interface{}{ - "results": results, - "type": "ErrRepoNotExist", - "err": fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), }) return } } } else { log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), }) return } @@ -157,20 +153,18 @@ func ServCommand(ctx *context.PrivateContext) { results.RepoID = repo.ID if repo.IsBeingCreated() { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": "Repository is being created, you could retry after it finished", + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: "Repository is being created, you could retry after it finished", }) return } // We can shortcut at this point if the repo is a mirror if mode > models.AccessModeRead && repo.IsMirror { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrMirrorReadOnly", - "err": fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusForbidden, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), }) return } @@ -180,18 +174,16 @@ func ServCommand(ctx *context.PrivateContext) { key, err := models.GetPublicKeyByID(keyID) if err != nil { if models.IsErrKeyNotExist(err) { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrKeyNotExist", - "err": fmt.Sprintf("Cannot find key: %d", keyID), + ctx.JSON(http.StatusNotFound, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Cannot find key: %d", keyID), }) return } log.Error("Unable to get public key: %d Error: %v", keyID, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), }) return } @@ -201,10 +193,9 @@ func ServCommand(ctx *context.PrivateContext) { // If repo doesn't exist, deploy key doesn't make sense if !repoExist && key.Type == models.KeyTypeDeploy { - ctx.JSON(http.StatusNotFound, map[string]interface{}{ - "results": results, - "type": "ErrRepoNotExist", - "err": fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), }) return } @@ -221,18 +212,16 @@ func ServCommand(ctx *context.PrivateContext) { deployKey, err = models.GetDeployKeyByRepo(key.ID, repo.ID) if err != nil { if models.IsErrDeployKeyNotExist(err) { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrDeployKeyNotExist", - "err": fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), }) return } log.Error("Unable to get deploy for public (deploy) key: %d in %-v Error: %v", key.ID, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), }) return } @@ -252,25 +241,23 @@ func ServCommand(ctx *context.PrivateContext) { user, err = models.GetUserByID(key.OwnerID) if err != nil { if models.IsErrUserNotExist(err) { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrUserNotExist", - "err": fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), + ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), }) return } log.Error("Unable to get owner: %d for public key: %d:%s Error: %v", key.OwnerID, key.ID, key.Name, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), }) return } if !user.IsActive || user.ProhibitLogin { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "err": "Your account is disabled.", + ctx.JSON(http.StatusForbidden, private.Response{ + Err: "Your account is disabled.", }) return } @@ -283,10 +270,9 @@ func ServCommand(ctx *context.PrivateContext) { // Don't allow pushing if the repo is archived if repoExist && mode > models.AccessModeRead && repo.IsArchived { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrRepoIsArchived", - "err": fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), }) return } @@ -295,10 +281,9 @@ func ServCommand(ctx *context.PrivateContext) { if repoExist && (mode > models.AccessModeRead || repo.IsPrivate || setting.Service.RequireSignInView) { if key.Type == models.KeyTypeDeploy { if deployKey.Mode < mode { - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrUnauthorized", - "err": fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), }) return } @@ -306,10 +291,9 @@ func ServCommand(ctx *context.PrivateContext) { perm, err := models.GetUserRepoPermission(repo, user) if err != nil { log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), }) return } @@ -318,10 +302,9 @@ func ServCommand(ctx *context.PrivateContext) { if userMode < mode { log.Error("Failed authentication attempt for %s with key %s (not authorized to %s %s/%s) from %s", user.Name, key.Name, modeString, ownerName, repoName, ctx.RemoteAddr()) - ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ - "results": results, - "type": "ErrUnauthorized", - "err": fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), + ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), }) return } @@ -332,27 +315,24 @@ func ServCommand(ctx *context.PrivateContext) { if !repoExist { owner, err := models.GetUserByName(ownerName) if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err), }) return } if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "results": results, - "type": "ErrForbidden", - "err": "Push to create is not enabled for organizations.", + ctx.JSON(http.StatusForbidden, private.ErrServCommand{ + Results: results, + Err: "Push to create is not enabled for organizations.", }) return } if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "results": results, - "type": "ErrForbidden", - "err": "Push to create is not enabled for users.", + ctx.JSON(http.StatusForbidden, private.ErrServCommand{ + Results: results, + Err: "Push to create is not enabled for users.", }) return } @@ -360,10 +340,9 @@ func ServCommand(ctx *context.PrivateContext) { repo, err = repo_service.PushCreateRepo(user, owner, results.RepoName) if err != nil { log.Error("pushCreateRepo: %v", err) - ctx.JSON(http.StatusNotFound, map[string]interface{}{ - "results": results, - "type": "ErrRepoNotExist", - "err": fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), + ctx.JSON(http.StatusNotFound, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), }) return } @@ -374,18 +353,16 @@ func ServCommand(ctx *context.PrivateContext) { // Ensure the wiki is enabled before we allow access to it if _, err := repo.GetUnit(models.UnitTypeWiki); err != nil { if models.IsErrUnitTypeNotExist(err) { - ctx.JSON(http.StatusForbidden, map[string]interface{}{ - "results": results, - "type": "ErrForbidden", - "err": "repository wiki is disabled", + ctx.JSON(http.StatusForbidden, private.ErrServCommand{ + Results: results, + Err: "repository wiki is disabled", }) return } log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err), }) return } @@ -393,10 +370,9 @@ func ServCommand(ctx *context.PrivateContext) { // Finally if we're trying to touch the wiki we should init it if err = wiki_service.InitWiki(repo); err != nil { log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err) - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "results": results, - "type": "InternalServerError", - "err": fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), + ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ + Results: results, + Err: fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), }) return } diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go index 618f945704..a2b3ed1bcc 100644 --- a/routers/web/admin/orgs.go +++ b/routers/web/admin/orgs.go @@ -25,7 +25,8 @@ func Organizations(ctx *context.Context) { ctx.Data["PageIsAdminOrganizations"] = true explore.RenderUserSearch(ctx, &models.SearchUserOptions{ - Type: models.UserTypeOrganization, + Actor: ctx.User, + Type: models.UserTypeOrganization, ListOptions: models.ListOptions{ PageSize: setting.UI.Admin.OrgPagingNum, }, diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 1b65795865..dc2a97e526 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -37,7 +37,8 @@ func Users(ctx *context.Context) { ctx.Data["PageIsAdminUsers"] = true explore.RenderUserSearch(ctx, &models.SearchUserOptions{ - Type: models.UserTypeIndividual, + Actor: ctx.User, + Type: models.UserTypeIndividual, ListOptions: models.ListOptions{ PageSize: setting.UI.Admin.UserPagingNum, }, @@ -50,6 +51,7 @@ func NewUser(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.users.new_account") ctx.Data["PageIsAdmin"] = true ctx.Data["PageIsAdminUsers"] = true + ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode ctx.Data["login_type"] = "0-0" @@ -70,6 +72,7 @@ func NewUserPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.users.new_account") ctx.Data["PageIsAdmin"] = true ctx.Data["PageIsAdminUsers"] = true + ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode sources, err := models.LoginSources() if err != nil { @@ -126,7 +129,8 @@ func NewUserPost(ctx *context.Context) { } u.MustChangePassword = form.MustChangePassword } - if err := models.CreateUser(u); err != nil { + + if err := models.CreateUser(u, &models.CreateUserOverwriteOptions{Visibility: form.Visibility}); err != nil { switch { case models.IsErrUserAlreadyExist(err): ctx.Data["Err_UserName"] = true @@ -312,6 +316,8 @@ func EditUserPost(ctx *context.Context) { u.AllowImportLocal = form.AllowImportLocal u.AllowCreateOrganization = form.AllowCreateOrganization + u.Visibility = form.Visibility + // skip self Prohibit Login if ctx.User.ID == u.ID { u.ProhibitLogin = false diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go index b19dcb886b..17c5a309b4 100644 --- a/routers/web/admin/users_test.go +++ b/routers/web/admin/users_test.go @@ -8,6 +8,8 @@ import ( "testing" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" @@ -121,3 +123,82 @@ func TestNewUserPost_InvalidEmail(t *testing.T) { assert.NotEmpty(t, ctx.Flash.ErrorMsg) } + +func TestNewUserPost_VisiblityDefaultPublic(t *testing.T) { + + models.PrepareTestEnv(t) + ctx := test.MockContext(t, "admin/users/new") + + u := models.AssertExistsAndLoadBean(t, &models.User{ + IsAdmin: true, + ID: 2, + }).(*models.User) + + ctx.User = u + + username := "gitea" + email := "gitea@gitea.io" + + form := forms.AdminCreateUserForm{ + LoginType: "local", + LoginName: "local", + UserName: username, + Email: email, + Password: "abc123ABC!=$", + SendNotify: false, + MustChangePassword: false, + } + + web.SetForm(ctx, &form) + NewUserPost(ctx) + + assert.NotEmpty(t, ctx.Flash.SuccessMsg) + + u, err := models.GetUserByName(username) + + assert.NoError(t, err) + assert.Equal(t, username, u.Name) + assert.Equal(t, email, u.Email) + // As default user visibility + assert.Equal(t, setting.Service.DefaultUserVisibilityMode, u.Visibility) +} + +func TestNewUserPost_VisibilityPrivate(t *testing.T) { + + models.PrepareTestEnv(t) + ctx := test.MockContext(t, "admin/users/new") + + u := models.AssertExistsAndLoadBean(t, &models.User{ + IsAdmin: true, + ID: 2, + }).(*models.User) + + ctx.User = u + + username := "gitea" + email := "gitea@gitea.io" + + form := forms.AdminCreateUserForm{ + LoginType: "local", + LoginName: "local", + UserName: username, + Email: email, + Password: "abc123ABC!=$", + SendNotify: false, + MustChangePassword: false, + Visibility: api.VisibleTypePrivate, + } + + web.SetForm(ctx, &form) + NewUserPost(ctx) + + assert.NotEmpty(t, ctx.Flash.SuccessMsg) + + u, err := models.GetUserByName(username) + + assert.NoError(t, err) + assert.Equal(t, username, u.Name) + assert.Equal(t, email, u.Email) + // As default user visibility + assert.True(t, u.Visibility.IsPrivate()) +} diff --git a/routers/web/org/home.go b/routers/web/org/home.go index ad14f18454..aad0a2a90b 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -30,8 +30,8 @@ func Home(ctx *context.Context) { org := ctx.Org.Organization - if !models.HasOrgVisible(org, ctx.User) { - ctx.NotFound("HasOrgVisible", nil) + if !models.HasOrgOrUserVisible(org, ctx.User) { + ctx.NotFound("HasOrgOrUserVisible", nil) return } diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 4625b1a272..da72940144 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/repofiles" repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/utils" @@ -62,8 +63,8 @@ func Branches(ctx *context.Context) { } limit := ctx.QueryInt("limit") - if limit <= 0 || limit > git.BranchesRangeSize { - limit = git.BranchesRangeSize + if limit <= 0 || limit > setting.Git.BranchesRangeSize { + limit = setting.Git.BranchesRangeSize } skip := (page - 1) * limit @@ -73,7 +74,7 @@ func Branches(ctx *context.Context) { return } ctx.Data["Branches"] = branches - pager := context.NewPagination(int(branchesCount), git.BranchesRangeSize, page, 5) + pager := context.NewPagination(int(branchesCount), setting.Git.BranchesRangeSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 3e6148bcbb..45ef22f498 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -63,7 +63,7 @@ func Commits(ctx *context.Context) { pageSize := ctx.QueryInt("limit") if pageSize <= 0 { - pageSize = git.CommitsRangeSize + pageSize = setting.Git.CommitsRangeSize } // Both `git log branchName` and `git log commitId` work. @@ -82,7 +82,7 @@ func Commits(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount ctx.Data["Branch"] = ctx.Repo.BranchName - pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5) + pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager @@ -250,7 +250,7 @@ func FileHistory(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount ctx.Data["Branch"] = branchName - pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5) + pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index f53a31769d..fddfc4a63a 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -714,7 +714,11 @@ func ExcerptBlob(ctx *context.Context) { lastLeft += chunkSize lastRight += chunkSize } else { - section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, idxRight-lastRight-1) + offset := -1 + if direction == "down" { + offset = 0 + } + section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, idxRight-lastRight+offset) leftHunkSize = 0 rightHunkSize = 0 idxLeft = lastLeft diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index a7951b6bce..9ef5c1d1f0 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1461,7 +1461,7 @@ func ViewIssue(ctx *context.Context) { } if perm.CanWrite(models.UnitTypeCode) { // Check if branch is not protected - if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch, ctx.User); err != nil { + if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch); err != nil { log.Error("IsProtectedBranch: %v", err) } else if !protected { canDelete = true diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 3b700e8016..0665496d44 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -322,6 +322,18 @@ func NewReleasePost(ctx *context.Context) { return } + if models.IsErrInvalidTagName(err) { + ctx.Flash.Error(ctx.Tr("repo.release.tag_name_invalid")) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + return + } + + if models.IsErrProtectedTagName(err) { + ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected")) + ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()) + return + } + ctx.ServerError("releaseservice.CreateNewTag", err) return } @@ -333,7 +345,9 @@ func NewReleasePost(ctx *context.Context) { rel = &models.Release{ RepoID: ctx.Repo.Repository.ID, + Repo: ctx.Repo.Repository, PublisherID: ctx.User.ID, + Publisher: ctx.User, Title: form.Title, TagName: form.TagName, Target: form.Target, @@ -350,6 +364,8 @@ func NewReleasePost(ctx *context.Context) { ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) case models.IsErrInvalidTagName(err): ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form) + case models.IsErrProtectedTagName(err): + ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form) default: ctx.ServerError("CreateRelease", err) } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index f149e92a8b..919fd4620d 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -15,8 +15,10 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/web" archiver_service "code.gitea.io/gitea/services/archiver" "code.gitea.io/gitea/services/forms" @@ -364,25 +366,123 @@ func RedirectDownload(ctx *context.Context) { ctx.Error(http.StatusNotFound) } -// InitiateDownload will enqueue an archival request, as needed. It may submit -// a request that's already in-progress, but the archiver service will just -// kind of drop it on the floor if this is the case. -func InitiateDownload(ctx *context.Context) { +// Download an archive of a repository +func Download(ctx *context.Context) { uri := ctx.Params("*") - aReq := archiver_service.DeriveRequestFrom(ctx, uri) - + aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) + if err != nil { + ctx.ServerError("archiver_service.NewRequest", err) + return + } if aReq == nil { ctx.Error(http.StatusNotFound) return } - complete := aReq.IsComplete() - if !complete { - aReq = archiver_service.ArchiveRepository(aReq) - complete, _ = aReq.TimedWaitForCompletion(ctx, 2*time.Second) + archiver, err := models.GetRepoArchiver(models.DefaultDBContext(), aReq.RepoID, aReq.Type, aReq.CommitID) + if err != nil { + ctx.ServerError("models.GetRepoArchiver", err) + return + } + if archiver != nil && archiver.Status == models.RepoArchiverReady { + download(ctx, aReq.GetArchiveName(), archiver) + return + } + + if err := archiver_service.StartArchive(aReq); err != nil { + ctx.ServerError("archiver_service.StartArchive", err) + return + } + + var times int + var t = time.NewTicker(time.Second * 1) + defer t.Stop() + + for { + select { + case <-graceful.GetManager().HammerContext().Done(): + log.Warn("exit archive download because system stop") + return + case <-t.C: + if times > 20 { + ctx.ServerError("wait download timeout", nil) + return + } + times++ + archiver, err = models.GetRepoArchiver(models.DefaultDBContext(), aReq.RepoID, aReq.Type, aReq.CommitID) + if err != nil { + ctx.ServerError("archiver_service.StartArchive", err) + return + } + if archiver != nil && archiver.Status == models.RepoArchiverReady { + download(ctx, aReq.GetArchiveName(), archiver) + return + } + } + } +} + +func download(ctx *context.Context, archiveName string, archiver *models.RepoArchiver) { + downloadName := ctx.Repo.Repository.Name + "-" + archiveName + + rPath, err := archiver.RelativePath() + if err != nil { + ctx.ServerError("archiver.RelativePath", err) + return + } + + if setting.RepoArchive.ServeDirect { + //If we have a signed url (S3, object storage), redirect to this directly. + u, err := storage.RepoArchives.URL(rPath, downloadName) + if u != nil && err == nil { + ctx.Redirect(u.String()) + return + } + } + + //If we have matched and access to release or issue + fr, err := storage.RepoArchives.Open(rPath) + if err != nil { + ctx.ServerError("Open", err) + return + } + defer fr.Close() + ctx.ServeStream(fr, downloadName) +} + +// InitiateDownload will enqueue an archival request, as needed. It may submit +// a request that's already in-progress, but the archiver service will just +// kind of drop it on the floor if this is the case. +func InitiateDownload(ctx *context.Context) { + uri := ctx.Params("*") + aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) + if err != nil { + ctx.ServerError("archiver_service.NewRequest", err) + return + } + if aReq == nil { + ctx.Error(http.StatusNotFound) + return + } + + archiver, err := models.GetRepoArchiver(models.DefaultDBContext(), aReq.RepoID, aReq.Type, aReq.CommitID) + if err != nil { + ctx.ServerError("archiver_service.StartArchive", err) + return + } + if archiver == nil || archiver.Status != models.RepoArchiverReady { + if err := archiver_service.StartArchive(aReq); err != nil { + ctx.ServerError("archiver_service.StartArchive", err) + return + } + } + + var completed bool + if archiver != nil && archiver.Status == models.RepoArchiverReady { + completed = true } ctx.JSON(http.StatusOK, map[string]interface{}{ - "complete": complete, + "complete": completed, }) } diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index c48b19b63c..5e8c2c5276 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -40,6 +40,7 @@ const ( tplSettingsOptions base.TplName = "repo/settings/options" tplCollaboration base.TplName = "repo/settings/collaboration" tplBranches base.TplName = "repo/settings/branches" + tplTags base.TplName = "repo/settings/tags" tplGithooks base.TplName = "repo/settings/githooks" tplGithookEdit base.TplName = "repo/settings/githook_edit" tplDeployKeys base.TplName = "repo/settings/deploy_keys" diff --git a/routers/web/repo/tag.go b/routers/web/repo/tag.go new file mode 100644 index 0000000000..7928591371 --- /dev/null +++ b/routers/web/repo/tag.go @@ -0,0 +1,182 @@ +// Copyright 2021 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 ( + "fmt" + "net/http" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/forms" +) + +// Tags render the page to protect tags +func Tags(ctx *context.Context) { + if setTagsContext(ctx) != nil { + return + } + + ctx.HTML(http.StatusOK, tplTags) +} + +// NewProtectedTagPost handles creation of a protect tag +func NewProtectedTagPost(ctx *context.Context) { + if setTagsContext(ctx) != nil { + return + } + + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplTags) + return + } + + repo := ctx.Repo.Repository + form := web.GetForm(ctx).(*forms.ProtectTagForm) + + pt := &models.ProtectedTag{ + RepoID: repo.ID, + NamePattern: strings.TrimSpace(form.NamePattern), + } + + if strings.TrimSpace(form.AllowlistUsers) != "" { + pt.AllowlistUserIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistUsers, ",")) + } + if strings.TrimSpace(form.AllowlistTeams) != "" { + pt.AllowlistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistTeams, ",")) + } + + if err := models.InsertProtectedTag(pt); err != nil { + ctx.ServerError("InsertProtectedTag", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.Path) +} + +// EditProtectedTag render the page to edit a protect tag +func EditProtectedTag(ctx *context.Context) { + if setTagsContext(ctx) != nil { + return + } + + ctx.Data["PageIsEditProtectedTag"] = true + + pt := selectProtectedTagByContext(ctx) + if pt == nil { + return + } + + ctx.Data["name_pattern"] = pt.NamePattern + ctx.Data["allowlist_users"] = strings.Join(base.Int64sToStrings(pt.AllowlistUserIDs), ",") + ctx.Data["allowlist_teams"] = strings.Join(base.Int64sToStrings(pt.AllowlistTeamIDs), ",") + + ctx.HTML(http.StatusOK, tplTags) +} + +// EditProtectedTagPost handles creation of a protect tag +func EditProtectedTagPost(ctx *context.Context) { + if setTagsContext(ctx) != nil { + return + } + + ctx.Data["PageIsEditProtectedTag"] = true + + if ctx.HasError() { + ctx.HTML(http.StatusOK, tplTags) + return + } + + pt := selectProtectedTagByContext(ctx) + if pt == nil { + return + } + + form := web.GetForm(ctx).(*forms.ProtectTagForm) + + pt.NamePattern = strings.TrimSpace(form.NamePattern) + pt.AllowlistUserIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistUsers, ",")) + pt.AllowlistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistTeams, ",")) + + if err := models.UpdateProtectedTag(pt); err != nil { + ctx.ServerError("UpdateProtectedTag", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags") +} + +// DeleteProtectedTagPost handles deletion of a protected tag +func DeleteProtectedTagPost(ctx *context.Context) { + pt := selectProtectedTagByContext(ctx) + if pt == nil { + return + } + + if err := models.DeleteProtectedTag(pt); err != nil { + ctx.ServerError("DeleteProtectedTag", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags") +} + +func setTagsContext(ctx *context.Context) error { + ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["PageIsSettingsTags"] = true + + protectedTags, err := ctx.Repo.Repository.GetProtectedTags() + if err != nil { + ctx.ServerError("GetProtectedTags", err) + return err + } + ctx.Data["ProtectedTags"] = protectedTags + + users, err := ctx.Repo.Repository.GetReaders() + if err != nil { + ctx.ServerError("Repo.Repository.GetReaders", err) + return err + } + ctx.Data["Users"] = users + + if ctx.Repo.Owner.IsOrganization() { + teams, err := ctx.Repo.Owner.TeamsWithAccessToRepo(ctx.Repo.Repository.ID, models.AccessModeRead) + if err != nil { + ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err) + return err + } + ctx.Data["Teams"] = teams + } + + return nil +} + +func selectProtectedTagByContext(ctx *context.Context) *models.ProtectedTag { + id := ctx.QueryInt64("id") + if id == 0 { + id = ctx.ParamsInt64(":id") + } + + tag, err := models.GetProtectedTagByID(id) + if err != nil { + ctx.ServerError("GetProtectedTagByID", err) + return nil + } + + if tag != nil && tag.RepoID == ctx.Repo.Repository.ID { + return tag + } + + ctx.NotFound("", fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, ctx.Repo.Repository)) + + return nil +} diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index cceb8451e5..5271fe9b4a 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -316,7 +317,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["Commits"] = commitsHistory - pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5) + pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go index 827b7cdef0..6b4beff0e0 100644 --- a/routers/web/user/auth.go +++ b/routers/web/user/auth.go @@ -1478,6 +1478,7 @@ func ForgotPasswd(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title") if setting.MailService == nil { + log.Warn(ctx.Tr("auth.disable_forgot_password_mail_admin")) ctx.Data["IsResetDisable"] = true ctx.HTML(http.StatusOK, tplForgotPassword) return diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 72d0066645..631ca21135 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -75,6 +75,17 @@ func Profile(ctx *context.Context) { return } + if ctxUser.IsOrganization() { + org.Home(ctx) + return + } + + // check view permissions + if !ctxUser.IsVisibleToUser(ctx.User) { + ctx.NotFound("user", fmt.Errorf(uname)) + return + } + // Show SSH keys. if isShowKeys { ShowSSHKeys(ctx, ctxUser.ID) @@ -87,11 +98,6 @@ func Profile(ctx *context.Context) { return } - if ctxUser.IsOrganization() { - org.Home(ctx) - return - } - // Show OpenID URIs openIDs, err := models.GetUserOpenIDs(ctxUser.ID) if err != nil { diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 20042caca4..463c4ec203 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -114,6 +114,7 @@ func ProfilePost(ctx *context.Context) { } ctx.User.Description = form.Description ctx.User.KeepActivityPrivate = form.KeepActivityPrivate + ctx.User.Visibility = form.Visibility if err := models.UpdateUserSetting(ctx.User); err != nil { if _, ok := err.(models.ErrEmailAlreadyUsed); ok { ctx.Flash.Error(ctx.Tr("form.email_been_used")) diff --git a/routers/web/web.go b/routers/web/web.go index 2c8a6411a1..627c88aab1 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/misc" - "code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/web/admin" "code.gitea.io/gitea/routers/web/dev" "code.gitea.io/gitea/routers/web/events" @@ -595,12 +594,21 @@ func RegisterRoutes(m *web.Route) { m.Post("/delete", repo.DeleteTeam) }) }) + m.Group("/branches", func() { m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost) m.Combo("/*").Get(repo.SettingsProtectedBranch). Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost) }, repo.MustBeNotEmpty) + m.Group("/tags", func() { + m.Get("", repo.Tags) + m.Post("", bindIgnErr(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.NewProtectedTagPost) + m.Post("/delete", context.RepoMustNotBeArchived(), repo.DeleteProtectedTagPost) + m.Get("/{id}", repo.EditProtectedTag) + m.Post("/{id}", bindIgnErr(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.EditProtectedTagPost) + }) + m.Group("/hooks/git", func() { m.Get("", repo.GitHooks) m.Combo("/{name}").Get(repo.GitHooksEdit). @@ -888,7 +896,7 @@ func RegisterRoutes(m *web.Route) { }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypeCode)) m.Group("/archive", func() { - m.Get("/*", common.Download) + m.Get("/*", repo.Download) m.Post("/*", repo.InitiateDownload) }, repo.MustBeNotEmpty, reqRepoCodeReader) diff --git a/services/archiver/archiver.go b/services/archiver/archiver.go index dfa6334d95..00c0281306 100644 --- a/services/archiver/archiver.go +++ b/services/archiver/archiver.go @@ -6,22 +6,20 @@ package archiver import ( + "errors" + "fmt" "io" - "io/ioutil" "os" - "path" "regexp" "strings" - "sync" - "time" - "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" "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/util" + "code.gitea.io/gitea/modules/storage" ) // ArchiveRequest defines the parameters of an archive request, which notably @@ -30,223 +28,174 @@ import ( // This is entirely opaque to external entities, though, and mostly used as a // handle elsewhere. type ArchiveRequest struct { - uri string - repo *git.Repository - refName string - ext string - archivePath string - archiveType git.ArchiveType - archiveComplete bool - commit *git.Commit - cchan chan struct{} + RepoID int64 + refName string + Type git.ArchiveType + CommitID string } -var archiveInProgress []*ArchiveRequest -var archiveMutex sync.Mutex - // SHA1 hashes will only go up to 40 characters, but SHA256 hashes will go all // the way to 64. var shaRegex = regexp.MustCompile(`^[0-9a-f]{4,64}$`) -// These facilitate testing, by allowing the unit tests to control (to some extent) -// the goroutine used for processing the queue. -var archiveQueueMutex *sync.Mutex -var archiveQueueStartCond *sync.Cond -var archiveQueueReleaseCond *sync.Cond +// NewRequest creates an archival request, based on the URI. The +// resulting ArchiveRequest is suitable for being passed to ArchiveRepository() +// if it's determined that the request still needs to be satisfied. +func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest, error) { + r := &ArchiveRequest{ + RepoID: repoID, + } -// GetArchivePath returns the path from which we can serve this archive. -func (aReq *ArchiveRequest) GetArchivePath() string { - return aReq.archivePath + var ext string + switch { + case strings.HasSuffix(uri, ".zip"): + ext = ".zip" + r.Type = git.ZIP + case strings.HasSuffix(uri, ".tar.gz"): + ext = ".tar.gz" + r.Type = git.TARGZ + default: + return nil, fmt.Errorf("Unknown format: %s", uri) + } + + r.refName = strings.TrimSuffix(uri, ext) + + var err error + // Get corresponding commit. + if repo.IsBranchExist(r.refName) { + r.CommitID, err = repo.GetBranchCommitID(r.refName) + if err != nil { + return nil, err + } + } else if repo.IsTagExist(r.refName) { + r.CommitID, err = repo.GetTagCommitID(r.refName) + if err != nil { + return nil, err + } + } else if shaRegex.MatchString(r.refName) { + if repo.IsCommitExist(r.refName) { + r.CommitID = r.refName + } else { + return nil, git.ErrNotExist{ + ID: r.refName, + } + } + } else { + return nil, fmt.Errorf("Unknow ref %s type", r.refName) + } + + return r, nil } // GetArchiveName returns the name of the caller, based on the ref used by the // caller to create this request. func (aReq *ArchiveRequest) GetArchiveName() string { - return aReq.refName + aReq.ext + return strings.ReplaceAll(aReq.refName, "/", "-") + "." + aReq.Type.String() } -// IsComplete returns the completion status of this request. -func (aReq *ArchiveRequest) IsComplete() bool { - return aReq.archiveComplete -} - -// WaitForCompletion will wait for this request to complete, with no timeout. -// It returns whether the archive was actually completed, as the channel could -// have also been closed due to an error. -func (aReq *ArchiveRequest) WaitForCompletion(ctx *context.Context) bool { - select { - case <-aReq.cchan: - case <-ctx.Done(): - } - - return aReq.IsComplete() -} - -// TimedWaitForCompletion will wait for this request to complete, with timeout -// happening after the specified Duration. It returns whether the archive is -// now complete and whether we hit the timeout or not. The latter may not be -// useful if the request is complete or we started to shutdown. -func (aReq *ArchiveRequest) TimedWaitForCompletion(ctx *context.Context, dur time.Duration) (bool, bool) { - timeout := false - select { - case <-time.After(dur): - timeout = true - case <-aReq.cchan: - case <-ctx.Done(): - } - - return aReq.IsComplete(), timeout -} - -// The caller must hold the archiveMutex across calls to getArchiveRequest. -func getArchiveRequest(repo *git.Repository, commit *git.Commit, archiveType git.ArchiveType) *ArchiveRequest { - for _, r := range archiveInProgress { - // Need to be referring to the same repository. - if r.repo.Path == repo.Path && r.commit.ID == commit.ID && r.archiveType == archiveType { - return r - } - } - return nil -} - -// DeriveRequestFrom creates an archival request, based on the URI. The -// resulting ArchiveRequest is suitable for being passed to ArchiveRepository() -// if it's determined that the request still needs to be satisfied. -func DeriveRequestFrom(ctx *context.Context, uri string) *ArchiveRequest { - if ctx.Repo == nil || ctx.Repo.GitRepo == nil { - log.Trace("Repo not initialized") - return nil - } - r := &ArchiveRequest{ - uri: uri, - repo: ctx.Repo.GitRepo, - } - - switch { - case strings.HasSuffix(uri, ".zip"): - r.ext = ".zip" - r.archivePath = path.Join(r.repo.Path, "archives/zip") - r.archiveType = git.ZIP - case strings.HasSuffix(uri, ".tar.gz"): - r.ext = ".tar.gz" - r.archivePath = path.Join(r.repo.Path, "archives/targz") - r.archiveType = git.TARGZ - default: - log.Trace("Unknown format: %s", uri) - return nil - } - - r.refName = strings.TrimSuffix(r.uri, r.ext) - isDir, err := util.IsDir(r.archivePath) +func doArchive(r *ArchiveRequest) (*models.RepoArchiver, error) { + ctx, commiter, err := models.TxDBContext() if err != nil { - ctx.ServerError("Download -> util.IsDir(archivePath)", err) - return nil + return nil, err } - if !isDir { - if err := os.MkdirAll(r.archivePath, os.ModePerm); err != nil { - ctx.ServerError("Download -> os.MkdirAll(archivePath)", err) - return nil - } + defer commiter.Close() + + archiver, err := models.GetRepoArchiver(ctx, r.RepoID, r.Type, r.CommitID) + if err != nil { + return nil, err } - // Get corresponding commit. - if r.repo.IsBranchExist(r.refName) { - r.commit, err = r.repo.GetBranchCommit(r.refName) - if err != nil { - ctx.ServerError("GetBranchCommit", err) - return nil - } - } else if r.repo.IsTagExist(r.refName) { - r.commit, err = r.repo.GetTagCommit(r.refName) - if err != nil { - ctx.ServerError("GetTagCommit", err) - return nil - } - } else if shaRegex.MatchString(r.refName) { - r.commit, err = r.repo.GetCommit(r.refName) - if err != nil { - ctx.NotFound("GetCommit", nil) - return nil + if archiver != nil { + // FIXME: If another process are generating it, we think it's not ready and just return + // Or we should wait until the archive generated. + if archiver.Status == models.RepoArchiverGenerating { + return nil, nil } } else { - ctx.NotFound("DeriveRequestFrom", nil) - return nil + archiver = &models.RepoArchiver{ + RepoID: r.RepoID, + Type: r.Type, + CommitID: r.CommitID, + Status: models.RepoArchiverGenerating, + } + if err := models.AddRepoArchiver(ctx, archiver); err != nil { + return nil, err + } } - archiveMutex.Lock() - defer archiveMutex.Unlock() - if rExisting := getArchiveRequest(r.repo, r.commit, r.archiveType); rExisting != nil { - return rExisting - } - - r.archivePath = path.Join(r.archivePath, base.ShortSha(r.commit.ID.String())+r.ext) - r.archiveComplete, err = util.IsFile(r.archivePath) + rPath, err := archiver.RelativePath() if err != nil { - ctx.ServerError("util.IsFile", err) - return nil - } - return r -} - -func doArchive(r *ArchiveRequest) { - var ( - err error - tmpArchive *os.File - destArchive *os.File - ) - - // Close the channel to indicate to potential waiters that this request - // has finished. - defer close(r.cchan) - - // It could have happened that we enqueued two archival requests, due to - // race conditions and difficulties in locking. Do one last check that - // the archive we're referring to doesn't already exist. If it does exist, - // then just mark the request as complete and move on. - isFile, err := util.IsFile(r.archivePath) - if err != nil { - log.Error("Unable to check if %s util.IsFile: %v. Will ignore and recreate.", r.archivePath, err) - } - if isFile { - r.archiveComplete = true - return + return nil, err } - // Create a temporary file to use while the archive is being built. We - // will then copy it into place (r.archivePath) once it's fully - // constructed. - tmpArchive, err = ioutil.TempFile("", "archive") - if err != nil { - log.Error("Unable to create a temporary archive file! Error: %v", err) - return + _, err = storage.RepoArchives.Stat(rPath) + if err == nil { + if archiver.Status == models.RepoArchiverGenerating { + archiver.Status = models.RepoArchiverReady + return archiver, models.UpdateRepoArchiverStatus(ctx, archiver) + } + return archiver, nil } + + if !errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("unable to stat archive: %v", err) + } + + rd, w := io.Pipe() defer func() { - tmpArchive.Close() - os.Remove(tmpArchive.Name()) + w.Close() + rd.Close() }() - - if err = r.commit.CreateArchive(graceful.GetManager().ShutdownContext(), tmpArchive.Name(), git.CreateArchiveOpts{ - Format: r.archiveType, - Prefix: setting.Repository.PrefixArchiveFiles, - }); err != nil { - log.Error("Download -> CreateArchive "+tmpArchive.Name(), err) - return - } - - // Now we copy it into place - if destArchive, err = os.Create(r.archivePath); err != nil { - log.Error("Unable to open archive " + r.archivePath) - return - } - _, err = io.Copy(destArchive, tmpArchive) - destArchive.Close() + var done = make(chan error) + repo, err := archiver.LoadRepo() if err != nil { - log.Error("Unable to write archive " + r.archivePath) - return + return nil, fmt.Errorf("archiver.LoadRepo failed: %v", err) } - // Block any attempt to finalize creating a new request if we're marking - r.archiveComplete = true + gitRepo, err := git.OpenRepository(repo.RepoPath()) + if err != nil { + return nil, err + } + defer gitRepo.Close() + + go func(done chan error, w *io.PipeWriter, archiver *models.RepoArchiver, gitRepo *git.Repository) { + defer func() { + if r := recover(); r != nil { + done <- fmt.Errorf("%v", r) + } + }() + + err = gitRepo.CreateArchive( + graceful.GetManager().ShutdownContext(), + archiver.Type, + w, + setting.Repository.PrefixArchiveFiles, + archiver.CommitID, + ) + _ = w.CloseWithError(err) + done <- err + }(done, w, archiver, gitRepo) + + // TODO: add lfs data to zip + // TODO: add submodule data to zip + + if _, err := storage.RepoArchives.Save(rPath, rd, -1); err != nil { + return nil, fmt.Errorf("unable to write archive: %v", err) + } + + err = <-done + if err != nil { + return nil, err + } + + if archiver.Status == models.RepoArchiverGenerating { + archiver.Status = models.RepoArchiverReady + if err = models.UpdateRepoArchiverStatus(ctx, archiver); err != nil { + return nil, err + } + } + + return archiver, commiter.Commit() } // ArchiveRepository satisfies the ArchiveRequest being passed in. Processing @@ -255,65 +204,46 @@ func doArchive(r *ArchiveRequest) { // anything. In all cases, the caller should be examining the *ArchiveRequest // being returned for completion, as it may be different than the one they passed // in. -func ArchiveRepository(request *ArchiveRequest) *ArchiveRequest { - // We'll return the request that's already been enqueued if it has been - // enqueued, or we'll immediately enqueue it if it has not been enqueued - // and it is not marked complete. - archiveMutex.Lock() - defer archiveMutex.Unlock() - if rExisting := getArchiveRequest(request.repo, request.commit, request.archiveType); rExisting != nil { - return rExisting - } - if request.archiveComplete { - return request - } +func ArchiveRepository(request *ArchiveRequest) (*models.RepoArchiver, error) { + return doArchive(request) +} - request.cchan = make(chan struct{}) - archiveInProgress = append(archiveInProgress, request) - go func() { - // Wait to start, if we have the Cond for it. This is currently only - // useful for testing, so that the start and release of queued entries - // can be controlled to examine the queue. - if archiveQueueStartCond != nil { - archiveQueueMutex.Lock() - archiveQueueStartCond.Wait() - archiveQueueMutex.Unlock() - } +var archiverQueue queue.UniqueQueue - // Drop the mutex while we process the request. This may take a long - // time, and it's not necessary now that we've added the reequest to - // archiveInProgress. - doArchive(request) - - if archiveQueueReleaseCond != nil { - archiveQueueMutex.Lock() - archiveQueueReleaseCond.Wait() - archiveQueueMutex.Unlock() - } - - // Purge this request from the list. To do so, we'll just take the - // index at which we ended up at and swap the final element into that - // position, then chop off the now-redundant final element. The slice - // may have change in between these two segments and we may have moved, - // so we search for it here. We could perhaps avoid this search - // entirely if len(archiveInProgress) == 1, but we should verify - // correctness. - archiveMutex.Lock() - defer archiveMutex.Unlock() - - idx := -1 - for _idx, req := range archiveInProgress { - if req == request { - idx = _idx - break +// Init initlize archive +func Init() error { + handler := func(data ...queue.Data) { + for _, datum := range data { + archiveReq, ok := datum.(*ArchiveRequest) + if !ok { + log.Error("Unable to process provided datum: %v - not possible to cast to IndexerData", datum) + continue + } + log.Trace("ArchiverData Process: %#v", archiveReq) + if _, err := doArchive(archiveReq); err != nil { + log.Error("Archive %v faild: %v", datum, err) } } - if idx == -1 { - log.Error("ArchiveRepository: Failed to find request for removal.") - return - } - archiveInProgress = append(archiveInProgress[:idx], archiveInProgress[idx+1:]...) - }() + } - return request + archiverQueue = queue.CreateUniqueQueue("repo-archive", handler, new(ArchiveRequest)) + if archiverQueue == nil { + return errors.New("unable to create codes indexer queue") + } + + go graceful.GetManager().RunWithShutdownFns(archiverQueue.Run) + + return nil +} + +// StartArchive push the archive request to the queue +func StartArchive(request *ArchiveRequest) error { + has, err := archiverQueue.Has(request) + if err != nil { + return err + } + if has { + return nil + } + return archiverQueue.Push(request) } diff --git a/services/archiver/archiver_test.go b/services/archiver/archiver_test.go index 6dcd942bf5..3f3f369987 100644 --- a/services/archiver/archiver_test.go +++ b/services/archiver/archiver_test.go @@ -6,108 +6,75 @@ package archiver import ( "path/filepath" - "sync" "testing" "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) -var queueMutex sync.Mutex - func TestMain(m *testing.M) { models.MainTest(m, filepath.Join("..", "..")) } func waitForCount(t *testing.T, num int) { - var numQueued int - // Wait for up to 10 seconds for the queue to be impacted. - timeout := time.Now().Add(10 * time.Second) - for { - numQueued = len(archiveInProgress) - if numQueued == num || time.Now().After(timeout) { - break - } - } - - assert.Len(t, archiveInProgress, num) -} - -func releaseOneEntry(t *testing.T, inFlight []*ArchiveRequest) { - var nowQueued, numQueued int - - numQueued = len(archiveInProgress) - - // Release one, then wait up to 10 seconds for it to complete. - queueMutex.Lock() - archiveQueueReleaseCond.Signal() - queueMutex.Unlock() - timeout := time.Now().Add(10 * time.Second) - for { - nowQueued = len(archiveInProgress) - if nowQueued != numQueued || time.Now().After(timeout) { - break - } - } - - // Make sure we didn't just timeout. - assert.NotEqual(t, numQueued, nowQueued) - - // Also make sure that we released only one. - assert.Equal(t, numQueued-1, nowQueued) } func TestArchive_Basic(t *testing.T) { assert.NoError(t, models.PrepareTestDatabase()) - archiveQueueMutex = &queueMutex - archiveQueueStartCond = sync.NewCond(&queueMutex) - archiveQueueReleaseCond = sync.NewCond(&queueMutex) - defer func() { - archiveQueueMutex = nil - archiveQueueStartCond = nil - archiveQueueReleaseCond = nil - }() - ctx := test.MockContext(t, "user27/repo49") firstCommit, secondCommit := "51f84af23134", "aacbdfe9e1c4" - bogusReq := DeriveRequestFrom(ctx, firstCommit+".zip") - assert.Nil(t, bogusReq) - test.LoadRepo(t, ctx, 49) - bogusReq = DeriveRequestFrom(ctx, firstCommit+".zip") - assert.Nil(t, bogusReq) - test.LoadGitRepo(t, ctx) defer ctx.Repo.GitRepo.Close() + bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + assert.NoError(t, err) + assert.NotNil(t, bogusReq) + assert.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName()) + // Check a series of bogus requests. // Step 1, valid commit with a bad extension. - bogusReq = DeriveRequestFrom(ctx, firstCommit+".dilbert") + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".dilbert") + assert.Error(t, err) assert.Nil(t, bogusReq) // Step 2, missing commit. - bogusReq = DeriveRequestFrom(ctx, "dbffff.zip") + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff.zip") + assert.Error(t, err) assert.Nil(t, bogusReq) // Step 3, doesn't look like branch/tag/commit. - bogusReq = DeriveRequestFrom(ctx, "db.zip") + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db.zip") + assert.Error(t, err) assert.Nil(t, bogusReq) + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master.zip") + assert.NoError(t, err) + assert.NotNil(t, bogusReq) + assert.EqualValues(t, "master.zip", bogusReq.GetArchiveName()) + + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive.zip") + assert.NoError(t, err) + assert.NotNil(t, bogusReq) + assert.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName()) + // Now two valid requests, firstCommit with valid extensions. - zipReq := DeriveRequestFrom(ctx, firstCommit+".zip") + zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + assert.NoError(t, err) assert.NotNil(t, zipReq) - tgzReq := DeriveRequestFrom(ctx, firstCommit+".tar.gz") + tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".tar.gz") + assert.NoError(t, err) assert.NotNil(t, tgzReq) - secondReq := DeriveRequestFrom(ctx, secondCommit+".zip") + secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".zip") + assert.NoError(t, err) assert.NotNil(t, secondReq) inFlight := make([]*ArchiveRequest, 3) @@ -128,41 +95,9 @@ func TestArchive_Basic(t *testing.T) { // Sleep two seconds to make sure the queue doesn't change. time.Sleep(2 * time.Second) - assert.Len(t, archiveInProgress, 3) - // Release them all, they'll then stall at the archiveQueueReleaseCond while - // we examine the queue state. - queueMutex.Lock() - archiveQueueStartCond.Broadcast() - queueMutex.Unlock() - - // Iterate through all of the in-flight requests and wait for their - // completion. - for _, req := range inFlight { - req.WaitForCompletion(ctx) - } - - for _, req := range inFlight { - assert.True(t, req.IsComplete()) - exist, err := util.IsExist(req.GetArchivePath()) - assert.NoError(t, err) - assert.True(t, exist) - } - - arbitraryReq := inFlight[0] - // Reopen the channel so we don't double-close, mark it incomplete. We're - // going to run it back through the archiver, and it should get marked - // complete again. - arbitraryReq.cchan = make(chan struct{}) - arbitraryReq.archiveComplete = false - doArchive(arbitraryReq) - assert.True(t, arbitraryReq.IsComplete()) - - // Queues should not have drained yet, because we haven't released them. - // Do so now. - assert.Len(t, archiveInProgress, 3) - - zipReq2 := DeriveRequestFrom(ctx, firstCommit+".zip") + zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + assert.NoError(t, err) // This zipReq should match what's sitting in the queue, as we haven't // let it release yet. From the consumer's point of view, this looks like // a long-running archive task. @@ -173,46 +108,22 @@ func TestArchive_Basic(t *testing.T) { // predecessor has cleared out of the queue. ArchiveRepository(zipReq2) - // Make sure the queue hasn't grown any. - assert.Len(t, archiveInProgress, 3) - - // Make sure the queue drains properly - releaseOneEntry(t, inFlight) - assert.Len(t, archiveInProgress, 2) - releaseOneEntry(t, inFlight) - assert.Len(t, archiveInProgress, 1) - releaseOneEntry(t, inFlight) - assert.Empty(t, archiveInProgress) - // Now we'll submit a request and TimedWaitForCompletion twice, before and // after we release it. We should trigger both the timeout and non-timeout // cases. - var completed, timedout bool - timedReq := DeriveRequestFrom(ctx, secondCommit+".tar.gz") + timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz") + assert.NoError(t, err) assert.NotNil(t, timedReq) ArchiveRepository(timedReq) - // Guaranteed to timeout; we haven't signalled the request to start.. - completed, timedout = timedReq.TimedWaitForCompletion(ctx, 2*time.Second) - assert.False(t, completed) - assert.True(t, timedout) - - queueMutex.Lock() - archiveQueueStartCond.Broadcast() - queueMutex.Unlock() - - // Shouldn't timeout, we've now signalled it and it's a small request. - completed, timedout = timedReq.TimedWaitForCompletion(ctx, 15*time.Second) - assert.True(t, completed) - assert.False(t, timedout) - - zipReq2 = DeriveRequestFrom(ctx, firstCommit+".zip") + zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + assert.NoError(t, err) // Now, we're guaranteed to have released the original zipReq from the queue. // Ensure that we don't get handed back the released entry somehow, but they // should remain functionally equivalent in all fields. The exception here // is zipReq.cchan, which will be non-nil because it's a completed request. // It's fine to go ahead and set it to nil now. - zipReq.cchan = nil + assert.Equal(t, zipReq, zipReq2) assert.False(t, zipReq == zipReq2) diff --git a/services/forms/admin.go b/services/forms/admin.go index 2e6bbaf172..5abef0550e 100644 --- a/services/forms/admin.go +++ b/services/forms/admin.go @@ -8,6 +8,7 @@ import ( "net/http" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" "gitea.com/go-chi/binding" @@ -22,6 +23,7 @@ type AdminCreateUserForm struct { Password string `binding:"MaxSize(255)"` SendNotify bool MustChangePassword bool + Visibility structs.VisibleType } // Validate validates form fields @@ -49,6 +51,7 @@ type AdminEditUserForm struct { AllowCreateOrganization bool ProhibitLogin bool Reset2FA bool `form:"reset_2fa"` + Visibility structs.VisibleType } // Validate validates form fields diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go new file mode 100644 index 0000000000..337e7fe1ea --- /dev/null +++ b/services/forms/repo_tag_form.go @@ -0,0 +1,27 @@ +// Copyright 2021 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 forms + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/web/middleware" + + "gitea.com/go-chi/binding" +) + +// ProtectTagForm form for changing protected tag settings +type ProtectTagForm struct { + NamePattern string `binding:"Required;GlobOrRegexPattern"` + AllowlistUsers string + AllowlistTeams string +} + +// Validate validates the fields +func (f *ProtectTagForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 2c065dc511..439ddfc7c6 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" "gitea.com/go-chi/binding" @@ -226,10 +227,11 @@ type UpdateProfileForm struct { Name string `binding:"AlphaDashDot;MaxSize(40)"` FullName string `binding:"MaxSize(100)"` KeepEmailPrivate bool - Website string `binding:"ValidUrl;MaxSize(255)"` + Website string `binding:"ValidSiteUrl;MaxSize(255)"` Location string `binding:"MaxSize(50)"` Language string Description string `binding:"MaxSize(255)"` + Visibility structs.VisibleType KeepActivityPrivate bool } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index f8c25a3912..2386552efe 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -13,7 +13,6 @@ import ( "testing" "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/setting" jsoniter "github.com/json-iterator/go" @@ -514,7 +513,6 @@ func TestDiffLine_GetCommentSide(t *testing.T) { } func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) { - git.Debug = true for _, behavior := range []string{"-w", "--ignore-space-at-eol", "-b", ""} { diffs, err := GetDiffRangeWithWhitespaceBehavior("./testdata/academic-module", "559c156f8e0178b71cb44355428f24001b08fc68", "bd7063cc7c04689c4d082183d32a604ed27a24f9", setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles, behavior) diff --git a/services/pull/pull.go b/services/pull/pull.go index 02c0a7fe7c..db216ddbf4 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -9,6 +9,7 @@ import ( "bytes" "context" "fmt" + "regexp" "strings" "time" @@ -389,6 +390,10 @@ func checkIfPRContentChanged(pr *models.PullRequest, oldCommitID, newCommitID st // corresponding branches of base repository. // FIXME: Only push branches that are actually updates? func PushToBaseRepo(pr *models.PullRequest) (err error) { + return pushToBaseRepoHelper(pr, "") +} + +func pushToBaseRepoHelper(pr *models.PullRequest, prefixHeadBranch string) (err error) { log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitRefName()) if err := pr.LoadHeadRepo(); err != nil { @@ -414,7 +419,7 @@ func PushToBaseRepo(pr *models.PullRequest) (err error) { if err := git.Push(headRepoPath, git.PushOptions{ Remote: baseRepoPath, - Branch: pr.HeadBranch + ":" + gitRefName, + Branch: prefixHeadBranch + pr.HeadBranch + ":" + gitRefName, Force: true, // Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/... Env: models.InternalPushingEnvironment(pr.Issue.Poster, pr.BaseRepo), @@ -427,6 +432,14 @@ func PushToBaseRepo(pr *models.PullRequest) (err error) { rejectErr := err.(*git.ErrPushRejected) log.Info("Unable to push PR head for %s#%d (%-v:%s) due to rejection:\nStdout: %s\nStderr: %s\nError: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, rejectErr.StdOut, rejectErr.StdErr, rejectErr.Err) return err + } else if git.IsErrMoreThanOne(err) { + if prefixHeadBranch != "" { + log.Info("Can't push with %s%s", prefixHeadBranch, pr.HeadBranch) + return err + } + log.Info("Retrying to push with refs/heads/%s", pr.HeadBranch) + err = pushToBaseRepoHelper(pr, "refs/heads/") + return err } log.Error("Unable to push PR head for %s#%d (%-v:%s) due to Error: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, err) return fmt.Errorf("Push: %s:%s %s:%s %v", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), gitRefName, err) @@ -516,6 +529,8 @@ func CloseRepoBranchesPulls(doer *models.User, repo *models.Repository) error { return nil } +var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`) + // GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one) func GetSquashMergeCommitMessages(pr *models.PullRequest) string { if err := pr.LoadIssue(); err != nil { @@ -571,10 +586,13 @@ func GetSquashMergeCommitMessages(pr *models.PullRequest) string { stringBuilder := strings.Builder{} if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages { - stringBuilder.WriteString(pr.Issue.Content) + message := strings.TrimSpace(pr.Issue.Content) + stringBuilder.WriteString(message) if stringBuilder.Len() > 0 { stringBuilder.WriteRune('\n') - stringBuilder.WriteRune('\n') + if !commitMessageTrailersPattern.MatchString(message) { + stringBuilder.WriteRune('\n') + } } } @@ -645,13 +663,6 @@ func GetSquashMergeCommitMessages(pr *models.PullRequest) string { } } - if len(authors) > 0 { - if _, err := stringBuilder.WriteRune('\n'); err != nil { - log.Error("Unable to write to string builder Error: %v", err) - return "" - } - } - for _, author := range authors { if _, err := stringBuilder.Write([]byte("Co-authored-by: ")); err != nil { log.Error("Unable to write to string builder Error: %v", err) diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go index 64920e3550..81627ebb77 100644 --- a/services/pull/pull_test.go +++ b/services/pull/pull_test.go @@ -5,4 +5,27 @@ package pull +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + // TODO TestPullRequest_PushToBaseRepo + +func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) { + // Not a valid trailer section + assert.False(t, commitMessageTrailersPattern.MatchString("")) + assert.False(t, commitMessageTrailersPattern.MatchString("No trailer.")) + assert.False(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob \nNot a trailer due to following text.")) + assert.False(t, commitMessageTrailersPattern.MatchString("Message body not correctly separated from trailer section by empty line.\nSigned-off-by: Bob ")) + // Valid trailer section + assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob ")) + assert.True(t, commitMessageTrailersPattern.MatchString("Signed-off-by: Bob \nOther-Trailer: Value")) + assert.True(t, commitMessageTrailersPattern.MatchString("Message body correctly separated from trailer section by empty line.\n\nSigned-off-by: Bob ")) + assert.True(t, commitMessageTrailersPattern.MatchString("Multiple trailers.\n\nSigned-off-by: Bob \nOther-Trailer: Value")) + assert.True(t, commitMessageTrailersPattern.MatchString("Newline after trailer section.\n\nSigned-off-by: Bob \n")) + assert.True(t, commitMessageTrailersPattern.MatchString("No space after colon is accepted.\n\nSigned-off-by:Bob ")) + assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob ")) + assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value")) +} diff --git a/services/release/release.go b/services/release/release.go index 9d201edf6d..6f5aa02c85 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -23,6 +23,25 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool, // Only actual create when publish. if !rel.IsDraft { if !gitRepo.IsTagExist(rel.TagName) { + if err := rel.LoadAttributes(); err != nil { + log.Error("LoadAttributes: %v", err) + return false, err + } + + protectedTags, err := rel.Repo.GetProtectedTags() + if err != nil { + return false, fmt.Errorf("GetProtectedTags: %v", err) + } + isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, rel.TagName, rel.PublisherID) + if err != nil { + return false, err + } + if !isAllowed { + return false, models.ErrProtectedTagName{ + TagName: rel.TagName, + } + } + commit, err := gitRepo.GetCommit(rel.Target) if err != nil { return false, fmt.Errorf("GetCommit: %v", err) @@ -49,11 +68,7 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool, } created = true rel.LowerTagName = strings.ToLower(rel.TagName) - // Prepare Notify - if err := rel.LoadAttributes(); err != nil { - log.Error("LoadAttributes: %v", err) - return false, err - } + notification.NotifyPushCommits( rel.Publisher, rel.Repo, &repository.PushUpdateOptions{ @@ -137,7 +152,9 @@ func CreateNewTag(doer *models.User, repo *models.Repository, commit, tagName, m rel := &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: doer.ID, + Publisher: doer, TagName: tagName, Target: commit, IsDraft: false, diff --git a/services/release/release_test.go b/services/release/release_test.go index 085be55cb4..9f665fabab 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -33,7 +33,9 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1", Target: "master", Title: "v0.1 is released", @@ -45,7 +47,9 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.1", Target: "65f1bf27bc3bf70f64657658635e66094edbcb4d", Title: "v0.1.1 is released", @@ -57,7 +61,9 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.2", Target: "65f1bf2", Title: "v0.1.2 is released", @@ -69,7 +75,9 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.3", Target: "65f1bf2", Title: "v0.1.3 is released", @@ -81,7 +89,9 @@ func TestRelease_Create(t *testing.T) { assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.4", Target: "65f1bf2", Title: "v0.1.4 is released", @@ -99,7 +109,9 @@ func TestRelease_Create(t *testing.T) { var release = models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v0.1.5", Target: "65f1bf2", Title: "v0.1.5 is released", @@ -125,7 +137,9 @@ func TestRelease_Update(t *testing.T) { // Test a changed release assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.1.1", Target: "master", Title: "v1.1.1 is released", @@ -147,7 +161,9 @@ func TestRelease_Update(t *testing.T) { // Test a changed draft assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.2.1", Target: "65f1bf2", Title: "v1.2.1 is draft", @@ -169,7 +185,9 @@ func TestRelease_Update(t *testing.T) { // Test a changed pre-release assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.3.1", Target: "65f1bf2", Title: "v1.3.1 is pre-released", @@ -192,7 +210,9 @@ func TestRelease_Update(t *testing.T) { // Test create release release = &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v1.1.2", Target: "master", Title: "v1.1.2 is released", @@ -258,7 +278,9 @@ func TestRelease_createTag(t *testing.T) { // Test a changed release release := &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v2.1.1", Target: "master", Title: "v2.1.1 is released", @@ -280,7 +302,9 @@ func TestRelease_createTag(t *testing.T) { // Test a changed draft release = &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v2.2.1", Target: "65f1bf2", Title: "v2.2.1 is draft", @@ -301,7 +325,9 @@ func TestRelease_createTag(t *testing.T) { // Test a changed pre-release release = &models.Release{ RepoID: repo.ID, + Repo: repo, PublisherID: user.ID, + Publisher: user, TagName: "v2.3.1", Target: "65f1bf2", Title: "v2.3.1 is pre-released", diff --git a/services/repository/branch.go b/services/repository/branch.go index df07030be3..28d24f121d 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -26,7 +26,7 @@ func DeleteBranch(doer *models.User, repo *models.Repository, gitRepo *git.Repos return ErrBranchIsDefault } - isProtected, err := repo.IsProtectedBranch(branchName, doer) + isProtected, err := repo.IsProtectedBranch(branchName) if err != nil { return err } diff --git a/services/repository/push.go b/services/repository/push.go index f031073b2e..dcb3bc779f 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -193,16 +193,17 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { } commits = repo_module.ListToPushCommits(l) + + if err := repofiles.UpdateIssuesCommit(pusher, repo, commits.Commits, refName); err != nil { + log.Error("updateIssuesCommit: %v", err) + } + if len(commits.Commits) > setting.UI.FeedMaxCommitNum { commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] } commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID) notification.NotifyPushCommits(pusher, repo, opts, commits) - if err := repofiles.UpdateIssuesCommit(pusher, repo, commits.Commits, refName); err != nil { - log.Error("updateIssuesCommit: %v", err) - } - if err = models.RemoveDeletedBranch(repo.ID, branch); err != nil { log.Error("models.RemoveDeletedBranch %s/%s failed: %v", repo.ID, branch, err) } diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index af01489c0a..dba24d9837 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -28,6 +28,33 @@
+ +
+ + +
+
diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl index 885045dd02..2e39172535 100644 --- a/templates/admin/user/new.tmpl +++ b/templates/admin/user/new.tmpl @@ -24,6 +24,25 @@
+ +
+ + +
+
diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl index 61fe02037c..f7257f6e6b 100644 --- a/templates/mail/issue/default.tmpl +++ b/templates/mail/issue/default.tmpl @@ -33,7 +33,7 @@ {{if eq .Comment.Commits.Len 1}} {{.i18n.Tr "mail.issue.action.push_1" .Doer.Name .Comment.Issue.PullRequest.HeadBranch | Str2html}} {{else}} - {{.i18n.Tr "mail.issue.action.push_1" .Doer.Name .Comment.Issue.PullRequest.HeadBranch .Comment.Commits.Len | Str2html}} + {{.i18n.Tr "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch .Comment.Commits.Len | Str2html}} {{end}} {{end}}

diff --git a/templates/repo/settings/nav.tmpl b/templates/repo/settings/nav.tmpl index 4b89ece349..31672cb5ea 100644 --- a/templates/repo/settings/nav.tmpl +++ b/templates/repo/settings/nav.tmpl @@ -5,6 +5,7 @@
  • {{.i18n.Tr "repo.settings.options"}}
  • {{.i18n.Tr "repo.settings.collaboration"}}
  • {{.i18n.Tr "repo.settings.branches"}}
  • +
  • {{.i18n.Tr "repo.settings.tags"}}
  • {{if not DisableWebhooks}}
  • {{.i18n.Tr "repo.settings.hooks"}}
  • {{end}} diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl index 501c3c4630..d8cdf21871 100644 --- a/templates/repo/settings/navbar.tmpl +++ b/templates/repo/settings/navbar.tmpl @@ -11,6 +11,9 @@ {{.i18n.Tr "repo.settings.branches"}} {{end}} + + {{.i18n.Tr "repo.settings.tags"}} + {{if not DisableWebhooks}} {{.i18n.Tr "repo.settings.hooks"}} diff --git a/templates/repo/settings/tags.tmpl b/templates/repo/settings/tags.tmpl new file mode 100644 index 0000000000..a2c887b1f8 --- /dev/null +++ b/templates/repo/settings/tags.tmpl @@ -0,0 +1,132 @@ +{{template "base/head" .}} +
    + {{template "repo/header" .}} + {{template "repo/settings/navbar" .}} +
    + {{template "base/alert" .}} + {{if .Repository.IsArchived}} +
    + {{.i18n.Tr "repo.settings.archive.tagsettings_unavailable"}} +
    + {{else}} +

    + {{.i18n.Tr "repo.settings.tags.protection"}} +

    + +
    +
    + + +
    + + + + + + + + {{range .ProtectedTags}} + + + + + + {{else}} + + {{end}} + +
    {{.i18n.Tr "repo.settings.tags.protection.pattern"}}{{.i18n.Tr "repo.settings.tags.protection.allowed"}}
    {{.NamePattern}}
    + {{if or .AllowlistUserIDs (and $.Owner.IsOrganization .AllowlistTeamIDs)}} + {{$userIDs := .AllowlistUserIDs}} + {{range $.Users}} + {{if contain $userIDs .ID }} + {{avatar . 26}} {{.GetDisplayName}} + {{end}} + {{end}} + {{if $.Owner.IsOrganization}} + {{$teamIDs := .AllowlistTeamIDs}} + {{range $.Teams}} + {{if contain $teamIDs .ID }} + {{.Name}} + {{end}} + {{end}} + {{end}} + {{else}} + {{$.i18n.Tr "repo.settings.tags.protection.allowed.noone"}} + {{end}} + + {{$.i18n.Tr "edit"}} +
    + {{$.CsrfTokenHtml}} + + +
    +
    {{.i18n.Tr "repo.settings.tags.protection.none"}}
    +
    +
    +
    + {{end}} +
    +
    +{{template "base/footer" .}} diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl index 916272a97a..e77e747742 100644 --- a/templates/repo/settings/webhook/base_list.tmpl +++ b/templates/repo/settings/webhook/base_list.tmpl @@ -41,7 +41,7 @@ {{.Description | Str2html}}
    {{range .Webhooks}} -
    +
    {{if eq .LastStatus 1}} {{svg "octicon-check"}} {{else if eq .LastStatus 2}} @@ -49,8 +49,8 @@ {{else}} {{svg "octicon-dot-fill"}} {{end}} - {{.URL}} -
    + {{.URL}} + diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 017dc824d7..7f7907b3b0 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3657,7 +3657,7 @@ "repository" ], "summary": "Gets the tag object of an annotated tag (not lightweight tags)", - "operationId": "GetTag", + "operationId": "GetAnnotatedTag", "parameters": [ { "type": "string", @@ -9117,7 +9117,7 @@ ], "responses": { "200": { - "$ref": "#/responses/AnnotatedTag" + "$ref": "#/responses/Tag" }, "404": { "$ref": "#/responses/notFound" @@ -9129,6 +9129,47 @@ } }, "/repos/{owner}/{repo}/tags/{tag}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get the tag of a repository by tag name", + "operationId": "repoGetTag", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of tag", + "name": "tag", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Tag" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, "delete": { "produces": [ "application/json" @@ -10911,6 +10952,47 @@ } } }, + "/user/settings": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get user settings", + "operationId": "getUserSettings", + "responses": { + "200": { + "$ref": "#/responses/UserSettings" + } + } + }, + "patch": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Update user settings", + "operationId": "updateUserSettings", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UserSettingsOptions" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/UserSettings" + } + } + } + }, "/user/starred": { "get": { "produces": [ @@ -13252,6 +13334,10 @@ "username": { "type": "string", "x-go-name": "Username" + }, + "visibility": { + "type": "string", + "x-go-name": "Visibility" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -14061,6 +14147,10 @@ "format": "int64", "x-go-name": "SourceID" }, + "visibility": { + "type": "string", + "x-go-name": "Visibility" + }, "website": { "type": "string", "x-go-name": "Website" @@ -16555,6 +16645,11 @@ "format": "int64", "x-go-name": "StarredRepos" }, + "visibility": { + "description": "User visibility level option: public, limited, private", + "type": "string", + "x-go-name": "Visibility" + }, "website": { "description": "the user's website", "type": "string", @@ -16578,6 +16673,94 @@ }, "x-go-package": "code.gitea.io/gitea/models" }, + "UserSettings": { + "description": "UserSettings represents user settings", + "type": "object", + "properties": { + "description": { + "type": "string", + "x-go-name": "Description" + }, + "diff_view_style": { + "type": "string", + "x-go-name": "DiffViewStyle" + }, + "full_name": { + "type": "string", + "x-go-name": "FullName" + }, + "hide_activity": { + "type": "boolean", + "x-go-name": "HideActivity" + }, + "hide_email": { + "description": "Privacy", + "type": "boolean", + "x-go-name": "HideEmail" + }, + "language": { + "type": "string", + "x-go-name": "Language" + }, + "location": { + "type": "string", + "x-go-name": "Location" + }, + "theme": { + "type": "string", + "x-go-name": "Theme" + }, + "website": { + "type": "string", + "x-go-name": "Website" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "UserSettingsOptions": { + "description": "UserSettingsOptions represents options to change user settings", + "type": "object", + "properties": { + "description": { + "type": "string", + "x-go-name": "Description" + }, + "diff_view_style": { + "type": "string", + "x-go-name": "DiffViewStyle" + }, + "full_name": { + "type": "string", + "x-go-name": "FullName" + }, + "hide_activity": { + "type": "boolean", + "x-go-name": "HideActivity" + }, + "hide_email": { + "description": "Privacy", + "type": "boolean", + "x-go-name": "HideEmail" + }, + "language": { + "type": "string", + "x-go-name": "Language" + }, + "location": { + "type": "string", + "x-go-name": "Location" + }, + "theme": { + "type": "string", + "x-go-name": "Theme" + }, + "website": { + "type": "string", + "x-go-name": "Website" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "WatchInfo": { "description": "WatchInfo represents an API watch status of one repository", "type": "object", @@ -17281,6 +17464,15 @@ } } }, + "UserSettings": { + "description": "UserSettings", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/UserSettings" + } + } + }, "WatchInfo": { "description": "WatchInfo", "schema": { @@ -17335,7 +17527,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/CreateTagOption" + "$ref": "#/definitions/UserSettingsOptions" } }, "redirect": { diff --git a/templates/user/auth/forgot_passwd.tmpl b/templates/user/auth/forgot_passwd.tmpl index 241deeed4a..2ff7acb97d 100644 --- a/templates/user/auth/forgot_passwd.tmpl +++ b/templates/user/auth/forgot_passwd.tmpl @@ -22,7 +22,13 @@
    {{else if .IsResetDisable}} -

    {{.i18n.Tr "auth.disable_forgot_password_mail"}}

    +

    + {{if $.IsAdmin}} + {{.i18n.Tr "auth.disable_forgot_password_mail_admin"}} + {{else}} + {{.i18n.Tr "auth.disable_forgot_password_mail"}} + {{end}} +

    {{else if .ResendLimited}}

    {{.i18n.Tr "auth.resent_limit_prompt"}}

    {{end}} diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index 9f07226632..4b860049d8 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -47,27 +47,62 @@
    -
    - -