diff --git a/.drone.yml b/.drone.yml index 1e21c43f39..8a73e84a00 100644 --- a/.drone.yml +++ b/.drone.yml @@ -626,6 +626,8 @@ steps: commit_message: "[skip ci] Updated translations via Crowdin" remote: "git@github.com:go-gitea/gitea.git" environment: + DRONE_COMMIT_AUTHOR_EMAIL: "teabot@gitea.io" + DRONE_COMMIT_AUTHOR: GiteaBot GIT_PUSH_SSH_KEY: from_secret: git_push_ssh_key @@ -670,12 +672,14 @@ steps: pull: always settings: author_email: "teabot@gitea.io" - author_name: GiteaBot + author_name: "GiteaBot" branch: main commit: true - commit_message: "[skip ci] Updated licenses and gitignores " + commit_message: "[skip ci] Updated licenses and gitignores" remote: "git@github.com:go-gitea/gitea.git" environment: + DRONE_COMMIT_AUTHOR_EMAIL: "teabot@gitea.io" + DRONE_COMMIT_AUTHOR: "GiteaBot" GIT_PUSH_SSH_KEY: from_secret: git_push_ssh_key diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 66d4452ec5..4dafc8f492 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -35,9 +35,6 @@ overrides: rules: import/no-unresolved: [0] import/no-extraneous-dependencies: [0] - - files: ["*.test.js"] - env: - jest: true - files: ["*.config.js"] rules: import/no-unused-modules: [0] diff --git a/.gitpod.yml b/.gitpod.yml index 55a12cc9e4..6dc6bb513d 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,6 +1,7 @@ tasks: - name: Setup init: | + cp -r contrib/ide/vscode .vscode make deps make build command: | @@ -31,6 +32,7 @@ vscode: - DavidAnson.vscode-markdownlint - johnsoncodehk.volar - ms-azuretools.vscode-docker + - zixuanchen.vitest-explorer ports: - name: Gitea diff --git a/.golangci.yml b/.golangci.yml index 0e796a2016..99133badd9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,7 +12,6 @@ linters: - dupl #- gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time. - gofmt - - misspell - gocritic - bidichk - ineffassign @@ -148,9 +147,6 @@ issues: - path: models/issue_comment_list.go linters: - dupl - - linters: - - misspell - text: '`Unknwon` is a misspelling of `Unknown`' - path: models/update.go linters: - unused diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7fbac1bf..1150d70081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,86 @@ This changelog goes through all the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.io). +## [1.17.3](https://github.com/go-gitea/gitea/releases/tag/v1.17.3) - 2022-10-15 + +* SECURITY + * Sanitize and Escape refs in git backend (#21464) (#21463) + * Bump `golang.org/x/text` (#21412) (#21413) + * Update bluemonday (#21281) (#21287) +* ENHANCEMENTS + * Fix empty container layer history and UI (#21251) (#21278) + * Use en-US as fallback when using other default language (#21200) (#21256) + * Make the vscode clone link respect transport protocol (#20557) (#21128) +* BUGFIXES + * Do DB update after merge in hammer context (#21401) (#21416) + * Add Num{Issues,Pulls} stats checks (#21404) (#21414) + * Stop logging CheckPath returns error: context canceled (#21064) (#21405) + * Parse OAuth Authorization header when request omits client secret (#21351) (#21374) + * Ignore port for loopback redirect URIs (#21293) (#21373) + * Set SemverCompatible to false for Conan packages (#21275) (#21366) + * Tag list should include draft releases with existing tags (#21263) (#21365) + * Fix linked account translation (#21331) (#21334) + * Make NuGet service index publicly accessible (#21242) (#21277) + * Foreign ID conflicts if ID is 0 for each item (#21271) (#21272) + * Use absolute links in feeds (#21229) (#21265) + * Prevent invalid behavior for file reviewing when loading more files (#21230) (#21234) + * Respect `REQUIRE_SIGNIN_VIEW` for packages (#20873) (#21232) + * Treat git object mode 40755 as directory (#21195) (#21218) + * Allow uppercase ASCII alphabet in PyPI package names (#21095) (#21217) + * Fix limited user cannot view himself's profile (#21212) + * Fix template bug of admin monitor (#21209) + * Fix reaction of issues (#21185) (#21196) + * Fix CSV diff for added/deleted files (#21189) (#21193) + * Fix pagination limit parameter problem (#21111) +* TESTING + * Fix missing m.Run() in TestMain (#21341) +* BUILD + * Use Go 1.19 fmt for Gitea 1.17, sync emoji data (#21239) + +## [1.17.2](https://github.com/go-gitea/gitea/releases/tag/v1.17.2) - 2022-09-06 + +* SECURITY + * Double check CloneURL is acceptable (#20869) (#20892) + * Add more checks in migration code (#21011) (#21050) +* ENHANCEMENTS + * Fix hard-coded timeout and error panic in API archive download endpoint (#20925) (#21051) + * Improve arc-green code theme (#21039) (#21042) + * Enable contenthash in filename for dynamic assets (#20813) (#20932) + * Don't open new page for ext wiki on same repository (#20725) (#20910) + * Disable doctor logging on panic (#20847) (#20898) + * Remove calls to load Mirrors in user.Dashboard (#20855) (#20897) + * Update codemirror to 5.65.8 (#20875) + * Rework repo buttons (#20602, #20718) (#20719) +* BUGFIXES + * Ensure delete user deletes all comments (#21067) (#21068) + * Delete unreferenced packages when deleting a package version (#20977) (#21060) + * Redirect if user does not exist on admin pages (#20981) (#21059) + * Set uploadpack.allowFilter etc on gitea serv to enable partial clones with ssh (#20902) (#21058) + * Fix 500 on time in timeline API (#21052) (#21057) + * Fill the specified ref in webhook test payload (#20961) (#21055) + * Add another index for Action table on postgres (#21033) (#21054) + * Fix broken insecureskipverify handling in redis connection uris (#20967) (#21053) + * Add Dev, Peer and Optional dependencies to npm PackageMetadataVersion (#21017) (#21044) + * Do not add links to Posters or Assignees with ID < 0 (#20577) (#21037) + * Fix modified due date message (#20388) (#21032) + * Fix missed sort bug (#21006) + * Fix input.value attr for RequiredClaimName/Value (#20946) (#21001) + * Change review buttons to icons to make space for text (#20934) (#20978) + * Fix download archiver of a commit (#20962) (#20971) + * Return 404 NotFound if requested attachment does not exist (#20886) (#20941) + * Set no-tags in git fetch on compare (#20893) (#20936) + * Allow multiple metadata files for Maven packages (#20674) (#20916) + * Increase Content field size of gpg_key and public_key to MEDIUMTEXT (#20896) (#20911) + * Fix mirror address setting not working (#20850) (#20904) + * Fix push mirror address backend get error Address cause setting page display error (#20593) (#20901) + * Fix panic when an invalid oauth2 name is passed (#20820) (#20900) + * In PushMirrorsIterate and MirrorsIterate if limit is negative do not set it (#20837) (#20899) + * Ensure that graceful start-up is informed of unused SSH listener (#20877) (#20888) + * Pad GPG Key ID with preceding zeroes (#20878) (#20885) + * Fix SQL Query for `SearchTeam` (#20844) (#20872) + * Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867) + * Fix UI mis-align for PR commit history (#20845) (#20859) + ## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/1.17.1) - 2022-08-17 * SECURITY diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4aff27fc65..dbe418c356 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,6 +146,16 @@ If your PR could cause a breaking change you must add a BREAKING section to this To explain how this could affect users and how to mitigate these changes. +Once code review starts on your PR, do not rebase nor squash your branch as it makes it +difficult to review the new changes. Only if there is a need, sync your branch by merging +the base branch into yours. Don't worry about merge commits messing up your tree as +the final merge process squashes all commits into one, with the visible commit message (first +line) being the PR title + PR index and description being the PR's first comment. + +Once your PR gets the `lgtm/done` label, don't worry about keeping it up-to-date or breaking +builds (unless there's a merge conflict or a request is made by a maintainer to make +modifications). It is the maintainer team's responsibility from this point to get it merged. + ## Styleguide For imports you should use the following format (*without* the comments) diff --git a/MAINTAINERS b/MAINTAINERS index 8c4af74323..660f35607c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -49,3 +49,4 @@ silentcode (@silentcodeg) Wim (@42wim) xinyu (@penlinux) Jason Song (@wolfogre) +Yarden Shoham (@yardenshoham) diff --git a/Makefile b/Makefile index 9828275758..f1b6790dc5 100644 --- a/Makefile +++ b/Makefile @@ -130,6 +130,7 @@ TEST_TAGS ?= sqlite sqlite_unlock_notify TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMANTIC_WORK_DIR)/node_modules $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR) GO_DIRS := cmd tests models modules routers build services tools +WEB_DIRS := web_src/js web_src/less GO_SOURCES := $(wildcard *.go) GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" -not -path modules/options/bindata.go -not -path modules/public/bindata.go -not -path modules/templates/bindata.go) @@ -263,11 +264,24 @@ clean: .PHONY: fmt fmt: - @MISSPELL_PACKAGE=$(MISSPELL_PACKAGE) GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}' + GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}' $(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl')) @# strip whitespace after '{{' and before `}}` unless there is only whitespace before it @$(SED_INPLACE) -e 's/{{[ ]\{1,\}/{{/g' -e '/^[ ]\{1,\}}}/! s/[ ]\{1,\}}}/}}/g' $(TEMPLATES) +.PHONY: fmt-check +fmt-check: fmt + @diff=$$(git diff $(GO_SOURCES) templates $(WEB_DIRS)); \ + if [ -n "$$diff" ]; then \ + echo "Please run 'make fmt' and commit the result:"; \ + echo "$${diff}"; \ + exit 1; \ + fi + +.PHONY: misspell-check +misspell-check: + go run $(MISSPELL_PACKAGE) -error $(GO_DIRS) $(WEB_DIRS) + .PHONY: vet vet: @echo "Running go vet..." @@ -311,22 +325,6 @@ errcheck: @echo "Running errcheck..." $(GO) run $(ERRCHECK_PACKAGE) $(GO_PACKAGES) -.PHONY: fmt-check -fmt-check: - @# get all go files and run gitea-fmt (with gofmt) on them - @diff=$$(MISSPELL_PACKAGE=$(MISSPELL_PACKAGE) GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -l '{file-list}'); \ - if [ -n "$$diff" ]; then \ - echo "Please run 'make fmt' and commit the result:"; \ - echo "$${diff}"; \ - exit 1; \ - fi - @diff2=$$(git diff templates); \ - if [ -n "$$diff2" ]; then \ - echo "Please run 'make fmt' and commit the result:"; \ - echo "$${diff2}"; \ - exit 1; \ - fi - .PHONY: checks checks: checks-frontend checks-backend @@ -334,7 +332,7 @@ checks: checks-frontend checks-backend checks-frontend: lockfile-check svg-check .PHONY: checks-backend -checks-backend: tidy-check swagger-check swagger-validate +checks-backend: tidy-check swagger-check fmt-check misspell-check swagger-validate .PHONY: lint lint: lint-frontend lint-backend @@ -372,7 +370,7 @@ test-backend: .PHONY: test-frontend test-frontend: node_modules - @NODE_OPTIONS="--experimental-vm-modules --no-warnings" npx jest --color + npx vitest .PHONY: test-check test-check: diff --git a/README.md b/README.md index 115cafb58d..19703d85dd 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ Support this project by becoming a sponsor. Your logo will show up here with a l + ## FAQ diff --git a/build/code-batch-process.go b/build/code-batch-process.go index 24b13470ab..b6c4171ede 100644 --- a/build/code-batch-process.go +++ b/build/code-batch-process.go @@ -20,7 +20,7 @@ import ( ) // Windows has a limitation for command line arguments, the size can not exceed 32KB. -// So we have to feed the files to some tools (like gofmt/misspell) batch by batch +// So we have to feed the files to some tools (like gofmt) batch by batch // We also introduce a `gitea-fmt` command, it does better import formatting than gofmt/goimports. `gitea-fmt` calls `gofmt` internally. @@ -195,7 +195,6 @@ Options: Commands: %[1]s gofmt ... - %[1]s misspell ... Arguments: {file-list} the file list @@ -206,6 +205,17 @@ Example: `, "file-batch-exec") } +func getGoVersion() string { + goModFile, err := os.ReadFile("go.mod") + if err != nil { + log.Fatalf(`Faild to read "go.mod": %v`, err) + os.Exit(1) + } + goModVersionRegex := regexp.MustCompile(`go \d+\.\d+`) + goModVersionLine := goModVersionRegex.Find(goModFile) + return string(goModVersionLine[3:]) +} + func newFileCollectorFromMainOptions(mainOptions map[string]string) (fc *fileCollector, err error) { fileFilter := mainOptions["file-filter"] if fileFilter == "" { @@ -228,9 +238,9 @@ func containsString(a []string, s string) bool { return false } -func giteaFormatGoImports(files []string, hasChangedFiles, doWriteFile bool) error { +func giteaFormatGoImports(files []string, doWriteFile bool) error { for _, file := range files { - if err := codeformat.FormatGoImports(file, hasChangedFiles, doWriteFile); err != nil { + if err := codeformat.FormatGoImports(file, doWriteFile); err != nil { log.Printf("failed to format go imports: %s, err=%v", file, err) return err } @@ -269,10 +279,8 @@ func main() { if containsString(subArgs, "-d") { log.Print("the -d option is not supported by gitea-fmt") } - cmdErrors = append(cmdErrors, giteaFormatGoImports(files, containsString(subArgs, "-l"), containsString(subArgs, "-w"))) - cmdErrors = append(cmdErrors, passThroughCmd("go", append([]string{"run", os.Getenv("GOFUMPT_PACKAGE"), "-extra", "-lang", "1.17"}, substArgs...))) - case "misspell": - cmdErrors = append(cmdErrors, passThroughCmd("go", append([]string{"run", os.Getenv("MISSPELL_PACKAGE")}, substArgs...))) + cmdErrors = append(cmdErrors, giteaFormatGoImports(files, containsString(subArgs, "-w"))) + cmdErrors = append(cmdErrors, passThroughCmd("go", append([]string{"run", os.Getenv("GOFUMPT_PACKAGE"), "-extra", "-lang", getGoVersion()}, substArgs...))) default: log.Fatalf("unknown cmd: %s %v", subCmd, subArgs) } diff --git a/build/codeformat/formatimports.go b/build/codeformat/formatimports.go index 5d051b2726..1076e3a0d1 100644 --- a/build/codeformat/formatimports.go +++ b/build/codeformat/formatimports.go @@ -7,7 +7,6 @@ package codeformat import ( "bytes" "errors" - "fmt" "io" "os" "sort" @@ -159,7 +158,7 @@ func formatGoImports(contentBytes []byte) ([]byte, error) { } // FormatGoImports format the imports by our rules (see unit tests) -func FormatGoImports(file string, doChangedFiles, doWriteFile bool) error { +func FormatGoImports(file string, doWriteFile bool) error { f, err := os.Open(file) if err != nil { return err @@ -183,10 +182,6 @@ func FormatGoImports(file string, doChangedFiles, doWriteFile bool) error { return nil } - if doChangedFiles { - fmt.Println(file) - } - if doWriteFile { f, err = os.OpenFile(file, os.O_TRUNC|os.O_WRONLY, 0o644) if err != nil { diff --git a/cmd/dump.go b/cmd/dump.go index 73c2251b92..6569fb6e36 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -146,6 +146,10 @@ It can be used for backup and capture Gitea server image to send to maintainer`, Name: "skip-package-data", Usage: "Skip package data", }, + cli.BoolFlag{ + Name: "skip-index", + Usage: "Skip bleve index data", + }, cli.GenericFlag{ Name: "type", Value: outputTypeEnum, @@ -327,6 +331,11 @@ func runDump(ctx *cli.Context) error { excludes = append(excludes, opts.ProviderConfig) } + if ctx.IsSet("skip-index") && ctx.Bool("skip-index") { + excludes = append(excludes, setting.Indexer.RepoPath) + excludes = append(excludes, setting.Indexer.IssuePath) + } + excludes = append(excludes, setting.RepoRootPath) excludes = append(excludes, setting.LFS.Path) excludes = append(excludes, setting.Attachment.Path) diff --git a/contrib/init/gentoo/gitea b/contrib/init/gentoo/gitea index e423eae54d..db904e7bbb 100644 --- a/contrib/init/gentoo/gitea +++ b/contrib/init/gentoo/gitea @@ -2,14 +2,43 @@ DIR=/var/lib/gitea USER=git +HOME=/home/${USER} +GITEA_WORK_DIR=${DIR} +EXECUTABLE=/usr/local/bin/gitea +export USER +export HOME +export GITEA_WORK_DIR + +name=$RC_SVCNAME +cfgfile="/etc/$RC_SVCNAME/app.ini" +command="${EXECUTABLE}" +command_user="${USER}" +command_args="web -c /etc/$RC_SVCNAME/app.ini" +command_background="yes" +pidfile="/run/$RC_SVCNAME/$RC_SVCNAME.pid" start_stop_daemon_args="--user ${USER} --chdir ${DIR}" -command="/usr/local/bin/gitea" -command_args="web -c /etc/gitea/app.ini" -command_background=yes -pidfile=/run/gitea.pid depend() { need net + ### + # Don't forget to add the database service requirements + ### + #after postgresql + #after mysql + #after mariadb + #after memcached + #after redis +} + +start_pre() +{ + checkpath --directory --owner $command_user:$command_user --mode 0750 \ + /run/$RC_SVCNAME /var/log/$RC_SVCNAME + ## + # If you want to bind Gitea to a port below 1024, uncomment + # the value below + ## + #setcap cap_net_bind_service=+ep "${EXECUTABLE}" } diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go index 85476afd01..686a3ddffa 100644 --- a/contrib/pr/checkout.go +++ b/contrib/pr/checkout.go @@ -14,7 +14,6 @@ import ( "fmt" "log" "net/http" - "net/url" "os" "os/exec" "os/user" @@ -34,6 +33,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers" + markup_service "code.gitea.io/gitea/services/markup" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" @@ -62,11 +62,7 @@ func runPR() { } setting.AppWorkPath = curDir setting.StaticRootPath = curDir - setting.GravatarSourceURL, err = url.Parse("https://secure.gravatar.com/avatar/") - if err != nil { - log.Fatalf("url.Parse: %v\n", err) - } - + setting.GravatarSource = "https://secure.gravatar.com/avatar/" setting.AppURL = "http://localhost:8080/" setting.HTTPPort = "8080" setting.SSH.Domain = "localhost" @@ -117,7 +113,7 @@ func runPR() { log.Printf("[PR] Setting up router\n") // routers.GlobalInit() external.RegisterRenderers() - markup.Init() + markup.Init(markup_service.ProcessorHelper()) c := routers.NormalRoutes(graceful.GetManager().HammerContext()) log.Printf("[PR] Ready for testing !\n") diff --git a/docs/config.yaml b/docs/config.yaml index 92e033a907..66bd379c0c 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -18,7 +18,7 @@ params: description: Git with a cup of tea author: The Gitea Authors website: https://docs.gitea.io - version: 1.17.1 + version: 1.17.3 minGoVersion: 1.18 goVersion: 1.19 minNodeVersion: 14 diff --git a/docs/content/doc/developers/hacking-on-gitea.en-us.md b/docs/content/doc/developers/hacking-on-gitea.en-us.md index d8427b58f3..3283240c98 100644 --- a/docs/content/doc/developers/hacking-on-gitea.en-us.md +++ b/docs/content/doc/developers/hacking-on-gitea.en-us.md @@ -23,7 +23,7 @@ menu: To get a quick working development environment you could use Gitpod. -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/go-gitea/gitea) +[![Open in Gitpod](/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/go-gitea/gitea) ## Installing go diff --git a/docs/content/doc/developers/oauth2-provider.md b/docs/content/doc/developers/oauth2-provider.md index 9e6ab11742..c6765f19e7 100644 --- a/docs/content/doc/developers/oauth2-provider.md +++ b/docs/content/doc/developers/oauth2-provider.md @@ -44,6 +44,12 @@ To use the Authorization Code Grant as a third party application it is required Currently Gitea does not support scopes (see [#4300](https://github.com/go-gitea/gitea/issues/4300)) and all third party applications will be granted access to all resources of the user and their organizations. +## Client types + +Gitea supports both confidential and public client types, [as defined by RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1). + +For public clients, a redirect URI of a loopback IP address such as `http://127.0.0.1/` allows any port. Avoid using `localhost`, [as recommended by RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252#section-8.3). + ## Example **Note:** This example does not use PKCE. diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md index 89d92b2565..9baa6d5123 100644 --- a/docs/content/doc/features/comparison.en-us.md +++ b/docs/content/doc/features/comparison.en-us.md @@ -21,7 +21,7 @@ menu: To help decide if Gitea is suited for your needs, here is how it compares to other Git self hosted options. -Be warned that we don't regularly check for feature changes in other products, so this list may be outdated. If you find anything that needs to be updated in the table below, please report it in an [issue on GitHub](https://github.com/go-gitea/gitea/issues). +Be warned that we don't regularly check for feature changes in other products, so this list may be outdated. If you find anything that needs to be updated in the table below, please [open an issue](https://github.com/go-gitea/gitea/issues/new/choose). _Symbols used in table:_ @@ -33,103 +33,111 @@ _Symbols used in table:_ ## General Features -| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | -| ----------------------------------- | ---------------------------------------------------| ---- | --------- | --------- | --------- | -------------- | ------------ | -| Open source and free | ✓ | ✓ | ✘ | ✓ | ✘ | ✘ | ✓ | -| Low resource usage (RAM/CPU) | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | -| Multiple database support | ✓ | ✓ | ✘ | ⁄ | ⁄ | ✓ | ✓ | -| Multiple OS support | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ | -| Easy upgrade process | ✓ | ✓ | ✘ | ✓ | ✓ | ✘ | ✓ | -| Markdown support | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Orgmode support | ✓ | ✘ | ✓ | ✘ | ✘ | ✘ | ? | -| CSV support | ✓ | ✘ | ✓ | ✘ | ✘ | ✓ | ? | -| Third-party render tool support | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ | ? | -| Static Git-powered pages | [✘](https://github.com/go-gitea/gitea/issues/302) | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Integrated Git-powered wiki | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (cloud only) | ✘ | -| Deploy Tokens | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Repository Tokens with write rights | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Built-in Package/Container Registry | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| External git mirroring | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | -| WebAuthn (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ? | -| Built-in CI/CD | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Subgroups: groups within groups | [✘](https://github.com/go-gitea/gitea/issues/1872) | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | -| Mermaid diagrams in Markdown | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Math syntax in Markdown | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +| ------------------------------------------------ | --------------------------------------------------- | ---- | --------- | --------- | --------- | --------- | ------------ | +| Open source and free | ✓ | ✓ | ✘ | ✓ | ✘ | ✘ | ✓ | +| Low RAM/ CPU usage | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | +| Multiple database support | ✓ | ✓ | ✘ | ⁄ | ⁄ | ✓ | ✓ | +| Multiple OS support | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ | +| Easy upgrades | ✓ | ✓ | ✘ | ✓ | ✓ | ✘ | ✓ | +| Telemetry | **✘** | ✘ | ✓ | ✓ | ✓ | ✓ | ? | +| Third-party render tool support | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ | ? | +| WebAuthn (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ? | +| Extensive API | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Built-in Package/Container Registry | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Sync commits to an external repo (push mirror) | ✓ | ✓ | ✘ | ✓ | ✓ | ✘ | ✓ | +| Sync commits from an external repo (pull mirror) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ? | +| Light and Dark Theme | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ? | +| Custom Theme Support | ✓ | ✓ | ✘ | ✘ | ✘ | ✓ | ✘ | +| Markdown support | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| CSV support | ✓ | ✘ | ✓ | ✘ | ✘ | ✓ | ? | +| 'GitHub / GitLab pages' | [✘](https://github.com/go-gitea/gitea/issues/302) | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Repo-specific wiki (as a repo itself) | ✓ | ✓ | ✓ | ✓ | ✓ | / | ✘ | +| Deploy Tokens | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Repository Tokens with write rights | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| RSS Feeds | ✓ | ✘ | ✓ | ✘ | ✘ | ✘ | ✘ | +| Built-in CI/CD | [✘](https://github.com/go-gitea/gitea/issues/13539) | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Subgroups: groups within groups | [✘](https://github.com/go-gitea/gitea/issues/1872) | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | +| Interaction with other instances | [/](https://github.com/go-gitea/gitea/issues/18240) | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ | +| Mermaid diagrams in Markdown | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Math syntax in Markdown | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | ## Code management -| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | -| -------------------------------------------- | ------------------------------------------------ | ---- | --------- | --------- | --------- | --------- | ------------ | -| Repository topics | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Repository code search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Global code search | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ | -| Git LFS 2.0 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Group Milestones | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| Granular user roles (Code, Issues, Wiki etc) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| Verified Committer | ⁄ | ✘ | ? | ✓ | ✓ | ✓ | ✘ | -| GPG Signed Commits | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| SSH Signed Commits | ✓ | ✘ | ✘ | ✘ | ✘ | ? | ? | -| Reject unsigned commits | [✓](https://github.com/go-gitea/gitea/pull/9708) | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Repository Activity page | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Branch manager | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Create new branches | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Web code editor | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Commit graph | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Template Repositories | [✓](https://github.com/go-gitea/gitea/pull/8768) | ✘ | ✓ | ✘ | ✓ | ✓ | ✘ | +| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +| ------------------------------------------- | --------------------------------------------------- | ---- | --------- | --------- | --------- | --------- | ------------ | +| Repository topics | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Repository code search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Global code search | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ | +| Git LFS 2.0 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Group Milestones | [✘](https://github.com/go-gitea/gitea/issues/14622) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Granular user roles (Code, Issues, Wiki, …) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Verified Committer | ⁄ | ✘ | ? | ✓ | ✓ | ✓ | ✘ | +| GPG Signed Commits | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| SSH Signed Commits | ✓ | ✘ | ✓ | ✘ | ✘ | ? | ? | +| Reject unsigned commits | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Migrating repos from other services | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Repository Activity page | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Branch manager | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Create new branches | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Web code editor | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Commit graph | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Template Repositories | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✘ | +| Git Blame | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Visual comparison of image changes | ✓ | ✘ | ✓ | ? | ? | ? | ? | ## Issue Tracker -| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | -| ------------------------------- | -------------------------------------------------- | --------------------------------------------- | --------- | ----------------------------------------------------------------------- | --------- | -------------- | ------------ | -| Issue tracker | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (cloud only) | ✘ | -| Issue templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Labels | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Time tracking | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Multiple assignees for issues | ✓ | ✘ | ✓ | ✘ | ✓ | ✘ | ✘ | -| Related issues | ✘ | ✘ | ⁄ | [✓](https://docs.gitlab.com/ce/user/project/issues/related_issues.html) | ✓ | ✘ | ✘ | -| Confidential issues | [✘](https://github.com/go-gitea/gitea/issues/3217) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Lock Discussion | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Issue Boards (Kanban) | [✓](https://github.com/go-gitea/gitea/pull/8346) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| Create new branches from issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| Issue search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| Global issue search | [✘](https://github.com/go-gitea/gitea/issues/2434) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| Issue dependency | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ | -| Create issue via email | [✘](https://github.com/go-gitea/gitea/issues/6226) | [✘](https://github.com/gogs/gogs/issues/2602) | ✘ | ✘ | ✓ | ✓ | ✘ | -| Service Desk | [✘](https://github.com/go-gitea/gitea/issues/6219) | ✘ | ✘ | [✓](https://gitlab.com/groups/gitlab-org/-/epics/3103) | ✓ | ✘ | ✘ | +| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +| ----------------------------- | --------------------------------------------------- | ---- | --------- | --------- | --------- | --------- | ------------ | +| Issue tracker | ✓ | ✓ | ✓ | ✓ | ✓ | / | ✘ | +| Issue templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Labels | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Time tracking | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Multiple assignees for issues | ✓ | ✘ | ✓ | ✘ | ✓ | ✘ | ✘ | +| Related issues | ✘ | ✘ | ⁄ | ✓ | ✓ | ✘ | ✘ | +| Confidential issues | [✘](https://github.com/go-gitea/gitea/issues/3217) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Lock Discussion | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Issue Boards (Kanban) | [/](https://github.com/go-gitea/gitea/issues/14710) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Create branch from issue | [✘](https://github.com/go-gitea/gitea/issues/20226) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Convert comment to new issue | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Issue search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Global issue search | [/](https://github.com/go-gitea/gitea/issues/2434) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Issue dependency | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ | +| Create issue via email | [✘](https://github.com/go-gitea/gitea/issues/6226) | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | +| Service Desk | [✘](https://github.com/go-gitea/gitea/issues/6219) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | ## Pull/Merge requests -| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | -| ----------------------------------------------- | -------------------------------------------------- | ---- | --------- | --------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------ | ------------ | -| Pull/Merge requests | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Squash merging | ✓ | ✘ | ✓ | [✓](https://docs.gitlab.com/ce/user/project/merge_requests/squash_and_merge.html) | ✓ | ✓ | ✓ | -| Rebase merging | ✓ | ✓ | ✓ | ✘ | ⁄ | ✘ | ✓ | -| Pull/Merge request inline comments | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Pull/Merge request approval | ✓ | ✘ | ⁄ | ✓ | ✓ | ✓ | ✓ | -| Merge conflict resolution | [✘](https://github.com/go-gitea/gitea/issues/5158) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| Restrict push and merge access to certain users | ✓ | ✘ | ✓ | ⁄ | ✓ | ✓ | ✓ | -| Revert specific commits or a merge request | [✘](https://github.com/go-gitea/gitea/issues/5158) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| Pull/Merge requests templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Cherry-picking changes | [✘](https://github.com/go-gitea/gitea/issues/5158) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | -| Download Patch | ✓ | ✘ | ✓ | ✓ | ✓ | [/](https://jira.atlassian.com/plugins/servlet/mobile#issue/BCLOUD-8323) | ✘ | +| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +| ----------------------------------------------- | -------------------------------------------------- | ---- | --------- | --------- | --------- | --------- | ------------ | +| Pull/Merge requests | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Squash merging | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Rebase merging | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Pull/Merge request inline comments | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Pull/Merge request approval | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Merge conflict resolution | [✘](https://github.com/go-gitea/gitea/issues/9014) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Restrict push and merge access to certain users | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Revert specific commits | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Pull/Merge requests templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Cherry-picking changes | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Download Patch | ✓ | ✘ | ✓ | ✓ | ✓ | / | ✘ | ## 3rd-party integrations -| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | -| ---------------------------------------------- | ------------------------------------------------ | ---- | --------- | --------- | --------- | --------- | ------------ | -| Webhook support | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Custom Git Hooks | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| AD / LDAP integration | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Multiple LDAP / AD server support | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | -| LDAP user synchronization | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| SAML 2.0 service provider | [✘](https://github.com/go-gitea/gitea/issues/5512) | [✘](https://github.com/gogs/gogs/issues/1221) | ✓ | ✓ | ✓ | ✓ | ✘ | -| OpenId Connect support | ✓ | ✘ | ✓ | ✓ | ✓ | ? | ✘ | -| OAuth 2.0 integration (external authorization) | ✓ | ✘ | ⁄ | ✓ | ✓ | ? | ✓ | -| Act as OAuth 2.0 provider | [✓](https://github.com/go-gitea/gitea/pull/5378) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| Two factor authentication (2FA) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | -| Mattermost/Slack integration | ✓ | ✓ | ⁄ | ✓ | ✓ | ⁄ | ✓ | -| Discord integration | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | -| Microsoft Teams integration | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | -| External CI/CD status display | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +| ---------------------------------------------- | ------------------------------------------------ | ---- | --------- | --------- | --------- | --------- | ------------ | +| Webhooks | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Git Hooks | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| AD / LDAP integration | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Multiple LDAP / AD server support | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | +| LDAP user synchronization | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| SAML 2.0 service provider | [✘](https://github.com/go-gitea/gitea/issues/5512) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| OpenID Connect support | ✓ | ✘ | ✓ | ✓ | ✓ | ? | ✘ | +| OAuth 2.0 integration (external authorization) | ✓ | ✘ | ⁄ | ✓ | ✓ | ? | ✓ | +| Act as OAuth 2.0 provider | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Two factor authentication (2FA) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Integration with the most common services | ✓ | / | ⁄ | ✓ | ✓ | ⁄ | ✓ | +| Incorporate external CI/CD | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | diff --git a/docs/content/doc/installation/from-package.zh-cn.md b/docs/content/doc/installation/from-package.zh-cn.md index 7faf2a88b1..dd56ebdaa2 100644 --- a/docs/content/doc/installation/from-package.zh-cn.md +++ b/docs/content/doc/installation/from-package.zh-cn.md @@ -71,7 +71,7 @@ choco install gitea macOS 平台下当前我们仅支持通过 `brew` 来安装。如果你没有安装 [Homebrew](http://brew.sh/),你也可以查看 [从二进制安装]({{< relref "from-binary.zh-cn.md" >}})。在你安装了 `brew` 之后, 你可以执行以下命令: ``` -brew tap go-gitea/gitea +brew tap gitea/tap https://gitea.com/gitea/homebrew-gitea brew install gitea ``` diff --git a/docs/content/doc/packages/nuget.en-us.md b/docs/content/doc/packages/nuget.en-us.md index 6c8aaa70af..670abca7fd 100644 --- a/docs/content/doc/packages/nuget.en-us.md +++ b/docs/content/doc/packages/nuget.en-us.md @@ -14,7 +14,7 @@ menu: # NuGet Packages Repository -Publish [NuGet](https://www.nuget.org/) packages for your user or organization. The package registry supports [NuGet Symbol Packages](https://docs.microsoft.com/en-us/nuget/create-packages/symbol-packages-snupkg) too. +Publish [NuGet](https://www.nuget.org/) packages for your user or organization. The package registry supports the V2 and V3 API protocol and you can work with [NuGet Symbol Packages](https://docs.microsoft.com/en-us/nuget/create-packages/symbol-packages-snupkg) too. **Table of Contents** diff --git a/docs/content/doc/usage/reverse-proxies.zh-cn.md b/docs/content/doc/usage/reverse-proxies.zh-cn.md index 722b9c7c9d..f6d92e2a61 100644 --- a/docs/content/doc/usage/reverse-proxies.zh-cn.md +++ b/docs/content/doc/usage/reverse-proxies.zh-cn.md @@ -13,6 +13,12 @@ menu: identifier: "reverse-proxies" --- +# 反向代理 + +**目录** + +{{< toc >}} + ## 使用 Nginx 作为反向代理服务 如果您想使用 Nginx 作为 Gitea 的反向代理服务,您可以参照以下 `nginx.conf` 配置中 `server` 的 `http` 部分: @@ -24,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; } } ``` @@ -41,6 +51,10 @@ server { location /git/ { # 注意: 反向代理后端 URL 的最后需要有一个路径符号 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; } } ``` diff --git a/docs/static/open-in-gitpod.svg b/docs/static/open-in-gitpod.svg new file mode 100644 index 0000000000..b97cd29487 --- /dev/null +++ b/docs/static/open-in-gitpod.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod index 5bafaf4142..408249880c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( code.gitea.io/gitea-vet v0.2.2-0.20220122151748-48ebc902541b code.gitea.io/sdk/gitea v0.15.1 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 - gitea.com/go-chi/binding v0.0.0-20220309004920-114340dabecb + gitea.com/go-chi/binding v0.0.0-20221013104517-b29891619681 gitea.com/go-chi/cache v0.2.0 gitea.com/go-chi/captcha v0.0.0-20211013065431-70641c1a35d5 gitea.com/go-chi/session v0.0.0-20211218221615-e3605d8b28b8 diff --git a/go.sum b/go.sum index b53732f6a5..65841b8ec3 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcig dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= -gitea.com/go-chi/binding v0.0.0-20220309004920-114340dabecb h1:Yy0Bxzc8R2wxiwXoG/rECGplJUSpXqCsog9PuJFgiHs= -gitea.com/go-chi/binding v0.0.0-20220309004920-114340dabecb/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc= +gitea.com/go-chi/binding v0.0.0-20221013104517-b29891619681 h1:MMSPgnVULVwV9kEBgvyEUhC9v/uviZ55hPJEMjpbNR4= +gitea.com/go-chi/binding v0.0.0-20221013104517-b29891619681/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc= gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0= gitea.com/go-chi/cache v0.2.0 h1:E0npuTfDW6CT1yD8NMDVc1SK6IeRjfmRL2zlEsCEd7w= gitea.com/go-chi/cache v0.2.0/go.mod h1:iQlVK2aKTZ/rE9UcHyz9pQWGvdP9i1eI2spOpzgCrtE= diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 2767c1b7ae..0000000000 --- a/jest.config.js +++ /dev/null @@ -1,15 +0,0 @@ -// to run tests with ES6 module, node must run with "--experimental-vm-modules", or see Makefile's "test-frontend" for reference -export default { - rootDir: 'web_src', - setupFilesAfterEnv: ['jest-extended/all'], - testEnvironment: 'jest-environment-jsdom', - testMatch: ['/**/*.test.js'], - testTimeout: 20000, - transform: { - '\\.svg$': '/js/testUtils/jestRawLoader.js', - }, - setupFiles: [ - './js/testUtils/jestSetup.js', // prepare global variables used by our code (eg: window.config) - ], - verbose: false, -}; diff --git a/models/activities/notification.go b/models/activities/notification.go index 2f21dc74d1..0a61088167 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -806,7 +806,7 @@ func getNotificationByID(ctx context.Context, notificationID int64) (*Notificati } if !ok { - return nil, db.ErrNotExist{ID: notificationID} + return nil, db.ErrNotExist{Resource: "notification", ID: notificationID} } return notification, nil diff --git a/models/admin/notice_test.go b/models/admin/notice_test.go deleted file mode 100644 index f3767392d1..0000000000 --- a/models/admin/notice_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2017 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 admin_test - -import ( - "testing" - - "code.gitea.io/gitea/models/admin" - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" -) - -func TestNotice_TrStr(t *testing.T) { - notice := &admin.Notice{ - Type: admin.NoticeRepository, - Description: "test description", - } - assert.Equal(t, "admin.notices.type_1", notice.TrStr()) -} - -func TestCreateNotice(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - noticeBean := &admin.Notice{ - Type: admin.NoticeRepository, - Description: "test description", - } - unittest.AssertNotExistsBean(t, noticeBean) - assert.NoError(t, admin.CreateNotice(db.DefaultContext, noticeBean.Type, noticeBean.Description)) - unittest.AssertExistsAndLoadBean(t, noticeBean) -} - -func TestCreateRepositoryNotice(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - noticeBean := &admin.Notice{ - Type: admin.NoticeRepository, - Description: "test description", - } - unittest.AssertNotExistsBean(t, noticeBean) - assert.NoError(t, admin.CreateRepositoryNotice(noticeBean.Description)) - unittest.AssertExistsAndLoadBean(t, noticeBean) -} - -// TODO TestRemoveAllWithNotice - -func TestCountNotices(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - assert.Equal(t, int64(3), admin.CountNotices()) -} - -func TestNotices(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - notices, err := admin.Notices(1, 2) - assert.NoError(t, err) - if assert.Len(t, notices, 2) { - assert.Equal(t, int64(3), notices[0].ID) - assert.Equal(t, int64(2), notices[1].ID) - } - - notices, err = admin.Notices(2, 2) - assert.NoError(t, err) - if assert.Len(t, notices, 1) { - assert.Equal(t, int64(1), notices[0].ID) - } -} - -func TestDeleteNotice(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) - assert.NoError(t, admin.DeleteNotice(3)) - unittest.AssertNotExistsBean(t, &admin.Notice{ID: 3}) -} - -func TestDeleteNotices(t *testing.T) { - // delete a non-empty range - assert.NoError(t, unittest.PrepareTestDatabase()) - - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) - assert.NoError(t, admin.DeleteNotices(1, 2)) - unittest.AssertNotExistsBean(t, &admin.Notice{ID: 1}) - unittest.AssertNotExistsBean(t, &admin.Notice{ID: 2}) - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) -} - -func TestDeleteNotices2(t *testing.T) { - // delete an empty range - assert.NoError(t, unittest.PrepareTestDatabase()) - - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) - assert.NoError(t, admin.DeleteNotices(3, 2)) - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) -} - -func TestDeleteNoticesByIDs(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 1}) - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 3}) - assert.NoError(t, admin.DeleteNoticesByIDs([]int64{1, 3})) - unittest.AssertNotExistsBean(t, &admin.Notice{ID: 1}) - unittest.AssertExistsAndLoadBean(t, &admin.Notice{ID: 2}) - unittest.AssertNotExistsBean(t, &admin.Notice{ID: 3}) -} diff --git a/models/admin/task.go b/models/admin/task.go index 07eb61decc..4fa0f10394 100644 --- a/models/admin/task.go +++ b/models/admin/task.go @@ -167,6 +167,10 @@ func (err ErrTaskDoesNotExist) Error() string { err.ID, err.RepoID, err.Type) } +func (err ErrTaskDoesNotExist) Unwrap() error { + return util.ErrNotExist +} + // GetMigratingTask returns the migrating task by repo's id func GetMigratingTask(repoID int64) (*Task, error) { task := Task{ diff --git a/models/asymkey/error.go b/models/asymkey/error.go index 5d2be1f289..3ddeb0498a 100644 --- a/models/asymkey/error.go +++ b/models/asymkey/error.go @@ -4,7 +4,11 @@ package asymkey -import "fmt" +import ( + "fmt" + + "code.gitea.io/gitea/modules/util" +) // ErrKeyUnableVerify represents a "KeyUnableVerify" kind of error. type ErrKeyUnableVerify struct { @@ -36,6 +40,10 @@ func (err ErrKeyNotExist) Error() string { return fmt.Sprintf("public key does not exist [id: %d]", err.ID) } +func (err ErrKeyNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrKeyAlreadyExist represents a "KeyAlreadyExist" kind of error. type ErrKeyAlreadyExist struct { OwnerID int64 @@ -54,6 +62,10 @@ func (err ErrKeyAlreadyExist) Error() string { err.OwnerID, err.Fingerprint, err.Content) } +func (err ErrKeyAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrKeyNameAlreadyUsed represents a "KeyNameAlreadyUsed" kind of error. type ErrKeyNameAlreadyUsed struct { OwnerID int64 @@ -70,6 +82,10 @@ func (err ErrKeyNameAlreadyUsed) Error() string { return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name) } +func (err ErrKeyNameAlreadyUsed) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrGPGNoEmailFound represents a "ErrGPGNoEmailFound" kind of error. type ErrGPGNoEmailFound struct { FailedEmails []string @@ -132,6 +148,10 @@ func (err ErrGPGKeyNotExist) Error() string { return fmt.Sprintf("public gpg key does not exist [id: %d]", err.ID) } +func (err ErrGPGKeyNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrGPGKeyImportNotExist represents a "GPGKeyImportNotExist" kind of error. type ErrGPGKeyImportNotExist struct { ID string @@ -147,6 +167,10 @@ func (err ErrGPGKeyImportNotExist) Error() string { return fmt.Sprintf("public gpg key import does not exist [id: %s]", err.ID) } +func (err ErrGPGKeyImportNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrGPGKeyIDAlreadyUsed represents a "GPGKeyIDAlreadyUsed" kind of error. type ErrGPGKeyIDAlreadyUsed struct { KeyID string @@ -162,6 +186,10 @@ func (err ErrGPGKeyIDAlreadyUsed) Error() string { return fmt.Sprintf("public key already exists [key_id: %s]", err.KeyID) } +func (err ErrGPGKeyIDAlreadyUsed) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrGPGKeyAccessDenied represents a "GPGKeyAccessDenied" kind of Error. type ErrGPGKeyAccessDenied struct { UserID int64 @@ -180,6 +208,10 @@ func (err ErrGPGKeyAccessDenied) Error() string { err.UserID, err.KeyID) } +func (err ErrGPGKeyAccessDenied) Unwrap() error { + return util.ErrPermissionDenied +} + // ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error. type ErrKeyAccessDenied struct { UserID int64 @@ -198,6 +230,10 @@ func (err ErrKeyAccessDenied) Error() string { err.UserID, err.KeyID, err.Note) } +func (err ErrKeyAccessDenied) Unwrap() error { + return util.ErrPermissionDenied +} + // ErrDeployKeyNotExist represents a "DeployKeyNotExist" kind of error. type ErrDeployKeyNotExist struct { ID int64 @@ -215,6 +251,10 @@ func (err ErrDeployKeyNotExist) Error() string { return fmt.Sprintf("Deploy key does not exist [id: %d, key_id: %d, repo_id: %d]", err.ID, err.KeyID, err.RepoID) } +func (err ErrDeployKeyNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrDeployKeyAlreadyExist represents a "DeployKeyAlreadyExist" kind of error. type ErrDeployKeyAlreadyExist struct { KeyID int64 @@ -231,6 +271,10 @@ func (err ErrDeployKeyAlreadyExist) Error() string { return fmt.Sprintf("public key already exists [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID) } +func (err ErrDeployKeyAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrDeployKeyNameAlreadyUsed represents a "DeployKeyNameAlreadyUsed" kind of error. type ErrDeployKeyNameAlreadyUsed struct { RepoID int64 @@ -247,6 +291,10 @@ func (err ErrDeployKeyNameAlreadyUsed) Error() string { return fmt.Sprintf("public key with name already exists [repo_id: %d, name: %s]", err.RepoID, err.Name) } +func (err ErrDeployKeyNameAlreadyUsed) Unwrap() error { + return util.ErrNotExist +} + // ErrSSHInvalidTokenSignature represents a "ErrSSHInvalidTokenSignature" kind of error. type ErrSSHInvalidTokenSignature struct { Wrapped error @@ -262,3 +310,7 @@ func IsErrSSHInvalidTokenSignature(err error) bool { func (err ErrSSHInvalidTokenSignature) Error() string { return "the provided signature does not sign the token with the provided key" } + +func (err ErrSSHInvalidTokenSignature) Unwrap() error { + return util.ErrInvalidArgument +} diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index abcd9e1ca6..e42084c086 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -31,9 +31,14 @@ type OAuth2Application struct { Name string ClientID string `xorm:"unique"` ClientSecret string - RedirectURIs []string `xorm:"redirect_uris JSON TEXT"` - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + // OAuth defines both Confidential and Public client types + // https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 + // "Authorization servers MUST record the client type in the client registration details" + // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4 + ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"` + RedirectURIs []string `xorm:"redirect_uris JSON TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } func init() { @@ -57,15 +62,17 @@ func (app *OAuth2Application) PrimaryRedirectURI() string { // ContainsRedirectURI checks if redirectURI is allowed for app func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool { - uri, err := url.Parse(redirectURI) - // ignore port for http loopback uris following https://datatracker.ietf.org/doc/html/rfc8252#section-7.3 - if err == nil && uri.Scheme == "http" && uri.Port() != "" { - ip := net.ParseIP(uri.Hostname()) - if ip != nil && ip.IsLoopback() { - // strip port - uri.Host = uri.Hostname() - if util.IsStringInSlice(uri.String(), app.RedirectURIs, true) { - return true + if !app.ConfidentialClient { + uri, err := url.Parse(redirectURI) + // ignore port for http loopback uris following https://datatracker.ietf.org/doc/html/rfc8252#section-7.3 + if err == nil && uri.Scheme == "http" && uri.Port() != "" { + ip := net.ParseIP(uri.Hostname()) + if ip != nil && ip.IsLoopback() { + // strip port + uri.Host = uri.Hostname() + if util.IsStringInSlice(uri.String(), app.RedirectURIs, true) { + return true + } } } } @@ -161,19 +168,21 @@ func GetOAuth2ApplicationsByUserID(ctx context.Context, userID int64) (apps []*O // CreateOAuth2ApplicationOptions holds options to create an oauth2 application type CreateOAuth2ApplicationOptions struct { - Name string - UserID int64 - RedirectURIs []string + Name string + UserID int64 + ConfidentialClient bool + RedirectURIs []string } // CreateOAuth2Application inserts a new oauth2 application func CreateOAuth2Application(ctx context.Context, opts CreateOAuth2ApplicationOptions) (*OAuth2Application, error) { clientID := uuid.New().String() app := &OAuth2Application{ - UID: opts.UserID, - Name: opts.Name, - ClientID: clientID, - RedirectURIs: opts.RedirectURIs, + UID: opts.UserID, + Name: opts.Name, + ClientID: clientID, + RedirectURIs: opts.RedirectURIs, + ConfidentialClient: opts.ConfidentialClient, } if err := db.Insert(ctx, app); err != nil { return nil, err @@ -183,10 +192,11 @@ func CreateOAuth2Application(ctx context.Context, opts CreateOAuth2ApplicationOp // UpdateOAuth2ApplicationOptions holds options to update an oauth2 application type UpdateOAuth2ApplicationOptions struct { - ID int64 - Name string - UserID int64 - RedirectURIs []string + ID int64 + Name string + UserID int64 + ConfidentialClient bool + RedirectURIs []string } // UpdateOAuth2Application updates an oauth2 application @@ -207,6 +217,7 @@ func UpdateOAuth2Application(opts UpdateOAuth2ApplicationOptions) (*OAuth2Applic app.Name = opts.Name app.RedirectURIs = opts.RedirectURIs + app.ConfidentialClient = opts.ConfidentialClient if err = updateOAuth2Application(ctx, app); err != nil { return nil, err @@ -217,7 +228,7 @@ func UpdateOAuth2Application(opts UpdateOAuth2ApplicationOptions) (*OAuth2Applic } func updateOAuth2Application(ctx context.Context, app *OAuth2Application) error { - if _, err := db.GetEngine(ctx).ID(app.ID).Update(app); err != nil { + if _, err := db.GetEngine(ctx).ID(app.ID).UseBool("confidential_client").Update(app); err != nil { return err } return nil @@ -486,7 +497,7 @@ type ErrOAuthClientIDInvalid struct { ClientID string } -// IsErrOauthClientIDInvalid checks if an error is a ErrReviewNotExist. +// IsErrOauthClientIDInvalid checks if an error is a ErrOAuthClientIDInvalid. func IsErrOauthClientIDInvalid(err error) bool { _, ok := err.(ErrOAuthClientIDInvalid) return ok @@ -497,6 +508,11 @@ func (err ErrOAuthClientIDInvalid) Error() string { return fmt.Sprintf("Client ID invalid [Client ID: %s]", err.ClientID) } +// Unwrap unwraps this as a ErrNotExist err +func (err ErrOAuthClientIDInvalid) Unwrap() error { + return util.ErrNotExist +} + // ErrOAuthApplicationNotFound will be thrown if id cannot be found type ErrOAuthApplicationNotFound struct { ID int64 @@ -513,6 +529,11 @@ func (err ErrOAuthApplicationNotFound) Error() string { return fmt.Sprintf("OAuth application not found [ID: %d]", err.ID) } +// Unwrap unwraps this as a ErrNotExist err +func (err ErrOAuthApplicationNotFound) Unwrap() error { + return util.ErrNotExist +} + // GetActiveOAuth2ProviderSources returns all actived LoginOAuth2 sources func GetActiveOAuth2ProviderSources() ([]*Source, error) { sources := make([]*Source, 0, 1) diff --git a/models/auth/oauth2_test.go b/models/auth/oauth2_test.go index 3815cb3b2c..7a4df6b9ac 100644 --- a/models/auth/oauth2_test.go +++ b/models/auth/oauth2_test.go @@ -45,7 +45,8 @@ func TestOAuth2Application_ContainsRedirectURI(t *testing.T) { func TestOAuth2Application_ContainsRedirectURI_WithPort(t *testing.T) { app := &auth_model.OAuth2Application{ - RedirectURIs: []string{"http://127.0.0.1/", "http://::1/", "http://192.168.0.1/", "http://intranet/", "https://127.0.0.1/"}, + RedirectURIs: []string{"http://127.0.0.1/", "http://::1/", "http://192.168.0.1/", "http://intranet/", "https://127.0.0.1/"}, + ConfidentialClient: false, } // http loopback uris should ignore port diff --git a/models/auth/source.go b/models/auth/source.go index 6f4f5addcb..f8be5398ae 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/xorm" "xorm.io/xorm/convert" @@ -366,6 +367,11 @@ func (err ErrSourceNotExist) Error() string { return fmt.Sprintf("login source does not exist [id: %d]", err.ID) } +// Unwrap unwraps this as a ErrNotExist err +func (err ErrSourceNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrSourceAlreadyExist represents a "SourceAlreadyExist" kind of error. type ErrSourceAlreadyExist struct { Name string @@ -381,6 +387,11 @@ func (err ErrSourceAlreadyExist) Error() string { return fmt.Sprintf("login source already exists [name: %s]", err.Name) } +// Unwrap unwraps this as a ErrExist err +func (err ErrSourceAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrSourceInUse represents a "SourceInUse" kind of error. type ErrSourceInUse struct { ID int64 diff --git a/models/auth/token.go b/models/auth/token.go index 01654f2901..3afef832da 100644 --- a/models/auth/token.go +++ b/models/auth/token.go @@ -35,6 +35,10 @@ func (err ErrAccessTokenNotExist) Error() string { return fmt.Sprintf("access token does not exist [sha: %s]", err.Token) } +func (err ErrAccessTokenNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrAccessTokenEmpty represents a "AccessTokenEmpty" kind of error. type ErrAccessTokenEmpty struct{} @@ -48,6 +52,10 @@ func (err ErrAccessTokenEmpty) Error() string { return "access token is empty" } +func (err ErrAccessTokenEmpty) Unwrap() error { + return util.ErrInvalidArgument +} + var successfulAccessTokenCache *lru.Cache // AccessToken represents a personal access token. diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go index c5bd972f91..736d4c340c 100644 --- a/models/auth/twofactor.go +++ b/models/auth/twofactor.go @@ -41,6 +41,11 @@ func (err ErrTwoFactorNotEnrolled) Error() string { return fmt.Sprintf("user not enrolled in 2FA [uid: %d]", err.UID) } +// Unwrap unwraps this as a ErrNotExist err +func (err ErrTwoFactorNotEnrolled) Unwrap() error { + return util.ErrNotExist +} + // TwoFactor represents a two-factor authentication token. type TwoFactor struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/auth/webauthn.go b/models/auth/webauthn.go index d3062342f5..1575b6cbab 100644 --- a/models/auth/webauthn.go +++ b/models/auth/webauthn.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "github.com/duo-labs/webauthn/webauthn" "xorm.io/xorm" @@ -29,6 +30,11 @@ func (err ErrWebAuthnCredentialNotExist) Error() string { return fmt.Sprintf("WebAuthn credential does not exist [credential_id: %x]", err.CredentialID) } +// Unwrap unwraps this as a ErrNotExist err +func (err ErrWebAuthnCredentialNotExist) Unwrap() error { + return util.ErrNotExist +} + // IsErrWebAuthnCredentialNotExist checks if an error is a ErrWebAuthnCredentialNotExist. func IsErrWebAuthnCredentialNotExist(err error) bool { _, ok := err.(ErrWebAuthnCredentialNotExist) diff --git a/models/avatars/avatar.go b/models/avatars/avatar.go index 9f7b0c474f..418e9b9ccc 100644 --- a/models/avatars/avatar.go +++ b/models/avatars/avatar.go @@ -13,6 +13,7 @@ import ( "sync" "code.gitea.io/gitea/models/db" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/log" @@ -72,7 +73,7 @@ func GetEmailForHash(md5Sum string) (string, error) { // LibravatarURL returns the URL for the given email. Slow due to the DNS lookup. // This function should only be called if a federated avatar service is enabled. func LibravatarURL(email string) (*url.URL, error) { - urlStr, err := setting.LibravatarService.FromEmail(email) + urlStr, err := system_model.LibravatarService.FromEmail(email) if err != nil { log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err) return nil, err @@ -149,8 +150,10 @@ func generateEmailAvatarLink(email string, size int, final bool) string { return DefaultAvatarLink() } + enableFederatedAvatar, _ := system_model.GetSetting(system_model.KeyPictureEnableFederatedAvatar) + var err error - if setting.EnableFederatedAvatar && setting.LibravatarService != nil { + if enableFederatedAvatar != nil && enableFederatedAvatar.GetValueBool() && system_model.LibravatarService != nil { emailHash := saveEmailHash(email) if final { // for final link, we can spend more time on slow external query @@ -166,12 +169,16 @@ func generateEmailAvatarLink(email string, size int, final bool) string { urlStr += "?size=" + strconv.Itoa(size) } return urlStr - } else if !setting.DisableGravatar { + } + + disableGravatar, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar) + if disableGravatar != nil && !disableGravatar.GetValueBool() { // copy GravatarSourceURL, because we will modify its Path. - avatarURLCopy := *setting.GravatarSourceURL + avatarURLCopy := *system_model.GravatarSourceURL avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email)) return generateRecognizedAvatarURL(avatarURLCopy, size) } + return DefaultAvatarLink() } diff --git a/models/avatars/avatar_test.go b/models/avatars/avatar_test.go index 4d6255ca5f..ace5445fc0 100644 --- a/models/avatars/avatar_test.go +++ b/models/avatars/avatar_test.go @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package avatars +package avatars_test import ( - "net/url" "testing" + avatars_model "code.gitea.io/gitea/models/avatars" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -15,40 +16,43 @@ import ( const gravatarSource = "https://secure.gravatar.com/avatar/" -func disableGravatar() { - setting.EnableFederatedAvatar = false - setting.LibravatarService = nil - setting.DisableGravatar = true +func disableGravatar(t *testing.T) { + err := system_model.SetSettingNoVersion(system_model.KeyPictureEnableFederatedAvatar, "false") + assert.NoError(t, err) + err = system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, "true") + assert.NoError(t, err) + system_model.LibravatarService = nil } func enableGravatar(t *testing.T) { - setting.DisableGravatar = false - var err error - setting.GravatarSourceURL, err = url.Parse(gravatarSource) + err := system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, "false") + assert.NoError(t, err) + setting.GravatarSource = gravatarSource + err = system_model.Init() assert.NoError(t, err) } func TestHashEmail(t *testing.T) { assert.Equal(t, "d41d8cd98f00b204e9800998ecf8427e", - HashEmail(""), + avatars_model.HashEmail(""), ) assert.Equal(t, "353cbad9b58e69c96154ad99f92bedc7", - HashEmail("gitea@example.com"), + avatars_model.HashEmail("gitea@example.com"), ) } func TestSizedAvatarLink(t *testing.T) { setting.AppSubURL = "/testsuburl" - disableGravatar() + disableGravatar(t) assert.Equal(t, "/testsuburl/assets/img/avatar_default.png", - GenerateEmailAvatarFastLink("gitea@example.com", 100)) + avatars_model.GenerateEmailAvatarFastLink("gitea@example.com", 100)) enableGravatar(t) assert.Equal(t, "https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100", - GenerateEmailAvatarFastLink("gitea@example.com", 100), + avatars_model.GenerateEmailAvatarFastLink("gitea@example.com", 100), ) } diff --git a/models/admin/main_test.go b/models/avatars/main_test.go similarity index 95% rename from models/admin/main_test.go rename to models/avatars/main_test.go index 23277fe37c..0e98d8f64d 100644 --- a/models/admin/main_test.go +++ b/models/avatars/main_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package admin_test +package avatars_test import ( "path/filepath" diff --git a/models/db/engine.go b/models/db/engine.go index 2c329300e3..f0c9ec46e9 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -225,7 +225,7 @@ func NamesToBean(names ...string) ([]interface{}, error) { for _, name := range names { bean, ok := beanMap[strings.ToLower(strings.TrimSpace(name))] if !ok { - return nil, fmt.Errorf("No table found that matches: %s", name) + return nil, fmt.Errorf("no table found that matches: %s", name) } if !gotBean[bean] { beans = append(beans, bean) diff --git a/models/db/error.go b/models/db/error.go index 6557229943..9577fa55db 100644 --- a/models/db/error.go +++ b/models/db/error.go @@ -6,6 +6,8 @@ package db import ( "fmt" + + "code.gitea.io/gitea/modules/util" ) // ErrCancelled represents an error due to context cancellation @@ -45,7 +47,8 @@ func (err ErrSSHDisabled) Error() string { // ErrNotExist represents a non-exist error. type ErrNotExist struct { - ID int64 + Resource string + ID int64 } // IsErrNotExist checks if an error is an ErrNotExist @@ -55,5 +58,18 @@ func IsErrNotExist(err error) bool { } func (err ErrNotExist) Error() string { - return fmt.Sprintf("record does not exist [id: %d]", err.ID) + name := "record" + if err.Resource != "" { + name = err.Resource + } + + if err.ID != 0 { + return fmt.Sprintf("%s does not exist [id: %d]", name, err.ID) + } + return fmt.Sprintf("%s does not exist", name) +} + +// Unwrap unwraps this as a ErrNotExist err +func (err ErrNotExist) Unwrap() error { + return util.ErrNotExist } diff --git a/models/db/index.go b/models/db/index.go index 673c382b27..58a976ad52 100644 --- a/models/db/index.go +++ b/models/db/index.go @@ -8,45 +8,15 @@ import ( "context" "errors" "fmt" - - "code.gitea.io/gitea/modules/setting" ) // ResourceIndex represents a resource index which could be used as issue/release and others -// We can create different tables i.e. issue_index, release_index and etc. +// We can create different tables i.e. issue_index, release_index, etc. type ResourceIndex struct { GroupID int64 `xorm:"pk"` MaxIndex int64 `xorm:"index"` } -// UpsertResourceIndex the function will not return until it acquires the lock or receives an error. -func UpsertResourceIndex(ctx context.Context, tableName string, groupID int64) (err error) { - // An atomic UPSERT operation (INSERT/UPDATE) is the only operation - // that ensures that the key is actually locked. - switch { - case setting.Database.UseSQLite3 || setting.Database.UsePostgreSQL: - _, err = Exec(ctx, fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ - "VALUES (?,1) ON CONFLICT (group_id) DO UPDATE SET max_index = %s.max_index+1", - tableName, tableName), groupID) - case setting.Database.UseMySQL: - _, err = Exec(ctx, fmt.Sprintf("INSERT INTO %s (group_id, max_index) "+ - "VALUES (?,1) ON DUPLICATE KEY UPDATE max_index = max_index+1", tableName), - groupID) - case setting.Database.UseMSSQL: - // https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/ - _, err = Exec(ctx, fmt.Sprintf("MERGE %s WITH (HOLDLOCK) as target "+ - "USING (SELECT ? AS group_id) AS src "+ - "ON src.group_id = target.group_id "+ - "WHEN MATCHED THEN UPDATE SET target.max_index = target.max_index+1 "+ - "WHEN NOT MATCHED THEN INSERT (group_id, max_index) "+ - "VALUES (src.group_id, 1);", tableName), - groupID) - default: - return fmt.Errorf("database type not supported") - } - return err -} - var ( // ErrResouceOutdated represents an error when request resource outdated ErrResouceOutdated = errors.New("resource outdated") @@ -59,53 +29,85 @@ const ( MaxDupIndexAttempts = 3 ) -// GetNextResourceIndex retried 3 times to generate a resource index -func GetNextResourceIndex(tableName string, groupID int64) (int64, error) { - for i := 0; i < MaxDupIndexAttempts; i++ { - idx, err := getNextResourceIndex(tableName, groupID) - if err == ErrResouceOutdated { - continue - } - if err != nil { - return 0, err - } - return idx, nil +// SyncMaxResourceIndex sync the max index with the resource +func SyncMaxResourceIndex(ctx context.Context, tableName string, groupID, maxIndex int64) (err error) { + e := GetEngine(ctx) + + // try to update the max_index and acquire the write-lock for the record + res, err := e.Exec(fmt.Sprintf("UPDATE %s SET max_index=? WHERE group_id=? AND max_indexr Tw >< " + full_name: ' < Ur Tw >< ' email: user2@example.com keep_email_private: true email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user2 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar2 avatar_email: user2@example.com - num_repos: 9 - num_stars: 2 + use_custom_avatar: false num_followers: 2 num_following: 1 - is_active: true + num_stars: 2 + num_repos: 9 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 3 lower_name: user3 name: user3 - login_name: user3 - full_name: " <<<< >> >> > >> > >>> >> " + full_name: ' <<<< >> >> > >> > >>> >> ' email: user3@example.com + keep_email_private: false email_notifications_preference: onmention + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 1 # organization + must_change_password: false + login_source: 0 + login_name: user3 + type: 1 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: false is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar3 avatar_email: user3@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 3 - num_members: 3 num_teams: 4 + num_members: 3 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 4 lower_name: user4 name: user4 - login_name: user4 - full_name: " " + full_name: ' ' email: user4@example.com + keep_email_private: false email_notifications_preference: onmention + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user4 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar4 avatar_email: user4@example.com - num_repos: 0 + use_custom_avatar: false + num_followers: 0 num_following: 1 - is_active: true + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 5 lower_name: user5 name: user5 - login_name: user5 full_name: User Five email: user5@example.com + keep_email_private: false email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user5 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: false + prohibit_login: false avatar: avatar5 avatar_email: user5@example.com - num_repos: 1 - allow_create_organization: false - is_active: true + use_custom_avatar: false + num_followers: 0 num_following: 0 + num_stars: 0 + num_repos: 1 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 6 lower_name: user6 name: user6 - login_name: user6 full_name: User Six email: user6@example.com + keep_email_private: false email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 1 # organization + must_change_password: false + login_source: 0 + login_name: user6 + type: 1 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: false is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar6 avatar_email: user6@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 0 - num_members: 2 num_teams: 2 + num_members: 2 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 7 lower_name: user7 name: user7 - login_name: user7 full_name: User Seven email: user7@example.com + keep_email_private: false email_notifications_preference: disabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 1 # organization + must_change_password: false + login_source: 0 + login_name: user7 + type: 1 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: false is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar7 avatar_email: user7@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 0 - num_members: 1 num_teams: 1 + num_members: 1 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 8 lower_name: user8 name: user8 - login_name: user8 full_name: User Eight email: user8@example.com + keep_email_private: false email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user8 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar8 avatar_email: user8@example.com - num_repos: 0 - is_active: true + use_custom_avatar: false num_followers: 1 num_following: 1 + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 9 lower_name: user9 name: user9 - login_name: user9 full_name: User Nine email: user9@example.com + keep_email_private: false email_notifications_preference: onmention + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user9 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: false is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar9 avatar_email: user9@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 0 - is_active: false + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 10 lower_name: user10 name: user10 - login_name: user10 full_name: User Ten email: user10@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user10 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar10 avatar_email: user10@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 3 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 11 lower_name: user11 name: user11 - login_name: user11 full_name: User Eleven email: user11@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user11 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar11 avatar_email: user11@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 1 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 12 lower_name: user12 name: user12 - login_name: user12 full_name: User 12 email: user12@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user12 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar12 avatar_email: user12@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 1 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 13 lower_name: user13 name: user13 - login_name: user13 full_name: User 13 email: user13@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user13 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar13 avatar_email: user13@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 1 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 14 lower_name: user14 name: user14 - login_name: user14 full_name: User 14 email: user14@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user14 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar14 avatar_email: user13@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 3 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 15 lower_name: user15 name: user15 - login_name: user15 full_name: User 15 email: user15@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user15 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar15 avatar_email: user15@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 4 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 16 lower_name: user16 name: user16 - login_name: user16 full_name: User 16 email: user16@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user16 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar16 avatar_email: user16@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 2 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 17 lower_name: user17 name: user17 - login_name: user17 full_name: User 17 email: user17@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 1 # organization + must_change_password: false + login_source: 0 + login_name: user17 + type: 1 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar17 avatar_email: user17@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 2 - is_active: true - num_members: 4 num_teams: 3 + num_members: 4 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 18 lower_name: user18 name: user18 - login_name: user18 full_name: User 18 email: user18@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user18 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar18 avatar_email: user18@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 0 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 19 lower_name: user19 name: user19 - login_name: user19 full_name: User 19 email: user19@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 1 # organization + must_change_password: false + login_source: 0 + login_name: user19 + type: 1 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar19 avatar_email: user19@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 2 - is_active: true - num_members: 2 num_teams: 1 + num_members: 2 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 20 lower_name: user20 name: user20 - login_name: user20 full_name: User 20 email: user20@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user20 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar20 avatar_email: user20@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 4 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 21 lower_name: user21 name: user21 - login_name: user21 full_name: User 21 email: user21@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user21 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar21 avatar_email: user21@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 2 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 22 lower_name: limited_org name: limited_org - login_name: limited_org full_name: Limited Org email: limited_org@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 1 # organization + must_change_password: false + login_source: 0 + login_name: limited_org + type: 1 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar22 avatar_email: limited_org@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 2 - is_active: true - num_members: 0 num_teams: 0 + num_members: 0 visibility: 1 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 23 lower_name: privated_org name: privated_org - login_name: privated_org full_name: Privated Org email: privated_org@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 1 # organization + must_change_password: false + login_source: 0 + login_name: privated_org + type: 1 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar23 avatar_email: privated_org@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 2 - is_active: true - num_members: 0 num_teams: 0 + num_members: 0 visibility: 2 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 24 lower_name: user24 name: user24 - login_name: user24 - full_name: "user24" + full_name: user24 email: user24@example.com keep_email_private: true + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user24 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar24 avatar_email: user24@example.com - num_repos: 0 - num_stars: 0 + use_custom_avatar: false num_followers: 0 num_following: 0 - is_active: true + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 25 lower_name: org25 name: org25 - login_name: org25 - full_name: "org25" + full_name: org25 email: org25@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 1 # organization + must_change_password: false + login_source: 0 + login_name: org25 + type: 1 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: false is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar25 avatar_email: org25@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 0 - num_members: 1 num_teams: 1 + num_members: 1 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 26 lower_name: org26 name: org26 - login_name: org26 - full_name: "Org26" + full_name: Org26 email: org26@example.com + keep_email_private: false email_notifications_preference: onmention + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 1 # organization + must_change_password: false + login_source: 0 + login_name: org26 + type: 1 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: false is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar26 avatar_email: org26@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 4 - num_members: 0 num_teams: 1 + num_members: 0 + visibility: 0 repo_admin_change_team_access: true + theme: "" + keep_activity_private: false - id: 27 lower_name: user27 name: user27 - login_name: user27 full_name: User Twenty-Seven email: user27@example.com + keep_email_private: false email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user27 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar27 avatar_email: user27@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 3 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 28 lower_name: user28 name: user28 - login_name: user28 - full_name: "user27" + full_name: user27 email: user28@example.com keep_email_private: true + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user28 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar28 avatar_email: user28@example.com - num_repos: 0 - num_stars: 0 + use_custom_avatar: false num_followers: 0 num_following: 0 - is_active: true + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 29 lower_name: user29 name: user29 - login_name: user29 full_name: User 29 email: user29@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user29 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false is_restricted: true + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar29 avatar_email: user29@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 0 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 30 lower_name: user30 name: user30 - login_name: user30 full_name: User Thirty email: user30@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user30 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false is_restricted: true + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: true avatar: avatar29 avatar_email: user30@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 3 - is_active: true - prohibit_login: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 31 lower_name: user31 name: user31 - login_name: user31 - full_name: "user31" + full_name: user31 email: user31@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user31 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false - visibility: 2 + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar31 avatar_email: user31@example.com - num_repos: 0 + use_custom_avatar: false num_followers: 0 num_following: 1 - is_active: true + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 2 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 32 lower_name: user32 name: user32 - login_name: user32 full_name: User 32 (U2F test) email: user32@example.com - passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password - type: 0 # individual + keep_email_private: false + email_notifications_preference: enabled + passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a + passwd_hash_algo: argon2 + must_change_password: false + login_source: 0 + login_name: user32 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar32 avatar_email: user30@example.com + use_custom_avatar: false + num_followers: 0 + num_following: 0 + num_stars: 0 num_repos: 0 - is_active: true + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false - id: 33 lower_name: user33 name: user33 - login_name: user33 full_name: User 33 (Limited Visibility) email: user33@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b passwd_hash_algo: argon2 - passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password - type: 0 # individual + must_change_password: false + login_source: 0 + login_name: user33 + type: 0 salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true is_admin: false - visibility: 1 + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false avatar: avatar33 avatar_email: user33@example.com - num_repos: 0 + use_custom_avatar: false num_followers: 1 num_following: 0 - is_active: true + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 1 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false diff --git a/models/fixtures/webauthn_credential.yml b/models/fixtures/webauthn_credential.yml index b4109a03f2..bc43127fcd 100644 --- a/models/fixtures/webauthn_credential.yml +++ b/models/fixtures/webauthn_credential.yml @@ -1,5 +1,6 @@ -- id: 1 - name: "WebAuthn credential" +- + id: 1 + name: WebAuthn credential user_id: 32 attestation_type: none sign_count: 0 diff --git a/models/foreignreference/error.go b/models/foreignreference/error.go index d783a08730..a1db773cd2 100644 --- a/models/foreignreference/error.go +++ b/models/foreignreference/error.go @@ -6,6 +6,8 @@ package foreignreference import ( "fmt" + + "code.gitea.io/gitea/modules/util" ) // ErrLocalIndexNotExist represents a "LocalIndexNotExist" kind of error. @@ -25,6 +27,10 @@ func (err ErrLocalIndexNotExist) Error() string { return fmt.Sprintf("repository %d has no LocalIndex for ForeignIndex %d of type %s", err.RepoID, err.ForeignIndex, err.Type) } +func (err ErrLocalIndexNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrForeignIndexNotExist represents a "ForeignIndexNotExist" kind of error. type ErrForeignIndexNotExist struct { RepoID int64 @@ -41,3 +47,7 @@ func IsErrForeignIndexNotExist(err error) bool { func (err ErrForeignIndexNotExist) Error() string { return fmt.Sprintf("repository %d has no ForeignIndex for LocalIndex %d of type %s", err.RepoID, err.LocalIndex, err.Type) } + +func (err ErrForeignIndexNotExist) Unwrap() error { + return util.ErrNotExist +} diff --git a/models/git/lfs.go b/models/git/lfs.go index 179da3120a..1dab31d5f9 100644 --- a/models/git/lfs.go +++ b/models/git/lfs.go @@ -6,7 +6,6 @@ package git import ( "context" - "errors" "fmt" "code.gitea.io/gitea/models/db" @@ -17,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -38,6 +38,10 @@ func (err ErrLFSLockNotExist) Error() string { return fmt.Sprintf("lfs lock does not exist [id: %d, rid: %d, path: %s]", err.ID, err.RepoID, err.Path) } +func (err ErrLFSLockNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrLFSUnauthorizedAction represents a "LFSUnauthorizedAction" kind of error. type ErrLFSUnauthorizedAction struct { RepoID int64 @@ -58,6 +62,10 @@ func (err ErrLFSUnauthorizedAction) Error() string { return fmt.Sprintf("User %s doesn't have read access for lfs lock [rid: %d]", err.UserName, err.RepoID) } +func (err ErrLFSUnauthorizedAction) Unwrap() error { + return util.ErrPermissionDenied +} + // ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error. type ErrLFSLockAlreadyExist struct { RepoID int64 @@ -74,6 +82,10 @@ func (err ErrLFSLockAlreadyExist) Error() string { return fmt.Sprintf("lfs lock already exists [rid: %d, path: %s]", err.RepoID, err.Path) } +func (err ErrLFSLockAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrLFSFileLocked represents a "LFSFileLocked" kind of error. type ErrLFSFileLocked struct { RepoID int64 @@ -91,6 +103,10 @@ func (err ErrLFSFileLocked) Error() string { return fmt.Sprintf("File is lfs locked [repo: %d, locked by: %s, path: %s]", err.RepoID, err.UserName, err.Path) } +func (err ErrLFSFileLocked) Unwrap() error { + return util.ErrPermissionDenied +} + // LFSMetaObject stores metadata for LFS tracked files. type LFSMetaObject struct { ID int64 `xorm:"pk autoincr"` @@ -114,7 +130,7 @@ type LFSTokenResponse struct { // ErrLFSObjectNotExist is returned from lfs models functions in order // to differentiate between database and missing object errors. -var ErrLFSObjectNotExist = errors.New("LFS Meta object does not exist") +var ErrLFSObjectNotExist = db.ErrNotExist{Resource: "LFS Meta object"} // NewLFSMetaObject stores a given populated LFSMetaObject structure in the database // if it is not already present. diff --git a/models/issues/comment.go b/models/issues/comment.go index a71afda9e0..9ab6cab7d0 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -28,6 +28,7 @@ import ( "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" "xorm.io/xorm" @@ -49,6 +50,10 @@ func (err ErrCommentNotExist) Error() string { return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID) } +func (err ErrCommentNotExist) Unwrap() error { + return util.ErrNotExist +} + // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. type CommentType int diff --git a/models/issues/content_history.go b/models/issues/content_history.go index 3e321784bd..f5cfa65b8f 100644 --- a/models/issues/content_history.go +++ b/models/issues/content_history.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -201,6 +202,10 @@ func (err ErrIssueContentHistoryNotExist) Error() string { return fmt.Sprintf("issue content history does not exist [id: %d]", err.ID) } +func (err ErrIssueContentHistoryNotExist) Unwrap() error { + return util.ErrNotExist +} + // GetIssueContentHistoryByID get issue content history func GetIssueContentHistoryByID(dbCtx context.Context, id int64) (*ContentHistory, error) { h := &ContentHistory{} diff --git a/models/issues/dependency.go b/models/issues/dependency.go index d664c0758e..4754ed0f5f 100644 --- a/models/issues/dependency.go +++ b/models/issues/dependency.go @@ -11,6 +11,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" ) // ErrDependencyExists represents a "DependencyAlreadyExists" kind of error. @@ -29,6 +30,10 @@ func (err ErrDependencyExists) Error() string { return fmt.Sprintf("issue dependency does already exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID) } +func (err ErrDependencyExists) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrDependencyNotExists represents a "DependencyAlreadyExists" kind of error. type ErrDependencyNotExists struct { IssueID int64 @@ -45,6 +50,10 @@ func (err ErrDependencyNotExists) Error() string { return fmt.Sprintf("issue dependency does not exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID) } +func (err ErrDependencyNotExists) Unwrap() error { + return util.ErrNotExist +} + // ErrCircularDependency represents a "DependencyCircular" kind of error. type ErrCircularDependency struct { IssueID int64 @@ -91,6 +100,10 @@ func (err ErrUnknownDependencyType) Error() string { return fmt.Sprintf("unknown dependency type [type: %d]", err.Type) } +func (err ErrUnknownDependencyType) Unwrap() error { + return util.ErrInvalidArgument +} + // IssueDependency represents an issue dependency type IssueDependency struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/issues/issue.go b/models/issues/issue.go index 49bc229c6b..f77166db11 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -13,7 +13,6 @@ import ( "strconv" "strings" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/foreignreference" "code.gitea.io/gitea/models/organization" @@ -21,6 +20,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" @@ -52,6 +52,10 @@ func (err ErrIssueNotExist) Error() string { return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) } +func (err ErrIssueNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrIssueIsClosed represents a "IssueIsClosed" kind of error. type ErrIssueIsClosed struct { ID int64 @@ -1064,19 +1068,19 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue // NewIssue creates new issue with labels for repository. func NewIssue(repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { - idx, err := db.GetNextResourceIndex("issue_index", repo.ID) - if err != nil { - return fmt.Errorf("generate issue index failed: %v", err) - } - - issue.Index = idx - ctx, committer, err := db.TxContext() if err != nil { return err } defer committer.Close() + idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID) + if err != nil { + return fmt.Errorf("generate issue index failed: %w", err) + } + + issue.Index = idx + if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, Issue: issue, @@ -1492,7 +1496,8 @@ func applySubscribedCondition(sess *xorm.Session, subscriberID int64) *xorm.Sess builder.In("issue.repo_id", builder. Select("id"). From("watch"). - Where(builder.Eq{"user_id": subscriberID, "mode": true}), + Where(builder.And(builder.Eq{"user_id": subscriberID}, + builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))), ), ), ) @@ -2470,7 +2475,7 @@ func DeleteOrphanedIssues() error { // Remove issue attachment files. for i := range attachmentPaths { - admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i]) + system_model.RemoveAllWithNotice(db.DefaultContext, "Delete issue attachment", attachmentPaths[i]) } return nil } diff --git a/models/issues/issue_index.go b/models/issues/issue_index.go index 100e814317..f4acc5aa1b 100644 --- a/models/issues/issue_index.go +++ b/models/issues/issue_index.go @@ -15,16 +15,12 @@ func RecalculateIssueIndexForRepo(repoID int64) error { } defer committer.Close() - if err := db.UpsertResourceIndex(ctx, "issue_index", repoID); err != nil { - return err - } - var max int64 - if _, err := db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil { + if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil { return err } - if _, err := db.GetEngine(ctx).Exec("UPDATE `issue_index` SET max_index=? WHERE group_id=?", max, repoID); err != nil { + if err = db.SyncMaxResourceIndex(ctx, "issue_index", repoID, max); err != nil { return err } diff --git a/models/issues/issue_xref_test.go b/models/issues/issue_xref_test.go index af1c8bc685..0f72fc7ca6 100644 --- a/models/issues/issue_xref_test.go +++ b/models/issues/issue_xref_test.go @@ -131,7 +131,11 @@ func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispu r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo}) d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}) - idx, err := db.GetNextResourceIndex("issue_index", r.ID) + ctx, committer, err := db.TxContext() + assert.NoError(t, err) + defer committer.Close() + + idx, err := db.GetNextResourceIndex(ctx, "issue_index", r.ID) assert.NoError(t, err) i := &issues_model.Issue{ RepoID: r.ID, @@ -143,9 +147,6 @@ func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispu Index: idx, } - ctx, committer, err := db.TxContext() - assert.NoError(t, err) - defer committer.Close() err = issues_model.NewIssueWithIndex(ctx, d, issues_model.NewIssueOptions{ Repo: r, Issue: i, diff --git a/models/issues/label.go b/models/issues/label.go index 667a608687..be97454e26 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -38,6 +38,10 @@ func (err ErrRepoLabelNotExist) Error() string { return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID) } +func (err ErrRepoLabelNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error. type ErrOrgLabelNotExist struct { LabelID int64 @@ -54,6 +58,10 @@ func (err ErrOrgLabelNotExist) Error() string { return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID) } +func (err ErrOrgLabelNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrLabelNotExist represents a "LabelNotExist" kind of error. type ErrLabelNotExist struct { LabelID int64 @@ -69,6 +77,10 @@ func (err ErrLabelNotExist) Error() string { return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID) } +func (err ErrLabelNotExist) Unwrap() error { + return util.ErrNotExist +} + // LabelColorPattern is a regexp witch can validate LabelColor var LabelColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") diff --git a/models/issues/milestone.go b/models/issues/milestone.go index 1021938b20..3ccade7411 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -39,6 +40,10 @@ func (err ErrMilestoneNotExist) Error() string { return fmt.Sprintf("milestone does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID) } +func (err ErrMilestoneNotExist) Unwrap() error { + return util.ErrNotExist +} + // Milestone represents a milestone of repository. type Milestone struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/issues/pull.go b/models/issues/pull.go index f96b03445e..18b67eb305 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -46,6 +46,10 @@ func (err ErrPullRequestNotExist) Error() string { err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch) } +func (err ErrPullRequestNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error type ErrPullRequestAlreadyExists struct { ID int64 @@ -68,6 +72,10 @@ func (err ErrPullRequestAlreadyExists) Error() string { err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch) } +func (err ErrPullRequestAlreadyExists) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrPullRequestHeadRepoMissing represents a "ErrPullRequestHeadRepoMissing" error type ErrPullRequestHeadRepoMissing struct { ID int64 @@ -490,13 +498,6 @@ func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) { // NewPullRequest creates new pull request with labels for repository. func NewPullRequest(outerCtx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) { - idx, err := db.GetNextResourceIndex("issue_index", repo.ID) - if err != nil { - return fmt.Errorf("generate pull request index failed: %v", err) - } - - issue.Index = idx - ctx, committer, err := db.TxContext() if err != nil { return err @@ -504,6 +505,13 @@ func NewPullRequest(outerCtx context.Context, repo *repo_model.Repository, issue defer committer.Close() ctx.WithContext(outerCtx) + idx, err := db.GetNextResourceIndex(ctx, "issue_index", repo.ID) + if err != nil { + return fmt.Errorf("generate pull request index failed: %v", err) + } + + issue.Index = idx + if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{ Repo: repo, Issue: issue, diff --git a/models/issues/reaction.go b/models/issues/reaction.go index ccda10be2c..02cffad3ba 100644 --- a/models/issues/reaction.go +++ b/models/issues/reaction.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -34,6 +35,10 @@ func (err ErrForbiddenIssueReaction) Error() string { return fmt.Sprintf("'%s' is not an allowed reaction", err.Reaction) } +func (err ErrForbiddenIssueReaction) Unwrap() error { + return util.ErrPermissionDenied +} + // ErrReactionAlreadyExist is used when a existing reaction was try to created type ErrReactionAlreadyExist struct { Reaction string @@ -49,6 +54,10 @@ func (err ErrReactionAlreadyExist) Error() string { return fmt.Sprintf("reaction '%s' already exists", err.Reaction) } +func (err ErrReactionAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // Reaction represents a reactions on issues and comments. type Reaction struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/issues/review.go b/models/issues/review.go index 5835900801..26fcea9eef 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -39,6 +39,10 @@ func (err ErrReviewNotExist) Error() string { return fmt.Sprintf("review does not exist [id: %d]", err.ID) } +func (err ErrReviewNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrNotValidReviewRequest an not allowed review request modify type ErrNotValidReviewRequest struct { Reason string @@ -59,6 +63,10 @@ func (err ErrNotValidReviewRequest) Error() string { err.RepoID) } +func (err ErrNotValidReviewRequest) Unwrap() error { + return util.ErrInvalidArgument +} + // ReviewType defines the sort of feedback a review gives type ReviewType int diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go index 0a7ad41f9c..a87fbfafa2 100644 --- a/models/issues/stopwatch.go +++ b/models/issues/stopwatch.go @@ -25,6 +25,10 @@ func (err ErrIssueStopwatchNotExist) Error() string { return fmt.Sprintf("issue stopwatch doesn't exist[uid: %d, issue_id: %d", err.UserID, err.IssueID) } +func (err ErrIssueStopwatchNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrIssueStopwatchAlreadyExist represents an error that stopwatch is already exist type ErrIssueStopwatchAlreadyExist struct { UserID int64 @@ -35,6 +39,10 @@ func (err ErrIssueStopwatchAlreadyExist) Error() string { return fmt.Sprintf("issue stopwatch already exists[uid: %d, issue_id: %d", err.UserID, err.IssueID) } +func (err ErrIssueStopwatchAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // Stopwatch represents a stopwatch for time tracking. type Stopwatch struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index 9f8767362f..ca21eb5149 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -236,7 +236,7 @@ func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error { return err } if removedTime == 0 { - return db.ErrNotExist{} + return db.ErrNotExist{Resource: "tracked_time"} } if err := issue.LoadRepo(ctx); err != nil { @@ -296,7 +296,7 @@ func deleteTimes(ctx context.Context, opts FindTrackedTimesOptions) (removedTime func deleteTime(ctx context.Context, t *TrackedTime) error { if t.Deleted { - return db.ErrNotExist{ID: t.ID} + return db.ErrNotExist{Resource: "tracked_time", ID: t.ID} } t.Deleted = true _, err := db.GetEngine(ctx).ID(t.ID).Cols("deleted").Update(t) @@ -310,7 +310,7 @@ func GetTrackedTimeByID(id int64) (*TrackedTime, error) { if err != nil { return nil, err } else if !has { - return nil, db.ErrNotExist{ID: id} + return nil, db.ErrNotExist{Resource: "tracked_time", ID: id} } return time, nil } diff --git a/models/main_test.go b/models/main_test.go index 49b6e3e560..3584001569 100644 --- a/models/main_test.go +++ b/models/main_test.go @@ -14,6 +14,8 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + _ "code.gitea.io/gitea/models/system" + "github.com/stretchr/testify/assert" ) diff --git a/models/migrations/fixtures/Test_addConfidentialClientColumnToOAuth2ApplicationTable/o_auth2_application.yml b/models/migrations/fixtures/Test_addConfidentialClientColumnToOAuth2ApplicationTable/o_auth2_application.yml new file mode 100644 index 0000000000..a88c2ef89f --- /dev/null +++ b/models/migrations/fixtures/Test_addConfidentialClientColumnToOAuth2ApplicationTable/o_auth2_application.yml @@ -0,0 +1,2 @@ +- + id: 1 diff --git a/models/migrations/fixtures/Test_updateOpenMilestoneCounts/expected_milestone.yml b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/expected_milestone.yml new file mode 100644 index 0000000000..9326fa550b --- /dev/null +++ b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/expected_milestone.yml @@ -0,0 +1,19 @@ +# type Milestone struct { +# ID int64 `xorm:"pk autoincr"` +# IsClosed bool +# NumIssues int +# NumClosedIssues int +# Completeness int // Percentage(1-100). +# } +- + id: 1 + is_closed: false + num_issues: 3 + num_closed_issues: 1 + completeness: 33 +- + id: 2 + is_closed: true + num_issues: 5 + num_closed_issues: 5 + completeness: 100 diff --git a/models/migrations/fixtures/Test_updateOpenMilestoneCounts/issue.yml b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/issue.yml new file mode 100644 index 0000000000..fdaacd9f68 --- /dev/null +++ b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/issue.yml @@ -0,0 +1,25 @@ +# type Issue struct { +# ID int64 `xorm:"pk autoincr"` +# RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"` +# Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository. +# MilestoneID int64 `xorm:"INDEX"` +# IsClosed bool `xorm:"INDEX"` +# } +- + id: 1 + repo_id: 1 + index: 1 + milestone_id: 1 + is_closed: false +- + id: 2 + repo_id: 1 + index: 2 + milestone_id: 1 + is_closed: true +- + id: 4 + repo_id: 1 + index: 3 + milestone_id: 1 + is_closed: false diff --git a/models/migrations/fixtures/Test_updateOpenMilestoneCounts/milestone.yml b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/milestone.yml new file mode 100644 index 0000000000..0bcf4cffd3 --- /dev/null +++ b/models/migrations/fixtures/Test_updateOpenMilestoneCounts/milestone.yml @@ -0,0 +1,19 @@ +# type Milestone struct { +# ID int64 `xorm:"pk autoincr"` +# IsClosed bool +# NumIssues int +# NumClosedIssues int +# Completeness int // Percentage(1-100). +# } +- + id: 1 + is_closed: false + num_issues: 4 + num_closed_issues: 2 + completeness: 50 +- + id: 2 + is_closed: true + num_issues: 5 + num_closed_issues: 5 + completeness: 100 diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 2a38772180..a6201c1090 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -415,6 +415,14 @@ var migrations = []Migration{ NewMigration("Alter gpg_key/public_key content TEXT fields to MEDIUMTEXT", alterPublicGPGKeyContentFieldsToMediumText), // v226 -> v227 NewMigration("Conan and generic packages do not need to be semantically versioned", fixPackageSemverField), + // v227 -> v228 + NewMigration("Create key/value table for system settings", createSystemSettingsTable), + // v228 -> v229 + NewMigration("Add TeamInvite table", addTeamInviteTable), + // v229 -> v230 + NewMigration("Update counts of all open milestones", updateOpenMilestoneCounts), + // v230 -> v231 + NewMigration("Add ConfidentialClient column (default true) to OAuth2Application table", addConfidentialClientColumnToOAuth2ApplicationTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v128.go b/models/migrations/v128.go index 2aa68f9b64..aba0ed6897 100644 --- a/models/migrations/v128.go +++ b/models/migrations/v128.go @@ -83,17 +83,17 @@ func fixMergeBase(x *xorm.Engine) error { if !pr.HasMerged { var err error - pr.MergeBase, _, err = git.NewCommand(git.DefaultContext, "merge-base", "--", pr.BaseBranch, gitRefName).RunStdString(&git.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = git.NewCommand(git.DefaultContext, "merge-base").AddDashesAndList(pr.BaseBranch, gitRefName).RunStdString(&git.RunOpts{Dir: repoPath}) if err != nil { var err2 error - pr.MergeBase, _, err2 = git.NewCommand(git.DefaultContext, "rev-parse", git.BranchPrefix+pr.BaseBranch).RunStdString(&git.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err2 = git.NewCommand(git.DefaultContext, "rev-parse").AddDynamicArguments(git.BranchPrefix + pr.BaseBranch).RunStdString(&git.RunOpts{Dir: repoPath}) if err2 != nil { log.Error("Unable to get merge base for PR ID %d, Index %d in %s/%s. Error: %v & %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err, err2) continue } } } else { - parentsString, _, err := git.NewCommand(git.DefaultContext, "rev-list", "--parents", "-n", "1", pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath}) + parentsString, _, err := git.NewCommand(git.DefaultContext, "rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath}) if err != nil { log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue @@ -103,10 +103,11 @@ func fixMergeBase(x *xorm.Engine) error { continue } - args := append([]string{"merge-base", "--"}, parents[1:]...) - args = append(args, gitRefName) + refs := append([]string{}, parents[1:]...) + refs = append(refs, gitRefName) + cmd := git.NewCommand(git.DefaultContext, "merge-base").AddDashesAndList(refs...) - pr.MergeBase, _, err = git.NewCommand(git.DefaultContext, args...).RunStdString(&git.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath}) if err != nil { log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue diff --git a/models/migrations/v134.go b/models/migrations/v134.go index 16e8ec8e94..06d0f8276a 100644 --- a/models/migrations/v134.go +++ b/models/migrations/v134.go @@ -80,7 +80,7 @@ func refixMergeBase(x *xorm.Engine) error { gitRefName := fmt.Sprintf("refs/pull/%d/head", pr.Index) - parentsString, _, err := git.NewCommand(git.DefaultContext, "rev-list", "--parents", "-n", "1", pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath}) + parentsString, _, err := git.NewCommand(git.DefaultContext, "rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath}) if err != nil { log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue @@ -91,10 +91,11 @@ func refixMergeBase(x *xorm.Engine) error { } // we should recalculate - args := append([]string{"merge-base", "--"}, parents[1:]...) - args = append(args, gitRefName) + refs := append([]string{}, parents[1:]...) + refs = append(refs, gitRefName) + cmd := git.NewCommand(git.DefaultContext, "merge-base").AddDashesAndList(refs...) - pr.MergeBase, _, err = git.NewCommand(git.DefaultContext, args...).RunStdString(&git.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath}) if err != nil { log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue diff --git a/models/migrations/v227.go b/models/migrations/v227.go new file mode 100644 index 0000000000..8a3a9c877e --- /dev/null +++ b/models/migrations/v227.go @@ -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 migrations + +import ( + "fmt" + "strconv" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +type SystemSetting struct { + ID int64 `xorm:"pk autoincr"` + SettingKey string `xorm:"varchar(255) unique"` // ensure key is always lowercase + SettingValue string `xorm:"text"` + Version int `xorm:"version"` // prevent to override + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` +} + +func insertSettingsIfNotExist(x *xorm.Engine, sysSettings []*SystemSetting) error { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + for _, setting := range sysSettings { + exist, err := sess.Table("system_setting").Where("setting_key=?", setting.SettingKey).Exist() + if err != nil { + return err + } + if !exist { + if _, err := sess.Insert(setting); err != nil { + return err + } + } + } + return sess.Commit() +} + +func createSystemSettingsTable(x *xorm.Engine) error { + if err := x.Sync2(new(SystemSetting)); err != nil { + return fmt.Errorf("sync2: %v", err) + } + + // migrate xx to database + sysSettings := []*SystemSetting{ + { + SettingKey: "picture.disable_gravatar", + SettingValue: strconv.FormatBool(setting.DisableGravatar), + }, + { + SettingKey: "picture.enable_federated_avatar", + SettingValue: strconv.FormatBool(setting.EnableFederatedAvatar), + }, + } + + return insertSettingsIfNotExist(x, sysSettings) +} diff --git a/models/migrations/v228.go b/models/migrations/v228.go new file mode 100644 index 0000000000..62c81ef9d8 --- /dev/null +++ b/models/migrations/v228.go @@ -0,0 +1,26 @@ +// 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 ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func addTeamInviteTable(x *xorm.Engine) error { + type TeamInvite struct { + ID int64 `xorm:"pk autoincr"` + Token string `xorm:"UNIQUE(token) INDEX NOT NULL DEFAULT ''"` + InviterID int64 `xorm:"NOT NULL DEFAULT 0"` + OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0"` + TeamID int64 `xorm:"UNIQUE(team_mail) INDEX NOT NULL DEFAULT 0"` + Email string `xorm:"UNIQUE(team_mail) NOT NULL DEFAULT ''"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + + return x.Sync2(new(TeamInvite)) +} diff --git a/models/migrations/v229.go b/models/migrations/v229.go new file mode 100644 index 0000000000..42ec2033fe --- /dev/null +++ b/models/migrations/v229.go @@ -0,0 +1,47 @@ +// 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 ( + "fmt" + + "code.gitea.io/gitea/models/issues" + + "xorm.io/builder" + "xorm.io/xorm" +) + +func updateOpenMilestoneCounts(x *xorm.Engine) error { + var openMilestoneIDs []int64 + err := x.Table("milestone").Select("id").Where(builder.Neq{"is_closed": 1}).Find(&openMilestoneIDs) + if err != nil { + return fmt.Errorf("error selecting open milestone IDs: %w", err) + } + + for _, id := range openMilestoneIDs { + _, err := x.ID(id). + SetExpr("num_issues", builder.Select("count(*)").From("issue").Where( + builder.Eq{"milestone_id": id}, + )). + SetExpr("num_closed_issues", builder.Select("count(*)").From("issue").Where( + builder.Eq{ + "milestone_id": id, + "is_closed": true, + }, + )). + Update(&issues.Milestone{}) + if err != nil { + return fmt.Errorf("error updating issue counts in milestone %d: %w", id, err) + } + _, err = x.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?", + id, + ) + if err != nil { + return fmt.Errorf("error setting completeness on milestone %d: %w", id, err) + } + } + + return nil +} diff --git a/models/migrations/v229_test.go b/models/migrations/v229_test.go new file mode 100644 index 0000000000..f8a147c9bd --- /dev/null +++ b/models/migrations/v229_test.go @@ -0,0 +1,46 @@ +// 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 ( + "testing" + + "code.gitea.io/gitea/models/issues" + + "github.com/stretchr/testify/assert" +) + +func Test_updateOpenMilestoneCounts(t *testing.T) { + type ExpectedMilestone issues.Milestone + + // Prepare and load the testing database + x, deferable := prepareTestEnv(t, 0, new(issues.Milestone), new(ExpectedMilestone), new(issues.Issue)) + defer deferable() + if x == nil || t.Failed() { + return + } + + if err := updateOpenMilestoneCounts(x); err != nil { + assert.NoError(t, err) + return + } + + expected := []ExpectedMilestone{} + if err := x.Table("expected_milestone").Asc("id").Find(&expected); !assert.NoError(t, err) { + return + } + + got := []issues.Milestone{} + if err := x.Table("milestone").Asc("id").Find(&got); !assert.NoError(t, err) { + return + } + + for i, e := range expected { + got := got[i] + assert.Equal(t, e.ID, got.ID) + assert.Equal(t, e.NumIssues, got.NumIssues) + assert.Equal(t, e.NumClosedIssues, got.NumClosedIssues) + } +} diff --git a/models/migrations/v230.go b/models/migrations/v230.go new file mode 100644 index 0000000000..f08e6a3764 --- /dev/null +++ b/models/migrations/v230.go @@ -0,0 +1,18 @@ +// 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" +) + +// addConfidentialColumnToOAuth2ApplicationTable: add ConfidentialClient column, setting existing rows to true +func addConfidentialClientColumnToOAuth2ApplicationTable(x *xorm.Engine) error { + type OAuth2Application struct { + ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"` + } + + return x.Sync(new(OAuth2Application)) +} diff --git a/models/migrations/v230_test.go b/models/migrations/v230_test.go new file mode 100644 index 0000000000..98ba3f5d97 --- /dev/null +++ b/models/migrations/v230_test.go @@ -0,0 +1,46 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_addConfidentialClientColumnToOAuth2ApplicationTable(t *testing.T) { + // premigration + type OAuth2Application struct { + ID int64 + } + + // Prepare and load the testing database + x, deferable := prepareTestEnv(t, 0, new(OAuth2Application)) + defer deferable() + if x == nil || t.Failed() { + return + } + + if err := addConfidentialClientColumnToOAuth2ApplicationTable(x); err != nil { + assert.NoError(t, err) + return + } + + // postmigration + type ExpectedOAuth2Application struct { + ID int64 + ConfidentialClient bool + } + + got := []ExpectedOAuth2Application{} + if err := x.Table("o_auth2_application").Select("id, confidential_client").Find(&got); !assert.NoError(t, err) { + return + } + + assert.NotEmpty(t, got) + for _, e := range got { + assert.True(t, e.ConfidentialClient) + } +} diff --git a/models/org_team.go b/models/org_team.go index 61ddd2a047..6066e7f5c9 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -431,25 +431,15 @@ func DeleteTeam(t *organization.Team) error { } } - // Delete team-user. - if _, err := sess. - Where("org_id=?", t.OrgID). - Where("team_id=?", t.ID). - Delete(new(organization.TeamUser)); err != nil { + if err := db.DeleteBeans(ctx, + &organization.Team{ID: t.ID}, + &organization.TeamUser{OrgID: t.OrgID, TeamID: t.ID}, + &organization.TeamUnit{TeamID: t.ID}, + &organization.TeamInvite{TeamID: t.ID}, + ); err != nil { return err } - // Delete team-unit. - if _, err := sess. - Where("team_id=?", t.ID). - Delete(new(organization.TeamUnit)); err != nil { - return err - } - - // Delete team. - if _, err := sess.ID(t.ID).Delete(new(organization.Team)); err != nil { - return err - } // Update organization number of teams. if _, err := sess.Exec("UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil { return err diff --git a/models/organization/org.go b/models/organization/org.go index 044ea06563..58b58e6732 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -45,6 +46,10 @@ func (err ErrOrgNotExist) Error() string { return fmt.Sprintf("org does not exist [id: %d, name: %s]", err.ID, err.Name) } +func (err ErrOrgNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrLastOrgOwner represents a "LastOrgOwner" kind of error. type ErrLastOrgOwner struct { UID int64 @@ -73,6 +78,10 @@ func (err ErrUserNotAllowedCreateOrg) Error() string { return "user is not allowed to create organizations" } +func (err ErrUserNotAllowedCreateOrg) Unwrap() error { + return util.ErrPermissionDenied +} + // Organization represents an organization type Organization user_model.User @@ -361,8 +370,9 @@ func DeleteOrganization(ctx context.Context, org *Organization) error { &OrgUser{OrgID: org.ID}, &TeamUser{OrgID: org.ID}, &TeamUnit{OrgID: org.ID}, + &TeamInvite{OrgID: org.ID}, ); err != nil { - return fmt.Errorf("deleteBeans: %v", err) + return fmt.Errorf("DeleteBeans: %v", err) } if _, err := db.GetEngine(ctx).ID(org.ID).Delete(new(user_model.User)); err != nil { diff --git a/models/organization/team.go b/models/organization/team.go index bd80b1a8c7..aa9b24b57f 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -43,6 +44,10 @@ func (err ErrTeamAlreadyExist) Error() string { return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name) } +func (err ErrTeamAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrTeamNotExist represents a "TeamNotExist" error type ErrTeamNotExist struct { OrgID int64 @@ -60,6 +65,10 @@ func (err ErrTeamNotExist) Error() string { return fmt.Sprintf("team does not exist [org_id %d, team_id %d, name: %s]", err.OrgID, err.TeamID, err.Name) } +func (err ErrTeamNotExist) Unwrap() error { + return util.ErrNotExist +} + // OwnerTeamName return the owner team name const OwnerTeamName = "Owners" @@ -85,6 +94,7 @@ func init() { db.RegisterModel(new(TeamUser)) db.RegisterModel(new(TeamRepo)) db.RegisterModel(new(TeamUnit)) + db.RegisterModel(new(TeamInvite)) } // SearchTeamOptions holds the search options diff --git a/models/organization/team_invite.go b/models/organization/team_invite.go new file mode 100644 index 0000000000..4504a2e9fe --- /dev/null +++ b/models/organization/team_invite.go @@ -0,0 +1,162 @@ +// 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 organization + +import ( + "context" + "fmt" + + "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" + + "xorm.io/builder" +) + +type ErrTeamInviteAlreadyExist struct { + TeamID int64 + Email string +} + +func IsErrTeamInviteAlreadyExist(err error) bool { + _, ok := err.(ErrTeamInviteAlreadyExist) + return ok +} + +func (err ErrTeamInviteAlreadyExist) Error() string { + return fmt.Sprintf("team invite already exists [team_id: %d, email: %s]", err.TeamID, err.Email) +} + +func (err ErrTeamInviteAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + +type ErrTeamInviteNotFound struct { + Token string +} + +func IsErrTeamInviteNotFound(err error) bool { + _, ok := err.(ErrTeamInviteNotFound) + return ok +} + +func (err ErrTeamInviteNotFound) Error() string { + return fmt.Sprintf("team invite was not found [token: %s]", err.Token) +} + +func (err ErrTeamInviteNotFound) Unwrap() error { + return util.ErrNotExist +} + +// ErrUserEmailAlreadyAdded represents a "user by email already added to team" error. +type ErrUserEmailAlreadyAdded struct { + Email string +} + +// IsErrUserEmailAlreadyAdded checks if an error is a ErrUserEmailAlreadyAdded. +func IsErrUserEmailAlreadyAdded(err error) bool { + _, ok := err.(ErrUserEmailAlreadyAdded) + return ok +} + +func (err ErrUserEmailAlreadyAdded) Error() string { + return fmt.Sprintf("user with email already added [email: %s]", err.Email) +} + +func (err ErrUserEmailAlreadyAdded) Unwrap() error { + return util.ErrAlreadyExist +} + +// TeamInvite represents an invite to a team +type TeamInvite struct { + ID int64 `xorm:"pk autoincr"` + Token string `xorm:"UNIQUE(token) INDEX NOT NULL DEFAULT ''"` + InviterID int64 `xorm:"NOT NULL DEFAULT 0"` + OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0"` + TeamID int64 `xorm:"UNIQUE(team_mail) INDEX NOT NULL DEFAULT 0"` + Email string `xorm:"UNIQUE(team_mail) NOT NULL DEFAULT ''"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} + +func CreateTeamInvite(ctx context.Context, doer *user_model.User, team *Team, email string) (*TeamInvite, error) { + has, err := db.GetEngine(ctx).Exist(&TeamInvite{ + TeamID: team.ID, + Email: email, + }) + if err != nil { + return nil, err + } + if has { + return nil, ErrTeamInviteAlreadyExist{ + TeamID: team.ID, + Email: email, + } + } + + // check if the user is already a team member by email + exist, err := db.GetEngine(ctx). + Where(builder.Eq{ + "team_user.org_id": team.OrgID, + "team_user.team_id": team.ID, + "`user`.email": email, + }). + Join("INNER", "`user`", "`user`.id = team_user.uid"). + Table("team_user"). + Exist() + if err != nil { + return nil, err + } + + if exist { + return nil, ErrUserEmailAlreadyAdded{ + Email: email, + } + } + + token, err := util.CryptoRandomString(25) + if err != nil { + return nil, err + } + + invite := &TeamInvite{ + Token: token, + InviterID: doer.ID, + OrgID: team.OrgID, + TeamID: team.ID, + Email: email, + } + + return invite, db.Insert(ctx, invite) +} + +func RemoveInviteByID(ctx context.Context, inviteID, teamID int64) error { + _, err := db.DeleteByBean(ctx, &TeamInvite{ + ID: inviteID, + TeamID: teamID, + }) + return err +} + +func GetInvitesByTeamID(ctx context.Context, teamID int64) ([]*TeamInvite, error) { + invites := make([]*TeamInvite, 0, 10) + return invites, db.GetEngine(ctx). + Where("team_id=?", teamID). + Find(&invites) +} + +func GetInviteByToken(ctx context.Context, token string) (*TeamInvite, error) { + invite := &TeamInvite{} + + has, err := db.GetEngine(ctx).Where("token=?", token).Get(invite) + if err != nil { + return nil, err + } + if !has { + return nil, ErrTeamInviteNotFound{Token: token} + } + return invite, nil +} diff --git a/models/organization/team_invite_test.go b/models/organization/team_invite_test.go new file mode 100644 index 0000000000..e0596ec28d --- /dev/null +++ b/models/organization/team_invite_test.go @@ -0,0 +1,49 @@ +// 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 organization_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func TestTeamInvite(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + + t.Run("MailExistsInTeam", func(t *testing.T) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // user 2 already added to team 2, should result in error + _, err := organization.CreateTeamInvite(db.DefaultContext, user2, team, user2.Email) + assert.Error(t, err) + }) + + t.Run("CreateAndRemove", func(t *testing.T) { + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + invite, err := organization.CreateTeamInvite(db.DefaultContext, user1, team, "user3@example.com") + assert.NotNil(t, invite) + assert.NoError(t, err) + + // Shouldn't allow duplicate invite + _, err = organization.CreateTeamInvite(db.DefaultContext, user1, team, "user3@example.com") + assert.Error(t, err) + + // should remove invite + assert.NoError(t, organization.RemoveInviteByID(db.DefaultContext, invite.ID, invite.TeamID)) + + // invite should not exist + _, err = organization.GetInviteByToken(db.DefaultContext, invite.Token) + assert.Error(t, err) + }) +} diff --git a/models/packages/container/search.go b/models/packages/container/search.go index a3409fe743..e4a5a53848 100644 --- a/models/packages/container/search.go +++ b/models/packages/container/search.go @@ -165,6 +165,7 @@ type ImageTagsSearchOptions struct { PackageID int64 Query string IsTagged bool + Sort packages.VersionSort db.Paginator } @@ -195,12 +196,26 @@ func (opts *ImageTagsSearchOptions) toConds() builder.Cond { return cond } +func (opts *ImageTagsSearchOptions) configureOrderBy(e db.Engine) { + switch opts.Sort { + case packages.SortVersionDesc: + e.Desc("package_version.version") + case packages.SortVersionAsc: + e.Asc("package_version.version") + case packages.SortCreatedAsc: + e.Asc("package_version.created_unix") + default: + e.Desc("package_version.created_unix") + } +} + // SearchImageTags gets a sorted list of the tags of an image func SearchImageTags(ctx context.Context, opts *ImageTagsSearchOptions) ([]*packages.PackageVersion, int64, error) { sess := db.GetEngine(ctx). Join("INNER", "package", "package.id = package_version.package_id"). - Where(opts.toConds()). - Desc("package_version.created_unix") + Where(opts.toConds()) + + opts.configureOrderBy(sess) if opts.Paginator != nil { sess = db.SetSessionPagination(sess, opts) diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 5479bae1c2..f9965bcb74 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -163,6 +163,17 @@ type SearchValue struct { ExactMatch bool } +type VersionSort = string + +const ( + SortNameAsc VersionSort = "name_asc" + SortNameDesc VersionSort = "name_desc" + SortVersionAsc VersionSort = "version_asc" + SortVersionDesc VersionSort = "version_desc" + SortCreatedAsc VersionSort = "created_asc" + SortCreatedDesc VersionSort = "created_desc" +) + // PackageSearchOptions are options for SearchXXX methods // Besides IsInternal are all fields optional and are not used if they have their default value (nil, "", 0) type PackageSearchOptions struct { @@ -176,7 +187,7 @@ type PackageSearchOptions struct { IsInternal util.OptionalBool HasFileWithName string // only results are found which are associated with a file with the specific name HasFiles util.OptionalBool // only results are found which have associated files - Sort string + Sort VersionSort db.Paginator } @@ -254,15 +265,15 @@ func (opts *PackageSearchOptions) toConds() builder.Cond { func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) { switch opts.Sort { - case "alphabetically": + case SortNameAsc: e.Asc("package.name") - case "reversealphabetically": + case SortNameDesc: e.Desc("package.name") - case "highestversion": + case SortVersionDesc: e.Desc("package_version.version") - case "lowestversion": + case SortVersionAsc: e.Asc("package_version.version") - case "oldest": + case SortCreatedAsc: e.Asc("package_version.created_unix") default: e.Desc("package_version.created_unix") diff --git a/models/project/project.go b/models/project/project.go index 86a77947d8..af2c8ac2af 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -55,6 +55,10 @@ func (err ErrProjectNotExist) Error() string { return fmt.Sprintf("projects does not exist [id: %d]", err.ID) } +func (err ErrProjectNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrProjectBoardNotExist represents a "ProjectBoardNotExist" kind of error. type ErrProjectBoardNotExist struct { BoardID int64 @@ -70,6 +74,10 @@ func (err ErrProjectBoardNotExist) Error() string { return fmt.Sprintf("project board does not exist [id: %d]", err.BoardID) } +func (err ErrProjectBoardNotExist) Unwrap() error { + return util.ErrNotExist +} + // Project represents a project board type Project struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/pull/automerge.go b/models/pull/automerge.go index d0aca2e85f..16ab5af093 100644 --- a/models/pull/automerge.go +++ b/models/pull/automerge.go @@ -90,7 +90,7 @@ func DeleteScheduledAutoMerge(ctx context.Context, pullID int64) error { if err != nil { return err } else if !exist { - return db.ErrNotExist{ID: pullID} + return db.ErrNotExist{Resource: "auto_merge", ID: pullID} } _, err = db.GetEngine(ctx).ID(scheduledPRM.ID).Delete(&AutoMerge{}) diff --git a/models/repo.go b/models/repo.go index 558678bfe5..08fbb0abea 100644 --- a/models/repo.go +++ b/models/repo.go @@ -22,6 +22,7 @@ import ( access_model "code.gitea.io/gitea/models/perm/access" project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" @@ -32,9 +33,13 @@ import ( "xorm.io/builder" ) -// NewRepoContext creates a new repository context -func NewRepoContext() { +// ItemsPerPage maximum items per page in forks, watchers and stars of a repo +var ItemsPerPage = 40 + +// Init initialize model +func Init() error { unit.LoadUnitConfig() + return system_model.Init() } // DeleteRepository deletes a repository for a user or organization. @@ -118,6 +123,11 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { return err } + if _, err := db.GetEngine(ctx).In("hook_id", builder.Select("id").From("webhook").Where(builder.Eq{"webhook.repo_id": repo.ID})). + Delete(&webhook.HookTask{}); err != nil { + return err + } + if err := db.DeleteBeans(ctx, &access_model.Access{RepoID: repo.ID}, &activities_model.Action{RepoID: repo.ID}, @@ -125,7 +135,6 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { &issues_model.Comment{RefRepoID: repoID}, &git_model.CommitStatus{RepoID: repoID}, &git_model.DeletedBranch{RepoID: repoID}, - &webhook.HookTask{RepoID: repoID}, &git_model.LFSLock{RepoID: repoID}, &repo_model.LanguageStat{RepoID: repoID}, &issues_model.Milestone{RepoID: repoID}, @@ -163,7 +172,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { } // Delete issue index - if err := db.DeleteResouceIndex(ctx, "issue_index", repoID); err != nil { + if err := db.DeleteResourceIndex(ctx, "issue_index", repoID); err != nil { return err } @@ -267,36 +276,36 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error { // Remove repository files. repoPath := repo.RepoPath() - admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository files", repoPath) + system_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository files", repoPath) // Remove wiki files if repo.HasWiki() { - admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository wiki", repo.WikiPath()) + system_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository wiki", repo.WikiPath()) } // Remove archives for _, archive := range archivePaths { - admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archive) + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archive) } // Remove lfs objects for _, lfsObj := range lfsPaths { - admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsObj) + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsObj) } // Remove issue attachment files. for _, attachment := range attachmentPaths { - admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachment) + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachment) } // Remove release attachment files. for _, releaseAttachment := range releaseAttachments { - admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachment) + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachment) } // Remove attachment with no issue_id and release_id. for _, newAttachment := range newAttachmentPaths { - admin_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", newAttachment) + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", newAttachment) } if len(repo.Avatar) > 0 { diff --git a/models/repo/attachment.go b/models/repo/attachment.go index afec78a425..5d4e11ae72 100644 --- a/models/repo/attachment.go +++ b/models/repo/attachment.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" ) // Attachment represent a attachment of issue/comment/release. @@ -83,6 +84,10 @@ func (err ErrAttachmentNotExist) Error() string { return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID) } +func (err ErrAttachmentNotExist) Unwrap() error { + return util.ErrNotExist +} + // GetAttachmentByID returns attachment by given id func GetAttachmentByID(ctx context.Context, id int64) (*Attachment, error) { attach := &Attachment{} diff --git a/models/repo/redirect.go b/models/repo/redirect.go index 88fad6f3e3..f28220c2af 100644 --- a/models/repo/redirect.go +++ b/models/repo/redirect.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" ) // ErrRedirectNotExist represents a "RedirectNotExist" kind of error. @@ -28,6 +29,10 @@ func (err ErrRedirectNotExist) Error() string { return fmt.Sprintf("repository redirect does not exist [uid: %d, name: %s]", err.OwnerID, err.RepoName) } +func (err ErrRedirectNotExist) Unwrap() error { + return util.ErrNotExist +} + // Redirect represents that a repo name should be redirected to another type Redirect struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/repo/release.go b/models/repo/release.go index 2b484c9b84..2e7bc6d322 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -37,6 +37,10 @@ func (err ErrReleaseAlreadyExist) Error() string { return fmt.Sprintf("release tag already exist [tag_name: %s]", err.TagName) } +func (err ErrReleaseAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrReleaseNotExist represents a "ReleaseNotExist" kind of error. type ErrReleaseNotExist struct { ID int64 @@ -53,6 +57,10 @@ func (err ErrReleaseNotExist) Error() string { return fmt.Sprintf("release tag does not exist [id: %d, tag_name: %s]", err.ID, err.TagName) } +func (err ErrReleaseNotExist) Unwrap() error { + return util.ErrNotExist +} + // Release represents a release of repository. type Release struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/repo/repo.go b/models/repo/repo.go index 4fd97dcd1b..ce698baaef 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -43,6 +43,10 @@ func (err ErrUserDoesNotHaveAccessToRepo) Error() string { return fmt.Sprintf("user doesn't have access to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName) } +func (err ErrUserDoesNotHaveAccessToRepo) Unwrap() error { + return util.ErrPermissionDenied +} + var ( reservedRepoNames = []string{".", "..", "-"} reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"} @@ -643,6 +647,11 @@ func (err ErrRepoNotExist) Error() string { err.ID, err.UID, err.OwnerName, err.Name) } +// Unwrap unwraps this error as a ErrNotExist error +func (err ErrRepoNotExist) Unwrap() error { + return util.ErrNotExist +} + // GetRepositoryByOwnerAndNameCtx returns the repository by given owner name and repo name func GetRepositoryByOwnerAndNameCtx(ctx context.Context, ownerName, repoName string) (*Repository, error) { var repo Repository diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index da3e19dece..dd85ca9186 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/xorm" "xorm.io/xorm/convert" @@ -33,6 +34,10 @@ func (err ErrUnitTypeNotExist) Error() string { return fmt.Sprintf("Unit type does not exist: %s", err.UT.String()) } +func (err ErrUnitTypeNotExist) Unwrap() error { + return util.ErrNotExist +} + // RepoUnit describes all units of a repository type RepoUnit struct { //revive:disable-line:exported ID int64 diff --git a/models/repo/topic.go b/models/repo/topic.go index 7ba9a49e89..33bbb05af9 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -55,6 +56,10 @@ func (err ErrTopicNotExist) Error() string { return fmt.Sprintf("topic is not exist [name: %s]", err.Name) } +func (err ErrTopicNotExist) Unwrap() error { + return util.ErrNotExist +} + // ValidateTopic checks a topic by length and match pattern rules func ValidateTopic(topic string) bool { return len(topic) <= 35 && topicPattern.MatchString(topic) diff --git a/models/repo/update.go b/models/repo/update.go index 07776ebc01..64a225d2f5 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -63,6 +63,10 @@ func (err ErrReachLimitOfRepo) Error() string { return fmt.Sprintf("user has reached maximum limit of repositories [limit: %d]", err.Limit) } +func (err ErrReachLimitOfRepo) Unwrap() error { + return util.ErrPermissionDenied +} + // ErrRepoAlreadyExist represents a "RepoAlreadyExist" kind of error. type ErrRepoAlreadyExist struct { Uname string @@ -79,6 +83,10 @@ func (err ErrRepoAlreadyExist) Error() string { return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name) } +func (err ErrRepoAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrRepoFilesAlreadyExist represents a "RepoFilesAlreadyExist" kind of error. type ErrRepoFilesAlreadyExist struct { Uname string @@ -95,6 +103,10 @@ func (err ErrRepoFilesAlreadyExist) Error() string { return fmt.Sprintf("repository files already exist [uname: %s, name: %s]", err.Uname, err.Name) } +func (err ErrRepoFilesAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // CheckCreateRepository check if could created a repository func CheckCreateRepository(doer, u *user_model.User, name string, overwriteOrAdopt bool) error { if !doer.CanCreateRepo() { diff --git a/models/repo/upload.go b/models/repo/upload.go index 24544910b1..e3ce7e458f 100644 --- a/models/repo/upload.go +++ b/models/repo/upload.go @@ -36,6 +36,10 @@ func (err ErrUploadNotExist) Error() string { return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID) } +func (err ErrUploadNotExist) Unwrap() error { + return util.ErrNotExist +} + // Upload represent a uploaded file to a repo to be deleted when moved type Upload struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/repo/wiki.go b/models/repo/wiki.go index 72ec7c394b..c8886eaa34 100644 --- a/models/repo/wiki.go +++ b/models/repo/wiki.go @@ -30,6 +30,10 @@ func (err ErrWikiAlreadyExist) Error() string { return fmt.Sprintf("wiki page already exists [title: %s]", err.Title) } +func (err ErrWikiAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrWikiReservedName represents a reserved name error. type ErrWikiReservedName struct { Title string @@ -45,6 +49,10 @@ func (err ErrWikiReservedName) Error() string { return fmt.Sprintf("wiki title is reserved: %s", err.Title) } +func (err ErrWikiReservedName) Unwrap() error { + return util.ErrInvalidArgument +} + // ErrWikiInvalidFileName represents an invalid wiki file name. type ErrWikiInvalidFileName struct { FileName string @@ -60,6 +68,10 @@ func (err ErrWikiInvalidFileName) Error() string { return fmt.Sprintf("Invalid wiki filename: %s", err.FileName) } +func (err ErrWikiInvalidFileName) Unwrap() error { + return util.ErrInvalidArgument +} + // WikiCloneLink returns clone URLs of repository wiki. func (repo *Repository) WikiCloneLink() *CloneLink { return repo.cloneLink(true) diff --git a/models/appstate/appstate.go b/models/system/appstate.go similarity index 98% rename from models/appstate/appstate.go rename to models/system/appstate.go index aa5a59e1a3..c11a2512ab 100644 --- a/models/appstate/appstate.go +++ b/models/system/appstate.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package appstate +package system import ( "context" diff --git a/models/system/main_test.go b/models/system/main_test.go new file mode 100644 index 0000000000..a56c76aedc --- /dev/null +++ b/models/system/main_test.go @@ -0,0 +1,21 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package system_test + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models" // register models + _ "code.gitea.io/gitea/models/system" // register models of system +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + GiteaRootPath: filepath.Join("..", ".."), + }) +} diff --git a/models/admin/notice.go b/models/system/notice.go similarity index 99% rename from models/admin/notice.go rename to models/system/notice.go index 4d385cf951..3276fa3ffb 100644 --- a/models/admin/notice.go +++ b/models/system/notice.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package admin +package system import ( "context" diff --git a/models/system/notice_test.go b/models/system/notice_test.go new file mode 100644 index 0000000000..768bcca66c --- /dev/null +++ b/models/system/notice_test.go @@ -0,0 +1,117 @@ +// Copyright 2017 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 system_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestNotice_TrStr(t *testing.T) { + notice := &system.Notice{ + Type: system.NoticeRepository, + Description: "test description", + } + assert.Equal(t, "admin.notices.type_1", notice.TrStr()) +} + +func TestCreateNotice(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + noticeBean := &system.Notice{ + Type: system.NoticeRepository, + Description: "test description", + } + unittest.AssertNotExistsBean(t, noticeBean) + assert.NoError(t, system.CreateNotice(db.DefaultContext, noticeBean.Type, noticeBean.Description)) + unittest.AssertExistsAndLoadBean(t, noticeBean) +} + +func TestCreateRepositoryNotice(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + noticeBean := &system.Notice{ + Type: system.NoticeRepository, + Description: "test description", + } + unittest.AssertNotExistsBean(t, noticeBean) + assert.NoError(t, system.CreateRepositoryNotice(noticeBean.Description)) + unittest.AssertExistsAndLoadBean(t, noticeBean) +} + +// TODO TestRemoveAllWithNotice + +func TestCountNotices(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + assert.Equal(t, int64(3), system.CountNotices()) +} + +func TestNotices(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + notices, err := system.Notices(1, 2) + assert.NoError(t, err) + if assert.Len(t, notices, 2) { + assert.Equal(t, int64(3), notices[0].ID) + assert.Equal(t, int64(2), notices[1].ID) + } + + notices, err = system.Notices(2, 2) + assert.NoError(t, err) + if assert.Len(t, notices, 1) { + assert.Equal(t, int64(1), notices[0].ID) + } +} + +func TestDeleteNotice(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) + assert.NoError(t, system.DeleteNotice(3)) + unittest.AssertNotExistsBean(t, &system.Notice{ID: 3}) +} + +func TestDeleteNotices(t *testing.T) { + // delete a non-empty range + assert.NoError(t, unittest.PrepareTestDatabase()) + + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) + assert.NoError(t, system.DeleteNotices(1, 2)) + unittest.AssertNotExistsBean(t, &system.Notice{ID: 1}) + unittest.AssertNotExistsBean(t, &system.Notice{ID: 2}) + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) +} + +func TestDeleteNotices2(t *testing.T) { + // delete an empty range + assert.NoError(t, unittest.PrepareTestDatabase()) + + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) + assert.NoError(t, system.DeleteNotices(3, 2)) + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) +} + +func TestDeleteNoticesByIDs(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 1}) + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 3}) + assert.NoError(t, system.DeleteNoticesByIDs([]int64{1, 3})) + unittest.AssertNotExistsBean(t, &system.Notice{ID: 1}) + unittest.AssertExistsAndLoadBean(t, &system.Notice{ID: 2}) + unittest.AssertNotExistsBean(t, &system.Notice{ID: 3}) +} diff --git a/models/system/setting.go b/models/system/setting.go new file mode 100644 index 0000000000..ff8b48e618 --- /dev/null +++ b/models/system/setting.go @@ -0,0 +1,261 @@ +// 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 system + +import ( + "context" + "fmt" + "net/url" + "strconv" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + + "strk.kbt.io/projects/go/libravatar" + "xorm.io/builder" +) + +// Setting is a key value store of user settings +type Setting struct { + ID int64 `xorm:"pk autoincr"` + SettingKey string `xorm:"varchar(255) unique"` // ensure key is always lowercase + SettingValue string `xorm:"text"` + Version int `xorm:"version"` // prevent to override + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` +} + +// TableName sets the table name for the settings struct +func (s *Setting) TableName() string { + return "system_setting" +} + +func (s *Setting) GetValueBool() bool { + b, _ := strconv.ParseBool(s.SettingValue) + return b +} + +func init() { + db.RegisterModel(new(Setting)) +} + +// ErrSettingIsNotExist represents an error that a setting is not exist with special key +type ErrSettingIsNotExist struct { + Key string +} + +// Error implements error +func (err ErrSettingIsNotExist) Error() string { + return fmt.Sprintf("System setting[%s] is not exist", err.Key) +} + +// IsErrSettingIsNotExist return true if err is ErrSettingIsNotExist +func IsErrSettingIsNotExist(err error) bool { + _, ok := err.(ErrSettingIsNotExist) + return ok +} + +// ErrDataExpired represents an error that update a record which has been updated by another thread +type ErrDataExpired struct { + Key string +} + +// Error implements error +func (err ErrDataExpired) Error() string { + return fmt.Sprintf("System setting[%s] has been updated by another thread", err.Key) +} + +// IsErrDataExpired return true if err is ErrDataExpired +func IsErrDataExpired(err error) bool { + _, ok := err.(ErrDataExpired) + return ok +} + +// GetSetting returns specific setting +func GetSetting(key string) (*Setting, error) { + v, err := GetSettings([]string{key}) + if err != nil { + return nil, err + } + if len(v) == 0 { + return nil, ErrSettingIsNotExist{key} + } + return v[key], nil +} + +// GetSettings returns specific settings +func GetSettings(keys []string) (map[string]*Setting, error) { + for i := 0; i < len(keys); i++ { + keys[i] = strings.ToLower(keys[i]) + } + settings := make([]*Setting, 0, len(keys)) + if err := db.GetEngine(db.DefaultContext). + Where(builder.In("setting_key", keys)). + Find(&settings); err != nil { + return nil, err + } + settingsMap := make(map[string]*Setting) + for _, s := range settings { + settingsMap[s.SettingKey] = s + } + return settingsMap, nil +} + +type AllSettings map[string]*Setting + +func (settings AllSettings) Get(key string) Setting { + if v, ok := settings[key]; ok { + return *v + } + return Setting{} +} + +func (settings AllSettings) GetBool(key string) bool { + b, _ := strconv.ParseBool(settings.Get(key).SettingValue) + return b +} + +func (settings AllSettings) GetVersion(key string) int { + return settings.Get(key).Version +} + +// GetAllSettings returns all settings from user +func GetAllSettings() (AllSettings, error) { + settings := make([]*Setting, 0, 5) + if err := db.GetEngine(db.DefaultContext). + Find(&settings); err != nil { + return nil, err + } + settingsMap := make(map[string]*Setting) + for _, s := range settings { + settingsMap[s.SettingKey] = s + } + return settingsMap, nil +} + +// DeleteSetting deletes a specific setting for a user +func DeleteSetting(setting *Setting) error { + _, err := db.GetEngine(db.DefaultContext).Delete(setting) + return err +} + +func SetSettingNoVersion(key, value string) error { + s, err := GetSetting(key) + if IsErrSettingIsNotExist(err) { + return SetSetting(&Setting{ + SettingKey: key, + SettingValue: value, + }) + } + if err != nil { + return err + } + s.SettingValue = value + return SetSetting(s) +} + +// SetSetting updates a users' setting for a specific key +func SetSetting(setting *Setting) error { + if err := upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil { + return err + } + setting.Version++ + return nil +} + +func upsertSettingValue(key, value string, version int) error { + return db.WithTx(func(ctx context.Context) error { + e := db.GetEngine(ctx) + + // here we use a general method to do a safe upsert for different databases (and most transaction levels) + // 1. try to UPDATE the record and acquire the transaction write lock + // if UPDATE returns non-zero rows are changed, OK, the setting is saved correctly + // if UPDATE returns "0 rows changed", two possibilities: (a) record doesn't exist (b) value is not changed + // 2. do a SELECT to check if the row exists or not (we already have the transaction lock) + // 3. if the row doesn't exist, do an INSERT (we are still protected by the transaction lock, so it's safe) + // + // to optimize the SELECT in step 2, we can use an extra column like `revision=revision+1` + // to make sure the UPDATE always returns a non-zero value for existing (unchanged) records. + + res, err := e.Exec("UPDATE system_setting SET setting_value=?, version = version+1 WHERE setting_key=? AND version=?", value, key, version) + if err != nil { + return err + } + rows, _ := res.RowsAffected() + if rows > 0 { + // the existing row is updated, so we can return + return nil + } + + // in case the value isn't changed, update would return 0 rows changed, so we need this check + has, err := e.Exist(&Setting{SettingKey: key}) + if err != nil { + return err + } + if has { + return ErrDataExpired{Key: key} + } + + // if no existing row, insert a new row + _, err = e.Insert(&Setting{SettingKey: key, SettingValue: value}) + return err + }) +} + +var ( + GravatarSourceURL *url.URL + LibravatarService *libravatar.Libravatar +) + +func Init() error { + var disableGravatar bool + disableGravatarSetting, err := GetSetting(KeyPictureDisableGravatar) + if IsErrSettingIsNotExist(err) { + disableGravatar = setting.GetDefaultDisableGravatar() + disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)} + } else if err != nil { + return err + } else { + disableGravatar = disableGravatarSetting.GetValueBool() + } + + var enableFederatedAvatar bool + enableFederatedAvatarSetting, err := GetSetting(KeyPictureEnableFederatedAvatar) + if IsErrSettingIsNotExist(err) { + enableFederatedAvatar = setting.GetDefaultEnableFederatedAvatar(disableGravatar) + enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)} + } else if err != nil { + return err + } else { + enableFederatedAvatar = disableGravatarSetting.GetValueBool() + } + + if setting.OfflineMode { + disableGravatar = true + enableFederatedAvatar = false + } + + if disableGravatar || !enableFederatedAvatar { + var err error + GravatarSourceURL, err = url.Parse(setting.GravatarSource) + if err != nil { + return fmt.Errorf("Failed to parse Gravatar URL(%s): %v", setting.GravatarSource, err) + } + } + + if enableFederatedAvatarSetting.GetValueBool() { + LibravatarService = libravatar.New() + if GravatarSourceURL.Scheme == "https" { + LibravatarService.SetUseHTTPS(true) + LibravatarService.SetSecureFallbackHost(GravatarSourceURL.Host) + } else { + LibravatarService.SetUseHTTPS(false) + LibravatarService.SetFallbackHost(GravatarSourceURL.Host) + } + } + return nil +} diff --git a/models/system/setting_key.go b/models/system/setting_key.go new file mode 100644 index 0000000000..5a6ea6ed72 --- /dev/null +++ b/models/system/setting_key.go @@ -0,0 +1,11 @@ +// 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 system + +// enumerate all system setting keys +const ( + KeyPictureDisableGravatar = "picture.disable_gravatar" + KeyPictureEnableFederatedAvatar = "picture.enable_federated_avatar" +) diff --git a/models/system/setting_test.go b/models/system/setting_test.go new file mode 100644 index 0000000000..d25fc05f31 --- /dev/null +++ b/models/system/setting_test.go @@ -0,0 +1,53 @@ +// 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 system_test + +import ( + "strings" + "testing" + + "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestSettings(t *testing.T) { + keyName := "server.LFS_LOCKS_PAGING_NUM" + assert.NoError(t, unittest.PrepareTestDatabase()) + + newSetting := &system.Setting{SettingKey: keyName, SettingValue: "50"} + + // create setting + err := system.SetSetting(newSetting) + assert.NoError(t, err) + // test about saving unchanged values + err = system.SetSetting(newSetting) + assert.NoError(t, err) + + // get specific setting + settings, err := system.GetSettings([]string{keyName}) + assert.NoError(t, err) + assert.Len(t, settings, 1) + assert.EqualValues(t, newSetting.SettingValue, settings[strings.ToLower(keyName)].SettingValue) + + // updated setting + updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: newSetting.Version} + err = system.SetSetting(updatedSetting) + assert.NoError(t, err) + + // get all settings + settings, err = system.GetAllSettings() + assert.NoError(t, err) + assert.Len(t, settings, 3) + assert.EqualValues(t, updatedSetting.SettingValue, settings[strings.ToLower(updatedSetting.SettingKey)].SettingValue) + + // delete setting + err = system.DeleteSetting(&system.Setting{SettingKey: strings.ToLower(keyName)}) + assert.NoError(t, err) + settings, err = system.GetAllSettings() + assert.NoError(t, err) + assert.Len(t, settings, 2) +} diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 25129137f7..2e6c25ae48 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -7,12 +7,12 @@ package unittest import ( "context" "fmt" - "net/url" "os" "path/filepath" "testing" "code.gitea.io/gitea/models/db" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" @@ -91,10 +91,8 @@ func MainTest(m *testing.M, testOpts *TestOptions) { setting.AppDataPath = appDataPath setting.AppWorkPath = testOpts.GiteaRootPath setting.StaticRootPath = testOpts.GiteaRootPath - setting.GravatarSourceURL, err = url.Parse("https://secure.gravatar.com/avatar/") - if err != nil { - fatalTestError("url.Parse: %v\n", err) - } + setting.GravatarSource = "https://secure.gravatar.com/avatar/" + setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments") setting.LFS.Storage.Path = filepath.Join(setting.AppDataPath, "lfs") @@ -112,6 +110,9 @@ func MainTest(m *testing.M, testOpts *TestOptions) { if err = storage.Init(); err != nil { fatalTestError("storage.Init: %v\n", err) } + if err = system_model.Init(); err != nil { + fatalTestError("models.Init: %v\n", err) + } if err = util.RemoveAll(repoRootPath); err != nil { fatalTestError("util.RemoveAll: %v\n", err) diff --git a/models/user/avatar.go b/models/user/avatar.go index 6a44a3bcb3..1c75c7406b 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/models/avatars" "code.gitea.io/gitea/models/db" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/avatar" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -67,10 +68,16 @@ func (u *User) AvatarLinkWithSize(size int) string { useLocalAvatar := false autoGenerateAvatar := false + var disableGravatar bool + disableGravatarSetting, _ := system_model.GetSetting(system_model.KeyPictureDisableGravatar) + if disableGravatarSetting != nil { + disableGravatar = disableGravatarSetting.GetValueBool() + } + switch { case u.UseCustomAvatar: useLocalAvatar = true - case setting.DisableGravatar, setting.OfflineMode: + case disableGravatar, setting.OfflineMode: useLocalAvatar = true autoGenerateAvatar = true } diff --git a/models/user/email_address.go b/models/user/email_address.go index d87b945706..964e7ae08c 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -40,6 +40,10 @@ func (err ErrEmailCharIsNotSupported) Error() string { return fmt.Sprintf("e-mail address contains unsupported character [email: %s]", err.Email) } +func (err ErrEmailCharIsNotSupported) Unwrap() error { + return util.ErrInvalidArgument +} + // ErrEmailInvalid represents an error where the email address does not comply with RFC 5322 // or has a leading '-' character type ErrEmailInvalid struct { @@ -56,6 +60,10 @@ func (err ErrEmailInvalid) Error() string { return fmt.Sprintf("e-mail invalid [email: %s]", err.Email) } +func (err ErrEmailInvalid) Unwrap() error { + return util.ErrInvalidArgument +} + // ErrEmailAlreadyUsed represents a "EmailAlreadyUsed" kind of error. type ErrEmailAlreadyUsed struct { Email string @@ -71,6 +79,10 @@ func (err ErrEmailAlreadyUsed) Error() string { return fmt.Sprintf("e-mail already in use [email: %s]", err.Email) } +func (err ErrEmailAlreadyUsed) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrEmailAddressNotExist email address not exist type ErrEmailAddressNotExist struct { Email string @@ -86,6 +98,10 @@ func (err ErrEmailAddressNotExist) Error() string { return fmt.Sprintf("Email address does not exist [email: %s]", err.Email) } +func (err ErrEmailAddressNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrPrimaryEmailCannotDelete primary email address cannot be deleted type ErrPrimaryEmailCannotDelete struct { Email string @@ -101,6 +117,10 @@ func (err ErrPrimaryEmailCannotDelete) Error() string { return fmt.Sprintf("Primary email address cannot be deleted [email: %s]", err.Email) } +func (err ErrPrimaryEmailCannotDelete) Unwrap() error { + return util.ErrInvalidArgument +} + // EmailAddress is the list of all email addresses of a user. It also contains the // primary email address which is saved in user table. type EmailAddress struct { diff --git a/models/user/error.go b/models/user/error.go index 25e0d8ea8a..3fe4ee6657 100644 --- a/models/user/error.go +++ b/models/user/error.go @@ -6,6 +6,8 @@ package user import ( "fmt" + + "code.gitea.io/gitea/modules/util" ) // ____ ___ @@ -30,6 +32,11 @@ func (err ErrUserAlreadyExist) Error() string { return fmt.Sprintf("user already exists [name: %s]", err.Name) } +// Unwrap unwraps this error as a ErrExist error +func (err ErrUserAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrUserNotExist represents a "UserNotExist" kind of error. type ErrUserNotExist struct { UID int64 @@ -47,6 +54,11 @@ func (err ErrUserNotExist) Error() string { return fmt.Sprintf("user does not exist [uid: %d, name: %s, keyid: %d]", err.UID, err.Name, err.KeyID) } +// Unwrap unwraps this error as a ErrNotExist error +func (err ErrUserNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrUserProhibitLogin represents a "ErrUserProhibitLogin" kind of error. type ErrUserProhibitLogin struct { UID int64 @@ -63,6 +75,11 @@ func (err ErrUserProhibitLogin) Error() string { return fmt.Sprintf("user is not allowed login [uid: %d, name: %s]", err.UID, err.Name) } +// Unwrap unwraps this error as a ErrPermission error +func (err ErrUserProhibitLogin) Unwrap() error { + return util.ErrPermissionDenied +} + // ErrUserInactive represents a "ErrUserInactive" kind of error. type ErrUserInactive struct { UID int64 @@ -78,3 +95,8 @@ func IsErrUserInactive(err error) bool { func (err ErrUserInactive) Error() string { return fmt.Sprintf("user is inactive [uid: %d, name: %s]", err.UID, err.Name) } + +// Unwrap unwraps this error as a ErrPermission error +func (err ErrUserInactive) Unwrap() error { + return util.ErrPermissionDenied +} diff --git a/models/user/external_login_user.go b/models/user/external_login_user.go index 422823b89c..496717c57b 100644 --- a/models/user/external_login_user.go +++ b/models/user/external_login_user.go @@ -10,6 +10,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -31,6 +32,10 @@ func (err ErrExternalLoginUserAlreadyExist) Error() string { return fmt.Sprintf("external login user already exists [externalID: %s, userID: %d, loginSourceID: %d]", err.ExternalID, err.UserID, err.LoginSourceID) } +func (err ErrExternalLoginUserAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // ErrExternalLoginUserNotExist represents a "ExternalLoginUserNotExist" kind of error. type ErrExternalLoginUserNotExist struct { UserID int64 @@ -47,6 +52,10 @@ func (err ErrExternalLoginUserNotExist) Error() string { return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID) } +func (err ErrExternalLoginUserNotExist) Unwrap() error { + return util.ErrNotExist +} + // ExternalLoginUser makes the connecting between some existing user and additional external login sources type ExternalLoginUser struct { ExternalID string `xorm:"pk NOT NULL"` diff --git a/models/user/openid.go b/models/user/openid.go index 8ef0ce5ed7..f8e8a787e6 100644 --- a/models/user/openid.go +++ b/models/user/openid.go @@ -10,6 +10,7 @@ import ( "fmt" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" ) // ErrOpenIDNotExist openid is not known @@ -65,6 +66,10 @@ func (err ErrOpenIDAlreadyUsed) Error() string { return fmt.Sprintf("OpenID already in use [oid: %s]", err.OpenID) } +func (err ErrOpenIDAlreadyUsed) Unwrap() error { + return util.ErrAlreadyExist +} + // AddUserOpenID adds an pre-verified/normalized OpenID URI to given user. // NOTE: make sure openid.URI is normalized already func AddUserOpenID(ctx context.Context, openid *UserOpenID) error { diff --git a/models/user/redirect.go b/models/user/redirect.go index 49370218db..af8d6439ad 100644 --- a/models/user/redirect.go +++ b/models/user/redirect.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" ) // ErrUserRedirectNotExist represents a "UserRedirectNotExist" kind of error. @@ -27,6 +28,10 @@ func (err ErrUserRedirectNotExist) Error() string { return fmt.Sprintf("user redirect does not exist [name: %s]", err.Name) } +func (err ErrUserRedirectNotExist) Unwrap() error { + return util.ErrNotExist +} + // Redirect represents that a user name should be redirected to another type Redirect struct { ID int64 `xorm:"pk autoincr"` diff --git a/models/user/setting.go b/models/user/setting.go index fbb6fbab30..5fe7c2ec23 100644 --- a/models/user/setting.go +++ b/models/user/setting.go @@ -31,6 +31,34 @@ func init() { db.RegisterModel(new(Setting)) } +// ErrUserSettingIsNotExist represents an error that a setting is not exist with special key +type ErrUserSettingIsNotExist struct { + Key string +} + +// Error implements error +func (err ErrUserSettingIsNotExist) Error() string { + return fmt.Sprintf("Setting[%s] is not exist", err.Key) +} + +// IsErrUserSettingIsNotExist return true if err is ErrSettingIsNotExist +func IsErrUserSettingIsNotExist(err error) bool { + _, ok := err.(ErrUserSettingIsNotExist) + return ok +} + +// GetSetting returns specific setting +func GetSetting(uid int64, key string) (*Setting, error) { + v, err := GetUserSettings(uid, []string{key}) + if err != nil { + return nil, err + } + if len(v) == 0 { + return nil, ErrUserSettingIsNotExist{key} + } + return v[key], nil +} + // GetUserSettings returns specific settings from user func GetUserSettings(uid int64, keys []string) (map[string]*Setting, error) { settings := make([]*Setting, 0, len(keys)) diff --git a/models/user/user_test.go b/models/user/user_test.go index 678d6c186c..5f2ac0a60c 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -102,7 +102,7 @@ func TestSearchUsers(t *testing.T) { []int64{9}) testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue}, - []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 28, 29, 30, 32}) + []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32}) testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: util.OptionalBoolTrue}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go index 2adfcaa60d..2b9b63c09b 100644 --- a/models/webhook/hooktask.go +++ b/models/webhook/hooktask.go @@ -104,7 +104,6 @@ type HookResponse struct { // HookTask represents a hook task. type HookTask struct { ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX"` HookID int64 UUID string api.Payloader `xorm:"-"` @@ -178,14 +177,29 @@ func HookTasks(hookID int64, page int) ([]*HookTask, error) { // CreateHookTask creates a new hook task, // it handles conversion from Payload to PayloadContent. -func CreateHookTask(t *HookTask) error { +func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) { data, err := t.Payloader.JSONPayload() if err != nil { - return err + return nil, err } t.UUID = gouuid.New().String() t.PayloadContent = string(data) - return db.Insert(db.DefaultContext, t) + return t, db.Insert(ctx, t) +} + +func GetHookTaskByID(ctx context.Context, id int64) (*HookTask, error) { + t := &HookTask{} + + has, err := db.GetEngine(ctx).ID(id).Get(t) + if err != nil { + return nil, err + } + if !has { + return nil, ErrHookTaskNotExist{ + TaskID: id, + } + } + return t, nil } // UpdateHookTask updates information of hook task. @@ -195,53 +209,36 @@ func UpdateHookTask(t *HookTask) error { } // ReplayHookTask copies a hook task to get re-delivered -func ReplayHookTask(hookID int64, uuid string) (*HookTask, error) { - var newTask *HookTask - - err := db.WithTx(func(ctx context.Context) error { - task := &HookTask{ +func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, error) { + task := &HookTask{ + HookID: hookID, + UUID: uuid, + } + has, err := db.GetByBean(ctx, task) + if err != nil { + return nil, err + } else if !has { + return nil, ErrHookTaskNotExist{ HookID: hookID, UUID: uuid, } - has, err := db.GetByBean(ctx, task) - if err != nil { - return err - } else if !has { - return ErrHookTaskNotExist{ - HookID: hookID, - UUID: uuid, - } - } + } - newTask = &HookTask{ - UUID: gouuid.New().String(), - RepoID: task.RepoID, - HookID: task.HookID, - PayloadContent: task.PayloadContent, - EventType: task.EventType, - } - return db.Insert(ctx, newTask) - }) - - return newTask, err + newTask := &HookTask{ + UUID: gouuid.New().String(), + HookID: task.HookID, + PayloadContent: task.PayloadContent, + EventType: task.EventType, + } + return newTask, db.Insert(ctx, newTask) } // FindUndeliveredHookTasks represents find the undelivered hook tasks -func FindUndeliveredHookTasks() ([]*HookTask, error) { +func FindUndeliveredHookTasks(ctx context.Context) ([]*HookTask, error) { tasks := make([]*HookTask, 0, 10) - if err := db.GetEngine(db.DefaultContext).Where("is_delivered=?", false).Find(&tasks); err != nil { - return nil, err - } - return tasks, nil -} - -// FindRepoUndeliveredHookTasks represents find the undelivered hook tasks of one repository -func FindRepoUndeliveredHookTasks(repoID int64) ([]*HookTask, error) { - tasks := make([]*HookTask, 0, 5) - if err := db.GetEngine(db.DefaultContext).Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil { - return nil, err - } - return tasks, nil + return tasks, db.GetEngine(ctx). + Where("is_delivered=?", false). + Find(&tasks) } // CleanupHookTaskTable deletes rows from hook_task as needed. @@ -250,7 +247,7 @@ func CleanupHookTaskTable(ctx context.Context, cleanupType HookTaskCleanupType, if cleanupType == OlderThan { deleteOlderThan := time.Now().Add(-olderThan).UnixNano() - deletes, err := db.GetEngine(db.DefaultContext). + deletes, err := db.GetEngine(ctx). Where("is_delivered = ? and delivered < ?", true, deleteOlderThan). Delete(new(HookTask)) if err != nil { @@ -259,7 +256,8 @@ func CleanupHookTaskTable(ctx context.Context, cleanupType HookTaskCleanupType, log.Trace("Deleted %d rows from hook_task", deletes) } else if cleanupType == PerWebhook { hookIDs := make([]int64, 0, 10) - err := db.GetEngine(db.DefaultContext).Table("webhook"). + err := db.GetEngine(ctx). + Table("webhook"). Where("id > 0"). Cols("id"). Find(&hookIDs) diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index ac09220630..4551dcff5f 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -19,13 +19,6 @@ import ( "xorm.io/builder" ) -// __ __ ___. .__ __ -// / \ / \ ____\_ |__ | |__ ____ ____ | | __ -// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ / -// \ /\ ___/| \_\ \ Y ( <_> | <_> ) < -// \__/\ / \___ >___ /___| /\____/ \____/|__|_ \ -// \/ \/ \/ \/ \/ - // ErrWebhookNotExist represents a "WebhookNotExist" kind of error. type ErrWebhookNotExist struct { ID int64 @@ -41,8 +34,13 @@ func (err ErrWebhookNotExist) Error() string { return fmt.Sprintf("webhook does not exist [id: %d]", err.ID) } +func (err ErrWebhookNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrHookTaskNotExist represents a "HookTaskNotExist" kind of error. type ErrHookTaskNotExist struct { + TaskID int64 HookID int64 UUID string } @@ -54,7 +52,11 @@ func IsErrHookTaskNotExist(err error) bool { } func (err ErrHookTaskNotExist) Error() string { - return fmt.Sprintf("hook task does not exist [hook: %d, uuid: %s]", err.HookID, err.UUID) + return fmt.Sprintf("hook task does not exist [task: %d, hook: %d, uuid: %s]", err.TaskID, err.HookID, err.UUID) +} + +func (err ErrHookTaskNotExist) Unwrap() error { + return util.ErrNotExist } // HookContentType is the content type of a web hook diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index 7ec7edc0b7..8c4838ebdc 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -208,12 +208,12 @@ func TestHookTasks(t *testing.T) { func TestCreateHookTask(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - RepoID: 3, HookID: 3, Payloader: &api.PushPayload{}, } unittest.AssertNotExistsBean(t, hookTask) - assert.NoError(t, CreateHookTask(hookTask)) + _, err := CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) unittest.AssertExistsAndLoadBean(t, hookTask) } @@ -232,14 +232,14 @@ func TestUpdateHookTask(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - RepoID: 3, HookID: 3, Payloader: &api.PushPayload{}, IsDelivered: true, Delivered: time.Now().UnixNano(), } unittest.AssertNotExistsBean(t, hookTask) - assert.NoError(t, CreateHookTask(hookTask)) + _, err := CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) unittest.AssertExistsAndLoadBean(t, hookTask) assert.NoError(t, CleanupHookTaskTable(context.Background(), PerWebhook, 168*time.Hour, 0)) @@ -249,13 +249,13 @@ func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - RepoID: 2, HookID: 4, Payloader: &api.PushPayload{}, IsDelivered: false, } unittest.AssertNotExistsBean(t, hookTask) - assert.NoError(t, CreateHookTask(hookTask)) + _, err := CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) unittest.AssertExistsAndLoadBean(t, hookTask) assert.NoError(t, CleanupHookTaskTable(context.Background(), PerWebhook, 168*time.Hour, 0)) @@ -265,14 +265,14 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) { func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - RepoID: 2, HookID: 4, Payloader: &api.PushPayload{}, IsDelivered: true, Delivered: time.Now().UnixNano(), } unittest.AssertNotExistsBean(t, hookTask) - assert.NoError(t, CreateHookTask(hookTask)) + _, err := CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) unittest.AssertExistsAndLoadBean(t, hookTask) assert.NoError(t, CleanupHookTaskTable(context.Background(), PerWebhook, 168*time.Hour, 1)) @@ -282,14 +282,14 @@ func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - RepoID: 3, HookID: 3, Payloader: &api.PushPayload{}, IsDelivered: true, Delivered: time.Now().AddDate(0, 0, -8).UnixNano(), } unittest.AssertNotExistsBean(t, hookTask) - assert.NoError(t, CreateHookTask(hookTask)) + _, err := CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) unittest.AssertExistsAndLoadBean(t, hookTask) assert.NoError(t, CleanupHookTaskTable(context.Background(), OlderThan, 168*time.Hour, 0)) @@ -299,13 +299,13 @@ func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - RepoID: 2, HookID: 4, Payloader: &api.PushPayload{}, IsDelivered: false, } unittest.AssertNotExistsBean(t, hookTask) - assert.NoError(t, CreateHookTask(hookTask)) + _, err := CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) unittest.AssertExistsAndLoadBean(t, hookTask) assert.NoError(t, CleanupHookTaskTable(context.Background(), OlderThan, 168*time.Hour, 0)) @@ -315,14 +315,14 @@ func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) { func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) hookTask := &HookTask{ - RepoID: 2, HookID: 4, Payloader: &api.PushPayload{}, IsDelivered: true, Delivered: time.Now().AddDate(0, 0, -6).UnixNano(), } unittest.AssertNotExistsBean(t, hookTask) - assert.NoError(t, CreateHookTask(hookTask)) + _, err := CreateHookTask(db.DefaultContext, hookTask) + assert.NoError(t, err) unittest.AssertExistsAndLoadBean(t, hookTask) assert.NoError(t, CleanupHookTaskTable(context.Background(), OlderThan, 168*time.Hour, 0)) diff --git a/modules/convert/convert.go b/modules/convert/convert.go index 187c67fa76..8c92bbb371 100644 --- a/modules/convert/convert.go +++ b/modules/convert/convert.go @@ -392,12 +392,13 @@ func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse { // ToOAuth2Application convert from auth.OAuth2Application to api.OAuth2Application func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application { return &api.OAuth2Application{ - ID: app.ID, - Name: app.Name, - ClientID: app.ClientID, - ClientSecret: app.ClientSecret, - RedirectURIs: app.RedirectURIs, - Created: app.CreatedUnix.AsTime(), + ID: app.ID, + Name: app.Name, + ClientID: app.ClientID, + ClientSecret: app.ClientSecret, + ConfidentialClient: app.ConfidentialClient, + RedirectURIs: app.RedirectURIs, + Created: app.CreatedUnix.AsTime(), } } diff --git a/modules/doctor/heads.go b/modules/doctor/heads.go index ec14aa4ead..7f3b2a8a02 100644 --- a/modules/doctor/heads.go +++ b/modules/doctor/heads.go @@ -21,7 +21,7 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool) numRepos++ runOpts := &git.RunOpts{Dir: repo.RepoPath()} - _, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse", "--", repo.DefaultBranch).RunStdString(runOpts) + _, _, defaultBranchErr := git.NewCommand(ctx, "rev-parse").AddDashesAndList(repo.DefaultBranch).RunStdString(runOpts) head, _, headErr := git.NewCommand(ctx, "symbolic-ref", "--short", "HEAD").RunStdString(runOpts) @@ -49,7 +49,7 @@ func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool) } // otherwise, let's try fixing HEAD - err := git.NewCommand(ctx, "symbolic-ref", "--", "HEAD", repo.DefaultBranch).Run(runOpts) + err := git.NewCommand(ctx, "symbolic-ref").AddDashesAndList("HEAD", repo.DefaultBranch).Run(runOpts) if err != nil { logger.Warn("Failed to fix HEAD for %s/%s: %v", repo.OwnerName, repo.Name, err) return nil diff --git a/modules/doctor/mergebase.go b/modules/doctor/mergebase.go index 46369290a1..4a10c72e6e 100644 --- a/modules/doctor/mergebase.go +++ b/modules/doctor/mergebase.go @@ -44,17 +44,17 @@ func checkPRMergeBase(ctx context.Context, logger log.Logger, autofix bool) erro if !pr.HasMerged { var err error - pr.MergeBase, _, err = git.NewCommand(ctx, "merge-base", "--", pr.BaseBranch, pr.GetGitRefName()).RunStdString(&git.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = git.NewCommand(ctx, "merge-base").AddDashesAndList(pr.BaseBranch, pr.GetGitRefName()).RunStdString(&git.RunOpts{Dir: repoPath}) if err != nil { var err2 error - pr.MergeBase, _, err2 = git.NewCommand(ctx, "rev-parse", git.BranchPrefix+pr.BaseBranch).RunStdString(&git.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err2 = git.NewCommand(ctx, "rev-parse").AddDynamicArguments(git.BranchPrefix + pr.BaseBranch).RunStdString(&git.RunOpts{Dir: repoPath}) if err2 != nil { logger.Warn("Unable to get merge base for PR ID %d, #%d onto %s in %s/%s. Error: %v & %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err, err2) return nil } } } else { - parentsString, _, err := git.NewCommand(ctx, "rev-list", "--parents", "-n", "1", pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath}) + parentsString, _, err := git.NewCommand(ctx, "rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(&git.RunOpts{Dir: repoPath}) if err != nil { logger.Warn("Unable to get parents for merged PR ID %d, #%d onto %s in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err) return nil @@ -64,10 +64,10 @@ func checkPRMergeBase(ctx context.Context, logger log.Logger, autofix bool) erro return nil } - args := append([]string{"merge-base", "--"}, parents[1:]...) - args = append(args, pr.GetGitRefName()) - - pr.MergeBase, _, err = git.NewCommand(ctx, args...).RunStdString(&git.RunOpts{Dir: repoPath}) + refs := append([]string{}, parents[1:]...) + refs = append(refs, pr.GetGitRefName()) + cmd := git.NewCommand(ctx, "merge-base").AddDashesAndList(refs...) + pr.MergeBase, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath}) if err != nil { logger.Warn("Unable to get merge base for merged PR ID %d, #%d onto %s in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err) return nil diff --git a/modules/git/command.go b/modules/git/command.go index b24d32dbe8..abf40b0cd7 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -24,7 +24,7 @@ import ( var ( // globalCommandArgs global command args for external package setting - globalCommandArgs []string + globalCommandArgs []CmdArg // defaultCommandExecutionTimeout default command execution timeout duration defaultCommandExecutionTimeout = 360 * time.Second @@ -40,8 +40,11 @@ type Command struct { parentContext context.Context desc string globalArgsLength int + brokenArgs []string } +type CmdArg string + func (c *Command) String() string { if len(c.args) == 0 { return c.name @@ -50,28 +53,40 @@ func (c *Command) String() string { } // NewCommand creates and returns a new Git Command based on given command and arguments. -func NewCommand(ctx context.Context, args ...string) *Command { +// Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead. +func NewCommand(ctx context.Context, args ...CmdArg) *Command { // Make an explicit copy of globalCommandArgs, otherwise append might overwrite it - cargs := make([]string, len(globalCommandArgs)) - copy(cargs, globalCommandArgs) + cargs := make([]string, 0, len(globalCommandArgs)+len(args)) + for _, arg := range globalCommandArgs { + cargs = append(cargs, string(arg)) + } + for _, arg := range args { + cargs = append(cargs, string(arg)) + } return &Command{ name: GitExecutable, - args: append(cargs, args...), + args: cargs, parentContext: ctx, globalArgsLength: len(globalCommandArgs), } } // NewCommandNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args -func NewCommandNoGlobals(args ...string) *Command { +// Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead. +func NewCommandNoGlobals(args ...CmdArg) *Command { return NewCommandContextNoGlobals(DefaultContext, args...) } // NewCommandContextNoGlobals creates and returns a new Git Command based on given command and arguments only with the specify args and don't care global command args -func NewCommandContextNoGlobals(ctx context.Context, args ...string) *Command { +// Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead. +func NewCommandContextNoGlobals(ctx context.Context, args ...CmdArg) *Command { + cargs := make([]string, 0, len(args)) + for _, arg := range args { + cargs = append(cargs, string(arg)) + } return &Command{ name: GitExecutable, - args: args, + args: cargs, parentContext: ctx, } } @@ -89,12 +104,50 @@ func (c *Command) SetDescription(desc string) *Command { return c } -// AddArguments adds new argument(s) to the command. -func (c *Command) AddArguments(args ...string) *Command { +// AddArguments adds new git argument(s) to the command. Each argument must be safe to be trusted. +// User-provided arguments should be passed to AddDynamicArguments instead. +func (c *Command) AddArguments(args ...CmdArg) *Command { + for _, arg := range args { + c.args = append(c.args, string(arg)) + } + return c +} + +// AddDynamicArguments adds new dynamic argument(s) to the command. +// The arguments may come from user input and can not be trusted, so no leading '-' is allowed to avoid passing options +func (c *Command) AddDynamicArguments(args ...string) *Command { + for _, arg := range args { + if arg != "" && arg[0] == '-' { + c.brokenArgs = append(c.brokenArgs, arg) + } + } + if len(c.brokenArgs) != 0 { + return c + } c.args = append(c.args, args...) return c } +// AddDashesAndList adds the "--" and then add the list as arguments, it's usually for adding file list +// At the moment, this function can be only called once, maybe in future it can be refactored to support multiple calls (if necessary) +func (c *Command) AddDashesAndList(list ...string) *Command { + c.args = append(c.args, "--") + // Some old code also checks `arg != ""`, IMO it's not necessary. + // If the check is needed, the list should be prepared before the call to this function + c.args = append(c.args, list...) + return c +} + +// CmdArgCheck checks whether the string is safe to be used as a dynamic argument. +// It panics if the check fails. Usually it should not be used, it's just for refactoring purpose +// deprecated +func CmdArgCheck(s string) CmdArg { + if s != "" && s[0] == '-' { + panic("invalid git cmd argument: " + s) + } + return CmdArg(s) +} + // RunOpts represents parameters to run the command. If UseContextTimeout is specified, then Timeout is ignored. type RunOpts struct { Env []string @@ -133,13 +186,19 @@ func CommonGitCmdEnvs() []string { }...) } -// CommonCmdServEnvs is like CommonGitCmdEnvs but it only returns minimal required environment variables for the "gitea serv" command +// CommonCmdServEnvs is like CommonGitCmdEnvs, but it only returns minimal required environment variables for the "gitea serv" command func CommonCmdServEnvs() []string { return commonBaseEnvs() } +var ErrBrokenCommand = errors.New("git command is broken") + // Run runs the command with the RunOpts func (c *Command) Run(opts *RunOpts) error { + if len(c.brokenArgs) != 0 { + log.Error("git command is broken: %s, broken args: %s", c.String(), strings.Join(c.brokenArgs, " ")) + return ErrBrokenCommand + } if opts == nil { opts = &RunOpts{} } @@ -292,12 +351,12 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS } // AllowLFSFiltersArgs return globalCommandArgs with lfs filter, it should only be used for tests -func AllowLFSFiltersArgs() []string { +func AllowLFSFiltersArgs() []CmdArg { // Now here we should explicitly allow lfs filters to run - filteredLFSGlobalArgs := make([]string, len(globalCommandArgs)) + filteredLFSGlobalArgs := make([]CmdArg, len(globalCommandArgs)) j := 0 for _, arg := range globalCommandArgs { - if strings.Contains(arg, "lfs") { + if strings.Contains(string(arg), "lfs") { j-- } else { filteredLFSGlobalArgs[j] = arg diff --git a/modules/git/command_test.go b/modules/git/command_test.go index 67d4ca388e..52d25c9c74 100644 --- a/modules/git/command_test.go +++ b/modules/git/command_test.go @@ -26,4 +26,19 @@ func TestRunWithContextStd(t *testing.T) { assert.Contains(t, err.Error(), "exit status 129 - unknown option:") assert.Empty(t, stdout) } + + cmd = NewCommand(context.Background()) + cmd.AddDynamicArguments("-test") + assert.ErrorIs(t, cmd.Run(&RunOpts{}), ErrBrokenCommand) + + cmd = NewCommand(context.Background()) + cmd.AddDynamicArguments("--test") + assert.ErrorIs(t, cmd.Run(&RunOpts{}), ErrBrokenCommand) + + subCmd := "version" + cmd = NewCommand(context.Background()).AddDynamicArguments(subCmd) // for test purpose only, the sub-command should never be dynamic for production + stdout, stderr, err = cmd.RunStdString(&RunOpts{}) + assert.NoError(t, err) + assert.Empty(t, stderr) + assert.Contains(t, stdout, "git version") } diff --git a/modules/git/commit.go b/modules/git/commit.go index 32589f5349..061adc1082 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -92,13 +92,13 @@ func AddChanges(repoPath string, all bool, files ...string) error { } // AddChangesWithArgs marks local changes to be ready for commit. -func AddChangesWithArgs(repoPath string, globalArgs []string, all bool, files ...string) error { +func AddChangesWithArgs(repoPath string, globalArgs []CmdArg, all bool, files ...string) error { cmd := NewCommandNoGlobals(append(globalArgs, "add")...) if all { cmd.AddArguments("--all") } - cmd.AddArguments("--") - _, _, err := cmd.AddArguments(files...).RunStdString(&RunOpts{Dir: repoPath}) + cmd.AddDashesAndList(files...) + _, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) return err } @@ -112,17 +112,17 @@ type CommitChangesOptions struct { // CommitChanges commits local changes with given committer, author and message. // If author is nil, it will be the same as committer. func CommitChanges(repoPath string, opts CommitChangesOptions) error { - cargs := make([]string, len(globalCommandArgs)) + cargs := make([]CmdArg, len(globalCommandArgs)) copy(cargs, globalCommandArgs) return CommitChangesWithArgs(repoPath, cargs, opts) } // CommitChangesWithArgs commits local changes with given committer, author and message. // If author is nil, it will be the same as committer. -func CommitChangesWithArgs(repoPath string, args []string, opts CommitChangesOptions) error { +func CommitChangesWithArgs(repoPath string, args []CmdArg, opts CommitChangesOptions) error { cmd := NewCommandNoGlobals(args...) if opts.Committer != nil { - cmd.AddArguments("-c", "user.name="+opts.Committer.Name, "-c", "user.email="+opts.Committer.Email) + cmd.AddArguments("-c", CmdArg("user.name="+opts.Committer.Name), "-c", CmdArg("user.email="+opts.Committer.Email)) } cmd.AddArguments("commit") @@ -130,9 +130,9 @@ func CommitChangesWithArgs(repoPath string, args []string, opts CommitChangesOpt opts.Author = opts.Committer } if opts.Author != nil { - cmd.AddArguments(fmt.Sprintf("--author='%s <%s>'", opts.Author.Name, opts.Author.Email)) + cmd.AddArguments(CmdArg(fmt.Sprintf("--author='%s <%s>'", opts.Author.Name, opts.Author.Email))) } - cmd.AddArguments("-m", opts.Message) + cmd.AddArguments("-m").AddDynamicArguments(opts.Message) _, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) // No stderr but exit status 1 means nothing to commit. @@ -144,15 +144,13 @@ func CommitChangesWithArgs(repoPath string, args []string, opts CommitChangesOpt // AllCommitsCount returns count of all commits in repository func AllCommitsCount(ctx context.Context, repoPath string, hidePRRefs bool, files ...string) (int64, error) { - args := []string{"--all", "--count"} - if hidePRRefs { - args = append([]string{"--exclude=" + PullPrefix + "*"}, args...) - } cmd := NewCommand(ctx, "rev-list") - cmd.AddArguments(args...) + if hidePRRefs { + cmd.AddArguments("--exclude=" + PullPrefix + "*") + } + cmd.AddArguments("--all", "--count") if len(files) > 0 { - cmd.AddArguments("--") - cmd.AddArguments(files...) + cmd.AddDashesAndList(files...) } stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) @@ -166,10 +164,9 @@ func AllCommitsCount(ctx context.Context, repoPath string, hidePRRefs bool, file // CommitsCountFiles returns number of total commits of until given revision. func CommitsCountFiles(ctx context.Context, repoPath string, revision, relpath []string) (int64, error) { cmd := NewCommand(ctx, "rev-list", "--count") - cmd.AddArguments(revision...) + cmd.AddDynamicArguments(revision...) if len(relpath) > 0 { - cmd.AddArguments("--") - cmd.AddArguments(relpath...) + cmd.AddDashesAndList(relpath...) } stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) @@ -209,7 +206,7 @@ func (c *Commit) HasPreviousCommit(commitHash SHA1) (bool, error) { return false, nil } - _, _, err := NewCommand(c.repo.Ctx, "merge-base", "--is-ancestor", that, this).RunStdString(&RunOpts{Dir: c.repo.Path}) + _, _, err := NewCommand(c.repo.Ctx, "merge-base", "--is-ancestor").AddDynamicArguments(that, this).RunStdString(&RunOpts{Dir: c.repo.Path}) if err == nil { return true, nil } @@ -392,15 +389,12 @@ func (c *Commit) GetSubModule(entryname string) (*SubModule, error) { // GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') func (c *Commit) GetBranchName() (string, error) { - args := []string{ - "name-rev", - } + cmd := NewCommand(c.repo.Ctx, "name-rev") if CheckGitVersionAtLeast("2.13.0") == nil { - args = append(args, "--exclude", "refs/tags/*") + cmd.AddArguments("--exclude", "refs/tags/*") } - args = append(args, "--name-only", "--no-undefined", c.ID.String()) - - data, _, err := NewCommand(c.repo.Ctx, args...).RunStdString(&RunOpts{Dir: c.repo.Path}) + cmd.AddArguments("--name-only", "--no-undefined").AddDynamicArguments(c.ID.String()) + data, _, err := cmd.RunStdString(&RunOpts{Dir: c.repo.Path}) if err != nil { // handle special case where git can not describe commit if strings.Contains(err.Error(), "cannot describe") { @@ -426,7 +420,7 @@ func (c *Commit) LoadBranchName() (err error) { // GetTagName gets the current tag name for given commit func (c *Commit) GetTagName() (string, error) { - data, _, err := NewCommand(c.repo.Ctx, "describe", "--exact-match", "--tags", "--always", c.ID.String()).RunStdString(&RunOpts{Dir: c.repo.Path}) + data, _, err := NewCommand(c.repo.Ctx, "describe", "--exact-match", "--tags", "--always").AddDynamicArguments(c.ID.String()).RunStdString(&RunOpts{Dir: c.repo.Path}) if err != nil { // handle special case where there is no tag for this commit if strings.Contains(err.Error(), "no tag exactly matches") { @@ -503,9 +497,7 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi }() stderr := new(bytes.Buffer) - args := []string{"log", "--name-status", "-c", "--pretty=format:", "--parents", "--no-renames", "-z", "-1", commitID} - - err := NewCommand(ctx, args...).Run(&RunOpts{ + err := NewCommand(ctx, "log", "--name-status", "-c", "--pretty=format:", "--parents", "--no-renames", "-z", "-1").AddDynamicArguments(commitID).Run(&RunOpts{ Dir: repoPath, Stdout: w, Stderr: stderr, @@ -521,7 +513,7 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi // GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository. func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) { - commitID, _, err := NewCommand(ctx, "rev-parse", shortID).RunStdString(&RunOpts{Dir: repoPath}) + commitID, _, err := NewCommand(ctx, "rev-parse").AddDynamicArguments(shortID).RunStdString(&RunOpts{Dir: repoPath}) if err != nil { if strings.Contains(err.Error(), "exit status 128") { return "", ErrNotExist{shortID, ""} diff --git a/modules/git/diff.go b/modules/git/diff.go index f75ebd4fd2..13ff6bd1e6 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -35,7 +35,7 @@ func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer // GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer. func GetReverseRawDiff(ctx context.Context, repoPath, commitID string, writer io.Writer) error { stderr := new(bytes.Buffer) - cmd := NewCommand(ctx, "show", "--pretty=format:revert %H%n", "-R", commitID) + cmd := NewCommand(ctx, "show", "--pretty=format:revert %H%n", "-R").AddDynamicArguments(commitID) if err := cmd.Run(&RunOpts{ Dir: repoPath, Stdout: writer, @@ -52,39 +52,38 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff if err != nil { return err } - fileArgs := make([]string, 0) + var files []string if len(file) > 0 { - fileArgs = append(fileArgs, "--", file) + files = append(files, file) } - var args []string + cmd := NewCommand(repo.Ctx) switch diffType { case RawDiffNormal: if len(startCommit) != 0 { - args = append([]string{"diff", "-M", startCommit, endCommit}, fileArgs...) + cmd.AddArguments("diff", "-M").AddDynamicArguments(startCommit, endCommit).AddDashesAndList(files...) } else if commit.ParentCount() == 0 { - args = append([]string{"show", endCommit}, fileArgs...) + cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...) } else { c, _ := commit.Parent(0) - args = append([]string{"diff", "-M", c.ID.String(), endCommit}, fileArgs...) + cmd.AddArguments("diff", "-M").AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...) } case RawDiffPatch: if len(startCommit) != 0 { query := fmt.Sprintf("%s...%s", endCommit, startCommit) - args = append([]string{"format-patch", "--no-signature", "--stdout", "--root", query}, fileArgs...) + cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(query).AddDashesAndList(files...) } else if commit.ParentCount() == 0 { - args = append([]string{"format-patch", "--no-signature", "--stdout", "--root", endCommit}, fileArgs...) + cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...) } else { c, _ := commit.Parent(0) query := fmt.Sprintf("%s...%s", endCommit, c.ID.String()) - args = append([]string{"format-patch", "--no-signature", "--stdout", query}, fileArgs...) + cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...) } default: return fmt.Errorf("invalid diffType: %s", diffType) } stderr := new(bytes.Buffer) - cmd := NewCommand(repo.Ctx, args...) if err = cmd.Run(&RunOpts{ Dir: repo.Path, Stdout: writer, @@ -287,7 +286,7 @@ func GetAffectedFiles(repo *Repository, oldCommitID, newCommitID string, env []s affectedFiles := make([]string, 0, 32) // Run `git diff --name-only` to get the names of the changed files - err = NewCommand(repo.Ctx, "diff", "--name-only", oldCommitID, newCommitID). + err = NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID). Run(&RunOpts{ Env: env, Dir: repo.Path, diff --git a/modules/git/error.go b/modules/git/error.go index 080f077aad..42d4b761bd 100644 --- a/modules/git/error.go +++ b/modules/git/error.go @@ -8,6 +8,8 @@ import ( "fmt" "strings" "time" + + "code.gitea.io/gitea/modules/util" ) // ErrExecTimeout error when exec timed out @@ -41,6 +43,10 @@ func (err ErrNotExist) Error() string { return fmt.Sprintf("object does not exist [id: %s, rel_path: %s]", err.ID, err.RelPath) } +func (err ErrNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrBadLink entry.FollowLink error type ErrBadLink struct { Name string @@ -87,6 +93,10 @@ func (err ErrBranchNotExist) Error() string { return fmt.Sprintf("branch does not exist [name: %s]", err.Name) } +func (err ErrBranchNotExist) Unwrap() error { + return util.ErrNotExist +} + // ErrPushOutOfDate represents an error if merging fails due to unrelated histories type ErrPushOutOfDate struct { StdOut string diff --git a/modules/git/git.go b/modules/git/git.go index 28899222e7..18d62838df 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -313,7 +313,7 @@ func CheckGitVersionAtLeast(atLeast string) error { } func configSet(key, value string) error { - stdout, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil) + stdout, _, err := NewCommand(DefaultContext, "config", "--get").AddDynamicArguments(key).RunStdString(nil) if err != nil && !err.IsExitCode(1) { return fmt.Errorf("failed to get git config %s, err: %w", key, err) } @@ -323,7 +323,7 @@ func configSet(key, value string) error { return nil } - _, _, err = NewCommand(DefaultContext, "config", "--global", key, value).RunStdString(nil) + _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) if err != nil { return fmt.Errorf("failed to set git global config %s, err: %w", key, err) } @@ -332,14 +332,14 @@ func configSet(key, value string) error { } func configSetNonExist(key, value string) error { - _, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil) + _, _, err := NewCommand(DefaultContext, "config", "--get").AddDynamicArguments(key).RunStdString(nil) if err == nil { // already exist return nil } if err.IsExitCode(1) { // not exist, set new config - _, _, err = NewCommand(DefaultContext, "config", "--global", key, value).RunStdString(nil) + _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) if err != nil { return fmt.Errorf("failed to set git global config %s, err: %w", key, err) } @@ -350,14 +350,14 @@ func configSetNonExist(key, value string) error { } func configAddNonExist(key, value string) error { - _, _, err := NewCommand(DefaultContext, "config", "--get", key, regexp.QuoteMeta(value)).RunStdString(nil) + _, _, err := NewCommand(DefaultContext, "config", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) if err == nil { // already exist return nil } if err.IsExitCode(1) { // not exist, add new config - _, _, err = NewCommand(DefaultContext, "config", "--global", "--add", key, value).RunStdString(nil) + _, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil) if err != nil { return fmt.Errorf("failed to add git global config %s, err: %w", key, err) } @@ -367,10 +367,10 @@ func configAddNonExist(key, value string) error { } func configUnsetAll(key, value string) error { - _, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil) + _, _, err := NewCommand(DefaultContext, "config", "--get").AddDynamicArguments(key).RunStdString(nil) if err == nil { // exist, need to remove - _, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all", key, regexp.QuoteMeta(value)).RunStdString(nil) + _, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) if err != nil { return fmt.Errorf("failed to unset git global config %s, err: %w", key, err) } @@ -384,6 +384,6 @@ func configUnsetAll(key, value string) error { } // Fsck verifies the connectivity and validity of the objects in the database -func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args ...string) error { +func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args ...CmdArg) error { return NewCommand(ctx, "fsck").AddArguments(args...).Run(&RunOpts{Timeout: timeout, Dir: repoPath}) } diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index 469932525f..dee4fc226e 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -35,30 +35,33 @@ func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, p _ = stdoutWriter.Close() } - args := make([]string, 0, 8+len(paths)) - args = append(args, "log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z", head, "--") + cmd := NewCommand(ctx) + cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head) + + var files []string if len(paths) < 70 { if treepath != "" { - args = append(args, treepath) + files = append(files, treepath) for _, pth := range paths { if pth != "" { - args = append(args, path.Join(treepath, pth)) + files = append(files, path.Join(treepath, pth)) } } } else { for _, pth := range paths { if pth != "" { - args = append(args, pth) + files = append(files, pth) } } } } else if treepath != "" { - args = append(args, treepath) + files = append(files, treepath) } + cmd.AddDashesAndList(files...) go func() { stderr := strings.Builder{} - err := NewCommand(ctx, args...).Run(&RunOpts{ + err := cmd.Run(&RunOpts{ Dir: repository, Stdout: stdoutWriter, Stderr: &stderr, diff --git a/modules/git/pipeline/revlist.go b/modules/git/pipeline/revlist.go index 02619cb583..5f62e85e7f 100644 --- a/modules/git/pipeline/revlist.go +++ b/modules/git/pipeline/revlist.go @@ -43,7 +43,7 @@ func RevListObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync. defer revListWriter.Close() stderr := new(bytes.Buffer) var errbuf strings.Builder - cmd := git.NewCommand(ctx, "rev-list", "--objects", headSHA, "--not", baseSHA) + cmd := git.NewCommand(ctx, "rev-list", "--objects").AddDynamicArguments(headSHA).AddArguments("--not").AddDynamicArguments(baseSHA) if err := cmd.Run(&git.RunOpts{ Dir: tmpBasePath, Stdout: revListWriter, diff --git a/modules/git/remote.go b/modules/git/remote.go index cbb4ac6126..c416eea136 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -14,9 +14,9 @@ import ( func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) { var cmd *Command if CheckGitVersionAtLeast("2.7") == nil { - cmd = NewCommand(ctx, "remote", "get-url", remoteName) + cmd = NewCommand(ctx, "remote", "get-url").AddDynamicArguments(remoteName) } else { - cmd = NewCommand(ctx, "config", "--get", "remote."+remoteName+".url") + cmd = NewCommand(ctx, "config", "--get").AddDynamicArguments("remote." + remoteName + ".url") } result, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) diff --git a/modules/git/repo.go b/modules/git/repo.go index 3176e27695..575b686e29 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -59,7 +59,7 @@ func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, erro // IsRepoURLAccessible checks if given repository URL is accessible. func IsRepoURLAccessible(ctx context.Context, url string) bool { - _, _, err := NewCommand(ctx, "ls-remote", "-q", "-h", url, "HEAD").RunStdString(nil) + _, _, err := NewCommand(ctx, "ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(nil) return err == nil } @@ -112,13 +112,11 @@ type CloneRepoOptions struct { // Clone clones original repository to target path. func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error { - cargs := make([]string, len(globalCommandArgs)) - copy(cargs, globalCommandArgs) - return CloneWithArgs(ctx, from, to, cargs, opts) + return CloneWithArgs(ctx, globalCommandArgs, from, to, opts) } // CloneWithArgs original repository to target path. -func CloneWithArgs(ctx context.Context, from, to string, args []string, opts CloneRepoOptions) (err error) { +func CloneWithArgs(ctx context.Context, args []CmdArg, from, to string, opts CloneRepoOptions) (err error) { toDir := path.Dir(to) if err = os.MkdirAll(toDir, os.ModePerm); err != nil { return err @@ -144,15 +142,15 @@ func CloneWithArgs(ctx context.Context, from, to string, args []string, opts Clo cmd.AddArguments("--no-checkout") } if opts.Depth > 0 { - cmd.AddArguments("--depth", strconv.Itoa(opts.Depth)) + cmd.AddArguments("--depth").AddDynamicArguments(strconv.Itoa(opts.Depth)) } if opts.Filter != "" { - cmd.AddArguments("--filter", opts.Filter) + cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter) } if len(opts.Branch) > 0 { - cmd.AddArguments("-b", opts.Branch) + cmd.AddArguments("-b").AddDynamicArguments(opts.Branch) } - cmd.AddArguments("--", from, to) + cmd.AddDashesAndList(from, to) if strings.Contains(from, "://") && strings.Contains(from, "@") { cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, util.SanitizeCredentialURLs(from), to, opts.Shared, opts.Mirror, opts.Depth)) @@ -203,10 +201,12 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error { if opts.Mirror { cmd.AddArguments("--mirror") } - cmd.AddArguments("--", opts.Remote) + remoteBranchArgs := []string{opts.Remote} if len(opts.Branch) > 0 { - cmd.AddArguments(opts.Branch) + remoteBranchArgs = append(remoteBranchArgs, opts.Branch) } + cmd.AddDashesAndList(remoteBranchArgs...) + if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") { cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, util.SanitizeCredentialURLs(opts.Remote), opts.Force, opts.Mirror)) } else { @@ -276,7 +276,7 @@ type DivergeObject struct { func checkDivergence(ctx context.Context, repoPath, baseBranch, targetBranch string) (int, error) { branches := fmt.Sprintf("%s..%s", baseBranch, targetBranch) - cmd := NewCommand(ctx, "rev-list", "--count", branches) + cmd := NewCommand(ctx, "rev-list", "--count").AddDynamicArguments(branches) stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) if err != nil { return -1, err @@ -319,7 +319,7 @@ func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io. return err } - _, _, err = NewCommand(ctx, "reset", "--soft", commit).RunStdString(&RunOpts{Dir: tmp, Env: env}) + _, _, err = NewCommand(ctx, "reset", "--soft").AddDynamicArguments(commit).RunStdString(&RunOpts{Dir: tmp, Env: env}) if err != nil { return err } @@ -330,7 +330,7 @@ func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io. } tmpFile := filepath.Join(tmp, "bundle") - _, _, err = NewCommand(ctx, "bundle", "create", tmpFile, "bundle", "HEAD").RunStdString(&RunOpts{Dir: tmp, Env: env}) + _, _, err = NewCommand(ctx, "bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(&RunOpts{Dir: tmp, Env: env}) if err != nil { return err } diff --git a/modules/git/repo_archive.go b/modules/git/repo_archive.go index 4a97989949..a0cbfba5d9 100644 --- a/modules/git/repo_archive.go +++ b/modules/git/repo_archive.go @@ -44,20 +44,15 @@ func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, t return fmt.Errorf("unknown format: %v", format) } - args := []string{ - "archive", - } + cmd := NewCommand(ctx, "archive") if usePrefix { - args = append(args, "--prefix="+filepath.Base(strings.TrimSuffix(repo.Path, ".git"))+"/") + cmd.AddArguments(CmdArg("--prefix=" + filepath.Base(strings.TrimSuffix(repo.Path, ".git")) + "/")) } - - args = append(args, - "--format="+format.String(), - commitID, - ) + cmd.AddArguments(CmdArg("--format=" + format.String())) + cmd.AddDynamicArguments(commitID) var stderr strings.Builder - err := NewCommand(ctx, args...).Run(&RunOpts{ + err := cmd.Run(&RunOpts{ Dir: repo.Path, Stdout: target, Stderr: &stderr, diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index 21ff93e498..60dc993dc2 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -20,7 +20,7 @@ import ( type CheckAttributeOpts struct { CachedOnly bool AllAttributes bool - Attributes []string + Attributes []CmdArg Filenames []string IndexFile string WorkTree string @@ -44,31 +44,23 @@ func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[ stdOut := new(bytes.Buffer) stdErr := new(bytes.Buffer) - cmdArgs := []string{"check-attr", "-z"} + cmd := NewCommand(repo.Ctx, "check-attr", "-z") if opts.AllAttributes { - cmdArgs = append(cmdArgs, "-a") + cmd.AddArguments("-a") } else { for _, attribute := range opts.Attributes { if attribute != "" { - cmdArgs = append(cmdArgs, attribute) + cmd.AddArguments(attribute) } } } if opts.CachedOnly { - cmdArgs = append(cmdArgs, "--cached") + cmd.AddArguments("--cached") } - cmdArgs = append(cmdArgs, "--") - - for _, arg := range opts.Filenames { - if arg != "" { - cmdArgs = append(cmdArgs, arg) - } - } - - cmd := NewCommand(repo.Ctx, cmdArgs...) + cmd.AddDashesAndList(opts.Filenames...) if err := cmd.Run(&RunOpts{ Env: env, @@ -106,7 +98,7 @@ func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[ // CheckAttributeReader provides a reader for check-attribute content that can be long running type CheckAttributeReader struct { // params - Attributes []string + Attributes []CmdArg Repo *Repository IndexFile string WorkTree string @@ -122,7 +114,7 @@ type CheckAttributeReader struct { // Init initializes the CheckAttributeReader func (c *CheckAttributeReader) Init(ctx context.Context) error { - cmdArgs := []string{"check-attr", "--stdin", "-z"} + cmdArgs := []CmdArg{"check-attr", "--stdin", "-z"} if len(c.IndexFile) > 0 { cmdArgs = append(cmdArgs, "--cached") @@ -401,7 +393,7 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe } checker := &CheckAttributeReader{ - Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"}, + Attributes: []CmdArg{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"}, Repo: repo, IndexFile: indexFilename, WorkTree: worktree, diff --git a/modules/git/repo_blame.go b/modules/git/repo_blame.go index 6fe6d235ba..8a3707aa09 100644 --- a/modules/git/repo_blame.go +++ b/modules/git/repo_blame.go @@ -8,13 +8,16 @@ import "fmt" // FileBlame return the Blame object of file func (repo *Repository) FileBlame(revision, path, file string) ([]byte, error) { - stdout, _, err := NewCommand(repo.Ctx, "blame", "--root", "--", file).RunStdBytes(&RunOpts{Dir: path}) + stdout, _, err := NewCommand(repo.Ctx, "blame", "--root").AddDashesAndList(file).RunStdBytes(&RunOpts{Dir: path}) return stdout, err } // LineBlame returns the latest commit at the given line func (repo *Repository) LineBlame(revision, path, file string, line uint) (*Commit, error) { - res, _, err := NewCommand(repo.Ctx, "blame", fmt.Sprintf("-L %d,%d", line, line), "-p", revision, "--", file).RunStdString(&RunOpts{Dir: path}) + res, _, err := NewCommand(repo.Ctx, "blame"). + AddArguments(CmdArg(fmt.Sprintf("-L %d,%d", line, line))). + AddArguments("-p").AddDynamicArguments(revision). + AddDashesAndList(file).RunStdString(&RunOpts{Dir: path}) if err != nil { return nil, err } diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 17d243808e..a3fc7e0c42 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -25,7 +25,7 @@ const PullRequestPrefix = "refs/for/" // IsReferenceExist returns true if given reference exists in the repository. func IsReferenceExist(ctx context.Context, repoPath, name string) bool { - _, _, err := NewCommand(ctx, "show-ref", "--verify", "--", name).RunStdString(&RunOpts{Dir: repoPath}) + _, _, err := NewCommand(ctx, "show-ref", "--verify").AddDashesAndList(name).RunStdString(&RunOpts{Dir: repoPath}) return err == nil } @@ -66,7 +66,7 @@ func (repo *Repository) GetHEADBranch() (*Branch, error) { // SetDefaultBranch sets default branch of repository. func (repo *Repository) SetDefaultBranch(name string) error { - _, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD", BranchPrefix+name).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand(repo.Ctx, "symbolic-ref", "HEAD").AddDynamicArguments(BranchPrefix + name).RunStdString(&RunOpts{Dir: repo.Path}) return err } @@ -141,7 +141,7 @@ func (repo *Repository) DeleteBranch(name string, opts DeleteBranchOptions) erro cmd.AddArguments("-d") } - cmd.AddArguments("--", name) + cmd.AddDashesAndList(name) _, _, err := cmd.RunStdString(&RunOpts{Dir: repo.Path}) return err @@ -150,7 +150,7 @@ func (repo *Repository) DeleteBranch(name string, opts DeleteBranchOptions) erro // CreateBranch create a new branch func (repo *Repository) CreateBranch(branch, oldbranchOrCommit string) error { cmd := NewCommand(repo.Ctx, "branch") - cmd.AddArguments("--", branch, oldbranchOrCommit) + cmd.AddDashesAndList(branch, oldbranchOrCommit) _, _, err := cmd.RunStdString(&RunOpts{Dir: repo.Path}) @@ -163,7 +163,7 @@ func (repo *Repository) AddRemote(name, url string, fetch bool) error { if fetch { cmd.AddArguments("-f") } - cmd.AddArguments(name, url) + cmd.AddDynamicArguments(name, url) _, _, err := cmd.RunStdString(&RunOpts{Dir: repo.Path}) return err @@ -171,7 +171,7 @@ func (repo *Repository) AddRemote(name, url string, fetch bool) error { // RemoveRemote removes a remote from repository. func (repo *Repository) RemoveRemote(name string) error { - _, _, err := NewCommand(repo.Ctx, "remote", "rm", name).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand(repo.Ctx, "remote", "rm").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path}) return err } @@ -182,6 +182,6 @@ func (branch *Branch) GetCommit() (*Commit, error) { // RenameBranch rename a branch func (repo *Repository) RenameBranch(from, to string) error { - _, _, err := NewCommand(repo.Ctx, "branch", "-m", from, to).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand(repo.Ctx, "branch", "-m").AddDynamicArguments(from, to).RunStdString(&RunOpts{Dir: repo.Path}) return err } diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go index 91112d0190..95c3718841 100644 --- a/modules/git/repo_branch_nogogit.go +++ b/modules/git/repo_branch_nogogit.go @@ -63,7 +63,7 @@ func (repo *Repository) IsBranchExist(name string) bool { // GetBranchNames returns branches from the repository, skipping skip initial branches and // returning at most limit branches, or all branches if limit is 0. func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { - return callShowRef(repo.Ctx, repo.Path, BranchPrefix, []string{BranchPrefix, "--sort=-committerdate"}, skip, limit) + return callShowRef(repo.Ctx, repo.Path, BranchPrefix, []CmdArg{BranchPrefix, "--sort=-committerdate"}, skip, limit) } // WalkReferences walks all the references from the repository @@ -74,19 +74,19 @@ func WalkReferences(ctx context.Context, repoPath string, walkfn func(sha1, refn // WalkReferences walks all the references from the repository // refType should be empty, ObjectTag or ObjectBranch. All other values are equivalent to empty. func (repo *Repository) WalkReferences(refType ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) { - var args []string + var args []CmdArg switch refType { case ObjectTag: - args = []string{TagPrefix, "--sort=-taggerdate"} + args = []CmdArg{TagPrefix, "--sort=-taggerdate"} case ObjectBranch: - args = []string{BranchPrefix, "--sort=-committerdate"} + args = []CmdArg{BranchPrefix, "--sort=-committerdate"} } return walkShowRef(repo.Ctx, repo.Path, args, skip, limit, walkfn) } // callShowRef return refs, if limit = 0 it will not limit -func callShowRef(ctx context.Context, repoPath, trimPrefix string, extraArgs []string, skip, limit int) (branchNames []string, countAll int, err error) { +func callShowRef(ctx context.Context, repoPath, trimPrefix string, extraArgs []CmdArg, skip, limit int) (branchNames []string, countAll int, err error) { countAll, err = walkShowRef(ctx, repoPath, extraArgs, skip, limit, func(_, branchName string) error { branchName = strings.TrimPrefix(branchName, trimPrefix) branchNames = append(branchNames, branchName) @@ -96,7 +96,7 @@ func callShowRef(ctx context.Context, repoPath, trimPrefix string, extraArgs []s return branchNames, countAll, err } -func walkShowRef(ctx context.Context, repoPath string, extraArgs []string, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) { +func walkShowRef(ctx context.Context, repoPath string, extraArgs []CmdArg, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) { stdoutReader, stdoutWriter := io.Pipe() defer func() { _ = stdoutReader.Close() @@ -105,7 +105,7 @@ func walkShowRef(ctx context.Context, repoPath string, extraArgs []string, skip, go func() { stderrBuilder := &strings.Builder{} - args := []string{"for-each-ref", "--format=%(objectname) %(refname)"} + args := []CmdArg{"for-each-ref", "--format=%(objectname) %(refname)"} args = append(args, extraArgs...) err := NewCommand(ctx, args...).Run(&RunOpts{ Dir: repoPath, diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index d3731cb928..90259fd746 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -61,7 +61,7 @@ func (repo *Repository) getCommitByPathWithID(id SHA1, relpath string) (*Commit, relpath = `\` + relpath } - stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat, id.String(), "--", relpath).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat).AddDynamicArguments(id.String()).AddDashesAndList(relpath).RunStdString(&RunOpts{Dir: repo.Path}) if runErr != nil { return nil, runErr } @@ -76,7 +76,7 @@ func (repo *Repository) getCommitByPathWithID(id SHA1, relpath string) (*Commit, // GetCommitByPath returns the last commit of relative path. func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { - stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat, "--", relpath).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat).AddDashesAndList(relpath).RunStdBytes(&RunOpts{Dir: repo.Path}) if runErr != nil { return nil, runErr } @@ -89,8 +89,10 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { } func (repo *Repository) commitsByRange(id SHA1, page, pageSize int) ([]*Commit, error) { - stdout, _, err := NewCommand(repo.Ctx, "log", id.String(), "--skip="+strconv.Itoa((page-1)*pageSize), - "--max-count="+strconv.Itoa(pageSize), prettyLogFormat).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand(repo.Ctx, "log"). + AddArguments(CmdArg("--skip="+strconv.Itoa((page-1)*pageSize)), CmdArg("--max-count="+strconv.Itoa(pageSize)), prettyLogFormat). + AddDynamicArguments(id.String()). + RunStdBytes(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err } @@ -99,30 +101,30 @@ func (repo *Repository) commitsByRange(id SHA1, page, pageSize int) ([]*Commit, func (repo *Repository) searchCommits(id SHA1, opts SearchCommitsOptions) ([]*Commit, error) { // create new git log command with limit of 100 commis - cmd := NewCommand(repo.Ctx, "log", id.String(), "-100", prettyLogFormat) + cmd := NewCommand(repo.Ctx, "log", "-100", prettyLogFormat).AddDynamicArguments(id.String()) // ignore case - args := []string{"-i"} + args := []CmdArg{"-i"} // add authors if present in search query if len(opts.Authors) > 0 { for _, v := range opts.Authors { - args = append(args, "--author="+v) + args = append(args, CmdArg("--author="+v)) } } // add committers if present in search query if len(opts.Committers) > 0 { for _, v := range opts.Committers { - args = append(args, "--committer="+v) + args = append(args, CmdArg("--committer="+v)) } } // add time constraints if present in search query if len(opts.After) > 0 { - args = append(args, "--after="+opts.After) + args = append(args, CmdArg("--after="+opts.After)) } if len(opts.Before) > 0 { - args = append(args, "--before="+opts.Before) + args = append(args, CmdArg("--before="+opts.Before)) } // pretend that all refs along with HEAD were listed on command line as @@ -136,7 +138,7 @@ func (repo *Repository) searchCommits(id SHA1, opts SearchCommitsOptions) ([]*Co // note this is done only for command created above if len(opts.Keywords) > 0 { for _, v := range opts.Keywords { - cmd.AddArguments("--grep=" + v) + cmd.AddArguments(CmdArg("--grep=" + v)) } } @@ -154,14 +156,14 @@ func (repo *Repository) searchCommits(id SHA1, opts SearchCommitsOptions) ([]*Co // then let's iterate over them if len(opts.Keywords) > 0 { for _, v := range opts.Keywords { - // ignore anything below 4 characters as too unspecific - if len(v) >= 4 { + // ignore anything not matching a valid sha pattern + if IsValidSHAPattern(v) { // create new git log command with 1 commit limit hashCmd := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat) // add previous arguments except for --grep and --all hashCmd.AddArguments(args...) // add keyword as - hashCmd.AddArguments(v) + hashCmd.AddDynamicArguments(v) // search with given constraints for commit matching sha hash of v hashMatching, _, err := hashCmd.RunStdBytes(&RunOpts{Dir: repo.Path}) @@ -178,7 +180,7 @@ func (repo *Repository) searchCommits(id SHA1, opts SearchCommitsOptions) ([]*Co } func (repo *Repository) getFilesChanged(id1, id2 string) ([]string, error) { - stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", id1, id2).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(id1, id2).RunStdBytes(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err } @@ -188,7 +190,7 @@ func (repo *Repository) getFilesChanged(id1, id2 string) ([]string, error) { // FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2 // You must ensure that id1 and id2 are valid commit ids. func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bool, error) { - stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", "-z", id1, id2, "--", filename).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", "-z").AddDynamicArguments(id1, id2).AddDashesAndList(filename).RunStdBytes(&RunOpts{Dir: repo.Path}) if err != nil { return false, err } @@ -211,14 +213,16 @@ func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) ( }() go func() { stderr := strings.Builder{} - err := NewCommand(repo.Ctx, "rev-list", revision, - "--max-count="+strconv.Itoa(setting.Git.CommitsRangeSize*page), - "--skip="+strconv.Itoa(skip), "--", file). - Run(&RunOpts{ - Dir: repo.Path, - Stdout: stdoutWriter, - Stderr: &stderr, - }) + gitCmd := NewCommand(repo.Ctx, "rev-list"). + AddArguments(CmdArg("--max-count=" + strconv.Itoa(setting.Git.CommitsRangeSize*page))). + AddArguments(CmdArg("--skip=" + strconv.Itoa(skip))) + gitCmd.AddDynamicArguments(revision) + gitCmd.AddDashesAndList(file) + err := gitCmd.Run(&RunOpts{ + Dir: repo.Path, + Stdout: stdoutWriter, + Stderr: &stderr, + }) if err != nil { _ = stdoutWriter.CloseWithError(ConcatenateError(err, (&stderr).String())) } else { @@ -254,11 +258,11 @@ func (repo *Repository) CommitsByFileAndRange(revision, file string, page int) ( // FilesCountBetween return the number of files changed between two commits func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (int, error) { - stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", startCommitID+"..."+endCommitID).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(startCommitID + "..." + endCommitID).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil && strings.Contains(err.Error(), "no merge base") { // git >= 2.28 now returns an error if startCommitID and endCommitID have become unrelated. // previously it would return the results of git diff --name-only startCommitID endCommitID so let's try that... - stdout, _, err = NewCommand(repo.Ctx, "diff", "--name-only", startCommitID, endCommitID).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(startCommitID, endCommitID).RunStdString(&RunOpts{Dir: repo.Path}) } if err != nil { return 0, err @@ -272,13 +276,13 @@ func (repo *Repository) CommitsBetween(last, before *Commit) ([]*Commit, error) var stdout []byte var err error if before == nil { - stdout, _, err = NewCommand(repo.Ctx, "rev-list", last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) } else { - stdout, _, err = NewCommand(repo.Ctx, "rev-list", before.ID.String()+".."+last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String() + ".." + last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list before last so let's try that... - stdout, _, err = NewCommand(repo.Ctx, "rev-list", before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) } } if err != nil { @@ -292,13 +296,22 @@ func (repo *Repository) CommitsBetweenLimit(last, before *Commit, limit, skip in var stdout []byte var err error if before == nil { - stdout, _, err = NewCommand(repo.Ctx, "rev-list", "--max-count", strconv.Itoa(limit), "--skip", strconv.Itoa(skip), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand(repo.Ctx, "rev-list", + "--max-count", CmdArg(strconv.Itoa(limit)), + "--skip", CmdArg(strconv.Itoa(skip))). + AddDynamicArguments(last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) } else { - stdout, _, err = NewCommand(repo.Ctx, "rev-list", "--max-count", strconv.Itoa(limit), "--skip", strconv.Itoa(skip), before.ID.String()+".."+last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand(repo.Ctx, "rev-list", + "--max-count", CmdArg(strconv.Itoa(limit)), + "--skip", CmdArg(strconv.Itoa(skip))). + AddDynamicArguments(before.ID.String() + ".." + last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list --max-count n before last so let's try that... - stdout, _, err = NewCommand(repo.Ctx, "rev-list", "--max-count", strconv.Itoa(limit), "--skip", strconv.Itoa(skip), before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + stdout, _, err = NewCommand(repo.Ctx, "rev-list", + "--max-count", CmdArg(strconv.Itoa(limit)), + "--skip", CmdArg(strconv.Itoa(skip))). + AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) } } if err != nil { @@ -339,9 +352,9 @@ func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) { func (repo *Repository) commitsBefore(id SHA1, limit int) ([]*Commit, error) { cmd := NewCommand(repo.Ctx, "log") if limit > 0 { - cmd.AddArguments("-"+strconv.Itoa(limit), prettyLogFormat, id.String()) + cmd.AddArguments(CmdArg("-"+strconv.Itoa(limit)), prettyLogFormat).AddDynamicArguments(id.String()) } else { - cmd.AddArguments(prettyLogFormat, id.String()) + cmd.AddArguments(prettyLogFormat).AddDynamicArguments(id.String()) } stdout, _, runErr := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) @@ -381,7 +394,11 @@ func (repo *Repository) getCommitsBeforeLimit(id SHA1, num int) ([]*Commit, erro func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error) { if CheckGitVersionAtLeast("2.7.0") == nil { - stdout, _, err := NewCommand(repo.Ctx, "for-each-ref", "--count="+strconv.Itoa(limit), "--format=%(refname:strip=2)", "--contains", commit.ID.String(), BranchPrefix).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand(repo.Ctx, "for-each-ref", + CmdArg("--count="+strconv.Itoa(limit)), + "--format=%(refname:strip=2)", "--contains"). + AddDynamicArguments(commit.ID.String(), BranchPrefix). + RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err } @@ -390,7 +407,7 @@ func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error) return branches, nil } - stdout, _, err := NewCommand(repo.Ctx, "branch", "--contains", commit.ID.String()).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand(repo.Ctx, "branch", "--contains").AddDynamicArguments(commit.ID.String()).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err } @@ -429,7 +446,7 @@ func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit { // IsCommitInBranch check if the commit is on the branch func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err error) { - stdout, _, err := NewCommand(repo.Ctx, "branch", "--contains", commitID, branch).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand(repo.Ctx, "branch", "--contains").AddDynamicArguments(commitID, branch).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { return false, err } diff --git a/modules/git/repo_commit_gogit.go b/modules/git/repo_commit_gogit.go index 9333b0d7b7..14fec3f9c6 100644 --- a/modules/git/repo_commit_gogit.go +++ b/modules/git/repo_commit_gogit.go @@ -49,7 +49,7 @@ func (repo *Repository) ConvertToSHA1(commitID string) (SHA1, error) { } } - actualCommitID, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify", commitID).RunStdString(&RunOpts{Dir: repo.Path}) + actualCommitID, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(commitID).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { if strings.Contains(err.Error(), "unknown revision or path") || strings.Contains(err.Error(), "fatal: Needed a single revision") { diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index efea307b37..13a7be778f 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -17,7 +17,7 @@ import ( // ResolveReference resolves a name to a reference func (repo *Repository) ResolveReference(name string) (string, error) { - stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--hash", name).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--hash").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { if strings.Contains(err.Error(), "not a valid ref") { return "", ErrNotExist{name, ""} @@ -50,19 +50,19 @@ func (repo *Repository) GetRefCommitID(name string) (string, error) { // SetReference sets the commit ID string of given reference (e.g. branch or tag). func (repo *Repository) SetReference(name, commitID string) error { - _, _, err := NewCommand(repo.Ctx, "update-ref", name, commitID).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand(repo.Ctx, "update-ref").AddDynamicArguments(name, commitID).RunStdString(&RunOpts{Dir: repo.Path}) return err } // RemoveReference removes the given reference (e.g. branch or tag). func (repo *Repository) RemoveReference(name string) error { - _, _, err := NewCommand(repo.Ctx, "update-ref", "--no-deref", "-d", name).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand(repo.Ctx, "update-ref", "--no-deref", "-d").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path}) return err } // IsCommitExist returns true if given commit exists in current repository. func (repo *Repository) IsCommitExist(name string) bool { - _, _, err := NewCommand(repo.Ctx, "cat-file", "-e", name).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand(repo.Ctx, "cat-file", "-e").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path}) return err == nil } diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index 589a7eae00..aad78370ff 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -40,13 +40,13 @@ func (repo *Repository) GetMergeBase(tmpRemote, base, head string) (string, stri if tmpRemote != "origin" { tmpBaseName := RemotePrefix + tmpRemote + "/tmp_" + base // Fetch commit into a temporary branch in order to be able to handle commits and tags - _, _, err := NewCommand(repo.Ctx, "fetch", "--no-tags", tmpRemote, "--", base+":"+tmpBaseName).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand(repo.Ctx, "fetch", "--no-tags").AddDynamicArguments(tmpRemote).AddDashesAndList(base + ":" + tmpBaseName).RunStdString(&RunOpts{Dir: repo.Path}) if err == nil { base = tmpBaseName } } - stdout, _, err := NewCommand(repo.Ctx, "merge-base", "--", base, head).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand(repo.Ctx, "merge-base").AddDashesAndList(base, head).RunStdString(&RunOpts{Dir: repo.Path}) return strings.TrimSpace(stdout), base, err } @@ -94,7 +94,7 @@ func (repo *Repository) GetCompareInfo(basePath, baseBranch, headBranch string, // We have a common base - therefore we know that ... should work if !fileOnly { var logs []byte - logs, _, err = NewCommand(repo.Ctx, "log", baseCommitID+separator+headBranch, prettyLogFormat).RunStdBytes(&RunOpts{Dir: repo.Path}) + logs, _, err = NewCommand(repo.Ctx, "log").AddDynamicArguments(baseCommitID + separator + headBranch).AddArguments(prettyLogFormat).RunStdBytes(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err } @@ -147,7 +147,7 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis separator = ".." } - if err := NewCommand(repo.Ctx, "diff", "-z", "--name-only", base+separator+head). + if err := NewCommand(repo.Ctx, "diff", "-z", "--name-only").AddDynamicArguments(base + separator + head). Run(&RunOpts{ Dir: repo.Path, Stdout: w, @@ -158,7 +158,7 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis // previously it would return the results of git diff -z --name-only base head so let's try that... w = &lineCountWriter{} stderr.Reset() - if err = NewCommand(repo.Ctx, "diff", "-z", "--name-only", base, head).Run(&RunOpts{ + if err = NewCommand(repo.Ctx, "diff", "-z", "--name-only").AddDynamicArguments(base, head).Run(&RunOpts{ Dir: repo.Path, Stdout: w, Stderr: stderr, @@ -173,20 +173,20 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis // GetDiffShortStat counts number of changed files, number of additions and deletions func (repo *Repository) GetDiffShortStat(base, head string) (numFiles, totalAdditions, totalDeletions int, err error) { - numFiles, totalAdditions, totalDeletions, err = GetDiffShortStat(repo.Ctx, repo.Path, base+"..."+head) + numFiles, totalAdditions, totalDeletions, err = GetDiffShortStat(repo.Ctx, repo.Path, CmdArgCheck(base+"..."+head)) if err != nil && strings.Contains(err.Error(), "no merge base") { - return GetDiffShortStat(repo.Ctx, repo.Path, base, head) + return GetDiffShortStat(repo.Ctx, repo.Path, CmdArgCheck(base), CmdArgCheck(head)) } return numFiles, totalAdditions, totalDeletions, err } // GetDiffShortStat counts number of changed files, number of additions and deletions -func GetDiffShortStat(ctx context.Context, repoPath string, args ...string) (numFiles, totalAdditions, totalDeletions int, err error) { +func GetDiffShortStat(ctx context.Context, repoPath string, args ...CmdArg) (numFiles, totalAdditions, totalDeletions int, err error) { // Now if we call: // $ git diff --shortstat 1ebb35b98889ff77299f24d82da426b434b0cca0...788b8b1440462d477f45b0088875 // we get: // " 9902 files changed, 2034198 insertions(+), 298800 deletions(-)\n" - args = append([]string{ + args = append([]CmdArg{ "diff", "--shortstat", }, args...) @@ -247,7 +247,7 @@ func (repo *Repository) GetDiffOrPatch(base, head string, w io.Writer, patch, bi // GetDiff generates and returns patch data between given revisions, optimized for human readability func (repo *Repository) GetDiff(base, head string, w io.Writer) error { - return NewCommand(repo.Ctx, "diff", "-p", base, head).Run(&RunOpts{ + return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base, head).Run(&RunOpts{ Dir: repo.Path, Stdout: w, }) @@ -255,7 +255,7 @@ func (repo *Repository) GetDiff(base, head string, w io.Writer) error { // GetDiffBinary generates and returns patch data between given revisions, including binary diffs. func (repo *Repository) GetDiffBinary(base, head string, w io.Writer) error { - return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram", base, head).Run(&RunOpts{ + return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base, head).Run(&RunOpts{ Dir: repo.Path, Stdout: w, }) @@ -264,14 +264,14 @@ func (repo *Repository) GetDiffBinary(base, head string, w io.Writer) error { // GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply` func (repo *Repository) GetPatch(base, head string, w io.Writer) error { stderr := new(bytes.Buffer) - err := NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout", base+"..."+head). + err := NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(base + "..." + head). Run(&RunOpts{ Dir: repo.Path, Stdout: w, Stderr: stderr, }) if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { - return NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout", base, head). + return NewCommand(repo.Ctx, "format-patch", "--binary", "--stdout").AddDynamicArguments(base, head). Run(&RunOpts{ Dir: repo.Path, Stdout: w, @@ -282,7 +282,7 @@ func (repo *Repository) GetPatch(base, head string, w io.Writer) error { // GetFilesChangedBetween returns a list of all files that have been changed between the given commits func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) { - stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", base+".."+head).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(base + ".." + head).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err } @@ -292,7 +292,7 @@ func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, err // GetDiffFromMergeBase generates and return patch data from merge base to head func (repo *Repository) GetDiffFromMergeBase(base, head string, w io.Writer) error { stderr := new(bytes.Buffer) - err := NewCommand(repo.Ctx, "diff", "-p", "--binary", base+"..."+head). + err := NewCommand(repo.Ctx, "diff", "-p", "--binary").AddDynamicArguments(base + "..." + head). Run(&RunOpts{ Dir: repo.Path, Stdout: w, diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index 50d82c77d7..5542883288 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -18,7 +18,7 @@ import ( // ReadTreeToIndex reads a treeish to the index func (repo *Repository) ReadTreeToIndex(treeish string, indexFilename ...string) error { if len(treeish) != 40 { - res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify", treeish).RunStdString(&RunOpts{Dir: repo.Path}) + res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(treeish).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { return err } @@ -38,7 +38,7 @@ func (repo *Repository) readTreeToIndex(id SHA1, indexFilename ...string) error if len(indexFilename) > 0 { env = append(os.Environ(), "GIT_INDEX_FILE="+indexFilename[0]) } - _, _, err := NewCommand(repo.Ctx, "read-tree", id.String()).RunStdString(&RunOpts{Dir: repo.Path, Env: env}) + _, _, err := NewCommand(repo.Ctx, "read-tree").AddDynamicArguments(id.String()).RunStdString(&RunOpts{Dir: repo.Path, Env: env}) if err != nil { return err } @@ -75,12 +75,7 @@ func (repo *Repository) EmptyIndex() error { // LsFiles checks if the given filenames are in the index func (repo *Repository) LsFiles(filenames ...string) ([]string, error) { - cmd := NewCommand(repo.Ctx, "ls-files", "-z", "--") - for _, arg := range filenames { - if arg != "" { - cmd.AddArguments(arg) - } - } + cmd := NewCommand(repo.Ctx, "ls-files", "-z").AddDashesAndList(filenames...) res, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err @@ -116,7 +111,7 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error { // AddObjectToIndex adds the provided object hash to the index at the provided filename func (repo *Repository) AddObjectToIndex(mode string, object SHA1, filename string) error { - cmd := NewCommand(repo.Ctx, "update-index", "--add", "--replace", "--cacheinfo", mode, object.String(), filename) + cmd := NewCommand(repo.Ctx, "update-index", "--add", "--replace", "--cacheinfo").AddDynamicArguments(mode, object.String(), filename) _, _, err := cmd.RunStdString(&RunOpts{Dir: repo.Path}) return err } diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go index cb44c58cec..002e2525e2 100644 --- a/modules/git/repo_stats.go +++ b/modules/git/repo_stats.go @@ -41,7 +41,7 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) since := fromTime.Format(time.RFC3339) - stdout, _, runErr := NewCommand(repo.Ctx, "rev-list", "--count", "--no-merges", "--branches=*", "--date=iso", fmt.Sprintf("--since='%s'", since)).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, runErr := NewCommand(repo.Ctx, "rev-list", "--count", "--no-merges", "--branches=*", "--date=iso", CmdArg(fmt.Sprintf("--since='%s'", since))).RunStdString(&RunOpts{Dir: repo.Path}) if runErr != nil { return nil, runErr } @@ -61,15 +61,15 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) _ = stdoutWriter.Close() }() - args := []string{"log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso", fmt.Sprintf("--since='%s'", since)} + gitCmd := NewCommand(repo.Ctx, "log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso", CmdArg(fmt.Sprintf("--since='%s'", since))) if len(branch) == 0 { - args = append(args, "--branches=*") + gitCmd.AddArguments("--branches=*") } else { - args = append(args, "--first-parent", branch) + gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch) } stderr := new(strings.Builder) - err = NewCommand(repo.Ctx, args...).Run(&RunOpts{ + err = gitCmd.Run(&RunOpts{ Env: []string{}, Dir: repo.Path, Stdout: stdoutWriter, diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index 8444e8d035..fd0de7b3a1 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -25,13 +25,13 @@ func IsTagExist(ctx context.Context, repoPath, name string) bool { // CreateTag create one tag in the repository func (repo *Repository) CreateTag(name, revision string) error { - _, _, err := NewCommand(repo.Ctx, "tag", "--", name, revision).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand(repo.Ctx, "tag").AddDashesAndList(name, revision).RunStdString(&RunOpts{Dir: repo.Path}) return err } // CreateAnnotatedTag create one annotated tag in the repository func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error { - _, _, err := NewCommand(repo.Ctx, "tag", "-a", "-m", message, "--", name, revision).RunStdString(&RunOpts{Dir: repo.Path}) + _, _, err := NewCommand(repo.Ctx, "tag", "-a", "-m").AddDynamicArguments(message).AddDashesAndList(name, revision).RunStdString(&RunOpts{Dir: repo.Path}) return err } @@ -64,7 +64,7 @@ func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { // GetTagID returns the object ID for a tag (annotated tags have both an object SHA AND a commit SHA) func (repo *Repository) GetTagID(name string) (string, error) { - stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--tags", "--", name).RunStdString(&RunOpts{Dir: repo.Path}) + stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--tags").AddDashesAndList(name).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { return "", err } @@ -122,7 +122,7 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { rc := &RunOpts{Dir: repo.Path, Stdout: stdoutWriter, Stderr: &stderr} go func() { - err := NewCommand(repo.Ctx, "for-each-ref", "--format", forEachRefFmt.Flag(), "--sort", "-*creatordate", "refs/tags").Run(rc) + err := NewCommand(repo.Ctx, "for-each-ref", CmdArg("--format="+forEachRefFmt.Flag()), "--sort", "-*creatordate", "refs/tags").Run(rc) if err != nil { _ = stdoutWriter.CloseWithError(ConcatenateError(err, stderr.String())) } else { diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go index 72c1c979dc..5d3aace52f 100644 --- a/modules/git/repo_tag_nogogit.go +++ b/modules/git/repo_tag_nogogit.go @@ -26,7 +26,7 @@ func (repo *Repository) IsTagExist(name string) bool { // GetTags returns all tags of the repository. // returning at most limit tags, or all if limit is 0. func (repo *Repository) GetTags(skip, limit int) (tags []string, err error) { - tags, _, err = callShowRef(repo.Ctx, repo.Path, TagPrefix, []string{TagPrefix, "--sort=-taggerdate"}, skip, limit) + tags, _, err = callShowRef(repo.Ctx, repo.Path, TagPrefix, []CmdArg{TagPrefix, "--sort=-taggerdate"}, skip, limit) return tags, err } diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index 2ea3f0187a..ba81bfc6db 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -35,10 +35,10 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt "GIT_COMMITTER_EMAIL="+committer.Email, "GIT_COMMITTER_DATE="+commitTimeStr, ) - cmd := NewCommand(repo.Ctx, "commit-tree", tree.ID.String()) + cmd := NewCommand(repo.Ctx, "commit-tree").AddDynamicArguments(tree.ID.String()) for _, parent := range opts.Parents { - cmd.AddArguments("-p", parent) + cmd.AddArguments("-p").AddDynamicArguments(parent) } messageBytes := new(bytes.Buffer) @@ -46,7 +46,7 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt _, _ = messageBytes.WriteString("\n") if opts.KeyID != "" || opts.AlwaysSign { - cmd.AddArguments(fmt.Sprintf("-S%s", opts.KeyID)) + cmd.AddArguments(CmdArg(fmt.Sprintf("-S%s", opts.KeyID))) } if opts.NoGPGSign { diff --git a/modules/git/repo_tree_gogit.go b/modules/git/repo_tree_gogit.go index eef09cddd6..e720164936 100644 --- a/modules/git/repo_tree_gogit.go +++ b/modules/git/repo_tree_gogit.go @@ -21,7 +21,7 @@ func (repo *Repository) getTree(id SHA1) (*Tree, error) { // GetTree find the tree object in the repository. func (repo *Repository) GetTree(idStr string) (*Tree, error) { if len(idStr) != 40 { - res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify", idStr).RunStdString(&RunOpts{Dir: repo.Path}) + res, _, err := NewCommand(repo.Ctx, "rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err } diff --git a/modules/git/sha1_test.go b/modules/git/sha1_test.go new file mode 100644 index 0000000000..c5c00f5445 --- /dev/null +++ b/modules/git/sha1_test.go @@ -0,0 +1,21 @@ +// 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 git + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsValidSHAPattern(t *testing.T) { + assert.True(t, IsValidSHAPattern("fee1")) + assert.True(t, IsValidSHAPattern("abc000")) + assert.True(t, IsValidSHAPattern("9023902390239023902390239023902390239023")) + assert.False(t, IsValidSHAPattern("90239023902390239023902390239023902390239023")) + assert.False(t, IsValidSHAPattern("abc")) + assert.False(t, IsValidSHAPattern("123g")) + assert.False(t, IsValidSHAPattern("some random text")) +} diff --git a/modules/git/tree.go b/modules/git/tree.go index a83336f3db..f5944dd29c 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -49,12 +49,9 @@ func (t *Tree) SubTree(rpath string) (*Tree, error) { // LsTree checks if the given filenames are in the tree func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error) { - cmd := NewCommand(repo.Ctx, "ls-tree", "-z", "--name-only", "--", ref) - for _, arg := range filenames { - if arg != "" { - cmd.AddArguments(arg) - } - } + cmd := NewCommand(repo.Ctx, "ls-tree", "-z", "--name-only"). + AddDashesAndList(append([]string{ref}, filenames...)...) + res, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) if err != nil { return nil, err diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index d61d19e4c2..5cbb5ffc94 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -80,7 +80,7 @@ func (t *Tree) ListEntries() (Entries, error) { } } - stdout, _, runErr := NewCommand(t.repo.Ctx, "ls-tree", "-l", t.ID.String()).RunStdBytes(&RunOpts{Dir: t.repo.Path}) + stdout, _, runErr := NewCommand(t.repo.Ctx, "ls-tree", "-l").AddDynamicArguments(t.ID.String()).RunStdBytes(&RunOpts{Dir: t.repo.Path}) if runErr != nil { if strings.Contains(runErr.Error(), "fatal: Not a valid object name") || strings.Contains(runErr.Error(), "fatal: not a tree object") { return nil, ErrNotExist{ @@ -101,13 +101,13 @@ func (t *Tree) ListEntries() (Entries, error) { // listEntriesRecursive returns all entries of current tree recursively including all subtrees // extraArgs could be "-l" to get the size, which is slower -func (t *Tree) listEntriesRecursive(extraArgs ...string) (Entries, error) { +func (t *Tree) listEntriesRecursive(extraArgs ...CmdArg) (Entries, error) { if t.entriesRecursiveParsed { return t.entriesRecursive, nil } - args := append([]string{"ls-tree", "-t", "-r"}, extraArgs...) - args = append(args, t.ID.String()) + args := append([]CmdArg{"ls-tree", "-t", "-r"}, extraArgs...) + args = append(args, CmdArg(t.ID.String())) stdout, _, runErr := NewCommand(t.repo.Ctx, args...).RunStdBytes(&RunOpts{Dir: t.repo.Path}) if runErr != nil { return nil, runErr diff --git a/modules/gitgraph/graph.go b/modules/gitgraph/graph.go index 271382525a..d6342c9280 100644 --- a/modules/gitgraph/graph.go +++ b/modules/gitgraph/graph.go @@ -24,35 +24,29 @@ func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bo page = 1 } - args := make([]string, 0, 12+len(branches)+len(files)) - - args = append(args, "--graph", "--date-order", "--decorate=full") + graphCmd := git.NewCommand(r.Ctx, "log", "--graph", "--date-order", "--decorate=full") if hidePRRefs { - args = append(args, "--exclude="+git.PullPrefix+"*") + graphCmd.AddArguments("--exclude=" + git.PullPrefix + "*") } if len(branches) == 0 { - args = append(args, "--all") + graphCmd.AddArguments("--all") } - args = append(args, + graphCmd.AddArguments( "-C", "-M", - fmt.Sprintf("-n %d", setting.UI.GraphMaxCommitNum*page), + git.CmdArg(fmt.Sprintf("-n %d", setting.UI.GraphMaxCommitNum*page)), "--date=iso", - fmt.Sprintf("--pretty=format:%s", format)) + git.CmdArg(fmt.Sprintf("--pretty=format:%s", format))) if len(branches) > 0 { - args = append(args, branches...) + graphCmd.AddDynamicArguments(branches...) } - args = append(args, "--") if len(files) > 0 { - args = append(args, files...) + graphCmd.AddDashesAndList(files...) } - - graphCmd := git.NewCommand(r.Ctx, "log") - graphCmd.AddArguments(args...) graph := NewGraph() stderr := new(strings.Builder) diff --git a/modules/indexer/code/bleve.go b/modules/indexer/code/bleve.go index 0b31f7119c..f1298b01ed 100644 --- a/modules/indexer/code/bleve.go +++ b/modules/indexer/code/bleve.go @@ -194,7 +194,7 @@ func (b *BleveIndexer) addUpdate(ctx context.Context, batchWriter git.WriteClose var err error if !update.Sized { var stdout string - stdout, _, err = git.NewCommand(ctx, "cat-file", "-s", update.BlobSha).RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) + stdout, _, err = git.NewCommand(ctx, "cat-file", "-s").AddDynamicArguments(update.BlobSha).RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) if err != nil { return err } diff --git a/modules/indexer/code/elastic_search.go b/modules/indexer/code/elastic_search.go index 7727bfacde..108a224675 100644 --- a/modules/indexer/code/elastic_search.go +++ b/modules/indexer/code/elastic_search.go @@ -223,7 +223,7 @@ func (b *ElasticSearchIndexer) addUpdate(ctx context.Context, batchWriter git.Wr var err error if !update.Sized { var stdout string - stdout, _, err = git.NewCommand(ctx, "cat-file", "-s", update.BlobSha).RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) + stdout, _, err = git.NewCommand(ctx, "cat-file", "-s").AddDynamicArguments(update.BlobSha).RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) if err != nil { return nil, err } diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go index 66d76377ad..774dcc8149 100644 --- a/modules/indexer/code/git.go +++ b/modules/indexer/code/git.go @@ -29,7 +29,7 @@ type repoChanges struct { } func getDefaultBranchSha(ctx context.Context, repo *repo_model.Repository) (string, error) { - stdout, _, err := git.NewCommand(ctx, "show-ref", "-s", git.BranchPrefix+repo.DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) + stdout, _, err := git.NewCommand(ctx, "show-ref", "-s").AddDynamicArguments(git.BranchPrefix + repo.DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) if err != nil { return "", err } @@ -92,7 +92,7 @@ func parseGitLsTreeOutput(stdout []byte) ([]fileUpdate, error) { // genesisChanges get changes to add repo to the indexer for the first time func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*repoChanges, error) { var changes repoChanges - stdout, _, runErr := git.NewCommand(ctx, "ls-tree", "--full-tree", "-l", "-r", revision).RunStdBytes(&git.RunOpts{Dir: repo.RepoPath()}) + stdout, _, runErr := git.NewCommand(ctx, "ls-tree", "--full-tree", "-l", "-r").AddDynamicArguments(revision).RunStdBytes(&git.RunOpts{Dir: repo.RepoPath()}) if runErr != nil { return nil, runErr } @@ -104,7 +104,7 @@ func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision s // nonGenesisChanges get changes since the previous indexer update func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*repoChanges, error) { - diffCmd := git.NewCommand(ctx, "diff", "--name-status", repo.CodeIndexerStatus.CommitSha, revision) + diffCmd := git.NewCommand(ctx, "diff", "--name-status").AddDynamicArguments(repo.CodeIndexerStatus.CommitSha, revision) stdout, _, runErr := diffCmd.RunStdString(&git.RunOpts{Dir: repo.RepoPath()}) if runErr != nil { // previous commit sha may have been removed by a force push, so @@ -169,8 +169,8 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio } } - cmd := git.NewCommand(ctx, "ls-tree", "--full-tree", "-l", revision, "--") - cmd.AddArguments(updatedFilenames...) + cmd := git.NewCommand(ctx, "ls-tree", "--full-tree", "-l").AddDynamicArguments(revision). + AddDashesAndList(updatedFilenames...) lsTreeStdout, _, err := cmd.RunStdBytes(&git.RunOpts{Dir: repo.RepoPath()}) if err != nil { return nil, err diff --git a/modules/markup/html.go b/modules/markup/html.go index a5606dbb51..ae00c3905f 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -603,8 +603,14 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { start = loc.End continue } - replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention")) - node = node.NextSibling.NextSibling + mentionedUsername := mention[1:] + + if processorHelper.IsUsernameMentionable != nil && processorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) { + replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mentionedUsername), mention, "mention")) + node = node.NextSibling.NextSibling + } else { + node = node.NextSibling + } start = 0 } } diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go index 5191d94cdd..c82d5e5e73 100644 --- a/modules/markup/markdown/ast.go +++ b/modules/markup/markdown/ast.go @@ -144,3 +144,39 @@ func IsIcon(node ast.Node) bool { _, ok := node.(*Icon) return ok } + +// ColorPreview is an inline for a color preview +type ColorPreview struct { + ast.BaseInline + Color []byte +} + +// Dump implements Node.Dump. +func (n *ColorPreview) Dump(source []byte, level int) { + m := map[string]string{} + m["Color"] = string(n.Color) + ast.DumpHelper(n, source, level, m, nil) +} + +// KindColorPreview is the NodeKind for ColorPreview +var KindColorPreview = ast.NewNodeKind("ColorPreview") + +// Kind implements Node.Kind. +func (n *ColorPreview) Kind() ast.NodeKind { + return KindColorPreview +} + +// NewColorPreview returns a new Span node. +func NewColorPreview(color []byte) *ColorPreview { + return &ColorPreview{ + BaseInline: ast.BaseInline{}, + Color: color, + } +} + +// IsColorPreview returns true if the given node implements the ColorPreview interface, +// otherwise false. +func IsColorPreview(node ast.Node) bool { + _, ok := node.(*ColorPreview) + return ok +} diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 8417019ddb..1a36681366 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/setting" giteautil "code.gitea.io/gitea/modules/util" + "github.com/microcosm-cc/bluemonday/css" "github.com/yuin/goldmark/ast" east "github.com/yuin/goldmark/extension/ast" "github.com/yuin/goldmark/parser" @@ -178,6 +179,11 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments) } } + case *ast.CodeSpan: + colorContent := n.Text(reader.Source()) + if css.ColorHandler(strings.ToLower(string(colorContent))) { + v.AppendChild(v, NewColorPreview(colorContent)) + } } return ast.WalkContinue, nil }) @@ -266,10 +272,43 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(KindDetails, r.renderDetails) reg.Register(KindSummary, r.renderSummary) reg.Register(KindIcon, r.renderIcon) + reg.Register(ast.KindCodeSpan, r.renderCodeSpan) reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem) reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox) } +// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements. +// See #21474 for reference +func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + if n.Attributes() != nil { + _, _ = w.WriteString("') + } else { + _, _ = w.WriteString("") + } + for c := n.FirstChild(); c != nil; c = c.NextSibling() { + switch v := c.(type) { + case *ast.Text: + segment := v.Segment + value := segment.Value(source) + if bytes.HasSuffix(value, []byte("\n")) { + r.Writer.RawWrite(w, value[:len(value)-1]) + r.Writer.RawWrite(w, []byte(" ")) + } else { + r.Writer.RawWrite(w, value) + } + case *ColorPreview: + _, _ = w.WriteString(fmt.Sprintf(``, string(v.Color))) + } + } + return ast.WalkSkipChildren, nil + } + _, _ = w.WriteString("") + return ast.WalkContinue, nil +} + func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.Document) diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 49ed3d75d6..fbb741d1cd 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -38,6 +38,11 @@ func TestMain(m *testing.M) { if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } + markup.Init(&markup.ProcessorHelper{ + IsUsernameMentionable: func(ctx context.Context, username string) bool { + return username == "r-lyeh" + }, + }) os.Exit(m.Run()) } @@ -429,6 +434,61 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) { assert.Equal(t, expected, res) } +func TestColorPreview(t *testing.T) { + const nl = "\n" + positiveTests := []struct { + testcase string + expected string + }{ + { // hex + "`#FF0000`", + `

#FF0000

` + nl, + }, + { // rgb + "`rgb(16, 32, 64)`", + `

rgb(16, 32, 64)

` + nl, + }, + { // short hex + "This is the color white `#000`", + `

This is the color white #000

` + nl, + }, + { // hsl + "HSL stands for hue, saturation, and lightness. An example: `hsl(0, 100%, 50%)`.", + `

HSL stands for hue, saturation, and lightness. An example: hsl(0, 100%, 50%).

` + nl, + }, + { // uppercase hsl + "HSL stands for hue, saturation, and lightness. An example: `HSL(0, 100%, 50%)`.", + `

HSL stands for hue, saturation, and lightness. An example: HSL(0, 100%, 50%).

` + nl, + }, + } + + for _, test := range positiveTests { + res, err := RenderString(&markup.RenderContext{}, test.testcase) + assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) + assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) + + } + + negativeTests := []string{ + // not a color code + "`FF0000`", + // inside a code block + "```javascript" + nl + `const red = "#FF0000";` + nl + "```", + // no backticks + "rgb(166, 32, 64)", + // typo + "`hsI(0, 100%, 50%)`", + // looks like a color but not really + "`hsl(40, 60, 80)`", + } + + for _, test := range negativeTests { + res, err := RenderString(&markup.RenderContext{}, test) + assert.NoError(t, err, "Unexpected error in testcase: %q", test) + assert.NotContains(t, res, ` 0 { CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 807a8a7892..ff7165c131 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -55,6 +55,9 @@ func createDefaultPolicy() *bluemonday.Policy { // For JS code copy and Mermaid loading state policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre") + // For color preview + policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span") + // For Chroma markdown plugin policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code") @@ -88,8 +91,8 @@ func createDefaultPolicy() *bluemonday.Policy { // Allow 'style' attribute on text elements. policy.AllowAttrs("style").OnElements("span", "p") - // Allow 'color' property for the style attribute on text elements. - policy.AllowStyles("color").OnElements("span", "p") + // Allow 'color' and 'background-color' properties for the style attribute on text elements. + policy.AllowStyles("color", "background-color").OnElements("span", "p") // Allow generally safe attributes generalSafeAttrs := []string{ diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index 0eb2099a20..630b565984 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -61,7 +61,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(doer *user_model.User, issue *i return } - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventPullRequestLabel, &api.PullRequestPayload{ + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventPullRequestLabel, &api.PullRequestPayload{ Action: api.HookIssueLabelCleared, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), @@ -69,7 +69,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(doer *user_model.User, issue *i Sender: convert.ToUser(doer, nil), }) } else { - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventIssueLabel, &api.IssuePayload{ + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventIssueLabel, &api.IssuePayload{ Action: api.HookIssueLabelCleared, Index: issue.Index, Issue: convert.ToAPIIssue(issue), @@ -87,7 +87,7 @@ func (m *webhookNotifier) NotifyForkRepository(doer *user_model.User, oldRepo, r mode, _ := access_model.AccessLevel(doer, repo) // forked webhook - if err := webhook_services.PrepareWebhooks(oldRepo, webhook.HookEventFork, &api.ForkPayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: oldRepo}, webhook.HookEventFork, &api.ForkPayload{ Forkee: convert.ToRepo(oldRepo, oldMode), Repo: convert.ToRepo(repo, mode), Sender: convert.ToUser(doer, nil), @@ -99,7 +99,7 @@ func (m *webhookNotifier) NotifyForkRepository(doer *user_model.User, oldRepo, r // Add to hook queue for created repo after session commit. if u.IsOrganization() { - if err := webhook_services.PrepareWebhooks(repo, webhook.HookEventRepository, &api.RepositoryPayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: repo}, webhook.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoCreated, Repository: convert.ToRepo(repo, perm.AccessModeOwner), Organization: convert.ToUser(u, nil), @@ -112,7 +112,7 @@ func (m *webhookNotifier) NotifyForkRepository(doer *user_model.User, oldRepo, r func (m *webhookNotifier) NotifyCreateRepository(doer, u *user_model.User, repo *repo_model.Repository) { // Add to hook queue for created repo after session commit. - if err := webhook_services.PrepareWebhooks(repo, webhook.HookEventRepository, &api.RepositoryPayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: repo}, webhook.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoCreated, Repository: convert.ToRepo(repo, perm.AccessModeOwner), Organization: convert.ToUser(u, nil), @@ -125,7 +125,7 @@ func (m *webhookNotifier) NotifyCreateRepository(doer, u *user_model.User, repo func (m *webhookNotifier) NotifyDeleteRepository(doer *user_model.User, repo *repo_model.Repository) { u := repo.MustOwner() - if err := webhook_services.PrepareWebhooks(repo, webhook.HookEventRepository, &api.RepositoryPayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: repo}, webhook.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoDeleted, Repository: convert.ToRepo(repo, perm.AccessModeOwner), Organization: convert.ToUser(u, nil), @@ -137,7 +137,7 @@ func (m *webhookNotifier) NotifyDeleteRepository(doer *user_model.User, repo *re func (m *webhookNotifier) NotifyMigrateRepository(doer, u *user_model.User, repo *repo_model.Repository) { // Add to hook queue for created repo after session commit. - if err := webhook_services.PrepareWebhooks(repo, webhook.HookEventRepository, &api.RepositoryPayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: repo}, webhook.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoCreated, Repository: convert.ToRepo(repo, perm.AccessModeOwner), Organization: convert.ToUser(u, nil), @@ -171,7 +171,7 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue apiPullRequest.Action = api.HookIssueAssigned } // Assignee comment triggers a webhook - if err := webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventPullRequestAssign, apiPullRequest); err != nil { + if err := webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventPullRequestAssign, apiPullRequest); err != nil { log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err) return } @@ -189,7 +189,7 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(doer *user_model.User, issue apiIssue.Action = api.HookIssueAssigned } // Assignee comment triggers a webhook - if err := webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventIssueAssign, apiIssue); err != nil { + if err := webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventIssueAssign, apiIssue); err != nil { log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err) return } @@ -208,7 +208,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *i return } issue.PullRequest.Issue = issue - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventPullRequest, &api.PullRequestPayload{ + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueEdited, Index: issue.Index, Changes: &api.ChangesPayload{ @@ -221,7 +221,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(doer *user_model.User, issue *i Sender: convert.ToUser(doer, nil), }) } else { - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventIssues, &api.IssuePayload{ + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventIssues, &api.IssuePayload{ Action: api.HookIssueEdited, Index: issue.Index, Changes: &api.ChangesPayload{ @@ -263,7 +263,7 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue * } else { apiPullRequest.Action = api.HookIssueReOpened } - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventPullRequest, apiPullRequest) + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventPullRequest, apiPullRequest) } else { apiIssue := &api.IssuePayload{ Index: issue.Index, @@ -276,7 +276,7 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(doer *user_model.User, issue * } else { apiIssue.Action = api.HookIssueReOpened } - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventIssues, apiIssue) + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventIssues, apiIssue) } if err != nil { log.Error("PrepareWebhooks [is_pull: %v, is_closed: %v]: %v", issue.IsPull, isClosed, err) @@ -294,7 +294,7 @@ func (m *webhookNotifier) NotifyNewIssue(issue *issues_model.Issue, mentions []* } mode, _ := access_model.AccessLevel(issue.Poster, issue.Repo) - if err := webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventIssues, &api.IssuePayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventIssues, &api.IssuePayload{ Action: api.HookIssueOpened, Index: issue.Index, Issue: convert.ToAPIIssue(issue), @@ -323,7 +323,7 @@ func (m *webhookNotifier) NotifyNewPullRequest(pull *issues_model.PullRequest, m } mode, _ := access_model.AccessLevel(pull.Issue.Poster, pull.Issue.Repo) - if err := webhook_services.PrepareWebhooks(pull.Issue.Repo, webhook.HookEventPullRequest, &api.PullRequestPayload{ + if err := webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: pull.Issue.Repo}, webhook.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueOpened, Index: pull.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pull, nil), @@ -342,7 +342,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(doer *user_model.User, issue var err error if issue.IsPull { issue.PullRequest.Issue = issue - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventPullRequest, &api.PullRequestPayload{ + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueEdited, Index: issue.Index, Changes: &api.ChangesPayload{ @@ -355,7 +355,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(doer *user_model.User, issue Sender: convert.ToUser(doer, nil), }) } else { - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventIssues, &api.IssuePayload{ + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventIssues, &api.IssuePayload{ Action: api.HookIssueEdited, Index: issue.Index, Changes: &api.ChangesPayload{ @@ -374,54 +374,41 @@ func (m *webhookNotifier) NotifyIssueChangeContent(doer *user_model.User, issue } func (m *webhookNotifier) NotifyUpdateComment(doer *user_model.User, c *issues_model.Comment, oldContent string) { - var err error - - if err = c.LoadPoster(); err != nil { + if err := c.LoadPoster(); err != nil { log.Error("LoadPoster: %v", err) return } - if err = c.LoadIssue(); err != nil { + if err := c.LoadIssue(); err != nil { log.Error("LoadIssue: %v", err) return } - if err = c.Issue.LoadAttributes(db.DefaultContext); err != nil { + if err := c.Issue.LoadAttributes(db.DefaultContext); err != nil { log.Error("LoadAttributes: %v", err) return } - mode, _ := access_model.AccessLevel(doer, c.Issue.Repo) + var eventType webhook.HookEventType if c.Issue.IsPull { - err = webhook_services.PrepareWebhooks(c.Issue.Repo, webhook.HookEventPullRequestComment, &api.IssueCommentPayload{ - Action: api.HookIssueCommentEdited, - Issue: convert.ToAPIIssue(c.Issue), - Comment: convert.ToComment(c), - Changes: &api.ChangesPayload{ - Body: &api.ChangesFromPayload{ - From: oldContent, - }, - }, - Repository: convert.ToRepo(c.Issue.Repo, mode), - Sender: convert.ToUser(doer, nil), - IsPull: true, - }) + eventType = webhook.HookEventPullRequestComment } else { - err = webhook_services.PrepareWebhooks(c.Issue.Repo, webhook.HookEventIssueComment, &api.IssueCommentPayload{ - Action: api.HookIssueCommentEdited, - Issue: convert.ToAPIIssue(c.Issue), - Comment: convert.ToComment(c), - Changes: &api.ChangesPayload{ - Body: &api.ChangesFromPayload{ - From: oldContent, - }, - }, - Repository: convert.ToRepo(c.Issue.Repo, mode), - Sender: convert.ToUser(doer, nil), - IsPull: false, - }) + eventType = webhook.HookEventIssueComment } - if err != nil { + mode, _ := access_model.AccessLevel(doer, c.Issue.Repo) + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: c.Issue.Repo}, eventType, &api.IssueCommentPayload{ + Action: api.HookIssueCommentEdited, + Issue: convert.ToAPIIssue(c.Issue), + Comment: convert.ToComment(c), + Changes: &api.ChangesPayload{ + Body: &api.ChangesFromPayload{ + From: oldContent, + }, + }, + Repository: convert.ToRepo(c.Issue.Repo, mode), + Sender: convert.ToUser(doer, nil), + IsPull: c.Issue.IsPull, + }); err != nil { log.Error("PrepareWebhooks [comment_id: %d]: %v", c.ID, err) } } @@ -429,30 +416,22 @@ func (m *webhookNotifier) NotifyUpdateComment(doer *user_model.User, c *issues_m func (m *webhookNotifier) NotifyCreateIssueComment(doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { - mode, _ := access_model.AccessLevel(doer, repo) - - var err error + var eventType webhook.HookEventType if issue.IsPull { - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventPullRequestComment, &api.IssueCommentPayload{ - Action: api.HookIssueCommentCreated, - Issue: convert.ToAPIIssue(issue), - Comment: convert.ToComment(comment), - Repository: convert.ToRepo(repo, mode), - Sender: convert.ToUser(doer, nil), - IsPull: true, - }) + eventType = webhook.HookEventPullRequestComment } else { - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventIssueComment, &api.IssueCommentPayload{ - Action: api.HookIssueCommentCreated, - Issue: convert.ToAPIIssue(issue), - Comment: convert.ToComment(comment), - Repository: convert.ToRepo(repo, mode), - Sender: convert.ToUser(doer, nil), - IsPull: false, - }) + eventType = webhook.HookEventIssueComment } - if err != nil { + mode, _ := access_model.AccessLevel(doer, repo) + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: issue.Repo}, eventType, &api.IssueCommentPayload{ + Action: api.HookIssueCommentCreated, + Issue: convert.ToAPIIssue(issue), + Comment: convert.ToComment(comment), + Repository: convert.ToRepo(repo, mode), + Sender: convert.ToUser(doer, nil), + IsPull: issue.IsPull, + }); err != nil { log.Error("PrepareWebhooks [comment_id: %d]: %v", comment.ID, err) } } @@ -474,36 +453,29 @@ func (m *webhookNotifier) NotifyDeleteComment(doer *user_model.User, comment *is return } - mode, _ := access_model.AccessLevel(doer, comment.Issue.Repo) - + var eventType webhook.HookEventType if comment.Issue.IsPull { - err = webhook_services.PrepareWebhooks(comment.Issue.Repo, webhook.HookEventPullRequestComment, &api.IssueCommentPayload{ - Action: api.HookIssueCommentDeleted, - Issue: convert.ToAPIIssue(comment.Issue), - Comment: convert.ToComment(comment), - Repository: convert.ToRepo(comment.Issue.Repo, mode), - Sender: convert.ToUser(doer, nil), - IsPull: true, - }) + eventType = webhook.HookEventPullRequestComment } else { - err = webhook_services.PrepareWebhooks(comment.Issue.Repo, webhook.HookEventIssueComment, &api.IssueCommentPayload{ - Action: api.HookIssueCommentDeleted, - Issue: convert.ToAPIIssue(comment.Issue), - Comment: convert.ToComment(comment), - Repository: convert.ToRepo(comment.Issue.Repo, mode), - Sender: convert.ToUser(doer, nil), - IsPull: false, - }) + eventType = webhook.HookEventIssueComment } - if err != nil { + mode, _ := access_model.AccessLevel(doer, comment.Issue.Repo) + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: comment.Issue.Repo}, eventType, &api.IssueCommentPayload{ + Action: api.HookIssueCommentDeleted, + Issue: convert.ToAPIIssue(comment.Issue), + Comment: convert.ToComment(comment), + Repository: convert.ToRepo(comment.Issue.Repo, mode), + Sender: convert.ToUser(doer, nil), + IsPull: comment.Issue.IsPull, + }); err != nil { log.Error("PrepareWebhooks [comment_id: %d]: %v", comment.ID, err) } } func (m *webhookNotifier) NotifyNewWikiPage(doer *user_model.User, repo *repo_model.Repository, page, comment string) { // Add to hook queue for created wiki page. - if err := webhook_services.PrepareWebhooks(repo, webhook.HookEventWiki, &api.WikiPayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: repo}, webhook.HookEventWiki, &api.WikiPayload{ Action: api.HookWikiCreated, Repository: convert.ToRepo(repo, perm.AccessModeOwner), Sender: convert.ToUser(doer, nil), @@ -516,7 +488,7 @@ func (m *webhookNotifier) NotifyNewWikiPage(doer *user_model.User, repo *repo_mo func (m *webhookNotifier) NotifyEditWikiPage(doer *user_model.User, repo *repo_model.Repository, page, comment string) { // Add to hook queue for edit wiki page. - if err := webhook_services.PrepareWebhooks(repo, webhook.HookEventWiki, &api.WikiPayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: repo}, webhook.HookEventWiki, &api.WikiPayload{ Action: api.HookWikiEdited, Repository: convert.ToRepo(repo, perm.AccessModeOwner), Sender: convert.ToUser(doer, nil), @@ -529,7 +501,7 @@ func (m *webhookNotifier) NotifyEditWikiPage(doer *user_model.User, repo *repo_m func (m *webhookNotifier) NotifyDeleteWikiPage(doer *user_model.User, repo *repo_model.Repository, page string) { // Add to hook queue for edit wiki page. - if err := webhook_services.PrepareWebhooks(repo, webhook.HookEventWiki, &api.WikiPayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: repo}, webhook.HookEventWiki, &api.WikiPayload{ Action: api.HookWikiDeleted, Repository: convert.ToRepo(repo, perm.AccessModeOwner), Sender: convert.ToUser(doer, nil), @@ -567,7 +539,7 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue * log.Error("LoadIssue: %v", err) return } - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventPullRequestLabel, &api.PullRequestPayload{ + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventPullRequestLabel, &api.PullRequestPayload{ Action: api.HookIssueLabelUpdated, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), @@ -575,7 +547,7 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(doer *user_model.User, issue * Sender: convert.ToUser(doer, nil), }) } else { - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventIssueLabel, &api.IssuePayload{ + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventIssueLabel, &api.IssuePayload{ Action: api.HookIssueLabelUpdated, Index: issue.Index, Issue: convert.ToAPIIssue(issue), @@ -612,7 +584,7 @@ func (m *webhookNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issu log.Error("LoadIssue: %v", err) return } - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventPullRequestMilestone, &api.PullRequestPayload{ + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventPullRequestMilestone, &api.PullRequestPayload{ Action: hookAction, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), @@ -620,7 +592,7 @@ func (m *webhookNotifier) NotifyIssueChangeMilestone(doer *user_model.User, issu Sender: convert.ToUser(doer, nil), }) } else { - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventIssueMilestone, &api.IssuePayload{ + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventIssueMilestone, &api.IssuePayload{ Action: hookAction, Index: issue.Index, Issue: convert.ToAPIIssue(issue), @@ -644,16 +616,17 @@ func (m *webhookNotifier) NotifyPushCommits(pusher *user_model.User, repo *repo_ return } - if err := webhook_services.PrepareWebhooks(repo, webhook.HookEventPush, &api.PushPayload{ - Ref: opts.RefFullName, - Before: opts.OldCommitID, - After: opts.NewCommitID, - CompareURL: setting.AppURL + commits.CompareURL, - Commits: apiCommits, - HeadCommit: apiHeadCommit, - Repo: convert.ToRepo(repo, perm.AccessModeOwner), - Pusher: apiPusher, - Sender: apiPusher, + if err := webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: repo}, webhook.HookEventPush, &api.PushPayload{ + Ref: opts.RefFullName, + Before: opts.OldCommitID, + After: opts.NewCommitID, + CompareURL: setting.AppURL + commits.CompareURL, + Commits: apiCommits, + TotalCommits: commits.Len, + HeadCommit: apiHeadCommit, + Repo: convert.ToRepo(repo, perm.AccessModeOwner), + Pusher: apiPusher, + Sender: apiPusher, }); err != nil { log.Error("PrepareWebhooks: %v", err) } @@ -694,7 +667,7 @@ func (*webhookNotifier) NotifyMergePullRequest(pr *issues_model.PullRequest, doe Action: api.HookIssueClosed, } - err = webhook_services.PrepareWebhooks(pr.Issue.Repo, webhook.HookEventPullRequest, apiPullRequest) + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: pr.Issue.Repo}, webhook.HookEventPullRequest, apiPullRequest) if err != nil { log.Error("PrepareWebhooks: %v", err) } @@ -716,7 +689,7 @@ func (m *webhookNotifier) NotifyPullRequestChangeTargetBranch(doer *user_model.U } issue.PullRequest.Issue = issue mode, _ := access_model.AccessLevel(issue.Poster, issue.Repo) - err = webhook_services.PrepareWebhooks(issue.Repo, webhook.HookEventPullRequest, &api.PullRequestPayload{ + err = webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: issue.Repo}, webhook.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueEdited, Index: issue.Index, Changes: &api.ChangesPayload{ @@ -763,7 +736,7 @@ func (m *webhookNotifier) NotifyPullRequestReview(pr *issues_model.PullRequest, log.Error("models.AccessLevel: %v", err) return } - if err := webhook_services.PrepareWebhooks(review.Issue.Repo, reviewHookType, &api.PullRequestPayload{ + if err := webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: review.Issue.Repo}, reviewHookType, &api.PullRequestPayload{ Action: api.HookIssueReviewed, Index: review.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), @@ -783,7 +756,7 @@ func (m *webhookNotifier) NotifyCreateRef(pusher *user_model.User, repo *repo_mo apiRepo := convert.ToRepo(repo, perm.AccessModeNone) refName := git.RefEndName(refFullName) - if err := webhook_services.PrepareWebhooks(repo, webhook.HookEventCreate, &api.CreatePayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: repo}, webhook.HookEventCreate, &api.CreatePayload{ Ref: refName, Sha: refID, RefType: refType, @@ -807,7 +780,7 @@ func (m *webhookNotifier) NotifyPullRequestSynchronized(doer *user_model.User, p return } - if err := webhook_services.PrepareWebhooks(pr.Issue.Repo, webhook.HookEventPullRequestSync, &api.PullRequestPayload{ + if err := webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: pr.Issue.Repo}, webhook.HookEventPullRequestSync, &api.PullRequestPayload{ Action: api.HookIssueSynchronized, Index: pr.Issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), @@ -823,7 +796,7 @@ func (m *webhookNotifier) NotifyDeleteRef(pusher *user_model.User, repo *repo_mo apiRepo := convert.ToRepo(repo, perm.AccessModeNone) refName := git.RefEndName(refFullName) - if err := webhook_services.PrepareWebhooks(repo, webhook.HookEventDelete, &api.DeletePayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: repo}, webhook.HookEventDelete, &api.DeletePayload{ Ref: refName, RefType: refType, PusherType: api.PusherTypeUser, @@ -841,7 +814,7 @@ func sendReleaseHook(doer *user_model.User, rel *repo_model.Release, action api. } mode, _ := access_model.AccessLevel(doer, rel.Repo) - if err := webhook_services.PrepareWebhooks(rel.Repo, webhook.HookEventRelease, &api.ReleasePayload{ + if err := webhook_services.PrepareWebhooks(db.DefaultContext, webhook_services.EventSource{Repository: rel.Repo}, webhook.HookEventRelease, &api.ReleasePayload{ Action: action, Release: convert.ToRelease(rel), Repository: convert.ToRepo(rel.Repo, mode), @@ -874,16 +847,17 @@ func (m *webhookNotifier) NotifySyncPushCommits(pusher *user_model.User, repo *r return } - if err := webhook_services.PrepareWebhooks(repo, webhook.HookEventPush, &api.PushPayload{ - Ref: opts.RefFullName, - Before: opts.OldCommitID, - After: opts.NewCommitID, - CompareURL: setting.AppURL + commits.CompareURL, - Commits: apiCommits, - HeadCommit: apiHeadCommit, - Repo: convert.ToRepo(repo, perm.AccessModeOwner), - Pusher: apiPusher, - Sender: apiPusher, + if err := webhook_services.PrepareWebhooks(ctx, webhook_services.EventSource{Repository: repo}, webhook.HookEventPush, &api.PushPayload{ + Ref: opts.RefFullName, + Before: opts.OldCommitID, + After: opts.NewCommitID, + CompareURL: setting.AppURL + commits.CompareURL, + Commits: apiCommits, + TotalCommits: commits.Len, + HeadCommit: apiHeadCommit, + Repo: convert.ToRepo(repo, perm.AccessModeOwner), + Pusher: apiPusher, + Sender: apiPusher, }); err != nil { log.Error("PrepareWebhooks: %v", err) } @@ -906,9 +880,9 @@ func (m *webhookNotifier) NotifyPackageDelete(doer *user_model.User, pd *package } func notifyPackage(sender *user_model.User, pd *packages_model.PackageDescriptor, action api.HookPackageAction) { - if pd.Repository == nil { - // TODO https://github.com/go-gitea/gitea/pull/17940 - return + source := webhook_services.EventSource{ + Repository: pd.Repository, + Owner: pd.Owner, } ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("webhook.notifyPackage Package: %s[%d]", pd.Package.Name, pd.Package.ID)) @@ -920,7 +894,7 @@ func notifyPackage(sender *user_model.User, pd *packages_model.PackageDescriptor return } - if err := webhook_services.PrepareWebhooks(pd.Repository, webhook.HookEventPackage, &api.PackagePayload{ + if err := webhook_services.PrepareWebhooks(ctx, source, webhook.HookEventPackage, &api.PackagePayload{ Action: action, Package: apiPackage, Sender: convert.ToUser(sender, nil), diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 797bff45ac..2b555e47e9 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -55,12 +55,13 @@ type Package struct { // Metadata represents the metadata of a Nuget package type Metadata struct { - Description string `json:"description,omitempty"` - ReleaseNotes string `json:"release_notes,omitempty"` - Authors string `json:"authors,omitempty"` - ProjectURL string `json:"project_url,omitempty"` - RepositoryURL string `json:"repository_url,omitempty"` - Dependencies map[string][]Dependency `json:"dependencies,omitempty"` + Description string `json:"description,omitempty"` + ReleaseNotes string `json:"release_notes,omitempty"` + Authors string `json:"authors,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + RequireLicenseAcceptance bool `json:"require_license_acceptance"` + Dependencies map[string][]Dependency `json:"dependencies,omitempty"` } // Dependency represents a dependency of a Nuget package @@ -155,12 +156,13 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) { } m := &Metadata{ - Description: p.Metadata.Description, - ReleaseNotes: p.Metadata.ReleaseNotes, - Authors: p.Metadata.Authors, - ProjectURL: p.Metadata.ProjectURL, - RepositoryURL: p.Metadata.Repository.URL, - Dependencies: make(map[string][]Dependency), + Description: p.Metadata.Description, + ReleaseNotes: p.Metadata.ReleaseNotes, + Authors: p.Metadata.Authors, + ProjectURL: p.Metadata.ProjectURL, + RepositoryURL: p.Metadata.Repository.URL, + RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance, + Dependencies: make(map[string][]Dependency), } for _, group := range p.Metadata.Dependencies.Group { diff --git a/modules/paginator/paginator.go b/modules/paginator/paginator.go index 873cfe49d4..342ee8929c 100644 --- a/modules/paginator/paginator.go +++ b/modules/paginator/paginator.go @@ -1,5 +1,5 @@ // Copyright 2022 The Gitea Authors. -// Copyright 2015 Unknwon. Licensed under the Apache License, Version 2.0 +// Copyright 2015 https://github.com/unknwon. Licensed under the Apache License, Version 2.0 package paginator diff --git a/modules/paginator/paginator_test.go b/modules/paginator/paginator_test.go index ce7b7275e1..404f76f6c4 100644 --- a/modules/paginator/paginator_test.go +++ b/modules/paginator/paginator_test.go @@ -1,5 +1,5 @@ // Copyright 2022 The Gitea Authors. -// Copyright 2015 Unknwon. Licensed under the Apache License, Version 2.0 +// Copyright 2015 https://github.com/unknwon. Licensed under the Apache License, Version 2.0 package paginator diff --git a/modules/references/references_test.go b/modules/references/references_test.go index adf86a3c6c..507adadb1f 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -309,7 +309,7 @@ func TestRegExp_mentionPattern(t *testing.T) { pat string exp string }{ - {"@Unknwon", "@Unknwon"}, + {"@User", "@User"}, {"@ANT_123", "@ANT_123"}, {"@xxx-DiN0-z-A..uru..s-xxx", "@xxx-DiN0-z-A..uru..s-xxx"}, {" @lol ", "@lol"}, diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go index c62e324b66..7bd741d0c8 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -11,8 +11,10 @@ import ( "time" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" ) @@ -100,6 +102,14 @@ func TestPushCommits_ToAPIPayloadCommits(t *testing.T) { assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified) } +func enableGravatar(t *testing.T) { + err := system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, "false") + assert.NoError(t, err) + setting.GravatarSource = "https://secure.gravatar.com/avatar" + err = system_model.Init() + assert.NoError(t, err) +} + func TestPushCommits_AvatarLink(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) @@ -123,6 +133,8 @@ func TestPushCommits_AvatarLink(t *testing.T) { }, } + enableGravatar(t) + assert.Equal(t, "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s=84", pushCommits.AvatarLink("user2@example.com")) diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 4d76d33993..a69dd12a78 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -211,7 +211,7 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r } repoPath := repo.RepoPath() - if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin", repoPath). + if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repoPath). SetDescription(fmt.Sprintf("generateRepoCommit (git remote add): %s to %s", templateRepoPath, tmpDir)). RunStdString(&git.RunOpts{Dir: tmpDir, Env: env}); err != nil { log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err) diff --git a/modules/repository/init.go b/modules/repository/init.go index 37ed0748b4..473729a599 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -228,7 +228,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, ) // Clone to temporary path and do the init commit. - if stdout, _, err := git.NewCommand(ctx, "clone", repoPath, tmpDir). + if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir). SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)). RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil { log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err) @@ -317,14 +317,14 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi return fmt.Errorf("git add --all: %v", err) } - args := []string{ - "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), + cmd := git.NewCommand(ctx, + "commit", git.CmdArg(fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email)), "-m", "Initial commit", - } + ) sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u) if sign { - args = append(args, "-S"+keyID) + cmd.AddArguments(git.CmdArg("-S" + keyID)) if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { // need to set the committer to the KeyID owner @@ -332,7 +332,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi committerEmail = signer.Email } } else { - args = append(args, "--no-gpg-sign") + cmd.AddArguments("--no-gpg-sign") } env = append(env, @@ -340,10 +340,10 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi "GIT_COMMITTER_EMAIL="+committerEmail, ) - if stdout, _, err := git.NewCommand(ctx, args...). + if stdout, _, err := cmd. SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)). RunStdString(&git.RunOpts{Dir: tmpPath, Env: env}); err != nil { - log.Error("Failed to commit: %v: Stdout: %s\nError: %v", args, stdout, err) + log.Error("Failed to commit: %v: Stdout: %s\nError: %v", cmd.String(), stdout, err) return fmt.Errorf("git commit: %v", err) } @@ -351,7 +351,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi defaultBranch = setting.Repository.DefaultBranch } - if stdout, _, err := git.NewCommand(ctx, "push", "origin", "HEAD:"+defaultBranch). + if stdout, _, err := git.NewCommand(ctx, "push", "origin").AddDynamicArguments("HEAD:" + defaultBranch). SetDescription(fmt.Sprintf("initRepoCommit (git push): %s", tmpPath)). RunStdString(&git.RunOpts{Dir: tmpPath, Env: InternalPushingEnvironment(u, repo)}); err != nil { log.Error("Failed to push back to HEAD: Stdout: %s\nError: %v", stdout, err) diff --git a/modules/repository/push.go b/modules/repository/push.go index e1dbd5d2fc..4e4b4000b1 100644 --- a/modules/repository/push.go +++ b/modules/repository/push.go @@ -104,7 +104,7 @@ func IsForcePush(ctx context.Context, opts *PushUpdateOptions) (bool, error) { return false, nil } - output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1", opts.OldCommitID, "^"+opts.NewCommitID). + output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(opts.OldCommitID, "^"+opts.NewCommitID). RunStdString(&git.RunOpts{Dir: repo_model.RepoPath(opts.RepoUserName, opts.RepoName)}) if err != nil { return false, err diff --git a/modules/setting/picture.go b/modules/setting/picture.go index a6d3447dcc..af9041ade3 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -4,14 +4,6 @@ package setting -import ( - "net/url" - - "code.gitea.io/gitea/modules/log" - - "strk.kbt.io/projects/go/libravatar" -) - // settings var ( // Picture settings @@ -30,10 +22,8 @@ var ( } GravatarSource string - GravatarSourceURL *url.URL - DisableGravatar bool - EnableFederatedAvatar bool - LibravatarService *libravatar.Libravatar + DisableGravatar bool // Depreciated: migrated to database + EnableFederatedAvatar bool // Depreciated: migrated to database RepoAvatar = struct { Storage @@ -69,38 +59,30 @@ func newPictureService() { default: GravatarSource = source } - DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool() - EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(!InstallLock) - if OfflineMode { - DisableGravatar = true - EnableFederatedAvatar = false - } - if DisableGravatar { - EnableFederatedAvatar = false - } - if EnableFederatedAvatar || !DisableGravatar { - var err error - GravatarSourceURL, err = url.Parse(GravatarSource) - if err != nil { - log.Fatal("Failed to parse Gravatar URL(%s): %v", - GravatarSource, err) - } - } - if EnableFederatedAvatar { - LibravatarService = libravatar.New() - if GravatarSourceURL.Scheme == "https" { - LibravatarService.SetUseHTTPS(true) - LibravatarService.SetSecureFallbackHost(GravatarSourceURL.Host) - } else { - LibravatarService.SetUseHTTPS(false) - LibravatarService.SetFallbackHost(GravatarSourceURL.Host) - } - } + DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool(GetDefaultDisableGravatar()) + deprecatedSettingDB("", "DISABLE_GRAVATAR") + EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(GetDefaultEnableFederatedAvatar(DisableGravatar)) + deprecatedSettingDB("", "ENABLE_FEDERATED_AVATAR") newRepoAvatarService() } +func GetDefaultDisableGravatar() bool { + return !OfflineMode +} + +func GetDefaultEnableFederatedAvatar(disableGravatar bool) bool { + v := !InstallLock + if OfflineMode { + v = false + } + if disableGravatar { + v = false + } + return v +} + func newRepoAvatarService() { sec := Cfg.Section("picture") diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 007e3ef61f..f93be2fbd1 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -606,6 +606,13 @@ func deprecatedSetting(oldSection, oldKey, newSection, newKey string) { } } +// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini +func deprecatedSettingDB(oldSection, oldKey string) { + if Cfg.Section(oldSection).HasKey(oldKey) { + log.Error("Deprecated `[%s]` `%s` present which has been copied to database table sys_setting", oldSection, oldKey) + } +} + // loadFromConf initializes configuration context. // NOTE: do not print any log except error. func loadFromConf(allowEmpty bool, extraConfig string) { diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 132b24b856..8321a15a8f 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -267,15 +267,16 @@ func (p *ReleasePayload) JSONPayload() ([]byte, error) { // PushPayload represents a payload information of push event. type PushPayload struct { - Ref string `json:"ref"` - Before string `json:"before"` - After string `json:"after"` - CompareURL string `json:"compare_url"` - Commits []*PayloadCommit `json:"commits"` - HeadCommit *PayloadCommit `json:"head_commit"` - Repo *Repository `json:"repository"` - Pusher *User `json:"pusher"` - Sender *User `json:"sender"` + Ref string `json:"ref"` + Before string `json:"before"` + After string `json:"after"` + CompareURL string `json:"compare_url"` + Commits []*PayloadCommit `json:"commits"` + TotalCommits int `json:"total_commits"` + HeadCommit *PayloadCommit `json:"head_commit"` + Repo *Repository `json:"repository"` + Pusher *User `json:"pusher"` + Sender *User `json:"sender"` } // JSONPayload FIXME diff --git a/modules/structs/user_app.go b/modules/structs/user_app.go index 44df5a6a49..4cfa5538c8 100644 --- a/modules/structs/user_app.go +++ b/modules/structs/user_app.go @@ -30,19 +30,21 @@ type CreateAccessTokenOption struct { // CreateOAuth2ApplicationOptions holds options to create an oauth2 application type CreateOAuth2ApplicationOptions struct { - Name string `json:"name" binding:"Required"` - RedirectURIs []string `json:"redirect_uris" binding:"Required"` + Name string `json:"name" binding:"Required"` + ConfidentialClient bool `json:"confidential_client"` + RedirectURIs []string `json:"redirect_uris" binding:"Required"` } // OAuth2Application represents an OAuth2 application. // swagger:response OAuth2Application type OAuth2Application struct { - ID int64 `json:"id"` - Name string `json:"name"` - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - RedirectURIs []string `json:"redirect_uris"` - Created time.Time `json:"created"` + ID int64 `json:"id"` + Name string `json:"name"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + ConfidentialClient bool `json:"confidential_client"` + RedirectURIs []string `json:"redirect_uris"` + Created time.Time `json:"created"` } // OAuth2ApplicationList represents a list of OAuth2 applications. diff --git a/modules/appstate/appstate.go b/modules/system/appstate.go similarity index 97% rename from modules/appstate/appstate.go rename to modules/system/appstate.go index f65f5367e2..deee8cd029 100644 --- a/modules/appstate/appstate.go +++ b/modules/system/appstate.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package appstate +package system // StateStore is the interface to get/set app state items type StateStore interface { diff --git a/modules/appstate/appstate_test.go b/modules/system/appstate_test.go similarity index 98% rename from modules/appstate/appstate_test.go rename to modules/system/appstate_test.go index e4a0d72850..fb0c2aaf9f 100644 --- a/modules/appstate/appstate_test.go +++ b/modules/system/appstate_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package appstate +package system import ( "path/filepath" diff --git a/modules/appstate/db.go b/modules/system/db.go similarity index 77% rename from modules/appstate/db.go rename to modules/system/db.go index 2538d1b5c8..b1c283c488 100644 --- a/modules/appstate/db.go +++ b/modules/system/db.go @@ -2,10 +2,10 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package appstate +package system import ( - "code.gitea.io/gitea/models/appstate" + "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/json" "github.com/yuin/goldmark/util" @@ -16,7 +16,7 @@ type DBStore struct{} // Get reads the state item func (f *DBStore) Get(item StateItem) error { - content, err := appstate.GetAppStateContent(item.Name()) + content, err := system.GetAppStateContent(item.Name()) if err != nil { return err } @@ -32,5 +32,5 @@ func (f *DBStore) Set(item StateItem) error { if err != nil { return err } - return appstate.SaveAppStateContent(item.Name(), util.BytesToReadOnlyString(b)) + return system.SaveAppStateContent(item.Name(), util.BytesToReadOnlyString(b)) } diff --git a/modules/appstate/item_runtime.go b/modules/system/item_runtime.go similarity index 96% rename from modules/appstate/item_runtime.go rename to modules/system/item_runtime.go index 7fdc53f642..ef758a5675 100644 --- a/modules/appstate/item_runtime.go +++ b/modules/system/item_runtime.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package appstate +package system // RuntimeState contains app state for runtime, and we can save remote version for update checker here in future type RuntimeState struct { diff --git a/modules/system/setting.go b/modules/system/setting.go new file mode 100644 index 0000000000..aebf24a501 --- /dev/null +++ b/modules/system/setting.go @@ -0,0 +1,46 @@ +// 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 system + +import ( + "strconv" + + "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/modules/cache" +) + +func genKey(key string) string { + return "system.setting." + key +} + +// GetSetting returns the setting value via the key +func GetSetting(key string) (string, error) { + return cache.GetString(genKey(key), func() (string, error) { + res, err := system.GetSetting(key) + if err != nil { + return "", err + } + return res.SettingValue, nil + }) +} + +// GetSettingBool return bool value of setting, +// none existing keys and errors are ignored and result in false +func GetSettingBool(key string) bool { + s, _ := GetSetting(key) + b, _ := strconv.ParseBool(s) + return b +} + +// SetSetting sets the setting value +func SetSetting(key, value string, version int) error { + cache.Remove(genKey(key)) + + return system.SetSetting(&system.Setting{ + SettingKey: key, + SettingValue: value, + Version: version, + }) +} diff --git a/modules/system/user_setting.go b/modules/system/user_setting.go new file mode 100644 index 0000000000..eaf146c08d --- /dev/null +++ b/modules/system/user_setting.go @@ -0,0 +1,34 @@ +// 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 system + +import ( + "fmt" + + "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" +) + +func genUserKey(userID int64, key string) string { + return fmt.Sprintf("user_%d.setting.%s", userID, key) +} + +// GetUserSetting returns the user setting value via the key +func GetUserSetting(userID int64, key string) (string, error) { + return cache.GetString(genUserKey(userID, key), func() (string, error) { + res, err := user.GetSetting(userID, key) + if err != nil { + return "", err + } + return res.SettingValue, nil + }) +} + +// SetUserSetting sets the user setting value +func SetUserSetting(userID int64, key, value string) error { + cache.Remove(genUserKey(userID, key)) + + return user.SetUserSetting(userID, key, value) +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index a8e4075248..a723291440 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -29,6 +29,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/emoji" @@ -41,6 +42,7 @@ import ( "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" + system_module "code.gitea.io/gitea/modules/system" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/gitdiff" @@ -85,7 +87,7 @@ func NewFuncMap() []template.FuncMap { return setting.AssetVersion }, "DisableGravatar": func() bool { - return setting.DisableGravatar + return system_module.GetSettingBool(system_model.KeyPictureDisableGravatar) }, "DefaultShowFullName": func() bool { return setting.UI.DefaultShowFullName @@ -161,6 +163,7 @@ func NewFuncMap() []template.FuncMap { "RenderCommitMessageLink": RenderCommitMessageLink, "RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject, "RenderCommitBody": RenderCommitBody, + "RenderCodeBlock": RenderCodeBlock, "RenderIssueTitle": RenderIssueTitle, "RenderEmoji": RenderEmoji, "RenderEmojiPlain": emoji.ReplaceAliases, @@ -456,6 +459,19 @@ func NewFuncMap() []template.FuncMap { return items }, "HasPrefix": strings.HasPrefix, + "CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string { + var curBranch string + if repo.ID != baseRepo.ID { + curBranch += fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name)) + } + curBranch += util.PathEscapeSegments(branchName) + + return fmt.Sprintf("%s/compare/%s...%s", + baseRepo.Link(), + util.PathEscapeSegments(baseRepo.DefaultBranch), + curBranch, + ) + }, }} } @@ -795,6 +811,16 @@ func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[stri return template.HTML(renderedMessage) } +// Match text that is between back ticks. +var codeMatcher = regexp.MustCompile("`([^`]+)`") + +// RenderCodeBlock renders "`…`" as highlighted "" block. +// Intended for issue and PR titles, these containers should have styles for "" elements +func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { + htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "$1") // replace with HTML tags + return template.HTML(htmlWithCodeTags) +} + // RenderIssueTitle renders issue/pull title with defined post processors func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML { renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ diff --git a/modules/translation/i18n/errors.go b/modules/translation/i18n/errors.go index b485badd1d..a81b0bc1ac 100644 --- a/modules/translation/i18n/errors.go +++ b/modules/translation/i18n/errors.go @@ -4,9 +4,11 @@ package i18n -import "errors" +import ( + "code.gitea.io/gitea/modules/util" +) var ( - ErrLocaleAlreadyExist = errors.New("lang already exists") - ErrUncertainArguments = errors.New("arguments to i18n should not contain uncertain slices") + ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist} + ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument} ) diff --git a/modules/updatechecker/update_checker.go b/modules/updatechecker/update_checker.go index 9c1569b15e..816fb3764c 100644 --- a/modules/updatechecker/update_checker.go +++ b/modules/updatechecker/update_checker.go @@ -8,10 +8,10 @@ import ( "io" "net/http" - "code.gitea.io/gitea/modules/appstate" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/system" "github.com/hashicorp/go-version" ) @@ -64,13 +64,13 @@ func GiteaUpdateChecker(httpEndpoint string) error { // UpdateRemoteVersion updates the latest available version of Gitea func UpdateRemoteVersion(version string) (err error) { - return appstate.AppState.Set(&CheckerState{LatestVersion: version}) + return system.AppState.Set(&CheckerState{LatestVersion: version}) } // GetRemoteVersion returns the current remote version (or currently installed version if fail to fetch from DB) func GetRemoteVersion() string { item := new(CheckerState) - if err := appstate.AppState.Get(item); err != nil { + if err := system.AppState.Get(item); err != nil { return "" } return item.LatestVersion diff --git a/modules/util/error.go b/modules/util/error.go new file mode 100644 index 0000000000..08e491dbaf --- /dev/null +++ b/modules/util/error.go @@ -0,0 +1,37 @@ +// 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 util + +import ( + "errors" +) + +// Common Errors forming the base of our error system +// +// Many Errors returned by Gitea can be tested against these errors +// using errors.Is. +var ( + ErrInvalidArgument = errors.New("invalid argument") + ErrPermissionDenied = errors.New("permission denied") + ErrAlreadyExist = errors.New("resource already exists") + ErrNotExist = errors.New("resource does not exist") +) + +// SilentWrap provides a simple wrapper for a wrapped error where the wrapped error message plays no part in the error message +// Especially useful for "untyped" errors created with "errors.New(…)" that can be classified as 'invalid argument', 'permission denied', 'exists already', or 'does not exist' +type SilentWrap struct { + Message string + Err error +} + +// Error returns the message +func (w SilentWrap) Error() string { + return w.Message +} + +// Unwrap returns the underlying error +func (w SilentWrap) Unwrap() error { + return w.Err +} diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini index 3a63f3cb81..24617e648d 100644 --- a/options/locale/locale_bg-BG.ini +++ b/options/locale/locale_bg-BG.ini @@ -261,6 +261,7 @@ register_success=Успешна регистрация + [modal] yes=Да no=Не @@ -1190,6 +1191,7 @@ config.log_config=Конфигурация на журнал config.log_mode=Режим на журнал config.disabled_logger=Изключено + monitor.cron=Cron задачи monitor.name=Име monitor.schedule=График diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 80f0b12661..6da1cac65d 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -268,8 +268,11 @@ users=Uživatelé organizations=Organizace search=Vyhledat code=Zdrojový kód +search.type.tooltip=Druh vyhledávání search.fuzzy=Fuzzy +search.fuzzy.tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu search.match=Shoda +search.match.tooltip=Zahrnout pouze výsledky, které odpovídají přesnému hledanému výrazu code_search_unavailable=V současné době není vyhledávání kódu dostupné. Obraťte se na správce webu. repo_no_results=Nebyly nalezeny žádné odpovídající repozitáře. user_no_results=Nebyly nalezeni žádní odpovídající uživatelé. @@ -409,6 +412,7 @@ repo.transfer.body=Chcete-li ji přijmout nebo odmítnout, navštivte %s nebo ji repo.collaborator.added.subject=%s vás přidal do %s repo.collaborator.added.text=Byl jste přidán jako spolupracovník repozitáře: + [modal] yes=Ano no=Ne @@ -507,6 +511,7 @@ activity=Veřejná aktivita followers=Sledující starred=Oblíbené repozitáře watched=Sledované repozitáře +code=Kód projects=Projekty following=Sledovaní follow=Sledovat @@ -1763,8 +1768,11 @@ activity.git_stats_deletion_n=%d odebrání search=Vyhledat search.search_repo=Hledat repozitář +search.type.tooltip=Druh vyhledávání search.fuzzy=Fuzzy +search.fuzzy.tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu search.match=Shoda +search.match.tooltip=Zahrnout pouze výsledky, které odpovídají přesnému hledanému výrazu search.results=Výsledky hledání „%s“ v %s search.code_no_results=Nebyl nalezen žádný zdrojový kód odpovídající hledanému výrazu. search.code_search_unavailable=V současné době není vyhledávání kódu dostupné. Obraťte se na správce webu. @@ -2310,6 +2318,7 @@ create_org=Vytvořit organizaci repo_updated=Upraveno people=Lidé teams=Týmy +code=Kód lower_members=členové lower_repositories=repozitáře create_new_team=Nový tým @@ -2871,6 +2880,9 @@ config.access_log_template=Šablona config.xorm_log_mode=Režim logování XORM config.xorm_log_sql=Logovat SQL +config.get_setting_failed=Získání nastavení %s se nezdařilo +config.set_setting_failed=Nastavení %s se nezdařilo + monitor.cron=Naplánované úlohy monitor.name=Název monitor.schedule=Rozvrh @@ -3034,6 +3046,9 @@ pin=Připnout upozornění mark_as_read=Označit jako přečtené mark_as_unread=Označit jako nepřečtené mark_all_as_read=Označit vše jako přečtené +subscriptions=Odběry +watching=Sledované +no_subscriptions=Žádné odběry [gpg] default_key=Podepsáno výchozím klíčem diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 35cc9a1c53..789774e322 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -407,6 +407,7 @@ repo.transfer.body=Um es anzunehmen oder abzulehnen, öffne %s, oder ignoriere e repo.collaborator.added.subject=%s hat dich zu %s hinzugefügt repo.collaborator.added.text=Du wurdest als Mitarbeiter für folgendes Repository hinzugefügt: + [modal] yes=Ja no=Abbrechen @@ -2850,6 +2851,7 @@ config.access_log_template=Vorlage config.xorm_log_mode=XORM Log-Modus config.xorm_log_sql=SQL protokollieren + monitor.cron=Cron-Aufgaben monitor.name=Name monitor.schedule=Zeitplan diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index fa067484ff..62ef37d4b1 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -409,6 +409,7 @@ repo.transfer.body=Για να το αποδεχτείτε ή να το απορ repo.collaborator.added.subject=%s σας πρόσθεσε στο %s repo.collaborator.added.text=Έχετε προστεθεί ως συνεργάτης του αποθετηρίου: + [modal] yes=Ναι no=Όχι @@ -2870,6 +2871,7 @@ config.access_log_template=Πρότυπο config.xorm_log_mode=Λειτουργία Καταγραφών XORM config.xorm_log_sql=Καταγραφή SQL + monitor.cron=Προγραμματισμένες Εργασίες monitor.name=Όνομα monitor.schedule=Πρόγραμμα diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index fbf9b70643..1566dfc97d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -106,6 +106,10 @@ never = Never rss_feed = RSS Feed +[filter] +string.asc = A - Z +string.desc = Z - A + [error] occurred = An error occurred report_message = If you are sure this is a Gitea bug, please search for issues on GitHub or open a new issue if necessary. @@ -412,6 +416,11 @@ repo.transfer.body = To accept or reject it visit %s or just ignore it. repo.collaborator.added.subject = %s added you to %s repo.collaborator.added.text = You have been added as a collaborator of repository: +team_invite.subject = %[1]s has invited you to join the %[2]s organization +team_invite.text_1 = %[1]s has invited you to join team %[2]s in organization %[3]s. +team_invite.text_2 = Please click the following link to join the team: +team_invite.text_3 = Note: This invitation was intended for %[1]s. If you were not expecting this invitation, you can ignore this email. + [modal] yes = Yes no = No @@ -487,6 +496,7 @@ user_not_exist = The user does not exist. team_not_exist = The team does not exist. last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner for an organization. cannot_add_org_to_team = An organization cannot be added as a team member. +duplicate_invite_to_team = The user was already invited as a team member. invalid_ssh_key = Can not verify your SSH key: %s invalid_gpg_key = Can not verify your GPG key: %s @@ -739,9 +749,7 @@ create_oauth2_application_button = Create Application create_oauth2_application_success = You've successfully created a new OAuth2 application. update_oauth2_application_success = You've successfully updated the OAuth2 application. oauth2_application_name = Application Name -oauth2_select_type = Which application type fits? -oauth2_type_web = Web (e.g. Node.JS, Tomcat, Go) -oauth2_type_native = Native (e.g. Mobile, Desktop, Browser) +oauth2_confidential_client = Confidential Client. Select for apps that keep the secret confidential, such as web apps. Do not select for native apps including desktop and mobile apps. oauth2_redirect_uri = Redirect URI save_application = Save oauth2_client_id = Client ID @@ -2402,6 +2410,8 @@ teams.members = Team Members teams.update_settings = Update Settings teams.delete_team = Delete Team teams.add_team_member = Add Team Member +teams.invite_team_member = Invite to %s +teams.invite_team_member.list = Pending Invitations teams.delete_team_title = Delete Team teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue? teams.delete_team_success = The team has been deleted. @@ -2426,6 +2436,9 @@ teams.all_repositories_helper = Team has access to all repositories. Selecting t teams.all_repositories_read_permission_desc = This team grants Read access to all repositories: members can view and clone repositories. teams.all_repositories_write_permission_desc = This team grants Write access to all repositories: members can read from and push to repositories. teams.all_repositories_admin_permission_desc = This team grants Admin access to all repositories: members can read from, push to and add collaborators to repositories. +teams.invite.title = You've been invited to join team %s in organization %s. +teams.invite.by = Invited by %s +teams.invite.description = Please click the button below to join the team. [admin] dashboard = Dashboard @@ -2879,6 +2892,9 @@ config.access_log_template = Template config.xorm_log_mode = XORM Log Mode config.xorm_log_sql = Log SQL +config.get_setting_failed = Get setting %s failed +config.set_setting_failed = Set setting %s failed + monitor.cron = Cron Tasks monitor.name = Name monitor.schedule = Schedule diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 637f88c9bd..31264ea9bd 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -268,8 +268,10 @@ users=Usuarios organizations=Organizaciones search=Buscar code=Código +search.type.tooltip=Tipo de búsqueda search.fuzzy=Parcial search.match=Coincidir +search.match.tooltip=Incluye sólo los resultados que coincidan con el término de búsqueda exacto code_search_unavailable=Actualmente la búsqueda de código no está disponible. Póngase en contacto con el administrador de su sitio. repo_no_results=No se ha encontrado ningún repositorio coincidente. user_no_results=No se ha encontrado ningún usuario coincidente. @@ -409,6 +411,7 @@ repo.transfer.body=Para aceptarlo o rechazarlo, visita %s o simplemente ignórel repo.collaborator.added.subject=%s le añadió en %s repo.collaborator.added.text=Has sido añadido como colaborador del repositorio: + [modal] yes=Sí no=No @@ -507,6 +510,7 @@ activity=Actividad pública followers=Seguidores starred=Repositorios Favoritos watched=Repositorios seguidos +code=Código projects=Proyectos following=Siguiendo follow=Seguir @@ -1231,6 +1235,8 @@ issues.new.add_reviewer_title=Solicitar revisión issues.choose.get_started=Comenzar issues.choose.blank=Predeterminado issues.choose.blank_about=Crear una incidencia a partir de la plantilla predeterminada. +issues.choose.ignore_invalid_templates=Las plantillas no válidas han sido ignoradas +issues.choose.invalid_templates=%v plantilla(s) no válida(s) encontradas issues.no_ref=Ninguna Rama/Etiqueta especificada issues.create=Crear incidencia issues.new_label=Nueva Etiqueta @@ -1761,8 +1767,10 @@ activity.git_stats_deletion_n=%d eliminaciones search=Buscar search.search_repo=Buscar repositorio +search.type.tooltip=Tipo de búsqueda search.fuzzy=Parcial search.match=Coincidir +search.match.tooltip=Incluye sólo los resultados que coincidan con el término de búsqueda exacto search.results=Resultados de la búsqueda para "%s" en %s search.code_no_results=No se ha encontrado código de fuente que coincida con su término de búsqueda. search.code_search_unavailable=Actualmente la búsqueda de código no está disponible. Póngase en contacto con el administrador de su sitio. @@ -1896,6 +1904,7 @@ settings.confirm_delete=Eliminar este repositorio settings.add_collaborator=Añadir colaborador settings.add_collaborator_success=El nuevo colaborador ha sido añadido. settings.add_collaborator_inactive_user=No se puede añadir un usuario inactivo como colaborador. +settings.add_collaborator_owner=No se puede añadir un propietario como colaborador. settings.add_collaborator_duplicate=El colaborador ya está añadido a este repositorio. settings.delete_collaborator=Eliminar settings.collaborator_deletion=Eliminar colaborador @@ -1954,6 +1963,8 @@ settings.event_delete=Eliminar settings.event_delete_desc=Rama o etiqueta eliminada. settings.event_fork=Fork settings.event_fork_desc=Repositorio forkeado. +settings.event_wiki=Wiki +settings.event_wiki_desc=Página de la Wiki creada, renombrada, editada o eliminada. settings.event_release=Lanzamiento settings.event_release_desc=Lanzamiento publicado, actualizado o eliminado en un repositorio. settings.event_push=Push @@ -2305,6 +2316,7 @@ create_org=Crear Organización repo_updated=Actualizado people=Personas teams=Equipos +code=Código lower_members=miembros lower_repositories=repositorios create_new_team=Nuevo equipo @@ -2866,6 +2878,7 @@ config.access_log_template=Plantilla config.xorm_log_mode=Modo de registro XORM config.xorm_log_sql=Registrar SQL + monitor.cron=Tareas de Cron monitor.name=Nombre monitor.schedule=Agenda @@ -3029,6 +3042,9 @@ pin=Fijar notificación mark_as_read=Marcar como leído mark_as_unread=Marcar como no leído mark_all_as_read=Marcar todo como leído +subscriptions=Suscripciones +watching=Siguiendo +no_subscriptions=Sin suscripciones [gpg] default_key=Firmado con clave predeterminada @@ -3088,6 +3104,7 @@ container.details.platform=Plataforma container.details.repository_site=Sitio del repositorio container.details.documentation_site=Sitio de documentación container.pull=Arrastra la imagen desde la línea de comandos: +container.digest=Resumen: container.documentation=Para más información sobre el registro de Container, consulte la documentación. container.multi_arch=SO / Arquitectura container.layers=Capas de imagen diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 0536bd42f2..e86480d0bc 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -379,6 +379,7 @@ repo.transfer.body=برای تایید یا رد آن %s را ببینید یا repo.collaborator.added.subject=%s شما را به پروژه %s اضافه کرد repo.collaborator.added.text=شما به عنوان مشارکت‌کننده در این مخزن اضافه شدید: + [modal] yes=بله no=خیر @@ -2634,6 +2635,7 @@ config.access_log_template=الگو config.xorm_log_mode=شیوه ثبت رخداد XORM config.xorm_log_sql=ثبت رخداد SQL + monitor.cron=وظایف Cron monitor.name=نام monitor.schedule=زمان بندی diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index ae95ac844d..7f13d1b807 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -358,6 +358,7 @@ release.download.targz=Lähdekoodi (TAR.GZ) repo.transfer.to_you=sinä + [modal] yes=Kyllä no=Ei @@ -1660,6 +1661,7 @@ config.log_mode=Loki tila config.disabled_logger=Pois käytöstä config.access_log_template=Malli + monitor.cron=Cron tehtävät monitor.name=Nimi monitor.schedule=Aikataulu diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 053c2da214..eaedeaf9c3 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -268,8 +268,11 @@ users=Utilisateurs organizations=Organisations search=Rechercher code=Code +search.type.tooltip=Type de recherche search.fuzzy=Approximative +search.fuzzy.tooltip=Inclure également les résultats proches de la recherche search.match=Exacte +search.match.tooltip=Inclure uniquement les résultats exacts code_search_unavailable=Actuellement, la recherche de code n'est pas disponible. Veuillez contacter l'administrateur de votre site. repo_no_results=Aucun dépôt correspondant n'a été trouvé. user_no_results=Aucun utilisateur correspondant n'a été trouvé. @@ -278,6 +281,7 @@ code_no_results=Aucun code source correspondant à votre terme de recherche n'a code_search_results=Résultats de recherche pour "%s" code_last_indexed_at=Dernière indexation %s relevant_repositories_tooltip=Les dépôts qui sont des forks ou qui n'ont aucun sujet, aucune icône et aucune description sont cachés. +relevant_repositories=Seuls les dépôts pertinents sont affichés, afficher les résultats non filtrés. [auth] @@ -379,11 +383,17 @@ issue_assigned.issue=@%[1]s vous a assigné le ticket %[2]s dans le dépôt %[3] issue.x_mentioned_you=@%s vous a mentionné: issue.action.force_push=%[1]s a forcé la mise à jour de %[2]s depuis %[3]s vers %[4]s. +issue.action.push_1=@%[1]s a mis à jour le commit %[3]d vers %[2]s +issue.action.push_n=@%[1]s a poussé les révisions %[3]d vers %[2]s +issue.action.close=@%[1]s a fermé #%[2]d. +issue.action.reopen=@%[1]s a réouvert #%[2]d. +issue.action.merge=@%[1]s a fusionné de #%[2]d vers %[3]s. issue.action.approve=@%[1]s a approuvé cette demande d'ajout. issue.action.reject=@%[1]s a demandé des modifications sur cette demande d'ajout. issue.action.review=@%[1]s a commenté sur cette demande d'ajout. issue.action.review_dismissed=@%[1]s a rejeté la dernière révision de %[2]s pour cette demande d'ajout. issue.action.ready_for_review=@%[1]s a marqué cette demande d'ajout prête à être revue. +issue.action.new=@%[1]s a créé #%[2]d. issue.in_tree_path=Dans %s: release.new.subject=%s publiée dans %s @@ -402,6 +412,7 @@ repo.transfer.body=Pour l'accepter ou le rejeter, visitez %s ou ignorez-le. repo.collaborator.added.subject=%s vous a ajouté à %s repo.collaborator.added.text=Vous avez été ajouté en tant que collaborateur du dépôt : + [modal] yes=Oui no=Non @@ -450,6 +461,7 @@ lang_select_error=Sélectionnez une langue dans la liste. username_been_taken=Le nom d'utilisateur est déjà pris. username_change_not_local_user=Les utilisateurs non-locaux n'ont pas le droit de modifier leur nom d'utilisateur. repo_name_been_taken=Ce nom de dépôt est déjà utilisé. +repository_force_private=Force Private est activé : les dépôts privés ne peuvent pas être rendus publics. repository_files_already_exist=Les fichiers existent déjà pour ce dépôt. Contactez l'administrateur système. repository_files_already_exist.adopt=Des fichiers existent déjà pour ce dépôt et peuvent seulement être adoptés. repository_files_already_exist.delete=Des fichiers existent déjà pour ce dépôt. Vous devez les supprimer. @@ -485,6 +497,7 @@ auth_failed=Échec d'authentification : %v still_own_repo=Ce compte possède toujours un ou plusieurs dépôts, vous devez d'abord les supprimer ou les transférer. still_has_org=Votre compte est un membre d’une ou plusieurs organisations, veuillez d'abord les quitter. +still_own_packages=Votre compte possède toujours un ou plusieurs paquets, vous devez d'abord les supprimer ou les transférer. org_still_own_repo=Cette organisation possède encore un ou plusieurs dépôts. Vous devez d'abord les supprimer ou les transférer. org_still_own_packages=Cette organisation possède encore un ou plusieurs paquets. Vous devez d'abord les supprimer. @@ -498,6 +511,7 @@ activity=Activité publique followers=abonnés starred=Dépôts favoris watched=Dépôts surveillés +code=Code projects=Projets following=Abonnements follow=Suivre @@ -549,6 +563,7 @@ continue=Continuer cancel=Annuler language=Langues ui=Thème +hidden_comment_types=Texte de commentaires caché comment_type_group_reference=Référence comment_type_group_label=Étiquette comment_type_group_milestone=Jalon @@ -640,7 +655,9 @@ ssh_principal_been_used=Ce principal a déjà été ajouté au serveur. gpg_key_id_used=Une clef GPG publique avec le même identifiant existe déjà. gpg_no_key_email_found=Cette clé GPG ne correspond à aucune adresse e-mail activée et associée avec votre compte. Elle peut toujours être ajoutée si vous signez le jeton fourni. gpg_key_matched_identities=Identités correspondantes : +gpg_key_matched_identities_long=Les identités embarquées dans cette clé correspondent à l'adresse courriel activée ci-après pour cet utilisateur. Les révisions correspondantes à cette adresse courriel peuvent être vérifiés avec cette clé. gpg_key_verified=Clé vérifiée +gpg_key_verified_long=La clé a été vérifiée avec un jeton et peut être utilisée pour vérifier les commits correspondant à toutes les adresses courriel pour cet utilisateur en plus de toutes les identités pour cette clé. gpg_key_verify=Vérifier gpg_invalid_token_signature=La clé GPG fournie, la signature et le jeton ne correspondent pas ou le jeton n'est pas à jour. gpg_token_required=Vous devez fournir une signature pour le jeton ci-dessous @@ -651,11 +668,14 @@ gpg_token_signature=Signature GPG renforcée key_signature_gpg_placeholder=Commence par '-----BEGIN PGP SIGNATURE-----' verify_gpg_key_success=La clef GPG '%s' a été vérifiée. ssh_key_verified=Clé vérifiée +ssh_key_verified_long=La clé a été vérifiée avec un jeton et peut être utilisée pour vérifier les commits correspondant à toutes les adresses courriel activées pour cet utilisateur. ssh_key_verify=Vérifier +ssh_invalid_token_signature=La clé SSH, la signature ou le jeton fournis ne correspondent pas ou le jeton n'est pas à jour. ssh_token_required=Vous devez fournir une signature pour le jeton ci-dessous ssh_token=Jeton ssh_token_help=Vous pouvez générer une signature en utilisant : ssh_token_signature=Signature SSH renforcée +key_signature_ssh_placeholder=Commence par '-----BEGIN PGP SIGNATURE-----' verify_ssh_key_success=La clef SSH '%s' a été vérifiée. subkeys=Sous-clés key_id=Clé ID @@ -706,6 +726,7 @@ delete_token=Supprimer access_token_deletion=Suppression de jetons d'accès access_token_deletion_cancel_action=Annuler access_token_deletion_confirm_action=Supprimer +access_token_deletion_desc=Supprimer un jeton révoquera l'accès à votre compte pour toutes les applications l'utilisant. Cette action est irréversible. Continuer ? delete_token_success=Ce jeton a été supprimé. Les applications l'utilisant n'ont plus accès à votre compte. manage_oauth2_applications=Gérer les applications OAuth2 @@ -758,9 +779,11 @@ passcode_invalid=Le mot de passe est invalide. Réessayez. twofa_enrolled=L'authentification à deux facteurs a été activée pour votre compte. Gardez votre jeton de secours (%s) en lieu sûr car il ne vous sera montré qu'une seule fois ! twofa_failed_get_secret=Impossible d'obtenir le secret. +webauthn_desc=Les clefs de sécurité sont des dispositifs matériels contenant des clefs cryptographiques. Elles peuvent être utilisées pour l'authentification à deux facteurs. La clef de sécurité doit supporter le standard WebAuthn Authenticator. webauthn_register_key=Ajouter une clé de sécurité webauthn_nickname=Pseudonyme webauthn_delete_key=Supprimer la clé de sécurité +webauthn_delete_key_desc=Si vous retirez une clé de sécurité vous ne pourrez plus l'utiliser pour vous connecter. Continuer ? manage_account_links=Gérer les comptes liés manage_account_links_desc=Ces comptes externes sont liés à votre compte Gitea. @@ -784,7 +807,9 @@ email_notifications.enable=Activer les notifications par e-mail email_notifications.onmention=N'envoyer un e-mail que si vous êtes mentionné email_notifications.disable=Désactiver les notifications par email email_notifications.submit=Définir la préférence e-mail +email_notifications.andyourown=Et vos propres notifications +visibility=Visibilité de l'utilisateur visibility.public=Publique visibility.public_tooltip=Visible par tous les utilisateurs visibility.limited=Limitée @@ -811,6 +836,8 @@ visibility_fork_helper=(Changer ceci affectera toutes les bifurcations.) clone_helper=Besoin d'aide pour dupliquer ? Visitez l'aide. fork_repo=Créer une bifurcation du dépôt fork_from=Bifurquer depuis +already_forked=Vous avez déjà forké %s +fork_to_different_account=Créer un embranchement vers un autre compte fork_visibility_helper=La visibilité d'un dépôt bifurqué ne peut pas être modifiée. use_template=Utiliser ce modèle clone_in_vsc=Cloner dans VS Code @@ -843,6 +870,7 @@ default_branch=Branche par défaut default_branch_helper=La branche par défaut est la branche de base pour les demandes d'ajout et les révisions de code. mirror_prune=Purger mirror_prune_desc=Supprimer les références externes obsolètes +mirror_interval=Intervalle de synchronisation (les unités de temps valides sont 'h', 'm' et 's'). 0 pour désactiver la synchronisation automatique. (Intervalle minimum : %s) mirror_interval_invalid=L'intervalle de synchronisation est invalide. mirror_sync_on_commit=Synchroniser quand les commits sont poussés mirror_address=Cloner depuis une URL @@ -893,6 +921,7 @@ desc.archived=Archivé template.items=Élément du modèle template.git_content=Contenu Git (branche par défaut) template.git_hooks=Déclencheurs Git +template.git_hooks_tooltip=Vous ne pouvez actuellement pas modifier ou supprimer les déclencheurs Git ajoutés. Sélectionnez cette option uniquement si vous faites confiance au modèle de dépôt. template.webhooks=Déclencheurs Web template.topics=Sujets template.avatar=Avatar @@ -929,8 +958,10 @@ migrate_items_releases=Versions migrate_repo=Migrer le dépôt migrate.clone_address=Migrer/Cloner depuis une URL migrate.clone_address_desc=L'URL HTTP(S) ou Git "clone" d'un dépôt existant +migrate.github_token_desc=Vous pouvez mettre un ou plusieurs jetons séparés par des virgules ici pour rendre la migration plus rapide en raison de la limite de débit de l'API GitHub. ATTENTION : Abuser de cette fonctionnalité peut enfreindre la politique du fournisseur de services et entraîner un blocage de compte. migrate.clone_local_path=ou un chemin serveur local migrate.permission_denied=Vous n'êtes pas autorisé à importer des dépôts locaux. +migrate.permission_denied_blocked=Vous ne pouvez pas importer depuis des hôtes interdits, veuillez demander à l'administrateur de vérifier les paramètres ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS. migrate.invalid_local_path=Chemin local non valide, non existant ou n'étant pas un dossier. migrate.invalid_lfs_endpoint=Le point d'accès LFS n'est pas valide. migrate.failed=Echec de migration: %v @@ -949,6 +980,7 @@ migrate.gitea.description=Migrer les données depuis gitea.com ou d’autres ins migrate.gogs.description=Migrer les données depuis notabug.org ou d’autres instances de Gogs. migrate.onedev.description=Migrer les données depuis code.onedev.io ou d’autre instance de OneDev. migrate.codebase.description=Migrer les données depuis codebasehq.com. +migrate.gitbucket.description=Migrer les données depuis des instances GitBucket. migrate.migrating_git=Migration des données Git migrate.migrating_topics=Migration des sujets migrate.migrating_milestones=Migration des jalons @@ -977,6 +1009,7 @@ clone_this_repo=Cloner ce dépôt create_new_repo_command=Création d'un nouveau dépôt en ligne de commande push_exist_repo=Soumission d'un dépôt existant par ligne de commande empty_message=Ce dépôt n'a pas de contenu. +broken_message=Les données git de ce dépôt ne peuvent pas être lues. Contactez l'administrateur de cette instance ou supprimez ce dépôt. code=Code code.desc=Accéder au code source, fichiers, révisions et branches. @@ -1011,6 +1044,7 @@ file_view_raw=Voir le Raw file_permalink=Lien permanent file_too_large=Le fichier est trop gros pour être affiché. invisible_runes_header=`Ce fichier contient des caractères Unicode invisibles !` +invisible_runes_description=`Ce fichier contient des caractères Unicode invisibles qui pourraient être affichés différemment de ce qui apparaît ci-dessous. Si votre cas d'utilisation est intentionnel et légitime, vous pouvez ignorer en toute sécurité cet avertissement. Utilisez le bouton Échapper pour mettre en évidence ces caractères invisbles.` ambiguous_runes_header=`Ce fichier contient des caractères Unicode ambigus !` invisible_runes_line=`Cette ligne contient des caractères Unicode invisibles` ambiguous_runes_line=`Cette ligne contient des caractères Unicode ambigus` @@ -2633,6 +2667,7 @@ config.access_log_template=Modèle config.xorm_log_mode=Mode de journalisation de XORM config.xorm_log_sql=Activer la journalisation SQL + monitor.cron=Tâches récurrentes monitor.name=Nom monitor.schedule=Planification diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index bacaff9452..c5bd8c5af2 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -291,6 +291,7 @@ register_success=Sikeres regisztráció + [modal] yes=Igen no=Nem @@ -1672,6 +1673,7 @@ config.disabled_logger=Letiltva config.access_log_template=Sablon config.xorm_log_sql=SQL naplózása + monitor.cron=Ütemezett Feladatok monitor.name=Név monitor.schedule=Ütemezés diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 39818b81ee..8e537abf71 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -282,6 +282,7 @@ register_success=Pendaftaran berhasil + [modal] yes=Ya no=Tidak @@ -1283,6 +1284,7 @@ config.log_mode=Mode catat config.disabled_logger=Nonaktif config.xorm_log_sql=Catatan SQL + monitor.cron=Tugas Cron monitor.name=Nama monitor.schedule=Jadwal diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index eda95c1b6c..2e33940b99 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -339,6 +339,7 @@ repo.transfer.body=Til að samþykkja eða hafna því skaltu fara á %s eða hu repo.collaborator.added.subject=%s bætti þér við í %s repo.collaborator.added.text=Þér hefur verið bætt við sem aðila hugbúnaðarsafns: + [modal] yes=Já no=Nei @@ -1273,6 +1274,7 @@ config.https_only=Aðeins HTTPS + monitor.name=Heiti monitor.goroutines=%d Górútínur monitor.desc=Lýsing diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 027c7e0575..2a159dda6f 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -407,6 +407,7 @@ repo.transfer.body=Per accettare o respingerla visita %s o semplicemente ignorar repo.collaborator.added.subject=%s ti ha aggiunto a %s repo.collaborator.added.text=Sei stato aggiunto come collaboratore del repository: + [modal] yes=Sì no=No @@ -2864,6 +2865,7 @@ config.access_log_template=Template config.xorm_log_mode=Modalità log XORM config.xorm_log_sql=Log SQL + monitor.cron=Incarichi Cron monitor.name=Nome monitor.schedule=Agenda diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 6611d24e84..a4c6349eee 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -268,8 +268,11 @@ users=ユーザー organizations=組織 search=検索 code=コード +search.type.tooltip=検索タイプ search.fuzzy=あいまい +search.fuzzy.tooltip=検索ワードにおおよそ一致している結果も含めます search.match=一致 +search.match.tooltip=検索ワードに一致する結果だけを含めます code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。 repo_no_results=一致するリポジトリが見つかりません。 user_no_results=一致するユーザーが見つかりません。 @@ -409,6 +412,7 @@ repo.transfer.body=承認または拒否するには %s を開きます。 も repo.collaborator.added.subject=%s が %s にあなたを追加しました repo.collaborator.added.text=あなたは次のリポジトリの共同作業者に追加されました: + [modal] yes=はい no=いいえ @@ -507,6 +511,7 @@ activity=公開アクティビティ followers=フォロワー starred=スター付きリポジトリ watched=ウォッチ中リポジトリ +code=コード projects=プロジェクト following=フォロー中 follow=フォロー @@ -1763,8 +1768,11 @@ activity.git_stats_deletion_n=%d行削除 search=検索 search.search_repo=リポジトリを検索 +search.type.tooltip=検索タイプ search.fuzzy=あいまい +search.fuzzy.tooltip=検索ワードにおおよそ一致している結果も含めます search.match=一致 +search.match.tooltip=検索ワードに一致する結果だけを含めます search.results=%[3]s 内での "%[1]s" の検索結果 search.code_no_results=検索ワードに一致するソースコードが見つかりません。 search.code_search_unavailable=現在コード検索は利用できません。 サイト管理者にお問い合わせください。 @@ -2310,6 +2318,7 @@ create_org=組織を作成 repo_updated=最終更新 people=メンバー teams=チーム +code=コード lower_members=メンバー lower_repositories=リポジトリ create_new_team=新しいチーム @@ -2871,6 +2880,7 @@ config.access_log_template=テンプレート config.xorm_log_mode=XORMログのモード config.xorm_log_sql=SQLのログ出力 + monitor.cron=Cronタスク monitor.name=名称 monitor.schedule=スケジュール diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index e80eea95f7..db2cee952b 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -273,6 +273,7 @@ register_success=등록 완료 + [modal] yes=예 no=아니오 @@ -1496,6 +1497,7 @@ config.git_gc_timeout=가비지 콜렉션 작업 시간 제한 config.log_config=로그 설정 config.log_mode=로그 모드 + monitor.cron=Cron 작업 monitor.name=이름 monitor.schedule=스케줄 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 5cc5323636..d6fd4fee75 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -407,6 +407,7 @@ repo.transfer.body=Ja vēlaties to noraidīt vai apstiprināt, tad apmeklējiet repo.collaborator.added.subject=%s pievienoja Jūs repozitorijam %s repo.collaborator.added.text=Jūs tikāt pievienots kā līdzstrādnieks repozitorijam: + [modal] yes=Jā no=Nē @@ -2855,6 +2856,7 @@ config.access_log_template=Šablons config.xorm_log_mode=XORM žurnalizēšanas veids config.xorm_log_sql=SQL žurnalizēšana + monitor.cron=Cron uzdevumi monitor.name=Nosaukums monitor.schedule=Grafiks diff --git a/options/locale/locale_ml-IN.ini b/options/locale/locale_ml-IN.ini index 6474abc59e..90731f6f70 100644 --- a/options/locale/locale_ml-IN.ini +++ b/options/locale/locale_ml-IN.ini @@ -261,6 +261,7 @@ register_success=രജിസ്ട്രേഷൻ വിജയകരം + [modal] yes=അതെ no=ഇല്ല @@ -785,6 +786,7 @@ repos.issues=ഇഷ്യൂകള്‍ + [action] diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 9bcb0f7c75..0320efbf22 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -407,6 +407,7 @@ repo.transfer.body=Om het te accepteren of afwijzen, bezoek %s of negeer het gew repo.collaborator.added.subject=%s heeft jou toegevoegd aan %s repo.collaborator.added.text=U bent toegevoegd als een medewerker van de repository: + [modal] yes=Ja no=Nee @@ -2714,6 +2715,7 @@ config.access_log_template=Sjabloon config.xorm_log_mode=XORM Log-modus config.xorm_log_sql=Log SQL + monitor.cron=Cron-taken monitor.name=Naam monitor.schedule=Planning diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 5abf069c52..005b9bdb20 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -392,6 +392,7 @@ repo.transfer.body=Aby zaakceptować lub odrzucić go, odwiedź %s lub po prostu repo.collaborator.added.subject=%s dodał Cię do %s repo.collaborator.added.text=Zostałeś dodany jako współtwórca repozytorium: + [modal] yes=Tak no=Nie @@ -2552,6 +2553,7 @@ config.access_log_template=Szablon config.xorm_log_mode=Tryb dziennika XORM config.xorm_log_sql=Dziennik SQL + monitor.cron=Zadania cron monitor.name=Nazwa monitor.schedule=Harmonogram diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index eb63580cb5..86e375c6ba 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -409,6 +409,7 @@ repo.transfer.body=Para o aceitar ou rejeitar visite %s, ou simplesmente o ignor repo.collaborator.added.subject=%s adicionou você a %s repo.collaborator.added.text=Você foi adicionado como um colaborador do repositório: + [modal] yes=Sim no=Não @@ -2857,6 +2858,7 @@ config.access_log_template=Modelo config.xorm_log_mode=Modo log XORM config.xorm_log_sql=Log SQL + monitor.cron=Tarefas cron monitor.name=Nome monitor.schedule=Cronograma diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 1a301ddde6..14f6a33ca2 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -146,7 +146,7 @@ sqlite_helper=Localização do ficheiro da base de dados em SQLite3.
Insira u reinstall_error=Está a tentar instalar numa base de dados do Gitea já existente reinstall_confirm_message=Reinstalar com uma base de dados do Gitea já existente pode causar múltiplos problemas. Na maioria dos casos deve usar o seu "app.ini" existente para correr o Gitea. Se souber o que está a fazer, confirme o seguinte: reinstall_confirm_check_1=Os dados encriptados pela chave secreta (SECRET_KEY) no ficheiro app.ini poderão ser perdidos: utilizadores poderão não ser capazes de iniciar a sessão com autenticação em dois passos (2FA) ou com chaves de utilização única (OTP) e as réplicas poderão deixar de funcionar em condições. Ao marcar esta opção estará a confirmar que o ficheiro app.ini vigente contém a SECRET_KEY certa. -reinstall_confirm_check_2=Os repositórios e as configurações poderão ter de voltar a ser sincronizados. Ao marcar esta opção estará a confirmar que vai voltar a sincronizar os automatismos para os repositórios e o ficheiro authorized_keys manualmente. Estará também a confirmar que vai assegurar que as configurações do repositório e das réplicas estão em condições. +reinstall_confirm_check_2=Os repositórios e as configurações poderão ter de voltar a ser sincronizados. Ao marcar esta opção estará a confirmar que vai voltar a sincronizar manualmente os automatismos para os repositórios e o ficheiro authorized_keys. Estará também a confirmar que vai assegurar que as configurações do repositório e das réplicas estão em condições. reinstall_confirm_check_3=Você confirma que tem a certeza absoluta de que este Gitea está a correr com a localização certa do ficheiro app.ini e que tem a certeza de que tem de voltar a instalar. Você confirma que tomou conhecimento dos riscos acima descritos. err_empty_db_path=A localização da base de dados SQLite3 não pode estar vazia. no_admin_and_disable_registration=Não pode desabilitar a auto-inscrição de utilizadores sem criar uma conta de administrador. @@ -412,6 +412,11 @@ repo.transfer.body=Para o aceitar ou rejeitar visite %s, ou ignore-o, simplesmen repo.collaborator.added.subject=%s adicionou você a %s repo.collaborator.added.text=Foi adicionado(a) como colaborador(a) do repositório: +team_invite.subject=%[1]s fez-lhe um convite para se juntar à organização %[2]s +team_invite.text_1=%[1]s fez-lhe um convite para se juntar à equipa %[2]s na organização %[3]s. +team_invite.text_2=Clique na ligação seguinte para se juntar à equipa: +team_invite.text_3=Nota: Este convite é dirigido a %[1]s. Se não estava à espera deste convite, pode ignorar este email. + [modal] yes=Sim no=Não @@ -487,6 +492,7 @@ user_not_exist=O utilizador não existe. team_not_exist=A equipa não existe. last_org_owner=Não pode remover o último utilizador da equipa 'proprietários'. Tem que haver pelo menos um proprietário numa organização. cannot_add_org_to_team=Uma organização não pode ser adicionada como membro de uma equipa. +duplicate_invite_to_team=O(A) utilizador(a) já tinha sido convidado(a) para ser membro da equipa. invalid_ssh_key=Não é possível validar a sua chave SSH: %s invalid_gpg_key=Não é possível validar a sua chave GPG: %s @@ -1788,16 +1794,16 @@ settings.collaboration.undefined=Não definido settings.hooks=Automatismos web settings.githooks=Automatismos do Git settings.basic_settings=Configurações básicas -settings.mirror_settings=Configurações da réplica +settings.mirror_settings=Configuração de réplicas settings.mirror_settings.docs=Configure o seu repositório para puxar e/ou enviar automaticamente as modificações de/para outro repositório. Ramos, etiquetas e cometimentos serão sincronizados automaticamente. Como é que eu faço uma réplica de outro repositório? settings.mirror_settings.mirrored_repository=Repositório replicado settings.mirror_settings.direction=Sentido settings.mirror_settings.direction.pull=Puxada settings.mirror_settings.direction.push=Envio settings.mirror_settings.last_update=Última modificação -settings.mirror_settings.push_mirror.none=Não foram configuradas quaisquer réplicas de envio +settings.mirror_settings.push_mirror.none=Não foram configuradas quaiquer réplicas deste repositório settings.mirror_settings.push_mirror.remote_url=URL do repositório remoto Git -settings.mirror_settings.push_mirror.add=Adicionar réplica de envio +settings.mirror_settings.push_mirror.add=Adicionar réplica deste repositório settings.sync_mirror=Sincronizar agora settings.mirror_sync_in_progress=A sincronização da réplica está em andamento. Volte a verificar daqui a um minuto. settings.site=Sítio web @@ -2402,6 +2408,8 @@ teams.members=Membros da equipa teams.update_settings=Modificar configurações teams.delete_team=Eliminar equipa teams.add_team_member=Adicionar membro da equipa +teams.invite_team_member=Convidar para %s +teams.invite_team_member.list=Convites pendentes teams.delete_team_title=Eliminar equipa teams.delete_team_desc=Eliminar uma equipa revoga o acesso dos seus membros ao repositório. Quer continuar? teams.delete_team_success=A equipa foi eliminada. @@ -2426,6 +2434,9 @@ teams.all_repositories_helper=A equipa tem acesso a todos os repositórios. Esco teams.all_repositories_read_permission_desc=Esta equipa atribui o acesso de leitura a todos os repositórios: os seus membros podem ver e clonar os repositórios. teams.all_repositories_write_permission_desc=Esta equipa atribui o acesso de escrita a todos os repositórios: os seus membros podem ler de, e enviar para os repositórios. teams.all_repositories_admin_permission_desc=Esta equipa atribui o acesso de administração a todos os repositórios: os seus membros podem ler de, enviar para, e adicionar colaboradores aos repositórios. +teams.invite.title=Foi-lhe feito um convite para se juntar à equipa %s na organização%s. +teams.invite.by=Convidado(a) por %s +teams.invite.description=Clique no botão abaixo para se juntar à equipa. [admin] dashboard=Painel de controlo @@ -2862,7 +2873,7 @@ config.git_max_diff_line_characters=Número máximos de caracteres diff (por lin config.git_max_diff_files=Número máximo de ficheiros diff a serem apresentados config.git_gc_args=Argumentos da recolha de lixo config.git_migrate_timeout=Prazo da migração -config.git_mirror_timeout=Tempo limite da réplica +config.git_mirror_timeout=Prazo para sincronização da réplica config.git_clone_timeout=Prazo da operação de clonagem config.git_pull_timeout=Prazo da operação de puxar config.git_gc_timeout=Prazo da operação de recolha de lixo @@ -2879,6 +2890,9 @@ config.access_log_template=Modelo config.xorm_log_mode=Modo de registo XORM config.xorm_log_sql=Registo do SQL +config.get_setting_failed=Falha ao obter a configuração %s +config.set_setting_failed=Falha ao definir a configuração %s + monitor.cron=Tarefas Cron monitor.name=Nome monitor.schedule=Programação diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 348fa74329..f83ccb73ed 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -403,6 +403,7 @@ repo.transfer.body=Для того чтобы принять или отклон repo.collaborator.added.subject=%s добавил вас в %s repo.collaborator.added.text=Вы были добавлены в качестве соавтора репозитория: + [modal] yes=Да no=Нет @@ -2739,6 +2740,7 @@ config.access_log_template=Шаблон config.xorm_log_mode=Режим журнала XORM config.xorm_log_sql=Лог SQL + monitor.cron=Задачи cron monitor.name=Название monitor.schedule=Расписание diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 9d19176daf..6652c43f22 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -365,6 +365,7 @@ repo.transfer.body=එය පිළිගැනීමට හෝ ප්රති repo.collaborator.added.subject=%s ඔබව %s ට එකතු කළා repo.collaborator.added.text=ඔබ ගබඩාවේ සහයෝගිතාකරුවෙකු ලෙස එකතු කර ඇත: + [modal] yes=ඔව් no=නැහැ @@ -2566,6 +2567,7 @@ config.access_log_template=සැකිල්ල config.xorm_log_mode=XORM ලොග් ප්රකාරය config.xorm_log_sql=ලොග් SQL + monitor.cron=Con කාර්යයන් monitor.name=නම monitor.schedule=කාලසටහන diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index 9c4475ac5c..1e0f9dc006 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -409,6 +409,7 @@ repo.transfer.body=Ak to chcete prijať alebo odmietnuť, navštívte %s alebo t repo.collaborator.added.subject=%s vás pridal do %s repo.collaborator.added.text=Boli ste pridaný ako spolupracovník repozitára: + [modal] yes=Áno no=Nie @@ -1288,6 +1289,7 @@ config.oauth_enabled=Povolené + monitor.process.cancel=Zrušiť proces monitor.queue.review=Konfigurácia revidovania monitor.queue.review_add=Revidovať/Pridať revidentov diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index f1d1872df3..12d6693bde 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -304,6 +304,7 @@ register_success=Registreringen lyckades + [modal] yes=Ja no=Nej @@ -2033,6 +2034,7 @@ config.disabled_logger=Inaktiverad config.access_log_template=Mall config.xorm_log_sql=Logga SQL + monitor.cron=Cron-jobb monitor.name=Namn monitor.schedule=Schemaläggning diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index dfaefb7d62..d6eb62acaa 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -268,8 +268,11 @@ users=Kullanıcılar organizations=Organizasyonlar search=Ara code=Kod +search.type.tooltip=Arama türü search.fuzzy=Belirsiz +search.fuzzy.tooltip=Arama terimine benzeyen sonuçları da içer search.match=Eşleştir +search.match.tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer code_search_unavailable=Kod arama şu an mevcut değil. Lütfen site yöneticinizle bağlantıya geçin. repo_no_results=Eşleşen bir depo bulunamadı. user_no_results=Eşleşen kullanıcı bulunamadı. @@ -409,6 +412,7 @@ repo.transfer.body=Kabul veya reddetmek için %s ziyaret edin veya görmezden ge repo.collaborator.added.subject=%s sizi %s ekledi repo.collaborator.added.text=Bu depo için katkıcı olarak eklendiniz: + [modal] yes=Evet no=Hayır @@ -507,6 +511,7 @@ activity=Genel Aktivite followers=Takipçiler starred=Yıldızlanmış depolar watched=İzlenen Depolar +code=Kod projects=Projeler following=Takip Edilenler follow=Takip Et @@ -1763,8 +1768,11 @@ activity.git_stats_deletion_n=%d silme oldu search=Ara search.search_repo=Depo ara +search.type.tooltip=Arama türü search.fuzzy=Belirsiz +search.fuzzy.tooltip=Arama terimine benzeyen sonuçları da içer search.match=Eşleştir +search.match.tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer search.results="%s" için %s içinde sonuçları ara search.code_no_results=Arama teriminizle eşleşen bir kaynak kod bulunamadı. search.code_search_unavailable=Kod arama şu an mevcut değil. Lütfen site yöneticisiyle iletişime geçin. @@ -1898,6 +1906,7 @@ settings.confirm_delete=Depoyu Sil settings.add_collaborator=Katkıcı Ekle settings.add_collaborator_success=Katkıcı eklendi. settings.add_collaborator_inactive_user=Etkin olmayan bir kullanıcı katkıcı olarak eklenemez. +settings.add_collaborator_owner=Bir sahip katkıcı olarak eklenemez. settings.add_collaborator_duplicate=Katkıcı bu depoya zaten eklenmiş. settings.delete_collaborator=Sil settings.collaborator_deletion=Katkıcıyı Sil @@ -2309,6 +2318,7 @@ create_org=Organizasyon Oluştur repo_updated=Güncellendi people=İnsanlar teams=Takımlar +code=Kod lower_members=üyeler lower_repositories=depo create_new_team=Yeni Takım @@ -2870,6 +2880,7 @@ config.access_log_template=Şablon config.xorm_log_mode=XORM Günlük Kipi config.xorm_log_sql=SQL Günlüğü + monitor.cron=Cron Görevleri monitor.name=İsim monitor.schedule=Program @@ -3033,6 +3044,9 @@ pin=Pin bildirimi mark_as_read=Okundu olarak işaretle mark_as_unread=Okunmadı olarak işaretle mark_all_as_read=Tümünü okundu olarak işaretle +subscriptions=Abonelikler +watching=İzleniyor +no_subscriptions=Abonelik yok [gpg] default_key=Varsayılan anahtarla imzalanmış diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index f71ef1fa7d..e88e9cf14b 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -356,7 +356,7 @@ issue.action.force_push=%[1]s force-pushed %[2]s з %[3]s в %[4]s issue.action.push_1=@%[1]s надіслав %[3]d коміти %[2]s issue.action.push_n=@%[1]s відправив %[3]d коміти до %[2]s issue.action.close=@%[1]s закрито #%[2]d. -issue.action.reopen=@%[1] заново відкрив #%[2]d. +issue.action.reopen=@%[1]s заново відкрив #%[2]d. issue.action.merge=@%[1]s об'єднав #%[2]d до %[3]s. issue.action.approve=@%[1]s затвердили цей запит на злиття. issue.action.reject=@%[1]s запитують зміни на цей запит на злиття. @@ -382,6 +382,7 @@ repo.transfer.body=Щоб прийняти або відхилити перей repo.collaborator.added.subject=%s додав вас до %s repo.collaborator.added.text=Ви були додані в якості співавтора репозиторію: + [modal] yes=Так no=Ні @@ -2640,6 +2641,7 @@ config.access_log_template=Шаблон config.xorm_log_mode=XORM-режим запису журналу config.xorm_log_sql=Журнал SQL + monitor.cron=Завдання cron monitor.name=Ім'я monitor.schedule=Розклад diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 486c427b05..3e009b5d2b 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -268,8 +268,11 @@ users=用户 organizations=组织 search=搜索 code=代码 +search.type.tooltip=搜索类型 search.fuzzy=模糊 +search.fuzzy.tooltip=包含近似匹配搜索词的结果 search.match=匹配 +search.match.tooltip=仅包含精确匹配搜索词的结果 code_search_unavailable=当前代码搜索不可用。请与网站管理员联系。 repo_no_results=未找到匹配的仓库。 user_no_results=未找到匹配的用户。 @@ -277,6 +280,8 @@ org_no_results=未找到匹配的组织。 code_no_results=未找到与搜索字词匹配的源代码。 code_search_results=“%s” 的搜索结果是 code_last_indexed_at=最后索引于 %s +relevant_repositories_tooltip=派生的仓库,以及缺少主题、图标和描述的仓库将被隐藏。 +relevant_repositories=只显示相关的仓库, 显示未过滤结果。 [auth] @@ -407,6 +412,11 @@ repo.transfer.body=访问 %s 以接受或拒绝转移,亦可忽略此邮件。 repo.collaborator.added.subject=%s 把你添加到了 %s repo.collaborator.added.text=您已被添加为代码库的协作者: +team_invite.subject=%[1]s 邀请您加入组织 %[2]s +team_invite.text_1=%[1]s 邀请您加入组织 %[3]s 中的团队 %[2]s。 +team_invite.text_2=请点击下面的链接加入团队: +team_invite.text_3=注意:这是发送给 %[1]s 的邀请。如果您未曾收到过此类邀请,请忽略这封电子邮件。 + [modal] yes=确认操作 no=取消操作 @@ -482,6 +492,7 @@ user_not_exist=该用户不存在 team_not_exist=团队不存在 last_org_owner=您不能从 "所有者" 团队中删除最后一个用户。组织中必须至少有一个所有者。 cannot_add_org_to_team=组织不能被加入到团队中。 +duplicate_invite_to_team=此用户已被邀请为团队成员。 invalid_ssh_key=无法验证您的 SSH 密钥: %s invalid_gpg_key=无法验证您的 GPG 密钥: %s @@ -505,6 +516,7 @@ activity=公开活动 followers=关注者 starred=已点赞 watched=已关注仓库 +code=代码 projects=项目 following=关注中 follow=关注 @@ -855,8 +867,8 @@ readme_helper_desc=这是您可以为您的项目撰写完整描述的地方。 auto_init=初始化仓库(添加. gitignore、许可证和自述文件) trust_model_helper=选择签名验证的“信任模型”。可能的选项是: trust_model_helper_collaborator=协作者:信任协作者的签名 -trust_model_helper_committer=提交者:信任匹配提交者的签名 -trust_model_helper_collaborator_committer=协作者+提交者:信任与提交者匹配的协作者的签名 +trust_model_helper_committer=提交者:信任与提交者相符的签名 +trust_model_helper_collaborator_committer=协作者+提交者:信任协作者同时是提交者的签名 trust_model_helper_default=默认:使用此安装的默认信任模型 create_repo=创建仓库 default_branch=默认分支 @@ -1037,7 +1049,7 @@ file_view_raw=查看原始文件 file_permalink=永久链接 file_too_large=文件过大,无法显示。 invisible_runes_header=`此文件包含不可见的 Unicode 字符!` -invisible_runes_description=`这个文件包含不可见的 Unicode 字符,其处理方式可能不同于下面显示的字符。 如果您是有意且正当地使用它们,您可以安全地忽略这个警告。使用 Escape 按钮来显示隐藏的字符。 +invisible_runes_description=`这个文件包含不可见的 Unicode 字符,其处理方式可能不同于下面显示的字符。 如果您是有意且正当地使用它们,您可以安全地忽略这个警告。使用 Escape 按钮来显示隐藏的字符。` ambiguous_runes_header=`此行包含模棱两可的 Unicode 字符!` ambiguous_runes_description=`此文件包含模棱两可的 Unicode 字符,这些字符可能会与您当前语言环境的其他字符混淆。 如果您是有意且正当地使用它们,您可以安全地忽略这个警告。使用 Escape 按钮来高亮这些字符。` invisible_runes_line=`此行含有不可见的 unicode 字符` @@ -1229,6 +1241,8 @@ issues.new.add_reviewer_title=请求审核 issues.choose.get_started=开始 issues.choose.blank=默认模板 issues.choose.blank_about=从默认模板创建一个工单。 +issues.choose.ignore_invalid_templates=已忽略无效模板 +issues.choose.invalid_templates=发现了 %v 个无效模板 issues.no_ref=分支/标记未指定 issues.create=创建工单 issues.new_label=创建标签 @@ -1759,8 +1773,11 @@ activity.git_stats_deletion_n=删除 %d 行 search=搜索 search.search_repo=搜索仓库... +search.type.tooltip=搜索类型 search.fuzzy=模糊 +search.fuzzy.tooltip=包含近似匹配搜索词的结果 search.match=匹配 +search.match.tooltip=仅包含精确匹配搜索词的结果 search.results=在 %[3]s 中搜索 "%[1]s" 的结果 search.code_no_results=未找到与搜索字词匹配的源代码。 search.code_search_unavailable=当前代码搜索不可用。请与网站管理员联系。 @@ -1871,13 +1888,13 @@ settings.trust_model.default=默认信任模型 settings.trust_model.default.desc=为此安装使用默认仓库信任模型。 settings.trust_model.collaborator=协作者 settings.trust_model.collaborator.long=协作者:信任协作者的签名 -settings.trust_model.collaborator.desc=此仓库中协作者的有效签名将被标记为“可信” - 不管他们是否是提交者。否则,如果签名匹配了提交者,有效的签名将被标记为“不可信”。 +settings.trust_model.collaborator.desc=此仓库中协作者的有效签名将被标记为「可信」(无论它们是否是提交者),签名只符合提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。 settings.trust_model.committer=提交者 -settings.trust_model.committer.long=提交者: 信任与提交者匹配的签名 (匹配GitHub 并强制Gitea签名的提交者将Gitea作为提交者) -settings.trust_model.committer.desc=有效的签名只有与提交者匹配时才会被标记为“可信”,否则会被标记为“不匹配”。这会强制Gitea成为已签名提交的提交者,而实际提交者在提交中被标记为Co-authored-by: 和Co-committed-by: trailer。默认的Gitea密钥必须与数据库中的一位用户相匹配。 +settings.trust_model.committer.long=提交者: 信任与提交者相符的签名 (此特性类似 GitHub,这会强制采用 Gitea 作为提交者和签名者) +settings.trust_model.committer.desc=提交者的有效签名将被标记为「可信」,否则将被标记为「不匹配」。这会强制 Gitea 成为签名者和提交者,实际的提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Gitea 签名密钥必须匹配数据库中的一个用户密钥。 settings.trust_model.collaboratorcommitter=协作者+提交者 settings.trust_model.collaboratorcommitter.long=协作者+提交者:信任协作者同时是提交者的签名 -settings.trust_model.collaboratorcommitter.desc=如果匹配为提交者,此仓库中协作者的有效签名将被标记为“可信”。否则,如果签名匹配了提交者或者未匹配,有效的签名将被标记为“不可信”。这将强制 Gitea 在签名提交上将实际提交者加上 Co-Authored-By: 和 Co-Committed-By: 。默认的Gitea密钥必须匹配Gitea用户。 +settings.trust_model.collaboratorcommitter.desc=此仓库中协作者的有效签名在他同时是提交者时将被标记为「可信」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这会强制 Gitea 成为签名者和提交者,实际的提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Gitea 签名密钥必须匹配数据库中的一个用户密钥。 settings.wiki_delete=删除百科数据 settings.wiki_delete_desc=删除仓库百科数据是永久性的,无法撤消。 settings.wiki_delete_notices_1=- 这将永久删除和禁用 %s 的百科。 @@ -1894,6 +1911,7 @@ settings.confirm_delete=删除本仓库 settings.add_collaborator=增加协作者 settings.add_collaborator_success=协作者添加成功! settings.add_collaborator_inactive_user=无法添加未激活的用户作为合作者。 +settings.add_collaborator_owner=不能将所有者添加为协作者。 settings.add_collaborator_duplicate=合作者已经被添加到本仓库。 settings.delete_collaborator=删除 settings.collaborator_deletion=删除协作者 @@ -1952,6 +1970,8 @@ settings.event_delete=刪除 settings.event_delete_desc=分支或标签已删除。 settings.event_fork=派生 settings.event_fork_desc=仓库被派生。 +settings.event_wiki=百科 +settings.event_wiki_desc=创建、重命名、编辑或删除了百科页面。 settings.event_release=版本发布 settings.event_release_desc=发布、更新或删除版本时。 settings.event_push=推送 @@ -2303,6 +2323,7 @@ create_org=创建组织 repo_updated=最后更新于 people=组织成员 teams=组织团队 +code=代码 lower_members=名成员 lower_repositories=个仓库 create_new_team=新建团队 @@ -2387,6 +2408,8 @@ teams.members=团队成员 teams.update_settings=更新团队设置 teams.delete_team=删除团队 teams.add_team_member=添加团队成员 +teams.invite_team_member=邀请加入 %s +teams.invite_team_member.list=待处理的邀请 teams.delete_team_title=删除团队 teams.delete_team_desc=删除一个团队将删除团队成员的访问权限,继续? teams.delete_team_success=该团队已被删除。 @@ -2411,6 +2434,9 @@ teams.all_repositories_helper=团队可以访问所有仓库。选择此选项 teams.all_repositories_read_permission_desc=此团队授予读取所有仓库的访问权限: 成员可以查看和克隆仓库。 teams.all_repositories_write_permission_desc=此团队授予修改所有仓库的访问权限: 成员可以查看和推送至仓库。 teams.all_repositories_admin_permission_desc=该团队拥有 管理 所有仓库的权限:团队成员可以读取、克隆、推送以及添加其它仓库协作者。 +teams.invite.title=您已被邀请加入组织 %s 中的团队 %s。 +teams.invite.by=邀请人 %s +teams.invite.description=请点击下面的按钮加入团队。 [admin] dashboard=管理面板 @@ -2864,6 +2890,9 @@ config.access_log_template=模板 config.xorm_log_mode=XORM 日志模式 config.xorm_log_sql=日志 SQL +config.get_setting_failed=获取设置 %s 失败 +config.set_setting_failed=设置 %s 失败 + monitor.cron=Cron 任务 monitor.name=任务名称 monitor.schedule=任务安排 @@ -3027,6 +3056,9 @@ pin=Pin 通知 mark_as_read=标记为已读 mark_as_unread=标记为未读 mark_all_as_read=全部标记为已读 +subscriptions=订阅 +watching=关注 +no_subscriptions=无订阅 [gpg] default_key=使用默认密钥签名 @@ -3086,6 +3118,7 @@ container.details.platform=平台 container.details.repository_site=仓库站点 container.details.documentation_site=文档网站 container.pull=从命令行拉取镜像: +container.digest=摘要: container.documentation=关于 Container 注册中心的更多信息,请参阅 文档。 container.multi_arch=OS / Arch container.layers=镜像层 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 0aa7cff23d..cb241d5ef4 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -124,6 +124,7 @@ register_success=註冊成功 + [modal] yes=確認操作 no=取消操作 @@ -798,6 +799,7 @@ config.git_gc_timeout=GC 操作超時 config.log_config=日誌設定 config.log_mode=日誌模式 + monitor.cron=Cron 任務 monitor.name=任務名稱 monitor.schedule=任務安排 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 55d3456ef4..5827d0b207 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -268,8 +268,11 @@ users=使用者 organizations=組織 search=搜尋 code=程式碼 +search.type.tooltip=搜尋類型 search.fuzzy=模糊 +search.fuzzy.tooltip=包含近似關鍵字的結果 search.match=符合 +search.match.tooltip=只包含完全符合關鍵字的結果 code_search_unavailable=現在無法使用程式碼搜尋。請與網站管理員聯絡。 repo_no_results=沒有找到符合的儲存庫。 user_no_results=沒有找到符合的使用者。 @@ -407,6 +410,7 @@ repo.transfer.body=請造訪 %s 以接受或拒絕轉移,您也可以忽略它 repo.collaborator.added.subject=%s 把您加入到 %s repo.collaborator.added.text=您已被新增為儲存庫的協作者: + [modal] yes=是 no=否 @@ -505,6 +509,7 @@ activity=公開動態 followers=追蹤者 starred=已加星號 watched=關注的儲存庫 +code=程式碼 projects=專案 following=追蹤中 follow=追蹤 @@ -852,7 +857,7 @@ license_helper_desc=授權條款定義了他人使用您原始碼的允許和禁 readme=讀我檔案 readme_helper=選擇讀我檔案範本。 readme_helper_desc=這是您能為專案撰寫完整描述的地方。 -auto_init=初始化儲存庫 (加入 .gitignore、授權條款和讀我檔案) +auto_init=初始化儲存庫 (加入 .gitignore、授權條款、讀我檔案) trust_model_helper=選擇簽署驗證的信任模型。可用的選項: trust_model_helper_collaborator=協作者: 信任協作者的簽署 trust_model_helper_committer=提交者: 信任與提交者相符的簽署 @@ -1134,7 +1139,7 @@ commits.commits=次程式碼提交 commits.no_commits=沒有共同的提交。「%s」和「%s」的歷史完全不同。 commits.nothing_to_compare=這些分支是相同的。 commits.search=搜尋提交歷史... -commits.search.tooltip=你可以用「author:」、「committer:」、「after:」或「before:」作為關鍵詞的前綴,例如:「revert author:Alice before:2019-04-01」。 +commits.search.tooltip=你可以用「author:」、「committer:」、「after:」、「before:」等作為關鍵字的前綴,例如: 「revert author:Alice before:2019-04-01」。 commits.find=搜尋 commits.search_all=所有分支 commits.author=作者 @@ -1229,6 +1234,8 @@ issues.new.add_reviewer_title=請求審核 issues.choose.get_started=開始 issues.choose.blank=預設 issues.choose.blank_about=從預設範本建立問題。 +issues.choose.ignore_invalid_templates=已忽略無效的範本 +issues.choose.invalid_templates=找到了 %v 個無效的範本 issues.no_ref=未指定分支或標籤 issues.create=建立問題 issues.new_label=新增標籤 @@ -1759,8 +1766,11 @@ activity.git_stats_deletion_n=刪除 %d 行 search=搜尋 search.search_repo=搜尋儲存庫 +search.type.tooltip=搜尋類型 search.fuzzy=模糊 +search.fuzzy.tooltip=包含近似關鍵字的結果 search.match=符合 +search.match.tooltip=只包含完全符合關鍵字的結果 search.results=在 %s 中搜尋 "%s" 的结果 search.code_no_results=找不到符合您關鍵字的原始碼。 search.code_search_unavailable=現在無法使用程式碼搜尋。請與網站管理員聯絡。 @@ -1893,7 +1903,8 @@ settings.update_settings_success=已更新儲存庫的設定。 settings.confirm_delete=刪除儲存庫 settings.add_collaborator=增加協作者 settings.add_collaborator_success=成功增加協作者! -settings.add_collaborator_inactive_user=無法加入未啟用的使用者為協作者。 +settings.add_collaborator_inactive_user=無法將未啟用的使用者加入為協作者。 +settings.add_collaborator_owner=無法將擁有者加入為協作者。 settings.add_collaborator_duplicate=此協作者早已被加入此儲存庫。 settings.delete_collaborator=移除 settings.collaborator_deletion=移除協作者 @@ -1952,6 +1963,8 @@ settings.event_delete=刪除 settings.event_delete_desc=刪除分支或標籤。 settings.event_fork=Fork settings.event_fork_desc=儲存庫已被 fork。 +settings.event_wiki=Wiki +settings.event_wiki_desc=建立、重新命名、編輯、刪除 Wiki 頁面。 settings.event_release=版本發布 settings.event_release_desc=在儲存庫中發布、更新或刪除版本。 settings.event_push=推送 @@ -2303,6 +2316,7 @@ create_org=建立組織 repo_updated=更新於 people=成員 teams=團隊 +code=程式碼 lower_members=名成員 lower_repositories=個儲存庫 create_new_team=建立團隊 @@ -2864,6 +2878,7 @@ config.access_log_template=範本 config.xorm_log_mode=XORM 日誌模式 config.xorm_log_sql=記錄 SQL + monitor.cron=Cron 任務 monitor.name=任務名稱 monitor.schedule=任務安排 @@ -3027,6 +3042,9 @@ pin=固定通知 mark_as_read=標記為已讀 mark_as_unread=標記為未讀 mark_all_as_read=標記所有為已讀 +subscriptions=訂閱 +watching=正在關注 +no_subscriptions=沒有訂閱 [gpg] default_key=使用預設金鑰簽署 @@ -3086,6 +3104,7 @@ container.details.platform=平台 container.details.repository_site=儲存庫網站 container.details.documentation_site=文件網站 container.pull=透過下列命令拉取映像檔: +container.digest=摘要: container.documentation=關於 Container registry 的詳情請參閱說明文件。 container.multi_arch=作業系統 / 架構 container.layers=映像檔 Layers @@ -3129,6 +3148,8 @@ rubygems.dependencies.development=開發相依性 rubygems.required.ruby=需要的 Ruby 版本 rubygems.required.rubygems=需要的 RubyGem 版本 rubygems.documentation=關於 RubyGems registry 的詳情請參閱說明文件。 +vagrant.install=執行下列命令以新增 Vagrant box: +vagrant.documentation=關於 Vagrant registry 的詳情請參閱說明文件。 settings.link=連結此套件到儲存庫 settings.link.description=如果您將套件連結到儲存庫,該套件會顯示在儲存庫的套件清單。 settings.link.select=選擇儲存庫 diff --git a/package-lock.json b/package-lock.json index 784adfebe8..69f1ce409b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ }, "devDependencies": { "@playwright/test": "1.27.0", + "@rollup/pluginutils": "5.0.1", "@stoplight/spectral-cli": "6.5.1", "eslint": "8.25.0", "eslint-plugin-import": "2.26.0", @@ -55,33 +56,19 @@ "eslint-plugin-sonarjs": "0.15.0", "eslint-plugin-unicorn": "44.0.2", "eslint-plugin-vue": "9.6.0", - "jest": "29.1.2", - "jest-environment-jsdom": "29.1.2", - "jest-extended": "3.1.0", + "jsdom": "20.0.1", "markdownlint-cli": "0.32.2", "postcss-less": "6.0.0", "stylelint": "14.13.0", "stylelint-config-standard": "28.0.0", "svgo": "2.8.0", - "updates": "13.1.8" + "updates": "13.1.8", + "vitest": "0.24.1" }, "engines": { "node": ">= 14.0.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@asyncapi/specs": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-3.2.1.tgz", @@ -100,216 +87,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.3.tgz", - "integrity": "sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", - "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.3", - "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-module-transforms": "^7.19.0", - "@babel/helpers": "^7.19.0", - "@babel/parser": "^7.19.3", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.3", - "@babel/types": "^7.19.3", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.3.tgz", - "integrity": "sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.19.3", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz", - "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.19.3", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "dev": true, - "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", - "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.18.6", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", - "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", - "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", - "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-validator-identifier": { "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", @@ -319,29 +96,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz", - "integrity": "sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/highlight": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", @@ -438,247 +192,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", - "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.3.tgz", - "integrity": "sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.3", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.3", - "@babel/types": "^7.19.3", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.3.tgz", - "integrity": "sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, "node_modules/@braintree/sanitize-url": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz", @@ -825,406 +338,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.1.2.tgz", - "integrity": "sha512-ujEBCcYs82BTmRxqfHMQggSlkUZP63AE5YEaTPj7eFyJOzukkTorstOUC7L6nE3w5SYadGVAnTsQ/ZjTGL0qYQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.1.2", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.1.2.tgz", - "integrity": "sha512-sCO2Va1gikvQU2ynDN8V4+6wB7iVrD2CvT0zaRst4rglf56yLly0NQ9nuRRAWFeimRf+tCdFsb1Vk1N9LrrMPA==", - "dev": true, - "dependencies": { - "@jest/console": "^29.1.2", - "@jest/reporters": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.0.0", - "jest-config": "^29.1.2", - "jest-haste-map": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-resolve-dependencies": "^29.1.2", - "jest-runner": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", - "jest-watcher": "^29.1.2", - "micromatch": "^4.0.4", - "pretty-format": "^29.1.2", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.1.2.tgz", - "integrity": "sha512-rG7xZ2UeOfvOVzoLIJ0ZmvPl4tBEQ2n73CZJSlzUjPw4or1oSWC0s0Rk0ZX+pIBJ04aVr6hLWFn1DFtrnf8MhQ==", - "dev": true, - "dependencies": { - "@jest/fake-timers": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "jest-mock": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.1.2.tgz", - "integrity": "sha512-FXw/UmaZsyfRyvZw3M6POgSNqwmuOXJuzdNiMWW9LCYo0GRoRDhg+R5iq5higmRTHQY7hx32+j7WHwinRmoILQ==", - "dev": true, - "dependencies": { - "expect": "^29.1.2", - "jest-snapshot": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.1.2.tgz", - "integrity": "sha512-4a48bhKfGj/KAH39u0ppzNTABXQ8QPccWAFUFobWBaEMSMp+sB31Z2fK/l47c4a/Mu1po2ffmfAIPxXbVTXdtg==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.1.2.tgz", - "integrity": "sha512-GppaEqS+QQYegedxVMpCe2xCXxxeYwQ7RsNx55zc8f+1q1qevkZGKequfTASI7ejmg9WwI+SJCrHe9X11bLL9Q==", - "dev": true, - "dependencies": { - "@jest/types": "^29.1.2", - "@sinonjs/fake-timers": "^9.1.2", - "@types/node": "*", - "jest-message-util": "^29.1.2", - "jest-mock": "^29.1.2", - "jest-util": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.1.2.tgz", - "integrity": "sha512-uMgfERpJYoQmykAd0ffyMq8wignN4SvLUG6orJQRe9WAlTRc9cdpCaE/29qurXixYJVZWUqIBXhSk8v5xN1V9g==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.1.2", - "@jest/expect": "^29.1.2", - "@jest/types": "^29.1.2", - "jest-mock": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.1.2.tgz", - "integrity": "sha512-X4fiwwyxy9mnfpxL0g9DD0KcTmEIqP0jUdnc2cfa9riHy+I6Gwwp5vOZiwyg0vZxfSDxrOlK9S4+340W4d+DAA==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", - "@jridgewell/trace-mapping": "^0.3.15", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", - "jest-worker": "^29.1.2", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", - "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.24.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.0.0.tgz", - "integrity": "sha512-nOr+0EM8GiHf34mq2GcJyz/gYFyLQ2INDhAylrZJ9mMWoW21mLBfZa0BUVPPMxVYrLjeiRe2Z7kWXOGnS0TFhQ==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.15", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.1.2.tgz", - "integrity": "sha512-jjYYjjumCJjH9hHCoMhA8PCl1OxNeGgAoZ7yuGYILRJX9NjgzTN0pCT5qAoYR4jfOP8htIByvAlz9vfNSSBoVg==", - "dev": true, - "dependencies": { - "@jest/console": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.1.2.tgz", - "integrity": "sha512-fU6dsUqqm8sA+cd85BmeF7Gu9DsXVWFdGn9taxM6xN1cKdcP/ivSgXh5QucFRFz1oZxKv3/9DYYbq0ULly3P/Q==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.1.2", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.1.2.tgz", - "integrity": "sha512-2uaUuVHTitmkx1tHF+eBjb4p7UuzBG7SXIaA/hNIkaMP6K+gXYGxP38ZcrofzqN0HeZ7A90oqsOa97WU7WZkSw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.1.2", - "@jridgewell/trace-mapping": "^0.3.15", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-util": "^29.1.2", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.1.2.tgz", - "integrity": "sha512-DcXGtoTykQB5jiwCmVr8H4vdg2OJhQex3qPkG+ISyDO7xQXbt/4R6dowcRyPemRnkH7JoHvZuxPBdlq+9JxFCg==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.0.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -1436,7 +549,7 @@ "rollup": "^2.38.3" } }, - "node_modules/@rollup/pluginutils": { + "node_modules/@rollup/plugin-commonjs/node_modules/@rollup/pluginutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", @@ -1453,36 +566,40 @@ "rollup": "^1.20.0||^2.0.0" } }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "node_modules/@rollup/plugin-commonjs/node_modules/@rollup/pluginutils/node_modules/estree-walker": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", "dev": true }, - "node_modules/@sinclair/typebox": { - "version": "0.24.44", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.44.tgz", - "integrity": "sha512-ka0W0KN5i6LfrSocduwliMMpqVgohtPFidKdMEOUjoOFCHcOOYkKsPRxfs5f15oPNHTm6ERAm0GV/+/LTKeiWg==", + "node_modules/@rollup/pluginutils": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.1.tgz", + "integrity": "sha512-4HaCVEXXuObvcPUaUlLt4faHYHCeQOOWNj8NKFGaRSrw3ZLD0TWeAFZicV9vXjnE2nkNuaVTfTuwAnjR+6uc9A==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", "dev": true }, - "node_modules/@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, "node_modules/@stoplight/better-ajv-errors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", @@ -1878,6 +995,29 @@ "rollup": "^2.68.0" } }, + "node_modules/@stoplight/spectral-ruleset-bundler/node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@stoplight/spectral-ruleset-bundler/node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, "node_modules/@stoplight/spectral-ruleset-bundler/node_modules/@stoplight/types": { "version": "13.7.0", "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.7.0.tgz", @@ -2072,45 +1212,19 @@ "node": ">=10.13.0" } }, - "node_modules/@types/babel__core": { - "version": "7.1.19", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", - "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } + "node_modules/@types/chai": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", + "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", + "dev": true }, - "node_modules/@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", "dev": true, "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", - "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.3.0" + "@types/chai": "*" } }, "node_modules/@types/codemirror": { @@ -2153,50 +1267,6 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" }, - "node_modules/@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jsdom": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.0.tgz", - "integrity": "sha512-YfAchFs0yM1QPDrLm2VHe+WHGtqms3NXnXAMolrgrVP6fgBHHXy1ozAbo/dFtPNtZC/m66bPiCTWYmqp1F14gA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -2236,18 +1306,6 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, - "node_modules/@types/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", - "dev": true - }, - "node_modules/@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, "node_modules/@types/tern": { "version": "0.23.4", "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz", @@ -2256,33 +1314,12 @@ "@types/estree": "*" } }, - "node_modules/@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", - "dev": true - }, "node_modules/@types/urijs": { "version": "1.19.19", "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.19.tgz", "integrity": "sha512-FDJNkyhmKLw7uEvTxx5tSXfPeQpO0iy73Ry+PmYZJvQy0QIWX8a7kJ4kLWRf+EbTPJEPDSgPXHaM7pzr5lmvCg==", "dev": true }, - "node_modules/@types/yargs": { - "version": "17.0.13", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", - "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true - }, "node_modules/@vue/compiler-core": { "version": "3.2.40", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.40.tgz", @@ -2715,33 +1752,6 @@ "ajv": "^8.8.2" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2764,19 +1774,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2855,6 +1852,15 @@ "printable-characters": "^1.0.42" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/ast-types": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", @@ -2891,97 +1897,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, - "node_modules/babel-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.1.2.tgz", - "integrity": "sha512-IuG+F3HTHryJb7gacC7SQ59A9kO56BctUsT67uJHp1mMCHUOMXpDwOHWGifWqdWVknN2WNkCVQELPjXx0aLJ9Q==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.1.2", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.0.2", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.0.2.tgz", - "integrity": "sha512-eBr2ynAEFjcebVvu8Ktx580BD1QKCrBG1XwEUTXJe285p9HA/4hOhfWCFRQhTKSyBV0VzjhG7H91Eifz9s29hg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.0.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.0.2.tgz", - "integrity": "sha512-BeVXp7rH5TK96ofyEnHjznjLMQ2nAeDJ+QzxKnHAAMs0RgrQsCywjAN8m4mOm5Di0pxU//3AoEeJJrerMH5UeA==", - "dev": true, - "dependencies": { - "babel-plugin-jest-hoist": "^29.0.2", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3054,15 +1969,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3158,6 +2064,24 @@ } ] }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3173,13 +2097,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/char-regex": { + "node_modules/check-error": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "dev": true, "engines": { - "node": ">=10" + "node": "*" } }, "node_modules/chrome-trace-event": { @@ -3196,12 +2120,6 @@ "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", "dev": true }, - "node_modules/cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", - "dev": true - }, "node_modules/clean-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", @@ -3275,16 +2193,6 @@ "node": ">=0.10.0" } }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, "node_modules/codemirror": { "version": "5.65.9", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz", @@ -3298,12 +2206,6 @@ "typo-js": "*" } }, - "node_modules/collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3362,15 +2264,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "node_modules/convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, "node_modules/copy-anything": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", @@ -4310,11 +3203,17 @@ "integrity": "sha512-F29o+vci4DodHYT9UrR5IEbfBw9pE5eSapIJdTqXK5+6hq+t8VRxwQyKlW2i+KDKFkkJQRvFyI/QXD83h8LyQw==", "dev": true }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } }, "node_modules/deep-extend": { "version": "0.6.0", @@ -4331,15 +3230,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -4500,24 +3390,6 @@ "node": ">= 0.6.0" } }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/diff-sequences": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.0.0.tgz", - "integrity": "sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4654,18 +3526,6 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz", "integrity": "sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ==" }, - "node_modules/emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5680,54 +4540,6 @@ "node": ">=0.8.x" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.1.2.tgz", - "integrity": "sha512-AuAGn1uxva5YBbBlXb+2JPxJRuemZsmlGcapPXWNSBNsQtAULfjioREGBWuI0EOvYUKjDnrCy8PW5Zlr1md5mw==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5792,15 +4604,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "dependencies": { - "bser": "2.1.1" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5968,15 +4771,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5986,6 +4780,15 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -6000,15 +4803,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/get-source": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", @@ -6037,18 +4831,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -6350,12 +5132,6 @@ "node": ">=12" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, "node_modules/html-tags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", @@ -6411,15 +5187,6 @@ "node": ">= 6" } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6696,15 +5463,6 @@ "node": ">=8" } }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6812,18 +5570,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -6890,674 +5636,6 @@ "node": ">=0.10.0" } }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", - "dev": true, - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.1.2.tgz", - "integrity": "sha512-5wEIPpCezgORnqf+rCaYD1SK+mNN7NsstWzIsuvsnrhR/hSxXWd82oI7DkrbJ+XTD28/eG8SmxdGvukrGGK6Tw==", - "dev": true, - "dependencies": { - "@jest/core": "^29.1.2", - "@jest/types": "^29.1.2", - "import-local": "^3.0.2", - "jest-cli": "^29.1.2" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.0.0.tgz", - "integrity": "sha512-28/iDMDrUpGoCitTURuDqUzWQoWmOmOKOFST1mi2lwh62X4BFf6khgH3uSuo1e49X/UDjuApAj3w0wLOex4VPQ==", - "dev": true, - "dependencies": { - "execa": "^5.0.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.1.2.tgz", - "integrity": "sha512-ajQOdxY6mT9GtnfJRZBRYS7toNIJayiiyjDyoZcnvPRUPwJ58JX0ci0PKAKUo2C1RyzlHw0jabjLGKksO42JGA==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.1.2", - "@jest/expect": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.1.2", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", - "p-limit": "^3.1.0", - "pretty-format": "^29.1.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.1.2.tgz", - "integrity": "sha512-vsvBfQ7oS2o4MJdAH+4u9z76Vw5Q8WBQF5MchDbkylNknZdrPTX1Ix7YRJyTlOWqRaS7ue/cEAn+E4V1MWyMzw==", - "dev": true, - "dependencies": { - "@jest/core": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", - "prompts": "^2.0.1", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.1.2.tgz", - "integrity": "sha512-EC3Zi86HJUOz+2YWQcJYQXlf0zuBhJoeyxLM6vb6qJsVmpP7KcCP1JnyF0iaqTaXdBP8Rlwsvs7hnKWQWWLwwA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.1.2", - "@jest/types": "^29.1.2", - "babel-jest": "^29.1.2", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.1.2", - "jest-environment-node": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-runner": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.1.2", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.1.2.tgz", - "integrity": "sha512-4GQts0aUopVvecIT4IwD/7xsBaMhKTYoM4/njE/aVw9wpw+pIUVp8Vab/KnSzSilr84GnLBkaP3JLDnQYCKqVQ==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.0.0", - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.0.0.tgz", - "integrity": "sha512-s5Kpra/kLzbqu9dEjov30kj1n4tfu3e7Pl8v+f8jOkeWNqM6Ds8jRaJfZow3ducoQUrf2Z4rs2N5S3zXnb83gw==", - "dev": true, - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.1.2.tgz", - "integrity": "sha512-AmTQp9b2etNeEwMyr4jc0Ql/LIX/dhbgP21gHAizya2X6rUspHn2gysMXaj6iwWuOJ2sYRgP8c1P4cXswgvS1A==", - "dev": true, - "dependencies": { - "@jest/types": "^29.1.2", - "chalk": "^4.0.0", - "jest-get-type": "^29.0.0", - "jest-util": "^29.1.2", - "pretty-format": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-jsdom": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.1.2.tgz", - "integrity": "sha512-D+XNIKia5+uDjSMwL/G1l6N9MCb7LymKI8FpcLo7kkISjc/Sa9w+dXXEa7u1Wijo3f8sVLqfxdGqYtRhmca+Xw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.1.2", - "@jest/fake-timers": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/jsdom": "^20.0.0", - "@types/node": "*", - "jest-mock": "^29.1.2", - "jest-util": "^29.1.2", - "jsdom": "^20.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.1.2.tgz", - "integrity": "sha512-C59yVbdpY8682u6k/lh8SUMDJPbOyCHOTgLVVi1USWFxtNV+J8fyIwzkg+RJIVI30EKhKiAGNxYaFr3z6eyNhQ==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.1.2", - "@jest/fake-timers": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "jest-mock": "^29.1.2", - "jest-util": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-extended": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-3.1.0.tgz", - "integrity": "sha512-BbuAVUb2dchgwm7euayVt/7hYlkKaknQItKyzie7Li8fmXCglgf21XJeRIdOITZ/cMOTTj5Oh5IjQOxQOe/hfQ==", - "dev": true, - "dependencies": { - "jest-diff": "^29.0.0", - "jest-get-type": "^29.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "jest": ">=27.2.5" - } - }, - "node_modules/jest-get-type": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.0.0.tgz", - "integrity": "sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.1.2.tgz", - "integrity": "sha512-xSjbY8/BF11Jh3hGSPfYTa/qBFrm3TPM7WU8pU93m2gqzORVLkHFWvuZmFsTEBPRKndfewXhMOuzJNHyJIZGsw==", - "dev": true, - "dependencies": { - "@jest/types": "^29.1.2", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.0.0", - "jest-util": "^29.1.2", - "jest-worker": "^29.1.2", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.1.2.tgz", - "integrity": "sha512-TG5gAZJpgmZtjb6oWxBLf2N6CfQ73iwCe6cofu/Uqv9iiAm6g502CAnGtxQaTfpHECBdVEMRBhomSXeLnoKjiQ==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.1.2.tgz", - "integrity": "sha512-MV5XrD3qYSW2zZSHRRceFzqJ39B2z11Qv0KPyZYxnzDHFeYZGJlgGi0SW+IXSJfOewgJp/Km/7lpcFT+cgZypw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.1.2", - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.1.2.tgz", - "integrity": "sha512-9oJ2Os+Qh6IlxLpmvshVbGUiSkZVc2FK+uGOm6tghafnB2RyjKAxMZhtxThRMxfX1J1SOMhTn9oK3/MutRWQJQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.1.2", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.1.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.1.2.tgz", - "integrity": "sha512-PFDAdjjWbjPUtQPkQufvniXIS3N9Tv7tbibePEjIIprzjgo0qQlyUiVMrT4vL8FaSJo1QXifQUOuPH3HQC/aMA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.1.2", - "@types/node": "*", - "jest-util": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.0.0.tgz", - "integrity": "sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.1.2.tgz", - "integrity": "sha512-7fcOr+k7UYSVRJYhSmJHIid3AnDBcLQX3VmT9OSbPWsWz1MfT7bcoerMhADKGvKCoMpOHUQaDHtQoNp/P9JMGg==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.1.2.tgz", - "integrity": "sha512-44yYi+yHqNmH3OoWZvPgmeeiwKxhKV/0CfrzaKLSkZG9gT973PX8i+m8j6pDrTYhhHoiKfF3YUFg/6AeuHw4HQ==", - "dev": true, - "dependencies": { - "jest-regex-util": "^29.0.0", - "jest-snapshot": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.1.2.tgz", - "integrity": "sha512-yy3LEWw8KuBCmg7sCGDIqKwJlULBuNIQa2eFSVgVASWdXbMYZ9H/X0tnXt70XFoGf92W2sOQDOIFAA6f2BG04Q==", - "dev": true, - "dependencies": { - "@jest/console": "^29.1.2", - "@jest/environment": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.0.0", - "jest-environment-node": "^29.1.2", - "jest-haste-map": "^29.1.2", - "jest-leak-detector": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-resolve": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-util": "^29.1.2", - "jest-watcher": "^29.1.2", - "jest-worker": "^29.1.2", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.1.2.tgz", - "integrity": "sha512-jr8VJLIf+cYc+8hbrpt412n5jX3tiXmpPSYTGnwcvNemY+EOuLNiYnHJ3Kp25rkaAcTWOEI4ZdOIQcwYcXIAZw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.1.2", - "@jest/fake-timers": "^29.1.2", - "@jest/globals": "^29.1.2", - "@jest/source-map": "^29.0.0", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-mock": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.1.2.tgz", - "integrity": "sha512-rYFomGpVMdBlfwTYxkUp3sjD6usptvZcONFYNqVlaz4EpHPnDvlWjvmOQ9OCSNKqYZqLM2aS3wq01tWujLg7gg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.1.2", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-haste-map": "^29.1.2", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", - "natural-compare": "^1.4.0", - "pretty-format": "^29.1.2", - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.1.2.tgz", - "integrity": "sha512-vPCk9F353i0Ymx3WQq3+a4lZ07NXu9Ca8wya6o4Fe4/aO1e1awMMprZ3woPFpKwghEOW+UXgd15vVotuNN9ONQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.1.2", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.1.2.tgz", - "integrity": "sha512-k71pOslNlV8fVyI+mEySy2pq9KdXdgZtm7NHrBX8LghJayc3wWZH0Yr0mtYNGaCU4F1OLPXRkwZR0dBm/ClshA==", - "dev": true, - "dependencies": { - "@jest/types": "^29.1.2", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.0.0", - "leven": "^3.1.0", - "pretty-format": "^29.1.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.1.2.tgz", - "integrity": "sha512-6JUIUKVdAvcxC6bM8/dMgqY2N4lbT+jZVsxh0hCJRbwkIEnbr/aPjMQ28fNDI5lB51Klh00MWZZeVf27KBUj5w==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^29.1.2", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.1.2.tgz", - "integrity": "sha512-AdTZJxKjTSPHbXT/AIOjQVmoFx0LHFcVabWu0sxI7PAy7rFf8c0upyvgBKgguVXdM4vY74JdwkyD4hSmpTW8jA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "jest-util": "^29.1.2", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -7660,18 +5738,6 @@ "node": ">= 10.16.0" } }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -7765,15 +5831,6 @@ "node": ">=0.10.0" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/klona": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", @@ -7961,6 +6018,18 @@ "node": ">=8.9.0" } }, + "node_modules/local-pkg": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz", + "integrity": "sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8033,6 +6102,15 @@ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8050,39 +6128,6 @@ "sourcemap-codec": "^1.4.8" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "dependencies": { - "tmpl": "1.0.5" - } - }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -8399,15 +6444,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -8622,12 +6658,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, "node_modules/node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -8663,18 +6693,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -8762,21 +6780,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -9000,6 +7003,15 @@ "node": ">=8" } }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -9025,15 +7037,6 @@ "node": ">=6" } }, - "node_modules/pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -9267,32 +7270,6 @@ "node": ">= 0.8.0" } }, - "node_modules/pretty-format": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.1.2.tgz", - "integrity": "sha512-CGJ6VVGXVRP2o2Dorl4mAwwvDWT25luIsYhkyVQW32E4nL+TgW939J7LlKT/npq5Cpq6j3s+sy+13yk7xYpBmg==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/pretty-ms": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz", @@ -9313,19 +7290,6 @@ "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", "dev": true }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", @@ -9463,12 +7427,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -9721,15 +7679,6 @@ "node": ">=4" } }, - "node_modules/resolve.exports": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", - "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -9997,12 +7946,6 @@ "node": ">=12" } }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -10099,16 +8042,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", @@ -10176,12 +8109,6 @@ "spdx-ranges": "^2.0.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -10189,27 +8116,6 @@ "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", "dev": true }, - "node_modules/stack-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", - "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/stacktracey": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", @@ -10235,19 +8141,6 @@ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "dev": true }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -10300,24 +8193,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -10342,6 +8217,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-0.4.2.tgz", + "integrity": "sha512-pv48ybn4iE1O9RLgCAN0iU4Xv7RlBTiit6DKmMiErbs9x1wH6vXBs45tWc0H5wUIF6TLTrKweqkmYF/iraQKNw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/style-search": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", @@ -10556,22 +8443,6 @@ "node": ">=6" } }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/terser": { "version": "5.15.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", @@ -10708,26 +8579,36 @@ "source-map": "^0.6.0" } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tinybench": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.0.tgz", + "integrity": "sha512-zs1gMVBwyyG2QbVchYIbnabRhMOCGvrwZz/q+SV+LIMa9q5YDQZi2kkI6ZRqV2Bz7ba1uvrc7ieUoE4KWnGeKg==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.0.tgz", + "integrity": "sha512-NX5KeqHOBZU6Bc0xj9Vr5Szbb1j8tUHIeD18s41aDJaPeC5QTdEhK0SpdpUrZlj2nv5cctNcSjaKNanXlfcVEQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.0.2.tgz", + "integrity": "sha512-bSGlgwLBYf7PnUsQ6WOc6SJ3pGOcd+d8AA6EUnLDDM0kWEstC1JIlSZA3UNliDXhd9ABoS7hiRBDCu+XP/sf1Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tippy.js": { "version": "6.3.7", "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", @@ -10736,21 +8617,6 @@ "@popperjs/core": "^2.9.0" } }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11022,20 +8888,6 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, - "node_modules/v8-to-istanbul": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", - "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -11055,6 +8907,114 @@ "builtins": "^1.0.3" } }, + "node_modules/vite": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.1.8.tgz", + "integrity": "sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg==", + "dev": true, + "dependencies": { + "esbuild": "^0.15.9", + "postcss": "^8.4.16", + "resolve": "^1.22.1", + "rollup": "~2.78.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "2.78.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.1.tgz", + "integrity": "sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vitest": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.24.1.tgz", + "integrity": "sha512-NKkK1xnDIOOr42pKBfGQQl6b6IWdFVBpG6ZS1T+nUlJuqcOiZ7lxjVwHy9wrtTYpJ0BWww9y6bSGYXubD29Nag==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.3", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "chai": "^4.3.6", + "debug": "^4.3.4", + "local-pkg": "^0.4.2", + "strip-literal": "^0.4.2", + "tinybench": "^2.3.0", + "tinypool": "^0.3.0", + "tinyspy": "^1.0.2", + "vite": "^3.0.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.16.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/vm2": { "version": "3.9.11", "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.11.tgz", @@ -11155,15 +9115,6 @@ "node": ">=12" } }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -11760,16 +9711,6 @@ } }, "dependencies": { - "@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, "@asyncapi/specs": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-3.2.1.tgz", @@ -11785,190 +9726,12 @@ "@babel/highlight": "^7.18.6" } }, - "@babel/compat-data": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.3.tgz", - "integrity": "sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==", - "dev": true - }, - "@babel/core": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", - "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.3", - "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-module-transforms": "^7.19.0", - "@babel/helpers": "^7.19.0", - "@babel/parser": "^7.19.3", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.3", - "@babel/types": "^7.19.3", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.3.tgz", - "integrity": "sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==", - "dev": true, - "requires": { - "@babel/types": "^7.19.3", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" - }, - "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - } - } - }, - "@babel/helper-compilation-targets": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz", - "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.19.3", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true - }, - "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "dev": true, - "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-module-transforms": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", - "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.18.6", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", - "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", - "dev": true - }, - "@babel/helper-simple-access": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", - "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-string-parser": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", - "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", - "dev": true - }, "@babel/helper-validator-identifier": { "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", "dev": true }, - "@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", - "dev": true - }, - "@babel/helpers": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz", - "integrity": "sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==", - "dev": true, - "requires": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" - } - }, "@babel/highlight": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", @@ -12043,186 +9806,6 @@ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.3.tgz", "integrity": "sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==" }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-typescript": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", - "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" - } - }, - "@babel/traverse": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.3.tgz", - "integrity": "sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.3", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.3", - "@babel/types": "^7.19.3", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "dependencies": { - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - } - } - }, - "@babel/types": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.3.tgz", - "integrity": "sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, "@braintree/sanitize-url": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz", @@ -12318,320 +9901,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "dependencies": { - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/console": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.1.2.tgz", - "integrity": "sha512-ujEBCcYs82BTmRxqfHMQggSlkUZP63AE5YEaTPj7eFyJOzukkTorstOUC7L6nE3w5SYadGVAnTsQ/ZjTGL0qYQ==", - "dev": true, - "requires": { - "@jest/types": "^29.1.2", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", - "slash": "^3.0.0" - } - }, - "@jest/core": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.1.2.tgz", - "integrity": "sha512-sCO2Va1gikvQU2ynDN8V4+6wB7iVrD2CvT0zaRst4rglf56yLly0NQ9nuRRAWFeimRf+tCdFsb1Vk1N9LrrMPA==", - "dev": true, - "requires": { - "@jest/console": "^29.1.2", - "@jest/reporters": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.0.0", - "jest-config": "^29.1.2", - "jest-haste-map": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-resolve-dependencies": "^29.1.2", - "jest-runner": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", - "jest-watcher": "^29.1.2", - "micromatch": "^4.0.4", - "pretty-format": "^29.1.2", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "@jest/environment": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.1.2.tgz", - "integrity": "sha512-rG7xZ2UeOfvOVzoLIJ0ZmvPl4tBEQ2n73CZJSlzUjPw4or1oSWC0s0Rk0ZX+pIBJ04aVr6hLWFn1DFtrnf8MhQ==", - "dev": true, - "requires": { - "@jest/fake-timers": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "jest-mock": "^29.1.2" - } - }, - "@jest/expect": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.1.2.tgz", - "integrity": "sha512-FXw/UmaZsyfRyvZw3M6POgSNqwmuOXJuzdNiMWW9LCYo0GRoRDhg+R5iq5higmRTHQY7hx32+j7WHwinRmoILQ==", - "dev": true, - "requires": { - "expect": "^29.1.2", - "jest-snapshot": "^29.1.2" - } - }, - "@jest/expect-utils": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.1.2.tgz", - "integrity": "sha512-4a48bhKfGj/KAH39u0ppzNTABXQ8QPccWAFUFobWBaEMSMp+sB31Z2fK/l47c4a/Mu1po2ffmfAIPxXbVTXdtg==", - "dev": true, - "requires": { - "jest-get-type": "^29.0.0" - } - }, - "@jest/fake-timers": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.1.2.tgz", - "integrity": "sha512-GppaEqS+QQYegedxVMpCe2xCXxxeYwQ7RsNx55zc8f+1q1qevkZGKequfTASI7ejmg9WwI+SJCrHe9X11bLL9Q==", - "dev": true, - "requires": { - "@jest/types": "^29.1.2", - "@sinonjs/fake-timers": "^9.1.2", - "@types/node": "*", - "jest-message-util": "^29.1.2", - "jest-mock": "^29.1.2", - "jest-util": "^29.1.2" - } - }, - "@jest/globals": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.1.2.tgz", - "integrity": "sha512-uMgfERpJYoQmykAd0ffyMq8wignN4SvLUG6orJQRe9WAlTRc9cdpCaE/29qurXixYJVZWUqIBXhSk8v5xN1V9g==", - "dev": true, - "requires": { - "@jest/environment": "^29.1.2", - "@jest/expect": "^29.1.2", - "@jest/types": "^29.1.2", - "jest-mock": "^29.1.2" - } - }, - "@jest/reporters": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.1.2.tgz", - "integrity": "sha512-X4fiwwyxy9mnfpxL0g9DD0KcTmEIqP0jUdnc2cfa9riHy+I6Gwwp5vOZiwyg0vZxfSDxrOlK9S4+340W4d+DAA==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", - "@jridgewell/trace-mapping": "^0.3.15", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", - "jest-worker": "^29.1.2", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^9.0.1" - } - }, - "@jest/schemas": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", - "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.24.1" - } - }, - "@jest/source-map": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.0.0.tgz", - "integrity": "sha512-nOr+0EM8GiHf34mq2GcJyz/gYFyLQ2INDhAylrZJ9mMWoW21mLBfZa0BUVPPMxVYrLjeiRe2Z7kWXOGnS0TFhQ==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.15", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - } - }, - "@jest/test-result": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.1.2.tgz", - "integrity": "sha512-jjYYjjumCJjH9hHCoMhA8PCl1OxNeGgAoZ7yuGYILRJX9NjgzTN0pCT5qAoYR4jfOP8htIByvAlz9vfNSSBoVg==", - "dev": true, - "requires": { - "@jest/console": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.1.2.tgz", - "integrity": "sha512-fU6dsUqqm8sA+cd85BmeF7Gu9DsXVWFdGn9taxM6xN1cKdcP/ivSgXh5QucFRFz1oZxKv3/9DYYbq0ULly3P/Q==", - "dev": true, - "requires": { - "@jest/test-result": "^29.1.2", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "slash": "^3.0.0" - } - }, - "@jest/transform": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.1.2.tgz", - "integrity": "sha512-2uaUuVHTitmkx1tHF+eBjb4p7UuzBG7SXIaA/hNIkaMP6K+gXYGxP38ZcrofzqN0HeZ7A90oqsOa97WU7WZkSw==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.1.2", - "@jridgewell/trace-mapping": "^0.3.15", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-util": "^29.1.2", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" - } - }, - "@jest/types": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.1.2.tgz", - "integrity": "sha512-DcXGtoTykQB5jiwCmVr8H4vdg2OJhQex3qPkG+ISyDO7xQXbt/4R6dowcRyPemRnkH7JoHvZuxPBdlq+9JxFCg==", - "dev": true, - "requires": { - "@jest/schemas": "^29.0.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -12763,49 +10032,46 @@ "is-reference": "^1.2.1", "magic-string": "^0.25.7", "resolve": "^1.17.0" - } - }, - "@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dev": true, - "requires": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" }, "dependencies": { - "estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + } + } } } }, - "@sinclair/typebox": { - "version": "0.24.44", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.44.tgz", - "integrity": "sha512-ka0W0KN5i6LfrSocduwliMMpqVgohtPFidKdMEOUjoOFCHcOOYkKsPRxfs5f15oPNHTm6ERAm0GV/+/LTKeiWg==", - "dev": true - }, - "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "@rollup/pluginutils": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.1.tgz", + "integrity": "sha512-4HaCVEXXuObvcPUaUlLt4faHYHCeQOOWNj8NKFGaRSrw3ZLD0TWeAFZicV9vXjnE2nkNuaVTfTuwAnjR+6uc9A==", "dev": true, "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "dependencies": { + "@types/estree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", + "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", + "dev": true + } } }, "@stoplight/better-ajv-errors": { @@ -13138,6 +10404,25 @@ "resolve": "^1.17.0" } }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + } + } + }, "@stoplight/types": { "version": "13.7.0", "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.7.0.tgz", @@ -13304,45 +10589,19 @@ "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "dev": true }, - "@types/babel__core": { - "version": "7.1.19", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", - "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } + "@types/chai": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", + "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", + "dev": true }, - "@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", "dev": true, "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", - "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", - "dev": true, - "requires": { - "@babel/types": "^7.3.0" + "@types/chai": "*" } }, "@types/codemirror": { @@ -13385,50 +10644,6 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" }, - "@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, - "@types/jsdom": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.0.tgz", - "integrity": "sha512-YfAchFs0yM1QPDrLm2VHe+WHGtqms3NXnXAMolrgrVP6fgBHHXy1ozAbo/dFtPNtZC/m66bPiCTWYmqp1F14gA==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -13468,18 +10683,6 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, - "@types/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", - "dev": true - }, - "@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, "@types/tern": { "version": "0.23.4", "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz", @@ -13488,33 +10691,12 @@ "@types/estree": "*" } }, - "@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", - "dev": true - }, "@types/urijs": { "version": "1.19.19", "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.19.tgz", "integrity": "sha512-FDJNkyhmKLw7uEvTxx5tSXfPeQpO0iy73Ry+PmYZJvQy0QIWX8a7kJ4kLWRf+EbTPJEPDSgPXHaM7pzr5lmvCg==", "dev": true }, - "@types/yargs": { - "version": "17.0.13", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", - "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true - }, "@vue/compiler-core": { "version": "3.2.40", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.40.tgz", @@ -13880,23 +11062,6 @@ "fast-deep-equal": "^3.1.3" } }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - } - } - }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -13910,16 +11075,6 @@ "color-convert": "^2.0.1" } }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -13977,6 +11132,12 @@ "printable-characters": "^1.0.42" } }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "ast-types": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.14.2.tgz", @@ -14004,76 +11165,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, - "babel-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.1.2.tgz", - "integrity": "sha512-IuG+F3HTHryJb7gacC7SQ59A9kO56BctUsT67uJHp1mMCHUOMXpDwOHWGifWqdWVknN2WNkCVQELPjXx0aLJ9Q==", - "dev": true, - "requires": { - "@jest/transform": "^29.1.2", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.0.2", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - } - }, - "babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "29.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.0.2.tgz", - "integrity": "sha512-eBr2ynAEFjcebVvu8Ktx580BD1QKCrBG1XwEUTXJe285p9HA/4hOhfWCFRQhTKSyBV0VzjhG7H91Eifz9s29hg==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "29.0.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.0.2.tgz", - "integrity": "sha512-BeVXp7rH5TK96ofyEnHjznjLMQ2nAeDJ+QzxKnHAAMs0RgrQsCywjAN8m4mOm5Di0pxU//3AoEeJJrerMH5UeA==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^29.0.2", - "babel-preset-current-node-syntax": "^1.0.0" - } - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -14124,15 +11215,6 @@ "update-browserslist-db": "^1.0.9" } }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -14194,6 +11276,21 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz", "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==" }, + "chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -14203,10 +11300,10 @@ "supports-color": "^7.1.0" } }, - "char-regex": { + "check-error": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "dev": true }, "chrome-trace-event": { @@ -14220,12 +11317,6 @@ "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", "dev": true }, - "cjs-module-lexer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", - "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", - "dev": true - }, "clean-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", @@ -14287,12 +11378,6 @@ } } }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true - }, "codemirror": { "version": "5.65.9", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz", @@ -14306,12 +11391,6 @@ "typo-js": "*" } }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", - "dev": true - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -14361,15 +11440,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, "copy-anything": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", @@ -15124,11 +12194,14 @@ "integrity": "sha512-F29o+vci4DodHYT9UrR5IEbfBw9pE5eSapIJdTqXK5+6hq+t8VRxwQyKlW2i+KDKFkkJQRvFyI/QXD83h8LyQw==", "dev": true }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", - "dev": true + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } }, "deep-extend": { "version": "0.6.0", @@ -15142,12 +12215,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, "define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -15265,18 +12332,6 @@ "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", "dev": true }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "diff-sequences": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.0.0.tgz", - "integrity": "sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==", - "dev": true - }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -15385,12 +12440,6 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.276.tgz", "integrity": "sha512-EpuHPqu8YhonqLBXHoU6hDJCD98FCe6KDoet3/gY1qsQ6usjJoHqBH2YIVs8FXaAtHwVL8Uqa/fsYao/vq9VWQ==" }, - "emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", - "dev": true - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -16048,42 +13097,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true - }, - "expect": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.1.2.tgz", - "integrity": "sha512-AuAGn1uxva5YBbBlXb+2JPxJRuemZsmlGcapPXWNSBNsQtAULfjioREGBWuI0EOvYUKjDnrCy8PW5Zlr1md5mw==", - "dev": true, - "requires": { - "@jest/expect-utils": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2" - } - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -16141,15 +13154,6 @@ "reusify": "^1.0.4" } }, - "fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -16271,18 +13275,18 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true + }, "get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -16294,12 +13298,6 @@ "has-symbols": "^1.0.3" } }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, "get-source": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", @@ -16324,12 +13322,6 @@ "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", "dev": true }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, "get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -16557,12 +13549,6 @@ "whatwg-encoding": "^2.0.0" } }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, "html-tags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", @@ -16603,12 +13589,6 @@ "debug": "4" } }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true - }, "iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -16792,12 +13772,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -16872,12 +13846,6 @@ "call-bind": "^1.0.2" } }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, "is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -16926,520 +13894,6 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" }, - "istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - } - }, - "istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.1.2.tgz", - "integrity": "sha512-5wEIPpCezgORnqf+rCaYD1SK+mNN7NsstWzIsuvsnrhR/hSxXWd82oI7DkrbJ+XTD28/eG8SmxdGvukrGGK6Tw==", - "dev": true, - "requires": { - "@jest/core": "^29.1.2", - "@jest/types": "^29.1.2", - "import-local": "^3.0.2", - "jest-cli": "^29.1.2" - } - }, - "jest-changed-files": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.0.0.tgz", - "integrity": "sha512-28/iDMDrUpGoCitTURuDqUzWQoWmOmOKOFST1mi2lwh62X4BFf6khgH3uSuo1e49X/UDjuApAj3w0wLOex4VPQ==", - "dev": true, - "requires": { - "execa": "^5.0.0", - "p-limit": "^3.1.0" - } - }, - "jest-circus": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.1.2.tgz", - "integrity": "sha512-ajQOdxY6mT9GtnfJRZBRYS7toNIJayiiyjDyoZcnvPRUPwJ58JX0ci0PKAKUo2C1RyzlHw0jabjLGKksO42JGA==", - "dev": true, - "requires": { - "@jest/environment": "^29.1.2", - "@jest/expect": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.1.2", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", - "p-limit": "^3.1.0", - "pretty-format": "^29.1.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - } - }, - "jest-cli": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.1.2.tgz", - "integrity": "sha512-vsvBfQ7oS2o4MJdAH+4u9z76Vw5Q8WBQF5MchDbkylNknZdrPTX1Ix7YRJyTlOWqRaS7ue/cEAn+E4V1MWyMzw==", - "dev": true, - "requires": { - "@jest/core": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", - "prompts": "^2.0.1", - "yargs": "^17.3.1" - } - }, - "jest-config": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.1.2.tgz", - "integrity": "sha512-EC3Zi86HJUOz+2YWQcJYQXlf0zuBhJoeyxLM6vb6qJsVmpP7KcCP1JnyF0iaqTaXdBP8Rlwsvs7hnKWQWWLwwA==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.1.2", - "@jest/types": "^29.1.2", - "babel-jest": "^29.1.2", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.1.2", - "jest-environment-node": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-runner": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.1.2", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - } - }, - "jest-diff": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.1.2.tgz", - "integrity": "sha512-4GQts0aUopVvecIT4IwD/7xsBaMhKTYoM4/njE/aVw9wpw+pIUVp8Vab/KnSzSilr84GnLBkaP3JLDnQYCKqVQ==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^29.0.0", - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" - } - }, - "jest-docblock": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.0.0.tgz", - "integrity": "sha512-s5Kpra/kLzbqu9dEjov30kj1n4tfu3e7Pl8v+f8jOkeWNqM6Ds8jRaJfZow3ducoQUrf2Z4rs2N5S3zXnb83gw==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.1.2.tgz", - "integrity": "sha512-AmTQp9b2etNeEwMyr4jc0Ql/LIX/dhbgP21gHAizya2X6rUspHn2gysMXaj6iwWuOJ2sYRgP8c1P4cXswgvS1A==", - "dev": true, - "requires": { - "@jest/types": "^29.1.2", - "chalk": "^4.0.0", - "jest-get-type": "^29.0.0", - "jest-util": "^29.1.2", - "pretty-format": "^29.1.2" - } - }, - "jest-environment-jsdom": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.1.2.tgz", - "integrity": "sha512-D+XNIKia5+uDjSMwL/G1l6N9MCb7LymKI8FpcLo7kkISjc/Sa9w+dXXEa7u1Wijo3f8sVLqfxdGqYtRhmca+Xw==", - "dev": true, - "requires": { - "@jest/environment": "^29.1.2", - "@jest/fake-timers": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/jsdom": "^20.0.0", - "@types/node": "*", - "jest-mock": "^29.1.2", - "jest-util": "^29.1.2", - "jsdom": "^20.0.0" - } - }, - "jest-environment-node": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.1.2.tgz", - "integrity": "sha512-C59yVbdpY8682u6k/lh8SUMDJPbOyCHOTgLVVi1USWFxtNV+J8fyIwzkg+RJIVI30EKhKiAGNxYaFr3z6eyNhQ==", - "dev": true, - "requires": { - "@jest/environment": "^29.1.2", - "@jest/fake-timers": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "jest-mock": "^29.1.2", - "jest-util": "^29.1.2" - } - }, - "jest-extended": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-3.1.0.tgz", - "integrity": "sha512-BbuAVUb2dchgwm7euayVt/7hYlkKaknQItKyzie7Li8fmXCglgf21XJeRIdOITZ/cMOTTj5Oh5IjQOxQOe/hfQ==", - "dev": true, - "requires": { - "jest-diff": "^29.0.0", - "jest-get-type": "^29.0.0" - } - }, - "jest-get-type": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.0.0.tgz", - "integrity": "sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==", - "dev": true - }, - "jest-haste-map": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.1.2.tgz", - "integrity": "sha512-xSjbY8/BF11Jh3hGSPfYTa/qBFrm3TPM7WU8pU93m2gqzORVLkHFWvuZmFsTEBPRKndfewXhMOuzJNHyJIZGsw==", - "dev": true, - "requires": { - "@jest/types": "^29.1.2", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.0.0", - "jest-util": "^29.1.2", - "jest-worker": "^29.1.2", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - } - }, - "jest-leak-detector": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.1.2.tgz", - "integrity": "sha512-TG5gAZJpgmZtjb6oWxBLf2N6CfQ73iwCe6cofu/Uqv9iiAm6g502CAnGtxQaTfpHECBdVEMRBhomSXeLnoKjiQ==", - "dev": true, - "requires": { - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" - } - }, - "jest-matcher-utils": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.1.2.tgz", - "integrity": "sha512-MV5XrD3qYSW2zZSHRRceFzqJ39B2z11Qv0KPyZYxnzDHFeYZGJlgGi0SW+IXSJfOewgJp/Km/7lpcFT+cgZypw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^29.1.2", - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" - } - }, - "jest-message-util": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.1.2.tgz", - "integrity": "sha512-9oJ2Os+Qh6IlxLpmvshVbGUiSkZVc2FK+uGOm6tghafnB2RyjKAxMZhtxThRMxfX1J1SOMhTn9oK3/MutRWQJQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.1.2", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.1.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - } - }, - "jest-mock": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.1.2.tgz", - "integrity": "sha512-PFDAdjjWbjPUtQPkQufvniXIS3N9Tv7tbibePEjIIprzjgo0qQlyUiVMrT4vL8FaSJo1QXifQUOuPH3HQC/aMA==", - "dev": true, - "requires": { - "@jest/types": "^29.1.2", - "@types/node": "*", - "jest-util": "^29.1.2" - } - }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "requires": {} - }, - "jest-regex-util": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.0.0.tgz", - "integrity": "sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug==", - "dev": true - }, - "jest-resolve": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.1.2.tgz", - "integrity": "sha512-7fcOr+k7UYSVRJYhSmJHIid3AnDBcLQX3VmT9OSbPWsWz1MfT7bcoerMhADKGvKCoMpOHUQaDHtQoNp/P9JMGg==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - } - }, - "jest-resolve-dependencies": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.1.2.tgz", - "integrity": "sha512-44yYi+yHqNmH3OoWZvPgmeeiwKxhKV/0CfrzaKLSkZG9gT973PX8i+m8j6pDrTYhhHoiKfF3YUFg/6AeuHw4HQ==", - "dev": true, - "requires": { - "jest-regex-util": "^29.0.0", - "jest-snapshot": "^29.1.2" - } - }, - "jest-runner": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.1.2.tgz", - "integrity": "sha512-yy3LEWw8KuBCmg7sCGDIqKwJlULBuNIQa2eFSVgVASWdXbMYZ9H/X0tnXt70XFoGf92W2sOQDOIFAA6f2BG04Q==", - "dev": true, - "requires": { - "@jest/console": "^29.1.2", - "@jest/environment": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.0.0", - "jest-environment-node": "^29.1.2", - "jest-haste-map": "^29.1.2", - "jest-leak-detector": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-resolve": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-util": "^29.1.2", - "jest-watcher": "^29.1.2", - "jest-worker": "^29.1.2", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - } - }, - "jest-runtime": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.1.2.tgz", - "integrity": "sha512-jr8VJLIf+cYc+8hbrpt412n5jX3tiXmpPSYTGnwcvNemY+EOuLNiYnHJ3Kp25rkaAcTWOEI4ZdOIQcwYcXIAZw==", - "dev": true, - "requires": { - "@jest/environment": "^29.1.2", - "@jest/fake-timers": "^29.1.2", - "@jest/globals": "^29.1.2", - "@jest/source-map": "^29.0.0", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-mock": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - } - }, - "jest-snapshot": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.1.2.tgz", - "integrity": "sha512-rYFomGpVMdBlfwTYxkUp3sjD6usptvZcONFYNqVlaz4EpHPnDvlWjvmOQ9OCSNKqYZqLM2aS3wq01tWujLg7gg==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.1.2", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-haste-map": "^29.1.2", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", - "natural-compare": "^1.4.0", - "pretty-format": "^29.1.2", - "semver": "^7.3.5" - } - }, - "jest-util": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.1.2.tgz", - "integrity": "sha512-vPCk9F353i0Ymx3WQq3+a4lZ07NXu9Ca8wya6o4Fe4/aO1e1awMMprZ3woPFpKwghEOW+UXgd15vVotuNN9ONQ==", - "dev": true, - "requires": { - "@jest/types": "^29.1.2", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - } - }, - "jest-validate": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.1.2.tgz", - "integrity": "sha512-k71pOslNlV8fVyI+mEySy2pq9KdXdgZtm7NHrBX8LghJayc3wWZH0Yr0mtYNGaCU4F1OLPXRkwZR0dBm/ClshA==", - "dev": true, - "requires": { - "@jest/types": "^29.1.2", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.0.0", - "leven": "^3.1.0", - "pretty-format": "^29.1.2" - }, - "dependencies": { - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - } - } - }, - "jest-watcher": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.1.2.tgz", - "integrity": "sha512-6JUIUKVdAvcxC6bM8/dMgqY2N4lbT+jZVsxh0hCJRbwkIEnbr/aPjMQ28fNDI5lB51Klh00MWZZeVf27KBUj5w==", - "dev": true, - "requires": { - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^29.1.2", - "string-length": "^4.0.1" - } - }, - "jest-worker": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.1.2.tgz", - "integrity": "sha512-AdTZJxKjTSPHbXT/AIOjQVmoFx0LHFcVabWu0sxI7PAy7rFf8c0upyvgBKgguVXdM4vY74JdwkyD4hSmpTW8jA==", - "dev": true, - "requires": { - "@types/node": "*", - "jest-util": "^29.1.2", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -17519,12 +13973,6 @@ "integrity": "sha512-NFbZTr1t13fPKw53swmZFKwBkEDWDnno7uLJk+a+Rw9tGDTkGgnGdZJ8A/o3gR1+XaAXmSsbpfIBIBgqRBZWDA==", "dev": true }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -17596,12 +14044,6 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - }, "klona": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", @@ -17744,6 +14186,12 @@ "json5": "^2.1.2" } }, + "local-pkg": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.2.tgz", + "integrity": "sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==", + "dev": true + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -17810,6 +14258,15 @@ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true }, + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -17827,32 +14284,6 @@ "sourcemap-codec": "^1.4.8" } }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "requires": { - "tmpl": "1.0.5" - } - }, "map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -18091,12 +14522,6 @@ "mime-db": "1.52.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -18260,12 +14685,6 @@ } } }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, "node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -18297,15 +14716,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, "nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -18369,15 +14779,6 @@ "wrappy": "1" } }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -18536,6 +14937,12 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -18552,12 +14959,6 @@ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "optional": true }, - "pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true - }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -18707,25 +15108,6 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "pretty-format": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.1.2.tgz", - "integrity": "sha512-CGJ6VVGXVRP2o2Dorl4mAwwvDWT25luIsYhkyVQW32E4nL+TgW939J7LlKT/npq5Cpq6j3s+sy+13yk7xYpBmg==", - "dev": true, - "requires": { - "@jest/schemas": "^29.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, "pretty-ms": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz", @@ -18740,16 +15122,6 @@ "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", "dev": true }, - "prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, "proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", @@ -18856,12 +15228,6 @@ } } }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -19045,12 +15411,6 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "resolve.exports": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", - "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", - "dev": true - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -19250,12 +15610,6 @@ "jsep": "^1.1.2" } }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -19328,16 +15682,6 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" }, - "source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", @@ -19405,35 +15749,12 @@ "spdx-ranges": "^2.0.0" } }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, "stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "dev": true }, - "stack-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", - "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - } - } - }, "stacktracey": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", @@ -19456,16 +15777,6 @@ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "dev": true }, - "string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "requires": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - } - }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -19506,18 +15817,6 @@ "ansi-regex": "^5.0.1" } }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, "strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -19533,6 +15832,15 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "strip-literal": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-0.4.2.tgz", + "integrity": "sha512-pv48ybn4iE1O9RLgCAN0iU4Xv7RlBTiit6DKmMiErbs9x1wH6vXBs45tWc0H5wUIF6TLTrKweqkmYF/iraQKNw==", + "dev": true, + "requires": { + "acorn": "^8.8.0" + } + }, "style-search": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", @@ -19706,16 +16014,6 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - } - }, "terser": { "version": "5.15.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", @@ -19807,23 +16105,30 @@ } } }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - } - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "tinybench": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.3.0.tgz", + "integrity": "sha512-zs1gMVBwyyG2QbVchYIbnabRhMOCGvrwZz/q+SV+LIMa9q5YDQZi2kkI6ZRqV2Bz7ba1uvrc7ieUoE4KWnGeKg==", + "dev": true + }, + "tinypool": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.3.0.tgz", + "integrity": "sha512-NX5KeqHOBZU6Bc0xj9Vr5Szbb1j8tUHIeD18s41aDJaPeC5QTdEhK0SpdpUrZlj2nv5cctNcSjaKNanXlfcVEQ==", + "dev": true + }, + "tinyspy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-1.0.2.tgz", + "integrity": "sha512-bSGlgwLBYf7PnUsQ6WOc6SJ3pGOcd+d8AA6EUnLDDM0kWEstC1JIlSZA3UNliDXhd9ABoS7hiRBDCu+XP/sf1Q==", + "dev": true + }, "tippy.js": { "version": "6.3.7", "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", @@ -19832,18 +16137,6 @@ "@popperjs/core": "^2.9.0" } }, - "tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -20049,17 +16342,6 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, - "v8-to-istanbul": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", - "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - } - }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -20079,6 +16361,49 @@ "builtins": "^1.0.3" } }, + "vite": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-3.1.8.tgz", + "integrity": "sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg==", + "dev": true, + "requires": { + "esbuild": "^0.15.9", + "fsevents": "~2.3.2", + "postcss": "^8.4.16", + "resolve": "^1.22.1", + "rollup": "~2.78.0" + }, + "dependencies": { + "rollup": { + "version": "2.78.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.78.1.tgz", + "integrity": "sha512-VeeCgtGi4P+o9hIg+xz4qQpRl6R401LWEXBmxYKOV4zlF82lyhgh2hTZnheFUbANE8l2A41F458iwj2vEYaXJg==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + } + } + }, + "vitest": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.24.1.tgz", + "integrity": "sha512-NKkK1xnDIOOr42pKBfGQQl6b6IWdFVBpG6ZS1T+nUlJuqcOiZ7lxjVwHy9wrtTYpJ0BWww9y6bSGYXubD29Nag==", + "dev": true, + "requires": { + "@types/chai": "^4.3.3", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "chai": "^4.3.6", + "debug": "^4.3.4", + "local-pkg": "^0.4.2", + "strip-literal": "^0.4.2", + "tinybench": "^2.3.0", + "tinypool": "^0.3.0", + "tinyspy": "^1.0.2", + "vite": "^3.0.0" + } + }, "vm2": { "version": "3.9.11", "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.11.tgz", @@ -20152,15 +16477,6 @@ "xml-name-validator": "^4.0.0" } }, - "walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "requires": { - "makeerror": "1.0.12" - } - }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 599c77287c..cd83d399a0 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ }, "devDependencies": { "@playwright/test": "1.27.0", + "@rollup/pluginutils": "5.0.1", "@stoplight/spectral-cli": "6.5.1", "eslint": "8.25.0", "eslint-plugin-import": "2.26.0", @@ -55,15 +56,14 @@ "eslint-plugin-sonarjs": "0.15.0", "eslint-plugin-unicorn": "44.0.2", "eslint-plugin-vue": "9.6.0", - "jest": "29.1.2", - "jest-environment-jsdom": "29.1.2", - "jest-extended": "3.1.0", + "jsdom": "20.0.1", "markdownlint-cli": "0.32.2", "postcss-less": "6.0.0", "stylelint": "14.13.0", "stylelint-config-standard": "28.0.0", "svgo": "2.8.0", - "updates": "13.1.8" + "updates": "13.1.8", + "vitest": "0.24.1" }, "browserslist": [ "defaults", diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index a54add0621..f6ab961f5e 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -180,15 +180,19 @@ func Routes(ctx gocontext.Context) *web.Route { r.Get("/*", maven.DownloadPackageFile) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/nuget", func() { - r.Get("/index.json", nuget.ServiceIndex) // Needs to be unauthenticated for the NuGet client. + r.Group("", func() { // Needs to be unauthenticated for the NuGet client. + r.Get("/", nuget.ServiceIndexV2) + r.Get("/index.json", nuget.ServiceIndexV3) + r.Get("/$metadata", nuget.FeedCapabilityResource) + }) r.Group("", func() { - r.Get("/query", nuget.SearchService) + r.Get("/query", nuget.SearchServiceV3) r.Group("/registration/{id}", func() { r.Get("/index.json", nuget.RegistrationIndex) - r.Get("/{version}", nuget.RegistrationLeaf) + r.Get("/{version}", nuget.RegistrationLeafV3) }) r.Group("/package/{id}", func() { - r.Get("/index.json", nuget.EnumeratePackageVersions) + r.Get("/index.json", nuget.EnumeratePackageVersionsV3) r.Get("/{version}/{filename}", nuget.DownloadPackageFile) }) r.Group("", func() { @@ -197,6 +201,10 @@ func Routes(ctx gocontext.Context) *web.Route { r.Delete("/{id}/{version}", nuget.DeletePackage) }, reqPackageAccess(perm.AccessModeWrite)) r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile) + r.Get("/Packages(Id='{id:[^']+}',Version='{version:[^']+}')", nuget.RegistrationLeafV2) + r.Get("/Packages()", nuget.SearchServiceV2) + r.Get("/FindPackagesById()", nuget.EnumeratePackageVersionsV2) + r.Get("/Search()", nuget.SearchServiceV2) }, reqPackageAccess(perm.AccessModeRead)) }) r.Group("/npm", func() { diff --git a/routers/api/packages/nuget/api_v2.go b/routers/api/packages/nuget/api_v2.go new file mode 100644 index 0000000000..60a5d9c0e4 --- /dev/null +++ b/routers/api/packages/nuget/api_v2.go @@ -0,0 +1,393 @@ +// 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 nuget + +import ( + "encoding/xml" + "strings" + "time" + + packages_model "code.gitea.io/gitea/models/packages" + nuget_module "code.gitea.io/gitea/modules/packages/nuget" +) + +type AtomTitle struct { + Type string `xml:"type,attr"` + Text string `xml:",chardata"` +} + +type ServiceCollection struct { + Href string `xml:"href,attr"` + Title AtomTitle `xml:"atom:title"` +} + +type ServiceWorkspace struct { + Title AtomTitle `xml:"atom:title"` + Collection ServiceCollection `xml:"collection"` +} + +type ServiceIndexResponseV2 struct { + XMLName xml.Name `xml:"service"` + Base string `xml:"base,attr"` + Xmlns string `xml:"xmlns,attr"` + XmlnsAtom string `xml:"xmlns:atom,attr"` + Workspace ServiceWorkspace `xml:"workspace"` +} + +type EdmxPropertyRef struct { + Name string `xml:"Name,attr"` +} + +type EdmxProperty struct { + Name string `xml:"Name,attr"` + Type string `xml:"Type,attr"` + Nullable bool `xml:"Nullable,attr"` +} + +type EdmxEntityType struct { + Name string `xml:"Name,attr"` + HasStream bool `xml:"m:HasStream,attr"` + Keys []EdmxPropertyRef `xml:"Key>PropertyRef"` + Properties []EdmxProperty `xml:"Property"` +} + +type EdmxFunctionParameter struct { + Name string `xml:"Name,attr"` + Type string `xml:"Type,attr"` +} + +type EdmxFunctionImport struct { + Name string `xml:"Name,attr"` + ReturnType string `xml:"ReturnType,attr"` + EntitySet string `xml:"EntitySet,attr"` + Parameter []EdmxFunctionParameter `xml:"Parameter"` +} + +type EdmxEntitySet struct { + Name string `xml:"Name,attr"` + EntityType string `xml:"EntityType,attr"` +} + +type EdmxEntityContainer struct { + Name string `xml:"Name,attr"` + IsDefaultEntityContainer bool `xml:"m:IsDefaultEntityContainer,attr"` + EntitySet EdmxEntitySet `xml:"EntitySet"` + FunctionImports []EdmxFunctionImport `xml:"FunctionImport"` +} + +type EdmxSchema struct { + Xmlns string `xml:"xmlns,attr"` + Namespace string `xml:"Namespace,attr"` + EntityType *EdmxEntityType `xml:"EntityType,omitempty"` + EntityContainer *EdmxEntityContainer `xml:"EntityContainer,omitempty"` +} + +type EdmxDataServices struct { + XmlnsM string `xml:"xmlns:m,attr"` + DataServiceVersion string `xml:"m:DataServiceVersion,attr"` + MaxDataServiceVersion string `xml:"m:MaxDataServiceVersion,attr"` + Schema []EdmxSchema `xml:"Schema"` +} + +type EdmxMetadata struct { + XMLName xml.Name `xml:"edmx:Edmx"` + XmlnsEdmx string `xml:"xmlns:edmx,attr"` + Version string `xml:"Version,attr"` + DataServices EdmxDataServices `xml:"edmx:DataServices"` +} + +var Metadata = &EdmxMetadata{ + XmlnsEdmx: "http://schemas.microsoft.com/ado/2007/06/edmx", + Version: "1.0", + DataServices: EdmxDataServices{ + XmlnsM: "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata", + DataServiceVersion: "2.0", + MaxDataServiceVersion: "2.0", + Schema: []EdmxSchema{ + { + Xmlns: "http://schemas.microsoft.com/ado/2006/04/edm", + Namespace: "NuGetGallery.OData", + EntityType: &EdmxEntityType{ + Name: "V2FeedPackage", + HasStream: true, + Keys: []EdmxPropertyRef{ + {Name: "Id"}, + {Name: "Version"}, + }, + Properties: []EdmxProperty{ + { + Name: "Id", + Type: "Edm.String", + }, + { + Name: "Version", + Type: "Edm.String", + }, + { + Name: "NormalizedVersion", + Type: "Edm.String", + Nullable: true, + }, + { + Name: "Authors", + Type: "Edm.String", + Nullable: true, + }, + { + Name: "Created", + Type: "Edm.DateTime", + }, + { + Name: "Dependencies", + Type: "Edm.String", + }, + { + Name: "Description", + Type: "Edm.String", + }, + { + Name: "DownloadCount", + Type: "Edm.Int64", + }, + { + Name: "LastUpdated", + Type: "Edm.DateTime", + }, + { + Name: "Published", + Type: "Edm.DateTime", + }, + { + Name: "PackageSize", + Type: "Edm.Int64", + }, + { + Name: "ProjectUrl", + Type: "Edm.String", + Nullable: true, + }, + { + Name: "ReleaseNotes", + Type: "Edm.String", + Nullable: true, + }, + { + Name: "RequireLicenseAcceptance", + Type: "Edm.Boolean", + Nullable: false, + }, + { + Name: "Title", + Type: "Edm.String", + Nullable: true, + }, + { + Name: "VersionDownloadCount", + Type: "Edm.Int64", + Nullable: false, + }, + }, + }, + }, + { + Xmlns: "http://schemas.microsoft.com/ado/2006/04/edm", + Namespace: "NuGetGallery", + EntityContainer: &EdmxEntityContainer{ + Name: "V2FeedContext", + IsDefaultEntityContainer: true, + EntitySet: EdmxEntitySet{ + Name: "Packages", + EntityType: "NuGetGallery.OData.V2FeedPackage", + }, + FunctionImports: []EdmxFunctionImport{ + { + Name: "Search", + ReturnType: "Collection(NuGetGallery.OData.V2FeedPackage)", + EntitySet: "Packages", + Parameter: []EdmxFunctionParameter{ + { + Name: "searchTerm", + Type: "Edm.String", + }, + }, + }, + { + Name: "FindPackagesById", + ReturnType: "Collection(NuGetGallery.OData.V2FeedPackage)", + EntitySet: "Packages", + Parameter: []EdmxFunctionParameter{ + { + Name: "id", + Type: "Edm.String", + }, + }, + }, + }, + }, + }, + }, + }, +} + +type FeedEntryCategory struct { + Term string `xml:"term,attr"` + Scheme string `xml:"scheme,attr"` +} + +type FeedEntryLink struct { + Rel string `xml:"rel,attr"` + Href string `xml:"href,attr"` +} + +type TypedValue[T any] struct { + Type string `xml:"type,attr,omitempty"` + Value T `xml:",chardata"` +} + +type FeedEntryProperties struct { + Version string `xml:"d:Version"` + NormalizedVersion string `xml:"d:NormalizedVersion"` + Authors string `xml:"d:Authors"` + Dependencies string `xml:"d:Dependencies"` + Description string `xml:"d:Description"` + VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"` + DownloadCount TypedValue[int64] `xml:"d:DownloadCount"` + PackageSize TypedValue[int64] `xml:"d:PackageSize"` + Created TypedValue[time.Time] `xml:"d:Created"` + LastUpdated TypedValue[time.Time] `xml:"d:LastUpdated"` + Published TypedValue[time.Time] `xml:"d:Published"` + ProjectURL string `xml:"d:ProjectUrl,omitempty"` + ReleaseNotes string `xml:"d:ReleaseNotes,omitempty"` + RequireLicenseAcceptance TypedValue[bool] `xml:"d:RequireLicenseAcceptance"` + Title string `xml:"d:Title"` +} + +type FeedEntry struct { + XMLName xml.Name `xml:"entry"` + Xmlns string `xml:"xmlns,attr,omitempty"` + XmlnsD string `xml:"xmlns:d,attr,omitempty"` + XmlnsM string `xml:"xmlns:m,attr,omitempty"` + Base string `xml:"xml:base,attr,omitempty"` + ID string `xml:"id"` + Category FeedEntryCategory `xml:"category"` + Links []FeedEntryLink `xml:"link"` + Title TypedValue[string] `xml:"title"` + Updated time.Time `xml:"updated"` + Author string `xml:"author>name"` + Summary string `xml:"summary"` + Properties *FeedEntryProperties `xml:"m:properties"` + Content string `xml:",innerxml"` +} + +type FeedResponse struct { + XMLName xml.Name `xml:"feed"` + Xmlns string `xml:"xmlns,attr,omitempty"` + XmlnsD string `xml:"xmlns:d,attr,omitempty"` + XmlnsM string `xml:"xmlns:m,attr,omitempty"` + Base string `xml:"xml:base,attr,omitempty"` + ID string `xml:"id"` + Title TypedValue[string] `xml:"title"` + Updated time.Time `xml:"updated"` + Link FeedEntryLink `xml:"link"` + Entries []*FeedEntry `xml:"entry"` + Count int64 `xml:"m:count"` +} + +func createFeedResponse(l *linkBuilder, totalEntries int64, pds []*packages_model.PackageDescriptor) *FeedResponse { + entries := make([]*FeedEntry, 0, len(pds)) + for _, pd := range pds { + entries = append(entries, createEntry(l, pd, false)) + } + + return &FeedResponse{ + Xmlns: "http://www.w3.org/2005/Atom", + Base: l.Base, + XmlnsD: "http://schemas.microsoft.com/ado/2007/08/dataservices", + XmlnsM: "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata", + ID: "http://schemas.datacontract.org/2004/07/", + Updated: time.Now(), + Link: FeedEntryLink{Rel: "self", Href: l.Base}, + Count: totalEntries, + Entries: entries, + } +} + +func createEntryResponse(l *linkBuilder, pd *packages_model.PackageDescriptor) *FeedEntry { + return createEntry(l, pd, true) +} + +func createEntry(l *linkBuilder, pd *packages_model.PackageDescriptor, withNamespace bool) *FeedEntry { + metadata := pd.Metadata.(*nuget_module.Metadata) + + id := l.GetPackageMetadataURL(pd.Package.Name, pd.Version.Version) + + // Workaround to force a self-closing tag to satisfy XmlReader.IsEmptyElement used by the NuGet client. + // https://learn.microsoft.com/en-us/dotnet/api/system.xml.xmlreader.isemptyelement + content := `` + + createdValue := TypedValue[time.Time]{ + Type: "Edm.DateTime", + Value: pd.Version.CreatedUnix.AsLocalTime(), + } + + entry := &FeedEntry{ + ID: id, + Category: FeedEntryCategory{Term: "NuGetGallery.OData.V2FeedPackage", Scheme: "http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"}, + Links: []FeedEntryLink{ + {Rel: "self", Href: id}, + {Rel: "edit", Href: id}, + }, + Title: TypedValue[string]{Type: "text", Value: pd.Package.Name}, + Updated: pd.Version.CreatedUnix.AsLocalTime(), + Author: metadata.Authors, + Content: content, + Properties: &FeedEntryProperties{ + Version: pd.Version.Version, + NormalizedVersion: normalizeVersion(pd.SemVer), + Authors: metadata.Authors, + Dependencies: buildDependencyString(metadata), + Description: metadata.Description, + VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, + DownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, + PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()}, + Created: createdValue, + LastUpdated: createdValue, + Published: createdValue, + ProjectURL: metadata.ProjectURL, + ReleaseNotes: metadata.ReleaseNotes, + RequireLicenseAcceptance: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.RequireLicenseAcceptance}, + Title: pd.Package.Name, + }, + } + + if withNamespace { + entry.Xmlns = "http://www.w3.org/2005/Atom" + entry.Base = l.Base + entry.XmlnsD = "http://schemas.microsoft.com/ado/2007/08/dataservices" + entry.XmlnsM = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" + } + + return entry +} + +func buildDependencyString(metadata *nuget_module.Metadata) string { + var b strings.Builder + first := true + for group, deps := range metadata.Dependencies { + for _, dep := range deps { + if !first { + b.WriteByte('|') + } + first = false + + b.WriteString(dep.ID) + b.WriteByte(':') + b.WriteString(dep.Version) + b.WriteByte(':') + b.WriteString(group) + } + } + return b.String() +} diff --git a/routers/api/packages/nuget/api.go b/routers/api/packages/nuget/api_v3.go similarity index 76% rename from routers/api/packages/nuget/api.go rename to routers/api/packages/nuget/api_v3.go index 964e05f926..bb3e447bd6 100644 --- a/routers/api/packages/nuget/api.go +++ b/routers/api/packages/nuget/api_v3.go @@ -16,36 +16,19 @@ import ( "github.com/hashicorp/go-version" ) -// ServiceIndexResponse https://docs.microsoft.com/en-us/nuget/api/service-index#resources -type ServiceIndexResponse struct { +// https://docs.microsoft.com/en-us/nuget/api/service-index#resources +type ServiceIndexResponseV3 struct { Version string `json:"version"` Resources []ServiceResource `json:"resources"` } -// ServiceResource https://docs.microsoft.com/en-us/nuget/api/service-index#resource +// https://docs.microsoft.com/en-us/nuget/api/service-index#resource type ServiceResource struct { ID string `json:"@id"` Type string `json:"@type"` } -func createServiceIndexResponse(root string) *ServiceIndexResponse { - return &ServiceIndexResponse{ - Version: "3.0.0", - Resources: []ServiceResource{ - {ID: root + "/query", Type: "SearchQueryService"}, - {ID: root + "/query", Type: "SearchQueryService/3.0.0-beta"}, - {ID: root + "/query", Type: "SearchQueryService/3.0.0-rc"}, - {ID: root + "/registration", Type: "RegistrationsBaseUrl"}, - {ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-beta"}, - {ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-rc"}, - {ID: root + "/package", Type: "PackageBaseAddress/3.0.0"}, - {ID: root, Type: "PackagePublish/2.0.0"}, - {ID: root + "/symbolpackage", Type: "SymbolPackagePublish/4.9.0"}, - }, - } -} - -// RegistrationIndexResponse https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response +// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response type RegistrationIndexResponse struct { RegistrationIndexURL string `json:"@id"` Type []string `json:"@type"` @@ -53,7 +36,7 @@ type RegistrationIndexResponse struct { Pages []*RegistrationIndexPage `json:"items"` } -// RegistrationIndexPage https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page-object +// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page-object type RegistrationIndexPage struct { RegistrationPageURL string `json:"@id"` Lower string `json:"lower"` @@ -62,14 +45,14 @@ type RegistrationIndexPage struct { Items []*RegistrationIndexPageItem `json:"items"` } -// RegistrationIndexPageItem https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf-object-in-a-page +// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf-object-in-a-page type RegistrationIndexPageItem struct { RegistrationLeafURL string `json:"@id"` PackageContentURL string `json:"packageContent"` CatalogEntry *CatalogEntry `json:"catalogEntry"` } -// CatalogEntry https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry +// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry type CatalogEntry struct { CatalogLeafURL string `json:"@id"` PackageContentURL string `json:"packageContent"` @@ -83,13 +66,13 @@ type CatalogEntry struct { DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"` } -// PackageDependencyGroup https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group +// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group type PackageDependencyGroup struct { TargetFramework string `json:"targetFramework"` Dependencies []*PackageDependency `json:"dependencies"` } -// PackageDependency https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency +// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency type PackageDependency struct { ID string `json:"id"` Range string `json:"range"` @@ -162,7 +145,7 @@ func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDepe return dependencyGroups } -// RegistrationLeafResponse https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf +// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf type RegistrationLeafResponse struct { RegistrationLeafURL string `json:"@id"` Type []string `json:"@type"` @@ -183,7 +166,7 @@ func createRegistrationLeafResponse(l *linkBuilder, pd *packages_model.PackageDe } } -// PackageVersionsResponse https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#response +// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#response type PackageVersionsResponse struct { Versions []string `json:"versions"` } @@ -199,13 +182,13 @@ func createPackageVersionsResponse(pds []*packages_model.PackageDescriptor) *Pac } } -// SearchResultResponse https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#response +// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#response type SearchResultResponse struct { TotalHits int64 `json:"totalHits"` Data []*SearchResult `json:"data"` } -// SearchResult https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result +// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result type SearchResult struct { ID string `json:"id"` Version string `json:"version"` @@ -216,7 +199,7 @@ type SearchResult struct { RegistrationIndexURL string `json:"registration"` } -// SearchResultVersion https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result +// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result type SearchResultVersion struct { RegistrationLeafURL string `json:"@id"` Version string `json:"version"` @@ -224,20 +207,13 @@ type SearchResultVersion struct { } func createSearchResultResponse(l *linkBuilder, totalHits int64, pds []*packages_model.PackageDescriptor) *SearchResultResponse { + grouped := make(map[string][]*packages_model.PackageDescriptor) + for _, pd := range pds { + grouped[pd.Package.Name] = append(grouped[pd.Package.Name], pd) + } + data := make([]*SearchResult, 0, len(pds)) - - if len(pds) > 0 { - groupID := pds[0].Package.Name - group := make([]*packages_model.PackageDescriptor, 0, 10) - - for i := 0; i < len(pds); i++ { - if groupID != pds[i].Package.Name { - data = append(data, createSearchResult(l, group)) - groupID = pds[i].Package.Name - group = group[:0] - } - group = append(group, pds[i]) - } + for _, group := range grouped { data = append(data, createSearchResult(l, group)) } diff --git a/routers/api/packages/nuget/links.go b/routers/api/packages/nuget/links.go index f782c7f2cb..618b54ae8d 100644 --- a/routers/api/packages/nuget/links.go +++ b/routers/api/packages/nuget/links.go @@ -26,3 +26,8 @@ func (l *linkBuilder) GetRegistrationLeafURL(id, version string) string { func (l *linkBuilder) GetPackageDownloadURL(id, version string) string { return fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", l.Base, id, version, id, version) } + +// GetPackageMetadataURL builds the package metadata url +func (l *linkBuilder) GetPackageMetadataURL(id, version string) string { + return fmt.Sprintf("%s/Packages(Id='%s',Version='%s')", l.Base, id, version) +} diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index 3c61ae28bb..e84aef3160 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -5,15 +5,18 @@ package nuget import ( + "encoding/xml" "errors" "fmt" "io" "net/http" + "regexp" "strings" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" nuget_module "code.gitea.io/gitea/modules/packages/nuget" "code.gitea.io/gitea/modules/setting" @@ -30,15 +33,121 @@ func apiError(ctx *context.Context, status int, obj interface{}) { }) } -// ServiceIndex https://docs.microsoft.com/en-us/nuget/api/service-index -func ServiceIndex(ctx *context.Context) { - resp := createServiceIndexResponse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget") - - ctx.JSON(http.StatusOK, resp) +func xmlResponse(ctx *context.Context, status int, obj interface{}) { + ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8") + ctx.Resp.WriteHeader(status) + if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil { + log.Error("Write failed: %v", err) + } + if err := xml.NewEncoder(ctx.Resp).Encode(obj); err != nil { + log.Error("XML encode failed: %v", err) + } } -// SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages -func SearchService(ctx *context.Context) { +// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs +func ServiceIndexV2(ctx *context.Context) { + base := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget" + + xmlResponse(ctx, http.StatusOK, &ServiceIndexResponseV2{ + Base: base, + Xmlns: "http://www.w3.org/2007/app", + XmlnsAtom: "http://www.w3.org/2005/Atom", + Workspace: ServiceWorkspace{ + Title: AtomTitle{ + Type: "text", + Text: "Default", + }, + Collection: ServiceCollection{ + Href: "Packages", + Title: AtomTitle{ + Type: "text", + Text: "Packages", + }, + }, + }, + }) +} + +// https://docs.microsoft.com/en-us/nuget/api/service-index +func ServiceIndexV3(ctx *context.Context) { + root := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget" + + ctx.JSON(http.StatusOK, &ServiceIndexResponseV3{ + Version: "3.0.0", + Resources: []ServiceResource{ + {ID: root + "/query", Type: "SearchQueryService"}, + {ID: root + "/query", Type: "SearchQueryService/3.0.0-beta"}, + {ID: root + "/query", Type: "SearchQueryService/3.0.0-rc"}, + {ID: root + "/registration", Type: "RegistrationsBaseUrl"}, + {ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-beta"}, + {ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-rc"}, + {ID: root + "/package", Type: "PackageBaseAddress/3.0.0"}, + {ID: root, Type: "PackagePublish/2.0.0"}, + {ID: root + "/symbolpackage", Type: "SymbolPackagePublish/4.9.0"}, + }, + }) +} + +// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/LegacyFeedCapabilityResourceV2Feed.cs +func FeedCapabilityResource(ctx *context.Context) { + xmlResponse(ctx, http.StatusOK, Metadata) +} + +var searchTermExtract = regexp.MustCompile(`'([^']+)'`) + +// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs +func SearchServiceV2(ctx *context.Context) { + searchTerm := strings.Trim(ctx.FormTrim("searchTerm"), "'") + if searchTerm == "" { + // $filter contains a query like: + // (((Id ne null) and substringof('microsoft',tolower(Id))) + // We don't support these queries, just extract the search term. + match := searchTermExtract.FindStringSubmatch(ctx.FormTrim("$filter")) + if len(match) == 2 { + searchTerm = strings.TrimSpace(match[1]) + } + } + + skip, take := ctx.FormInt("skip"), ctx.FormInt("take") + if skip == 0 { + skip = ctx.FormInt("$skip") + } + if take == 0 { + take = ctx.FormInt("$top") + } + + pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeNuGet, + Name: packages_model.SearchValue{Value: searchTerm}, + IsInternal: util.OptionalBoolFalse, + Paginator: db.NewAbsoluteListOptions( + skip, + take, + ), + }) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + resp := createFeedResponse( + &linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"}, + total, + pds, + ) + + xmlResponse(ctx, http.StatusOK, resp) +} + +// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages +func SearchServiceV3(ctx *context.Context) { pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeNuGet, @@ -69,7 +178,7 @@ func SearchService(ctx *context.Context) { ctx.JSON(http.StatusOK, resp) } -// RegistrationIndex https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index +// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index func RegistrationIndex(ctx *context.Context) { packageName := ctx.Params("id") @@ -97,8 +206,37 @@ func RegistrationIndex(ctx *context.Context) { ctx.JSON(http.StatusOK, resp) } -// RegistrationLeaf https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf -func RegistrationLeaf(ctx *context.Context) { +// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs +func RegistrationLeafV2(ctx *context.Context) { + packageName := ctx.Params("id") + packageVersion := ctx.Params("version") + + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion) + if err != nil { + if err == packages_model.ErrPackageNotExist { + apiError(ctx, http.StatusNotFound, err) + return + } + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pd, err := packages_model.GetPackageDescriptor(ctx, pv) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + resp := createEntryResponse( + &linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"}, + pd, + ) + + xmlResponse(ctx, http.StatusOK, resp) +} + +// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf +func RegistrationLeafV3(ctx *context.Context) { packageName := ctx.Params("id") packageVersion := strings.TrimSuffix(ctx.Params("version"), ".json") @@ -126,8 +264,33 @@ func RegistrationLeaf(ctx *context.Context) { ctx.JSON(http.StatusOK, resp) } -// EnumeratePackageVersions https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions -func EnumeratePackageVersions(ctx *context.Context) { +// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs +func EnumeratePackageVersionsV2(ctx *context.Context) { + packageName := strings.Trim(ctx.FormTrim("id"), "'") + + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + pds, err := packages_model.GetPackageDescriptors(ctx, pvs) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + resp := createFeedResponse( + &linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"}, + int64(len(pds)), + pds, + ) + + xmlResponse(ctx, http.StatusOK, resp) +} + +// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions +func EnumeratePackageVersionsV3(ctx *context.Context) { packageName := ctx.Params("id") pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName) @@ -151,7 +314,7 @@ func EnumeratePackageVersions(ctx *context.Context) { ctx.JSON(http.StatusOK, resp) } -// DownloadPackageFile https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg +// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg func DownloadPackageFile(ctx *context.Context) { packageName := ctx.Params("id") packageVersion := ctx.Params("version") @@ -350,7 +513,7 @@ func processUploadedFile(ctx *context.Context, expectedType nuget_module.Package return np, buf, closables } -// DownloadSymbolFile https://github.com/dotnet/symstore/blob/main/docs/specs/Simple_Symbol_Query_Protocol.md#request +// https://github.com/dotnet/symstore/blob/main/docs/specs/Simple_Symbol_Query_Protocol.md#request func DownloadSymbolFile(ctx *context.Context) { filename := ctx.Params("filename") guid := ctx.Params("guid")[:32] diff --git a/routers/api/v1/misc/markdown_test.go b/routers/api/v1/misc/markdown_test.go index 7809fa5cc7..65ce060278 100644 --- a/routers/api/v1/misc/markdown_test.go +++ b/routers/api/v1/misc/markdown_test.go @@ -5,6 +5,7 @@ package misc import ( + go_context "context" "io" "net/http" "net/http/httptest" @@ -13,6 +14,7 @@ import ( "testing" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates" @@ -50,6 +52,11 @@ func wrap(ctx *context.Context) *context.APIContext { func TestAPI_RenderGFM(t *testing.T) { setting.AppURL = AppURL + markup.Init(&markup.ProcessorHelper{ + IsUsernameMentionable: func(ctx go_context.Context, username string) bool { + return username == "r-lyeh" + }, + }) options := api.MarkdownOption{ Mode: "gfm", diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index b17142c0f6..5956fe9da9 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -168,16 +168,17 @@ func TestHook(ctx *context.APIContext) { commit := convert.ToPayloadCommit(ctx.Repo.Repository, ctx.Repo.Commit) commitID := ctx.Repo.Commit.ID.String() - if err := webhook_service.PrepareWebhook(hook, ctx.Repo.Repository, webhook.HookEventPush, &api.PushPayload{ - Ref: ref, - Before: commitID, - After: commitID, - CompareURL: setting.AppURL + ctx.Repo.Repository.ComposeCompareURL(commitID, commitID), - Commits: []*api.PayloadCommit{commit}, - HeadCommit: commit, - Repo: convert.ToRepo(ctx.Repo.Repository, perm.AccessModeNone), - Pusher: convert.ToUserWithAccessMode(ctx.Doer, perm.AccessModeNone), - Sender: convert.ToUserWithAccessMode(ctx.Doer, perm.AccessModeNone), + if err := webhook_service.PrepareWebhook(ctx, hook, webhook.HookEventPush, &api.PushPayload{ + Ref: ref, + Before: commitID, + After: commitID, + CompareURL: setting.AppURL + ctx.Repo.Repository.ComposeCompareURL(commitID, commitID), + Commits: []*api.PayloadCommit{commit}, + TotalCommits: 1, + HeadCommit: commit, + Repo: convert.ToRepo(ctx.Repo.Repository, perm.AccessModeNone), + Pusher: convert.ToUserWithAccessMode(ctx.Doer, perm.AccessModeNone), + Sender: convert.ToUserWithAccessMode(ctx.Doer, perm.AccessModeNone), }); err != nil { ctx.Error(http.StatusInternalServerError, "PrepareWebhook: ", err) return diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go index 07f1532f82..fd9a165bf3 100644 --- a/routers/api/v1/repo/hook_test.go +++ b/routers/api/v1/repo/hook_test.go @@ -28,7 +28,6 @@ func TestTestHook(t *testing.T) { assert.EqualValues(t, http.StatusNoContent, ctx.Resp.Status()) unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{ - RepoID: 1, HookID: 1, }, unittest.Cond("is_delivered=?", false)) } diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index a94db79239..14f1592591 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -213,9 +213,10 @@ func CreateOauth2Application(ctx *context.APIContext) { data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) app, err := auth_model.CreateOAuth2Application(ctx, auth_model.CreateOAuth2ApplicationOptions{ - Name: data.Name, - UserID: ctx.Doer.ID, - RedirectURIs: data.RedirectURIs, + Name: data.Name, + UserID: ctx.Doer.ID, + RedirectURIs: data.RedirectURIs, + ConfidentialClient: data.ConfidentialClient, }) if err != nil { ctx.Error(http.StatusBadRequest, "", "error creating oauth2 application") @@ -363,10 +364,11 @@ func UpdateOauth2Application(ctx *context.APIContext) { data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) app, err := auth_model.UpdateOAuth2Application(auth_model.UpdateOAuth2ApplicationOptions{ - Name: data.Name, - UserID: ctx.Doer.ID, - ID: appID, - RedirectURIs: data.RedirectURIs, + Name: data.Name, + UserID: ctx.Doer.ID, + ID: appID, + RedirectURIs: data.RedirectURIs, + ConfidentialClient: data.ConfidentialClient, }) if err != nil { if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) { diff --git a/routers/init.go b/routers/init.go index 85a38899e3..9045437f87 100644 --- a/routers/init.go +++ b/routers/init.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" - "code.gitea.io/gitea/modules/appstate" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/eventsource" "code.gitea.io/gitea/modules/git" @@ -27,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/ssh" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/svg" + "code.gitea.io/gitea/modules/system" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" @@ -41,6 +41,7 @@ import ( "code.gitea.io/gitea/services/automerge" "code.gitea.io/gitea/services/cron" "code.gitea.io/gitea/services/mailer" + markup_service "code.gitea.io/gitea/services/markup" repo_migrations "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" pull_service "code.gitea.io/gitea/services/pull" @@ -76,8 +77,8 @@ func InitGitServices() { } func syncAppPathForGit(ctx context.Context) error { - runtimeState := new(appstate.RuntimeState) - if err := appstate.AppState.Get(runtimeState); err != nil { + runtimeState := new(system.RuntimeState) + if err := system.AppState.Get(runtimeState); err != nil { return err } if runtimeState.LastAppPath != setting.AppPath { @@ -90,7 +91,7 @@ func syncAppPathForGit(ctx context.Context) error { mustInit(asymkey_model.RewriteAllPublicKeys) runtimeState.LastAppPath = setting.AppPath - return appstate.AppState.Set(runtimeState) + return system.AppState.Set(runtimeState) } return nil } @@ -123,7 +124,7 @@ func GlobalInitInstalled(ctx context.Context) { highlight.NewContext() external.RegisterRenderers() - markup.Init() + markup.Init(markup_service.ProcessorHelper()) if setting.EnableSQLite3 { log.Info("SQLite3 support is enabled") @@ -133,10 +134,10 @@ func GlobalInitInstalled(ctx context.Context) { mustInitCtx(ctx, common.InitDBEngine) log.Info("ORM engine initialization successful!") - mustInit(appstate.Init) + mustInit(system.Init) mustInit(oauth2.Init) - models.NewRepoContext() + mustInit(models.Init) mustInit(repo_service.Init) // Booting long running goroutines. diff --git a/routers/install/install.go b/routers/install/install.go index 890725b9a7..962dee8c86 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -12,12 +12,14 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "time" "code.gitea.io/gitea/models/db" db_install "code.gitea.io/gitea/models/db/install" "code.gitea.io/gitea/models/migrations" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -147,8 +149,9 @@ func Install(ctx *context.Context) { // Server and other services settings form.OfflineMode = setting.OfflineMode - form.DisableGravatar = setting.DisableGravatar - form.EnableFederatedAvatar = setting.EnableFederatedAvatar + form.DisableGravatar = false // when installing, there is no database connection so that given a default value + form.EnableFederatedAvatar = false // when installing, there is no database connection so that given a default value + form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp form.DisableRegistration = setting.Service.DisableRegistration @@ -372,7 +375,6 @@ func SubmitInstall(ctx *context.Context) { ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form) return } - db.UnsetDefaultEngine() // Save settings. cfg := ini.Empty() @@ -439,7 +441,11 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(fmt.Sprint(form.MailNotify)) cfg.Section("server").Key("OFFLINE_MODE").SetValue(fmt.Sprint(form.OfflineMode)) - cfg.Section("picture").Key("DISABLE_GRAVATAR").SetValue(fmt.Sprint(form.DisableGravatar)) + // if you are reinstalling, this maybe not right because of missing version + if err := system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, strconv.FormatBool(form.DisableGravatar)); err != nil { + ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) + return + } cfg.Section("picture").Key("ENABLE_FEDERATED_AVATAR").SetValue(fmt.Sprint(form.EnableFederatedAvatar)) cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn)) cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp)) @@ -501,6 +507,9 @@ func SubmitInstall(ctx *context.Context) { return } + // unset default engine before reload database setting + db.UnsetDefaultEngine() + // ---- All checks are passed // Reload settings (and re-initialize database connection) diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 3e7d1fe9ef..fdd0a0bc3a 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -186,7 +186,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN // 2. Disallow force pushes to protected branches if git.EmptySHA != oldCommitID { - output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: ctx.env}) + output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+newCommitID).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: ctx.env}) if err != nil { log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err) ctx.JSON(http.StatusInternalServerError, private.Response{ diff --git a/routers/private/hook_verification.go b/routers/private/hook_verification.go index dfa6195b19..8a2d1cf33d 100644 --- a/routers/private/hook_verification.go +++ b/routers/private/hook_verification.go @@ -44,7 +44,7 @@ func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env [] }() // This is safe as force pushes are already forbidden - err = git.NewCommand(repo.Ctx, "rev-list", oldCommitID+"..."+newCommitID). + err = git.NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(oldCommitID + "..." + newCommitID). Run(&git.RunOpts{ Env: env, Dir: repo.Path, @@ -91,7 +91,7 @@ func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error { }() hash := git.MustIDFromString(sha) - return git.NewCommand(repo.Ctx, "cat-file", "commit", sha). + return git.NewCommand(repo.Ctx, "cat-file", "commit").AddDynamicArguments(sha). Run(&git.RunOpts{ Env: env, Dir: repo.Path, diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go index 17471ef8a6..d0664eb780 100644 --- a/routers/web/admin/admin.go +++ b/routers/web/admin/admin.go @@ -8,18 +8,13 @@ package admin import ( "fmt" "net/http" - "net/url" - "os" "runtime" "strconv" - "strings" "time" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" @@ -27,18 +22,13 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/updatechecker" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/cron" "code.gitea.io/gitea/services/forms" - "code.gitea.io/gitea/services/mailer" - - "gitea.com/go-chi/session" ) const ( tplDashboard base.TplName = "admin/dashboard" - tplConfig base.TplName = "admin/config" tplMonitor base.TplName = "admin/monitor" tplStacktrace base.TplName = "admin/stacktrace" tplQueue base.TplName = "admin/queue" @@ -165,165 +155,6 @@ func DashboardPost(ctx *context.Context) { } } -// SendTestMail send test mail to confirm mail service is OK -func SendTestMail(ctx *context.Context) { - email := ctx.FormString("email") - // Send a test email to the user's email address and redirect back to Config - if err := mailer.SendTestMail(email); err != nil { - ctx.Flash.Error(ctx.Tr("admin.config.test_mail_failed", email, err)) - } else { - ctx.Flash.Info(ctx.Tr("admin.config.test_mail_sent", email)) - } - - ctx.Redirect(setting.AppSubURL + "/admin/config") -} - -func shadowPasswordKV(cfgItem, splitter string) string { - fields := strings.Split(cfgItem, splitter) - for i := 0; i < len(fields); i++ { - if strings.HasPrefix(fields[i], "password=") { - fields[i] = "password=******" - break - } - } - return strings.Join(fields, splitter) -} - -func shadowURL(provider, cfgItem string) string { - u, err := url.Parse(cfgItem) - if err != nil { - log.Error("Shadowing Password for %v failed: %v", provider, err) - return cfgItem - } - if u.User != nil { - atIdx := strings.Index(cfgItem, "@") - if atIdx > 0 { - colonIdx := strings.LastIndex(cfgItem[:atIdx], ":") - if colonIdx > 0 { - return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] - } - } - } - return cfgItem -} - -func shadowPassword(provider, cfgItem string) string { - switch provider { - case "redis": - return shadowPasswordKV(cfgItem, ",") - case "mysql": - // root:@tcp(localhost:3306)/macaron?charset=utf8 - atIdx := strings.Index(cfgItem, "@") - if atIdx > 0 { - colonIdx := strings.Index(cfgItem[:atIdx], ":") - if colonIdx > 0 { - return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] - } - } - return cfgItem - case "postgres": - // user=jiahuachen dbname=macaron port=5432 sslmode=disable - if !strings.HasPrefix(cfgItem, "postgres://") { - return shadowPasswordKV(cfgItem, " ") - } - fallthrough - case "couchbase": - return shadowURL(provider, cfgItem) - // postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full - // Notice: use shadowURL - } - return cfgItem -} - -// Config show admin config page -func Config(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("admin.config") - ctx.Data["PageIsAdmin"] = true - ctx.Data["PageIsAdminConfig"] = true - - ctx.Data["CustomConf"] = setting.CustomConf - ctx.Data["AppUrl"] = setting.AppURL - ctx.Data["Domain"] = setting.Domain - ctx.Data["OfflineMode"] = setting.OfflineMode - ctx.Data["DisableRouterLog"] = setting.DisableRouterLog - ctx.Data["RunUser"] = setting.RunUser - ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode) - ctx.Data["GitVersion"] = git.VersionInfo() - - ctx.Data["RepoRootPath"] = setting.RepoRootPath - ctx.Data["CustomRootPath"] = setting.CustomPath - ctx.Data["StaticRootPath"] = setting.StaticRootPath - ctx.Data["LogRootPath"] = setting.LogRootPath - ctx.Data["ScriptType"] = setting.ScriptType - ctx.Data["ReverseProxyAuthUser"] = setting.ReverseProxyAuthUser - ctx.Data["ReverseProxyAuthEmail"] = setting.ReverseProxyAuthEmail - ctx.Data["ReverseProxyAuthFullName"] = setting.ReverseProxyAuthFullName - - ctx.Data["SSH"] = setting.SSH - ctx.Data["LFS"] = setting.LFS - - ctx.Data["Service"] = setting.Service - ctx.Data["DbCfg"] = setting.Database - ctx.Data["Webhook"] = setting.Webhook - - ctx.Data["MailerEnabled"] = false - if setting.MailService != nil { - ctx.Data["MailerEnabled"] = true - ctx.Data["Mailer"] = setting.MailService - } - - ctx.Data["CacheAdapter"] = setting.CacheService.Adapter - ctx.Data["CacheInterval"] = setting.CacheService.Interval - - ctx.Data["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn) - ctx.Data["CacheItemTTL"] = setting.CacheService.TTL - - sessionCfg := setting.SessionConfig - if sessionCfg.Provider == "VirtualSession" { - var realSession session.Options - if err := json.Unmarshal([]byte(sessionCfg.ProviderConfig), &realSession); err != nil { - log.Error("Unable to unmarshall session config for virtualed provider config: %s\nError: %v", sessionCfg.ProviderConfig, err) - } - sessionCfg.Provider = realSession.Provider - sessionCfg.ProviderConfig = realSession.ProviderConfig - sessionCfg.CookieName = realSession.CookieName - sessionCfg.CookiePath = realSession.CookiePath - sessionCfg.Gclifetime = realSession.Gclifetime - sessionCfg.Maxlifetime = realSession.Maxlifetime - sessionCfg.Secure = realSession.Secure - sessionCfg.Domain = realSession.Domain - } - sessionCfg.ProviderConfig = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig) - ctx.Data["SessionConfig"] = sessionCfg - - ctx.Data["DisableGravatar"] = setting.DisableGravatar - ctx.Data["EnableFederatedAvatar"] = setting.EnableFederatedAvatar - - ctx.Data["Git"] = setting.Git - - type envVar struct { - Name, Value string - } - - envVars := map[string]*envVar{} - if len(os.Getenv("GITEA_WORK_DIR")) > 0 { - envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")} - } - if len(os.Getenv("GITEA_CUSTOM")) > 0 { - envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")} - } - - ctx.Data["EnvVars"] = envVars - ctx.Data["Loggers"] = setting.GetLogDescriptions() - ctx.Data["EnableAccessLog"] = setting.EnableAccessLog - ctx.Data["AccessLogTemplate"] = setting.AccessLogTemplate - ctx.Data["DisableRouterLog"] = setting.DisableRouterLog - ctx.Data["EnableXORMLog"] = setting.EnableXORMLog - ctx.Data["LogSQL"] = setting.Database.LogSQL - - ctx.HTML(http.StatusOK, tplConfig) -} - // Monitor show admin monitor page func Monitor(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.monitor") diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go new file mode 100644 index 0000000000..614d3d4f66 --- /dev/null +++ b/routers/web/admin/config.go @@ -0,0 +1,217 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package admin + +import ( + "net/http" + "net/url" + "os" + "strings" + + system_model "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + system_module "code.gitea.io/gitea/modules/system" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/mailer" + + "gitea.com/go-chi/session" +) + +const tplConfig base.TplName = "admin/config" + +// SendTestMail send test mail to confirm mail service is OK +func SendTestMail(ctx *context.Context) { + email := ctx.FormString("email") + // Send a test email to the user's email address and redirect back to Config + if err := mailer.SendTestMail(email); err != nil { + ctx.Flash.Error(ctx.Tr("admin.config.test_mail_failed", email, err)) + } else { + ctx.Flash.Info(ctx.Tr("admin.config.test_mail_sent", email)) + } + + ctx.Redirect(setting.AppSubURL + "/admin/config") +} + +func shadowPasswordKV(cfgItem, splitter string) string { + fields := strings.Split(cfgItem, splitter) + for i := 0; i < len(fields); i++ { + if strings.HasPrefix(fields[i], "password=") { + fields[i] = "password=******" + break + } + } + return strings.Join(fields, splitter) +} + +func shadowURL(provider, cfgItem string) string { + u, err := url.Parse(cfgItem) + if err != nil { + log.Error("Shadowing Password for %v failed: %v", provider, err) + return cfgItem + } + if u.User != nil { + atIdx := strings.Index(cfgItem, "@") + if atIdx > 0 { + colonIdx := strings.LastIndex(cfgItem[:atIdx], ":") + if colonIdx > 0 { + return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] + } + } + } + return cfgItem +} + +func shadowPassword(provider, cfgItem string) string { + switch provider { + case "redis": + return shadowPasswordKV(cfgItem, ",") + case "mysql": + // root:@tcp(localhost:3306)/macaron?charset=utf8 + atIdx := strings.Index(cfgItem, "@") + if atIdx > 0 { + colonIdx := strings.Index(cfgItem[:atIdx], ":") + if colonIdx > 0 { + return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:] + } + } + return cfgItem + case "postgres": + // user=jiahuachen dbname=macaron port=5432 sslmode=disable + if !strings.HasPrefix(cfgItem, "postgres://") { + return shadowPasswordKV(cfgItem, " ") + } + fallthrough + case "couchbase": + return shadowURL(provider, cfgItem) + // postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full + // Notice: use shadowURL + } + return cfgItem +} + +// Config show admin config page +func Config(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("admin.config") + ctx.Data["PageIsAdmin"] = true + ctx.Data["PageIsAdminConfig"] = true + + systemSettings, err := system_model.GetAllSettings() + if err != nil { + ctx.ServerError("system_model.GetAllSettings", err) + return + } + + // All editable settings from UI + ctx.Data["SystemSettings"] = systemSettings + ctx.PageData["adminConfigPage"] = true + + ctx.Data["CustomConf"] = setting.CustomConf + ctx.Data["AppUrl"] = setting.AppURL + ctx.Data["Domain"] = setting.Domain + ctx.Data["OfflineMode"] = setting.OfflineMode + ctx.Data["DisableRouterLog"] = setting.DisableRouterLog + ctx.Data["RunUser"] = setting.RunUser + ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode) + ctx.Data["GitVersion"] = git.VersionInfo() + + ctx.Data["RepoRootPath"] = setting.RepoRootPath + ctx.Data["CustomRootPath"] = setting.CustomPath + ctx.Data["StaticRootPath"] = setting.StaticRootPath + ctx.Data["LogRootPath"] = setting.LogRootPath + ctx.Data["ScriptType"] = setting.ScriptType + ctx.Data["ReverseProxyAuthUser"] = setting.ReverseProxyAuthUser + ctx.Data["ReverseProxyAuthEmail"] = setting.ReverseProxyAuthEmail + + ctx.Data["SSH"] = setting.SSH + ctx.Data["LFS"] = setting.LFS + + ctx.Data["Service"] = setting.Service + ctx.Data["DbCfg"] = setting.Database + ctx.Data["Webhook"] = setting.Webhook + + ctx.Data["MailerEnabled"] = false + if setting.MailService != nil { + ctx.Data["MailerEnabled"] = true + ctx.Data["Mailer"] = setting.MailService + } + + ctx.Data["CacheAdapter"] = setting.CacheService.Adapter + ctx.Data["CacheInterval"] = setting.CacheService.Interval + + ctx.Data["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn) + ctx.Data["CacheItemTTL"] = setting.CacheService.TTL + + sessionCfg := setting.SessionConfig + if sessionCfg.Provider == "VirtualSession" { + var realSession session.Options + if err := json.Unmarshal([]byte(sessionCfg.ProviderConfig), &realSession); err != nil { + log.Error("Unable to unmarshall session config for virtual provider config: %s\nError: %v", sessionCfg.ProviderConfig, err) + } + sessionCfg.Provider = realSession.Provider + sessionCfg.ProviderConfig = realSession.ProviderConfig + sessionCfg.CookieName = realSession.CookieName + sessionCfg.CookiePath = realSession.CookiePath + sessionCfg.Gclifetime = realSession.Gclifetime + sessionCfg.Maxlifetime = realSession.Maxlifetime + sessionCfg.Secure = realSession.Secure + sessionCfg.Domain = realSession.Domain + } + sessionCfg.ProviderConfig = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig) + ctx.Data["SessionConfig"] = sessionCfg + + ctx.Data["Git"] = setting.Git + + type envVar struct { + Name, Value string + } + + envVars := map[string]*envVar{} + if len(os.Getenv("GITEA_WORK_DIR")) > 0 { + envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")} + } + if len(os.Getenv("GITEA_CUSTOM")) > 0 { + envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")} + } + + ctx.Data["EnvVars"] = envVars + ctx.Data["Loggers"] = setting.GetLogDescriptions() + ctx.Data["EnableAccessLog"] = setting.EnableAccessLog + ctx.Data["AccessLogTemplate"] = setting.AccessLogTemplate + ctx.Data["DisableRouterLog"] = setting.DisableRouterLog + ctx.Data["EnableXORMLog"] = setting.EnableXORMLog + ctx.Data["LogSQL"] = setting.Database.LogSQL + + ctx.HTML(http.StatusOK, tplConfig) +} + +func ChangeConfig(ctx *context.Context) { + key := strings.TrimSpace(ctx.FormString("key")) + if key == "" { + ctx.JSON(http.StatusOK, map[string]string{ + "redirect": ctx.Req.URL.String(), + }) + return + } + value := ctx.FormString("value") + version := ctx.FormInt("version") + + if err := system_module.SetSetting(key, value, version); err != nil { + log.Error("set setting failed: %v", err) + ctx.JSON(http.StatusOK, map[string]string{ + "err": ctx.Tr("admin.config.set_setting_failed", key), + }) + return + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "version": version + 1, + }) +} diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go index b50549b804..f5ec294cc3 100644 --- a/routers/web/admin/notice.go +++ b/routers/web/admin/notice.go @@ -9,7 +9,7 @@ import ( "net/http" "strconv" - admin_model "code.gitea.io/gitea/models/admin" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" @@ -26,13 +26,13 @@ func Notices(ctx *context.Context) { ctx.Data["PageIsAdmin"] = true ctx.Data["PageIsAdminNotices"] = true - total := admin_model.CountNotices() + total := system_model.CountNotices() page := ctx.FormInt("page") if page <= 1 { page = 1 } - notices, err := admin_model.Notices(page, setting.UI.Admin.NoticePagingNum) + notices, err := system_model.Notices(page, setting.UI.Admin.NoticePagingNum) if err != nil { ctx.ServerError("Notices", err) return @@ -57,7 +57,7 @@ func DeleteNotices(ctx *context.Context) { } } - if err := admin_model.DeleteNoticesByIDs(ids); err != nil { + if err := system_model.DeleteNoticesByIDs(ids); err != nil { ctx.Flash.Error("DeleteNoticesByIDs: " + err.Error()) ctx.Status(http.StatusInternalServerError) } else { @@ -68,7 +68,7 @@ func DeleteNotices(ctx *context.Context) { // EmptyNotices delete all the notices func EmptyNotices(ctx *context.Context) { - if err := admin_model.DeleteNotices(0, 0); err != nil { + if err := system_model.DeleteNotices(0, 0); err != nil { ctx.ServerError("DeleteNotices", err) return } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 8a4c12d57b..b48bdb9951 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -6,6 +6,7 @@ package auth import ( + "errors" "fmt" "net/http" "strings" @@ -24,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/utils" @@ -619,7 +621,9 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth. // update external user information if gothUser != nil { if err := externalaccount.UpdateExternalUser(u, *gothUser); err != nil { - log.Error("UpdateExternalUser failed: %v", err) + if !errors.Is(err, util.ErrNotExist) { + log.Error("UpdateExternalUser failed: %v", err) + } } } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index c172215b90..9c929d990e 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -48,6 +48,7 @@ const ( // TODO move error and responses to SDK or models // AuthorizeErrorCode represents an error code specified in RFC 6749 +// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1 type AuthorizeErrorCode string const ( @@ -68,6 +69,7 @@ const ( ) // AuthorizeError represents an error type specified in RFC 6749 +// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1 type AuthorizeError struct { ErrorCode AuthorizeErrorCode `json:"error" form:"error"` ErrorDescription string @@ -80,6 +82,7 @@ func (err AuthorizeError) Error() string { } // AccessTokenErrorCode represents an error code specified in RFC 6749 +// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 type AccessTokenErrorCode string const ( @@ -98,6 +101,7 @@ const ( ) // AccessTokenError represents an error response specified in RFC 6749 +// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 type AccessTokenError struct { ErrorCode AccessTokenErrorCode `json:"error" form:"error"` ErrorDescription string `json:"error_description"` @@ -129,6 +133,7 @@ const ( ) // AccessTokenResponse represents a successful access token response +// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2 type AccessTokenResponse struct { AccessToken string `json:"access_token"` TokenType TokenType `json:"token_type"` @@ -433,8 +438,21 @@ func AuthorizeOAuth(ctx *context.Context) { log.Error("Unable to save changes to the session: %v", err) } case "": - break + // "Authorization servers SHOULD reject authorization requests from native apps that don't use PKCE by returning an error message" + // https://datatracker.ietf.org/doc/html/rfc8252#section-8.1 + if !app.ConfidentialClient { + // "the authorization endpoint MUST return the authorization error response with the "error" value set to "invalid_request"" + // https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1 + handleAuthorizeError(ctx, AuthorizeError{ + ErrorCode: ErrorCodeInvalidRequest, + ErrorDescription: "PKCE is required for public clients", + State: form.State, + }, form.RedirectURI) + return + } default: + // "If the server supporting PKCE does not support the requested transformation, the authorization endpoint MUST return the authorization error response with "error" value set to "invalid_request"." + // https://www.rfc-editor.org/rfc/rfc7636#section-4.4.1 handleAuthorizeError(ctx, AuthorizeError{ ErrorCode: ErrorCodeInvalidRequest, ErrorDescription: "unsupported code challenge method", @@ -663,6 +681,30 @@ func AccessTokenOAuth(ctx *context.Context) { } func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2.JWTSigningKey) { + app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID) + if err != nil { + handleAccessTokenError(ctx, AccessTokenError{ + ErrorCode: AccessTokenErrorCodeInvalidClient, + ErrorDescription: fmt.Sprintf("cannot load client with client id: %q", form.ClientID), + }) + return + } + // "The authorization server MUST ... require client authentication for confidential clients" + // https://datatracker.ietf.org/doc/html/rfc6749#section-6 + if !app.ValidateClientSecret([]byte(form.ClientSecret)) { + errorDescription := "invalid client secret" + if form.ClientSecret == "" { + errorDescription = "invalid empty client secret" + } + // "invalid_client ... Client authentication failed" + // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + handleAccessTokenError(ctx, AccessTokenError{ + ErrorCode: AccessTokenErrorCodeInvalidClient, + ErrorDescription: errorDescription, + }) + return + } + token, err := oauth2.ParseToken(form.RefreshToken, serverKey) if err != nil { handleAccessTokenError(ctx, AccessTokenError{ @@ -1068,7 +1110,9 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model // update external user information if err := externalaccount.UpdateExternalUser(u, gothUser); err != nil { - log.Error("UpdateExternalUser failed: %v", err) + if !errors.Is(err, util.ErrNotExist) { + log.Error("UpdateExternalUser failed: %v", err) + } } if err := resetLocale(ctx, u); err != nil { diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 2357b34fd0..38474255d1 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -110,6 +110,18 @@ func Code(ctx *context.Context) { } ctx.Data["RepoMaps"] = repoMaps + + if len(loadRepoIDs) != len(repoMaps) { + // Remove deleted repos from search results + cleanedSearchResults := make([]*code_indexer.Result, 0, len(repoMaps)) + for _, sr := range searchResults { + if _, found := repoMaps[sr.RepoID]; found { + cleanedSearchResults = append(cleanedSearchResults, sr) + } + } + + searchResults = cleanedSearchResults + } } ctx.Data["SearchResults"] = searchResults diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 13c88565c4..399d07fe47 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -14,7 +14,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/organization" + org_model "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/services/forms" @@ -38,6 +39,8 @@ const ( tplTeamMembers base.TplName = "org/team/members" // tplTeamRepositories template path for showing team repositories page tplTeamRepositories base.TplName = "org/team/repositories" + // tplTeamInvite template path for team invites page + tplTeamInvite base.TplName = "org/team/invite" ) // Teams render teams list page @@ -59,12 +62,6 @@ func Teams(ctx *context.Context) { // TeamsAction response for join, leave, remove, add operations to team func TeamsAction(ctx *context.Context) { - uid := ctx.FormInt64("uid") - if uid == 0 { - ctx.Redirect(ctx.Org.OrgLink + "/teams") - return - } - page := ctx.FormString("page") var err error switch ctx.Params(":action") { @@ -77,7 +74,7 @@ func TeamsAction(ctx *context.Context) { case "leave": err = models.RemoveTeamMember(ctx.Org.Team, ctx.Doer.ID) if err != nil { - if organization.IsErrLastOrgOwner(err) { + if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) } else { log.Error("Action(%s): %v", ctx.Params(":action"), err) @@ -98,9 +95,16 @@ func TeamsAction(ctx *context.Context) { ctx.Error(http.StatusNotFound) return } + + uid := ctx.FormInt64("uid") + if uid == 0 { + ctx.Redirect(ctx.Org.OrgLink + "/teams") + return + } + err = models.RemoveTeamMember(ctx.Org.Team, uid) if err != nil { - if organization.IsErrLastOrgOwner(err) { + if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) } else { log.Error("Action(%s): %v", ctx.Params(":action"), err) @@ -126,10 +130,23 @@ func TeamsAction(ctx *context.Context) { u, err = user_model.GetUserByName(ctx, uname) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Flash.Error(ctx.Tr("form.user_not_exist")) + if setting.MailService != nil && user_model.ValidateEmail(uname) == nil { + if err := org_service.CreateTeamInvite(ctx, ctx.Doer, ctx.Org.Team, uname); err != nil { + if org_model.IsErrTeamInviteAlreadyExist(err) { + ctx.Flash.Error(ctx.Tr("form.duplicate_invite_to_team")) + } else if org_model.IsErrUserEmailAlreadyAdded(err) { + ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users")) + } else { + ctx.ServerError("CreateTeamInvite", err) + return + } + } + } else { + ctx.Flash.Error(ctx.Tr("form.user_not_exist")) + } ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) } else { - ctx.ServerError(" GetUserByName", err) + ctx.ServerError("GetUserByName", err) } return } @@ -146,11 +163,30 @@ func TeamsAction(ctx *context.Context) { err = models.AddTeamMember(ctx.Org.Team, u.ID) } + page = "team" + case "remove_invite": + if !ctx.Org.IsOwner { + ctx.Error(http.StatusNotFound) + return + } + + iid := ctx.FormInt64("iid") + if iid == 0 { + ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(ctx.Org.Team.LowerName)) + return + } + + if err := org_model.RemoveInviteByID(ctx, iid, ctx.Org.Team.ID); err != nil { + log.Error("Action(%s): %v", ctx.Params(":action"), err) + ctx.ServerError("RemoveInviteByID", err) + return + } + page = "team" } if err != nil { - if organization.IsErrLastOrgOwner(err) { + if org_model.IsErrLastOrgOwner(err) { ctx.Flash.Error(ctx.Tr("form.last_org_owner")) } else { log.Error("Action(%s): %v", ctx.Params(":action"), err) @@ -224,7 +260,7 @@ func NewTeam(ctx *context.Context) { ctx.Data["Title"] = ctx.Org.Organization.FullName ctx.Data["PageIsOrgTeams"] = true ctx.Data["PageIsOrgTeamsNew"] = true - ctx.Data["Team"] = &organization.Team{} + ctx.Data["Team"] = &org_model.Team{} ctx.Data["Units"] = unit_model.Units ctx.HTML(http.StatusOK, tplTeamNew) } @@ -255,7 +291,7 @@ func NewTeamPost(ctx *context.Context) { p = unit_model.MinUnitAccessMode(unitPerms) } - t := &organization.Team{ + t := &org_model.Team{ OrgID: ctx.Org.Organization.ID, Name: form.TeamName, Description: form.Description, @@ -265,9 +301,9 @@ func NewTeamPost(ctx *context.Context) { } if t.AccessMode < perm.AccessModeAdmin { - units := make([]*organization.TeamUnit, 0, len(unitPerms)) + units := make([]*org_model.TeamUnit, 0, len(unitPerms)) for tp, perm := range unitPerms { - units = append(units, &organization.TeamUnit{ + units = append(units, &org_model.TeamUnit{ OrgID: ctx.Org.Organization.ID, Type: tp, AccessMode: perm, @@ -295,7 +331,7 @@ func NewTeamPost(ctx *context.Context) { if err := models.NewTeam(t); err != nil { ctx.Data["Err_TeamName"] = true switch { - case organization.IsErrTeamAlreadyExist(err): + case org_model.IsErrTeamAlreadyExist(err): ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) default: ctx.ServerError("NewTeam", err) @@ -316,6 +352,15 @@ func TeamMembers(ctx *context.Context) { return } ctx.Data["Units"] = unit_model.Units + + invites, err := org_model.GetInvitesByTeamID(ctx, ctx.Org.Team.ID) + if err != nil { + ctx.ServerError("GetInvitesByTeamID", err) + return + } + ctx.Data["Invites"] = invites + ctx.Data["IsEmailInviteEnabled"] = setting.MailService != nil + ctx.HTML(http.StatusOK, tplTeamMembers) } @@ -339,7 +384,7 @@ func SearchTeam(ctx *context.Context) { PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), } - opts := &organization.SearchTeamOptions{ + opts := &org_model.SearchTeamOptions{ // UserID is not set because the router already requires the doer to be an org admin. Thus, we don't need to restrict to teams that the user belongs in Keyword: ctx.FormTrim("q"), OrgID: ctx.Org.Organization.ID, @@ -347,7 +392,7 @@ func SearchTeam(ctx *context.Context) { ListOptions: listOptions, } - teams, maxResults, err := organization.SearchTeam(opts) + teams, maxResults, err := org_model.SearchTeam(opts) if err != nil { log.Error("SearchTeam failed: %v", err) ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ @@ -424,16 +469,16 @@ func EditTeamPost(ctx *context.Context) { t.Description = form.Description if t.AccessMode < perm.AccessModeAdmin { - units := make([]organization.TeamUnit, 0, len(unitPerms)) + units := make([]org_model.TeamUnit, 0, len(unitPerms)) for tp, perm := range unitPerms { - units = append(units, organization.TeamUnit{ + units = append(units, org_model.TeamUnit{ OrgID: t.OrgID, TeamID: t.ID, Type: tp, AccessMode: perm, }) } - if err := organization.UpdateTeamUnits(t, units); err != nil { + if err := org_model.UpdateTeamUnits(t, units); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateTeamUnits", err.Error()) return } @@ -452,7 +497,7 @@ func EditTeamPost(ctx *context.Context) { if err := models.UpdateTeam(t, isAuthChanged, isIncludeAllChanged); err != nil { ctx.Data["Err_TeamName"] = true switch { - case organization.IsErrTeamAlreadyExist(err): + case org_model.IsErrTeamAlreadyExist(err): ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) default: ctx.ServerError("UpdateTeam", err) @@ -474,3 +519,72 @@ func DeleteTeam(ctx *context.Context) { "redirect": ctx.Org.OrgLink + "/teams", }) } + +// TeamInvite renders the team invite page +func TeamInvite(ctx *context.Context) { + invite, org, team, inviter, err := getTeamInviteFromContext(ctx) + if err != nil { + if org_model.IsErrTeamInviteNotFound(err) { + ctx.NotFound("ErrTeamInviteNotFound", err) + } else { + ctx.ServerError("getTeamInviteFromContext", err) + } + return + } + + ctx.Data["Title"] = ctx.Tr("org.teams.invite_team_member", team.Name) + ctx.Data["Invite"] = invite + ctx.Data["Organization"] = org + ctx.Data["Team"] = team + ctx.Data["Inviter"] = inviter + + ctx.HTML(http.StatusOK, tplTeamInvite) +} + +// TeamInvitePost handles the team invitation +func TeamInvitePost(ctx *context.Context) { + invite, org, team, _, err := getTeamInviteFromContext(ctx) + if err != nil { + if org_model.IsErrTeamInviteNotFound(err) { + ctx.NotFound("ErrTeamInviteNotFound", err) + } else { + ctx.ServerError("getTeamInviteFromContext", err) + } + return + } + + if err := models.AddTeamMember(team, ctx.Doer.ID); err != nil { + ctx.ServerError("AddTeamMember", err) + return + } + + if err := org_model.RemoveInviteByID(ctx, invite.ID, team.ID); err != nil { + log.Error("RemoveInviteByID: %v", err) + } + + ctx.Redirect(org.OrganisationLink() + "/teams/" + url.PathEscape(team.LowerName)) +} + +func getTeamInviteFromContext(ctx *context.Context) (*org_model.TeamInvite, *org_model.Organization, *org_model.Team, *user_model.User, error) { + invite, err := org_model.GetInviteByToken(ctx, ctx.Params("token")) + if err != nil { + return nil, nil, nil, nil, err + } + + inviter, err := user_model.GetUserByIDCtx(ctx, invite.InviterID) + if err != nil { + return nil, nil, nil, nil, err + } + + team, err := org_model.GetTeamByID(ctx, invite.TeamID) + if err != nil { + return nil, nil, nil, nil, err + } + + org, err := user_model.GetUserByIDCtx(ctx, team.OrgID) + if err != nil { + return nil, nil, nil, nil, err + } + + return invite, org_model.OrgFromUser(org), team, inviter, nil +} diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index c53a53b471..64a6f0ec53 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -216,7 +216,7 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{ CachedOnly: true, - Attributes: []string{"linguist-language", "gitlab-language"}, + Attributes: []git.CmdArg{"linguist-language", "gitlab-language"}, Filenames: []string{ctx.Repo.TreePath}, IndexFile: indexFilename, WorkTree: worktree, diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index e7e68d3c5e..db6b59471f 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -560,7 +560,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { func PrepareCompareDiff( ctx *context.Context, ci *CompareInfo, - whitespaceBehavior string, + whitespaceBehavior git.CmdArg, ) bool { var ( repo = ctx.Repo.Repository diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go index 5aa2bcd134..1ec781bb13 100644 --- a/routers/web/repo/http.go +++ b/routers/web/repo/http.go @@ -398,7 +398,7 @@ func (h *serviceHandler) sendFile(contentType, file string) { var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`) func getGitConfig(ctx gocontext.Context, option, dir string) string { - out, _, err := git.NewCommand(ctx, "config", option).RunStdString(&git.RunOpts{Dir: dir}) + out, _, err := git.NewCommand(ctx, "config").AddDynamicArguments(option).RunStdString(&git.RunOpts{Dir: dir}) if err != nil { log.Error("%v - %s", err, out) } @@ -471,7 +471,7 @@ func serviceRPC(ctx gocontext.Context, h serviceHandler, service string) { } var stderr bytes.Buffer - cmd := git.NewCommand(h.r.Context(), service, "--stateless-rpc", h.dir) + cmd := git.NewCommand(h.r.Context(), git.CmdArgCheck(service), "--stateless-rpc").AddDynamicArguments(h.dir) cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir)) if err := cmd.Run(&git.RunOpts{ Dir: h.dir, @@ -543,7 +543,7 @@ func GetInfoRefs(ctx *context.Context) { } h.environ = append(os.Environ(), h.environ...) - refs, _, err := git.NewCommand(ctx, service, "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.dir}) + refs, _, err := git.NewCommand(ctx, git.CmdArgCheck(service), "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.dir}) if err != nil { log.Error(fmt.Sprintf("%v - %s", err, string(refs))) } diff --git a/routers/web/repo/lfs.go b/routers/web/repo/lfs.go index 633b8ab1a5..41639c4603 100644 --- a/routers/web/repo/lfs.go +++ b/routers/web/repo/lfs.go @@ -147,7 +147,7 @@ func LFSLocks(ctx *context.Context) { } name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{ - Attributes: []string{"lockable"}, + Attributes: []git.CmdArg{"lockable"}, Filenames: filenames, CachedOnly: true, }) diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go index ae4177cf1e..c9e8eb4a89 100644 --- a/routers/web/repo/middlewares.go +++ b/routers/web/repo/middlewares.go @@ -7,7 +7,7 @@ package repo import ( "fmt" - admin_model "code.gitea.io/gitea/models/admin" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" @@ -24,7 +24,7 @@ func SetEditorconfigIfExists(ctx *context.Context) { if err != nil && !git.IsErrNotExist(err) { description := fmt.Sprintf("Error while getting .editorconfig file: %v", err) - if err := admin_model.CreateRepositoryNotice(description); err != nil { + if err := system_model.CreateRepositoryNotice(description); err != nil { ctx.ServerError("ErrCreatingReporitoryNotice", err) } return diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index aa2c4cdb53..fc95bbf240 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -359,7 +359,7 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) } if commitSHA != "" { // Get immediate parent of the first commit in the patch, grab history back - parentCommit, _, err = git.NewCommand(ctx, "rev-list", "-1", "--skip=1", commitSHA).RunStdString(&git.RunOpts{Dir: ctx.Repo.GitRepo.Path}) + parentCommit, _, err = git.NewCommand(ctx, "rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(&git.RunOpts{Dir: ctx.Repo.GitRepo.Path}) if err == nil { parentCommit = strings.TrimSpace(parentCommit) } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index a43840467d..d35ec48df0 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -119,6 +119,7 @@ func renderDirectory(ctx *context.Context, treeLink string) { } if ctx.Repo.TreePath != "" { + ctx.Data["HideRepoInfo"] = true ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) } @@ -150,8 +151,8 @@ func localizedExtensions(ext, languageCode string) (localizedExts []string) { if strings.Contains(lowerLangCode, "-") { underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_") indexOfDash := strings.Index(lowerLangCode, "-") - // e.g. [.zh-cn.md, .zh_cn.md, .zh.md, .md] - return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, ext} + // e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md] + return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext} } // e.g. [.en.md, .md] @@ -360,6 +361,7 @@ func renderReadmeFile(ctx *context.Context, readmeFile *namedBlob, readmeTreelin func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink string) { ctx.Data["IsViewFile"] = true + ctx.Data["HideRepoInfo"] = true blob := entry.Blob() dataRc, err := blob.DataAsync() if err != nil { @@ -549,7 +551,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{ CachedOnly: true, - Attributes: []string{"linguist-language", "gitlab-language"}, + Attributes: []git.CmdArg{"linguist-language", "gitlab-language"}, Filenames: []string{ctx.Repo.TreePath}, IndexFile: indexFilename, WorkTree: worktree, diff --git a/routers/web/repo/view_test.go b/routers/web/repo/view_test.go index 9d5a88fca4..803906b217 100644 --- a/routers/web/repo/view_test.go +++ b/routers/web/repo/view_test.go @@ -38,19 +38,19 @@ func Test_localizedExtensions(t *testing.T) { name: "With region - lowercase", languageCode: "en-us", ext: ".md", - wantLocalizedExts: []string{".en-us.md", ".en_us.md", ".en.md", ".md"}, + wantLocalizedExts: []string{".en-us.md", ".en_us.md", ".en.md", "_en.md", ".md"}, }, { name: "With region - uppercase", languageCode: "en-CA", ext: ".MD", - wantLocalizedExts: []string{".en-ca.MD", ".en_ca.MD", ".en.MD", ".MD"}, + wantLocalizedExts: []string{".en-ca.MD", ".en_ca.MD", ".en.MD", "_en.MD", ".MD"}, }, { name: "With region - all uppercase", languageCode: "ZH-TW", ext: ".md", - wantLocalizedExts: []string{".zh-tw.md", ".zh_tw.md", ".zh.md", ".md"}, + wantLocalizedExts: []string{".zh-tw.md", ".zh_tw.md", ".zh.md", "_zh.md", ".md"}, }, } for _, tt := range tests { diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index a8939e72bd..ee980333b7 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -633,7 +633,7 @@ func TestWebhook(ctx *context.Context) { hookID := ctx.ParamsInt64(":id") w, err := webhook.GetWebhookByRepoID(ctx.Repo.Repository.ID, hookID) if err != nil { - ctx.Flash.Error("GetWebhookByID: " + err.Error()) + ctx.Flash.Error("GetWebhookByRepoID: " + err.Error()) ctx.Status(http.StatusInternalServerError) return } @@ -668,17 +668,18 @@ func TestWebhook(ctx *context.Context) { commitID := commit.ID.String() p := &api.PushPayload{ - Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch, - Before: commitID, - After: commitID, - CompareURL: setting.AppURL + ctx.Repo.Repository.ComposeCompareURL(commitID, commitID), - Commits: []*api.PayloadCommit{apiCommit}, - HeadCommit: apiCommit, - Repo: convert.ToRepo(ctx.Repo.Repository, perm.AccessModeNone), - Pusher: apiUser, - Sender: apiUser, + Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch, + Before: commitID, + After: commitID, + CompareURL: setting.AppURL + ctx.Repo.Repository.ComposeCompareURL(commitID, commitID), + Commits: []*api.PayloadCommit{apiCommit}, + TotalCommits: 1, + HeadCommit: apiCommit, + Repo: convert.ToRepo(ctx.Repo.Repository, perm.AccessModeNone), + Pusher: apiUser, + Sender: apiUser, } - if err := webhook_service.PrepareWebhook(w, ctx.Repo.Repository, webhook.HookEventPush, p); err != nil { + if err := webhook_service.PrepareWebhook(ctx, w, webhook.HookEventPush, p); err != nil { ctx.Flash.Error("PrepareWebhook: " + err.Error()) ctx.Status(http.StatusInternalServerError) } else { @@ -696,7 +697,7 @@ func ReplayWebhook(ctx *context.Context) { return } - if err := webhook_service.ReplayHookTask(w, hookTaskUUID); err != nil { + if err := webhook_service.ReplayHookTask(ctx, w, hookTaskUUID); err != nil { if webhook.IsErrHookTaskNotExist(err) { ctx.NotFound("ReplayHookTask", nil) } else { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 0f349547cd..a10e12ee63 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -99,7 +99,7 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err return nil, nil, err } - commit, err := wikiRepo.GetBranchCommit("master") + commit, err := wikiRepo.GetBranchCommit(wiki_service.DefaultBranch) if err != nil { return wikiRepo, nil, err } @@ -302,7 +302,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Data["toc"] = rctx.TableOfContents // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount return wikiRepo, entry @@ -351,7 +351,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["footerContent"] = "" // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount // get page @@ -361,7 +361,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) } // get Commit Count - commitsHistory, err := wikiRepo.CommitsByFileAndRange("master", pageFilename, page) + commitsHistory, err := wikiRepo.CommitsByFileAndRange(wiki_service.DefaultBranch, pageFilename, page) if err != nil { if wikiRepo != nil { wikiRepo.Close() diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go index 53a603fab0..05896299d2 100644 --- a/routers/web/user/avatar.go +++ b/routers/web/user/avatar.go @@ -31,6 +31,10 @@ func AvatarByUserName(ctx *context.Context) { if strings.ToLower(userName) != "ghost" { var err error if user, err = user_model.GetUserByName(ctx, userName); err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.NotFound("GetUserByName", err) + return + } ctx.ServerError("Invalid user: "+userName, err) return } diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go index 9ad0711dc0..36e99bba5e 100644 --- a/routers/web/user/home_test.go +++ b/routers/web/user/home_test.go @@ -76,7 +76,7 @@ func TestPulls(t *testing.T) { Pulls(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) - assert.Len(t, ctx.Data["Issues"], 3) + assert.Len(t, ctx.Data["Issues"], 4) } func TestMilestones(t *testing.T) { diff --git a/routers/web/user/package.go b/routers/web/user/package.go index c72592e728..7179e2df97 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -233,6 +233,7 @@ func ListPackageVersions(ctx *context.Context) { } query := ctx.FormTrim("q") + sort := ctx.FormTrim("sort") ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["IsPackagesPage"] = true @@ -243,9 +244,11 @@ func ListPackageVersions(ctx *context.Context) { Owner: ctx.Package.Owner, } ctx.Data["Query"] = query + ctx.Data["Sort"] = sort pagerParams := map[string]string{ - "q": query, + "q": query, + "sort": sort, } var ( @@ -264,6 +267,7 @@ func ListPackageVersions(ctx *context.Context) { PackageID: p.ID, Query: query, IsTagged: tagged == "" || tagged == "tagged", + Sort: sort, }) if err != nil { ctx.ServerError("SearchImageTags", err) @@ -278,6 +282,7 @@ func ListPackageVersions(ctx *context.Context) { Value: query, }, IsInternal: util.OptionalBoolFalse, + Sort: sort, }) if err != nil { ctx.ServerError("SearchVersions", err) diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go index f02f6ab041..49ee5c7c2f 100644 --- a/routers/web/user/setting/oauth2_common.go +++ b/routers/web/user/setting/oauth2_common.go @@ -39,9 +39,10 @@ func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) { // TODO validate redirect URI app, err := auth.CreateOAuth2Application(ctx, auth.CreateOAuth2ApplicationOptions{ - Name: form.Name, - RedirectURIs: []string{form.RedirectURI}, - UserID: oa.OwnerID, + Name: form.Name, + RedirectURIs: []string{form.RedirectURI}, + UserID: oa.OwnerID, + ConfidentialClient: form.ConfidentialClient, }) if err != nil { ctx.ServerError("CreateOAuth2Application", err) @@ -90,10 +91,11 @@ func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) { // TODO validate redirect URI var err error if ctx.Data["App"], err = auth.UpdateOAuth2Application(auth.UpdateOAuth2ApplicationOptions{ - ID: ctx.ParamsInt64("id"), - Name: form.Name, - RedirectURIs: []string{form.RedirectURI}, - UserID: oa.OwnerID, + ID: ctx.ParamsInt64("id"), + Name: form.Name, + RedirectURIs: []string{form.RedirectURI}, + UserID: oa.OwnerID, + ConfidentialClient: form.ConfidentialClient, }); err != nil { ctx.ServerError("UpdateOAuth2Application", err) return diff --git a/routers/web/web.go b/routers/web/web.go index c01a2bce40..9b814c3f54 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -473,8 +473,13 @@ func RegisterRoutes(m *web.Route) { m.Group("/admin", func() { m.Get("", adminReq, admin.Dashboard) m.Post("", adminReq, bindIgnErr(forms.AdminDashboardForm{}), admin.DashboardPost) - m.Get("/config", admin.Config) - m.Post("/config/test_mail", admin.SendTestMail) + + m.Group("/config", func() { + m.Get("", admin.Config) + m.Post("", admin.ChangeConfig) + m.Post("/test_mail", admin.SendTestMail) + }) + m.Group("/monitor", func() { m.Get("", admin.Monitor) m.Get("/stacktrace", admin.GoroutineStacktrace) @@ -586,6 +591,7 @@ func RegisterRoutes(m *web.Route) { }) }, func(ctx *context.Context) { ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable + ctx.Data["EnablePackages"] = setting.Packages.Enabled }, adminReq) // ***** END: Admin ***** @@ -646,6 +652,11 @@ func RegisterRoutes(m *web.Route) { m.Post("/create", bindIgnErr(forms.CreateOrgForm{}), org.CreatePost) }) + m.Group("/invite/{token}", func() { + m.Get("", org.TeamInvite) + m.Post("", org.TeamInvitePost) + }) + m.Group("/{org}", func() { m.Get("/dashboard", user.Dashboard) m.Get("/dashboard/{team}", user.Dashboard) diff --git a/services/agit/agit.go b/services/agit/agit.go index 9f0ce75123..cea2b1f580 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -179,7 +179,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. } if !forcePush { - output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1", oldCommitID, "^"+opts.NewCommitIDs[i]).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()}) + output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()}) if err != nil { return nil, fmt.Errorf("Fail to detect force push: %v", err) } else if len(output) > 0 { diff --git a/services/cron/tasks.go b/services/cron/tasks.go index c26e47e0ce..6ff5964d1e 100644 --- a/services/cron/tasks.go +++ b/services/cron/tasks.go @@ -10,8 +10,8 @@ import ( "reflect" "sync" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" @@ -114,7 +114,7 @@ func (t *Task) RunWithUser(doer *user_model.User, config Config) { t.LastDoer = doerName t.lock.Unlock() - if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "cancelled", doerName, message)); err != nil { + if err := system_model.CreateNotice(ctx, system_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "cancelled", doerName, message)); err != nil { log.Error("CreateNotice: %v", err) } return @@ -127,7 +127,7 @@ func (t *Task) RunWithUser(doer *user_model.User, config Config) { t.lock.Unlock() if config.DoNoticeOnSuccess() { - if err := admin_model.CreateNotice(ctx, admin_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "finished", doerName)); err != nil { + if err := system_model.CreateNotice(ctx, system_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "finished", doerName)); err != nil { log.Error("CreateNotice: %v", err) } } diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go index 0d7ef4af03..ca35f5be57 100644 --- a/services/cron/tasks_basic.go +++ b/services/cron/tasks_basic.go @@ -12,6 +12,7 @@ import ( git_model "code.gitea.io/gitea/models/git" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/migrations" @@ -58,7 +59,12 @@ func registerRepoHealthCheck() { Args: []string{}, }, func(ctx context.Context, _ *user_model.User, config Config) error { rhcConfig := config.(*RepoHealthCheckConfig) - return repo_service.GitFsck(ctx, rhcConfig.Timeout, rhcConfig.Args) + // the git args are set by config, they can be safe to be trusted + args := make([]git.CmdArg, 0, len(rhcConfig.Args)) + for _, arg := range rhcConfig.Args { + args = append(args, git.CmdArg(arg)) + } + return repo_service.GitFsck(ctx, rhcConfig.Timeout, args) }) } diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go index c3455ec327..c730477cbd 100644 --- a/services/cron/tasks_extended.go +++ b/services/cron/tasks_extended.go @@ -9,10 +9,11 @@ import ( "time" activities_model "code.gitea.io/gitea/models/activities" - "code.gitea.io/gitea/models/admin" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/updatechecker" repo_service "code.gitea.io/gitea/services/repository" @@ -60,7 +61,12 @@ func registerGarbageCollectRepositories() { Args: setting.Git.GCArgs, }, func(ctx context.Context, _ *user_model.User, config Config) error { rhcConfig := config.(*RepoHealthCheckConfig) - return repo_service.GitGcRepos(ctx, rhcConfig.Timeout, rhcConfig.Args...) + // the git args are set by config, they can be safe to be trusted + args := make([]git.CmdArg, 0, len(rhcConfig.Args)) + for _, arg := range rhcConfig.Args { + args = append(args, git.CmdArg(arg)) + } + return repo_service.GitGcRepos(ctx, rhcConfig.Timeout, args...) }) } @@ -166,7 +172,7 @@ func registerDeleteOldSystemNotices() { OlderThan: 365 * 24 * time.Hour, }, func(ctx context.Context, _ *user_model.User, config Config) error { olderThanConfig := config.(*OlderThanConfig) - return admin.DeleteOldSystemNotices(olderThanConfig.OlderThan) + return system.DeleteOldSystemNotices(olderThanConfig.OlderThan) }) } diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 8ce1d85c57..036c2ca3ec 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -379,8 +379,9 @@ func (f *NewAccessTokenForm) Validate(req *http.Request, errs binding.Errors) bi // EditOAuth2ApplicationForm form for editing oauth2 applications type EditOAuth2ApplicationForm struct { - Name string `binding:"Required;MaxSize(255)" form:"application_name"` - RedirectURI string `binding:"Required" form:"redirect_uri"` + Name string `binding:"Required;MaxSize(255)" form:"application_name"` + RedirectURI string `binding:"Required" form:"redirect_uri"` + ConfidentialClient bool `form:"confidential_client"` } // Validate validates the fields diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index e7362cdcdd..3c8c5c81a5 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1056,7 +1056,7 @@ type DiffOptions struct { MaxLines int MaxLineCharacters int MaxFiles int - WhitespaceBehavior string + WhitespaceBehavior git.CmdArg DirectComparison bool } @@ -1082,7 +1082,7 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff argsLength += len(files) + 1 } - diffArgs := make([]string, 0, argsLength) + diffArgs := make([]git.CmdArg, 0, argsLength) if (len(opts.BeforeCommitID) == 0 || opts.BeforeCommitID == git.EmptySHA) && commit.ParentCount() == 0 { diffArgs = append(diffArgs, "diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M") if len(opts.WhitespaceBehavior) != 0 { @@ -1090,7 +1090,7 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff } // append empty tree ref diffArgs = append(diffArgs, "4b825dc642cb6eb9a060e54bf8d69288fbee4904") - diffArgs = append(diffArgs, opts.AfterCommitID) + diffArgs = append(diffArgs, git.CmdArgCheck(opts.AfterCommitID)) } else { actualBeforeCommitID := opts.BeforeCommitID if len(actualBeforeCommitID) == 0 { @@ -1101,8 +1101,8 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff if len(opts.WhitespaceBehavior) != 0 { diffArgs = append(diffArgs, opts.WhitespaceBehavior) } - diffArgs = append(diffArgs, actualBeforeCommitID) - diffArgs = append(diffArgs, opts.AfterCommitID) + diffArgs = append(diffArgs, git.CmdArgCheck(actualBeforeCommitID)) + diffArgs = append(diffArgs, git.CmdArgCheck(opts.AfterCommitID)) opts.BeforeCommitID = actualBeforeCommitID } @@ -1111,13 +1111,15 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff // the skipping for us parsePatchSkipToFile := opts.SkipTo if opts.SkipTo != "" && git.CheckGitVersionAtLeast("2.31") == nil { - diffArgs = append(diffArgs, "--skip-to="+opts.SkipTo) + diffArgs = append(diffArgs, git.CmdArg("--skip-to="+opts.SkipTo)) parsePatchSkipToFile = "" } if len(files) > 0 { diffArgs = append(diffArgs, "--") - diffArgs = append(diffArgs, files...) + for _, file := range files { + diffArgs = append(diffArgs, git.CmdArg(file)) // it's safe to cast it to CmdArg because there is a "--" before + } } reader, writer := io.Pipe() @@ -1126,7 +1128,7 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff _ = writer.Close() }() - go func(ctx context.Context, diffArgs []string, repoPath string, writer *io.PipeWriter) { + go func(ctx context.Context, diffArgs []git.CmdArg, repoPath string, writer *io.PipeWriter) { cmd := git.NewCommand(ctx, diffArgs...) cmd.SetDescription(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath)) if err := cmd.Run(&git.RunOpts{ @@ -1199,15 +1201,15 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff separator = ".." } - shortstatArgs := []string{opts.BeforeCommitID + separator + opts.AfterCommitID} + shortstatArgs := []git.CmdArg{git.CmdArgCheck(opts.BeforeCommitID + separator + opts.AfterCommitID)} if len(opts.BeforeCommitID) == 0 || opts.BeforeCommitID == git.EmptySHA { - shortstatArgs = []string{git.EmptyTreeSHA, opts.AfterCommitID} + shortstatArgs = []git.CmdArg{git.EmptyTreeSHA, git.CmdArgCheck(opts.AfterCommitID)} } diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(gitRepo.Ctx, repoPath, shortstatArgs...) if err != nil && strings.Contains(err.Error(), "no merge base") { // git >= 2.28 now returns an error if base and head have become unrelated. // previously it would return the results of git diff --shortstat base head so let's try that... - shortstatArgs = []string{opts.BeforeCommitID, opts.AfterCommitID} + shortstatArgs = []git.CmdArg{git.CmdArgCheck(opts.BeforeCommitID), git.CmdArgCheck(opts.AfterCommitID)} diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(gitRepo.Ctx, repoPath, shortstatArgs...) } if err != nil { @@ -1235,8 +1237,13 @@ func SyncAndGetUserSpecificDiff(ctx context.Context, userID int64, pull *issues_ } changedFiles, err := gitRepo.GetFilesChangedBetween(review.CommitSHA, latestCommit) + // There are way too many possible errors. + // Examples are various git errors such as the commit the review was based on was gc'ed and hence doesn't exist anymore as well as unrecoverable errors where we should serve a 500 response + // Due to the current architecture and physical limitation of needing to compare explicit error messages, we can only choose one approach without the code getting ugly + // For SOME of the errors such as the gc'ed commit, it would be best to mark all files as changed + // But as that does not work for all potential errors, we simply mark all files as unchanged and drop the error which always works, even if not as good as possible if err != nil { - return diff, err + log.Error("Could not get changed files between %s and %s for pull request %d in repo with path %s. Assuming no changes. Error: %w", review.CommitSHA, latestCommit, pull.Index, gitRepo.Path, err) } filesChangedSinceLastDiff := make(map[string]pull_model.ViewedState) @@ -1317,7 +1324,7 @@ func CommentMustAsDiff(c *issues_model.Comment) *Diff { } // GetWhitespaceFlag returns git diff flag for treating whitespaces -func GetWhitespaceFlag(whitespaceBehavior string) string { +func GetWhitespaceFlag(whitespaceBehavior string) git.CmdArg { whitespaceFlags := map[string]string{ "ignore-all": "-w", "ignore-change": "-b", @@ -1326,7 +1333,7 @@ func GetWhitespaceFlag(whitespaceBehavior string) string { } if flag, ok := whitespaceFlags[whitespaceBehavior]; ok { - return flag + return git.CmdArg(flag) } log.Warn("unknown whitespace behavior: %q, default to 'show-all'", whitespaceBehavior) return "" diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index dfdd4df9c4..a7fd677fef 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -627,7 +627,7 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) { return } defer gitRepo.Close() - for _, behavior := range []string{"-w", "--ignore-space-at-eol", "-b", ""} { + for _, behavior := range []git.CmdArg{"-w", "--ignore-space-at-eol", "-b", ""} { diffs, err := GetDiff(gitRepo, &DiffOptions{ AfterCommitID: "bd7063cc7c04689c4d082183d32a604ed27a24f9", diff --git a/services/issue/issue.go b/services/issue/issue.go index bbd0278792..47782e50d3 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -8,12 +8,12 @@ import ( "fmt" activities_model "code.gitea.io/gitea/models/activities" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/notification" @@ -224,6 +224,11 @@ func deleteIssue(issue *issues_model.Issue) error { return err } + if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { + return fmt.Errorf("error updating counters for milestone id %d: %w", + issue.MilestoneID, err) + } + if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID); err != nil { return err } @@ -234,7 +239,7 @@ func deleteIssue(issue *issues_model.Issue) error { } for i := range issue.Attachments { - admin_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath()) + system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath()) } // delete all database data still assigned to this issue diff --git a/services/lfs/server.go b/services/lfs/server.go index b868db39db..830112fac6 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -438,14 +438,21 @@ func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, downloa } if download { - rep.Actions["download"] = &lfs_module.Link{Href: rc.DownloadLink(pointer), Header: header} + var link *lfs_module.Link if setting.LFS.ServeDirect { // If we have a signed url (S3, object storage), redirect to this directly. u, err := storage.LFS.URL(pointer.RelativePath(), pointer.Oid) if u != nil && err == nil { - rep.Actions["download"] = &lfs_module.Link{Href: u.String(), Header: header} + // Presigned url does not need the Authorization header + // https://github.com/go-gitea/gitea/issues/21525 + delete(header, "Authorization") + link = &lfs_module.Link{Href: u.String(), Header: header} } } + if link == nil { + link = &lfs_module.Link{Href: rc.DownloadLink(pointer), Header: header} + } + rep.Actions["download"] = link } if upload { rep.Actions["upload"] = &lfs_module.Link{Href: rc.UploadLink(pointer), Header: header} diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go index 7c44f93929..6df3fbbf1d 100644 --- a/services/mailer/mail_release.go +++ b/services/mailer/mail_release.go @@ -23,7 +23,7 @@ const ( tplNewReleaseMail base.TplName = "release" ) -// MailNewRelease send new release notify to all all repo watchers. +// MailNewRelease send new release notify to all repo watchers. func MailNewRelease(ctx context.Context, rel *repo_model.Release) { if setting.MailService == nil { // No mail service configured diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go new file mode 100644 index 0000000000..c2b2a00e76 --- /dev/null +++ b/services/mailer/mail_team_invite.go @@ -0,0 +1,62 @@ +// 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 mailer + +import ( + "bytes" + "context" + + org_model "code.gitea.io/gitea/models/organization" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/translation" +) + +const ( + tplTeamInviteMail base.TplName = "team_invite" +) + +// MailTeamInvite sends team invites +func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_model.Team, invite *org_model.TeamInvite) error { + if setting.MailService == nil { + return nil + } + + org, err := user_model.GetUserByIDCtx(ctx, team.OrgID) + if err != nil { + return err + } + + locale := translation.NewLocale(inviter.Language) + + subject := locale.Tr("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName()) + mailMeta := map[string]interface{}{ + "Inviter": inviter, + "Organization": org, + "Team": team, + "Invite": invite, + "Subject": subject, + // helper + "locale": locale, + "Str2html": templates.Str2html, + "DotEscape": templates.DotEscape, + } + + var mailBody bytes.Buffer + if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplTeamInviteMail), mailMeta); err != nil { + log.Error("ExecuteTemplate [%s]: %v", string(tplTeamInviteMail)+"/body", err) + return err + } + + msg := NewMessage([]string{invite.Email}, subject, mailBody.String()) + msg.Info = subject + + SendAsync(msg) + + return nil +} diff --git a/services/markup/main_test.go b/services/markup/main_test.go new file mode 100644 index 0000000000..8efd08e69d --- /dev/null +++ b/services/markup/main_test.go @@ -0,0 +1,19 @@ +// 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 markup + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + GiteaRootPath: filepath.Join("..", ".."), + FixtureFiles: []string{"user.yml"}, + }) +} diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go new file mode 100644 index 0000000000..5042884e5e --- /dev/null +++ b/services/markup/processorhelper.go @@ -0,0 +1,33 @@ +// 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 markup + +import ( + "context" + + "code.gitea.io/gitea/models/user" + gitea_context "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/markup" +) + +func ProcessorHelper() *markup.ProcessorHelper { + return &markup.ProcessorHelper{ + IsUsernameMentionable: func(ctx context.Context, username string) bool { + mentionedUser, err := user.GetUserByName(ctx, username) + if err != nil { + return false + } + + giteaCtx, ok := ctx.(*gitea_context.Context) + if !ok { + // when using general context, use user's visibility to check + return mentionedUser.Visibility.IsPublic() + } + + // when using gitea context (web context), use user's visibility and user's permission to check + return user.IsUserVisibleToViewer(giteaCtx, mentionedUser, giteaCtx.Doer) + }, + } +} diff --git a/services/markup/processorhelper_test.go b/services/markup/processorhelper_test.go new file mode 100644 index 0000000000..f7eab3d958 --- /dev/null +++ b/services/markup/processorhelper_test.go @@ -0,0 +1,53 @@ +// 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 markup + +import ( + "context" + "net/http" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/models/user" + gitea_context "code.gitea.io/gitea/modules/context" + + "github.com/stretchr/testify/assert" +) + +func TestProcessorHelper(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + userPublic := "user1" + userPrivate := "user31" + userLimited := "user33" + userNoSuch := "no-such-user" + + unittest.AssertCount(t, &user.User{Name: userPublic}, 1) + unittest.AssertCount(t, &user.User{Name: userPrivate}, 1) + unittest.AssertCount(t, &user.User{Name: userLimited}, 1) + unittest.AssertCount(t, &user.User{Name: userNoSuch}, 0) + + // when using general context, use user's visibility to check + assert.True(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPublic)) + assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userLimited)) + assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPrivate)) + assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userNoSuch)) + + // when using web context, use user.IsUserVisibleToViewer to check + var err error + giteaCtx := &gitea_context.Context{} + giteaCtx.Req, err = http.NewRequest("GET", "/", nil) + assert.NoError(t, err) + + giteaCtx.Doer = nil + assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) + assert.False(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) + + giteaCtx.Doer, err = user.GetUserByName(db.DefaultContext, userPrivate) + assert.NoError(t, err) + assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) + assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) +} diff --git a/services/migrations/common.go b/services/migrations/common.go index 305ae89b2d..052975c9e7 100644 --- a/services/migrations/common.go +++ b/services/migrations/common.go @@ -8,7 +8,7 @@ import ( "fmt" "strings" - admin_model "code.gitea.io/gitea/models/admin" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" @@ -17,7 +17,7 @@ import ( // WarnAndNotice will log the provided message and send a repository notice func WarnAndNotice(fmtStr string, args ...interface{}) { log.Warn(fmtStr, args...) - if err := admin_model.CreateRepositoryNotice(fmt.Sprintf(fmtStr, args...)); err != nil { + if err := system_model.CreateRepositoryNotice(fmt.Sprintf(fmtStr, args...)); err != nil { log.Error("create repository notice failed: ", err) } } diff --git a/services/migrations/dump.go b/services/migrations/dump.go index 188f2775e0..31fb1b4cf3 100644 --- a/services/migrations/dump.go +++ b/services/migrations/dump.go @@ -491,7 +491,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error { if pr.Head.CloneURL == "" || pr.Head.Ref == "" { // Set head information if pr.Head.SHA is available if pr.Head.SHA != "" { - _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref", pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) + _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) if err != nil { log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err) } @@ -521,7 +521,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error { if !ok { // Set head information if pr.Head.SHA is available if pr.Head.SHA != "" { - _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref", pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) + _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) if err != nil { log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err) } @@ -556,7 +556,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error { fetchArg = git.BranchPrefix + fetchArg } - _, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags", "--", remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.gitPath()}) + _, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.gitPath()}) if err != nil { log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err) // We need to continue here so that the Head.Ref is reset and we attempt to set the gitref for the PR @@ -580,7 +580,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error { pr.Head.SHA = headSha } if pr.Head.SHA != "" { - _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref", pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) + _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) if err != nil { log.Error("unable to set %s as the local head for PR #%d from %s in %s/%s. Error: %v", pr.Head.SHA, pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err) } diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 83388391da..c4cb59f572 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -626,7 +626,7 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head fetchArg = git.BranchPrefix + fetchArg } - _, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags", "--", remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) + _, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) if err != nil { log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err) return head, nil @@ -645,7 +645,7 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head pr.Head.SHA = headSha } - _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref", pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) + _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) if err != nil { return "", err } @@ -662,13 +662,13 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head // The SHA is empty log.Warn("Empty reference, no pull head for PR #%d in %s/%s", pr.Number, g.repoOwner, g.repoName) } else { - _, _, err = git.NewCommand(g.ctx, "rev-list", "--quiet", "-1", pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) + _, _, err = git.NewCommand(g.ctx, "rev-list", "--quiet", "-1").AddDynamicArguments(pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) if err != nil { // Git update-ref remove bad references with a relative path log.Warn("Deprecated local head %s for PR #%d in %s/%s, removing %s", pr.Head.SHA, pr.Number, g.repoOwner, g.repoName, pr.GetGitRefName()) } else { // set head information - _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref", pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) + _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) if err != nil { log.Error("unable to set %s as the local head for PR #%d from %s in %s/%s. Error: %v", pr.Head.SHA, pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err) } diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go index af6230decb..68a7182b07 100644 --- a/services/migrations/gitea_uploader_test.go +++ b/services/migrations/gitea_uploader_test.go @@ -234,7 +234,7 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) { fromRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) baseRef := "master" assert.NoError(t, git.InitRepository(git.DefaultContext, fromRepo.RepoPath(), false)) - err := git.NewCommand(git.DefaultContext, "symbolic-ref", "HEAD", git.BranchPrefix+baseRef).Run(&git.RunOpts{Dir: fromRepo.RepoPath()}) + err := git.NewCommand(git.DefaultContext, "symbolic-ref").AddDynamicArguments("HEAD", git.BranchPrefix+baseRef).Run(&git.RunOpts{Dir: fromRepo.RepoPath()}) assert.NoError(t, err) assert.NoError(t, os.WriteFile(filepath.Join(fromRepo.RepoPath(), "README.md"), []byte(fmt.Sprintf("# Testing Repository\n\nOriginally created in: %s", fromRepo.RepoPath())), 0o644)) assert.NoError(t, git.AddChanges(fromRepo.RepoPath(), true)) @@ -258,7 +258,7 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) { // fromRepo branch1 // headRef := "branch1" - _, _, err = git.NewCommand(git.DefaultContext, "checkout", "-b", headRef).RunStdString(&git.RunOpts{Dir: fromRepo.RepoPath()}) + _, _, err = git.NewCommand(git.DefaultContext, "checkout", "-b").AddDynamicArguments(headRef).RunStdString(&git.RunOpts{Dir: fromRepo.RepoPath()}) assert.NoError(t, err) assert.NoError(t, os.WriteFile(filepath.Join(fromRepo.RepoPath(), "README.md"), []byte("SOMETHING"), 0o644)) assert.NoError(t, git.AddChanges(fromRepo.RepoPath(), true)) @@ -279,10 +279,10 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) { // forkHeadRef := "branch2" forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8}) - assert.NoError(t, git.CloneWithArgs(git.DefaultContext, fromRepo.RepoPath(), forkRepo.RepoPath(), []string{}, git.CloneRepoOptions{ + assert.NoError(t, git.CloneWithArgs(git.DefaultContext, nil, fromRepo.RepoPath(), forkRepo.RepoPath(), git.CloneRepoOptions{ Branch: headRef, })) - _, _, err = git.NewCommand(git.DefaultContext, "checkout", "-b", forkHeadRef).RunStdString(&git.RunOpts{Dir: forkRepo.RepoPath()}) + _, _, err = git.NewCommand(git.DefaultContext, "checkout", "-b").AddDynamicArguments(forkHeadRef).RunStdString(&git.RunOpts{Dir: forkRepo.RepoPath()}) assert.NoError(t, err) assert.NoError(t, os.WriteFile(filepath.Join(forkRepo.RepoPath(), "README.md"), []byte(fmt.Sprintf("# branch2 %s", forkRepo.RepoPath())), 0o644)) assert.NoError(t, git.AddChanges(forkRepo.RepoPath(), true)) diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go index 040f0aebb1..dfb21b884b 100644 --- a/services/migrations/migrate.go +++ b/services/migrations/migrate.go @@ -14,8 +14,8 @@ import ( "strings" "code.gitea.io/gitea/models" - admin_model "code.gitea.io/gitea/models/admin" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/hostmatcher" "code.gitea.io/gitea/modules/log" @@ -132,7 +132,7 @@ func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName str if err1 := uploader.Rollback(); err1 != nil { log.Error("rollback failed: %v", err1) } - if err2 := admin_model.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.OriginalURL, err)); err2 != nil { + if err2 := system_model.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.OriginalURL, err)); err2 != nil { log.Error("create respotiry notice failed: ", err2) } return nil, err diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index f4c527bbdc..6002e6b8ed 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -10,9 +10,9 @@ import ( "strings" "time" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/lfs" @@ -33,12 +33,12 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error remoteName := m.GetRemoteName() repoPath := m.GetRepository().RepoPath() // Remove old remote - _, _, err := git.NewCommand(ctx, "remote", "rm", remoteName).RunStdString(&git.RunOpts{Dir: repoPath}) + _, _, err := git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: repoPath}) if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { return err } - cmd := git.NewCommand(ctx, "remote", "add", remoteName, "--mirror=fetch", addr) + cmd := git.NewCommand(ctx, "remote", "add").AddDynamicArguments(remoteName).AddArguments("--mirror=fetch").AddDynamicArguments(addr) if strings.Contains(addr, "://") && strings.Contains(addr, "@") { cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, util.SanitizeCredentialURLs(addr), repoPath)) } else { @@ -53,12 +53,12 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error wikiPath := m.Repo.WikiPath() wikiRemotePath := repo_module.WikiRemoteURL(ctx, addr) // Remove old remote of wiki - _, _, err = git.NewCommand(ctx, "remote", "rm", remoteName).RunStdString(&git.RunOpts{Dir: wikiPath}) + _, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: wikiPath}) if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { return err } - cmd = git.NewCommand(ctx, "remote", "add", remoteName, "--mirror=fetch", wikiRemotePath) + cmd = git.NewCommand(ctx, "remote", "add").AddDynamicArguments(remoteName).AddArguments("--mirror=fetch").AddDynamicArguments(wikiRemotePath) if strings.Contains(wikiRemotePath, "://") && strings.Contains(wikiRemotePath, "@") { cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, util.SanitizeCredentialURLs(wikiRemotePath), wikiPath)) } else { @@ -169,7 +169,7 @@ func pruneBrokenReferences(ctx context.Context, stderrBuilder.Reset() stdoutBuilder.Reset() - pruneErr := git.NewCommand(ctx, "remote", "prune", m.GetRemoteName()). + pruneErr := git.NewCommand(ctx, "remote", "prune").AddDynamicArguments(m.GetRemoteName()). SetDescription(fmt.Sprintf("Mirror.runSync %ssPrune references: %s ", wiki, m.Repo.FullName())). Run(&git.RunOpts{ Timeout: timeout, @@ -188,7 +188,7 @@ func pruneBrokenReferences(ctx context.Context, log.Error("Failed to prune mirror repository %s%-v references:\nStdout: %s\nStderr: %s\nErr: %v", wiki, m.Repo, stdoutMessage, stderrMessage, pruneErr) desc := fmt.Sprintf("Failed to prune mirror repository %s'%s' references: %s", wiki, repoPath, stderrMessage) - if err := admin_model.CreateRepositoryNotice(desc); err != nil { + if err := system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } // this if will only be reached on a successful prune so try to get the mirror again @@ -204,11 +204,11 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo log.Trace("SyncMirrors [repo: %-v]: running git remote update...", m.Repo) - gitArgs := []string{"remote", "update"} + gitArgs := []git.CmdArg{"remote", "update"} if m.EnablePrune { gitArgs = append(gitArgs, "--prune") } - gitArgs = append(gitArgs, m.GetRemoteName()) + gitArgs = append(gitArgs, git.CmdArgCheck(m.GetRemoteName())) remoteURL, remoteErr := git.GetRemoteURL(ctx, repoPath, m.GetRemoteName()) if remoteErr != nil { @@ -267,7 +267,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo if err != nil { log.Error("SyncMirrors [repo: %-v]: failed to update mirror repository:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err) desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, stderrMessage) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return nil, false @@ -309,7 +309,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo log.Trace("SyncMirrors [repo: %-v Wiki]: running git remote update...", m.Repo) stderrBuilder.Reset() stdoutBuilder.Reset() - if err := git.NewCommand(ctx, "remote", "update", "--prune", m.GetRemoteName()). + if err := git.NewCommand(ctx, "remote", "update", "--prune").AddDynamicArguments(m.GetRemoteName()). SetDescription(fmt.Sprintf("Mirror.runSync Wiki: %s ", m.Repo.FullName())). Run(&git.RunOpts{ Timeout: timeout, @@ -336,7 +336,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo stderrBuilder.Reset() stdoutBuilder.Reset() - if err = git.NewCommand(ctx, "remote", "update", "--prune", m.GetRemoteName()). + if err = git.NewCommand(ctx, "remote", "update", "--prune").AddDynamicArguments(m.GetRemoteName()). SetDescription(fmt.Sprintf("Mirror.runSync Wiki: %s ", m.Repo.FullName())). Run(&git.RunOpts{ Timeout: timeout, @@ -356,7 +356,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo if err != nil { log.Error("SyncMirrors [repo: %-v Wiki]: failed to update mirror repository wiki:\nStdout: %s\nStderr: %s\nErr: %v", m.Repo, stdoutMessage, stderrMessage, err) desc := fmt.Sprintf("Failed to update mirror repository wiki '%s': %s", wikiPath, stderrMessage) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return nil, false @@ -568,7 +568,7 @@ func checkAndUpdateEmptyRepository(m *repo_model.Mirror, gitRepo *git.Repository if !git.IsErrUnsupportedVersion(err) { log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err) desc := fmt.Sprintf("Failed to uupdate default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return false @@ -579,7 +579,7 @@ func checkAndUpdateEmptyRepository(m *repo_model.Mirror, gitRepo *git.Repository if err := repo_model.UpdateRepositoryCols(db.DefaultContext, m.Repo, "default_branch", "is_empty"); err != nil { log.Error("Failed to update default branch of repository %-v. Error: %v", m.Repo, err) desc := fmt.Sprintf("Failed to uupdate default branch of repository '%s': %v", m.Repo.RepoPath(), err) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return false diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 0c8960d78b..60611130ba 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -29,7 +29,7 @@ var stripExitStatus = regexp.MustCompile(`exit status \d+ - `) // AddPushMirrorRemote registers the push mirror remote. func AddPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr string) error { addRemoteAndConfig := func(addr, path string) error { - cmd := git.NewCommand(ctx, "remote", "add", "--mirror=push", m.RemoteName, addr) + cmd := git.NewCommand(ctx, "remote", "add", "--mirror=push").AddDynamicArguments(m.RemoteName, addr) if strings.Contains(addr, "://") && strings.Contains(addr, "@") { cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, util.SanitizeCredentialURLs(addr), path)) } else { @@ -38,10 +38,10 @@ func AddPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr str if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: path}); err != nil { return err } - if _, _, err := git.NewCommand(ctx, "config", "--add", "remote."+m.RemoteName+".push", "+refs/heads/*:refs/heads/*").RunStdString(&git.RunOpts{Dir: path}); err != nil { + if _, _, err := git.NewCommand(ctx, "config", "--add", git.CmdArg("remote."+m.RemoteName+".push"), "+refs/heads/*:refs/heads/*").RunStdString(&git.RunOpts{Dir: path}); err != nil { return err } - if _, _, err := git.NewCommand(ctx, "config", "--add", "remote."+m.RemoteName+".push", "+refs/tags/*:refs/tags/*").RunStdString(&git.RunOpts{Dir: path}); err != nil { + if _, _, err := git.NewCommand(ctx, "config", "--add", git.CmdArg("remote."+m.RemoteName+".push"), "+refs/tags/*:refs/tags/*").RunStdString(&git.RunOpts{Dir: path}); err != nil { return err } return nil @@ -65,7 +65,7 @@ func AddPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr str // RemovePushMirrorRemote removes the push mirror remote. func RemovePushMirrorRemote(ctx context.Context, m *repo_model.PushMirror) error { - cmd := git.NewCommand(ctx, "remote", "rm", m.RemoteName) + cmd := git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(m.RemoteName) _ = m.GetRepository() if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: m.Repo.RepoPath()}); err != nil { diff --git a/services/org/team_invite.go b/services/org/team_invite.go new file mode 100644 index 0000000000..1108a46da5 --- /dev/null +++ b/services/org/team_invite.go @@ -0,0 +1,23 @@ +// 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 org + +import ( + "context" + + org_model "code.gitea.io/gitea/models/organization" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/services/mailer" +) + +// CreateTeamInvite make a persistent invite in db and mail it +func CreateTeamInvite(ctx context.Context, inviter *user_model.User, team *org_model.Team, uname string) error { + invite, err := org_model.CreateTeamInvite(ctx, inviter, team, uname) + if err != nil { + return err + } + + return mailer.MailTeamInvite(ctx, inviter, team, invite) +} diff --git a/services/pull/check.go b/services/pull/check.go index 288f4dc0b7..765672190d 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -185,7 +185,7 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com headFile := pr.GetGitRefName() // Check if a pull request is merged into BaseBranch - _, _, err = git.NewCommand(ctx, "merge-base", "--is-ancestor", headFile, pr.BaseBranch). + _, _, err = git.NewCommand(ctx, "merge-base", "--is-ancestor").AddDynamicArguments(headFile, pr.BaseBranch). RunStdString(&git.RunOpts{Dir: pr.BaseRepo.RepoPath(), Env: []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()}}) if err != nil { // Errors are signaled by a non-zero status that is not 1 @@ -206,7 +206,7 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com cmd := commitID[:40] + ".." + pr.BaseBranch // Get the commit from BaseBranch where the pull request got merged - mergeCommit, _, err := git.NewCommand(ctx, "rev-list", "--ancestry-path", "--merges", "--reverse", cmd). + mergeCommit, _, err := git.NewCommand(ctx, "rev-list", "--ancestry-path", "--merges", "--reverse").AddDynamicArguments(cmd). RunStdString(&git.RunOpts{Dir: "", Env: []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()}}) if err != nil { return nil, fmt.Errorf("git rev-list --ancestry-path --merges --reverse: %v", err) diff --git a/services/pull/merge.go b/services/pull/merge.go index 6f3df6ab2a..f9a4e6705f 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -244,7 +244,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode stagingBranch := "staging" if expectedHeadCommitID != "" { - trackingCommitID, _, err := git.NewCommand(ctx, "show-ref", "--hash", git.BranchPrefix+trackingBranch).RunStdString(&git.RunOpts{Dir: tmpBasePath}) + trackingCommitID, _, err := git.NewCommand(ctx, "show-ref", "--hash").AddDynamicArguments(git.BranchPrefix + trackingBranch).RunStdString(&git.RunOpts{Dir: tmpBasePath}) if err != nil { log.Error("show-ref[%s] --hash refs/heads/trackingn: %v", tmpBasePath, git.BranchPrefix+trackingBranch, err) return "", fmt.Errorf("getDiffTree: %v", err) @@ -360,15 +360,15 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode committer := sig // Determine if we should sign - var signArg string + var signArg git.CmdArg sign, keyID, signer, _ := asymkey_service.SignMerge(ctx, pr, doer, tmpBasePath, "HEAD", trackingBranch) if sign { - signArg = "-S" + keyID + signArg = git.CmdArg("-S" + keyID) if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { committer = signer } } else { - signArg = "--no-gpg-sign" + signArg = git.CmdArg("--no-gpg-sign") } commitTimeStr := time.Now().Format(time.RFC3339) @@ -386,7 +386,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode // Merge commits. switch mergeStyle { case repo_model.MergeStyleMerge: - cmd := git.NewCommand(ctx, "merge", "--no-ff", "--no-commit", trackingBranch) + cmd := git.NewCommand(ctx, "merge", "--no-ff", "--no-commit").AddDynamicArguments(trackingBranch) if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil { log.Error("Unable to merge tracking into base: %v", err) return "", err @@ -402,7 +402,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode fallthrough case repo_model.MergeStyleRebaseMerge: // Checkout head branch - if err := git.NewCommand(ctx, "checkout", "-b", stagingBranch, trackingBranch). + if err := git.NewCommand(ctx, "checkout", "-b").AddDynamicArguments(stagingBranch, trackingBranch). Run(&git.RunOpts{ Dir: tmpBasePath, Stdout: &outbuf, @@ -415,7 +415,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode errbuf.Reset() // Rebase before merging - if err := git.NewCommand(ctx, "rebase", baseBranch). + if err := git.NewCommand(ctx, "rebase").AddDynamicArguments(baseBranch). Run(&git.RunOpts{ Dir: tmpBasePath, Stdout: &outbuf, @@ -468,7 +468,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode } // Checkout base branch again - if err := git.NewCommand(ctx, "checkout", baseBranch). + if err := git.NewCommand(ctx, "checkout").AddDynamicArguments(baseBranch). Run(&git.RunOpts{ Dir: tmpBasePath, Stdout: &outbuf, @@ -486,7 +486,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode } else { cmd.AddArguments("--no-ff", "--no-commit") } - cmd.AddArguments(stagingBranch) + cmd.AddDynamicArguments(stagingBranch) // Prepare merge with commit if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil { @@ -501,7 +501,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode } case repo_model.MergeStyleSquash: // Merge with squash - cmd := git.NewCommand(ctx, "merge", "--squash", trackingBranch) + cmd := git.NewCommand(ctx, "merge", "--squash").AddDynamicArguments(trackingBranch) if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil { log.Error("Unable to merge --squash tracking into base: %v", err) return "", err @@ -513,7 +513,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode } sig := pr.Issue.Poster.NewGitSig() if signArg == "" { - if err := git.NewCommand(ctx, "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message). + if err := git.NewCommand(ctx, "commit", git.CmdArg(fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email)), "-m").AddDynamicArguments(message). Run(&git.RunOpts{ Env: env, Dir: tmpBasePath, @@ -528,7 +528,10 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode // add trailer message += fmt.Sprintf("\nCo-authored-by: %s\nCo-committed-by: %s\n", sig.String(), sig.String()) } - if err := git.NewCommand(ctx, "commit", signArg, fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message). + if err := git.NewCommand(ctx, "commit"). + AddArguments(signArg). + AddArguments(git.CmdArg(fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email))). + AddArguments("-m").AddDynamicArguments(message). Run(&git.RunOpts{ Env: env, Dir: tmpBasePath, @@ -592,9 +595,9 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode var pushCmd *git.Command if mergeStyle == repo_model.MergeStyleRebaseUpdate { // force push the rebase result to head branch - pushCmd = git.NewCommand(ctx, "push", "-f", "head_repo", stagingBranch+":"+git.BranchPrefix+pr.HeadBranch) + pushCmd = git.NewCommand(ctx, "push", "-f", "head_repo").AddDynamicArguments(stagingBranch + ":" + git.BranchPrefix + pr.HeadBranch) } else { - pushCmd = git.NewCommand(ctx, "push", "origin", baseBranch+":"+git.BranchPrefix+pr.BaseBranch) + pushCmd = git.NewCommand(ctx, "push", "origin").AddDynamicArguments(baseBranch + ":" + git.BranchPrefix + pr.BaseBranch) } // Push back to upstream. @@ -629,10 +632,10 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode return mergeCommitID, nil } -func commitAndSignNoAuthor(ctx context.Context, pr *issues_model.PullRequest, message, signArg, tmpBasePath string, env []string) error { +func commitAndSignNoAuthor(ctx context.Context, pr *issues_model.PullRequest, message string, signArg git.CmdArg, tmpBasePath string, env []string) error { var outbuf, errbuf strings.Builder if signArg == "" { - if err := git.NewCommand(ctx, "commit", "-m", message). + if err := git.NewCommand(ctx, "commit", "-m").AddDynamicArguments(message). Run(&git.RunOpts{ Env: env, Dir: tmpBasePath, @@ -643,7 +646,7 @@ func commitAndSignNoAuthor(ctx context.Context, pr *issues_model.PullRequest, me return fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) } } else { - if err := git.NewCommand(ctx, "commit", signArg, "-m", message). + if err := git.NewCommand(ctx, "commit").AddArguments(signArg).AddArguments("-m").AddDynamicArguments(message). Run(&git.RunOpts{ Env: env, Dir: tmpBasePath, @@ -696,7 +699,7 @@ func getDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string) ( getDiffTreeFromBranch := func(repoPath, baseBranch, headBranch string) (string, error) { var outbuf, errbuf strings.Builder // Compute the diff-tree for sparse-checkout - if err := git.NewCommand(ctx, "diff-tree", "--no-commit-id", "--name-only", "-r", "-z", "--root", baseBranch, headBranch, "--"). + if err := git.NewCommand(ctx, "diff-tree", "--no-commit-id", "--name-only", "-r", "-z", "--root").AddDynamicArguments(baseBranch, headBranch). Run(&git.RunOpts{ Dir: repoPath, Stdout: &outbuf, diff --git a/services/pull/patch.go b/services/pull/patch.go index dafd577069..b9f190826a 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -178,7 +178,7 @@ func attemptMerge(ctx context.Context, file *unmergedFile, tmpBasePath string, g } // Need to get the objects from the object db to attempt to merge - root, _, err := git.NewCommand(ctx, "unpack-file", file.stage1.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath}) + root, _, err := git.NewCommand(ctx, "unpack-file").AddDynamicArguments(file.stage1.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath}) if err != nil { return fmt.Errorf("unable to get root object: %s at path: %s for merging. Error: %w", file.stage1.sha, file.stage1.path, err) } @@ -187,7 +187,7 @@ func attemptMerge(ctx context.Context, file *unmergedFile, tmpBasePath string, g _ = util.Remove(filepath.Join(tmpBasePath, root)) }() - base, _, err := git.NewCommand(ctx, "unpack-file", file.stage2.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath}) + base, _, err := git.NewCommand(ctx, "unpack-file").AddDynamicArguments(file.stage2.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath}) if err != nil { return fmt.Errorf("unable to get base object: %s at path: %s for merging. Error: %w", file.stage2.sha, file.stage2.path, err) } @@ -195,7 +195,7 @@ func attemptMerge(ctx context.Context, file *unmergedFile, tmpBasePath string, g defer func() { _ = util.Remove(base) }() - head, _, err := git.NewCommand(ctx, "unpack-file", file.stage3.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath}) + head, _, err := git.NewCommand(ctx, "unpack-file").AddDynamicArguments(file.stage3.sha).RunStdString(&git.RunOpts{Dir: tmpBasePath}) if err != nil { return fmt.Errorf("unable to get head object:%s at path: %s for merging. Error: %w", file.stage3.sha, file.stage3.path, err) } @@ -205,13 +205,13 @@ func attemptMerge(ctx context.Context, file *unmergedFile, tmpBasePath string, g }() // now git merge-file annoyingly takes a different order to the merge-tree ... - _, _, conflictErr := git.NewCommand(ctx, "merge-file", base, root, head).RunStdString(&git.RunOpts{Dir: tmpBasePath}) + _, _, conflictErr := git.NewCommand(ctx, "merge-file").AddDynamicArguments(base, root, head).RunStdString(&git.RunOpts{Dir: tmpBasePath}) if conflictErr != nil { return &errMergeConflict{file.stage2.path} } // base now contains the merged data - hash, _, err := git.NewCommand(ctx, "hash-object", "-w", "--path", file.stage2.path, base).RunStdString(&git.RunOpts{Dir: tmpBasePath}) + hash, _, err := git.NewCommand(ctx, "hash-object", "-w", "--path").AddDynamicArguments(file.stage2.path, base).RunStdString(&git.RunOpts{Dir: tmpBasePath}) if err != nil { return err } @@ -235,7 +235,7 @@ func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repo defer cancel() // First we use read-tree to do a simple three-way merge - if _, _, err := git.NewCommand(ctx, "read-tree", "-m", base, ours, theirs).RunStdString(&git.RunOpts{Dir: gitPath}); err != nil { + if _, _, err := git.NewCommand(ctx, "read-tree", "-m").AddDynamicArguments(base, ours, theirs).RunStdString(&git.RunOpts{Dir: gitPath}); err != nil { log.Error("Unable to run read-tree -m! Error: %v", err) return false, nil, fmt.Errorf("unable to run read-tree -m! Error: %v", err) } @@ -361,7 +361,7 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * prConfig := prUnit.PullRequestsConfig() // 6. Prepare the arguments to apply the patch against the index - args := []string{"apply", "--check", "--cached"} + args := []git.CmdArg{"apply", "--check", "--cached"} if prConfig.IgnoreWhitespaceConflicts { args = append(args, "--ignore-whitespace") } @@ -370,7 +370,7 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo * args = append(args, "--3way") is3way = true } - args = append(args, patchPath) + args = append(args, git.CmdArgCheck(patchPath)) // 7. Prep the pipe: // - Here we could do the equivalent of: diff --git a/services/pull/pull.go b/services/pull/pull.go index 9de7cb5d4f..3b58c675a3 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -490,7 +490,7 @@ func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) { return err } - _, _, err = git.NewCommand(ctx, "update-ref", pr.GetGitRefName(), pr.HeadCommitID).RunStdString(&git.RunOpts{Dir: pr.BaseRepo.RepoPath()}) + _, _, err = git.NewCommand(ctx, "update-ref").AddDynamicArguments(pr.GetGitRefName(), pr.HeadCommitID).RunStdString(&git.RunOpts{Dir: pr.BaseRepo.RepoPath()}) if err != nil { log.Error("Unable to update ref in base repository for PR[%d] Error: %v", pr.ID, err) } diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go index c1456ef0a9..6624f5659b 100644 --- a/services/pull/temp_repo.go +++ b/services/pull/temp_repo.go @@ -94,7 +94,7 @@ func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (str } var outbuf, errbuf strings.Builder - if err := git.NewCommand(ctx, "remote", "add", "-t", pr.BaseBranch, "-m", pr.BaseBranch, "origin", baseRepoPath). + if err := git.NewCommand(ctx, "remote", "add", "-t").AddDynamicArguments(pr.BaseBranch).AddArguments("-m").AddDynamicArguments(pr.BaseBranch).AddDynamicArguments("origin", baseRepoPath). Run(&git.RunOpts{ Dir: tmpBasePath, Stdout: &outbuf, @@ -109,7 +109,7 @@ func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (str outbuf.Reset() errbuf.Reset() - if err := git.NewCommand(ctx, "fetch", "origin", "--no-tags", "--", pr.BaseBranch+":"+baseBranch, pr.BaseBranch+":original_"+baseBranch). + if err := git.NewCommand(ctx, "fetch", "origin", "--no-tags").AddDashesAndList(pr.BaseBranch+":"+baseBranch, pr.BaseBranch+":original_"+baseBranch). Run(&git.RunOpts{ Dir: tmpBasePath, Stdout: &outbuf, @@ -124,7 +124,7 @@ func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (str outbuf.Reset() errbuf.Reset() - if err := git.NewCommand(ctx, "symbolic-ref", "HEAD", git.BranchPrefix+baseBranch). + if err := git.NewCommand(ctx, "symbolic-ref").AddDynamicArguments("HEAD", git.BranchPrefix+baseBranch). Run(&git.RunOpts{ Dir: tmpBasePath, Stdout: &outbuf, @@ -147,7 +147,7 @@ func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (str return "", fmt.Errorf("Unable to head base repository to temporary repo [%s -> tmpBasePath]: %v", pr.HeadRepo.FullName(), err) } - if err := git.NewCommand(ctx, "remote", "add", remoteRepoName, headRepoPath). + if err := git.NewCommand(ctx, "remote", "add").AddDynamicArguments(remoteRepoName, headRepoPath). Run(&git.RunOpts{ Dir: tmpBasePath, Stdout: &outbuf, @@ -172,7 +172,7 @@ func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (str } else { headBranch = pr.GetGitRefName() } - if err := git.NewCommand(ctx, "fetch", "--no-tags", remoteRepoName, headBranch+":"+trackingBranch). + if err := git.NewCommand(ctx, "fetch", "--no-tags").AddDynamicArguments(remoteRepoName, headBranch+":"+trackingBranch). Run(&git.RunOpts{ Dir: tmpBasePath, Stdout: &outbuf, diff --git a/services/release/release.go b/services/release/release.go index 187ebeb486..25b89e1a0b 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -271,13 +271,12 @@ func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *repo_mod } } - if !isCreated { - notification.NotifyUpdateRelease(doer, rel) - return - } - if !rel.IsDraft { - notification.NotifyNewRelease(rel) + if isCreated { + notification.NotifyNewRelease(rel) + } else { + notification.NotifyUpdateRelease(doer, rel) + } } return err @@ -310,7 +309,7 @@ func DeleteReleaseByID(ctx context.Context, id int64, doer *user_model.User, del } } - if stdout, _, err := git.NewCommand(ctx, "tag", "-d", "--", rel.TagName). + if stdout, _, err := git.NewCommand(ctx, "tag", "-d").AddDashesAndList(rel.TagName). SetDescription(fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID)). RunStdString(&git.RunOpts{Dir: repo.RepoPath()}); err != nil && !strings.Contains(err.Error(), "not found") { log.Error("DeleteReleaseByID (git tag -d): %d in %v Failed:\nStdout: %s\nError: %v", rel.ID, repo, stdout, err) @@ -353,7 +352,9 @@ func DeleteReleaseByID(ctx context.Context, id int64, doer *user_model.User, del } } - notification.NotifyDeleteRelease(doer, rel) + if !rel.IsDraft { + notification.NotifyDeleteRelease(doer, rel) + } return nil } diff --git a/services/repository/check.go b/services/repository/check.go index 17bdf2fac1..1b2da4cc08 100644 --- a/services/repository/check.go +++ b/services/repository/check.go @@ -11,9 +11,9 @@ import ( "time" "code.gitea.io/gitea/models" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -24,7 +24,7 @@ import ( ) // GitFsck calls 'git fsck' to check repository health. -func GitFsck(ctx context.Context, timeout time.Duration, args []string) error { +func GitFsck(ctx context.Context, timeout time.Duration, args []git.CmdArg) error { log.Trace("Doing: GitFsck") if err := db.Iterate( @@ -42,7 +42,7 @@ func GitFsck(ctx context.Context, timeout time.Duration, args []string) error { repoPath := repo.RepoPath() if err := git.Fsck(ctx, repoPath, timeout, args...); err != nil { log.Warn("Failed to health check repository (%v): %v", repo, err) - if err = admin_model.CreateRepositoryNotice("Failed to health check repository (%s): %v", repo.FullName(), err); err != nil { + if err = system_model.CreateRepositoryNotice("Failed to health check repository (%s): %v", repo.FullName(), err); err != nil { log.Error("CreateRepositoryNotice: %v", err) } } @@ -58,9 +58,9 @@ func GitFsck(ctx context.Context, timeout time.Duration, args []string) error { } // GitGcRepos calls 'git gc' to remove unnecessary files and optimize the local repository -func GitGcRepos(ctx context.Context, timeout time.Duration, args ...string) error { +func GitGcRepos(ctx context.Context, timeout time.Duration, args ...git.CmdArg) error { log.Trace("Doing: GitGcRepos") - args = append([]string{"gc"}, args...) + args = append([]git.CmdArg{"gc"}, args...) if err := db.Iterate( ctx, @@ -83,7 +83,7 @@ func GitGcRepos(ctx context.Context, timeout time.Duration, args ...string) erro if err != nil { log.Error("Repository garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err) desc := fmt.Sprintf("Repository garbage collection failed for %s. Stdout: %s\nError: %v", repo.RepoPath(), stdout, err) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return fmt.Errorf("Repository garbage collection failed in repo: %s: Error: %v", repo.FullName(), err) @@ -93,7 +93,7 @@ func GitGcRepos(ctx context.Context, timeout time.Duration, args ...string) erro if err := repo_module.UpdateRepoSize(ctx, repo); err != nil { log.Error("Updating size as part of garbage collection failed for %v. Stdout: %s\nError: %v", repo, stdout, err) desc := fmt.Sprintf("Updating size as part of garbage collection failed for %s. Stdout: %s\nError: %v", repo.RepoPath(), stdout, err) - if err = admin_model.CreateRepositoryNotice(desc); err != nil { + if err = system_model.CreateRepositoryNotice(desc); err != nil { log.Error("CreateRepositoryNotice: %v", err) } return fmt.Errorf("Updating size as part of garbage collection failed in repo: %s: Error: %v", repo.FullName(), err) @@ -135,7 +135,7 @@ func gatherMissingRepoRecords(ctx context.Context) ([]*repo_model.Repository, er if strings.HasPrefix(err.Error(), "Aborted gathering missing repo") { return nil, err } - if err2 := admin_model.CreateRepositoryNotice("gatherMissingRepoRecords: %v", err); err2 != nil { + if err2 := system_model.CreateRepositoryNotice("gatherMissingRepoRecords: %v", err); err2 != nil { log.Error("CreateRepositoryNotice: %v", err2) } return nil, err @@ -163,7 +163,7 @@ func DeleteMissingRepositories(ctx context.Context, doer *user_model.User) error log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID) if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { log.Error("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err) - if err2 := admin_model.CreateRepositoryNotice("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil { + if err2 := system_model.CreateRepositoryNotice("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil { log.Error("CreateRepositoryNotice: %v", err) } } @@ -191,7 +191,7 @@ func ReinitMissingRepositories(ctx context.Context) error { log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID) if err := git.InitRepository(ctx, repo.RepoPath(), true); err != nil { log.Error("Unable (re)initialize repository %d at %s. Error: %v", repo.ID, repo.RepoPath(), err) - if err2 := admin_model.CreateRepositoryNotice("InitRepository [%d]: %v", repo.ID, err); err2 != nil { + if err2 := system_model.CreateRepositoryNotice("InitRepository [%d]: %v", repo.ID, err); err2 != nil { log.Error("CreateRepositoryNotice: %v", err2) } } diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go index d55d793f28..437890c488 100644 --- a/services/repository/files/patch.go +++ b/services/repository/files/patch.go @@ -139,7 +139,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user stdout := &strings.Builder{} stderr := &strings.Builder{} - args := []string{"apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary"} + args := []git.CmdArg{"apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary"} if git.CheckGitVersionAtLeast("2.32") == nil { args = append(args, "-3") diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index 1e60d55613..9513f2341e 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -53,7 +53,7 @@ func (t *TemporaryUploadRepository) Close() { // Clone the base repository to our path and set branch as the HEAD func (t *TemporaryUploadRepository) Clone(branch string) error { - if _, _, err := git.NewCommand(t.ctx, "clone", "-s", "--bare", "-b", branch, t.repo.RepoPath(), t.basePath).RunStdString(nil); err != nil { + if _, _, err := git.NewCommand(t.ctx, "clone", "-s", "--bare", "-b").AddDynamicArguments(branch, t.repo.RepoPath(), t.basePath).RunStdString(nil); err != nil { stderr := err.Error() if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched { return git.ErrBranchNotExist{ @@ -104,14 +104,7 @@ func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, erro stdOut := new(bytes.Buffer) stdErr := new(bytes.Buffer) - cmdArgs := []string{"ls-files", "-z", "--"} - for _, arg := range filenames { - if arg != "" { - cmdArgs = append(cmdArgs, arg) - } - } - - if err := git.NewCommand(t.ctx, cmdArgs...). + if err := git.NewCommand(t.ctx, "ls-files", "-z").AddDashesAndList(filenames...). Run(&git.RunOpts{ Dir: t.basePath, Stdout: stdOut, @@ -177,7 +170,7 @@ func (t *TemporaryUploadRepository) HashObject(content io.Reader) (string, error // AddObjectToIndex adds the provided object hash to the index with the provided mode and path func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPath string) error { - if _, _, err := git.NewCommand(t.ctx, "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, objectPath).RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil { + if _, _, err := git.NewCommand(t.ctx, "update-index", "--add", "--replace", "--cacheinfo").AddDynamicArguments(mode, objectHash, objectPath).RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil { stderr := err.Error() if matched, _ := regexp.MatchString(".*Invalid path '.*", stderr); matched { return models.ErrFilePathInvalid{ @@ -211,7 +204,7 @@ func (t *TemporaryUploadRepository) GetLastCommitByRef(ref string) (string, erro if ref == "" { ref = "HEAD" } - stdout, _, err := git.NewCommand(t.ctx, "rev-parse", ref).RunStdString(&git.RunOpts{Dir: t.basePath}) + stdout, _, err := git.NewCommand(t.ctx, "rev-parse").AddDynamicArguments(ref).RunStdString(&git.RunOpts{Dir: t.basePath}) if err != nil { log.Error("Unable to get last ref for %s in temporary repo: %s(%s): Error: %v", ref, t.repo.FullName(), t.basePath, err) return "", fmt.Errorf("Unable to rev-parse %s in temporary repo for: %s Error: %v", ref, t.repo.FullName(), err) @@ -241,11 +234,11 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co _, _ = messageBytes.WriteString(message) _, _ = messageBytes.WriteString("\n") - var args []string + var args []git.CmdArg if parent != "" { - args = []string{"commit-tree", treeHash, "-p", parent} + args = []git.CmdArg{"commit-tree", git.CmdArgCheck(treeHash), "-p", git.CmdArgCheck(parent)} } else { - args = []string{"commit-tree", treeHash} + args = []git.CmdArg{"commit-tree", git.CmdArgCheck(treeHash)} } var sign bool @@ -257,7 +250,7 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co sign, keyID, signer, _ = asymkey_service.SignInitialCommit(t.ctx, t.repo.RepoPath(), author) } if sign { - args = append(args, "-S"+keyID) + args = append(args, git.CmdArg("-S"+keyID)) if t.repo.GetTrustModel() == repo_model.CommitterTrustModel || t.repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email { // Add trailers diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 4615a9153a..87c622a9be 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -370,7 +370,7 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do if setting.LFS.StartServer && hasOldBranch { // Check there is no way this can return multiple infos filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ - Attributes: []string{"filter"}, + Attributes: []git.CmdArg{"filter"}, Filenames: []string{treePath}, CachedOnly: true, }) diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index 327a2e121c..b2dadb6497 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -96,7 +96,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use var filename2attribute2info map[string]map[string]string if setting.LFS.StartServer { filename2attribute2info, err = t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ - Attributes: []string{"filter"}, + Attributes: []git.CmdArg{"filter"}, Filenames: names, CachedOnly: true, }) diff --git a/services/repository/fork.go b/services/repository/fork.go index 96c391e715..af0ade99a9 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -39,6 +39,10 @@ func (err ErrForkAlreadyExist) Error() string { return fmt.Sprintf("repository is already forked by user [uname: %s, repo path: %s, fork path: %s]", err.Uname, err.RepoName, err.ForkName) } +func (err ErrForkAlreadyExist) Unwrap() error { + return util.ErrAlreadyExist +} + // ForkRepoOptions contains the fork repository options type ForkRepoOptions struct { BaseRepo *repo_model.Repository @@ -126,7 +130,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork repoPath := repo_model.RepoPath(owner.Name, repo.Name) if stdout, _, err := git.NewCommand(txCtx, - "clone", "--bare", oldRepoPath, repoPath). + "clone", "--bare").AddDynamicArguments(oldRepoPath, repoPath). SetDescription(fmt.Sprintf("ForkRepository(git clone): %s to %s", opts.BaseRepo.FullName(), repo.FullName())). RunStdBytes(&git.RunOpts{Timeout: 10 * time.Minute}); err != nil { log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err) diff --git a/services/repository/push.go b/services/repository/push.go index f3f505aa00..d645928c43 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -219,10 +219,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { log.Error("updateIssuesCommit: %v", err) } - if len(commits.Commits) > setting.UI.FeedMaxCommitNum { - commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] - } - oldCommitID := opts.OldCommitID if oldCommitID == git.EmptySHA && len(commits.Commits) > 0 { oldCommit, err := gitRepo.GetCommit(commits.Commits[len(commits.Commits)-1].Sha1) @@ -250,6 +246,10 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { commits.CompareURL = "" } + if len(commits.Commits) > setting.UI.FeedMaxCommitNum { + commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] + } + notification.NotifyPushCommits(pusher, repo, opts, commits) if err = git_model.RemoveDeletedBranchByName(repo.ID, branch); err != nil { diff --git a/services/repository/repository.go b/services/repository/repository.go index d530358360..47687d9a73 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -9,12 +9,12 @@ import ( "fmt" "code.gitea.io/gitea/models" - admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" @@ -83,8 +83,8 @@ func PushCreateRepo(authUser, owner *user_model.User, repoName string) (*repo_mo // Init start repository service func Init() error { repo_module.LoadRepoConfig() - admin_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repository uploads", setting.Repository.Upload.TempPath) - admin_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repositories", repo_module.LocalCopyPath()) + system_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repository uploads", setting.Repository.Upload.TempPath) + system_model.RemoveAllWithNotice(db.DefaultContext, "Clean up temporary repositories", repo_module.LocalCopyPath()) return initPushQueue() } diff --git a/services/user/user.go b/services/user/user.go index 1edd9294cb..dab9ac61a4 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -13,12 +13,12 @@ import ( "time" "code.gitea.io/gitea/models" - admin_model "code.gitea.io/gitea/models/admin" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/avatar" "code.gitea.io/gitea/modules/eventsource" @@ -186,7 +186,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { path := user_model.UserPath(u.Name) if err := util.RemoveAll(path); err != nil { err = fmt.Errorf("Failed to RemoveAll %s: %v", path, err) - _ = admin_model.CreateNotice(ctx, admin_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err)) + _ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err)) return err } @@ -194,7 +194,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { avatarPath := u.CustomAvatarRelativePath() if err := storage.Avatars.Delete(avatarPath); err != nil { err = fmt.Errorf("Failed to remove %s: %v", avatarPath, err) - _ = admin_model.CreateNotice(ctx, admin_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err)) + _ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err)) return err } } diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index 77744473f1..74a69c297c 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -23,7 +23,6 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/hostmatcher" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" @@ -44,7 +43,7 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { return } // There was a panic whilst delivering a hook... - log.Error("PANIC whilst trying to deliver webhook[%d] for repo[%d] to %s Panic: %v\nStacktrace: %s", t.ID, t.RepoID, w.URL, err, log.Stack(2)) + log.Error("PANIC whilst trying to deliver webhook[%d] to %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2)) }() t.IsDelivered = true @@ -202,35 +201,6 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error { return nil } -// populateDeliverHooks checks and delivers undelivered hooks. -func populateDeliverHooks(ctx context.Context) { - select { - case <-ctx.Done(): - return - default: - } - ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Service: DeliverHooks", process.SystemProcessType, true) - defer finished() - tasks, err := webhook_model.FindUndeliveredHookTasks() - if err != nil { - log.Error("DeliverHooks: %v", err) - return - } - - // Update hook task status. - for _, t := range tasks { - select { - case <-ctx.Done(): - return - default: - } - - if err := addToTask(t.RepoID); err != nil { - log.Error("DeliverHook failed [%d]: %v", t.RepoID, err) - } - } -} - var ( webhookHTTPClient *http.Client once sync.Once @@ -281,13 +251,23 @@ func Init() error { }, } - hookQueue = queue.CreateUniqueQueue("webhook_sender", handle, "") + hookQueue = queue.CreateUniqueQueue("webhook_sender", handle, int64(0)) if hookQueue == nil { return fmt.Errorf("Unable to create webhook_sender Queue") } go graceful.GetManager().RunWithShutdownFns(hookQueue.Run) - populateDeliverHooks(graceful.GetManager().HammerContext()) + tasks, err := webhook_model.FindUndeliveredHookTasks(graceful.GetManager().HammerContext()) + if err != nil { + log.Error("FindUndeliveredHookTasks failed: %v", err) + return err + } + + for _, task := range tasks { + if err := enqueueHookTask(task); err != nil { + log.Error("enqueueHookTask failed: %v", err) + } + } return nil } diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index 5112bdb347..e047e994c2 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -67,14 +67,14 @@ func (d *DingtalkPayload) Push(p *api.PushPayload) (api.Payloader, error) { ) var titleLink, linkText string - if len(p.Commits) == 1 { + if p.TotalCommits == 1 { commitDesc = "1 new commit" titleLink = p.Commits[0].URL - linkText = fmt.Sprintf("view commit %s", p.Commits[0].ID[:7]) + linkText = "view commit" } else { - commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) + commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits) titleLink = p.CompareURL - linkText = fmt.Sprintf("view commit %s...%s", p.Commits[0].ID[:7], p.Commits[len(p.Commits)-1].ID[:7]) + linkText = "view commits" } if titleLink == "" { titleLink = p.Repo.HTMLURL + "/src/" + util.PathEscapeSegments(branchName) diff --git a/services/webhook/dingtalk_test.go b/services/webhook/dingtalk_test.go index efc9601c12..fc15380f4d 100644 --- a/services/webhook/dingtalk_test.go +++ b/services/webhook/dingtalk_test.go @@ -82,7 +82,7 @@ func TestDingTalkPayload(t *testing.T) { assert.Equal(t, "[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", pl.(*DingtalkPayload).ActionCard.Text) assert.Equal(t, "[test/repo:test] 2 new commits", pl.(*DingtalkPayload).ActionCard.Title) - assert.Equal(t, "view commit 2020558...2020558", pl.(*DingtalkPayload).ActionCard.SingleTitle) + assert.Equal(t, "view commits", pl.(*DingtalkPayload).ActionCard.SingleTitle) assert.Equal(t, "http://localhost:3000/test/repo/src/test", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL)) }) diff --git a/services/webhook/discord.go b/services/webhook/discord.go index 7b6f0f0b1d..22d75db893 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -142,11 +142,11 @@ func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) { ) var titleLink string - if len(p.Commits) == 1 { + if p.TotalCommits == 1 { commitDesc = "1 new commit" titleLink = p.Commits[0].URL } else { - commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) + commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits) titleLink = p.CompareURL } if titleLink == "" { diff --git a/services/webhook/general_test.go b/services/webhook/general_test.go index 4acf51ac71..da3123d3ab 100644 --- a/services/webhook/general_test.go +++ b/services/webhook/general_test.go @@ -82,12 +82,13 @@ func pushTestPayload() *api.PushPayload { } return &api.PushPayload{ - Ref: "refs/heads/test", - Before: "2020558fe2e34debb818a514715839cabd25e777", - After: "2020558fe2e34debb818a514715839cabd25e778", - CompareURL: "", - HeadCommit: commit, - Commits: []*api.PayloadCommit{commit, commit}, + Ref: "refs/heads/test", + Before: "2020558fe2e34debb818a514715839cabd25e777", + After: "2020558fe2e34debb818a514715839cabd25e778", + CompareURL: "", + HeadCommit: commit, + Commits: []*api.PayloadCommit{commit, commit}, + TotalCommits: 2, Repo: &api.Repository{ HTMLURL: "http://localhost:3000/test/repo", Name: "repo", diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 59eae48307..6bbae70422 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -161,10 +161,10 @@ func (m *MatrixPayloadUnsafe) Release(p *api.ReleasePayload) (api.Payloader, err func (m *MatrixPayloadUnsafe) Push(p *api.PushPayload) (api.Payloader, error) { var commitDesc string - if len(p.Commits) == 1 { + if p.TotalCommits == 1 { commitDesc = "1 commit" } else { - commitDesc = fmt.Sprintf("%d commits", len(p.Commits)) + commitDesc = fmt.Sprintf("%d commits", p.TotalCommits) } repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 1406004781..bf9e95edc5 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -125,11 +125,11 @@ func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) { ) var titleLink string - if len(p.Commits) == 1 { + if p.TotalCommits == 1 { commitDesc = "1 new commit" titleLink = p.Commits[0].URL } else { - commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) + commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits) titleLink = p.CompareURL } if titleLink == "" { @@ -156,7 +156,7 @@ func (m *MSTeamsPayload) Push(p *api.PushPayload) (api.Payloader, error) { text, titleLink, greenColor, - &MSTeamsFact{"Commit count:", fmt.Sprintf("%d", len(p.Commits))}, + &MSTeamsFact{"Commit count:", fmt.Sprintf("%d", p.TotalCommits)}, ), nil } diff --git a/services/webhook/slack.go b/services/webhook/slack.go index 63cc6f8ca2..f5c69d74b6 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -179,10 +179,10 @@ func (s *SlackPayload) Push(p *api.PushPayload) (api.Payloader, error) { commitString string ) - if len(p.Commits) == 1 { + if p.TotalCommits == 1 { commitDesc = "1 new commit" } else { - commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) + commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits) } if len(p.CompareURL) > 0 { commitString = SlackLinkFormatter(p.CompareURL, commitDesc) diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index 13471d864e..7ca5f61062 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -89,11 +89,11 @@ func (t *TelegramPayload) Push(p *api.PushPayload) (api.Payloader, error) { ) var titleLink string - if len(p.Commits) == 1 { + if p.TotalCommits == 1 { commitDesc = "1 new commit" titleLink = p.Commits[0].URL } else { - commitDesc = fmt.Sprintf("%d new commits", len(p.Commits)) + commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits) titleLink = p.CompareURL } if titleLink == "" { diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 767c3701f3..e877e16eda 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -7,11 +7,10 @@ package webhook import ( "context" "fmt" - "strconv" "strings" - "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" @@ -104,49 +103,38 @@ func getPayloadBranch(p api.Payloader) string { return "" } -// handle passed PR IDs and test the PRs +// EventSource represents the source of a webhook action. Repository and/or Owner must be set. +type EventSource struct { + Repository *repo_model.Repository + Owner *user_model.User +} + +// handle delivers hook tasks func handle(data ...queue.Data) []queue.Data { - for _, datum := range data { - repoIDStr := datum.(string) - log.Trace("DeliverHooks [repo_id: %v]", repoIDStr) + ctx := graceful.GetManager().HammerContext() - repoID, err := strconv.ParseInt(repoIDStr, 10, 64) + for _, taskID := range data { + task, err := webhook_model.GetHookTaskByID(ctx, taskID.(int64)) if err != nil { - log.Error("Invalid repo ID: %s", repoIDStr) - continue - } - - tasks, err := webhook_model.FindRepoUndeliveredHookTasks(repoID) - if err != nil { - log.Error("Get repository [%d] hook tasks: %v", repoID, err) - continue - } - for _, t := range tasks { - if err = Deliver(graceful.GetManager().HammerContext(), t); err != nil { - log.Error("deliver: %v", err) + log.Error("GetHookTaskByID failed: %v", err) + } else { + if err := Deliver(ctx, task); err != nil { + log.Error("webhook.Deliver failed: %v", err) } } } + return nil } -func addToTask(repoID int64) error { - err := hookQueue.PushFunc(strconv.FormatInt(repoID, 10), nil) +func enqueueHookTask(task *webhook_model.HookTask) error { + err := hookQueue.PushFunc(task.ID, nil) if err != nil && err != queue.ErrAlreadyInQueue { return err } return nil } -// PrepareWebhook adds special webhook to task queue for given payload. -func PrepareWebhook(w *webhook_model.Webhook, repo *repo_model.Repository, event webhook_model.HookEventType, p api.Payloader) error { - if err := prepareWebhook(w, repo, event, p); err != nil { - return err - } - - return addToTask(repo.ID) -} - func checkBranch(w *webhook_model.Webhook, branch string) bool { if w.BranchFilter == "" || w.BranchFilter == "*" { return true @@ -162,7 +150,8 @@ func checkBranch(w *webhook_model.Webhook, branch string) bool { return g.Match(branch) } -func prepareWebhook(w *webhook_model.Webhook, repo *repo_model.Repository, event webhook_model.HookEventType, p api.Payloader) error { +// PrepareWebhook creates a hook task and enqueues it for processing +func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_model.HookEventType, p api.Payloader) error { // Skip sending if webhooks are disabled. if setting.DisableWebhooks { return nil @@ -207,44 +196,45 @@ func prepareWebhook(w *webhook_model.Webhook, repo *repo_model.Repository, event payloader = p } - if err = webhook_model.CreateHookTask(&webhook_model.HookTask{ - RepoID: repo.ID, + task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{ HookID: w.ID, Payloader: payloader, EventType: event, - }); err != nil { + }) + if err != nil { return fmt.Errorf("CreateHookTask: %v", err) } - return nil + + return enqueueHookTask(task) } // PrepareWebhooks adds new webhooks to task queue for given payload. -func PrepareWebhooks(repo *repo_model.Repository, event webhook_model.HookEventType, p api.Payloader) error { - if err := prepareWebhooks(db.DefaultContext, repo, event, p); err != nil { - return err - } +func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_model.HookEventType, p api.Payloader) error { + owner := source.Owner - return addToTask(repo.ID) -} + var ws []*webhook_model.Webhook -func prepareWebhooks(ctx context.Context, repo *repo_model.Repository, event webhook_model.HookEventType, p api.Payloader) error { - ws, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ - RepoID: repo.ID, - IsActive: util.OptionalBoolTrue, - }) - if err != nil { - return fmt.Errorf("GetActiveWebhooksByRepoID: %v", err) - } - - // check if repo belongs to org and append additional webhooks - if repo.MustOwner().IsOrganization() { - // get hooks for org - orgHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ - OrgID: repo.OwnerID, + if source.Repository != nil { + repoHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ + RepoID: source.Repository.ID, IsActive: util.OptionalBoolTrue, }) if err != nil { - return fmt.Errorf("GetActiveWebhooksByOrgID: %v", err) + return fmt.Errorf("ListWebhooksByOpts: %v", err) + } + ws = append(ws, repoHooks...) + + owner = source.Repository.MustOwner() + } + + // check if owner is an org and append additional webhooks + if owner != nil && owner.IsOrganization() { + orgHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ + OrgID: owner.ID, + IsActive: util.OptionalBoolTrue, + }) + if err != nil { + return fmt.Errorf("ListWebhooksByOpts: %v", err) } ws = append(ws, orgHooks...) } @@ -261,7 +251,7 @@ func prepareWebhooks(ctx context.Context, repo *repo_model.Repository, event web } for _, w := range ws { - if err = prepareWebhook(w, repo, event, p); err != nil { + if err := PrepareWebhook(ctx, w, event, p); err != nil { return err } } @@ -269,11 +259,11 @@ func prepareWebhooks(ctx context.Context, repo *repo_model.Repository, event web } // ReplayHookTask replays a webhook task -func ReplayHookTask(w *webhook_model.Webhook, uuid string) error { - t, err := webhook_model.ReplayHookTask(w.ID, uuid) +func ReplayHookTask(ctx context.Context, w *webhook_model.Webhook, uuid string) error { + task, err := webhook_model.ReplayHookTask(ctx, w.ID, uuid) if err != nil { return err } - return addToTask(t.RepoID) + return enqueueHookTask(task) } diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 1887cc71fe..8d44aa504a 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -7,6 +7,7 @@ package webhook import ( "testing" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" webhook_model "code.gitea.io/gitea/models/webhook" @@ -32,12 +33,12 @@ func TestPrepareWebhooks(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) hookTasks := []*webhook_model.HookTask{ - {RepoID: repo.ID, HookID: 1, EventType: webhook_model.HookEventPush}, + {HookID: 1, EventType: webhook_model.HookEventPush}, } for _, hookTask := range hookTasks { unittest.AssertNotExistsBean(t, hookTask) } - assert.NoError(t, PrepareWebhooks(repo, webhook_model.HookEventPush, &api.PushPayload{Commits: []*api.PayloadCommit{{}}})) + assert.NoError(t, PrepareWebhooks(db.DefaultContext, EventSource{Repository: repo}, webhook_model.HookEventPush, &api.PushPayload{Commits: []*api.PayloadCommit{{}}})) for _, hookTask := range hookTasks { unittest.AssertExistsAndLoadBean(t, hookTask) } @@ -48,13 +49,13 @@ func TestPrepareWebhooksBranchFilterMatch(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) hookTasks := []*webhook_model.HookTask{ - {RepoID: repo.ID, HookID: 4, EventType: webhook_model.HookEventPush}, + {HookID: 4, EventType: webhook_model.HookEventPush}, } for _, hookTask := range hookTasks { unittest.AssertNotExistsBean(t, hookTask) } // this test also ensures that * doesn't handle / in any special way (like shell would) - assert.NoError(t, PrepareWebhooks(repo, webhook_model.HookEventPush, &api.PushPayload{Ref: "refs/heads/feature/7791", Commits: []*api.PayloadCommit{{}}})) + assert.NoError(t, PrepareWebhooks(db.DefaultContext, EventSource{Repository: repo}, webhook_model.HookEventPush, &api.PushPayload{Ref: "refs/heads/feature/7791", Commits: []*api.PayloadCommit{{}}})) for _, hookTask := range hookTasks { unittest.AssertExistsAndLoadBean(t, hookTask) } @@ -65,12 +66,12 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) hookTasks := []*webhook_model.HookTask{ - {RepoID: repo.ID, HookID: 4, EventType: webhook_model.HookEventPush}, + {HookID: 4, EventType: webhook_model.HookEventPush}, } for _, hookTask := range hookTasks { unittest.AssertNotExistsBean(t, hookTask) } - assert.NoError(t, PrepareWebhooks(repo, webhook_model.HookEventPush, &api.PushPayload{Ref: "refs/heads/fix_weird_bug"})) + assert.NoError(t, PrepareWebhooks(db.DefaultContext, EventSource{Repository: repo}, webhook_model.HookEventPush, &api.PushPayload{Ref: "refs/heads/fix_weird_bug"})) for _, hookTask := range hookTasks { unittest.AssertNotExistsBean(t, hookTask) diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index bd4c3e0851..5344ccaa22 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -93,7 +93,7 @@ func (f *WechatworkPayload) Push(p *api.PushPayload) (api.Payloader, error) { for i, commit := range p.Commits { var authorName string if commit.Author != nil { - authorName = "Author:" + commit.Author.Name + authorName = "Author: " + commit.Author.Name } message := strings.ReplaceAll(commit.Message, "\n\n", "\r\n") diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index b341b048cc..8faada4d59 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -12,8 +12,8 @@ import ( "os" "strings" - admin_model "code.gitea.io/gitea/models/admin" repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -30,6 +30,11 @@ var ( wikiWorkingPool = sync.NewExclusivePool() ) +const ( + DefaultRemote = "origin" + DefaultBranch = "master" +) + func nameAllowed(name string) error { if util.IsStringInSlice(name, reservedWikiNames) { return repo_model.ErrWikiReservedName{ @@ -81,7 +86,7 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error { return fmt.Errorf("InitRepository: %v", err) } else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil { return fmt.Errorf("createDelegateHooks: %v", err) - } else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD", git.BranchPrefix+"master").RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil { + } else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD", git.BranchPrefix+DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil { return fmt.Errorf("unable to set default wiki branch to master: %v", err) } return nil @@ -94,7 +99,7 @@ func prepareWikiFileName(gitRepo *git.Repository, wikiName string) (bool, string escaped := NameToFilename(wikiName) // Look for both files - filesInIndex, err := gitRepo.LsTree("master", unescaped, escaped) + filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, escaped) if err != nil { if strings.Contains(err.Error(), "Not a valid object name master") { return false, escaped, nil @@ -130,7 +135,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model return fmt.Errorf("InitWiki: %v", err) } - hasMasterBranch := git.IsBranchExist(ctx, repo.WikiPath(), "master") + hasMasterBranch := git.IsBranchExist(ctx, repo.WikiPath(), DefaultBranch) basePath, err := repo_module.CreateTemporaryPath("update-wiki") if err != nil { @@ -148,7 +153,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model } if hasMasterBranch { - cloneOpts.Branch = "master" + cloneOpts.Branch = DefaultBranch } if err := git.Clone(ctx, repo.WikiPath(), basePath, cloneOpts); err != nil { @@ -246,8 +251,8 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model } if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{ - Remote: "origin", - Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"), + Remote: DefaultRemote, + Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch), Env: repo_module.FullPushingEnvironment( doer, doer, @@ -299,7 +304,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{ Bare: true, Shared: true, - Branch: "master", + Branch: DefaultBranch, }); err != nil { log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err) return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err) @@ -360,8 +365,8 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model } if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{ - Remote: "origin", - Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"), + Remote: DefaultRemote, + Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch), Env: repo_module.PushingEnvironment(doer, repo), }); err != nil { if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { @@ -379,6 +384,6 @@ func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error { return err } - admin_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath()) + system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath()) return nil } diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go index cabeecb60a..774ef6c283 100644 --- a/services/wiki/wiki_test.go +++ b/services/wiki/wiki_test.go @@ -140,7 +140,7 @@ func TestRepository_AddWikiPage(t *testing.T) { gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath()) assert.NoError(t, err) defer gitRepo.Close() - masterTree, err := gitRepo.GetTree("master") + masterTree, err := gitRepo.GetTree(DefaultBranch) assert.NoError(t, err) wikiPath := NameToFilename(wikiName) entry, err := masterTree.GetTreeEntryByPath(wikiPath) @@ -184,7 +184,7 @@ func TestRepository_EditWikiPage(t *testing.T) { // Now need to show that the page has been added: gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath()) assert.NoError(t, err) - masterTree, err := gitRepo.GetTree("master") + masterTree, err := gitRepo.GetTree(DefaultBranch) assert.NoError(t, err) wikiPath := NameToFilename(newWikiName) entry, err := masterTree.GetTreeEntryByPath(wikiPath) @@ -209,7 +209,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) { gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath()) assert.NoError(t, err) defer gitRepo.Close() - masterTree, err := gitRepo.GetTree("master") + masterTree, err := gitRepo.GetTree(DefaultBranch) assert.NoError(t, err) wikiPath := NameToFilename("Home") _, err = masterTree.GetTreeEntryByPath(wikiPath) diff --git a/templates/admin/auth/list.tmpl b/templates/admin/auth/list.tmpl index 3ce138449d..c43287ee1a 100644 --- a/templates/admin/auth/list.tmpl +++ b/templates/admin/auth/list.tmpl @@ -29,8 +29,8 @@ {{.Name}} {{.TypeName}} {{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} - {{.UpdatedUnix.FormatShort}} - {{.CreatedUnix.FormatShort}} + + {{svg "octicon-pencil"}} {{end}} diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 0d9432b395..982cfb2800 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -301,10 +301,18 @@
{{.locale.Tr "admin.config.disable_gravatar"}}
-
{{if .DisableGravatar}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+
+
+ +
+
{{.locale.Tr "admin.config.enable_federated_avatar"}}
-
{{if .EnableFederatedAvatar}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+
+
+ +
+
diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index b138eb79ba..1c8b12fc2f 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -12,9 +12,11 @@ {{.locale.Tr "admin.repositories"}} - - {{.locale.Tr "packages.title"}} - + {{if .EnablePackages}} + + {{.locale.Tr "packages.title"}} + + {{end}} {{if not DisableWebhooks}} {{.locale.Tr "admin.hooks"}} diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl index b831f50c85..2777741efb 100644 --- a/templates/admin/notice.tmpl +++ b/templates/admin/notice.tmpl @@ -29,7 +29,7 @@ {{.ID}} {{$.locale.Tr .TrStr}} {{.Description}} - {{.CreatedUnix.FormatShort}} + {{svg "octicon-note" 16 "view-detail"}} {{end}} diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl index aec3f2c841..11dc23c60e 100644 --- a/templates/admin/org/list.tmpl +++ b/templates/admin/org/list.tmpl @@ -44,7 +44,7 @@ {{.NumTeams}} {{.NumMembers}} {{.NumRepos}} - {{.CreatedUnix.FormatShort}} + {{svg "octicon-pencil"}} {{end}} diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 06d6163476..3aab2873c6 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -37,20 +37,20 @@ ID {{.locale.Tr "admin.packages.owner"}} {{.locale.Tr "admin.packages.type"}} - + {{.locale.Tr "admin.packages.name"}} - {{SortArrow "alphabetically" "reversealphabetically" .SortType false}} + {{SortArrow "name_asc" "name_desc" .SortType false}} - + {{.locale.Tr "admin.packages.version"}} - {{SortArrow "highestversion" "lowestversion" .SortType false}} + {{SortArrow "version_desc" "version_asc" .SortType false}} {{.locale.Tr "admin.packages.creator"}} {{.locale.Tr "admin.packages.repository"}} {{.locale.Tr "admin.packages.size"}} - + {{.locale.Tr "admin.packages.published"}} - {{SortArrow "oldest" "newest" .SortType true}} + {{SortArrow "created_asc" "created_desc" .SortType true}} {{.locale.Tr "admin.notices.op"}} @@ -75,7 +75,7 @@ {{end}} {{FileSize .CalculateBlobSize}} - {{.Version.CreatedUnix.FormatShort}} + {{svg "octicon-trash"}} {{end}} diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index b26ec2eb78..837802f0d0 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -83,7 +83,7 @@ {{.NumForks}} {{.NumIssues}} {{FileSize .Size}} - {{.CreatedUnix.FormatShort}} + {{svg "octicon-trash"}} {{end}} diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index 061e663850..56f6eaa3ad 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -94,9 +94,9 @@ {{if .IsRestricted}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{if index $.UsersTwoFaStatus .ID}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}} {{.NumRepos}} - {{.CreatedUnix.FormatShort}} + {{if .LastLoginUnix}} - {{.LastLoginUnix.FormatShort}} + {{else}} {{$.locale.Tr "admin.users.never_login"}} {{end}} diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 8369b63b35..c4ac18a86e 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -6,7 +6,6 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
-
+
{{range $i, $file := .Diff.Files}} {{/*notice: the index of Diff.Files should not be used for element ID, because the index will be restarted from 0 when doing load-more for PRs with a lot of files*/}} diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index e0e6837203..029e7717a4 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -1,7 +1,7 @@ {{template "base/head" .}}
{{template "repo/header" .}} -
+

{{if and $.PageIsComparePull $.IsSigned (not .Repository.IsArchived)}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index e1aa1c4f3b..69eaf17429 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -3,6 +3,7 @@ {{template "repo/header" .}}
{{template "base/alert" .}} + {{if and (not .HideRepoInfo) (not .IsBlame)}}
{{$description := .Repository.DescriptionHTML $.Context}} @@ -31,6 +32,7 @@ {{range .Topics}}{{.Name}}{{end}} {{if and .Permission.IsAdmin (not .Repository.IsArchived)}}{{.locale.Tr "repo.topic.manage_topics"}}{{end}}
+ {{end}} {{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
diff --git a/templates/user/settings/keys_principal.tmpl b/templates/user/settings/keys_principal.tmpl index 5699214468..b0cacbe54c 100644 --- a/templates/user/settings/keys_principal.tmpl +++ b/templates/user/settings/keys_principal.tmpl @@ -25,7 +25,7 @@
{{.Name}}
- {{$.locale.Tr "settings.add_on"}} {{.CreatedUnix.FormatShort}} — {{svg "octicon-info" 16}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} {{.UpdatedUnix.FormatShort}}{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}} + {{$.locale.Tr "settings.add_on"}} — {{svg "octicon-info" 16}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} {{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl index bcab968d59..0933953909 100644 --- a/templates/user/settings/keys_ssh.tmpl +++ b/templates/user/settings/keys_ssh.tmpl @@ -58,7 +58,7 @@ {{.Fingerprint}}

- {{$.locale.Tr "settings.add_on"}} {{.CreatedUnix.FormatShort}} — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} {{.UpdatedUnix.FormatShort}}{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}} + {{$.locale.Tr "settings.add_on"}} — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} {{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}
diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go index dea0bdd063..d6bc6016ff 100644 --- a/tests/integration/api_admin_test.go +++ b/tests/integration/api_admin_test.go @@ -167,6 +167,33 @@ func TestAPICreateUserInvalidEmail(t *testing.T) { session.MakeRequest(t, req, http.StatusUnprocessableEntity) } +func TestAPICreateAndDeleteUser(t *testing.T) { + defer tests.PrepareTestEnv(t)() + adminUsername := "user1" + session := loginUser(t, adminUsername) + token := getTokenForLoggedInUser(t, session) + + req := NewRequestWithValues( + t, + "POST", + fmt.Sprintf("/api/v1/admin/users?token=%s", token), + map[string]string{ + "email": "deleteme@domain.com", + "full_name": "delete me", + "login_name": "deleteme", + "must_change_password": "true", + "password": "password", + "send_notify": "true", + "source_id": "0", + "username": "deleteme", + }, + ) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/admin/users/deleteme?token=%s", token)) + MakeRequest(t, req, http.StatusNoContent) +} + func TestAPIEditUser(t *testing.T) { defer tests.PrepareTestEnv(t)() adminUsername := "user1" @@ -209,3 +236,20 @@ func TestAPIEditUser(t *testing.T) { user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"}) assert.True(t, user2.IsRestricted) } + +func TestAPICreateRepoForUser(t *testing.T) { + defer tests.PrepareTestEnv(t)() + adminUsername := "user1" + session := loginUser(t, adminUsername) + token := getTokenForLoggedInUser(t, session) + + req := NewRequestWithJSON( + t, + "POST", + fmt.Sprintf("/api/v1/admin/users/%s/repos?token=%s", adminUsername, token), + &api.CreateRepoOption{ + Name: "admincreatedrepo", + }, + ) + MakeRequest(t, req, http.StatusCreated) +} diff --git a/tests/integration/api_feed_user_test.go b/tests/integration/api_feed_user_test.go new file mode 100644 index 0000000000..c137dd1209 --- /dev/null +++ b/tests/integration/api_feed_user_test.go @@ -0,0 +1,38 @@ +// 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 integration + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestFeed(t *testing.T) { + t.Run("User", func(t *testing.T) { + t.Run("Atom", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2.atom") + resp := MakeRequest(t, req, http.StatusOK) + + data := resp.Body.String() + assert.Contains(t, data, ` - - - ` + packageName + ` - ` + packageVersion + ` - ` + packageAuthors + ` - ` + packageDescription + ` - - - - - `)) - archive.Close() - content := buf.Bytes() + createPackage := func(id, version string) io.Reader { + var buf bytes.Buffer + archive := zip.NewWriter(&buf) + w, _ := archive.Create("package.nuspec") + w.Write([]byte(` + + + ` + id + ` + ` + version + ` + ` + packageAuthors + ` + ` + packageDescription + ` + + + + + + + `)) + archive.Close() + return &buf + } + + content, _ := ioutil.ReadAll(createPackage(packageName, packageVersion)) url := fmt.Sprintf("/api/packages/%s/nuget", user.Name) t.Run("ServiceIndex", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate}) + t.Run("v2", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - cases := []struct { - Owner string - UseBasicAuth bool - UseTokenAuth bool - }{ - {privateUser.Name, false, false}, - {privateUser.Name, true, false}, - {privateUser.Name, false, true}, - {user.Name, false, false}, - {user.Name, true, false}, - {user.Name, false, true}, - } + privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate}) - for _, c := range cases { - url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner) - - req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url)) - if c.UseBasicAuth { - req = AddBasicAuthHeader(req, user.Name) - } else if c.UseTokenAuth { - req = addNuGetAPIKeyHeader(req, token) + cases := []struct { + Owner string + UseBasicAuth bool + UseTokenAuth bool + }{ + {privateUser.Name, false, false}, + {privateUser.Name, true, false}, + {privateUser.Name, false, true}, + {user.Name, false, false}, + {user.Name, true, false}, + {user.Name, false, true}, } - resp := MakeRequest(t, req, http.StatusOK) - var result nuget.ServiceIndexResponse - DecodeJSON(t, resp, &result) + for _, c := range cases { + url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner) - assert.Equal(t, "3.0.0", result.Version) - assert.NotEmpty(t, result.Resources) + req := NewRequest(t, "GET", url) + if c.UseBasicAuth { + req = AddBasicAuthHeader(req, user.Name) + } else if c.UseTokenAuth { + req = addNuGetAPIKeyHeader(req, token) + } + resp := MakeRequest(t, req, http.StatusOK) - root := setting.AppURL + url[1:] - for _, r := range result.Resources { - switch r.Type { - case "SearchQueryService": - fallthrough - case "SearchQueryService/3.0.0-beta": - fallthrough - case "SearchQueryService/3.0.0-rc": - assert.Equal(t, root+"/query", r.ID) - case "RegistrationsBaseUrl": - fallthrough - case "RegistrationsBaseUrl/3.0.0-beta": - fallthrough - case "RegistrationsBaseUrl/3.0.0-rc": - assert.Equal(t, root+"/registration", r.ID) - case "PackageBaseAddress/3.0.0": - assert.Equal(t, root+"/package", r.ID) - case "PackagePublish/2.0.0": - assert.Equal(t, root, r.ID) + var result nuget.ServiceIndexResponseV2 + decodeXML(t, resp, &result) + + assert.Equal(t, setting.AppURL+url[1:], result.Base) + assert.Equal(t, "Packages", result.Workspace.Collection.Href) + } + }) + + t.Run("v3", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate}) + + cases := []struct { + Owner string + UseBasicAuth bool + UseTokenAuth bool + }{ + {privateUser.Name, false, false}, + {privateUser.Name, true, false}, + {privateUser.Name, false, true}, + {user.Name, false, false}, + {user.Name, true, false}, + {user.Name, false, true}, + } + + for _, c := range cases { + url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner) + + req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url)) + if c.UseBasicAuth { + req = AddBasicAuthHeader(req, user.Name) + } else if c.UseTokenAuth { + req = addNuGetAPIKeyHeader(req, token) + } + resp := MakeRequest(t, req, http.StatusOK) + + var result nuget.ServiceIndexResponseV3 + DecodeJSON(t, resp, &result) + + assert.Equal(t, "3.0.0", result.Version) + assert.NotEmpty(t, result.Resources) + + root := setting.AppURL + url[1:] + for _, r := range result.Resources { + switch r.Type { + case "SearchQueryService": + fallthrough + case "SearchQueryService/3.0.0-beta": + fallthrough + case "SearchQueryService/3.0.0-rc": + assert.Equal(t, root+"/query", r.ID) + case "RegistrationsBaseUrl": + fallthrough + case "RegistrationsBaseUrl/3.0.0-beta": + fallthrough + case "RegistrationsBaseUrl/3.0.0-rc": + assert.Equal(t, root+"/registration", r.ID) + case "PackageBaseAddress/3.0.0": + assert.Equal(t, root+"/package", r.ID) + case "PackagePublish/2.0.0": + assert.Equal(t, root, r.ID) + } } } - } + }) }) t.Run("Upload", func(t *testing.T) { @@ -160,7 +247,7 @@ func TestPackageNuGet(t *testing.T) { t.Run("SymbolPackage", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - createPackage := func(id, packageType string) io.Reader { + createSymbolPackage := func(id, packageType string) io.Reader { var buf bytes.Buffer archive := zip.NewWriter(&buf) @@ -186,15 +273,15 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) return &buf } - req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage("unknown-package", "SymbolsPackage")) + req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createSymbolPackage("unknown-package", "SymbolsPackage")) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusNotFound) - req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "DummyPackage")) + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createSymbolPackage(packageName, "DummyPackage")) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusBadRequest) - req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "SymbolsPackage")) + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createSymbolPackage(packageName, "SymbolsPackage")) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusCreated) @@ -238,7 +325,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) } } - req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "SymbolsPackage")) + req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createSymbolPackage(packageName, "SymbolsPackage")) req = AddBasicAuthHeader(req, user.Name) MakeRequest(t, req, http.StatusConflict) }) @@ -305,17 +392,94 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) {"test", 1, 10, 1, 0}, } - for i, c := range cases { - req := NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s&skip=%d&take=%d", url, c.Query, c.Skip, c.Take)) - req = AddBasicAuthHeader(req, user.Name) - resp := MakeRequest(t, req, http.StatusOK) + t.Run("v2", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - var result nuget.SearchResultResponse - DecodeJSON(t, resp, &result) + t.Run("Search()", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - assert.Equal(t, c.ExpectedTotal, result.TotalHits, "case %d: unexpected total hits", i) - assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i) - } + for i, c := range cases { + req := NewRequest(t, "GET", fmt.Sprintf("%s/Search()?searchTerm='%s'&skip=%d&take=%d", url, c.Query, c.Skip, c.Take)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + var result FeedResponse + decodeXML(t, resp, &result) + + assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i) + assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i) + } + }) + + t.Run("Packages()", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + for i, c := range cases { + req := NewRequest(t, "GET", fmt.Sprintf("%s/Search()?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + var result FeedResponse + decodeXML(t, resp, &result) + + assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i) + assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i) + } + }) + }) + + t.Run("v3", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + for i, c := range cases { + req := NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s&skip=%d&take=%d", url, c.Query, c.Skip, c.Take)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + var result nuget.SearchResultResponse + DecodeJSON(t, resp, &result) + + assert.Equal(t, c.ExpectedTotal, result.TotalHits, "case %d: unexpected total hits", i) + assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i) + } + + t.Run("EnforceGrouped", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithBody(t, "PUT", url, createPackage(packageName+".dummy", "1.0.0")) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequestWithBody(t, "PUT", url, createPackage(packageName, "1.0.99")) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s", url, packageName)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + var result nuget.SearchResultResponse + DecodeJSON(t, resp, &result) + + assert.EqualValues(t, 3, result.TotalHits) + assert.Len(t, result.Data, 2) + for _, sr := range result.Data { + if sr.ID == packageName { + assert.Len(t, sr.Versions, 2) + } else { + assert.Len(t, sr.Versions, 1) + } + } + + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName+".dummy", "1.0.0")) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, "1.0.99")) + req = AddBasicAuthHeader(req, user.Name) + MakeRequest(t, req, http.StatusNoContent) + }) + }) }) t.Run("RegistrationService", func(t *testing.T) { @@ -352,31 +516,70 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) t.Run("RegistrationLeaf", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/%s.json", url, packageName, packageVersion)) - req = AddBasicAuthHeader(req, user.Name) - resp := MakeRequest(t, req, http.StatusOK) + t.Run("v2", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - var result nuget.RegistrationLeafResponse - DecodeJSON(t, resp, &result) + req := NewRequest(t, "GET", fmt.Sprintf("%s/Packages(Id='%s',Version='%s')", url, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, leafURL, result.RegistrationLeafURL) - assert.Equal(t, contentURL, result.PackageContentURL) - assert.Equal(t, indexURL, result.RegistrationIndexURL) + var result FeedEntry + decodeXML(t, resp, &result) + + assert.Equal(t, packageName, result.Properties.Title) + assert.Equal(t, packageVersion, result.Properties.Version) + assert.Equal(t, packageAuthors, result.Properties.Authors) + assert.Equal(t, packageDescription, result.Properties.Description) + assert.Equal(t, "Microsoft.CSharp:4.5.0:.NETStandard2.0", result.Properties.Dependencies) + }) + + t.Run("v3", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/%s.json", url, packageName, packageVersion)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + var result nuget.RegistrationLeafResponse + DecodeJSON(t, resp, &result) + + assert.Equal(t, leafURL, result.RegistrationLeafURL) + assert.Equal(t, contentURL, result.PackageContentURL) + assert.Equal(t, indexURL, result.RegistrationIndexURL) + }) }) }) t.Run("PackageService", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/index.json", url, packageName)) - req = AddBasicAuthHeader(req, user.Name) - resp := MakeRequest(t, req, http.StatusOK) + t.Run("v2", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - var result nuget.PackageVersionsResponse - DecodeJSON(t, resp, &result) + req := NewRequest(t, "GET", fmt.Sprintf("%s/FindPackagesById()?id='%s'", url, packageName)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) - assert.Len(t, result.Versions, 1) - assert.Equal(t, packageVersion, result.Versions[0]) + var result FeedResponse + decodeXML(t, resp, &result) + + assert.Len(t, result.Entries, 1) + assert.Equal(t, packageVersion, result.Entries[0].Properties.Version) + }) + + t.Run("v3", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/index.json", url, packageName)) + req = AddBasicAuthHeader(req, user.Name) + resp := MakeRequest(t, req, http.StatusOK) + + var result nuget.PackageVersionsResponse + DecodeJSON(t, resp, &result) + + assert.Len(t, result.Versions, 1) + assert.Equal(t, packageVersion, result.Versions[0]) + }) }) t.Run("Delete", func(t *testing.T) { diff --git a/tests/integration/api_repo_git_tags_test.go b/tests/integration/api_repo_git_tags_test.go index 855eb2451e..3357f9568d 100644 --- a/tests/integration/api_repo_git_tags_test.go +++ b/tests/integration/api_repo_git_tags_test.go @@ -29,8 +29,8 @@ func TestAPIGitTags(t *testing.T) { token := getTokenForLoggedInUser(t, session) // Set up git config for the tagger - _ = git.NewCommand(git.DefaultContext, "config", "user.name", user.Name).Run(&git.RunOpts{Dir: repo.RepoPath()}) - _ = git.NewCommand(git.DefaultContext, "config", "user.email", user.Email).Run(&git.RunOpts{Dir: repo.RepoPath()}) + _ = git.NewCommand(git.DefaultContext, "config", "user.name").AddDynamicArguments(user.Name).Run(&git.RunOpts{Dir: repo.RepoPath()}) + _ = git.NewCommand(git.DefaultContext, "config", "user.email").AddDynamicArguments(user.Email).Run(&git.RunOpts{Dir: repo.RepoPath()}) gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath()) defer gitRepo.Close() diff --git a/tests/integration/api_user_follow_test.go b/tests/integration/api_user_follow_test.go new file mode 100644 index 0000000000..e21556aa5b --- /dev/null +++ b/tests/integration/api_user_follow_test.go @@ -0,0 +1,111 @@ +// 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 integration + +import ( + "fmt" + "net/http" + "testing" + + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPIFollow(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user1 := "user4" + user2 := "user1" + + session1 := loginUser(t, user1) + token1 := getTokenForLoggedInUser(t, session1) + + session2 := loginUser(t, user2) + token2 := getTokenForLoggedInUser(t, session2) + + t.Run("Follow", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/following/%s?token=%s", user1, token2)) + MakeRequest(t, req, http.StatusNoContent) + }) + + t.Run("ListFollowing", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/following?token=%s", user2, token2)) + resp := MakeRequest(t, req, http.StatusOK) + + var users []api.User + DecodeJSON(t, resp, &users) + assert.Len(t, users, 1) + assert.Equal(t, user1, users[0].UserName) + }) + + t.Run("ListMyFollowing", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/following?token=%s", token2)) + resp := MakeRequest(t, req, http.StatusOK) + + var users []api.User + DecodeJSON(t, resp, &users) + assert.Len(t, users, 1) + assert.Equal(t, user1, users[0].UserName) + }) + + t.Run("ListFollowers", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/followers?token=%s", user1, token1)) + resp := MakeRequest(t, req, http.StatusOK) + + var users []api.User + DecodeJSON(t, resp, &users) + assert.Len(t, users, 1) + assert.Equal(t, user2, users[0].UserName) + }) + + t.Run("ListMyFollowers", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/followers?token=%s", token1)) + resp := MakeRequest(t, req, http.StatusOK) + + var users []api.User + DecodeJSON(t, resp, &users) + assert.Len(t, users, 1) + assert.Equal(t, user2, users[0].UserName) + }) + + t.Run("CheckFollowing", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/following/%s?token=%s", user2, user1, token2)) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/following/%s?token=%s", user1, user2, token2)) + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("CheckMyFollowing", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/following/%s?token=%s", user1, token2)) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/following/%s?token=%s", user2, token1)) + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("Unfollow", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/following/%s?token=%s", user1, token2)) + MakeRequest(t, req, http.StatusNoContent) + }) +} diff --git a/tests/integration/api_user_info_test.go b/tests/integration/api_user_info_test.go new file mode 100644 index 0000000000..27fec1d587 --- /dev/null +++ b/tests/integration/api_user_info_test.go @@ -0,0 +1,51 @@ +// 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 integration + +import ( + "fmt" + "net/http" + "testing" + + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPIUserInfo(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := "user1" + user2 := "user31" + + session := loginUser(t, user) + token := getTokenForLoggedInUser(t, session) + + t.Run("GetInfo", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s?token=%s", user2, token)) + resp := MakeRequest(t, req, http.StatusOK) + + var u api.User + DecodeJSON(t, resp, &u) + assert.Equal(t, user2, u.UserName) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s", user2)) + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("GetAuthenticatedUser", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user?token=%s", token)) + resp := MakeRequest(t, req, http.StatusOK) + + var u api.User + DecodeJSON(t, resp, &u) + assert.Equal(t, user, u.UserName) + }) +} diff --git a/tests/integration/api_user_star_test.go b/tests/integration/api_user_star_test.go new file mode 100644 index 0000000000..76c3dc2d17 --- /dev/null +++ b/tests/integration/api_user_star_test.go @@ -0,0 +1,78 @@ +// 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 integration + +import ( + "fmt" + "net/http" + "testing" + + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPIStar(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := "user1" + repo := "user2/repo1" + + session := loginUser(t, user) + token := getTokenForLoggedInUser(t, session) + + t.Run("Star", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo, token)) + MakeRequest(t, req, http.StatusNoContent) + }) + + t.Run("GetStarredRepos", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/starred?token=%s", user, token)) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "1", resp.Header().Get("X-Total-Count")) + + var repos []api.Repository + DecodeJSON(t, resp, &repos) + assert.Len(t, repos, 1) + assert.Equal(t, repo, repos[0].FullName) + }) + + t.Run("GetMyStarredRepos", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/starred?token=%s", token)) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "1", resp.Header().Get("X-Total-Count")) + + var repos []api.Repository + DecodeJSON(t, resp, &repos) + assert.Len(t, repos, 1) + assert.Equal(t, repo, repos[0].FullName) + }) + + t.Run("IsStarring", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo, token)) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo+"notexisting", token)) + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("Unstar", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo, token)) + MakeRequest(t, req, http.StatusNoContent) + }) +} diff --git a/tests/integration/api_user_watch_test.go b/tests/integration/api_user_watch_test.go new file mode 100644 index 0000000000..e45050a278 --- /dev/null +++ b/tests/integration/api_user_watch_test.go @@ -0,0 +1,78 @@ +// 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 integration + +import ( + "fmt" + "net/http" + "testing" + + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPIWatch(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := "user1" + repo := "user2/repo1" + + session := loginUser(t, user) + token := getTokenForLoggedInUser(t, session) + + t.Run("Watch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/subscription?token=%s", repo, token)) + MakeRequest(t, req, http.StatusOK) + }) + + t.Run("GetWatchedRepos", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/subscriptions?token=%s", user, token)) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "1", resp.Header().Get("X-Total-Count")) + + var repos []api.Repository + DecodeJSON(t, resp, &repos) + assert.Len(t, repos, 1) + assert.Equal(t, repo, repos[0].FullName) + }) + + t.Run("GetMyWatchedRepos", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/subscriptions?token=%s", token)) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "1", resp.Header().Get("X-Total-Count")) + + var repos []api.Repository + DecodeJSON(t, resp, &repos) + assert.Len(t, repos, 1) + assert.Equal(t, repo, repos[0].FullName) + }) + + t.Run("IsWatching", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/subscription?token=%s", repo, token)) + MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/subscription?token=%s", repo+"notexisting", token)) + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("Unwatch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/subscription?token=%s", repo, token)) + MakeRequest(t, req, http.StatusNoContent) + }) +} diff --git a/tests/integration/git_clone_wiki_test.go b/tests/integration/git_clone_wiki_test.go index 3e10b17d40..e8764e6bd9 100644 --- a/tests/integration/git_clone_wiki_test.go +++ b/tests/integration/git_clone_wiki_test.go @@ -41,7 +41,7 @@ func TestRepoCloneWiki(t *testing.T) { u, _ = url.Parse(r) u.User = url.UserPassword("user2", userPassword) t.Run("Clone", func(t *testing.T) { - assert.NoError(t, git.CloneWithArgs(context.Background(), u.String(), dstPath, git.AllowLFSFiltersArgs(), git.CloneRepoOptions{})) + assert.NoError(t, git.CloneWithArgs(context.Background(), git.AllowLFSFiltersArgs(), u.String(), dstPath, git.CloneRepoOptions{})) assertFileEqual(t, filepath.Join(dstPath, "Home.md"), []byte("# Home page\n\nThis is the home page!\n")) assertFileExist(t, filepath.Join(dstPath, "Page-With-Image.md")) assertFileExist(t, filepath.Join(dstPath, "Page-With-Spaced-Name.md")) diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go index 4f2e540883..45b4f44475 100644 --- a/tests/integration/git_helper_for_declarative_test.go +++ b/tests/integration/git_helper_for_declarative_test.go @@ -98,7 +98,7 @@ func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL), prepare ...bo func doGitClone(dstLocalPath string, u *url.URL) func(*testing.T) { return func(t *testing.T) { - assert.NoError(t, git.CloneWithArgs(context.Background(), u.String(), dstLocalPath, git.AllowLFSFiltersArgs(), git.CloneRepoOptions{})) + assert.NoError(t, git.CloneWithArgs(context.Background(), git.AllowLFSFiltersArgs(), u.String(), dstLocalPath, git.CloneRepoOptions{})) exist, err := util.IsExist(filepath.Join(dstLocalPath, "README.md")) assert.NoError(t, err) assert.True(t, exist) @@ -107,7 +107,7 @@ func doGitClone(dstLocalPath string, u *url.URL) func(*testing.T) { func doPartialGitClone(dstLocalPath string, u *url.URL) func(*testing.T) { return func(t *testing.T) { - assert.NoError(t, git.CloneWithArgs(context.Background(), u.String(), dstLocalPath, git.AllowLFSFiltersArgs(), git.CloneRepoOptions{ + assert.NoError(t, git.CloneWithArgs(context.Background(), git.AllowLFSFiltersArgs(), u.String(), dstLocalPath, git.CloneRepoOptions{ Filter: "blob:none", })) exist, err := util.IsExist(filepath.Join(dstLocalPath, "README.md")) @@ -150,47 +150,47 @@ func doGitInitTestRepository(dstPath string) func(*testing.T) { func doGitAddRemote(dstPath, remoteName string, u *url.URL) func(*testing.T) { return func(t *testing.T) { - _, _, err := git.NewCommand(git.DefaultContext, "remote", "add", remoteName, u.String()).RunStdString(&git.RunOpts{Dir: dstPath}) + _, _, err := git.NewCommand(git.DefaultContext, "remote", "add").AddDynamicArguments(remoteName, u.String()).RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) } } -func doGitPushTestRepository(dstPath string, args ...string) func(*testing.T) { +func doGitPushTestRepository(dstPath string, args ...git.CmdArg) func(*testing.T) { return func(t *testing.T) { - _, _, err := git.NewCommand(git.DefaultContext, append([]string{"push", "-u"}, args...)...).RunStdString(&git.RunOpts{Dir: dstPath}) + _, _, err := git.NewCommand(git.DefaultContext, append([]git.CmdArg{"push", "-u"}, args...)...).RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) } } -func doGitPushTestRepositoryFail(dstPath string, args ...string) func(*testing.T) { +func doGitPushTestRepositoryFail(dstPath string, args ...git.CmdArg) func(*testing.T) { return func(t *testing.T) { - _, _, err := git.NewCommand(git.DefaultContext, append([]string{"push"}, args...)...).RunStdString(&git.RunOpts{Dir: dstPath}) + _, _, err := git.NewCommand(git.DefaultContext, append([]git.CmdArg{"push"}, args...)...).RunStdString(&git.RunOpts{Dir: dstPath}) assert.Error(t, err) } } func doGitCreateBranch(dstPath, branch string) func(*testing.T) { return func(t *testing.T) { - _, _, err := git.NewCommand(git.DefaultContext, "checkout", "-b", branch).RunStdString(&git.RunOpts{Dir: dstPath}) + _, _, err := git.NewCommand(git.DefaultContext, "checkout", "-b").AddDynamicArguments(branch).RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) } } -func doGitCheckoutBranch(dstPath string, args ...string) func(*testing.T) { +func doGitCheckoutBranch(dstPath string, args ...git.CmdArg) func(*testing.T) { return func(t *testing.T) { _, _, err := git.NewCommandNoGlobals(append(append(git.AllowLFSFiltersArgs(), "checkout"), args...)...).RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) } } -func doGitMerge(dstPath string, args ...string) func(*testing.T) { +func doGitMerge(dstPath string, args ...git.CmdArg) func(*testing.T) { return func(t *testing.T) { - _, _, err := git.NewCommand(git.DefaultContext, append([]string{"merge"}, args...)...).RunStdString(&git.RunOpts{Dir: dstPath}) + _, _, err := git.NewCommand(git.DefaultContext, append([]git.CmdArg{"merge"}, args...)...).RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) } } -func doGitPull(dstPath string, args ...string) func(*testing.T) { +func doGitPull(dstPath string, args ...git.CmdArg) func(*testing.T) { return func(t *testing.T) { _, _, err := git.NewCommandNoGlobals(append(append(git.AllowLFSFiltersArgs(), "pull"), args...)...).RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index bf5e809dab..6f656ef2ce 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -151,7 +151,7 @@ func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS strin prefix := "lfs-data-file-" err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("install").Run(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) - _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("track", prefix+"*").RunStdString(&git.RunOpts{Dir: dstPath}) + _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("track").AddDynamicArguments(prefix + "*").RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, err) err = git.AddChanges(dstPath, false, ".gitattributes") assert.NoError(t, err) @@ -279,11 +279,11 @@ func lockTest(t *testing.T, repoPath string) { func lockFileTest(t *testing.T, filename, repoPath string) { _, _, err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath}) assert.NoError(t, err) - _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("lock", filename).RunStdString(&git.RunOpts{Dir: repoPath}) + _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("lock").AddDynamicArguments(filename).RunStdString(&git.RunOpts{Dir: repoPath}) assert.NoError(t, err) _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath}) assert.NoError(t, err) - _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("unlock", filename).RunStdString(&git.RunOpts{Dir: repoPath}) + _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("unlock").AddDynamicArguments(filename).RunStdString(&git.RunOpts{Dir: repoPath}) assert.NoError(t, err) } @@ -509,7 +509,7 @@ func doCreatePRAndSetManuallyMerged(ctx, baseCtx APITestContext, dstPath, baseBr })) t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch)) - t.Run("PushToHeadBranch", doGitPushTestRepository(dstPath, "origin", headBranch)) + t.Run("PushToHeadBranch", doGitPushTestRepository(dstPath, "origin", git.CmdArgCheck(headBranch))) t.Run("CreateEmptyPullRequest", func(t *testing.T) { pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t) assert.NoError(t, err) @@ -736,7 +736,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB }) t.Run("Push", func(t *testing.T) { - err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o", "topic="+headBranch).Run(&git.RunOpts{Dir: dstPath}) + err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + headBranch).Run(&git.RunOpts{Dir: dstPath}) if !assert.NoError(t, err) { return } @@ -757,7 +757,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB assert.Contains(t, "Testing commit 1", prMsg.Body) assert.Equal(t, commit, prMsg.Head.Sha) - _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test/"+headBranch).RunStdString(&git.RunOpts{Dir: dstPath}) + _, _, err = git.NewCommand(git.DefaultContext, "push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).RunStdString(&git.RunOpts{Dir: dstPath}) if !assert.NoError(t, err) { return } @@ -810,7 +810,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB }) t.Run("Push2", func(t *testing.T) { - err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o", "topic="+headBranch).Run(&git.RunOpts{Dir: dstPath}) + err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + headBranch).Run(&git.RunOpts{Dir: dstPath}) if !assert.NoError(t, err) { return } @@ -822,7 +822,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB assert.Equal(t, false, prMsg.HasMerged) assert.Equal(t, commit, prMsg.Head.Sha) - _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test/"+headBranch).RunStdString(&git.RunOpts{Dir: dstPath}) + _, _, err = git.NewCommand(git.DefaultContext, "push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).RunStdString(&git.RunOpts{Dir: dstPath}) if !assert.NoError(t, err) { return } diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 8fc8a854a7..416cc126bd 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -18,6 +18,7 @@ import ( "os" "path/filepath" "strings" + "sync/atomic" "testing" "time" @@ -263,19 +264,19 @@ var tokenCounter int64 func getTokenForLoggedInUser(t testing.TB, session *TestSession) string { t.Helper() - tokenCounter++ req := NewRequest(t, "GET", "/user/settings/applications") resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) req = NewRequestWithValues(t, "POST", "/user/settings/applications", map[string]string{ "_csrf": doc.GetCSRF(), - "name": fmt.Sprintf("api-testing-token-%d", tokenCounter), + "name": fmt.Sprintf("api-testing-token-%d", atomic.AddInt64(&tokenCounter, 1)), }) session.MakeRequest(t, req, http.StatusSeeOther) req = NewRequest(t, "GET", "/user/settings/applications") resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) token := htmlDoc.doc.Find(".ui.info p").Text() + assert.NotEmpty(t, token) return token } diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index 9621acbbcc..7863313b14 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -86,6 +86,17 @@ func TestAuthorizeRedirectWithExistingGrant(t *testing.T) { assert.Equal(t, "https://example.com/xyzzy", u.String()) } +func TestAuthorizePKCERequiredForPublicClient(t *testing.T) { + defer tests.PrepareTestEnv(t)() + req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=ce5a1322-42a7-11ed-b878-0242ac120002&redirect_uri=http%3A%2F%2F127.0.0.1&response_type=code&state=thestate") + ctx := loginUser(t, "user1") + resp := ctx.MakeRequest(t, req, http.StatusSeeOther) + u, err := resp.Result().Location() + assert.NoError(t, err) + assert.Equal(t, "invalid_request", u.Query().Get("error")) + assert.Equal(t, "PKCE is required for public clients", u.Query().Get("error_description")) +} + func TestAccessTokenExchange(t *testing.T) { defer tests.PrepareTestEnv(t)() req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ @@ -299,10 +310,11 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) { "client_secret": "inconsistent", }) req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9") + resp = MakeRequest(t, req, http.StatusBadRequest) parsedError = new(auth.AccessTokenError) assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) assert.Equal(t, "invalid_request", string(parsedError.ErrorCode)) - assert.Equal(t, "client_id in request body inconsistent with Authorization header", parsedError.ErrorDescription) + assert.Equal(t, "client_secret in request body inconsistent with Authorization header", parsedError.ErrorDescription) } func TestRefreshTokenInvalidation(t *testing.T) { @@ -329,7 +341,33 @@ func TestRefreshTokenInvalidation(t *testing.T) { // test without invalidation setting.OAuth2.InvalidateRefreshTokens = false - refreshReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ + req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ + "grant_type": "refresh_token", + "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", + // omit secret + "redirect_uri": "a", + "refresh_token": parsed.RefreshToken, + }) + resp = MakeRequest(t, req, http.StatusBadRequest) + parsedError := new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "invalid_client", string(parsedError.ErrorCode)) + assert.Equal(t, "invalid empty client secret", parsedError.ErrorDescription) + + req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ + "grant_type": "refresh_token", + "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", + "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", + "redirect_uri": "a", + "refresh_token": "UNEXPECTED", + }) + resp = MakeRequest(t, req, http.StatusBadRequest) + parsedError = new(auth.AccessTokenError) + assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) + assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode)) + assert.Equal(t, "unable to parse refresh token", parsedError.ErrorDescription) + + req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ "grant_type": "refresh_token", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", @@ -337,24 +375,24 @@ func TestRefreshTokenInvalidation(t *testing.T) { "refresh_token": parsed.RefreshToken, }) - bs, err := io.ReadAll(refreshReq.Body) + bs, err := io.ReadAll(req.Body) assert.NoError(t, err) - refreshReq.Body = io.NopCloser(bytes.NewReader(bs)) - MakeRequest(t, refreshReq, http.StatusOK) + req.Body = io.NopCloser(bytes.NewReader(bs)) + MakeRequest(t, req, http.StatusOK) - refreshReq.Body = io.NopCloser(bytes.NewReader(bs)) - MakeRequest(t, refreshReq, http.StatusOK) + req.Body = io.NopCloser(bytes.NewReader(bs)) + MakeRequest(t, req, http.StatusOK) // test with invalidation setting.OAuth2.InvalidateRefreshTokens = true - refreshReq.Body = io.NopCloser(bytes.NewReader(bs)) - MakeRequest(t, refreshReq, http.StatusOK) + req.Body = io.NopCloser(bytes.NewReader(bs)) + MakeRequest(t, req, http.StatusOK) // repeat request should fail - refreshReq.Body = io.NopCloser(bytes.NewReader(bs)) - resp = MakeRequest(t, refreshReq, http.StatusBadRequest) - parsedError := new(auth.AccessTokenError) + req.Body = io.NopCloser(bytes.NewReader(bs)) + resp = MakeRequest(t, req, http.StatusBadRequest) + parsedError = new(auth.AccessTokenError) assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError)) assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode)) assert.Equal(t, "token was already used", parsedError.ErrorDescription) diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go new file mode 100644 index 0000000000..470478589a --- /dev/null +++ b/tests/integration/org_team_invite_test.go @@ -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 integration + +import ( + "fmt" + "net/http" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestOrgTeamEmailInvite(t *testing.T) { + if setting.MailService == nil { + t.Skip() + return + } + + defer tests.PrepareTestEnv(t)() + + org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + + isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID) + assert.NoError(t, err) + assert.False(t, isMember) + + session := loginUser(t, "user1") + + url := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) + csrf := GetCSRF(t, session, url) + req := NewRequestWithValues(t, "POST", url+"/action/add", map[string]string{ + "_csrf": csrf, + "uid": "1", + "uname": user.Email, + }) + resp := session.MakeRequest(t, req, http.StatusSeeOther) + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + // get the invite token + invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID) + assert.NoError(t, err) + assert.Len(t, invites, 1) + + session = loginUser(t, user.Name) + + // join the team + url = fmt.Sprintf("/org/invite/%s", invites[0].Token) + csrf = GetCSRF(t, session, url) + req = NewRequestWithValues(t, "POST", url, map[string]string{ + "_csrf": csrf, + }) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + req = NewRequest(t, "GET", test.RedirectURL(resp)) + session.MakeRequest(t, req, http.StatusOK) + + isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID) + assert.NoError(t, err) + assert.True(t, isMember) +} diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 335dae4b38..9bd430084d 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -287,7 +287,7 @@ func TestCantMergeUnrelated(t *testing.T) { assert.NoError(t, err) sha := strings.TrimSpace(stdout.String()) - _, _, err = git.NewCommand(git.DefaultContext, "update-index", "--add", "--replace", "--cacheinfo", "100644", sha, "somewher-over-the-rainbow").RunStdString(&git.RunOpts{Dir: path}) + _, _, err = git.NewCommand(git.DefaultContext, "update-index", "--add", "--replace", "--cacheinfo", "100644", git.CmdArgCheck(sha), "somewher-over-the-rainbow").RunStdString(&git.RunOpts{Dir: path}) assert.NoError(t, err) treeSha, _, err := git.NewCommand(git.DefaultContext, "write-tree").RunStdString(&git.RunOpts{Dir: path}) @@ -310,7 +310,7 @@ func TestCantMergeUnrelated(t *testing.T) { _, _ = messageBytes.WriteString("\n") stdout.Reset() - err = git.NewCommand(git.DefaultContext, "commit-tree", treeSha). + err = git.NewCommand(git.DefaultContext, "commit-tree").AddDynamicArguments(treeSha). Run(&git.RunOpts{ Env: env, Dir: path, @@ -320,7 +320,7 @@ func TestCantMergeUnrelated(t *testing.T) { assert.NoError(t, err) commitSha := strings.TrimSpace(stdout.String()) - _, _, err = git.NewCommand(git.DefaultContext, "branch", "unrelated", commitSha).RunStdString(&git.RunOpts{Dir: path}) + _, _, err = git.NewCommand(git.DefaultContext, "branch", "unrelated").AddDynamicArguments(commitSha).RunStdString(&git.RunOpts{Dir: path}) assert.NoError(t, err) testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n") diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index 8dfa9d08f1..8e494a9af4 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -22,13 +22,24 @@ import ( func TestViewRepo(t *testing.T) { defer tests.PrepareTestEnv(t)() + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user2/repo1") - MakeRequest(t, req, http.StatusOK) + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + noDescription := htmlDoc.doc.Find("#repo-desc").Children() + repoTopics := htmlDoc.doc.Find("#repo-topics").Children() + repoSummary := htmlDoc.doc.Find(".repository-summary").Children() + + assert.True(t, noDescription.HasClass("no-description")) + assert.True(t, repoTopics.HasClass("repo-topic")) + assert.True(t, repoSummary.HasClass("repository-menu")) req = NewRequest(t, "GET", "/user3/repo3") MakeRequest(t, req, http.StatusNotFound) - session := loginUser(t, "user1") + session = loginUser(t, "user1") session.MakeRequest(t, req, http.StatusNotFound) } @@ -178,7 +189,71 @@ func TestViewAsRepoAdmin(t *testing.T) { htmlDoc := NewHTMLParser(t, resp.Body) noDescription := htmlDoc.doc.Find("#repo-desc").Children() + repoTopics := htmlDoc.doc.Find("#repo-topics").Children() + repoSummary := htmlDoc.doc.Find(".repository-summary").Children() assert.Equal(t, expectedNoDescription, noDescription.HasClass("no-description")) + assert.True(t, repoTopics.HasClass("repo-topic")) + assert.True(t, repoSummary.HasClass("repository-menu")) } } + +// TestViewFileInRepo repo description, topics and summary should not be displayed when viewing a file +func TestViewFileInRepo(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user2/repo1/src/branch/master/README.md") + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + description := htmlDoc.doc.Find("#repo-desc") + repoTopics := htmlDoc.doc.Find("#repo-topics") + repoSummary := htmlDoc.doc.Find(".repository-summary") + + assert.EqualValues(t, 0, description.Length()) + assert.EqualValues(t, 0, repoTopics.Length()) + assert.EqualValues(t, 0, repoSummary.Length()) +} + +// TestBlameFileInRepo repo description, topics and summary should not be displayed when running blame on a file +func TestBlameFileInRepo(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user2/repo1/blame/branch/master/README.md") + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + description := htmlDoc.doc.Find("#repo-desc") + repoTopics := htmlDoc.doc.Find("#repo-topics") + repoSummary := htmlDoc.doc.Find(".repository-summary") + + assert.EqualValues(t, 0, description.Length()) + assert.EqualValues(t, 0, repoTopics.Length()) + assert.EqualValues(t, 0, repoSummary.Length()) +} + +// TestViewRepoDirectory repo description, topics and summary should not be displayed when within a directory +func TestViewRepoDirectory(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user2/repo20/src/branch/master/a") + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + description := htmlDoc.doc.Find("#repo-desc") + repoTopics := htmlDoc.doc.Find("#repo-topics") + repoSummary := htmlDoc.doc.Find(".repository-summary") + + repoFilesTable := htmlDoc.doc.Find("#repo-files-table") + assert.NotZero(t, len(repoFilesTable.Nodes)) + + assert.Zero(t, description.Length()) + assert.Zero(t, repoTopics.Length()) + assert.Zero(t, repoSummary.Length()) +} diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000000..838360970f --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,32 @@ +import {defineConfig} from 'vitest/dist/config.js'; +import {readFile} from 'fs/promises'; +import {dataToEsm} from '@rollup/pluginutils'; +import {extname} from 'path'; + +function stringPlugin() { + return { + name: 'string-plugin', + enforce: 'pre', + async load(id) { + const path = id.split('?')[0]; + if (extname(path) !== '.svg') return null; + return dataToEsm(await readFile(path, 'utf8')); + } + }; +} + +export default defineConfig({ + test: { + include: ['web_src/**/*.test.js'], + setupFiles: ['./web_src/js/test/setup.js'], + environment: 'jsdom', + testTimeout: 20000, + open: false, + allowOnly: true, + passWithNoTests: true, + watch: false, + }, + plugins: [ + stringPlugin(), + ], +}); diff --git a/web_src/js/bootstrap.js b/web_src/js/bootstrap.js index 213c9e41df..b5db398545 100644 --- a/web_src/js/bootstrap.js +++ b/web_src/js/bootstrap.js @@ -25,6 +25,10 @@ function processWindowErrorEvent(e) { // If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0. return; // ignore such nonsense error event } + + // Wait for upstream fix: https://github.com/microsoft/monaco-editor/issues/2962 + if (e.message.includes('Language id "vs.editor.nullLanguage" is not configured nor known')) return; + showGlobalErrorMessage(`JavaScript error: ${e.message} (${e.filename} @ ${e.lineno}:${e.colno}). Open browser console to see more details.`); } diff --git a/web_src/js/features/admin-common.js b/web_src/js/features/admin/common.js similarity index 100% rename from web_src/js/features/admin-common.js rename to web_src/js/features/admin/common.js diff --git a/web_src/js/features/admin/config.js b/web_src/js/features/admin/config.js new file mode 100644 index 0000000000..f5d8fae8fa --- /dev/null +++ b/web_src/js/features/admin/config.js @@ -0,0 +1,37 @@ +import $ from 'jquery'; +import {showTemporaryTooltip} from '../../modules/tippy.js'; + +const {appSubUrl, csrfToken, pageData} = window.config; + +export function initAdminConfigs() { + const isAdminConfigPage = pageData?.adminConfigPage; + if (!isAdminConfigPage) return; + + $("input[type='checkbox']").on('change', (e) => { + const $this = $(e.currentTarget); + $.ajax({ + url: `${appSubUrl}/admin/config`, + type: 'POST', + data: { + _csrf: csrfToken, + key: $this.attr('name'), + value: $this.is(':checked'), + version: $this.attr('version'), + } + }).done((resp) => { + if (resp) { + if (resp.redirect) { + window.location.href = resp.redirect; + } else if (resp.version) { + $this.attr('version', resp.version); + } else if (resp.err) { + showTemporaryTooltip(e.currentTarget, resp.err); + $this.prop('checked', !$this.is(':checked')); + } + } + }); + + e.preventDefault(); + return false; + }); +} diff --git a/web_src/js/features/admin-emails.js b/web_src/js/features/admin/emails.js similarity index 100% rename from web_src/js/features/admin-emails.js rename to web_src/js/features/admin/emails.js diff --git a/web_src/js/features/admin-users.js b/web_src/js/features/admin/users.js similarity index 100% rename from web_src/js/features/admin-users.js rename to web_src/js/features/admin/users.js diff --git a/web_src/js/features/codeeditor.js b/web_src/js/features/codeeditor.js index a22043c9d4..0366afc2c0 100644 --- a/web_src/js/features/codeeditor.js +++ b/web_src/js/features/codeeditor.js @@ -17,6 +17,7 @@ const baseOptions = { rulers: false, scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6}, scrollBeyondLastLine: false, + automaticLayout: true, }; function getEditorconfig(input) { @@ -111,10 +112,6 @@ export async function createMonaco(textarea, filename, editorOpts) { textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure }); - window.addEventListener('resize', () => { - editor.layout(); - }); - exportEditor(editor); const loading = document.querySelector('.editor-loading'); diff --git a/web_src/js/features/comp/SearchUserBox.js b/web_src/js/features/comp/SearchUserBox.js index 08f97595af..46ecb8ebf4 100644 --- a/web_src/js/features/comp/SearchUserBox.js +++ b/web_src/js/features/comp/SearchUserBox.js @@ -3,15 +3,20 @@ import {htmlEscape} from 'escape-goat'; const {appSubUrl} = window.config; +const looksLikeEmailAddressCheck = /^\S+@\S+$/; + export function initCompSearchUserBox() { const $searchUserBox = $('#search-user-box'); + const allowEmailInput = $searchUserBox.attr('data-allow-email') === 'true'; + const allowEmailDescription = $searchUserBox.attr('data-allow-email-description'); $searchUserBox.search({ minCharacters: 2, apiSettings: { url: `${appSubUrl}/user/search?q={query}`, onResponse(response) { const items = []; - const searchQueryUppercase = $searchUserBox.find('input').val().toUpperCase(); + const searchQuery = $searchUserBox.find('input').val(); + const searchQueryUppercase = searchQuery.toUpperCase(); $.each(response.data, (_i, item) => { let title = item.login; if (item.full_name && item.full_name.length > 0) { @@ -28,6 +33,14 @@ export function initCompSearchUserBox() { } }); + if (allowEmailInput && items.length === 0 && looksLikeEmailAddressCheck.test(searchQuery)) { + const resultItem = { + title: searchQuery, + description: allowEmailDescription + }; + items.push(resultItem); + } + return {results: items}; } }, diff --git a/web_src/js/features/formatting.js b/web_src/js/features/formatting.js index 5f4633bba2..c8f5db9e14 100644 --- a/web_src/js/features/formatting.js +++ b/web_src/js/features/formatting.js @@ -1,7 +1,10 @@ import {prettyNumber} from '../utils.js'; const {lang} = document.documentElement; + const dateFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'long', day: 'numeric'}); +const shortDateFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'short', day: 'numeric'}); +const dateTimeFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric'}); export function initFormattingReplacements() { // replace english formatted numbers with locale-specific separators @@ -13,9 +16,28 @@ export function initFormattingReplacements() { } } - // for each tag, if it has the data-format="date" attribute, format - // the text according to the user's chosen locale - for (const timeElement of document.querySelectorAll('time[data-format="date"]')) { - timeElement.textContent = dateFormatter.format(new Date(timeElement.dateTime)); + // for each tag, if it has the data-format attribute, format + // the text according to the user's chosen locale and formatter. + formatAllTimeElements(); +} + +function formatAllTimeElements() { + const timeElements = document.querySelectorAll('time[data-format]'); + for (const timeElement of timeElements) { + const formatter = getFormatter(timeElement.dataset.format); + timeElement.textContent = formatter.format(new Date(timeElement.dateTime)); + } +} + +function getFormatter(format) { + switch (format) { + case 'date': + return dateFormatter; + case 'short-date': + return shortDateFormatter; + case 'date-time': + return dateTimeFormatter; + default: + throw new Error('Unknown format'); } } diff --git a/web_src/js/features/repo-findfile.test.js b/web_src/js/features/repo-findfile.test.js index 2d96ed4463..a90b0bf0a2 100644 --- a/web_src/js/features/repo-findfile.test.js +++ b/web_src/js/features/repo-findfile.test.js @@ -1,3 +1,4 @@ +import {describe, expect, test} from 'vitest'; import {strSubMatch, calcMatchedWeight, filterRepoFilesWeighted} from './repo-findfile.js'; describe('Repo Find Files', () => { diff --git a/web_src/js/features/serviceworker.js b/web_src/js/features/serviceworker.js index 23d68bced0..a072811b04 100644 --- a/web_src/js/features/serviceworker.js +++ b/web_src/js/features/serviceworker.js @@ -1,6 +1,6 @@ import {joinPaths, parseUrl} from '../utils.js'; -const {useServiceWorker, assetUrlPrefix, appVer, assetVersionEncoded} = window.config; +const {useServiceWorker, assetUrlPrefix, assetVersionEncoded} = window.config; const cachePrefix = 'static-cache-v'; // actual version is set in the service worker script const workerUrl = `${joinPaths(assetUrlPrefix, 'serviceworker.js')}?v=${assetVersionEncoded}`; @@ -25,7 +25,7 @@ async function invalidateCache() { } async function checkCacheValidity() { - const cacheKey = appVer; + const cacheKey = assetVersionEncoded; const storedCacheKey = localStorage.getItem('staticCacheKey'); // invalidate cache if it belongs to a different gitea version diff --git a/web_src/js/index.js b/web_src/js/index.js index b13ad0e13a..a829deaf11 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -16,7 +16,8 @@ import initRepoMigration from './features/repo-migration.js'; import initRepoProject from './features/repo-projects.js'; import initServiceWorker from './features/serviceworker.js'; import initTableSort from './features/tablesort.js'; -import {initAdminUserListSearchForm} from './features/admin-users.js'; +import {initAdminUserListSearchForm} from './features/admin/users.js'; +import {initAdminConfigs} from './features/admin/config.js'; import {initMarkupAnchors} from './markup/anchors.js'; import {initNotificationCount, initNotificationsTable} from './features/notification.js'; import {initRepoIssueContentHistory} from './features/repo-issue-content.js'; @@ -60,8 +61,8 @@ import { initGlobalTooltips, } from './features/common-global.js'; import {initRepoTopicBar} from './features/repo-home.js'; -import {initAdminEmails} from './features/admin-emails.js'; -import {initAdminCommon} from './features/admin-common.js'; +import {initAdminEmails} from './features/admin/emails.js'; +import {initAdminCommon} from './features/admin/common.js'; import {initRepoTemplateSearch} from './features/repo-template.js'; import {initRepoCodeView} from './features/repo-code.js'; import {initSshKeyFormParser} from './features/sshkey-helper.js'; @@ -139,6 +140,7 @@ $(document).ready(() => { initAdminCommon(); initAdminEmails(); initAdminUserListSearchForm(); + initAdminConfigs(); initDashboardRepoList(); diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js index 773c46e791..62de9a3aae 100644 --- a/web_src/js/markup/mermaid.js +++ b/web_src/js/markup/mermaid.js @@ -50,7 +50,7 @@ export async function renderMermaid() { // can't use bindFunctions here because we can't cross the iframe boundary. This // means js-based interactions won't work but they aren't intended to work either mermaid.mermaidAPI.render('mermaid', source, (svgStr) => { - const heightStr = (svgStr.match(/height="(.+?)"/) || [])[1]; + const heightStr = (svgStr.match(/viewBox="(.+?)"/) || ['', ''])[1].split(/\s+/)[3]; if (!heightStr) return displayError(el, new Error('Could not determine chart height')); const iframe = document.createElement('iframe'); iframe.classList.add('markup-render'); diff --git a/web_src/js/svg.test.js b/web_src/js/svg.test.js index f1939c3a46..c5d6d07535 100644 --- a/web_src/js/svg.test.js +++ b/web_src/js/svg.test.js @@ -1,7 +1,8 @@ +import {expect, test} from 'vitest'; import {svg} from './svg.js'; test('svg', () => { - expect(svg('octicon-repo')).toStartWith(' { - return {code: `module.exports = ${JSON.stringify(content)}`}; - }, -}; diff --git a/web_src/js/utils.test.js b/web_src/js/utils.test.js index 762f29f6fe..7bf5bb7eb6 100644 --- a/web_src/js/utils.test.js +++ b/web_src/js/utils.test.js @@ -1,3 +1,4 @@ +import {expect, test} from 'vitest'; import { basename, extname, isObject, uniq, stripTags, joinPaths, parseIssueHref, prettyNumber, parseUrl, @@ -56,8 +57,8 @@ test('joinPaths', () => { }); test('isObject', () => { - expect(isObject({})).toBeTrue(); - expect(isObject([])).toBeFalse(); + expect(isObject({})).toBeTruthy(); + expect(isObject([])).toBeFalsy(); }); test('uniq', () => { diff --git a/web_src/less/_base.less b/web_src/less/_base.less index c66cabd8a1..1f32307012 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -12,6 +12,7 @@ --height-loading: 12rem; /* base colors */ --color-primary: #4183c4; + --color-primary-contrast: #ffffff; --color-primary-dark-1: #3876b3; --color-primary-dark-2: #31699f; --color-primary-dark-3: #2b5c8b; @@ -131,6 +132,7 @@ --color-timeline: #ececec; --color-input-text: #212121; --color-input-background: #ffffff; + --color-input-toggle-background: #dedede; --color-input-border: #dedede; --color-input-border-hover: #cecece; --color-navbar: #f8f8f8; @@ -162,9 +164,11 @@ --color-tooltip-text: #ffffff; --color-header-bar: #ffffff; --color-label-active-bg: #d0d0d0; + --color-accent: var(--color-primary-light-1); --color-small-accent: var(--color-primary-light-6); - --color-accent: var(--color-primary-light-4); --color-active-line: #fffbdd; + + accent-color: var(--color-accent); } :root * { @@ -229,10 +233,10 @@ progress::-webkit-progress-bar { background: var(--color-secondary-dark-1); } progress::-webkit-progress-value { - background-color: var(--color-secondary-dark-4); + background-color: var(--color-accent); } progress::-moz-progress-bar { - background: var(--color-secondary-dark-4); + background-color: var(--color-accent); } * { @@ -259,6 +263,17 @@ progress::-moz-progress-bar { background: transparent; } +::file-selector-button { + border: 1px solid var(--color-light-border); + color: var(--color-text-light); + background: var(--color-light); + border-radius: var(--border-radius); +} +::file-selector-button:hover { + color: var(--color-text); + background: var(--color-hover); +} + ::selection { background: var(--color-primary-light-1) !important; color: var(--color-white) !important; @@ -332,6 +347,11 @@ a.commit-statuses-trigger { &:extend(.unselectable); } +.issue-title code { + padding: 2px 4px; + border-radius: 6px; + background-color: var(--color-markup-code-block); +} /* try to match button with no icons in height */ .icon-button { padding-top: 7.42px !important; @@ -1365,6 +1385,14 @@ a.ui.card:hover, border-color: var(--color-secondary); } +.color-preview { + display: inline-block; + margin-left: .4em; + height: .67em; + width: .67em; + border-radius: .15em; +} + footer { background-color: var(--color-footer); border-top: 1px solid var(--color-secondary); @@ -1584,11 +1612,11 @@ footer { .activity-bar-graph { background-color: var(--color-primary); - color: #fff; + color: var(--color-primary-contrast); } .activity-bar-graph-alt { - color: #fff; + color: var(--color-primary-contrast); } .archived-icon { @@ -1885,6 +1913,7 @@ a.ui.label:hover { .ui.primary.button, .ui.primary.buttons .button { background-color: var(--color-primary) !important; + color: var(--color-primary-contrast) !important; } .ui.primary.button:hover, @@ -1900,7 +1929,7 @@ a.ui.label:hover { .ui.basic.primary.button, .ui.basic.primary.buttons .button { box-shadow: inset 0 0 0 1px var(--color-primary) !important; - color: #fff !important; + color: var(--color-primary-contrast) !important; } .ui.basic.primary.button:hover, diff --git a/web_src/less/_dashboard.less b/web_src/less/_dashboard.less index 570d772729..e9a906cbed 100644 --- a/web_src/less/_dashboard.less +++ b/web_src/less/_dashboard.less @@ -141,10 +141,9 @@ } code { - padding: 1px; - font-size: 85%; - background-color: rgba(0, 0, 0, .04); + padding: 2px 4px; border-radius: 3px; + background-color: var(--color-markup-code-block); word-break: break-all; } diff --git a/web_src/less/_form.less b/web_src/less/_form.less index 08e1f324b4..3d2ec9fb8a 100644 --- a/web_src/less/_form.less +++ b/web_src/less/_form.less @@ -113,7 +113,7 @@ textarea:focus, } .ui.toggle.checkbox label::before { - background: var(--color-input-background); + background: var(--color-input-toggle-background); } .ui.toggle.checkbox label, diff --git a/web_src/less/_organization.less b/web_src/less/_organization.less index b80739671f..c52753e29b 100644 --- a/web_src/less/_organization.less +++ b/web_src/less/_organization.less @@ -119,6 +119,11 @@ margin-top: -3px; } } + + .ui.avatar { + width: 100%; + height: 100%; + } } &.members { diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index c5d2a5f50a..bd007196d4 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -2804,6 +2804,11 @@ margin-top: 10px; } +.repo-button-row .dropdown > .dropdown.icon { + margin-left: .25rem !important; + margin-right: 0 !important; +} + .wiki .repo-button-row { margin-bottom: 0; } @@ -3099,11 +3104,11 @@ td.blob-excerpt { @media @mediaMdAndDown { #diff-file-tree { - display: none; + display: none !important; } .diff-toggle-file-tree-button { - display: none; + display: none !important; } } diff --git a/web_src/less/_review.less b/web_src/less/_review.less index 33e4003b2c..013d6d5aa1 100644 --- a/web_src/less/_review.less +++ b/web_src/less/_review.less @@ -194,7 +194,7 @@ a.blob-excerpt { a.blob-excerpt:hover { background: var(--color-primary); - color: #fff; + color: var(--color-primary-contrast); } // See the comment of createCommentEasyMDE() for the review editor @@ -244,7 +244,7 @@ a.blob-excerpt:hover { #review-box .review-comments-counter { background-color: var(--color-primary-light-4); - color: #fff; + color: var(--color-primary-contrast); } #review-box:hover .review-comments-counter { @@ -275,7 +275,7 @@ a.blob-excerpt:hover { .viewed-file-form { display: flex; align-items: center; - border: 1px none; + border: 1px solid transparent; padding: 4px 8px; margin: -8px 0; // just like other buttons in the diff box header border-radius: .285rem; // just like .ui.tiny.button @@ -288,7 +288,7 @@ a.blob-excerpt:hover { .viewed-file-checked-form { background-color: var(--color-small-accent); - border: 1px solid var(--color-accent); + border-color: var(--color-accent); } #viewed-files-summary { diff --git a/web_src/less/_tribute.less b/web_src/less/_tribute.less index 1626461c57..9adf155ffa 100644 --- a/web_src/less/_tribute.less +++ b/web_src/less/_tribute.less @@ -23,7 +23,7 @@ .tribute-container li.highlight, .tribute-container li:hover { background: var(--color-primary) !important; - color: #ffffff !important; + color: var(--color-primary-contrast) !important; } .tribute-item { diff --git a/web_src/less/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less index 12dba79266..1f6da76db9 100644 --- a/web_src/less/themes/theme-arc-green.less +++ b/web_src/less/themes/theme-arc-green.less @@ -4,6 +4,7 @@ :root { --is-dark-theme: true; --color-primary: #87ab63; + --color-primary-contrast: #ffffff; --color-primary-dark-1: #93b373; --color-primary-dark-2: #9fbc82; --color-primary-dark-3: #abc492; @@ -106,6 +107,7 @@ --color-timeline: #4c525e; --color-input-text: #d5dbe6; --color-input-background: #232933; + --color-input-toggle-background: #454a57; --color-input-border: #454a57; --color-input-border-hover: #505667; --color-navbar: #2a2e3a; @@ -134,8 +136,8 @@ --color-reaction-active-bg: var(--color-primary-alpha-40); --color-header-bar: #2e323e; --color-label-active-bg: #4c525e; + --color-accent: var(--color-primary-light-1); --color-small-accent: var(--color-primary-light-5); - --color-accent: var(--color-primary-light-3); --color-active-line: #534d1b; }