Merge branch 'main' into main

This commit is contained in:
6543 2022-03-08 18:35:34 +01:00 committed by GitHub
commit d87a16b86f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
553 changed files with 4787 additions and 64021 deletions

View file

@ -804,11 +804,12 @@ steps:
depends_on: [gpg-sign] depends_on: [gpg-sign]
- name: github - name: github
image: plugins/github-release:1 image: plugins/github-release:latest
pull: always pull: always
settings: settings:
files: files:
- "dist/release/*" - "dist/release/*"
file_exists: overwrite
environment: environment:
GITHUB_TOKEN: GITHUB_TOKEN:
from_secret: github_token from_secret: github_token

View file

@ -442,6 +442,7 @@ rules:
unicorn/require-post-message-target-origin: [0] unicorn/require-post-message-target-origin: [0]
unicorn/string-content: [0] unicorn/string-content: [0]
unicorn/template-indent: [2] unicorn/template-indent: [2]
unicorn/text-encoding-identifier-case: [0]
unicorn/throw-new-error: [2] unicorn/throw-new-error: [2]
use-isnan: [2] use-isnan: [2]
valid-typeof: [2, {requireStringLiterals: true}] valid-typeof: [2, {requireStringLiterals: true}]

View file

@ -14,6 +14,7 @@ rules:
declaration-block-no-redundant-longhand-properties: null declaration-block-no-redundant-longhand-properties: null
declaration-block-single-line-max-declarations: null declaration-block-single-line-max-declarations: null
declaration-empty-line-before: null declaration-empty-line-before: null
function-no-unknown: null
hue-degree-notation: null hue-degree-notation: null
indentation: 2 indentation: 2
max-line-length: null max-line-length: null

View file

@ -423,6 +423,10 @@ be reviewed by two maintainers and must pass the automatic tests.
* And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.) * And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.)
* If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version. * If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version.
* Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release. * Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release.
* Verify all release assets were correctly published through CI on dl.gitea.io and GitHub releases. Once ACKed:
* bump the version of https://dl.gitea.io/gitea/version.json
* merge the blog post PR
* announce the release in discord `#announcements`
## Copyright ## Copyright

View file

@ -235,7 +235,7 @@ clean:
.PHONY: fmt .PHONY: fmt
fmt: fmt:
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install mvdan.cc/gofumpt@latest; \ $(GO) install mvdan.cc/gofumpt@v0.3.0; \
fi fi
@echo "Running gitea-fmt (with gofumpt)..." @echo "Running gitea-fmt (with gofumpt)..."
@$(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}' @$(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}'
@ -287,7 +287,7 @@ errcheck:
.PHONY: fmt-check .PHONY: fmt-check
fmt-check: fmt-check:
@hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install mvdan.cc/gofumpt@latest; \ $(GO) install mvdan.cc/gofumpt@0.3.0; \
fi fi
# get all go files and run gitea-fmt (with gofmt) on them # get all go files and run gitea-fmt (with gofmt) on them
@diff=$$($(GO) run build/code-batch-process.go gitea-fmt -l '{file-list}'); \ @diff=$$($(GO) run build/code-batch-process.go gitea-fmt -l '{file-list}'); \
@ -313,10 +313,9 @@ lint: lint-frontend lint-backend
lint-frontend: node_modules lint-frontend: node_modules
npx eslint --color --max-warnings=0 web_src/js build templates *.config.js docs/assets/js npx eslint --color --max-warnings=0 web_src/js build templates *.config.js docs/assets/js
npx stylelint --color --max-warnings=0 web_src/less npx stylelint --color --max-warnings=0 web_src/less
npx editorconfig-checker templates
.PHONY: lint-backend .PHONY: lint-backend
lint-backend: golangci-lint vet lint-backend: golangci-lint vet editorconfig-checker
.PHONY: watch .PHONY: watch
watch: watch:
@ -405,6 +404,11 @@ test-sqlite-migration: migrations.sqlite.test migrations.individual.sqlite.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.sqlite.test GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.sqlite.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.individual.sqlite.test GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.individual.sqlite.test
.PHONY: test-sqlite-migration\#%
test-sqlite-migration\#%: migrations.sqlite.test migrations.individual.sqlite.test generate-ini-sqlite
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.individual.sqlite.test -test.run $(subst .,/,$*)
generate-ini-mysql: generate-ini-mysql:
sed -e 's|{{TEST_MYSQL_HOST}}|${TEST_MYSQL_HOST}|g' \ sed -e 's|{{TEST_MYSQL_HOST}}|${TEST_MYSQL_HOST}|g' \
-e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \ -e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \
@ -510,6 +514,10 @@ bench-pgsql: integrations.pgsql.test generate-ini-pgsql
integration-test-coverage: integrations.cover.test generate-ini-mysql integration-test-coverage: integrations.cover.test generate-ini-mysql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out
.PHONY: integration-test-coverage-sqlite
integration-test-coverage-sqlite: integrations.cover.sqlite.test generate-ini-sqlite
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out
integrations.mysql.test: git-check $(GO_SOURCES) integrations.mysql.test: git-check $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.mysql.test $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.mysql.test
@ -528,6 +536,9 @@ integrations.sqlite.test: git-check $(GO_SOURCES)
integrations.cover.test: git-check $(GO_SOURCES) integrations.cover.test: git-check $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -coverpkg $(shell echo $(GO_PACKAGES) | tr ' ' ',') -o integrations.cover.test $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -coverpkg $(shell echo $(GO_PACKAGES) | tr ' ' ',') -o integrations.cover.test
integrations.cover.sqlite.test: git-check $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -coverpkg $(shell echo $(GO_PACKAGES) | tr ' ' ',') -o integrations.cover.sqlite.test -tags '$(TEST_TAGS)'
.PHONY: migrations.mysql.test .PHONY: migrations.mysql.test
migrations.mysql.test: $(GO_SOURCES) migrations.mysql.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.mysql.test $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.mysql.test
@ -784,6 +795,13 @@ golangci-lint-check:
curl -sfL "https://raw.githubusercontent.com/golangci/golangci-lint/v${MIN_GOLANGCI_LINT_VER_FMT}/install.sh" | sh -s -- -b $(GOPATH)/bin v$(MIN_GOLANGCI_LINT_VER_FMT); \ curl -sfL "https://raw.githubusercontent.com/golangci/golangci-lint/v${MIN_GOLANGCI_LINT_VER_FMT}/install.sh" | sh -s -- -b $(GOPATH)/bin v$(MIN_GOLANGCI_LINT_VER_FMT); \
fi fi
.PHONY: editorconfig-checker
editorconfig-checker:
@hash editorconfig-checker > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@50adf46752da119dfef66e57be3ce2693ea4aa9c; \
fi
editorconfig-checker templates
.PHONY: docker .PHONY: docker
docker: docker:
docker build --disable-content-trust=false -t $(DOCKER_REF) . docker build --disable-content-trust=false -t $(DOCKER_REF) .

View file

@ -56,6 +56,7 @@ var (
microcmdUserList, microcmdUserList,
microcmdUserChangePassword, microcmdUserChangePassword,
microcmdUserDelete, microcmdUserDelete,
microcmdUserGenerateAccessToken,
}, },
} }
@ -154,6 +155,27 @@ var (
Action: runDeleteUser, Action: runDeleteUser,
} }
microcmdUserGenerateAccessToken = cli.Command{
Name: "generate-access-token",
Usage: "Generate a access token for a specific user",
Flags: []cli.Flag{
cli.StringFlag{
Name: "username,u",
Usage: "Username",
},
cli.StringFlag{
Name: "token-name,t",
Usage: "Token name",
Value: "gitea-admin",
},
cli.BoolFlag{
Name: "raw",
Usage: "Display only the token value",
},
},
Action: runGenerateAccessToken,
}
subcmdRepoSyncReleases = cli.Command{ subcmdRepoSyncReleases = cli.Command{
Name: "repo-sync-releases", Name: "repo-sync-releases",
Usage: "Synchronize repository releases with tags", Usage: "Synchronize repository releases with tags",
@ -641,6 +663,41 @@ func runDeleteUser(c *cli.Context) error {
return user_service.DeleteUser(user) return user_service.DeleteUser(user)
} }
func runGenerateAccessToken(c *cli.Context) error {
if !c.IsSet("username") {
return fmt.Errorf("You must provide the username to generate a token for them")
}
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil {
return err
}
user, err := user_model.GetUserByName(c.String("username"))
if err != nil {
return err
}
t := &models.AccessToken{
Name: c.String("token-name"),
UID: user.ID,
}
if err := models.NewAccessToken(t); err != nil {
return err
}
if c.Bool("raw") {
fmt.Printf("%s\n", t.Token)
} else {
fmt.Printf("Access token was successfully created: %s\n", t.Token)
}
return nil
}
func runRepoSyncReleases(_ *cli.Context) error { func runRepoSyncReleases(_ *cli.Context) error {
ctx, cancel := installSignals() ctx, cancel := installSignals()
defer cancel() defer cancel()

View file

@ -4,5 +4,5 @@ grep 'git' go.mod | grep '\.com' | grep -v indirect | grep -v replace | cut -f 2
go get -u "$line" go get -u "$line"
make vendor make vendor
git add . git add .
git commit -S -m "update $line" git commit -m "update $line"
done done

View file

@ -890,7 +890,7 @@ PATH =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ;;
;; Path for local repository copy. Defaults to `tmp/local-repo` ;; Path for local repository copy. Defaults to `tmp/local-repo` (content gets deleted on gitea restart)
;LOCAL_COPY_PATH = tmp/local-repo ;LOCAL_COPY_PATH = tmp/local-repo
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -902,7 +902,7 @@ PATH =
;; Whether repository file uploads are enabled. Defaults to `true` ;; Whether repository file uploads are enabled. Defaults to `true`
;ENABLED = true ;ENABLED = true
;; ;;
;; Path for uploads. Defaults to `data/tmp/uploads` (tmp gets deleted on gitea restart) ;; Path for uploads. Defaults to `data/tmp/uploads` (content gets deleted on gitea restart)
;TEMP_PATH = data/tmp/uploads ;TEMP_PATH = data/tmp/uploads
;; ;;
;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. ;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
@ -1115,7 +1115,7 @@ PATH =
;SEARCH_REPO_DESCRIPTION = true ;SEARCH_REPO_DESCRIPTION = true
;; ;;
;; Whether to enable a Service Worker to cache frontend assets ;; Whether to enable a Service Worker to cache frontend assets
;USE_SERVICE_WORKER = true ;USE_SERVICE_WORKER = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -2125,6 +2125,8 @@ PATH =
;RENDER_COMMAND = "asciidoc --out-file=- -" ;RENDER_COMMAND = "asciidoc --out-file=- -"
;; Don't pass the file on STDIN, pass the filename as argument instead. ;; Don't pass the file on STDIN, pass the filename as argument instead.
;IS_INPUT_FILE = false ;IS_INPUT_FILE = false
; Don't filter html tags and attributes if true
;DISABLE_SANITIZER = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -18,7 +18,7 @@ params:
description: Git with a cup of tea description: Git with a cup of tea
author: The Gitea Authors author: The Gitea Authors
website: https://docs.gitea.io website: https://docs.gitea.io
version: 1.16.0 version: 1.16.3
minGoVersion: 1.16 minGoVersion: 1.16
goVersion: 1.17 goVersion: 1.17
minNodeVersion: 12.17 minNodeVersion: 12.17

View file

@ -107,7 +107,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
### Repository - Upload (`repository.upload`) ### Repository - Upload (`repository.upload`)
- `ENABLED`: **true**: Whether repository file uploads are enabled - `ENABLED`: **true**: Whether repository file uploads are enabled
- `TEMP_PATH`: **data/tmp/uploads**: Path for uploads (tmp gets deleted on Gitea restart) - `TEMP_PATH`: **data/tmp/uploads**: Path for uploads (content gets deleted on Gitea restart)
- `ALLOWED_TYPES`: **\<empty\>**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types. - `ALLOWED_TYPES`: **\<empty\>**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
- `FILE_MAX_SIZE`: **3**: Max size of each file in megabytes. - `FILE_MAX_SIZE`: **3**: Max size of each file in megabytes.
- `MAX_FILES`: **5**: Max number of files per upload - `MAX_FILES`: **5**: Max number of files per upload
@ -144,7 +144,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
## Repository - Local (`repository.local`) ## Repository - Local (`repository.local`)
- `LOCAL_COPY_PATH`: **tmp/local-repo**: Path for temporary local repository copies. Defaults to `tmp/local-repo` - `LOCAL_COPY_PATH`: **tmp/local-repo**: Path for temporary local repository copies. Defaults to `tmp/local-repo` (content gets deleted on Gitea restart)
## Repository - MIME type mapping (`repository.mimetype_mapping`) ## Repository - MIME type mapping (`repository.mimetype_mapping`)
@ -189,7 +189,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
add it to this config. add it to this config.
- `DEFAULT_SHOW_FULL_NAME`: **false**: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used. - `DEFAULT_SHOW_FULL_NAME`: **false**: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
- `SEARCH_REPO_DESCRIPTION`: **true**: Whether to search within description at repository search on explore page. - `SEARCH_REPO_DESCRIPTION`: **true**: Whether to search within description at repository search on explore page.
- `USE_SERVICE_WORKER`: **true**: Whether to enable a Service Worker to cache frontend assets. - `USE_SERVICE_WORKER`: **false**: Whether to enable a Service Worker to cache frontend assets.
### UI - Admin (`ui.admin`) ### UI - Admin (`ui.admin`)
@ -1003,13 +1003,13 @@ IS_INPUT_FILE = false
command. Multiple extensions needs a comma as splitter. command. Multiple extensions needs a comma as splitter.
- RENDER\_COMMAND: External command to render all matching extensions. - RENDER\_COMMAND: External command to render all matching extensions.
- IS\_INPUT\_FILE: **false** Input is not a standard input but a file param followed `RENDER_COMMAND`. - IS\_INPUT\_FILE: **false** Input is not a standard input but a file param followed `RENDER_COMMAND`.
- DISABLE_SANITIZER: **false** Don't filter html tags and attributes if true. Don't change this to true except you know what that means.
Two special environment variables are passed to the render command: Two special environment variables are passed to the render command:
- `GITEA_PREFIX_SRC`, which contains the current URL prefix in the `src` path tree. To be used as prefix for links. - `GITEA_PREFIX_SRC`, which contains the current URL prefix in the `src` path tree. To be used as prefix for links.
- `GITEA_PREFIX_RAW`, which contains the current URL prefix in the `raw` path tree. To be used as prefix for image paths. - `GITEA_PREFIX_RAW`, which contains the current URL prefix in the `raw` path tree. To be used as prefix for image paths.
If `DISABLE_SANITIZER` is false, Gitea supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc.
Gitea supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc.
```ini ```ini
[markup.sanitizer.TeX] [markup.sanitizer.TeX]

View file

@ -318,6 +318,33 @@ IS_INPUT_FILE = false
- FILE_EXTENSIONS: 关联的文档的扩展名,多个扩展名用都好分隔。 - FILE_EXTENSIONS: 关联的文档的扩展名,多个扩展名用都好分隔。
- RENDER_COMMAND: 工具的命令行命令及参数。 - RENDER_COMMAND: 工具的命令行命令及参数。
- IS_INPUT_FILE: 输入方式是最后一个参数为文件路径还是从标准输入读取。 - IS_INPUT_FILE: 输入方式是最后一个参数为文件路径还是从标准输入读取。
- DISABLE_SANITIZER: **false** 如果为 true 则不过滤 HTML 标签和属性。除非你知道这意味着什么,否则不要设置为 true。
以下两个环境变量将会被传递给渲染命令:
- `GITEA_PREFIX_SRC`:包含当前的`src`路径的URL前缀可以被用于链接的前缀。
- `GITEA_PREFIX_RAW`:包含当前的`raw`路径的URL前缀可以被用于图片的前缀。
如果 `DISABLE_SANITIZER` 为 false则 Gitea 支持自定义渲染 HTML 的净化策略。以下例子将用 pandoc 支持 KaTeX 输出。
```ini
[markup.sanitizer.TeX]
; Pandoc renders TeX segments as <span>s with the "math" class, optionally
; with "inline" or "display" classes depending on context.
ELEMENT = span
ALLOW_ATTR = class
REGEXP = ^\s*((math(\s+|$)|inline(\s+|$)|display(\s+|$)))+
ALLOW_DATA_URI_IMAGES = true
```
- `ELEMENT`: 将要被应用到该策略的 HTML 元素,不能为空。
- `ALLOW_ATTR`: 将要被应用到该策略的属性,不能为空。
- `REGEXP`: 正则表达式,用来匹配属性的内容。如果为空,则跟属性内容无关。
- `ALLOW_DATA_URI_IMAGES`: **false** 允许 data uri 图片 (`<img src="data:image/png;base64,..."/>`)。
多个净化规则可以被同时定义只要section名称最后一位不重复即可。如 `[markup.sanitizer.TeX-2]`
为了针对一种渲染类型进行一个特殊的净化策略,必须使用形如 `[markup.sanitizer.asciidoc.rule-1]` 的方式来命名 seciton。
如果此规则没有匹配到任何渲染类型,它将会被应用到所有的渲染类型。
## Time (`time`) ## Time (`time`)

View file

@ -103,6 +103,27 @@ Once your configuration changes have been made, restart Gitea to have changes ta
**Note**: Prior to Gitea 1.12 there was a single `markup.sanitiser` section with keys that were redefined for multiple rules, however, **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. there were significant problems with this method of configuration necessitating configuration through multiple sections.
### Example: HTML
Render HTML files directly:
```ini
[markup.html]
ENABLED = true
FILE_EXTENSIONS = .html,.htm
RENDER_COMMAND = cat
; Input is not a standard input but a file
IS_INPUT_FILE = true
[markup.sanitizer.html.1]
ELEMENT = div
ALLOW_ATTR = class
[markup.sanitizer.html.2]
ELEMENT = a
ALLOW_ATTR = class
```
### Example: Office DOCX ### Example: Office DOCX
Display Office DOCX files with [`pandoc`](https://pandoc.org/): Display Office DOCX files with [`pandoc`](https://pandoc.org/):

View file

@ -185,8 +185,6 @@ Before committing, make sure the linters pass:
make lint-frontend make lint-frontend
``` ```
Note: When working on frontend code, set `USE_SERVICE_WORKER` to `false` in `app.ini` to prevent undesirable caching of frontend assets.
### Configuring local ElasticSearch instance ### Configuring local ElasticSearch instance
Start local ElasticSearch instance using docker: Start local ElasticSearch instance using docker:

View file

@ -48,3 +48,9 @@ To deploy Gitea to DigitalOcean, have a look at the [DigitalOcean Marketplace](h
[Linode](https://www.linode.com/) has Gitea as an app in their marketplace. [Linode](https://www.linode.com/) has Gitea as an app in their marketplace.
To deploy Gitea to Linode, have a look at the [Linode Marketplace](https://www.linode.com/marketplace/apps/linode/gitea/). To deploy Gitea to Linode, have a look at the [Linode Marketplace](https://www.linode.com/marketplace/apps/linode/gitea/).
## alwaysdata
[alwaysdata](https://www.alwaysdata.com/) has Gitea as an app in their marketplace.
To deploy Gitea to alwaysdata, have a look at the [alwaysdata Marketplace](https://www.alwaysdata.com/en/marketplace/gitea/).

View file

@ -95,3 +95,59 @@ Repository Git Hooks should be regenerated if installation method is changed (eg
With Gitea running, and from the directory Gitea's binary is located, execute: `./gitea admin regenerate hooks` With Gitea running, and from the directory Gitea's binary is located, execute: `./gitea admin regenerate hooks`
This ensures that application and configuration file paths in repository Git Hooks are consistent and applicable to the current installation. If these paths are not updated, repository `push` actions will fail. This ensures that application and configuration file paths in repository Git Hooks are consistent and applicable to the current installation. If these paths are not updated, repository `push` actions will fail.
### Using Docker (`restore`)
There is also no support for a recovery command in a Docker-based gitea instance. The restore process contains the same steps as described in the previous section but with different paths.
Example:
```sh
# open bash session in container
docker exec --user git -it 2a83b293548e bash
# unzip your backup file within the container
unzip gitea-dump-1610949662.zip
cd gitea-dump-1610949662
# restore the gitea data
mv data/* /data/gitea
# restore the repositories itself
mv repos/* /data/git/repositories/
# adjust file permissions
chown -R git:git /data
# Regenerate Git Hooks
/usr/local/bin/gitea -c '/data/gitea/conf/app.ini' admin regenerate hooks
```
The default user in the gitea container is `git` (1000:1000). Please replace `2a83b293548e` with your gitea container id or name.
These are the default paths used in the container:
```text
DEFAULT CONFIGURATION:
CustomPath: /data/gitea (GITEA_CUSTOM)
CustomConf: /data/gitea/conf/app.ini
AppPath: /usr/local/bin/gitea
AppWorkPath: /usr/local/bin
```
### Using Docker-rootless (`restore`)
The restore workflow in Docker-rootless containers differs only in the directories to be used:
```sh
# open bash session in container
docker exec --user git -it 2a83b293548e bash
# unzip your backup file within the container
unzip gitea-dump-1610949662.zip
cd gitea-dump-1610949662
# restore the app.ini
mv data/conf/app.ini /etc/gitea/app.ini
# restore the gitea data
mv data/* /var/lib/gitea
# restore the repositories itself
mv repos/* /var/lib/gitea/git/repositories
# adjust file permissions
chown -R git:git /etc/gitea/app.ini /var/lib/gitea
# Regenerate Git Hooks
/usr/local/bin/gitea -c '/etc/gitea/app.ini' admin regenerate hooks
```

View file

@ -30,6 +30,10 @@ server {
location / { location / {
proxy_pass http://localhost:3000; proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
} }
} }
``` ```
@ -47,6 +51,10 @@ server {
location /git/ { location /git/ {
# Note: Trailing slash # Note: Trailing slash
proxy_pass http://localhost:3000/; proxy_pass http://localhost:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
} }
} }
``` ```

118
go.mod
View file

@ -3,100 +3,100 @@ module code.gitea.io/gitea
go 1.16 go 1.16
require ( require (
cloud.google.com/go v0.78.0 // indirect
code.gitea.io/gitea-vet v0.2.2-0.20220122151748-48ebc902541b code.gitea.io/gitea-vet v0.2.2-0.20220122151748-48ebc902541b
code.gitea.io/sdk/gitea v0.15.1 code.gitea.io/sdk/gitea v0.15.1
gitea.com/go-chi/binding v0.0.0-20211013065440-d16dc407c2be gitea.com/go-chi/binding v0.0.0-20211013065440-d16dc407c2be
gitea.com/go-chi/cache v0.0.0-20211013020926-78790b11abf1 gitea.com/go-chi/cache v0.0.0-20211201020628-dcb774c4ffea
gitea.com/go-chi/captcha v0.0.0-20211013065431-70641c1a35d5 gitea.com/go-chi/captcha v0.0.0-20211013065431-70641c1a35d5
gitea.com/go-chi/session v0.0.0-20211218221615-e3605d8b28b8 gitea.com/go-chi/session v0.0.0-20211218221615-e3605d8b28b8
gitea.com/lunny/levelqueue v0.4.1 gitea.com/lunny/levelqueue v0.4.1
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
github.com/Microsoft/go-winio v0.5.0 // indirect github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/ProtonMail/go-crypto v0.0.0-20210705153151-cc34b1f6908b // indirect github.com/ProtonMail/go-crypto v0.0.0-20220113124808-70ae35bab23f // indirect
github.com/PuerkitoBio/goquery v1.7.0 github.com/PuerkitoBio/goquery v1.8.0
github.com/alecthomas/chroma v0.10.0 github.com/alecthomas/chroma v0.10.0
github.com/andybalholm/brotli v1.0.3 // indirect github.com/andybalholm/brotli v1.0.4 // indirect
github.com/andybalholm/cascadia v1.2.0 // indirect github.com/bits-and-blooms/bitset v1.2.1 // indirect
github.com/blevesearch/bleve/v2 v2.3.0 github.com/blevesearch/bleve/v2 v2.3.1
github.com/boombuler/barcode v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
github.com/caddyserver/certmagic v0.15.2 github.com/caddyserver/certmagic v0.15.4
github.com/chi-middleware/proxy v1.1.1 github.com/chi-middleware/proxy v1.1.1
github.com/couchbase/go-couchbase v0.0.0-20210224140812-5740cd35f448 // indirect github.com/couchbase/go-couchbase v0.0.0-20210224140812-5740cd35f448 // indirect
github.com/couchbase/gomemcached v0.1.2 // indirect github.com/couchbase/gomemcached v0.1.2 // indirect
github.com/couchbase/goutils v0.0.0-20210118111533-e33d3ffb5401 // indirect github.com/couchbase/goutils v0.0.0-20210118111533-e33d3ffb5401 // indirect
github.com/denisenkom/go-mssqldb v0.10.0 github.com/denisenkom/go-mssqldb v0.12.0
github.com/djherbis/buffer v1.2.0 github.com/djherbis/buffer v1.2.0
github.com/djherbis/nio/v3 v3.0.1 github.com/djherbis/nio/v3 v3.0.1
github.com/duo-labs/webauthn v0.0.0-20220122034320-81aea484c951 github.com/duo-labs/webauthn v0.0.0-20220223184316-4d1cf2d34051
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/editorconfig/editorconfig-core-go/v2 v2.4.2 github.com/editorconfig/editorconfig-core-go/v2 v2.4.3
github.com/emirpasic/gods v1.12.0 github.com/emirpasic/gods v1.12.0
github.com/ethantkoenig/rupture v1.0.0 github.com/ethantkoenig/rupture v1.0.1
github.com/gliderlabs/ssh v0.3.3 github.com/gliderlabs/ssh v0.3.3
github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect
github.com/go-chi/chi/v5 v5.0.4 github.com/go-chi/chi/v5 v5.0.7
github.com/go-chi/cors v1.2.0 github.com/go-chi/cors v1.2.0
github.com/go-enry/go-enry/v2 v2.7.1 github.com/go-enry/go-enry/v2 v2.8.0
github.com/go-git/go-billy/v5 v5.3.1 github.com/go-git/go-billy/v5 v5.3.1
github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4
github.com/go-ldap/ldap/v3 v3.3.0 github.com/go-ldap/ldap/v3 v3.4.2
github.com/go-redis/redis/v8 v8.11.0 github.com/go-redis/redis/v8 v8.11.4
github.com/go-sql-driver/mysql v1.6.0 github.com/go-sql-driver/mysql v1.6.0
github.com/go-swagger/go-swagger v0.27.0 github.com/go-swagger/go-swagger v0.29.0
github.com/go-testfixtures/testfixtures/v3 v3.6.1 github.com/go-testfixtures/testfixtures/v3 v3.6.1
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 github.com/goccy/go-json v0.9.5 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v4 v4.2.0 github.com/golang-jwt/jwt/v4 v4.3.0
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-github/v39 v39.2.0 github.com/google/go-github/v39 v39.2.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/gorilla/feeds v1.1.1 github.com/gorilla/feeds v1.1.1
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/sessions v1.2.1 github.com/gorilla/sessions v1.2.1
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/hashicorp/go-version v1.3.1 github.com/hashicorp/go-version v1.4.0
github.com/hashicorp/golang-lru v0.5.4 github.com/hashicorp/golang-lru v0.5.4
github.com/huandu/xstrings v1.3.2 github.com/huandu/xstrings v1.3.2
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/kevinburke/ssh_config v1.1.0 // indirect github.com/kevinburke/ssh_config v1.1.0 // indirect
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4
github.com/klauspost/compress v1.13.1 github.com/klauspost/compress v1.15.0
github.com/klauspost/cpuid/v2 v2.0.9 github.com/klauspost/cpuid/v2 v2.0.11
github.com/klauspost/pgzip v1.2.5 // indirect github.com/lib/pq v1.10.4
github.com/lib/pq v1.10.2
github.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 github.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
github.com/markbates/goth v1.68.0 github.com/markbates/goth v1.69.0
github.com/mattn/go-isatty v0.0.13 github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mattn/go-sqlite3 v1.14.8 github.com/mattn/go-sqlite3 v1.14.12
github.com/mholt/archiver/v3 v3.5.0 github.com/mholt/acmez v1.0.2 // indirect
github.com/microcosm-cc/bluemonday v1.0.16 github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.18
github.com/miekg/dns v1.1.46 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.12 github.com/minio/minio-go/v7 v7.0.23
github.com/minio/sha256-simd v1.0.0 // indirect github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
github.com/msteinert/pam v0.0.0-20201130170657-e61372126161 github.com/msteinert/pam v1.0.0
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/niklasfasching/go-org v1.5.0 github.com/niklasfasching/go-org v1.6.2
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/nwaples/rardecode v1.1.3 // indirect
github.com/oliamb/cutter v0.2.2 github.com/oliamb/cutter v0.2.2
github.com/olivere/elastic/v7 v7.0.25 github.com/olivere/elastic/v7 v7.0.31
github.com/pelletier/go-toml v1.9.0 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pierrec/lz4/v4 v4.1.8 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.3.0 github.com/pquerna/otp v1.3.0
github.com/prometheus/client_golang v1.11.0 github.com/prometheus/client_golang v1.12.1
github.com/quasoft/websspi v1.0.0 github.com/quasoft/websspi v1.1.2
github.com/rs/xid v1.3.0 // indirect github.com/rs/xid v1.3.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 github.com/santhosh-tekuri/jsonschema/v5 v5.0.0
github.com/sergi/go-diff v1.2.0 github.com/sergi/go-diff v1.2.0
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
@ -107,34 +107,34 @@ require (
github.com/tstranex/u2f v1.0.0 github.com/tstranex/u2f v1.0.0
github.com/ulikunitz/xz v0.5.10 // indirect github.com/ulikunitz/xz v0.5.10 // indirect
github.com/unknwon/com v1.0.1 github.com/unknwon/com v1.0.1
github.com/unknwon/i18n v0.0.0-20210321134014-0ebbf2df1c44 github.com/unknwon/i18n v0.0.0-20210904045753-ff3a8617e361
github.com/unknwon/paginater v0.0.0-20200328080006-042474bd0eae github.com/unknwon/paginater v0.0.0-20200328080006-042474bd0eae
github.com/unrolled/render v1.4.0 github.com/unrolled/render v1.4.1
github.com/urfave/cli v1.22.5 github.com/urfave/cli v1.22.5
github.com/xanzy/go-gitlab v0.50.1 github.com/xanzy/go-gitlab v0.58.0
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/yohcop/openid-go v1.0.0 github.com/yohcop/openid-go v1.0.0
github.com/yuin/goldmark v1.4.4 github.com/yuin/goldmark v1.4.8
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
github.com/yuin/goldmark-meta v1.0.0 github.com/yuin/goldmark-meta v1.1.0
go.etcd.io/bbolt v1.3.6 // indirect go.etcd.io/bbolt v1.3.6 // indirect
go.jolheiser.com/hcaptcha v0.0.4 go.jolheiser.com/hcaptcha v0.0.4
go.jolheiser.com/pwn v0.0.3 go.jolheiser.com/pwn v0.0.3
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.19.0 // indirect go.uber.org/zap v1.21.0 // indirect
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 golang.org/x/crypto v0.0.0-20220214200702-86341886e292
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 golang.org/x/net v0.0.0-20220225172249-27dd8689420f
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9
golang.org/x/text v0.3.7 golang.org/x/text v0.3.7
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
golang.org/x/tools v0.1.0 golang.org/x/tools v0.1.9
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/ini.v1 v1.62.0 gopkg.in/ini.v1 v1.66.4
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
mvdan.cc/xurls/v2 v2.2.0 mvdan.cc/xurls/v2 v2.4.0
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
xorm.io/builder v0.3.9 xorm.io/builder v0.3.9
xorm.io/xorm v1.2.5 xorm.io/xorm v1.2.5

974
go.sum

File diff suppressed because it is too large Load diff

View file

@ -468,7 +468,6 @@ func TestAPIRepoTransfer(t *testing.T) {
expectedStatus int expectedStatus int
}{ }{
// Disclaimer for test story: "user1" is an admin, "user2" is normal user and part of in owner team of org "user3" // Disclaimer for test story: "user1" is an admin, "user2" is normal user and part of in owner team of org "user3"
// Transfer to a user with teams in another org should fail // Transfer to a user with teams in another org should fail
{ctxUserID: 1, newOwner: "user3", teams: &[]int64{5}, expectedStatus: http.StatusForbidden}, {ctxUserID: 1, newOwner: "user3", teams: &[]int64{5}, expectedStatus: http.StatusForbidden},
// Transfer to a user with non-existent team IDs should fail // Transfer to a user with non-existent team IDs should fail

View file

@ -178,7 +178,9 @@ func (c *compareDump) assertEquals(repoBefore, repoAfter *repo_model.Repository)
assert.GreaterOrEqual(c.t, len(issues), 1) assert.GreaterOrEqual(c.t, len(issues), 1)
for _, issue := range issues { for _, issue := range issues {
filename := filepath.Join("comments", fmt.Sprintf("%d.yml", issue.Number)) filename := filepath.Join("comments", fmt.Sprintf("%d.yml", issue.Number))
comments, ok := c.assertEqual(filename, []base.Comment{}, compareFields{}).([]*base.Comment) comments, ok := c.assertEqual(filename, []base.Comment{}, compareFields{
"Index": {ignore: true},
}).([]*base.Comment)
assert.True(c.t, ok) assert.True(c.t, ok)
for _, comment := range comments { for _, comment := range comments {
assert.EqualValues(c.t, issue.Number, comment.IssueIndex) assert.EqualValues(c.t, issue.Number, comment.IssueIndex)

View file

@ -0,0 +1 @@
ref: refs/heads/main

View file

@ -0,0 +1,6 @@
[core]
bare = true
repositoryformatversion = 0
filemode = false
symlinks = false
ignorecase = true

View file

@ -51,8 +51,6 @@ func TestSignin(t *testing.T) {
{username: "wrongUsername", password: "password", message: i18n.Tr("en", "form.username_password_incorrect")}, {username: "wrongUsername", password: "password", message: i18n.Tr("en", "form.username_password_incorrect")},
{username: "user15", password: "wrongPassword", message: i18n.Tr("en", "form.username_password_incorrect")}, {username: "user15", password: "wrongPassword", message: i18n.Tr("en", "form.username_password_incorrect")},
{username: "user1@example.com", password: "wrongPassword", message: i18n.Tr("en", "form.username_password_incorrect")}, {username: "user1@example.com", password: "wrongPassword", message: i18n.Tr("en", "form.username_password_incorrect")},
// test for duplicate email
{username: "user2@example.com", password: "password", message: i18n.Tr("en", "form.email_been_used")},
} }
for _, s := range samples { for _, s := range samples {

View file

@ -181,6 +181,11 @@ func (log *TestLogger) Init(config string) error {
return nil return nil
} }
// Content returns the content accumulated in the content provider
func (log *TestLogger) Content() (string, error) {
return "", fmt.Errorf("not supported")
}
// Flush when log should be flushed // Flush when log should be flushed
func (log *TestLogger) Flush() { func (log *TestLogger) Flush() {
} }

View file

@ -121,6 +121,7 @@ func TestExportUserGPGKeys(t *testing.T) {
defer prepareTestEnv(t)() defer prepareTestEnv(t)()
// Export empty key list // Export empty key list
testExportUserGPGKeys(t, "user1", `-----BEGIN PGP PUBLIC KEY BLOCK----- testExportUserGPGKeys(t, "user1", `-----BEGIN PGP PUBLIC KEY BLOCK-----
Note: This user hasn't uploaded any GPG keys.
=twTO =twTO

View file

@ -43,7 +43,7 @@ type WebAuthnCredential struct {
Name string Name string
LowerName string `xorm:"unique(s)"` LowerName string `xorm:"unique(s)"`
UserID int64 `xorm:"INDEX unique(s)"` UserID int64 `xorm:"INDEX unique(s)"`
CredentialID string `xorm:"INDEX"` CredentialID string `xorm:"INDEX VARCHAR(410)"`
PublicKey []byte PublicKey []byte
AttestationType string AttestationType string
AAGUID []byte AAGUID []byte

View file

@ -148,6 +148,17 @@ func DeleteByBean(ctx context.Context, bean interface{}) (int64, error) {
return GetEngine(ctx).Delete(bean) return GetEngine(ctx).Delete(bean)
} }
// DeleteBeans deletes all given beans, beans should contain delete conditions.
func DeleteBeans(ctx context.Context, beans ...interface{}) (err error) {
e := GetEngine(ctx)
for i := range beans {
if _, err = e.Delete(beans[i]); err != nil {
return err
}
}
return nil
}
// CountByBean counts the number of database records according non-empty fields of the bean as conditions. // CountByBean counts the number of database records according non-empty fields of the bean as conditions.
func CountByBean(ctx context.Context, bean interface{}) (int64, error) { func CountByBean(ctx context.Context, bean interface{}) (int64, error) {
return GetEngine(ctx).Count(bean) return GetEngine(ctx).Count(bean)

View file

@ -24,6 +24,7 @@
owner_name: user2 owner_name: user2
lower_name: repo2 lower_name: repo2
name: repo2 name: repo2
is_empty: false
is_archived: false is_archived: false
is_private: true is_private: true
num_issues: 2 num_issues: 2
@ -40,6 +41,7 @@
owner_name: user3 owner_name: user3
lower_name: repo3 lower_name: repo3
name: repo3 name: repo3
is_empty: false
is_private: true is_private: true
num_issues: 1 num_issues: 1
num_closed_issues: 0 num_closed_issues: 0
@ -56,6 +58,7 @@
owner_name: user5 owner_name: user5
lower_name: repo4 lower_name: repo4
name: repo4 name: repo4
is_empty: false
is_private: false is_private: false
num_issues: 0 num_issues: 0
num_closed_issues: 0 num_closed_issues: 0
@ -143,6 +146,7 @@
owner_name: user12 owner_name: user12
lower_name: repo10 lower_name: repo10
name: repo10 name: repo10
is_empty: false
is_private: false is_private: false
num_issues: 0 num_issues: 0
num_closed_issues: 0 num_closed_issues: 0
@ -160,6 +164,7 @@
owner_name: user13 owner_name: user13
lower_name: repo11 lower_name: repo11
name: repo11 name: repo11
is_empty: false
is_private: false is_private: false
num_issues: 0 num_issues: 0
num_closed_issues: 0 num_closed_issues: 0
@ -217,7 +222,8 @@
owner_name: user2 owner_name: user2
lower_name: repo15 lower_name: repo15
name: repo15 name: repo15
is_empty: true is_empty: false
is_private: true
status: 0 status: 0
- -
@ -226,6 +232,7 @@
owner_name: user2 owner_name: user2
lower_name: repo16 lower_name: repo16
name: repo16 name: repo16
is_empty: false
is_private: true is_private: true
num_issues: 0 num_issues: 0
num_closed_issues: 0 num_closed_issues: 0
@ -459,6 +466,8 @@
owner_name: user2 owner_name: user2
lower_name: repo20 lower_name: repo20
name: repo20 name: repo20
is_empty: false
is_private: true
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
num_issues: 0 num_issues: 0
@ -484,6 +493,7 @@
owner_name: user2 owner_name: user2
lower_name: utf8 lower_name: utf8
name: utf8 name: utf8
is_empty: false
is_private: false is_private: false
status: 0 status: 0
@ -519,6 +529,7 @@
owner_name: user2 owner_name: user2
lower_name: commits_search_test lower_name: commits_search_test
name: commits_search_test name: commits_search_test
is_empty: false
is_private: false is_private: false
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
@ -532,6 +543,7 @@
owner_name: user2 owner_name: user2
lower_name: git_hooks_test lower_name: git_hooks_test
name: git_hooks_test name: git_hooks_test
is_empty: false
is_private: false is_private: false
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
@ -545,6 +557,7 @@
owner_name: limited_org owner_name: limited_org
lower_name: public_repo_on_limited_org lower_name: public_repo_on_limited_org
name: public_repo_on_limited_org name: public_repo_on_limited_org
is_empty: false
is_private: false is_private: false
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
@ -558,6 +571,7 @@
owner_name: limited_org owner_name: limited_org
lower_name: private_repo_on_limited_org lower_name: private_repo_on_limited_org
name: private_repo_on_limited_org name: private_repo_on_limited_org
is_empty: false
is_private: true is_private: true
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
@ -571,6 +585,7 @@
owner_name: privated_org owner_name: privated_org
lower_name: public_repo_on_private_org lower_name: public_repo_on_private_org
name: public_repo_on_private_org name: public_repo_on_private_org
is_empty: false
is_private: false is_private: false
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
@ -584,6 +599,7 @@
owner_name: privated_org owner_name: privated_org
lower_name: private_repo_on_private_org lower_name: private_repo_on_private_org
name: private_repo_on_private_org name: private_repo_on_private_org
is_empty: false
is_private: true is_private: true
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
@ -596,6 +612,7 @@
owner_name: user2 owner_name: user2
lower_name: glob lower_name: glob
name: glob name: glob
is_empty: false
is_private: false is_private: false
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
@ -622,6 +639,7 @@
owner_name: user27 owner_name: user27
lower_name: template1 lower_name: template1
name: template1 name: template1
is_empty: false
is_private: false is_private: false
is_template: true is_template: true
num_stars: 0 num_stars: 0
@ -650,6 +668,7 @@
owner_name: org26 owner_name: org26
lower_name: repo_external_tracker lower_name: repo_external_tracker
name: repo_external_tracker name: repo_external_tracker
is_empty: false
is_private: false is_private: false
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
@ -663,6 +682,7 @@
owner_name: org26 owner_name: org26
lower_name: repo_external_tracker_numeric lower_name: repo_external_tracker_numeric
name: repo_external_tracker_numeric name: repo_external_tracker_numeric
is_empty: false
is_private: false is_private: false
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
@ -676,6 +696,7 @@
owner_name: org26 owner_name: org26
lower_name: repo_external_tracker_alpha lower_name: repo_external_tracker_alpha
name: repo_external_tracker_alpha name: repo_external_tracker_alpha
is_empty: false
is_private: false is_private: false
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
@ -690,6 +711,7 @@
owner_name: user27 owner_name: user27
lower_name: repo49 lower_name: repo49
name: repo49 name: repo49
is_empty: false
is_private: false is_private: false
num_stars: 0 num_stars: 0
num_forks: 0 num_forks: 0
@ -736,3 +758,13 @@
num_projects: 0 num_projects: 0
num_closed_projects: 0 num_closed_projects: 0
status: 0 status: 0
-
id: 52
owner_id: 30
owner_name: user30
lower_name: empty
name: empty
is_empty: true
is_private: true
status: 0

View file

@ -522,7 +522,7 @@
is_restricted: true is_restricted: true
avatar: avatar29 avatar: avatar29
avatar_email: user30@example.com avatar_email: user30@example.com
num_repos: 2 num_repos: 3
is_active: true is_active: true
prohibit_login: true prohibit_login: true

View file

@ -13,6 +13,7 @@ import (
"strconv" "strconv"
"strings" "strings"
admin_model "code.gitea.io/gitea/models/admin"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
@ -24,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -1990,6 +1992,118 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *us
return committer.Commit() return committer.Commit()
} }
// DeleteIssue deletes the issue
func DeleteIssue(issue *Issue) error {
ctx, committer, err := db.TxContext()
if err != nil {
return err
}
defer committer.Close()
if err := deleteIssue(ctx, issue); err != nil {
return err
}
return committer.Commit()
}
func deleteInIssue(e db.Engine, issueID int64, beans ...interface{}) error {
for _, bean := range beans {
if _, err := e.In("issue_id", issueID).Delete(bean); err != nil {
return err
}
}
return nil
}
func deleteIssue(ctx context.Context, issue *Issue) error {
e := db.GetEngine(ctx)
if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
return err
}
if issue.IsPull {
if _, err := e.ID(issue.RepoID).Decr("num_pulls").Update(new(repo_model.Repository)); err != nil {
return err
}
if issue.IsClosed {
if _, err := e.ID(issue.RepoID).Decr("num_closed_pulls").Update(new(repo_model.Repository)); err != nil {
return err
}
}
} else {
if _, err := e.ID(issue.RepoID).Decr("num_issues").Update(new(repo_model.Repository)); err != nil {
return err
}
if issue.IsClosed {
if _, err := e.ID(issue.RepoID).Decr("num_closed_issues").Update(new(repo_model.Repository)); err != nil {
return err
}
}
}
// delete actions assigned to this issue
subQuery := builder.Select("`id`").
From("`comment`").
Where(builder.Eq{"`issue_id`": issue.ID})
if _, err := e.In("comment_id", subQuery).Delete(&Action{}); err != nil {
return err
}
if _, err := e.Table("action").Where("repo_id = ?", issue.RepoID).
In("op_type", ActionCreateIssue, ActionCreatePullRequest).
Where("content LIKE ?", strconv.FormatInt(issue.ID, 10)+"|%").
Delete(&Action{}); err != nil {
return err
}
// find attachments related to this issue and remove them
var attachments []*repo_model.Attachment
if err := e.In("issue_id", issue.ID).Find(&attachments); err != nil {
return err
}
for i := range attachments {
admin_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachments[i].RelativePath())
}
// delete all database data still assigned to this issue
if err := deleteInIssue(e, issue.ID,
&issues.ContentHistory{},
&Comment{},
&IssueLabel{},
&IssueDependency{},
&IssueAssignees{},
&IssueUser{},
&Reaction{},
&IssueWatch{},
&Stopwatch{},
&TrackedTime{},
&ProjectIssue{},
&repo_model.Attachment{},
&PullRequest{},
); err != nil {
return err
}
// References to this issue in other issues
if _, err := e.In("ref_issue_id", issue.ID).Delete(&Comment{}); err != nil {
return err
}
// Delete dependencies for issues in other repositories
if _, err := e.In("dependency_id", issue.ID).Delete(&IssueDependency{}); err != nil {
return err
}
// delete from dependent issues
if _, err := e.In("dependent_issue_id", issue.ID).Delete(&Comment{}); err != nil {
return err
}
return nil
}
// DependencyInfo represents high level information about an issue which is a dependency of another issue. // DependencyInfo represents high level information about an issue which is a dependency of another issue.
type DependencyInfo struct { type DependencyInfo struct {
Issue `xorm:"extends"` Issue `xorm:"extends"`

View file

@ -1152,9 +1152,7 @@ func DeleteComment(comment *Comment) error {
} }
func deleteComment(e db.Engine, comment *Comment) error { func deleteComment(e db.Engine, comment *Comment) error {
if _, err := e.Delete(&Comment{ if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil {
ID: comment.ID,
}); err != nil {
return err return err
} }

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
) )
// ErrIssueStopwatchNotExist represents an error that stopwatch is not exist // ErrIssueStopwatchNotExist represents an error that stopwatch is not exist
@ -53,7 +54,7 @@ func (s Stopwatch) Seconds() int64 {
// Duration returns a human-readable duration string based on local server time // Duration returns a human-readable duration string based on local server time
func (s Stopwatch) Duration() string { func (s Stopwatch) Duration() string {
return SecToTime(s.Seconds()) return util.SecToTime(s.Seconds())
} }
func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
@ -164,7 +165,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
Doer: user, Doer: user,
Issue: issue, Issue: issue,
Repo: issue.Repo, Repo: issue.Repo,
Content: SecToTime(timediff), Content: util.SecToTime(timediff),
Type: CommentTypeStopTracking, Type: CommentTypeStopTracking,
TimeID: tt.ID, TimeID: tt.ID,
}); err != nil { }); err != nil {
@ -263,32 +264,3 @@ func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) e
} }
return nil return nil
} }
// SecToTime converts an amount of seconds to a human-readable string (example: 66s -> 1min 6s)
func SecToTime(duration int64) string {
seconds := duration % 60
minutes := (duration / (60)) % 60
hours := duration / (60 * 60)
var hrs string
if hours > 0 {
hrs = fmt.Sprintf("%dh", hours)
}
if minutes > 0 {
if hours == 0 {
hrs = fmt.Sprintf("%dmin", minutes)
} else {
hrs = fmt.Sprintf("%s %dmin", hrs, minutes)
}
}
if seconds > 0 {
if hours == 0 && minutes == 0 {
hrs = fmt.Sprintf("%ds", seconds)
} else {
hrs = fmt.Sprintf("%s %ds", hrs, seconds)
}
}
return hrs
}

View file

@ -397,6 +397,58 @@ func TestIssue_InsertIssue(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestIssue_DeleteIssue(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
issueIDs, err := GetIssueIDsByRepoID(1)
assert.NoError(t, err)
assert.EqualValues(t, 5, len(issueIDs))
issue := &Issue{
RepoID: 1,
ID: issueIDs[2],
}
err = DeleteIssue(issue)
assert.NoError(t, err)
issueIDs, err = GetIssueIDsByRepoID(1)
assert.NoError(t, err)
assert.EqualValues(t, 4, len(issueIDs))
// check attachment removal
attachments, err := repo_model.GetAttachmentsByIssueID(4)
assert.NoError(t, err)
issue, err = GetIssueByID(4)
assert.NoError(t, err)
err = DeleteIssue(issue)
assert.NoError(t, err)
assert.EqualValues(t, 2, len(attachments))
for i := range attachments {
attachment, err := repo_model.GetAttachmentByUUID(attachments[i].UUID)
assert.Error(t, err)
assert.True(t, repo_model.IsErrAttachmentNotExist(err))
assert.Nil(t, attachment)
}
// check issue dependencies
user, err := user_model.GetUserByID(1)
assert.NoError(t, err)
issue1, err := GetIssueByID(1)
assert.NoError(t, err)
issue2, err := GetIssueByID(2)
assert.NoError(t, err)
err = CreateIssueDependency(user, issue1, issue2)
assert.NoError(t, err)
left, err := IssueNoDependenciesLeft(issue1)
assert.NoError(t, err)
assert.False(t, left)
err = DeleteIssue(&Issue{ID: 2})
assert.NoError(t, err)
left, err = IssueNoDependenciesLeft(issue1)
assert.NoError(t, err)
assert.True(t, left)
}
func TestIssue_ResolveMentions(t *testing.T) { func TestIssue_ResolveMentions(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())

View file

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder" "xorm.io/builder"
) )
@ -177,7 +178,7 @@ func AddTime(user *user_model.User, issue *Issue, amount int64, created time.Tim
Issue: issue, Issue: issue,
Repo: issue.Repo, Repo: issue.Repo,
Doer: user, Doer: user,
Content: SecToTime(amount), Content: util.SecToTime(amount),
Type: CommentTypeAddTimeManual, Type: CommentTypeAddTimeManual,
TimeID: t.ID, TimeID: t.ID,
}); err != nil { }); err != nil {
@ -226,7 +227,7 @@ func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string,
} }
return nil, err return nil, err
} }
totalTimes[user] = SecToTime(total) totalTimes[user] = util.SecToTime(total)
} }
return totalTimes, nil return totalTimes, nil
} }
@ -260,7 +261,7 @@ func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error {
Issue: issue, Issue: issue,
Repo: issue.Repo, Repo: issue.Repo,
Doer: user, Doer: user,
Content: "- " + SecToTime(removedTime), Content: "- " + util.SecToTime(removedTime),
Type: CommentTypeDeleteTimeManual, Type: CommentTypeDeleteTimeManual,
}); err != nil { }); err != nil {
return err return err
@ -289,7 +290,7 @@ func DeleteTime(t *TrackedTime) error {
Issue: t.Issue, Issue: t.Issue,
Repo: t.Issue.Repo, Repo: t.Issue.Repo,
Doer: t.User, Doer: t.User,
Content: "- " + SecToTime(t.Time), Content: "- " + util.SecToTime(t.Time),
Type: CommentTypeDeleteTimeManual, Type: CommentTypeDeleteTimeManual,
}); err != nil { }); err != nil {
return err return err

View file

@ -34,7 +34,7 @@ func TestAddTime(t *testing.T) {
assert.Equal(t, int64(3661), tt.Time) assert.Equal(t, int64(3661), tt.Time)
comment := unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*Comment) comment := unittest.AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*Comment)
assert.Equal(t, comment.Content, "1h 1min 1s") assert.Equal(t, comment.Content, "1 hour 1 minute")
} }
func TestGetTrackedTimes(t *testing.T) { func TestGetTrackedTimes(t *testing.T) {
@ -86,7 +86,7 @@ func TestTotalTimes(t *testing.T) {
assert.Len(t, total, 1) assert.Len(t, total, 1)
for user, time := range total { for user, time := range total {
assert.Equal(t, int64(1), user.ID) assert.Equal(t, int64(1), user.ID)
assert.Equal(t, "6min 40s", time) assert.Equal(t, "6 minutes 40 seconds", time)
} }
total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 2}) total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 2})
@ -94,9 +94,9 @@ func TestTotalTimes(t *testing.T) {
assert.Len(t, total, 2) assert.Len(t, total, 2)
for user, time := range total { for user, time := range total {
if user.ID == 2 { if user.ID == 2 {
assert.Equal(t, "1h 1min 2s", time) assert.Equal(t, "1 hour 1 minute", time)
} else if user.ID == 1 { } else if user.ID == 1 {
assert.Equal(t, "20s", time) assert.Equal(t, "20 seconds", time)
} else { } else {
assert.Error(t, assert.AnError) assert.Error(t, assert.AnError)
} }
@ -107,7 +107,7 @@ func TestTotalTimes(t *testing.T) {
assert.Len(t, total, 1) assert.Len(t, total, 1)
for user, time := range total { for user, time := range total {
assert.Equal(t, int64(2), user.ID) assert.Equal(t, int64(2), user.ID)
assert.Equal(t, "1s", time) assert.Equal(t, "1 second", time)
} }
total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 4}) total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 4})

View file

@ -195,7 +195,8 @@ func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *cross
// verifyReferencedIssue will check if the referenced issue exists, and whether the doer has permission to do what // verifyReferencedIssue will check if the referenced issue exists, and whether the doer has permission to do what
func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossReferencesContext, repo *repo_model.Repository, func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossReferencesContext, repo *repo_model.Repository,
ref references.IssueReference) (*Issue, references.XRefAction, error) { ref references.IssueReference,
) (*Issue, references.XRefAction, error) {
refIssue := &Issue{RepoID: repo.ID, Index: ref.Index} refIssue := &Issue{RepoID: repo.ID, Index: ref.Index}
refAction := ref.Action refAction := ref.Action
e := db.GetEngine(stdCtx) e := db.GetEngine(stdCtx)

View file

@ -137,6 +137,7 @@ func QueryIssueContentHistoryEditedCountMap(dbCtx context.Context, issueID int64
type IssueContentListItem struct { type IssueContentListItem struct {
UserID int64 UserID int64
UserName string UserName string
UserFullName string
UserAvatarLink string UserAvatarLink string
HistoryID int64 HistoryID int64
@ -148,7 +149,7 @@ type IssueContentListItem struct {
// FetchIssueContentHistoryList fetch list // FetchIssueContentHistoryList fetch list
func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int64) ([]*IssueContentListItem, error) { func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int64) ([]*IssueContentListItem, error) {
res := make([]*IssueContentListItem, 0) res := make([]*IssueContentListItem, 0)
err := db.GetEngine(dbCtx).Select("u.id as user_id, u.name as user_name,"+ err := db.GetEngine(dbCtx).Select("u.id as user_id, u.name as user_name, u.full_name as user_full_name,"+
"h.id as history_id, h.edited_unix, h.is_first_created, h.is_deleted"). "h.id as history_id, h.edited_unix, h.is_first_created, h.is_deleted").
Table([]string{"issue_content_history", "h"}). Table([]string{"issue_content_history", "h"}).
Join("LEFT", []string{"user", "u"}, "h.poster_id = u.id"). Join("LEFT", []string{"user", "u"}, "h.poster_id = u.id").

View file

@ -43,8 +43,9 @@ func TestContentHistory(t *testing.T) {
when the refactor of models are done, this test will be possible to be run then with a real `User` model. when the refactor of models are done, this test will be possible to be run then with a real `User` model.
*/ */
type User struct { type User struct {
ID int64 ID int64
Name string Name string
FullName string
} }
_ = dbEngine.Sync2(&User{}) _ = dbEngine.Sync2(&User{})

View file

@ -0,0 +1,12 @@
-
id: 1
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
-
id: 2
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
-
id: 3
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
-
id: 4
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="

View file

@ -0,0 +1,21 @@
-
id: 1
name: "u2fkey-correctly-migrated"
user_id: 1
raw: 0x05040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf240efe2e213b889daf3fc88e3952e8dd6b4cfd82f1a1212e2ab4b19389455ecf3e67f0aeafc91b9c0d413c9d6215a45177c1d5076358aa6ee20e1b30e3d7467cae2308202bd308201a5a00302010202041e8f8734300d06092a864886f70d01010b0500302e312c302a0603550403132359756269636f2055324620526f6f742043412053657269616c203435373230303633313020170d3134303830313030303030305a180f32303530303930343030303030305a306e310b300906035504061302534531123010060355040a0c0959756269636f20414231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3127302506035504030c1e59756269636f205532462045452053657269616c203531323732323734303059301306072a8648ce3d020106082a8648ce3d03010703420004a879f82338ed1494bac0704bcc7fc663d1b271715976243101c7605115d7c1529e281c1c67322d384b5cd55dd3e9818d5fd85c22af326e0c64fc20afe33f2366a36c306a302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e373013060b2b0601040182e51c0201010404030204303021060b2b0601040182e51c010104041204102fc0579f811347eab116bb5a8db9202a300c0603551d130101ff04023000300d06092a864886f70d01010b050003820101008693ff62df0d5779d4748d7fc8d10227318a8e580e6a3a57c108e94e03c38568b366894fce5624be4a3efd7f34118b3d993743f792a1989160c8fc9ae0b04e3df9ee15e3e88c04fc82a8dcbf5818e108dcc2968577ae79ff662b94734e3dec4597305d73e6e55ee2beb9cd9678ca0935e533eb638f8e26fabb817cda441fbe9831832ae5f6e2ad992f9ebbdb4c62238b8f8d7ab481d6d3263bcdbf9e4a57550370988ad5813440fa032cadb6723cadd8f8d7ba809f75b43cffa0a5b9add14232ef9d9e14812638233c4ca4a873b9f8ac98e32ba19167606e15909fcddb4a2dffbdae4620249f9a6646ac81e4832d1119febfaa731a882da25a77827d46d190173046022100b579338a44c236d3f214b2e150011a08cf251193ecfae2244edb0a5794e9b301022100fab468862c47d98204d437cf2be8c54a5a4ecd1ebb1c61a6c23da7b9c75f6841
counter: 0
- id: 2
name: "u2fkey-incorrectly-migrated"
user_id: 1
raw: 0x05040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf240efe2e213b889daf3fc88e3952e8dd6b4cfd82f1a1212e2ab4b19389455ecf3e67f0aeafc91b9c0d413c9d6215a45177c1d5076358aa6ee20e1b30e3d7467cae2308202bd308201a5a00302010202041e8f8734300d06092a864886f70d01010b0500302e312c302a0603550403132359756269636f2055324620526f6f742043412053657269616c203435373230303633313020170d3134303830313030303030305a180f32303530303930343030303030305a306e310b300906035504061302534531123010060355040a0c0959756269636f20414231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3127302506035504030c1e59756269636f205532462045452053657269616c203531323732323734303059301306072a8648ce3d020106082a8648ce3d03010703420004a879f82338ed1494bac0704bcc7fc663d1b271715976243101c7605115d7c1529e281c1c67322d384b5cd55dd3e9818d5fd85c22af326e0c64fc20afe33f2366a36c306a302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e373013060b2b0601040182e51c0201010404030204303021060b2b0601040182e51c010104041204102fc0579f811347eab116bb5a8db9202a300c0603551d130101ff04023000300d06092a864886f70d01010b050003820101008693ff62df0d5779d4748d7fc8d10227318a8e580e6a3a57c108e94e03c38568b366894fce5624be4a3efd7f34118b3d993743f792a1989160c8fc9ae0b04e3df9ee15e3e88c04fc82a8dcbf5818e108dcc2968577ae79ff662b94734e3dec4597305d73e6e55ee2beb9cd9678ca0935e533eb638f8e26fabb817cda441fbe9831832ae5f6e2ad992f9ebbdb4c62238b8f8d7ab481d6d3263bcdbf9e4a57550370988ad5813440fa032cadb6723cadd8f8d7ba809f75b43cffa0a5b9add14232ef9d9e14812638233c4ca4a873b9f8ac98e32ba19167606e15909fcddb4a2dffbdae4620249f9a6646ac81e4832d1119febfaa731a882da25a77827d46d190173046022100b579338a44c236d3f214b2e150011a08cf251193ecfae2244edb0a5794e9b301022100fab468862c47d98204d437cf2be8c54a5a4ecd1ebb1c61a6c23da7b9c75f6841
counter: 0
- id: 3
name: "u2fkey-deleted"
user_id: 1
raw: 0x05040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf240efe2e213b889daf3fc88e3952e8dd6b4cfd82f1a1212e2ab4b19389455ecf3e67f0aeafc91b9c0d413c9d6215a45177c1d5076358aa6ee20e1b30e3d7467cae2308202bd308201a5a00302010202041e8f8734300d06092a864886f70d01010b0500302e312c302a0603550403132359756269636f2055324620526f6f742043412053657269616c203435373230303633313020170d3134303830313030303030305a180f32303530303930343030303030305a306e310b300906035504061302534531123010060355040a0c0959756269636f20414231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3127302506035504030c1e59756269636f205532462045452053657269616c203531323732323734303059301306072a8648ce3d020106082a8648ce3d03010703420004a879f82338ed1494bac0704bcc7fc663d1b271715976243101c7605115d7c1529e281c1c67322d384b5cd55dd3e9818d5fd85c22af326e0c64fc20afe33f2366a36c306a302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e373013060b2b0601040182e51c0201010404030204303021060b2b0601040182e51c010104041204102fc0579f811347eab116bb5a8db9202a300c0603551d130101ff04023000300d06092a864886f70d01010b050003820101008693ff62df0d5779d4748d7fc8d10227318a8e580e6a3a57c108e94e03c38568b366894fce5624be4a3efd7f34118b3d993743f792a1989160c8fc9ae0b04e3df9ee15e3e88c04fc82a8dcbf5818e108dcc2968577ae79ff662b94734e3dec4597305d73e6e55ee2beb9cd9678ca0935e533eb638f8e26fabb817cda441fbe9831832ae5f6e2ad992f9ebbdb4c62238b8f8d7ab481d6d3263bcdbf9e4a57550370988ad5813440fa032cadb6723cadd8f8d7ba809f75b43cffa0a5b9add14232ef9d9e14812638233c4ca4a873b9f8ac98e32ba19167606e15909fcddb4a2dffbdae4620249f9a6646ac81e4832d1119febfaa731a882da25a77827d46d190173046022100b579338a44c236d3f214b2e150011a08cf251193ecfae2244edb0a5794e9b301022100fab468862c47d98204d437cf2be8c54a5a4ecd1ebb1c61a6c23da7b9c75f6841
counter: 0
- id: 4
name: "u2fkey-wrong-user-id"
user_id: 2
raw: 0x05040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf240efe2e213b889daf3fc88e3952e8dd6b4cfd82f1a1212e2ab4b19389455ecf3e67f0aeafc91b9c0d413c9d6215a45177c1d5076358aa6ee20e1b30e3d7467cae2308202bd308201a5a00302010202041e8f8734300d06092a864886f70d01010b0500302e312c302a0603550403132359756269636f2055324620526f6f742043412053657269616c203435373230303633313020170d3134303830313030303030305a180f32303530303930343030303030305a306e310b300906035504061302534531123010060355040a0c0959756269636f20414231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3127302506035504030c1e59756269636f205532462045452053657269616c203531323732323734303059301306072a8648ce3d020106082a8648ce3d03010703420004a879f82338ed1494bac0704bcc7fc663d1b271715976243101c7605115d7c1529e281c1c67322d384b5cd55dd3e9818d5fd85c22af326e0c64fc20afe33f2366a36c306a302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e373013060b2b0601040182e51c0201010404030204303021060b2b0601040182e51c010104041204102fc0579f811347eab116bb5a8db9202a300c0603551d130101ff04023000300d06092a864886f70d01010b050003820101008693ff62df0d5779d4748d7fc8d10227318a8e580e6a3a57c108e94e03c38568b366894fce5624be4a3efd7f34118b3d993743f792a1989160c8fc9ae0b04e3df9ee15e3e88c04fc82a8dcbf5818e108dcc2968577ae79ff662b94734e3dec4597305d73e6e55ee2beb9cd9678ca0935e533eb638f8e26fabb817cda441fbe9831832ae5f6e2ad992f9ebbdb4c62238b8f8d7ab481d6d3263bcdbf9e4a57550370988ad5813440fa032cadb6723cadd8f8d7ba809f75b43cffa0a5b9add14232ef9d9e14812638233c4ca4a873b9f8ac98e32ba19167606e15909fcddb4a2dffbdae4620249f9a6646ac81e4832d1119febfaa731a882da25a77827d46d190173046022100b579338a44c236d3f214b2e150011a08cf251193ecfae2244edb0a5794e9b301022100fab468862c47d98204d437cf2be8c54a5a4ecd1ebb1c61a6c23da7b9c75f6841
counter: 0

View file

@ -0,0 +1,30 @@
-
id: 1
lower_name: "u2fkey-correctly-migrated"
name: "u2fkey-correctly-migrated"
user_id: 1
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
public_key: 0x040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf2
attestation_type: 'fido-u2f'
sign_count: 1
clone_warning: false
-
id: 2
lower_name: "u2fkey-incorrectly-migrated"
name: "u2fkey-incorrectly-migrated"
user_id: 1
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8A"
public_key: 0x040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf2
attestation_type: 'fido-u2f'
sign_count: 1
clone_warning: false
-
id: 4
lower_name: "u2fkey-wrong-user-id"
name: "u2fkey-wrong-user-id"
user_id: 1
credential_id: "THIS SHOULD CHANGE"
public_key: 0x040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf2
attestation_type: 'fido-u2f'
sign_count: 1
clone_warning: false

View file

@ -61,7 +61,6 @@ type Version struct {
// update minDBVersion accordingly // update minDBVersion accordingly
var migrations = []Migration{ var migrations = []Migration{
// Gitea 1.5.0 ends at v69 // Gitea 1.5.0 ends at v69
// v70 -> v71 // v70 -> v71
NewMigration("add issue_dependencies", addIssueDependencies), NewMigration("add issue_dependencies", addIssueDependencies),
// v71 -> v72 // v71 -> v72
@ -367,9 +366,13 @@ var migrations = []Migration{
// v206 -> v207 // v206 -> v207
NewMigration("Add authorize column to team_unit table", addAuthorizeColForTeamUnit), NewMigration("Add authorize column to team_unit table", addAuthorizeColForTeamUnit),
// v207 -> v208 // v207 -> v208
NewMigration("Add webauthn table and migrate u2f data to webauthn", addWebAuthnCred), NewMigration("Add webauthn table and migrate u2f data to webauthn - NO-OPED", addWebAuthnCred),
// v208 -> v209 // v208 -> v209
NewMigration("Use base32.HexEncoding instead of base64 encoding for cred ID as it is case insensitive", useBase32HexForCredIDInWebAuthnCredential), NewMigration("Use base32.HexEncoding instead of base64 encoding for cred ID as it is case insensitive - NO-OPED", useBase32HexForCredIDInWebAuthnCredential),
// v209 -> v210
NewMigration("Increase WebAuthentication CredentialID size to 410 - NO-OPED", increaseCredentialIDTo410),
// v210 -> v211
NewMigration("v208 was completely broken - remigrate", remigrateU2FCredentials),
} }
// GetCurrentDBVersion returns the current db version // GetCurrentDBVersion returns the current db version

View file

@ -166,6 +166,11 @@ func (log *TestLogger) Init(config string) error {
return nil return nil
} }
// Content returns the content accumulated in the content provider
func (log *TestLogger) Content() (string, error) {
return "", fmt.Errorf("not supported")
}
// Flush when log should be flushed // Flush when log should be flushed
func (log *TestLogger) Flush() { func (log *TestLogger) Flush() {
} }

View file

@ -5,86 +5,11 @@
package migrations package migrations
import ( import (
"crypto/elliptic"
"encoding/base64"
"strings"
"code.gitea.io/gitea/modules/timeutil"
"github.com/tstranex/u2f"
"xorm.io/xorm" "xorm.io/xorm"
) )
func addWebAuthnCred(x *xorm.Engine) error { func addWebAuthnCred(x *xorm.Engine) error {
// Create webauthnCredential table // NO-OP Don't migrate here - let v210 do this.
type webauthnCredential struct {
ID int64 `xorm:"pk autoincr"`
Name string
LowerName string `xorm:"unique(s)"`
UserID int64 `xorm:"INDEX unique(s)"`
CredentialID string `xorm:"INDEX"`
PublicKey []byte
AttestationType string
AAGUID []byte
SignCount uint32 `xorm:"BIGINT"`
CloneWarning bool
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
if err := x.Sync2(&webauthnCredential{}); err != nil {
return err
}
// Now migrate the old u2f registrations to the new format
type u2fRegistration struct {
ID int64 `xorm:"pk autoincr"`
Name string
UserID int64 `xorm:"INDEX"`
Raw []byte
Counter uint32 `xorm:"BIGINT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
var start int
regs := make([]*u2fRegistration, 0, 50)
for {
err := x.OrderBy("id").Limit(50, start).Find(&regs)
if err != nil {
return err
}
for _, reg := range regs {
parsed := new(u2f.Registration)
err = parsed.UnmarshalBinary(reg.Raw)
if err != nil {
continue
}
c := &webauthnCredential{
ID: reg.ID,
Name: reg.Name,
LowerName: strings.ToLower(reg.Name),
UserID: reg.UserID,
CredentialID: base64.RawStdEncoding.EncodeToString(parsed.KeyHandle),
PublicKey: elliptic.Marshal(elliptic.P256(), parsed.PubKey.X, parsed.PubKey.Y),
AttestationType: "fido-u2f",
AAGUID: []byte{},
SignCount: reg.Counter,
}
_, err := x.Insert(c)
if err != nil {
return err
}
}
if len(regs) < 50 {
break
}
start += 50
regs = regs[:0]
}
return nil return nil
} }

View file

@ -5,46 +5,10 @@
package migrations package migrations
import ( import (
"encoding/base32"
"encoding/base64"
"xorm.io/xorm" "xorm.io/xorm"
) )
func useBase32HexForCredIDInWebAuthnCredential(x *xorm.Engine) error { func useBase32HexForCredIDInWebAuthnCredential(x *xorm.Engine) error {
// Create webauthnCredential table // noop
type webauthnCredential struct {
ID int64 `xorm:"pk autoincr"`
CredentialID string `xorm:"INDEX"`
}
if err := x.Sync2(&webauthnCredential{}); err != nil {
return err
}
var start int
regs := make([]*webauthnCredential, 0, 50)
for {
err := x.OrderBy("id").Limit(50, start).Find(&regs)
if err != nil {
return err
}
for _, reg := range regs {
credID, _ := base64.RawStdEncoding.DecodeString(reg.CredentialID)
reg.CredentialID = base32.HexEncoding.EncodeToString(credID)
_, err := x.Update(reg)
if err != nil {
return err
}
}
if len(regs) < 50 {
break
}
start += 50
regs = regs[:0]
}
return nil return nil
} }

17
models/migrations/v209.go Normal file
View file

@ -0,0 +1,17 @@
// Copyright 2022 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 increaseCredentialIDTo410(x *xorm.Engine) error {
// no-op
// v208 was completely wrong
// So now we have to no-op again.
return nil
}

178
models/migrations/v210.go Normal file
View file

@ -0,0 +1,178 @@
// Copyright 2022 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 (
"crypto/elliptic"
"encoding/base32"
"fmt"
"strings"
"code.gitea.io/gitea/modules/timeutil"
"github.com/tstranex/u2f"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
// v208 migration was completely broken
func remigrateU2FCredentials(x *xorm.Engine) error {
// Create webauthnCredential table
type webauthnCredential struct {
ID int64 `xorm:"pk autoincr"`
Name string
LowerName string `xorm:"unique(s)"`
UserID int64 `xorm:"INDEX unique(s)"`
CredentialID string `xorm:"INDEX VARCHAR(410)"` // CredentalID in U2F is at most 255bytes / 5 * 8 = 408 - add a few extra characters for safety
PublicKey []byte
AttestationType string
AAGUID []byte
SignCount uint32 `xorm:"BIGINT"`
CloneWarning bool
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
if err := x.Sync2(&webauthnCredential{}); err != nil {
return err
}
switch x.Dialect().URI().DBType {
case schemas.MYSQL:
_, err := x.Exec("ALTER TABLE webauthn_credential MODIFY COLUMN credential_id VARCHAR(410)")
if err != nil {
return err
}
case schemas.ORACLE:
_, err := x.Exec("ALTER TABLE webauthn_credential MODIFY credential_id VARCHAR(410)")
if err != nil {
return err
}
case schemas.MSSQL:
// This column has an index on it. I could write all of the code to attempt to change the index OR
// I could just use recreate table.
sess := x.NewSession()
if err := sess.Begin(); err != nil {
_ = sess.Close()
return err
}
if err := recreateTable(sess, new(webauthnCredential)); err != nil {
_ = sess.Close()
return err
}
if err := sess.Commit(); err != nil {
_ = sess.Close()
return err
}
if err := sess.Close(); err != nil {
return err
}
case schemas.POSTGRES:
_, err := x.Exec("ALTER TABLE webauthn_credential ALTER COLUMN credential_id TYPE VARCHAR(410)")
if err != nil {
return err
}
default:
// SQLite doesn't support ALTER COLUMN, and it already makes String _TEXT_ by default so no migration needed
// nor is there any need to re-migrate
}
exist, err := x.IsTableExist("u2f_registration")
if err != nil {
return err
}
if !exist {
return nil
}
// Now migrate the old u2f registrations to the new format
type u2fRegistration struct {
ID int64 `xorm:"pk autoincr"`
Name string
UserID int64 `xorm:"INDEX"`
Raw []byte
Counter uint32 `xorm:"BIGINT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
var start int
regs := make([]*u2fRegistration, 0, 50)
for {
err := x.OrderBy("id").Limit(50, start).Find(&regs)
if err != nil {
return err
}
err = func() error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return fmt.Errorf("unable to allow start session. Error: %w", err)
}
if x.Dialect().URI().DBType == schemas.MSSQL {
if _, err := sess.Exec("SET IDENTITY_INSERT `webauthn_credential` ON"); err != nil {
return fmt.Errorf("unable to allow identity insert on webauthn_credential. Error: %w", err)
}
}
for _, reg := range regs {
parsed := new(u2f.Registration)
err = parsed.UnmarshalBinary(reg.Raw)
if err != nil {
continue
}
remigrated := &webauthnCredential{
ID: reg.ID,
Name: reg.Name,
LowerName: strings.ToLower(reg.Name),
UserID: reg.UserID,
CredentialID: base32.HexEncoding.EncodeToString(parsed.KeyHandle),
PublicKey: elliptic.Marshal(elliptic.P256(), parsed.PubKey.X, parsed.PubKey.Y),
AttestationType: "fido-u2f",
AAGUID: []byte{},
SignCount: reg.Counter,
UpdatedUnix: reg.UpdatedUnix,
CreatedUnix: reg.CreatedUnix,
}
has, err := sess.ID(reg.ID).Get(new(webauthnCredential))
if err != nil {
return fmt.Errorf("unable to get webauthn_credential[%d]. Error: %w", reg.ID, err)
}
if !has {
has, err := sess.Where("`lower_name`=?", remigrated.LowerName).And("`user_id`=?", remigrated.UserID).Exist(new(webauthnCredential))
if err != nil {
return fmt.Errorf("unable to check webauthn_credential[lower_name: %s, user_id:%v]. Error: %w", remigrated.LowerName, remigrated.UserID, err)
}
if !has {
_, err = sess.Insert(remigrated)
if err != nil {
return fmt.Errorf("unable to (re)insert webauthn_credential[%d]. Error: %w", reg.ID, err)
}
continue
}
}
_, err = sess.ID(remigrated.ID).AllCols().Update(remigrated)
if err != nil {
return fmt.Errorf("unable to update webauthn_credential[%d]. Error: %w", reg.ID, err)
}
}
return sess.Commit()
}()
if err != nil {
return err
}
if len(regs) < 50 {
break
}
start += 50
regs = regs[:0]
}
return nil
}

View file

@ -0,0 +1,75 @@
// 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 (
"testing"
"code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
"xorm.io/xorm/schemas"
)
func Test_remigrateU2FCredentials(t *testing.T) {
// Create webauthnCredential table
type WebauthnCredential struct {
ID int64 `xorm:"pk autoincr"`
Name string
LowerName string `xorm:"unique(s)"`
UserID int64 `xorm:"INDEX unique(s)"`
CredentialID string `xorm:"INDEX VARCHAR(410)"` // CredentalID in U2F is at most 255bytes / 5 * 8 = 408 - add a few extra characters for safety
PublicKey []byte
AttestationType string
SignCount uint32 `xorm:"BIGINT"`
CloneWarning bool
}
// Now migrate the old u2f registrations to the new format
type U2fRegistration struct {
ID int64 `xorm:"pk autoincr"`
Name string
UserID int64 `xorm:"INDEX"`
Raw []byte
Counter uint32 `xorm:"BIGINT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
type ExpectedWebauthnCredential struct {
ID int64 `xorm:"pk autoincr"`
CredentialID string `xorm:"INDEX VARCHAR(410)"` // CredentalID in U2F is at most 255bytes / 5 * 8 = 408 - add a few extra characters for safety
}
// Prepare and load the testing database
x, deferable := prepareTestEnv(t, 0, new(WebauthnCredential), new(U2fRegistration), new(ExpectedWebauthnCredential))
if x == nil || t.Failed() {
defer deferable()
return
}
defer deferable()
if x.Dialect().URI().DBType == schemas.SQLITE {
return
}
// Run the migration
if err := remigrateU2FCredentials(x); err != nil {
assert.NoError(t, err)
return
}
expected := []ExpectedWebauthnCredential{}
if err := x.Table("expected_webauthn_credential").Asc("id").Find(&expected); !assert.NoError(t, err) {
return
}
got := []ExpectedWebauthnCredential{}
if err := x.Table("webauthn_credential").Select("id, credential_id").Asc("id").Find(&got); !assert.NoError(t, err) {
return
}
assert.EqualValues(t, expected, got)
}

View file

@ -498,14 +498,15 @@ func (n *Notification) APIURL() string {
type NotificationList []*Notification type NotificationList []*Notification
// LoadAttributes load Repo Issue User and Comment if not loaded // LoadAttributes load Repo Issue User and Comment if not loaded
func (nl NotificationList) LoadAttributes() (err error) { func (nl NotificationList) LoadAttributes() error {
var err error
for i := 0; i < len(nl); i++ { for i := 0; i < len(nl); i++ {
err = nl[i].LoadAttributes() err = nl[i].LoadAttributes()
if err != nil && !IsErrCommentNotExist(err) { if err != nil && !IsErrCommentNotExist(err) {
return return err
} }
} }
return return nil
} }
func (nl NotificationList) getPendingRepoIDs() []int64 { func (nl NotificationList) getPendingRepoIDs() []int64 {

View file

@ -331,9 +331,7 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
return fmt.Errorf("%s is a user not an organization", org.Name) return fmt.Errorf("%s is a user not an organization", org.Name)
} }
e := db.GetEngine(ctx) if err := db.DeleteBeans(ctx,
if err := deleteBeans(e,
&Team{OrgID: org.ID}, &Team{OrgID: org.ID},
&OrgUser{OrgID: org.ID}, &OrgUser{OrgID: org.ID},
&TeamUser{OrgID: org.ID}, &TeamUser{OrgID: org.ID},
@ -342,7 +340,7 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
return fmt.Errorf("deleteBeans: %v", err) return fmt.Errorf("deleteBeans: %v", err)
} }
if _, err := e.ID(org.ID).Delete(new(user_model.User)); err != nil { if _, err := db.GetEngine(ctx).ID(org.ID).Delete(new(user_model.User)); err != nil {
return fmt.Errorf("Delete: %v", err) return fmt.Errorf("Delete: %v", err)
} }

View file

@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"path/filepath"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -123,7 +122,8 @@ func NewRepoContext() {
loadRepoConfig() loadRepoConfig()
unit.LoadUnitConfig() unit.LoadUnitConfig()
admin_model.RemoveAllWithNotice(db.DefaultContext, "Clean up repository temporary data", filepath.Join(setting.AppDataPath, "tmp")) admin_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repository uploads", setting.Repository.Upload.TempPath)
admin_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repositories", LocalCopyPath())
} }
// CheckRepoUnitUser check whether user could visit the unit of this repository // CheckRepoUnitUser check whether user could visit the unit of this repository
@ -150,27 +150,56 @@ func getRepoAssignees(ctx context.Context, repo *repo_model.Repository) (_ []*us
} }
e := db.GetEngine(ctx) e := db.GetEngine(ctx)
accesses := make([]*Access, 0, 10) userIDs := make([]int64, 0, 10)
if err = e. if err = e.Table("access").
Where("repo_id = ? AND mode >= ?", repo.ID, perm.AccessModeWrite). Where("repo_id = ? AND mode >= ?", repo.ID, perm.AccessModeWrite).
Find(&accesses); err != nil { Select("user_id").
Find(&userIDs); err != nil {
return nil, err return nil, err
} }
additionalUserIDs := make([]int64, 0, 10)
if err = e.Table("team_user").
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode >= ?", repo.ID, perm.AccessModeWrite).
Distinct("`team_user`.uid").
Select("`team_user`.uid").
Find(&additionalUserIDs); err != nil {
return nil, err
}
uidMap := map[int64]bool{}
i := 0
for _, uid := range userIDs {
if uidMap[uid] {
continue
}
uidMap[uid] = true
userIDs[i] = uid
i++
}
userIDs = userIDs[:i]
userIDs = append(userIDs, additionalUserIDs...)
for _, uid := range additionalUserIDs {
if uidMap[uid] {
continue
}
userIDs[i] = uid
i++
}
userIDs = userIDs[:i]
// Leave a seat for owner itself to append later, but if owner is an organization // Leave a seat for owner itself to append later, but if owner is an organization
// and just waste 1 unit is cheaper than re-allocate memory once. // and just waste 1 unit is cheaper than re-allocate memory once.
users := make([]*user_model.User, 0, len(accesses)+1) users := make([]*user_model.User, 0, len(userIDs)+1)
if len(accesses) > 0 { if len(userIDs) > 0 {
userIDs := make([]int64, len(accesses))
for i := 0; i < len(accesses); i++ {
userIDs[i] = accesses[i].UserID
}
if err = e.In("id", userIDs).Find(&users); err != nil { if err = e.In("id", userIDs).Find(&users); err != nil {
return nil, err return nil, err
} }
} }
if !repo.Owner.IsOrganization() { if !repo.Owner.IsOrganization() && !uidMap[repo.OwnerID] {
users = append(users, repo.Owner) users = append(users, repo.Owner)
} }
@ -492,7 +521,7 @@ func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_
units = append(units, repo_model.RepoUnit{ units = append(units, repo_model.RepoUnit{
RepoID: repo.ID, RepoID: repo.ID,
Type: tp, Type: tp,
Config: &repo_model.PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, DefaultMergeStyle: repo_model.MergeStyleMerge}, Config: &repo_model.PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowRebaseMerge: true, AllowSquash: true, DefaultMergeStyle: repo_model.MergeStyleMerge, AllowRebaseUpdate: true},
}) })
} else { } else {
units = append(units, repo_model.RepoUnit{ units = append(units, repo_model.RepoUnit{
@ -765,7 +794,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
return err return err
} }
if err := deleteBeans(sess, if err := db.DeleteBeans(ctx,
&Access{RepoID: repo.ID}, &Access{RepoID: repo.ID},
&Action{RepoID: repo.ID}, &Action{RepoID: repo.ID},
&Collaboration{RepoID: repoID}, &Collaboration{RepoID: repoID},
@ -927,28 +956,28 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
} }
// Remove archives // Remove archives
for i := range archivePaths { for _, archive := range archivePaths {
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archivePaths[i]) admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archive)
} }
// Remove lfs objects // Remove lfs objects
for i := range lfsPaths { for _, lfsObj := range lfsPaths {
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsPaths[i]) admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsObj)
} }
// Remove issue attachment files. // Remove issue attachment files.
for i := range attachmentPaths { for _, attachment := range attachmentPaths {
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachmentPaths[i]) admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachment)
} }
// Remove release attachment files. // Remove release attachment files.
for i := range releaseAttachments { for _, releaseAttachment := range releaseAttachments {
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachments[i]) admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachment)
} }
// Remove attachment with no issue_id and release_id. // Remove attachment with no issue_id and release_id.
for i := range newAttachmentPaths { for _, newAttachment := range newAttachmentPaths {
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachmentPaths[i]) admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", newAttachment)
} }
if len(repo.Avatar) > 0 { if len(repo.Avatar) > 0 {

View file

@ -120,11 +120,12 @@ func DeleteMirrorByRepoID(repoID int64) error {
} }
// MirrorsIterate iterates all mirror repositories. // MirrorsIterate iterates all mirror repositories.
func MirrorsIterate(f func(idx int, bean interface{}) error) error { func MirrorsIterate(limit int, f func(idx int, bean interface{}) error) error {
return db.GetEngine(db.DefaultContext). return db.GetEngine(db.DefaultContext).
Where("next_update_unix<=?", time.Now().Unix()). Where("next_update_unix<=?", time.Now().Unix()).
And("next_update_unix!=0"). And("next_update_unix!=0").
OrderBy("updated_unix ASC"). OrderBy("updated_unix ASC").
Limit(limit).
Iterate(new(Mirror), f) Iterate(new(Mirror), f)
} }

View file

@ -101,10 +101,11 @@ func GetPushMirrorsByRepoID(repoID int64) ([]*PushMirror, error) {
} }
// PushMirrorsIterate iterates all push-mirror repositories. // PushMirrorsIterate iterates all push-mirror repositories.
func PushMirrorsIterate(f func(idx int, bean interface{}) error) error { func PushMirrorsIterate(limit int, f func(idx int, bean interface{}) error) error {
return db.GetEngine(db.DefaultContext). return db.GetEngine(db.DefaultContext).
Where("last_update + (`interval` / ?) <= ?", time.Second, time.Now().Unix()). Where("last_update + (`interval` / ?) <= ?", time.Second, time.Now().Unix()).
And("`interval` != 0"). And("`interval` != 0").
OrderBy("last_update ASC"). OrderBy("last_update ASC").
Limit(limit).
Iterate(new(PushMirror), f) Iterate(new(PushMirror), f)
} }

View file

@ -40,7 +40,7 @@ func TestPushMirrorsIterate(t *testing.T) {
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
PushMirrorsIterate(func(idx int, bean interface{}) error { PushMirrorsIterate(1, func(idx int, bean interface{}) error {
m, ok := bean.(*PushMirror) m, ok := bean.(*PushMirror)
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, "test-1", m.RemoteName) assert.Equal(t, "test-1", m.RemoteName)

View file

@ -290,7 +290,14 @@ func (repo *Repository) LoadUnits(ctx context.Context) (err error) {
} }
repo.Units, err = getUnitsByRepoID(db.GetEngine(ctx), repo.ID) repo.Units, err = getUnitsByRepoID(db.GetEngine(ctx), repo.ID)
log.Trace("repo.Units: %-+v", repo.Units) if log.IsTrace() {
unitTypeStrings := make([]string, len(repo.Units))
for i, unit := range repo.Units {
unitTypeStrings[i] = unit.Type.String()
}
log.Trace("repo.Units, ID=%d, Types: [%s]", repo.ID, strings.Join(unitTypeStrings, ", "))
}
return err return err
} }
@ -513,7 +520,7 @@ func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{ desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
URLPrefix: repo.HTMLURL(), URLPrefix: repo.HTMLURL(),
Metas: repo.ComposeMetas(), // Don't use Metas to speedup requests
}, repo.Description) }, repo.Description)
if err != nil { if err != nil {
log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err) log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)

View file

@ -115,12 +115,15 @@ type PullRequestsConfig struct {
AllowSquash bool AllowSquash bool
AllowManualMerge bool AllowManualMerge bool
AutodetectManualMerge bool AutodetectManualMerge bool
AllowRebaseUpdate bool
DefaultDeleteBranchAfterMerge bool DefaultDeleteBranchAfterMerge bool
DefaultMergeStyle MergeStyle DefaultMergeStyle MergeStyle
} }
// FromDB fills up a PullRequestsConfig from serialized format. // FromDB fills up a PullRequestsConfig from serialized format.
func (cfg *PullRequestsConfig) FromDB(bs []byte) error { func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
// AllowRebaseUpdate = true as default for existing PullRequestConfig in DB
cfg.AllowRebaseUpdate = true
return json.UnmarshalHandleDoubleEncode(bs, &cfg) return json.UnmarshalHandleDoubleEncode(bs, &cfg)
} }

View file

@ -167,3 +167,21 @@ func TestLinkedRepository(t *testing.T) {
}) })
} }
} }
func TestRepoAssignees(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
users, err := GetRepoAssignees(repo2)
assert.NoError(t, err)
assert.Len(t, users, 1)
assert.Equal(t, users[0].ID, int64(2))
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21}).(*repo_model.Repository)
users, err = GetRepoAssignees(repo21)
assert.NoError(t, err)
assert.Len(t, users, 3)
assert.Equal(t, users[0].ID, int64(15))
assert.Equal(t, users[1].ID, int64(18))
assert.Equal(t, users[2].ID, int64(16))
}

View file

@ -29,16 +29,6 @@ func GetOrganizationCount(ctx context.Context, u *user_model.User) (int64, error
Count(new(OrgUser)) Count(new(OrgUser))
} }
// deleteBeans deletes all given beans, beans should contain delete conditions.
func deleteBeans(e db.Engine, beans ...interface{}) (err error) {
for i := range beans {
if _, err = e.Delete(beans[i]); err != nil {
return err
}
}
return nil
}
// DeleteUser deletes models associated to an user. // DeleteUser deletes models associated to an user.
func DeleteUser(ctx context.Context, u *user_model.User) (err error) { func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
e := db.GetEngine(ctx) e := db.GetEngine(ctx)
@ -82,7 +72,7 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
} }
// ***** END: Follow ***** // ***** END: Follow *****
if err = deleteBeans(e, if err = db.DeleteBeans(ctx,
&AccessToken{UID: u.ID}, &AccessToken{UID: u.ID},
&Collaboration{UserID: u.ID}, &Collaboration{UserID: u.ID},
&Access{UserID: u.ID}, &Access{UserID: u.ID},

View file

@ -30,13 +30,19 @@ func (users UserList) GetTwoFaStatus() map[int64]bool {
for _, user := range users { for _, user := range users {
results[user.ID] = false // Set default to false results[user.ID] = false // Set default to false
} }
tokenMaps, err := users.loadTwoFactorStatus(db.GetEngine(db.DefaultContext))
if err == nil { if tokenMaps, err := users.loadTwoFactorStatus(db.GetEngine(db.DefaultContext)); err == nil {
for _, token := range tokenMaps { for _, token := range tokenMaps {
results[token.UID] = true results[token.UID] = true
} }
} }
if ids, err := users.userIDsWithWebAuthn(db.GetEngine(db.DefaultContext)); err == nil {
for _, id := range ids {
results[id] = true
}
}
return results return results
} }
@ -47,15 +53,23 @@ func (users UserList) loadTwoFactorStatus(e db.Engine) (map[int64]*auth.TwoFacto
userIDs := users.GetUserIDs() userIDs := users.GetUserIDs()
tokenMaps := make(map[int64]*auth.TwoFactor, len(userIDs)) tokenMaps := make(map[int64]*auth.TwoFactor, len(userIDs))
err := e. if err := e.In("uid", userIDs).Find(&tokenMaps); err != nil {
In("uid", userIDs).
Find(&tokenMaps)
if err != nil {
return nil, fmt.Errorf("find two factor: %v", err) return nil, fmt.Errorf("find two factor: %v", err)
} }
return tokenMaps, nil return tokenMaps, nil
} }
func (users UserList) userIDsWithWebAuthn(e db.Engine) ([]int64, error) {
if len(users) == 0 {
return nil, nil
}
ids := make([]int64, 0, len(users))
if err := e.Table(new(auth.WebAuthnCredential)).In("user_id", users.GetUserIDs()).Select("user_id").Distinct("user_id").Find(&ids); err != nil {
return nil, fmt.Errorf("find two factor: %v", err)
}
return ids, nil
}
// GetUsersByIDs returns all resolved users from a list of Ids. // GetUsersByIDs returns all resolved users from a list of Ids.
func GetUsersByIDs(ids []int64) (UserList, error) { func GetUsersByIDs(ids []int64) (UserList, error) {
ous := make([]*User, 0, len(ids)) ous := make([]*User, 0, len(ids))

View file

@ -20,6 +20,7 @@ import (
// SearchUserOptions contains the options for searching // SearchUserOptions contains the options for searching
type SearchUserOptions struct { type SearchUserOptions struct {
db.ListOptions db.ListOptions
Keyword string Keyword string
Type UserType Type UserType
UID int64 UID int64
@ -33,6 +34,8 @@ type SearchUserOptions struct {
IsRestricted util.OptionalBool IsRestricted util.OptionalBool
IsTwoFactorEnabled util.OptionalBool IsTwoFactorEnabled util.OptionalBool
IsProhibitLogin util.OptionalBool IsProhibitLogin util.OptionalBool
ExtraParamStrings map[string]string
} }
func (opts *SearchUserOptions) toSearchQueryBase() *xorm.Session { func (opts *SearchUserOptions) toSearchQueryBase() *xorm.Session {

View file

@ -827,8 +827,9 @@ func validateUser(u *User) error {
return ValidateEmail(u.Email) return ValidateEmail(u.Email)
} }
func updateUser(ctx context.Context, u *User, changePrimaryEmail bool) error { func updateUser(ctx context.Context, u *User, changePrimaryEmail bool, cols ...string) error {
if err := validateUser(u); err != nil { err := validateUser(u)
if err != nil {
return err return err
} }
@ -860,15 +861,35 @@ func updateUser(ctx context.Context, u *User, changePrimaryEmail bool) error {
}); err != nil { }); err != nil {
return err return err
} }
} else if !u.IsOrganization() { // check if primary email in email_address table
primaryEmailExist, err := e.Where("uid=? AND is_primary=?", u.ID, true).Exist(&EmailAddress{})
if err != nil {
return err
}
if !primaryEmailExist {
if _, err := e.Insert(&EmailAddress{
Email: u.Email,
UID: u.ID,
IsActivated: true,
IsPrimary: true,
}); err != nil {
return err
}
}
} }
_, err := e.ID(u.ID).AllCols().Update(u) if len(cols) == 0 {
_, err = e.ID(u.ID).AllCols().Update(u)
} else {
_, err = e.ID(u.ID).Cols(cols...).Update(u)
}
return err return err
} }
// UpdateUser updates user's information. // UpdateUser updates user's information.
func UpdateUser(u *User, emailChanged bool) error { func UpdateUser(u *User, emailChanged bool, cols ...string) error {
return updateUser(db.DefaultContext, u, emailChanged) return updateUser(db.DefaultContext, u, emailChanged, cols...)
} }
// UpdateUserCols update user according special columns // UpdateUserCols update user according special columns
@ -1117,19 +1138,9 @@ func GetUserByEmailContext(ctx context.Context, email string) (*User, error) {
} }
email = strings.ToLower(email) email = strings.ToLower(email)
// First try to find the user by primary email
user := &User{Email: email}
has, err := db.GetEngine(ctx).Get(user)
if err != nil {
return nil, err
}
if has {
return user, nil
}
// Otherwise, check in alternative list for activated email addresses // Otherwise, check in alternative list for activated email addresses
emailAddress := &EmailAddress{Email: email, IsActivated: true} emailAddress := &EmailAddress{LowerEmail: email, IsActivated: true}
has, err = db.GetEngine(ctx).Get(emailAddress) has, err := db.GetEngine(ctx).Get(emailAddress)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -235,6 +235,20 @@ func TestCreateUserInvalidEmail(t *testing.T) {
assert.True(t, IsErrEmailInvalid(err)) assert.True(t, IsErrEmailInvalid(err))
} }
func TestCreateUserEmailAlreadyUsed(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
// add new user with user2's email
user.Name = "testuser"
user.LowerName = strings.ToLower(user.Name)
user.ID = 0
err := CreateUser(user)
assert.Error(t, err)
assert.True(t, IsErrEmailAlreadyUsed(err))
}
func TestGetUserIDsByNames(t *testing.T) { func TestGetUserIDsByNames(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())

View file

@ -63,6 +63,7 @@ func EscapeControlBytes(text []byte) (EscapeStatus, []byte) {
func EscapeControlReader(text io.Reader, output io.Writer) (escaped EscapeStatus, err error) { func EscapeControlReader(text io.Reader, output io.Writer) (escaped EscapeStatus, err error) {
buf := make([]byte, 4096) buf := make([]byte, 4096)
readStart := 0 readStart := 0
runeCount := 0
var n int var n int
var writePos int var writePos int
@ -74,10 +75,13 @@ readingloop:
for err == nil { for err == nil {
n, err = text.Read(buf[readStart:]) n, err = text.Read(buf[readStart:])
bs := buf[:n+readStart] bs := buf[:n+readStart]
n = len(bs)
i := 0 i := 0
for i < len(bs) { for i < len(bs) {
r, size := utf8.DecodeRune(bs[i:]) r, size := utf8.DecodeRune(bs[i:])
runeCount++
// Now handle the codepoints // Now handle the codepoints
switch { switch {
case r == utf8.RuneError: case r == utf8.RuneError:
@ -112,6 +116,8 @@ readingloop:
lineHasRTLScript = false lineHasRTLScript = false
lineHasLTRScript = false lineHasLTRScript = false
case runeCount == 1 && r == 0xFEFF: // UTF BOM
// the first BOM is safe
case r == '\r' || r == '\t' || r == ' ': case r == '\r' || r == '\t' || r == ' ':
// These are acceptable control characters and space characters // These are acceptable control characters and space characters
case unicode.IsSpace(r): case unicode.IsSpace(r):

View file

@ -129,6 +129,14 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`,
"\n" + `if access_level != "user<span class="escaped-code-point" data-escaped="[U+202E]"><span class="char">` + "\u202e" + `</span></span> <span class="escaped-code-point" data-escaped="[U+2066]"><span class="char">` + "\u2066" + `</span></span>// Check if admin<span class="escaped-code-point" data-escaped="[U+2069]"><span class="char">` + "\u2069" + `</span></span> <span class="escaped-code-point" data-escaped="[U+2066]"><span class="char">` + "\u2066" + `</span></span>" {` + "\n", "\n" + `if access_level != "user<span class="escaped-code-point" data-escaped="[U+202E]"><span class="char">` + "\u202e" + `</span></span> <span class="escaped-code-point" data-escaped="[U+2066]"><span class="char">` + "\u2066" + `</span></span>// Check if admin<span class="escaped-code-point" data-escaped="[U+2069]"><span class="char">` + "\u2069" + `</span></span> <span class="escaped-code-point" data-escaped="[U+2066]"><span class="char">` + "\u2066" + `</span></span>" {` + "\n",
status: EscapeStatus{Escaped: true, HasBIDI: true, BadBIDI: true, HasLTRScript: true, HasRTLScript: true}, status: EscapeStatus{Escaped: true, HasBIDI: true, BadBIDI: true, HasLTRScript: true, HasRTLScript: true},
}, },
{
// UTF-8/16/32 all use the same codepoint for BOM
// Gitea could read UTF-16/32 content and convert into UTF-8 internally then render it, so we only process UTF-8 internally
name: "UTF BOM",
text: "\xef\xbb\xbftest",
result: "\xef\xbb\xbftest",
status: EscapeStatus{HasLTRScript: true},
},
} }
func TestEscapeControlString(t *testing.T) { func TestEscapeControlString(t *testing.T) {
@ -163,10 +171,18 @@ func TestEscapeControlReader(t *testing.T) {
// lets add some control characters to the tests // lets add some control characters to the tests
tests := make([]escapeControlTest, 0, len(escapeControlTests)*3) tests := make([]escapeControlTest, 0, len(escapeControlTests)*3)
copy(tests, escapeControlTests) copy(tests, escapeControlTests)
// if there is a BOM, we should keep the BOM
addPrefix := func(prefix, s string) string {
if strings.HasPrefix(s, "\xef\xbb\xbf") {
return s[:3] + prefix + s[3:]
}
return prefix + s
}
for _, test := range escapeControlTests { for _, test := range escapeControlTests {
test.name += " (+Control)" test.name += " (+Control)"
test.text = "\u001E" + test.text test.text = addPrefix("\u001E", test.text)
test.result = `<span class="escaped-code-point" data-escaped="[U+001E]"><span class="char">` + "\u001e" + `</span></span>` + test.result test.result = addPrefix(`<span class="escaped-code-point" data-escaped="[U+001E]"><span class="char">`+"\u001e"+`</span></span>`, test.result)
test.status.Escaped = true test.status.Escaped = true
test.status.HasControls = true test.status.HasControls = true
tests = append(tests, test) tests = append(tests, test)
@ -174,8 +190,8 @@ func TestEscapeControlReader(t *testing.T) {
for _, test := range escapeControlTests { for _, test := range escapeControlTests {
test.name += " (+Mark)" test.name += " (+Mark)"
test.text = "\u0300" + test.text test.text = addPrefix("\u0300", test.text)
test.result = `<span class="escaped-code-point" data-escaped="[U+0300]"><span class="char">` + "\u0300" + `</span></span>` + test.result test.result = addPrefix(`<span class="escaped-code-point" data-escaped="[U+0300]"><span class="char">`+"\u0300"+`</span></span>`, test.result)
test.status.Escaped = true test.status.Escaped = true
test.status.HasMarks = true test.status.HasMarks = true
tests = append(tests, test) tests = append(tests, test)
@ -200,3 +216,12 @@ func TestEscapeControlReader(t *testing.T) {
}) })
} }
} }
func TestEscapeControlReader_panic(t *testing.T) {
bs := make([]byte, 0, 20479)
bs = append(bs, 'A')
for i := 0; i < 6826; i++ {
bs = append(bs, []byte("—")...)
}
_, _ = EscapeControlBytes(bs)
}

View file

@ -129,7 +129,23 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
// Team. // Team.
if ctx.Org.IsMember { if ctx.Org.IsMember {
shouldSeeAllTeams := false
if ctx.Org.IsOwner { if ctx.Org.IsOwner {
shouldSeeAllTeams = true
} else {
teams, err := org.GetUserTeams(ctx.User.ID)
if err != nil {
ctx.ServerError("GetUserTeams", err)
return
}
for _, team := range teams {
if team.IncludesAllRepositories && team.AccessMode >= perm.AccessModeAdmin {
shouldSeeAllTeams = true
break
}
}
}
if shouldSeeAllTeams {
ctx.Org.Teams, err = org.LoadTeams() ctx.Org.Teams, err = org.LoadTeams()
if err != nil { if err != nil {
ctx.ServerError("LoadTeams", err) ctx.ServerError("LoadTeams", err)

View file

@ -914,7 +914,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
if refType == RepoRefLegacy { if refType == RepoRefLegacy {
// redirect from old URL scheme to new URL scheme // redirect from old URL scheme to new URL scheme
prefix := strings.TrimPrefix(setting.AppSubURL+strings.TrimSuffix(ctx.Req.URL.Path, ctx.Params("*")), ctx.Repo.RepoLink) prefix := strings.TrimPrefix(setting.AppSubURL+strings.ToLower(strings.TrimSuffix(ctx.Req.URL.Path, ctx.Params("*"))), strings.ToLower(ctx.Repo.RepoLink))
ctx.Redirect(path.Join( ctx.Redirect(path.Join(
ctx.Repo.RepoLink, ctx.Repo.RepoLink,

View file

@ -158,6 +158,8 @@ func (c *Command) RunWithContext(rc *RunContext) error {
fmt.Sprintf("LC_ALL=%s", DefaultLocale), fmt.Sprintf("LC_ALL=%s", DefaultLocale),
// avoid prompting for credentials interactively, supported since git v2.3 // avoid prompting for credentials interactively, supported since git v2.3
"GIT_TERMINAL_PROMPT=0", "GIT_TERMINAL_PROMPT=0",
// ignore replace references (https://git-scm.com/docs/git-replace)
"GIT_NO_REPLACE_OBJECTS=1",
) )
cmd.Dir = rc.Dir cmd.Dir = rc.Dir

View file

@ -80,21 +80,20 @@ func InitRepository(ctx context.Context, repoPath string, bare bool) error {
// IsEmpty Check if repository is empty. // IsEmpty Check if repository is empty.
func (repo *Repository) IsEmpty() (bool, error) { func (repo *Repository) IsEmpty() (bool, error) {
var errbuf, output strings.Builder var errbuf, output strings.Builder
if err := NewCommand(repo.Ctx, "rev-list", "--all", "--count", "--max-count=1"). if err := NewCommand(repo.Ctx, "show-ref", "--head", "^HEAD$").
RunWithContext(&RunContext{ RunWithContext(&RunContext{
Timeout: -1, Timeout: -1,
Dir: repo.Path, Dir: repo.Path,
Stdout: &output, Stdout: &output,
Stderr: &errbuf, Stderr: &errbuf,
}); err != nil { }); err != nil {
if err.Error() == "exit status 1" && errbuf.String() == "" {
return true, nil
}
return true, fmt.Errorf("check empty: %v - %s", err, errbuf.String()) return true, fmt.Errorf("check empty: %v - %s", err, errbuf.String())
} }
c, err := strconv.Atoi(strings.TrimSpace(output.String())) return strings.TrimSpace(output.String()) == "", nil
if err != nil {
return true, fmt.Errorf("check empty: convert %s to count failed: %v", output.String(), err)
}
return c == 0, nil
} }
// CloneRepoOptions options when clone a repository // CloneRepoOptions options when clone a repository

View file

@ -185,7 +185,8 @@ func (c *CheckAttributeReader) Init(ctx context.Context) error {
// Run run cmd // Run run cmd
func (c *CheckAttributeReader) Run() error { func (c *CheckAttributeReader) Run() error {
defer func() { defer func() {
_ = c.Close() _ = c.stdinReader.Close()
_ = c.stdOut.Close()
}() }()
stdErr := new(bytes.Buffer) stdErr := new(bytes.Buffer)
err := c.cmd.RunWithContext(&RunContext{ err := c.cmd.RunWithContext(&RunContext{
@ -196,14 +197,19 @@ func (c *CheckAttributeReader) Run() error {
Stdout: c.stdOut, Stdout: c.stdOut,
Stderr: stdErr, Stderr: stdErr,
PipelineFunc: func(_ context.Context, _ context.CancelFunc) error { PipelineFunc: func(_ context.Context, _ context.CancelFunc) error {
close(c.running) select {
case <-c.running:
default:
close(c.running)
}
return nil return nil
}, },
}) })
if err != nil && c.ctx.Err() != nil && err.Error() != "signal: killed" { if err != nil && // If there is an error we need to return but:
c.ctx.Err() != err && // 1. Ignore the context error if the context is cancelled or exceeds the deadline (RunWithContext could return c.ctx.Err() which is Canceled or DeadlineExceeded)
err.Error() != "signal: killed" { // 2. We should not pass up errors due to the program being killed
return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String()) return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String())
} }
return nil return nil
} }
@ -243,10 +249,8 @@ func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err
// Close close pip after use // Close close pip after use
func (c *CheckAttributeReader) Close() error { func (c *CheckAttributeReader) Close() error {
err := c.stdinWriter.Close()
_ = c.stdinReader.Close()
_ = c.stdOut.Close()
c.cancel() c.cancel()
err := c.stdinWriter.Close()
select { select {
case <-c.running: case <-c.running:
default: default:

View file

@ -88,7 +88,10 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
} }
}() }()
} }
defer cancel() defer func() {
_ = checker.Close()
cancel()
}()
} }
} }

View file

@ -193,6 +193,7 @@ func (g *Manager) RunAtHammer(hammer func()) {
func (g *Manager) doShutdown() { func (g *Manager) doShutdown() {
if !g.setStateTransition(stateRunning, stateShuttingDown) { if !g.setStateTransition(stateRunning, stateShuttingDown) {
g.DoImmediateHammer()
return return
} }
g.lock.Lock() g.lock.Lock()

View file

@ -168,8 +168,12 @@ func (g *Manager) DoGracefulRestart() {
if setting.GracefulRestartable { if setting.GracefulRestartable {
log.Info("PID: %d. Forking...", os.Getpid()) log.Info("PID: %d. Forking...", os.Getpid())
err := g.doFork() err := g.doFork()
if err != nil && err.Error() != "another process already forked. Ignoring this one" { if err != nil {
log.Error("Error whilst forking from PID: %d : %v", os.Getpid(), err) if err.Error() == "another process already forked. Ignoring this one" {
g.DoImmediateHammer()
} else {
log.Error("Error whilst forking from PID: %d : %v", os.Getpid(), err)
}
} }
} else { } else {
log.Info("PID: %d. Not set restartable. Shutting down...", os.Getpid()) log.Info("PID: %d. Not set restartable. Shutting down...", os.Getpid())

View file

@ -182,7 +182,8 @@ func NewBleveIndexer(indexDir string) (*BleveIndexer, bool, error) {
} }
func (b *BleveIndexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, commitSha string, func (b *BleveIndexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, commitSha string,
update fileUpdate, repo *repo_model.Repository, batch *gitea_bleve.FlushingBatch) error { update fileUpdate, repo *repo_model.Repository, batch *gitea_bleve.FlushingBatch,
) error {
// Ignore vendored files in code search // Ignore vendored files in code search
if setting.Indexer.ExcludeVendored && analyze.IsVendor(update.Filename) { if setting.Indexer.ExcludeVendored && analyze.IsVendor(update.Filename) {
return nil return nil

View file

@ -37,6 +37,9 @@ func (db *DBIndexer) Index(id int64) error {
gitRepo, err := git.OpenRepositoryCtx(ctx, repo.RepoPath()) gitRepo, err := git.OpenRepositoryCtx(ctx, repo.RepoPath())
if err != nil { if err != nil {
if err.Error() == "no such file or directory" {
return nil
}
return err return err
} }
defer gitRepo.Close() defer gitRepo.Close()

72
modules/log/buffer.go Normal file
View file

@ -0,0 +1,72 @@
// Copyright 2022 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 log
import (
"bytes"
"sync"
)
type bufferWriteCloser struct {
mu sync.Mutex
buffer bytes.Buffer
}
func (b *bufferWriteCloser) Write(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.buffer.Write(p)
}
func (b *bufferWriteCloser) Close() error {
return nil
}
func (b *bufferWriteCloser) String() string {
b.mu.Lock()
defer b.mu.Unlock()
return b.buffer.String()
}
// BufferLogger implements LoggerProvider and writes messages in a buffer.
type BufferLogger struct {
WriterLogger
}
// NewBufferLogger create BufferLogger returning as LoggerProvider.
func NewBufferLogger() LoggerProvider {
log := &BufferLogger{}
log.NewWriterLogger(&bufferWriteCloser{})
return log
}
// Init inits connection writer
func (log *BufferLogger) Init(string) error {
log.NewWriterLogger(log.out)
return nil
}
// Content returns the content accumulated in the content provider
func (log *BufferLogger) Content() (string, error) {
return log.out.(*bufferWriteCloser).String(), nil
}
// Flush when log should be flushed
func (log *BufferLogger) Flush() {
}
// ReleaseReopen does nothing
func (log *BufferLogger) ReleaseReopen() error {
return nil
}
// GetName returns the default name for this implementation
func (log *BufferLogger) GetName() string {
return "buffer"
}
func init() {
Register("buffer", NewBufferLogger)
}

View file

@ -0,0 +1,64 @@
// Copyright 2022 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 log
import (
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestBufferLogger(t *testing.T) {
logger := NewBufferLogger()
bufferLogger := logger.(*BufferLogger)
assert.NotNil(t, bufferLogger)
err := logger.Init("")
assert.NoError(t, err)
location, _ := time.LoadLocation("EST")
date := time.Date(2019, time.January, 13, 22, 3, 30, 15, location)
msg := "TEST MSG"
event := Event{
level: INFO,
msg: msg,
caller: "CALLER",
filename: "FULL/FILENAME",
line: 1,
time: date,
}
logger.LogEvent(&event)
content, err := bufferLogger.Content()
assert.NoError(t, err)
assert.Contains(t, content, msg)
logger.Close()
}
func TestBufferLoggerContent(t *testing.T) {
level := INFO
logger := NewLogger(0, "console", "console", fmt.Sprintf(`{"level":"%s"}`, level.String()))
logger.SetLogger("buffer", "buffer", "{}")
defer logger.DelLogger("buffer")
msg := "A UNIQUE MESSAGE"
Error(msg)
found := false
for i := 0; i < 30000; i++ {
content, err := logger.GetLoggerProviderContent("buffer")
assert.NoError(t, err)
if strings.Contains(content, msg) {
found = true
break
}
time.Sleep(1 * time.Millisecond)
}
assert.True(t, found)
}

View file

@ -119,6 +119,11 @@ func (log *ConnLogger) Init(jsonconfig string) error {
return nil return nil
} }
// Content returns the content accumulated in the content provider
func (log *ConnLogger) Content() (string, error) {
return "", fmt.Errorf("not supported")
}
// Flush does nothing for this implementation // Flush does nothing for this implementation
func (log *ConnLogger) Flush() { func (log *ConnLogger) Flush() {
} }

View file

@ -66,6 +66,11 @@ func (log *ConsoleLogger) Init(config string) error {
return nil return nil
} }
// Content returns the content accumulated in the content provider
func (log *ConsoleLogger) Content() (string, error) {
return "", fmt.Errorf("not supported")
}
// Flush when log should be flushed // Flush when log should be flushed
func (log *ConsoleLogger) Flush() { func (log *ConsoleLogger) Flush() {
} }

View file

@ -216,6 +216,12 @@ func (m *MultiChannelledLog) GetEventLogger(name string) EventLogger {
return m.loggers[name] return m.loggers[name]
} }
// GetEventProvider returns a sub logger provider content from this MultiChannelledLog
func (m *MultiChannelledLog) GetLoggerProviderContent(name string) (string, error) {
channelledLogger := m.GetEventLogger(name).(*ChannelledLog)
return channelledLogger.loggerProvider.Content()
}
// GetEventLoggerNames returns a list of names // GetEventLoggerNames returns a list of names
func (m *MultiChannelledLog) GetEventLoggerNames() []string { func (m *MultiChannelledLog) GetEventLoggerNames() []string {
m.rwmutex.RLock() m.rwmutex.RLock()

View file

@ -243,6 +243,15 @@ func (log *FileLogger) deleteOldLog() {
}) })
} }
// Content returns the content accumulated in the content provider
func (log *FileLogger) Content() (string, error) {
b, err := os.ReadFile(log.Filename)
if err != nil {
return "", err
}
return string(b), nil
}
// Flush flush file logger. // Flush flush file logger.
// there are no buffering messages in file logger in memory. // there are no buffering messages in file logger in memory.
// flush file means sync file from disk. // flush file means sync file from disk.

View file

@ -7,6 +7,7 @@ package log
// LoggerProvider represents behaviors of a logger provider. // LoggerProvider represents behaviors of a logger provider.
type LoggerProvider interface { type LoggerProvider interface {
Init(config string) error Init(config string) error
Content() (string, error)
EventLogger EventLogger
} }

View file

@ -95,6 +95,11 @@ func (log *SMTPLogger) sendMail(p []byte) (int, error) {
) )
} }
// Content returns the content accumulated in the content provider
func (log *SMTPLogger) Content() (string, error) {
return "", fmt.Errorf("not supported")
}
// Flush when log should be flushed // Flush when log should be flushed
func (log *SMTPLogger) Flush() { func (log *SMTPLogger) Flush() {
} }

View file

@ -46,6 +46,11 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
} }
} }
// SanitizerDisabled disabled sanitize if return true
func (Renderer) SanitizerDisabled() bool {
return false
}
func writeField(w io.Writer, element, class, field string) error { func writeField(w io.Writer, element, class, field string) error {
if _, err := io.WriteString(w, "<"); err != nil { if _, err := io.WriteString(w, "<"); err != nil {
return err return err

View file

@ -54,6 +54,11 @@ func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return p.MarkupSanitizerRules return p.MarkupSanitizerRules
} }
// SanitizerDisabled disabled sanitize if return true
func (p *Renderer) SanitizerDisabled() bool {
return p.DisableSanitizer
}
func envMark(envName string) string { func envMark(envName string) string {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return "%" + envName + "%" return "%" + envName + "%"

View file

@ -99,7 +99,7 @@ var issueFullPatternOnce sync.Once
func getIssueFullPattern() *regexp.Regexp { func getIssueFullPattern() *regexp.Regexp {
issueFullPatternOnce.Do(func() { issueFullPatternOnce.Do(func() {
issueFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) + issueFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) +
`\w+/\w+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`) `[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`)
}) })
return issueFullPattern return issueFullPattern
} }

View file

@ -97,6 +97,15 @@ func TestRender_CrossReferences(t *testing.T) {
test( test(
"/home/gitea/go-gitea/gitea#12345", "/home/gitea/go-gitea/gitea#12345",
`<p>/home/gitea/go-gitea/gitea#12345</p>`) `<p>/home/gitea/go-gitea/gitea#12345</p>`)
test(
util.URLJoin(TestAppURL, "gogitea", "gitea", "issues", "12345"),
`<p><a href="`+util.URLJoin(TestAppURL, "gogitea", "gitea", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogitea/gitea#12345</a></p>`)
test(
util.URLJoin(TestAppURL, "go-gitea", "gitea", "issues", "12345"),
`<p><a href="`+util.URLJoin(TestAppURL, "go-gitea", "gitea", "issues", "12345")+`" class="ref-issue" rel="nofollow">go-gitea/gitea#12345</a></p>`)
test(
util.URLJoin(TestAppURL, "gogitea", "some-repo-name", "issues", "12345"),
`<p><a href="`+util.URLJoin(TestAppURL, "gogitea", "some-repo-name", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogitea/some-repo-name#12345</a></p>`)
} }
func TestMisc_IsSameDomain(t *testing.T) { func TestMisc_IsSameDomain(t *testing.T) {

View file

@ -221,6 +221,11 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return []setting.MarkupSanitizerRule{} return []setting.MarkupSanitizerRule{}
} }
// SanitizerDisabled disabled sanitize if return true
func (Renderer) SanitizerDisabled() bool {
return false
}
// Render implements markup.Renderer // Render implements markup.Renderer
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
return render(ctx, input, output) return render(ctx, input, output)

View file

@ -47,6 +47,11 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return []setting.MarkupSanitizerRule{} return []setting.MarkupSanitizerRule{}
} }
// SanitizerDisabled disabled sanitize if return true
func (Renderer) SanitizerDisabled() bool {
return false
}
// Render renders orgmode rawbytes to HTML // Render renders orgmode rawbytes to HTML
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
htmlWriter := org.NewHTMLWriter() htmlWriter := org.NewHTMLWriter()

View file

@ -81,6 +81,7 @@ type Renderer interface {
Extensions() []string Extensions() []string
NeedPostProcess() bool NeedPostProcess() bool
SanitizerRules() []setting.MarkupSanitizerRule SanitizerRules() []setting.MarkupSanitizerRule
SanitizerDisabled() bool
Render(ctx *RenderContext, input io.Reader, output io.Writer) error Render(ctx *RenderContext, input io.Reader, output io.Writer) error
} }
@ -127,6 +128,12 @@ func RenderString(ctx *RenderContext, content string) (string, error) {
return buf.String(), nil return buf.String(), nil
} }
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error { return nil }
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
var wg sync.WaitGroup var wg sync.WaitGroup
var err error var err error
@ -136,18 +143,25 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
_ = pw.Close() _ = pw.Close()
}() }()
pr2, pw2 := io.Pipe() var pr2 io.ReadCloser
defer func() { var pw2 io.WriteCloser
_ = pr2.Close()
_ = pw2.Close()
}()
wg.Add(1) if !renderer.SanitizerDisabled() {
go func() { pr2, pw2 = io.Pipe()
err = SanitizeReader(pr2, renderer.Name(), output) defer func() {
_ = pr2.Close() _ = pr2.Close()
wg.Done() _ = pw2.Close()
}() }()
wg.Add(1)
go func() {
err = SanitizeReader(pr2, renderer.Name(), output)
_ = pr2.Close()
wg.Done()
}()
} else {
pw2 = nopCloser{output}
}
wg.Add(1) wg.Add(1)
go func() { go func() {

View file

@ -9,7 +9,8 @@ import "time"
// Comment is a standard comment information // Comment is a standard comment information
type Comment struct { type Comment struct {
IssueIndex int64 `yaml:"issue_index"` IssueIndex int64 `yaml:"issue_index"`
Index int64
PosterID int64 `yaml:"poster_id"` PosterID int64 `yaml:"poster_id"`
PosterName string `yaml:"poster_name"` PosterName string `yaml:"poster_name"`
PosterEmail string `yaml:"poster_email"` PosterEmail string `yaml:"poster_email"`

View file

@ -5,10 +5,13 @@
package nosql package nosql
import ( import (
"fmt"
"path" "path"
"strconv" "strconv"
"strings" "strings"
"code.gitea.io/gitea/modules/log"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors" "github.com/syndtr/goleveldb/leveldb/errors"
"github.com/syndtr/goleveldb/leveldb/opt" "github.com/syndtr/goleveldb/leveldb/opt"
@ -20,8 +23,16 @@ func (m *Manager) CloseLevelDB(connection string) error {
defer m.mutex.Unlock() defer m.mutex.Unlock()
db, ok := m.LevelDBConnections[connection] db, ok := m.LevelDBConnections[connection]
if !ok { if !ok {
connection = ToLevelDBURI(connection).String() // Try the full URI
db, ok = m.LevelDBConnections[connection] uri := ToLevelDBURI(connection)
db, ok = m.LevelDBConnections[uri.String()]
if !ok {
// Try the datadir directly
dataDir := path.Join(uri.Host, uri.Path)
db, ok = m.LevelDBConnections[dataDir]
}
} }
if !ok { if !ok {
return nil return nil
@ -40,6 +51,12 @@ func (m *Manager) CloseLevelDB(connection string) error {
// GetLevelDB gets a levelDB for a particular connection // GetLevelDB gets a levelDB for a particular connection
func (m *Manager) GetLevelDB(connection string) (*leveldb.DB, error) { func (m *Manager) GetLevelDB(connection string) (*leveldb.DB, error) {
// Convert the provided connection description to the common format
uri := ToLevelDBURI(connection)
// Get the datadir
dataDir := path.Join(uri.Host, uri.Path)
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
db, ok := m.LevelDBConnections[connection] db, ok := m.LevelDBConnections[connection]
@ -48,12 +65,28 @@ func (m *Manager) GetLevelDB(connection string) (*leveldb.DB, error) {
return db.db, nil return db.db, nil
} }
uri := ToLevelDBURI(connection)
db = &levelDBHolder{ db, ok = m.LevelDBConnections[uri.String()]
name: []string{connection, uri.String()}, if ok {
db.count++
return db.db, nil
}
// if there is already a connection to this leveldb reuse that
// NOTE: if there differing options then only the first leveldb connection will be used
db, ok = m.LevelDBConnections[dataDir]
if ok {
db.count++
log.Warn("Duplicate connnection to level db: %s with different connection strings. Initial connection: %s. This connection: %s", dataDir, db.name[0], connection)
db.name = append(db.name, connection)
m.LevelDBConnections[connection] = db
return db.db, nil
}
db = &levelDBHolder{
name: []string{connection, uri.String(), dataDir},
} }
dataDir := path.Join(uri.Host, uri.Path)
opts := &opt.Options{} opts := &opt.Options{}
for k, v := range uri.Query() { for k, v := range uri.Query() {
switch replacer.Replace(strings.ToLower(k)) { switch replacer.Replace(strings.ToLower(k)) {
@ -134,7 +167,11 @@ func (m *Manager) GetLevelDB(connection string) (*leveldb.DB, error) {
db.db, err = leveldb.OpenFile(dataDir, opts) db.db, err = leveldb.OpenFile(dataDir, opts)
if err != nil { if err != nil {
if !errors.IsCorrupted(err) { if !errors.IsCorrupted(err) {
return nil, err if strings.Contains(err.Error(), "resource temporarily unavailable") {
return nil, fmt.Errorf("unable to lock level db at %s: %w", dataDir, err)
}
return nil, fmt.Errorf("unable to open level db at %s: %w", dataDir, err)
} }
db.db, err = leveldb.RecoverFile(dataDir, opts) db.db, err = leveldb.RecoverFile(dataDir, opts)
if err != nil { if err != nil {

View file

@ -91,7 +91,8 @@ func (a *actionNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *m
// NotifyCreateIssueComment notifies comment on an issue to notifiers // NotifyCreateIssueComment notifies comment on an issue to notifiers
func (a *actionNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, func (a *actionNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
issue *models.Issue, comment *models.Comment, mentions []*user_model.User) { issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
) {
act := &models.Action{ act := &models.Action{
ActUserID: doer.ID, ActUserID: doer.ID,
ActUser: doer, ActUser: doer,

View file

@ -22,6 +22,7 @@ type Notifier interface {
NotifyTransferRepository(doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) NotifyTransferRepository(doer *user_model.User, repo *repo_model.Repository, oldOwnerName string)
NotifyNewIssue(issue *models.Issue, mentions []*user_model.User) NotifyNewIssue(issue *models.Issue, mentions []*user_model.User)
NotifyIssueChangeStatus(*user_model.User, *models.Issue, *models.Comment, bool) NotifyIssueChangeStatus(*user_model.User, *models.Issue, *models.Comment, bool)
NotifyDeleteIssue(*user_model.User, *models.Issue)
NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64) NotifyIssueChangeMilestone(doer *user_model.User, issue *models.Issue, oldMilestoneID int64)
NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment)
NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment) NotifyPullReviewRequest(doer *user_model.User, issue *models.Issue, reviewer *user_model.User, isRequest bool, comment *models.Comment)

View file

@ -33,6 +33,10 @@ func (*NullNotifier) NotifyNewIssue(issue *models.Issue, mentions []*user_model.
func (*NullNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) { func (*NullNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) {
} }
// NotifyDeleteIssue notify when some issue deleted
func (*NullNotifier) NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) {
}
// NotifyNewPullRequest places a place holder function // NotifyNewPullRequest places a place holder function
func (*NullNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) { func (*NullNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) {
} }

View file

@ -30,7 +30,8 @@ func NewNotifier() base.Notifier {
} }
func (r *indexerNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, func (r *indexerNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
issue *models.Issue, comment *models.Comment, mentions []*user_model.User) { issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
) {
if comment.Type == models.CommentTypeComment { if comment.Type == models.CommentTypeComment {
if issue.Comments == nil { if issue.Comments == nil {
if err := issue.LoadDiscussComments(); err != nil { if err := issue.LoadDiscussComments(); err != nil {

View file

@ -29,7 +29,8 @@ func NewNotifier() base.Notifier {
} }
func (m *mailNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, func (m *mailNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
issue *models.Issue, comment *models.Comment, mentions []*user_model.User) { issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
) {
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyCreateIssueComment Issue[%d] #%d in [%d]", issue.ID, issue.Index, issue.RepoID)) ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("mailNotifier.NotifyCreateIssueComment Issue[%d] #%d in [%d]", issue.ID, issue.Index, issue.RepoID))
defer finished() defer finished()

View file

@ -39,7 +39,8 @@ func NewContext() {
// NotifyCreateIssueComment notifies issue comment related message to notifiers // NotifyCreateIssueComment notifies issue comment related message to notifiers
func NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, func NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
issue *models.Issue, comment *models.Comment, mentions []*user_model.User) { issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
) {
for _, notifier := range notifiers { for _, notifier := range notifiers {
notifier.NotifyCreateIssueComment(doer, repo, issue, comment, mentions) notifier.NotifyCreateIssueComment(doer, repo, issue, comment, mentions)
} }
@ -59,6 +60,13 @@ func NotifyIssueChangeStatus(doer *user_model.User, issue *models.Issue, actionC
} }
} }
// NotifyDeleteIssue notify when some issue deleted
func NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) {
for _, notifier := range notifiers {
notifier.NotifyDeleteIssue(doer, issue)
}
}
// NotifyMergePullRequest notifies merge pull request to notifiers // NotifyMergePullRequest notifies merge pull request to notifiers
func NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) { func NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) {
for _, notifier := range notifiers { for _, notifier := range notifiers {
@ -201,7 +209,8 @@ func NotifyIssueChangeRef(doer *user_model.User, issue *models.Issue, oldRef str
// NotifyIssueChangeLabels notifies change labels to notifiers // NotifyIssueChangeLabels notifies change labels to notifiers
func NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue, func NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue,
addedLabels, removedLabels []*models.Label) { addedLabels, removedLabels []*models.Label,
) {
for _, notifier := range notifiers { for _, notifier := range notifiers {
notifier.NotifyIssueChangeLabels(doer, issue, addedLabels, removedLabels) notifier.NotifyIssueChangeLabels(doer, issue, addedLabels, removedLabels)
} }

View file

@ -53,7 +53,8 @@ func (ns *notificationService) Run() {
} }
func (ns *notificationService) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, func (ns *notificationService) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
issue *models.Issue, comment *models.Comment, mentions []*user_model.User) { issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
) {
opts := issueNotificationOpts{ opts := issueNotificationOpts{
IssueID: issue.ID, IssueID: issue.ID,
NotificationAuthorID: doer.ID, NotificationAuthorID: doer.ID,
@ -203,7 +204,7 @@ func (ns *notificationService) NotifyPullRevieweDismiss(doer *user_model.User, r
} }
func (ns *notificationService) NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) { func (ns *notificationService) NotifyIssueChangeAssignee(doer *user_model.User, issue *models.Issue, assignee *user_model.User, removed bool, comment *models.Comment) {
if !removed { if !removed && doer.ID != assignee.ID {
opts := issueNotificationOpts{ opts := issueNotificationOpts{
IssueID: issue.ID, IssueID: issue.ID,
NotificationAuthorID: doer.ID, NotificationAuthorID: doer.ID,

View file

@ -424,7 +424,8 @@ func (m *webhookNotifier) NotifyUpdateComment(doer *user_model.User, c *models.C
} }
func (m *webhookNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, func (m *webhookNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository,
issue *models.Issue, comment *models.Comment, mentions []*user_model.User) { issue *models.Issue, comment *models.Comment, mentions []*user_model.User,
) {
mode, _ := models.AccessLevel(doer, repo) mode, _ := models.AccessLevel(doer, repo)
var err error var err error
@ -498,7 +499,8 @@ func (m *webhookNotifier) NotifyDeleteComment(doer *user_model.User, comment *mo
} }
func (m *webhookNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue, func (m *webhookNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue,
addedLabels, removedLabels []*models.Label) { addedLabels, removedLabels []*models.Label,
) {
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeLabels User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID)) ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.NotifyIssueChangeLabels User: %s[%d] Issue[%d] #%d in [%d]", doer.Name, doer.ID, issue.ID, issue.Index, issue.RepoID))
defer finished() defer finished()

View file

@ -29,6 +29,7 @@ type MarkupRenderer struct {
IsInputFile bool IsInputFile bool
NeedPostProcess bool NeedPostProcess bool
MarkupSanitizerRules []MarkupSanitizerRule MarkupSanitizerRules []MarkupSanitizerRule
DisableSanitizer bool
} }
// MarkupSanitizerRule defines the policy for whitelisting attributes on // MarkupSanitizerRule defines the policy for whitelisting attributes on
@ -144,11 +145,12 @@ func newMarkupRenderer(name string, sec *ini.Section) {
} }
ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{ ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
Enabled: sec.Key("ENABLED").MustBool(false), Enabled: sec.Key("ENABLED").MustBool(false),
MarkupName: name, MarkupName: name,
FileExtensions: exts, FileExtensions: exts,
Command: command, Command: command,
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false), IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true), NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
DisableSanitizer: sec.Key("DISABLE_SANITIZER").MustBool(false),
}) })
} }

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