Merge branch 'main' into main
This commit is contained in:
commit
d87a16b86f
553 changed files with 4787 additions and 64021 deletions
|
@ -804,11 +804,12 @@ steps:
|
|||
depends_on: [gpg-sign]
|
||||
|
||||
- name: github
|
||||
image: plugins/github-release:1
|
||||
image: plugins/github-release:latest
|
||||
pull: always
|
||||
settings:
|
||||
files:
|
||||
- "dist/release/*"
|
||||
file_exists: overwrite
|
||||
environment:
|
||||
GITHUB_TOKEN:
|
||||
from_secret: github_token
|
||||
|
|
|
@ -442,6 +442,7 @@ rules:
|
|||
unicorn/require-post-message-target-origin: [0]
|
||||
unicorn/string-content: [0]
|
||||
unicorn/template-indent: [2]
|
||||
unicorn/text-encoding-identifier-case: [0]
|
||||
unicorn/throw-new-error: [2]
|
||||
use-isnan: [2]
|
||||
valid-typeof: [2, {requireStringLiterals: true}]
|
||||
|
|
|
@ -14,6 +14,7 @@ rules:
|
|||
declaration-block-no-redundant-longhand-properties: null
|
||||
declaration-block-single-line-max-declarations: null
|
||||
declaration-empty-line-before: null
|
||||
function-no-unknown: null
|
||||
hue-degree-notation: null
|
||||
indentation: 2
|
||||
max-line-length: null
|
||||
|
|
|
@ -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.)
|
||||
* 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.
|
||||
* 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
|
||||
|
||||
|
|
26
Makefile
26
Makefile
|
@ -235,7 +235,7 @@ clean:
|
|||
.PHONY: fmt
|
||||
fmt:
|
||||
@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
|
||||
@echo "Running gitea-fmt (with gofumpt)..."
|
||||
@$(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}'
|
||||
|
@ -287,7 +287,7 @@ errcheck:
|
|||
.PHONY: fmt-check
|
||||
fmt-check:
|
||||
@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
|
||||
# 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}'); \
|
||||
|
@ -313,10 +313,9 @@ lint: lint-frontend lint-backend
|
|||
lint-frontend: node_modules
|
||||
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 editorconfig-checker templates
|
||||
|
||||
.PHONY: lint-backend
|
||||
lint-backend: golangci-lint vet
|
||||
lint-backend: golangci-lint vet editorconfig-checker
|
||||
|
||||
.PHONY: 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.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:
|
||||
sed -e 's|{{TEST_MYSQL_HOST}}|${TEST_MYSQL_HOST}|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
|
||||
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)
|
||||
$(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)
|
||||
$(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
|
||||
migrations.mysql.test: $(GO_SOURCES)
|
||||
$(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); \
|
||||
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
|
||||
docker:
|
||||
docker build --disable-content-trust=false -t $(DOCKER_REF) .
|
||||
|
|
57
cmd/admin.go
57
cmd/admin.go
|
@ -56,6 +56,7 @@ var (
|
|||
microcmdUserList,
|
||||
microcmdUserChangePassword,
|
||||
microcmdUserDelete,
|
||||
microcmdUserGenerateAccessToken,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -154,6 +155,27 @@ var (
|
|||
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{
|
||||
Name: "repo-sync-releases",
|
||||
Usage: "Synchronize repository releases with tags",
|
||||
|
@ -641,6 +663,41 @@ func runDeleteUser(c *cli.Context) error {
|
|||
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 {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
|
|
|
@ -4,5 +4,5 @@ grep 'git' go.mod | grep '\.com' | grep -v indirect | grep -v replace | cut -f 2
|
|||
go get -u "$line"
|
||||
make vendor
|
||||
git add .
|
||||
git commit -S -m "update $line"
|
||||
git commit -m "update $line"
|
||||
done
|
||||
|
|
|
@ -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
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -902,7 +902,7 @@ PATH =
|
|||
;; Whether repository file uploads are enabled. Defaults to `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
|
||||
;;
|
||||
;; 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
|
||||
;;
|
||||
;; 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=- -"
|
||||
;; Don't pass the file on STDIN, pass the filename as argument instead.
|
||||
;IS_INPUT_FILE = false
|
||||
; Don't filter html tags and attributes if true
|
||||
;DISABLE_SANITIZER = false
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -18,7 +18,7 @@ params:
|
|||
description: Git with a cup of tea
|
||||
author: The Gitea Authors
|
||||
website: https://docs.gitea.io
|
||||
version: 1.16.0
|
||||
version: 1.16.3
|
||||
minGoVersion: 1.16
|
||||
goVersion: 1.17
|
||||
minNodeVersion: 12.17
|
||||
|
|
|
@ -107,7 +107,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
|||
### Repository - Upload (`repository.upload`)
|
||||
|
||||
- `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.
|
||||
- `FILE_MAX_SIZE`: **3**: Max size of each file in megabytes.
|
||||
- `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`)
|
||||
|
||||
- `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`)
|
||||
|
||||
|
@ -189,7 +189,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
|
|||
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.
|
||||
- `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`)
|
||||
|
||||
|
@ -1003,13 +1003,13 @@ IS_INPUT_FILE = false
|
|||
command. Multiple extensions needs a comma as splitter.
|
||||
- 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`.
|
||||
- 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:
|
||||
- `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 supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc.
|
||||
If `DISABLE_SANITIZER` is false, Gitea supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc.
|
||||
|
||||
```ini
|
||||
[markup.sanitizer.TeX]
|
||||
|
|
|
@ -318,6 +318,33 @@ IS_INPUT_FILE = false
|
|||
- FILE_EXTENSIONS: 关联的文档的扩展名,多个扩展名用都好分隔。
|
||||
- RENDER_COMMAND: 工具的命令行命令及参数。
|
||||
- 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`)
|
||||
|
||||
|
|
|
@ -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,
|
||||
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
|
||||
|
||||
Display Office DOCX files with [`pandoc`](https://pandoc.org/):
|
||||
|
|
|
@ -185,8 +185,6 @@ Before committing, make sure the linters pass:
|
|||
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
|
||||
|
||||
Start local ElasticSearch instance using docker:
|
||||
|
|
|
@ -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.
|
||||
|
||||
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/).
|
||||
|
|
|
@ -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`
|
||||
|
||||
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
|
||||
```
|
||||
|
|
|
@ -30,6 +30,10 @@ server {
|
|||
|
||||
location / {
|
||||
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/ {
|
||||
# Note: Trailing slash
|
||||
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
118
go.mod
|
@ -3,100 +3,100 @@ module code.gitea.io/gitea
|
|||
go 1.16
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.78.0 // indirect
|
||||
code.gitea.io/gitea-vet v0.2.2-0.20220122151748-48ebc902541b
|
||||
code.gitea.io/sdk/gitea v0.15.1
|
||||
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/session v0.0.0-20211218221615-e3605d8b28b8
|
||||
gitea.com/lunny/levelqueue v0.4.1
|
||||
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/ProtonMail/go-crypto v0.0.0-20210705153151-cc34b1f6908b // indirect
|
||||
github.com/PuerkitoBio/goquery v1.7.0
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20220113124808-70ae35bab23f // indirect
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/alecthomas/chroma v0.10.0
|
||||
github.com/andybalholm/brotli v1.0.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.2.0 // indirect
|
||||
github.com/blevesearch/bleve/v2 v2.3.0
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.2.1 // indirect
|
||||
github.com/blevesearch/bleve/v2 v2.3.1
|
||||
github.com/boombuler/barcode v1.0.1 // 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/couchbase/go-couchbase v0.0.0-20210224140812-5740cd35f448 // indirect
|
||||
github.com/couchbase/gomemcached v0.1.2 // 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/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/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/ethantkoenig/rupture v1.0.0
|
||||
github.com/ethantkoenig/rupture v1.0.1
|
||||
github.com/gliderlabs/ssh v0.3.3
|
||||
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-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-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4
|
||||
github.com/go-ldap/ldap/v3 v3.3.0
|
||||
github.com/go-redis/redis/v8 v8.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.2
|
||||
github.com/go-redis/redis/v8 v8.11.4
|
||||
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/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/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/google/go-github/v39 v39.2.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/feeds v1.1.1
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
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-version v1.3.1
|
||||
github.com/hashicorp/go-version v1.4.0
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/kevinburke/ssh_config v1.1.0 // indirect
|
||||
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4
|
||||
github.com/klauspost/compress v1.13.1
|
||||
github.com/klauspost/cpuid/v2 v2.0.9
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/lib/pq v1.10.2
|
||||
github.com/klauspost/compress v1.15.0
|
||||
github.com/klauspost/cpuid/v2 v2.0.11
|
||||
github.com/lib/pq v1.10.4
|
||||
github.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
||||
github.com/markbates/goth v1.68.0
|
||||
github.com/mattn/go-isatty v0.0.13
|
||||
github.com/markbates/goth v1.69.0
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.8
|
||||
github.com/mholt/archiver/v3 v3.5.0
|
||||
github.com/microcosm-cc/bluemonday v1.0.16
|
||||
github.com/mattn/go-sqlite3 v1.14.12
|
||||
github.com/mholt/acmez v1.0.2 // indirect
|
||||
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/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/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/niklasfasching/go-org v1.5.0
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/niklasfasching/go-org v1.6.2
|
||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||
github.com/oliamb/cutter v0.2.2
|
||||
github.com/olivere/elastic/v7 v7.0.25
|
||||
github.com/pelletier/go-toml v1.9.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.8 // indirect
|
||||
github.com/olivere/elastic/v7 v7.0.31
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/quasoft/websspi v1.0.0
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/quasoft/websspi v1.1.2
|
||||
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/sergi/go-diff v1.2.0
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
|
||||
|
@ -107,34 +107,34 @@ require (
|
|||
github.com/tstranex/u2f v1.0.0
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
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/unrolled/render v1.4.0
|
||||
github.com/unrolled/render v1.4.1
|
||||
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/yuin/goldmark v1.4.4
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
|
||||
github.com/yuin/goldmark-meta v1.0.0
|
||||
github.com/yuin/goldmark v1.4.8
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.jolheiser.com/hcaptcha v0.0.4
|
||||
go.jolheiser.com/pwn v0.0.3
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
go.uber.org/zap v1.19.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
|
||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
|
||||
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/zap v1.21.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9
|
||||
golang.org/x/text v0.3.7
|
||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect
|
||||
golang.org/x/tools v0.1.0
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
|
||||
golang.org/x/tools v0.1.9
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
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
|
||||
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
|
||||
xorm.io/builder v0.3.9
|
||||
xorm.io/xorm v1.2.5
|
||||
|
|
|
@ -468,7 +468,6 @@ func TestAPIRepoTransfer(t *testing.T) {
|
|||
expectedStatus int
|
||||
}{
|
||||
// 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
|
||||
{ctxUserID: 1, newOwner: "user3", teams: &[]int64{5}, expectedStatus: http.StatusForbidden},
|
||||
// Transfer to a user with non-existent team IDs should fail
|
||||
|
|
|
@ -178,7 +178,9 @@ func (c *compareDump) assertEquals(repoBefore, repoAfter *repo_model.Repository)
|
|||
assert.GreaterOrEqual(c.t, len(issues), 1)
|
||||
for _, issue := range issues {
|
||||
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)
|
||||
for _, comment := range comments {
|
||||
assert.EqualValues(c.t, issue.Number, comment.IssueIndex)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/main
|
|
@ -0,0 +1,6 @@
|
|||
[core]
|
||||
bare = true
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
symlinks = false
|
||||
ignorecase = true
|
|
@ -51,8 +51,6 @@ func TestSignin(t *testing.T) {
|
|||
{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: "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 {
|
||||
|
|
|
@ -181,6 +181,11 @@ func (log *TestLogger) Init(config string) error {
|
|||
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
|
||||
func (log *TestLogger) Flush() {
|
||||
}
|
||||
|
|
|
@ -121,6 +121,7 @@ func TestExportUserGPGKeys(t *testing.T) {
|
|||
defer prepareTestEnv(t)()
|
||||
// Export empty key list
|
||||
testExportUserGPGKeys(t, "user1", `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Note: This user hasn't uploaded any GPG keys.
|
||||
|
||||
|
||||
=twTO
|
||||
|
|
|
@ -43,7 +43,7 @@ type WebAuthnCredential struct {
|
|||
Name string
|
||||
LowerName string `xorm:"unique(s)"`
|
||||
UserID int64 `xorm:"INDEX unique(s)"`
|
||||
CredentialID string `xorm:"INDEX"`
|
||||
CredentialID string `xorm:"INDEX VARCHAR(410)"`
|
||||
PublicKey []byte
|
||||
AttestationType string
|
||||
AAGUID []byte
|
||||
|
|
|
@ -148,6 +148,17 @@ func DeleteByBean(ctx context.Context, bean interface{}) (int64, error) {
|
|||
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.
|
||||
func CountByBean(ctx context.Context, bean interface{}) (int64, error) {
|
||||
return GetEngine(ctx).Count(bean)
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
owner_name: user2
|
||||
lower_name: repo2
|
||||
name: repo2
|
||||
is_empty: false
|
||||
is_archived: false
|
||||
is_private: true
|
||||
num_issues: 2
|
||||
|
@ -40,6 +41,7 @@
|
|||
owner_name: user3
|
||||
lower_name: repo3
|
||||
name: repo3
|
||||
is_empty: false
|
||||
is_private: true
|
||||
num_issues: 1
|
||||
num_closed_issues: 0
|
||||
|
@ -56,6 +58,7 @@
|
|||
owner_name: user5
|
||||
lower_name: repo4
|
||||
name: repo4
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
|
@ -143,6 +146,7 @@
|
|||
owner_name: user12
|
||||
lower_name: repo10
|
||||
name: repo10
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
|
@ -160,6 +164,7 @@
|
|||
owner_name: user13
|
||||
lower_name: repo11
|
||||
name: repo11
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
|
@ -217,7 +222,8 @@
|
|||
owner_name: user2
|
||||
lower_name: repo15
|
||||
name: repo15
|
||||
is_empty: true
|
||||
is_empty: false
|
||||
is_private: true
|
||||
status: 0
|
||||
|
||||
-
|
||||
|
@ -226,6 +232,7 @@
|
|||
owner_name: user2
|
||||
lower_name: repo16
|
||||
name: repo16
|
||||
is_empty: false
|
||||
is_private: true
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
|
@ -459,6 +466,8 @@
|
|||
owner_name: user2
|
||||
lower_name: repo20
|
||||
name: repo20
|
||||
is_empty: false
|
||||
is_private: true
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_issues: 0
|
||||
|
@ -484,6 +493,7 @@
|
|||
owner_name: user2
|
||||
lower_name: utf8
|
||||
name: utf8
|
||||
is_empty: false
|
||||
is_private: false
|
||||
status: 0
|
||||
|
||||
|
@ -519,6 +529,7 @@
|
|||
owner_name: user2
|
||||
lower_name: commits_search_test
|
||||
name: commits_search_test
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
|
@ -532,6 +543,7 @@
|
|||
owner_name: user2
|
||||
lower_name: git_hooks_test
|
||||
name: git_hooks_test
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
|
@ -545,6 +557,7 @@
|
|||
owner_name: limited_org
|
||||
lower_name: public_repo_on_limited_org
|
||||
name: public_repo_on_limited_org
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
|
@ -558,6 +571,7 @@
|
|||
owner_name: limited_org
|
||||
lower_name: private_repo_on_limited_org
|
||||
name: private_repo_on_limited_org
|
||||
is_empty: false
|
||||
is_private: true
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
|
@ -571,6 +585,7 @@
|
|||
owner_name: privated_org
|
||||
lower_name: public_repo_on_private_org
|
||||
name: public_repo_on_private_org
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
|
@ -584,6 +599,7 @@
|
|||
owner_name: privated_org
|
||||
lower_name: private_repo_on_private_org
|
||||
name: private_repo_on_private_org
|
||||
is_empty: false
|
||||
is_private: true
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
|
@ -596,6 +612,7 @@
|
|||
owner_name: user2
|
||||
lower_name: glob
|
||||
name: glob
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
|
@ -622,6 +639,7 @@
|
|||
owner_name: user27
|
||||
lower_name: template1
|
||||
name: template1
|
||||
is_empty: false
|
||||
is_private: false
|
||||
is_template: true
|
||||
num_stars: 0
|
||||
|
@ -650,6 +668,7 @@
|
|||
owner_name: org26
|
||||
lower_name: repo_external_tracker
|
||||
name: repo_external_tracker
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
|
@ -663,6 +682,7 @@
|
|||
owner_name: org26
|
||||
lower_name: repo_external_tracker_numeric
|
||||
name: repo_external_tracker_numeric
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
|
@ -676,6 +696,7 @@
|
|||
owner_name: org26
|
||||
lower_name: repo_external_tracker_alpha
|
||||
name: repo_external_tracker_alpha
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
|
@ -690,6 +711,7 @@
|
|||
owner_name: user27
|
||||
lower_name: repo49
|
||||
name: repo49
|
||||
is_empty: false
|
||||
is_private: false
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
|
@ -736,3 +758,13 @@
|
|||
num_projects: 0
|
||||
num_closed_projects: 0
|
||||
status: 0
|
||||
|
||||
-
|
||||
id: 52
|
||||
owner_id: 30
|
||||
owner_name: user30
|
||||
lower_name: empty
|
||||
name: empty
|
||||
is_empty: true
|
||||
is_private: true
|
||||
status: 0
|
||||
|
|
|
@ -522,7 +522,7 @@
|
|||
is_restricted: true
|
||||
avatar: avatar29
|
||||
avatar_email: user30@example.com
|
||||
num_repos: 2
|
||||
num_repos: 3
|
||||
is_active: true
|
||||
prohibit_login: true
|
||||
|
||||
|
|
114
models/issue.go
114
models/issue.go
|
@ -13,6 +13,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
admin_model "code.gitea.io/gitea/models/admin"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
|
@ -24,6 +25,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
@ -1990,6 +1992,118 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *us
|
|||
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.
|
||||
type DependencyInfo struct {
|
||||
Issue `xorm:"extends"`
|
||||
|
|
|
@ -1152,9 +1152,7 @@ func DeleteComment(comment *Comment) error {
|
|||
}
|
||||
|
||||
func deleteComment(e db.Engine, comment *Comment) error {
|
||||
if _, err := e.Delete(&Comment{
|
||||
ID: comment.ID,
|
||||
}); err != nil {
|
||||
if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// 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
|
||||
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) {
|
||||
|
@ -164,7 +165,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
|
|||
Doer: user,
|
||||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Content: SecToTime(timediff),
|
||||
Content: util.SecToTime(timediff),
|
||||
Type: CommentTypeStopTracking,
|
||||
TimeID: tt.ID,
|
||||
}); err != nil {
|
||||
|
@ -263,32 +264,3 @@ func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) e
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -397,6 +397,58 @@ func TestIssue_InsertIssue(t *testing.T) {
|
|||
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) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
@ -177,7 +178,7 @@ func AddTime(user *user_model.User, issue *Issue, amount int64, created time.Tim
|
|||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Doer: user,
|
||||
Content: SecToTime(amount),
|
||||
Content: util.SecToTime(amount),
|
||||
Type: CommentTypeAddTimeManual,
|
||||
TimeID: t.ID,
|
||||
}); err != nil {
|
||||
|
@ -226,7 +227,7 @@ func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string,
|
|||
}
|
||||
return nil, err
|
||||
}
|
||||
totalTimes[user] = SecToTime(total)
|
||||
totalTimes[user] = util.SecToTime(total)
|
||||
}
|
||||
return totalTimes, nil
|
||||
}
|
||||
|
@ -260,7 +261,7 @@ func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error {
|
|||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Doer: user,
|
||||
Content: "- " + SecToTime(removedTime),
|
||||
Content: "- " + util.SecToTime(removedTime),
|
||||
Type: CommentTypeDeleteTimeManual,
|
||||
}); err != nil {
|
||||
return err
|
||||
|
@ -289,7 +290,7 @@ func DeleteTime(t *TrackedTime) error {
|
|||
Issue: t.Issue,
|
||||
Repo: t.Issue.Repo,
|
||||
Doer: t.User,
|
||||
Content: "- " + SecToTime(t.Time),
|
||||
Content: "- " + util.SecToTime(t.Time),
|
||||
Type: CommentTypeDeleteTimeManual,
|
||||
}); err != nil {
|
||||
return err
|
||||
|
|
|
@ -34,7 +34,7 @@ func TestAddTime(t *testing.T) {
|
|||
assert.Equal(t, int64(3661), tt.Time)
|
||||
|
||||
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) {
|
||||
|
@ -86,7 +86,7 @@ func TestTotalTimes(t *testing.T) {
|
|||
assert.Len(t, total, 1)
|
||||
for user, time := range total {
|
||||
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})
|
||||
|
@ -94,9 +94,9 @@ func TestTotalTimes(t *testing.T) {
|
|||
assert.Len(t, total, 2)
|
||||
for user, time := range total {
|
||||
if user.ID == 2 {
|
||||
assert.Equal(t, "1h 1min 2s", time)
|
||||
assert.Equal(t, "1 hour 1 minute", time)
|
||||
} else if user.ID == 1 {
|
||||
assert.Equal(t, "20s", time)
|
||||
assert.Equal(t, "20 seconds", time)
|
||||
} else {
|
||||
assert.Error(t, assert.AnError)
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ func TestTotalTimes(t *testing.T) {
|
|||
assert.Len(t, total, 1)
|
||||
for user, time := range total {
|
||||
assert.Equal(t, int64(2), user.ID)
|
||||
assert.Equal(t, "1s", time)
|
||||
assert.Equal(t, "1 second", time)
|
||||
}
|
||||
|
||||
total, err = TotalTimes(&FindTrackedTimesOptions{IssueID: 4})
|
||||
|
|
|
@ -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
|
||||
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}
|
||||
refAction := ref.Action
|
||||
e := db.GetEngine(stdCtx)
|
||||
|
|
|
@ -137,6 +137,7 @@ func QueryIssueContentHistoryEditedCountMap(dbCtx context.Context, issueID int64
|
|||
type IssueContentListItem struct {
|
||||
UserID int64
|
||||
UserName string
|
||||
UserFullName string
|
||||
UserAvatarLink string
|
||||
|
||||
HistoryID int64
|
||||
|
@ -148,7 +149,7 @@ type IssueContentListItem struct {
|
|||
// FetchIssueContentHistoryList fetch list
|
||||
func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int64) ([]*IssueContentListItem, error) {
|
||||
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").
|
||||
Table([]string{"issue_content_history", "h"}).
|
||||
Join("LEFT", []string{"user", "u"}, "h.poster_id = u.id").
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
type User struct {
|
||||
ID int64
|
||||
Name string
|
||||
ID int64
|
||||
Name string
|
||||
FullName string
|
||||
}
|
||||
_ = dbEngine.Sync2(&User{})
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
-
|
||||
id: 1
|
||||
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
|
||||
-
|
||||
id: 2
|
||||
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
|
||||
-
|
||||
id: 3
|
||||
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
|
||||
-
|
||||
id: 4
|
||||
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
|
|
@ -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
|
|
@ -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
|
|
@ -61,7 +61,6 @@ type Version struct {
|
|||
// update minDBVersion accordingly
|
||||
var migrations = []Migration{
|
||||
// Gitea 1.5.0 ends at v69
|
||||
|
||||
// v70 -> v71
|
||||
NewMigration("add issue_dependencies", addIssueDependencies),
|
||||
// v71 -> v72
|
||||
|
@ -367,9 +366,13 @@ var migrations = []Migration{
|
|||
// v206 -> v207
|
||||
NewMigration("Add authorize column to team_unit table", addAuthorizeColForTeamUnit),
|
||||
// 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
|
||||
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
|
||||
|
|
|
@ -166,6 +166,11 @@ func (log *TestLogger) Init(config string) error {
|
|||
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
|
||||
func (log *TestLogger) Flush() {
|
||||
}
|
||||
|
|
|
@ -5,86 +5,11 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/tstranex/u2f"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addWebAuthnCred(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"`
|
||||
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(®s)
|
||||
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]
|
||||
}
|
||||
// NO-OP Don't migrate here - let v210 do this.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -5,46 +5,10 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func useBase32HexForCredIDInWebAuthnCredential(x *xorm.Engine) error {
|
||||
// Create webauthnCredential table
|
||||
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(®s)
|
||||
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]
|
||||
}
|
||||
|
||||
// noop
|
||||
return nil
|
||||
}
|
||||
|
|
17
models/migrations/v209.go
Normal file
17
models/migrations/v209.go
Normal 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
178
models/migrations/v210.go
Normal 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(®s)
|
||||
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
|
||||
}
|
75
models/migrations/v210_test.go
Normal file
75
models/migrations/v210_test.go
Normal 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)
|
||||
}
|
|
@ -498,14 +498,15 @@ func (n *Notification) APIURL() string {
|
|||
type NotificationList []*Notification
|
||||
|
||||
// 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++ {
|
||||
err = nl[i].LoadAttributes()
|
||||
if err != nil && !IsErrCommentNotExist(err) {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nl NotificationList) getPendingRepoIDs() []int64 {
|
||||
|
|
|
@ -331,9 +331,7 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
|
|||
return fmt.Errorf("%s is a user not an organization", org.Name)
|
||||
}
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
|
||||
if err := deleteBeans(e,
|
||||
if err := db.DeleteBeans(ctx,
|
||||
&Team{OrgID: org.ID},
|
||||
&OrgUser{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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -123,7 +122,8 @@ func NewRepoContext() {
|
|||
loadRepoConfig()
|
||||
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
|
||||
|
@ -150,27 +150,56 @@ func getRepoAssignees(ctx context.Context, repo *repo_model.Repository) (_ []*us
|
|||
}
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
accesses := make([]*Access, 0, 10)
|
||||
if err = e.
|
||||
userIDs := make([]int64, 0, 10)
|
||||
if err = e.Table("access").
|
||||
Where("repo_id = ? AND mode >= ?", repo.ID, perm.AccessModeWrite).
|
||||
Find(&accesses); err != nil {
|
||||
Select("user_id").
|
||||
Find(&userIDs); err != nil {
|
||||
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
|
||||
// and just waste 1 unit is cheaper than re-allocate memory once.
|
||||
users := make([]*user_model.User, 0, len(accesses)+1)
|
||||
if len(accesses) > 0 {
|
||||
userIDs := make([]int64, len(accesses))
|
||||
for i := 0; i < len(accesses); i++ {
|
||||
userIDs[i] = accesses[i].UserID
|
||||
}
|
||||
|
||||
users := make([]*user_model.User, 0, len(userIDs)+1)
|
||||
if len(userIDs) > 0 {
|
||||
if err = e.In("id", userIDs).Find(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !repo.Owner.IsOrganization() {
|
||||
if !repo.Owner.IsOrganization() && !uidMap[repo.OwnerID] {
|
||||
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{
|
||||
RepoID: repo.ID,
|
||||
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 {
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
|
@ -765,7 +794,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := deleteBeans(sess,
|
||||
if err := db.DeleteBeans(ctx,
|
||||
&Access{RepoID: repo.ID},
|
||||
&Action{RepoID: repo.ID},
|
||||
&Collaboration{RepoID: repoID},
|
||||
|
@ -927,28 +956,28 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
|
|||
}
|
||||
|
||||
// Remove archives
|
||||
for i := range archivePaths {
|
||||
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archivePaths[i])
|
||||
for _, archive := range archivePaths {
|
||||
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archive)
|
||||
}
|
||||
|
||||
// Remove lfs objects
|
||||
for i := range lfsPaths {
|
||||
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsPaths[i])
|
||||
for _, lfsObj := range lfsPaths {
|
||||
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsObj)
|
||||
}
|
||||
|
||||
// Remove issue attachment files.
|
||||
for i := range attachmentPaths {
|
||||
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachmentPaths[i])
|
||||
for _, attachment := range attachmentPaths {
|
||||
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachment)
|
||||
}
|
||||
|
||||
// Remove release attachment files.
|
||||
for i := range releaseAttachments {
|
||||
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachments[i])
|
||||
for _, releaseAttachment := range releaseAttachments {
|
||||
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachment)
|
||||
}
|
||||
|
||||
// Remove attachment with no issue_id and release_id.
|
||||
for i := range newAttachmentPaths {
|
||||
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachmentPaths[i])
|
||||
for _, newAttachment := range newAttachmentPaths {
|
||||
admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", newAttachment)
|
||||
}
|
||||
|
||||
if len(repo.Avatar) > 0 {
|
||||
|
|
|
@ -120,11 +120,12 @@ func DeleteMirrorByRepoID(repoID int64) error {
|
|||
}
|
||||
|
||||
// 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).
|
||||
Where("next_update_unix<=?", time.Now().Unix()).
|
||||
And("next_update_unix!=0").
|
||||
OrderBy("updated_unix ASC").
|
||||
Limit(limit).
|
||||
Iterate(new(Mirror), f)
|
||||
}
|
||||
|
||||
|
|
|
@ -101,10 +101,11 @@ func GetPushMirrorsByRepoID(repoID int64) ([]*PushMirror, error) {
|
|||
}
|
||||
|
||||
// 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).
|
||||
Where("last_update + (`interval` / ?) <= ?", time.Second, time.Now().Unix()).
|
||||
And("`interval` != 0").
|
||||
OrderBy("last_update ASC").
|
||||
Limit(limit).
|
||||
Iterate(new(PushMirror), f)
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ func TestPushMirrorsIterate(t *testing.T) {
|
|||
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
||||
PushMirrorsIterate(func(idx int, bean interface{}) error {
|
||||
PushMirrorsIterate(1, func(idx int, bean interface{}) error {
|
||||
m, ok := bean.(*PushMirror)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test-1", m.RemoteName)
|
||||
|
|
|
@ -290,7 +290,14 @@ func (repo *Repository) LoadUnits(ctx context.Context) (err error) {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -513,7 +520,7 @@ func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
|
|||
desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
URLPrefix: repo.HTMLURL(),
|
||||
Metas: repo.ComposeMetas(),
|
||||
// Don't use Metas to speedup requests
|
||||
}, repo.Description)
|
||||
if err != nil {
|
||||
log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
|
||||
|
|
|
@ -115,12 +115,15 @@ type PullRequestsConfig struct {
|
|||
AllowSquash bool
|
||||
AllowManualMerge bool
|
||||
AutodetectManualMerge bool
|
||||
AllowRebaseUpdate bool
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
DefaultMergeStyle MergeStyle
|
||||
}
|
||||
|
||||
// FromDB fills up a PullRequestsConfig from serialized format.
|
||||
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
|
||||
// AllowRebaseUpdate = true as default for existing PullRequestConfig in DB
|
||||
cfg.AllowRebaseUpdate = true
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -29,16 +29,6 @@ func GetOrganizationCount(ctx context.Context, u *user_model.User) (int64, error
|
|||
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.
|
||||
func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
|
||||
e := db.GetEngine(ctx)
|
||||
|
@ -82,7 +72,7 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
|
|||
}
|
||||
// ***** END: Follow *****
|
||||
|
||||
if err = deleteBeans(e,
|
||||
if err = db.DeleteBeans(ctx,
|
||||
&AccessToken{UID: u.ID},
|
||||
&Collaboration{UserID: u.ID},
|
||||
&Access{UserID: u.ID},
|
||||
|
|
|
@ -30,13 +30,19 @@ func (users UserList) GetTwoFaStatus() map[int64]bool {
|
|||
for _, user := range users {
|
||||
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 {
|
||||
results[token.UID] = true
|
||||
}
|
||||
}
|
||||
|
||||
if ids, err := users.userIDsWithWebAuthn(db.GetEngine(db.DefaultContext)); err == nil {
|
||||
for _, id := range ids {
|
||||
results[id] = true
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
|
@ -47,15 +53,23 @@ func (users UserList) loadTwoFactorStatus(e db.Engine) (map[int64]*auth.TwoFacto
|
|||
|
||||
userIDs := users.GetUserIDs()
|
||||
tokenMaps := make(map[int64]*auth.TwoFactor, len(userIDs))
|
||||
err := e.
|
||||
In("uid", userIDs).
|
||||
Find(&tokenMaps)
|
||||
if err != nil {
|
||||
if err := e.In("uid", userIDs).Find(&tokenMaps); err != nil {
|
||||
return nil, fmt.Errorf("find two factor: %v", err)
|
||||
}
|
||||
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.
|
||||
func GetUsersByIDs(ids []int64) (UserList, error) {
|
||||
ous := make([]*User, 0, len(ids))
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
// SearchUserOptions contains the options for searching
|
||||
type SearchUserOptions struct {
|
||||
db.ListOptions
|
||||
|
||||
Keyword string
|
||||
Type UserType
|
||||
UID int64
|
||||
|
@ -33,6 +34,8 @@ type SearchUserOptions struct {
|
|||
IsRestricted util.OptionalBool
|
||||
IsTwoFactorEnabled util.OptionalBool
|
||||
IsProhibitLogin util.OptionalBool
|
||||
|
||||
ExtraParamStrings map[string]string
|
||||
}
|
||||
|
||||
func (opts *SearchUserOptions) toSearchQueryBase() *xorm.Session {
|
||||
|
|
|
@ -827,8 +827,9 @@ func validateUser(u *User) error {
|
|||
return ValidateEmail(u.Email)
|
||||
}
|
||||
|
||||
func updateUser(ctx context.Context, u *User, changePrimaryEmail bool) error {
|
||||
if err := validateUser(u); err != nil {
|
||||
func updateUser(ctx context.Context, u *User, changePrimaryEmail bool, cols ...string) error {
|
||||
err := validateUser(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -860,15 +861,35 @@ func updateUser(ctx context.Context, u *User, changePrimaryEmail bool) error {
|
|||
}); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// UpdateUser updates user's information.
|
||||
func UpdateUser(u *User, emailChanged bool) error {
|
||||
return updateUser(db.DefaultContext, u, emailChanged)
|
||||
func UpdateUser(u *User, emailChanged bool, cols ...string) error {
|
||||
return updateUser(db.DefaultContext, u, emailChanged, cols...)
|
||||
}
|
||||
|
||||
// UpdateUserCols update user according special columns
|
||||
|
@ -1117,19 +1138,9 @@ func GetUserByEmailContext(ctx context.Context, email string) (*User, error) {
|
|||
}
|
||||
|
||||
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
|
||||
emailAddress := &EmailAddress{Email: email, IsActivated: true}
|
||||
has, err = db.GetEngine(ctx).Get(emailAddress)
|
||||
emailAddress := &EmailAddress{LowerEmail: email, IsActivated: true}
|
||||
has, err := db.GetEngine(ctx).Get(emailAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -235,6 +235,20 @@ func TestCreateUserInvalidEmail(t *testing.T) {
|
|||
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) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ func EscapeControlBytes(text []byte) (EscapeStatus, []byte) {
|
|||
func EscapeControlReader(text io.Reader, output io.Writer) (escaped EscapeStatus, err error) {
|
||||
buf := make([]byte, 4096)
|
||||
readStart := 0
|
||||
runeCount := 0
|
||||
var n int
|
||||
var writePos int
|
||||
|
||||
|
@ -74,10 +75,13 @@ readingloop:
|
|||
for err == nil {
|
||||
n, err = text.Read(buf[readStart:])
|
||||
bs := buf[:n+readStart]
|
||||
n = len(bs)
|
||||
i := 0
|
||||
|
||||
for i < len(bs) {
|
||||
r, size := utf8.DecodeRune(bs[i:])
|
||||
runeCount++
|
||||
|
||||
// Now handle the codepoints
|
||||
switch {
|
||||
case r == utf8.RuneError:
|
||||
|
@ -112,6 +116,8 @@ readingloop:
|
|||
lineHasRTLScript = false
|
||||
lineHasLTRScript = false
|
||||
|
||||
case runeCount == 1 && r == 0xFEFF: // UTF BOM
|
||||
// the first BOM is safe
|
||||
case r == '\r' || r == '\t' || r == ' ':
|
||||
// These are acceptable control characters and space characters
|
||||
case unicode.IsSpace(r):
|
||||
|
|
|
@ -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",
|
||||
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) {
|
||||
|
@ -163,10 +171,18 @@ func TestEscapeControlReader(t *testing.T) {
|
|||
// lets add some control characters to the tests
|
||||
tests := make([]escapeControlTest, 0, len(escapeControlTests)*3)
|
||||
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 {
|
||||
test.name += " (+Control)"
|
||||
test.text = "\u001E" + test.text
|
||||
test.result = `<span class="escaped-code-point" data-escaped="[U+001E]"><span class="char">` + "\u001e" + `</span></span>` + test.result
|
||||
test.text = addPrefix("\u001E", test.text)
|
||||
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.HasControls = true
|
||||
tests = append(tests, test)
|
||||
|
@ -174,8 +190,8 @@ func TestEscapeControlReader(t *testing.T) {
|
|||
|
||||
for _, test := range escapeControlTests {
|
||||
test.name += " (+Mark)"
|
||||
test.text = "\u0300" + test.text
|
||||
test.result = `<span class="escaped-code-point" data-escaped="[U+0300]"><span class="char">` + "\u0300" + `</span></span>` + test.result
|
||||
test.text = addPrefix("\u0300", test.text)
|
||||
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.HasMarks = true
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -129,7 +129,23 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
|
|||
|
||||
// Team.
|
||||
if ctx.Org.IsMember {
|
||||
shouldSeeAllTeams := false
|
||||
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()
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadTeams", err)
|
||||
|
|
|
@ -914,7 +914,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
|
|||
|
||||
if refType == RepoRefLegacy {
|
||||
// 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.Repo.RepoLink,
|
||||
|
|
|
@ -158,6 +158,8 @@ func (c *Command) RunWithContext(rc *RunContext) error {
|
|||
fmt.Sprintf("LC_ALL=%s", DefaultLocale),
|
||||
// avoid prompting for credentials interactively, supported since git v2.3
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
// ignore replace references (https://git-scm.com/docs/git-replace)
|
||||
"GIT_NO_REPLACE_OBJECTS=1",
|
||||
)
|
||||
|
||||
cmd.Dir = rc.Dir
|
||||
|
|
|
@ -80,21 +80,20 @@ func InitRepository(ctx context.Context, repoPath string, bare bool) error {
|
|||
// IsEmpty Check if repository is empty.
|
||||
func (repo *Repository) IsEmpty() (bool, error) {
|
||||
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{
|
||||
Timeout: -1,
|
||||
Dir: repo.Path,
|
||||
Stdout: &output,
|
||||
Stderr: &errbuf,
|
||||
}); err != nil {
|
||||
if err.Error() == "exit status 1" && errbuf.String() == "" {
|
||||
return true, nil
|
||||
}
|
||||
return true, fmt.Errorf("check empty: %v - %s", err, errbuf.String())
|
||||
}
|
||||
|
||||
c, err := strconv.Atoi(strings.TrimSpace(output.String()))
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("check empty: convert %s to count failed: %v", output.String(), err)
|
||||
}
|
||||
return c == 0, nil
|
||||
return strings.TrimSpace(output.String()) == "", nil
|
||||
}
|
||||
|
||||
// CloneRepoOptions options when clone a repository
|
||||
|
|
|
@ -185,7 +185,8 @@ func (c *CheckAttributeReader) Init(ctx context.Context) error {
|
|||
// Run run cmd
|
||||
func (c *CheckAttributeReader) Run() error {
|
||||
defer func() {
|
||||
_ = c.Close()
|
||||
_ = c.stdinReader.Close()
|
||||
_ = c.stdOut.Close()
|
||||
}()
|
||||
stdErr := new(bytes.Buffer)
|
||||
err := c.cmd.RunWithContext(&RunContext{
|
||||
|
@ -196,14 +197,19 @@ func (c *CheckAttributeReader) Run() error {
|
|||
Stdout: c.stdOut,
|
||||
Stderr: stdErr,
|
||||
PipelineFunc: func(_ context.Context, _ context.CancelFunc) error {
|
||||
close(c.running)
|
||||
select {
|
||||
case <-c.running:
|
||||
default:
|
||||
close(c.running)
|
||||
}
|
||||
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 nil
|
||||
}
|
||||
|
||||
|
@ -243,10 +249,8 @@ func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err
|
|||
|
||||
// Close close pip after use
|
||||
func (c *CheckAttributeReader) Close() error {
|
||||
err := c.stdinWriter.Close()
|
||||
_ = c.stdinReader.Close()
|
||||
_ = c.stdOut.Close()
|
||||
c.cancel()
|
||||
err := c.stdinWriter.Close()
|
||||
select {
|
||||
case <-c.running:
|
||||
default:
|
||||
|
|
|
@ -88,7 +88,10 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
|||
}
|
||||
}()
|
||||
}
|
||||
defer cancel()
|
||||
defer func() {
|
||||
_ = checker.Close()
|
||||
cancel()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -193,6 +193,7 @@ func (g *Manager) RunAtHammer(hammer func()) {
|
|||
|
||||
func (g *Manager) doShutdown() {
|
||||
if !g.setStateTransition(stateRunning, stateShuttingDown) {
|
||||
g.DoImmediateHammer()
|
||||
return
|
||||
}
|
||||
g.lock.Lock()
|
||||
|
|
|
@ -168,8 +168,12 @@ func (g *Manager) DoGracefulRestart() {
|
|||
if setting.GracefulRestartable {
|
||||
log.Info("PID: %d. Forking...", os.Getpid())
|
||||
err := g.doFork()
|
||||
if err != nil && err.Error() != "another process already forked. Ignoring this one" {
|
||||
log.Error("Error whilst forking from PID: %d : %v", os.Getpid(), err)
|
||||
if err != nil {
|
||||
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 {
|
||||
log.Info("PID: %d. Not set restartable. Shutting down...", os.Getpid())
|
||||
|
|
|
@ -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,
|
||||
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
|
||||
if setting.Indexer.ExcludeVendored && analyze.IsVendor(update.Filename) {
|
||||
return nil
|
||||
|
|
|
@ -37,6 +37,9 @@ func (db *DBIndexer) Index(id int64) error {
|
|||
|
||||
gitRepo, err := git.OpenRepositoryCtx(ctx, repo.RepoPath())
|
||||
if err != nil {
|
||||
if err.Error() == "no such file or directory" {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
|
72
modules/log/buffer.go
Normal file
72
modules/log/buffer.go
Normal 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)
|
||||
}
|
64
modules/log/buffer_test.go
Normal file
64
modules/log/buffer_test.go
Normal 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)
|
||||
}
|
|
@ -119,6 +119,11 @@ func (log *ConnLogger) Init(jsonconfig string) error {
|
|||
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
|
||||
func (log *ConnLogger) Flush() {
|
||||
}
|
||||
|
|
|
@ -66,6 +66,11 @@ func (log *ConsoleLogger) Init(config string) error {
|
|||
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
|
||||
func (log *ConsoleLogger) Flush() {
|
||||
}
|
||||
|
|
|
@ -216,6 +216,12 @@ func (m *MultiChannelledLog) GetEventLogger(name string) EventLogger {
|
|||
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
|
||||
func (m *MultiChannelledLog) GetEventLoggerNames() []string {
|
||||
m.rwmutex.RLock()
|
||||
|
|
|
@ -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.
|
||||
// there are no buffering messages in file logger in memory.
|
||||
// flush file means sync file from disk.
|
||||
|
|
|
@ -7,6 +7,7 @@ package log
|
|||
// LoggerProvider represents behaviors of a logger provider.
|
||||
type LoggerProvider interface {
|
||||
Init(config string) error
|
||||
Content() (string, error)
|
||||
EventLogger
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
func (log *SMTPLogger) Flush() {
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
if _, err := io.WriteString(w, "<"); err != nil {
|
||||
return err
|
||||
|
|
5
modules/markup/external/external.go
vendored
5
modules/markup/external/external.go
vendored
|
@ -54,6 +54,11 @@ func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
|||
return p.MarkupSanitizerRules
|
||||
}
|
||||
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
func (p *Renderer) SanitizerDisabled() bool {
|
||||
return p.DisableSanitizer
|
||||
}
|
||||
|
||||
func envMark(envName string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "%" + envName + "%"
|
||||
|
|
|
@ -99,7 +99,7 @@ var issueFullPatternOnce sync.Once
|
|||
func getIssueFullPattern() *regexp.Regexp {
|
||||
issueFullPatternOnce.Do(func() {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -97,6 +97,15 @@ func TestRender_CrossReferences(t *testing.T) {
|
|||
test(
|
||||
"/home/gitea/go-gitea/gitea#12345",
|
||||
`<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) {
|
||||
|
|
|
@ -221,6 +221,11 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
|||
return []setting.MarkupSanitizerRule{}
|
||||
}
|
||||
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
func (Renderer) SanitizerDisabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Render implements markup.Renderer
|
||||
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
return render(ctx, input, output)
|
||||
|
|
|
@ -47,6 +47,11 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
|||
return []setting.MarkupSanitizerRule{}
|
||||
}
|
||||
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
func (Renderer) SanitizerDisabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Render renders orgmode rawbytes to HTML
|
||||
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
htmlWriter := org.NewHTMLWriter()
|
||||
|
|
|
@ -81,6 +81,7 @@ type Renderer interface {
|
|||
Extensions() []string
|
||||
NeedPostProcess() bool
|
||||
SanitizerRules() []setting.MarkupSanitizerRule
|
||||
SanitizerDisabled() bool
|
||||
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
|
||||
}
|
||||
|
||||
type nopCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (nopCloser) Close() error { return nil }
|
||||
|
||||
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
var wg sync.WaitGroup
|
||||
var err error
|
||||
|
@ -136,18 +143,25 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
|||
_ = pw.Close()
|
||||
}()
|
||||
|
||||
pr2, pw2 := io.Pipe()
|
||||
defer func() {
|
||||
_ = pr2.Close()
|
||||
_ = pw2.Close()
|
||||
}()
|
||||
var pr2 io.ReadCloser
|
||||
var pw2 io.WriteCloser
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err = SanitizeReader(pr2, renderer.Name(), output)
|
||||
_ = pr2.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
if !renderer.SanitizerDisabled() {
|
||||
pr2, pw2 = io.Pipe()
|
||||
defer func() {
|
||||
_ = pr2.Close()
|
||||
_ = pw2.Close()
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err = SanitizeReader(pr2, renderer.Name(), output)
|
||||
_ = pr2.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
} else {
|
||||
pw2 = nopCloser{output}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
|
|
|
@ -9,7 +9,8 @@ import "time"
|
|||
|
||||
// Comment is a standard comment information
|
||||
type Comment struct {
|
||||
IssueIndex int64 `yaml:"issue_index"`
|
||||
IssueIndex int64 `yaml:"issue_index"`
|
||||
Index int64
|
||||
PosterID int64 `yaml:"poster_id"`
|
||||
PosterName string `yaml:"poster_name"`
|
||||
PosterEmail string `yaml:"poster_email"`
|
||||
|
|
|
@ -5,10 +5,13 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
|
@ -20,8 +23,16 @@ func (m *Manager) CloseLevelDB(connection string) error {
|
|||
defer m.mutex.Unlock()
|
||||
db, ok := m.LevelDBConnections[connection]
|
||||
if !ok {
|
||||
connection = ToLevelDBURI(connection).String()
|
||||
db, ok = m.LevelDBConnections[connection]
|
||||
// Try the full URI
|
||||
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 {
|
||||
return nil
|
||||
|
@ -40,6 +51,12 @@ func (m *Manager) CloseLevelDB(connection string) error {
|
|||
|
||||
// GetLevelDB gets a levelDB for a particular connection
|
||||
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()
|
||||
defer m.mutex.Unlock()
|
||||
db, ok := m.LevelDBConnections[connection]
|
||||
|
@ -48,12 +65,28 @@ func (m *Manager) GetLevelDB(connection string) (*leveldb.DB, error) {
|
|||
|
||||
return db.db, nil
|
||||
}
|
||||
uri := ToLevelDBURI(connection)
|
||||
db = &levelDBHolder{
|
||||
name: []string{connection, uri.String()},
|
||||
|
||||
db, ok = m.LevelDBConnections[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{}
|
||||
for k, v := range uri.Query() {
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
|
|
|
@ -91,7 +91,8 @@ func (a *actionNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue *m
|
|||
|
||||
// NotifyCreateIssueComment notifies comment on an issue to notifiers
|
||||
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{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
|
|
|
@ -22,6 +22,7 @@ type Notifier interface {
|
|||
NotifyTransferRepository(doer *user_model.User, repo *repo_model.Repository, oldOwnerName string)
|
||||
NotifyNewIssue(issue *models.Issue, mentions []*user_model.User)
|
||||
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)
|
||||
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)
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
||||
// NotifyDeleteIssue notify when some issue deleted
|
||||
func (*NullNotifier) NotifyDeleteIssue(doer *user_model.User, issue *models.Issue) {
|
||||
}
|
||||
|
||||
// NotifyNewPullRequest places a place holder function
|
||||
func (*NullNotifier) NotifyNewPullRequest(pr *models.PullRequest, mentions []*user_model.User) {
|
||||
}
|
||||
|
|
|
@ -30,7 +30,8 @@ func NewNotifier() base.Notifier {
|
|||
}
|
||||
|
||||
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 issue.Comments == nil {
|
||||
if err := issue.LoadDiscussComments(); err != nil {
|
||||
|
|
|
@ -29,7 +29,8 @@ func NewNotifier() base.Notifier {
|
|||
}
|
||||
|
||||
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))
|
||||
defer finished()
|
||||
|
||||
|
|
|
@ -39,7 +39,8 @@ func NewContext() {
|
|||
|
||||
// NotifyCreateIssueComment notifies issue comment related message to notifiers
|
||||
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 {
|
||||
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
|
||||
func NotifyMergePullRequest(pr *models.PullRequest, doer *user_model.User) {
|
||||
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
|
||||
func NotifyIssueChangeLabels(doer *user_model.User, issue *models.Issue,
|
||||
addedLabels, removedLabels []*models.Label) {
|
||||
addedLabels, removedLabels []*models.Label,
|
||||
) {
|
||||
for _, notifier := range notifiers {
|
||||
notifier.NotifyIssueChangeLabels(doer, issue, addedLabels, removedLabels)
|
||||
}
|
||||
|
|
|
@ -53,7 +53,8 @@ func (ns *notificationService) Run() {
|
|||
}
|
||||
|
||||
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{
|
||||
IssueID: issue.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) {
|
||||
if !removed {
|
||||
if !removed && doer.ID != assignee.ID {
|
||||
opts := issueNotificationOpts{
|
||||
IssueID: issue.ID,
|
||||
NotificationAuthorID: doer.ID,
|
||||
|
|
|
@ -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,
|
||||
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)
|
||||
|
||||
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,
|
||||
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))
|
||||
defer finished()
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ type MarkupRenderer struct {
|
|||
IsInputFile bool
|
||||
NeedPostProcess bool
|
||||
MarkupSanitizerRules []MarkupSanitizerRule
|
||||
DisableSanitizer bool
|
||||
}
|
||||
|
||||
// MarkupSanitizerRule defines the policy for whitelisting attributes on
|
||||
|
@ -144,11 +145,12 @@ func newMarkupRenderer(name string, sec *ini.Section) {
|
|||
}
|
||||
|
||||
ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
|
||||
Enabled: sec.Key("ENABLED").MustBool(false),
|
||||
MarkupName: name,
|
||||
FileExtensions: exts,
|
||||
Command: command,
|
||||
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
|
||||
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
|
||||
Enabled: sec.Key("ENABLED").MustBool(false),
|
||||
MarkupName: name,
|
||||
FileExtensions: exts,
|
||||
Command: command,
|
||||
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
|
||||
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
Loading…
Add table
Reference in a new issue