diff --git a/.air.toml b/.air.toml
index 4b3297fc1a77e..069a88924388b 100644
--- a/.air.toml
+++ b/.air.toml
@@ -5,6 +5,6 @@ tmp_dir = ".air"
cmd = "make backend"
bin = "gitea"
include_ext = ["go", "tmpl"]
-exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata"]
-include_dir = ["cmd", "models", "modules", "options", "routers", "services", "templates"]
-exclude_regex = ["_test.go$"]
+exclude_dir = ["modules/git/tests", "services/gitdiff/testdata", "modules/avatar/testdata", "models/fixtures", "models/migrations/fixtures", "modules/migration/file_format_testdata", "modules/avatar/identicon/testdata"]
+include_dir = ["cmd", "models", "modules", "options", "routers", "services"]
+exclude_regex = ["_test.go$", "_gen.go$"]
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000..1ce2a87611e81
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,114 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# IntelliJ
+.idea
+# Goland's output filename can not be set manually
+/go_build_*
+
+# MS VSCode
+.vscode
+__debug_bin
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
+
+*coverage.out
+coverage.all
+cpu.out
+
+/modules/migration/bindata.go
+/modules/migration/bindata.go.hash
+/modules/options/bindata.go
+/modules/options/bindata.go.hash
+/modules/public/bindata.go
+/modules/public/bindata.go.hash
+/modules/templates/bindata.go
+/modules/templates/bindata.go.hash
+
+*.db
+*.log
+
+/gitea
+/gitea-vet
+/debug
+/integrations.test
+
+/bin
+/dist
+/custom/*
+!/custom/conf
+/custom/conf/*
+!/custom/conf/app.example.ini
+/data
+/indexers
+/log
+/public/img/avatar
+/tests/integration/gitea-integration-*
+/tests/integration/indexers-*
+/tests/e2e/gitea-e2e-*
+/tests/e2e/indexers-*
+/tests/e2e/reports
+/tests/e2e/test-artifacts
+/tests/e2e/test-snapshots
+/tests/*.ini
+/node_modules
+/yarn.lock
+/yarn-error.log
+/npm-debug.log*
+/public/js
+/public/serviceworker.js
+/public/css
+/public/fonts
+/public/img/webpack
+/vendor
+/web_src/fomantic/node_modules
+/web_src/fomantic/build/*
+!/web_src/fomantic/build/semantic.js
+!/web_src/fomantic/build/semantic.css
+!/web_src/fomantic/build/themes
+/web_src/fomantic/build/themes/*
+!/web_src/fomantic/build/themes/default
+/web_src/fomantic/build/themes/default/assets/*
+!/web_src/fomantic/build/themes/default/assets/fonts
+/web_src/fomantic/build/themes/default/assets/fonts/*
+!/web_src/fomantic/build/themes/default/assets/fonts/icons.woff2
+!/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2
+/VERSION
+/.air
+/.go-licenses
+
+# Snapcraft
+snap/.snapcraft/
+parts/
+stage/
+prime/
+*.snap
+*.snap-build
+*_source.tar.bz2
+.DS_Store
+
+# Make evidence files
+/.make_evidence
+
+# Manpage
+/man
diff --git a/.drone.yml b/.drone.yml
index b3b1682619a3f..d16386c7b49b5 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -19,13 +19,13 @@ volumes:
steps:
- name: deps-frontend
- image: node:16
+ image: node:18
pull: always
commands:
- make deps-frontend
- name: deps-backend
- image: golang:1.18
+ image: golang:1.19
pull: always
commands:
- make deps-backend
@@ -34,7 +34,7 @@ steps:
path: /go
- name: lint-frontend
- image: node:16
+ image: node:18
commands:
- make lint-frontend
depends_on: [deps-frontend]
@@ -82,34 +82,34 @@ steps:
path: /go
- name: checks-frontend
- image: node:16
+ image: node:18
commands:
- make checks-frontend
depends_on: [deps-frontend]
- name: checks-backend
- image: golang:1.18
+ image: golang:1.19
commands:
- - make checks-backend
+ - make --always-make checks-backend # ensure the 'go-licenses' make target runs
depends_on: [deps-backend]
volumes:
- name: deps
path: /go
- name: test-frontend
- image: node:16
+ image: node:18
commands:
- make test-frontend
depends_on: [lint-frontend]
- name: build-frontend
- image: node:16
+ image: node:18
commands:
- make frontend
- depends_on: [test-frontend]
+ depends_on: [deps-frontend]
- name: build-backend-no-gcc
- image: golang:1.17 # this step is kept as the lowest version of golang that we support
+ image: golang:1.18 # this step is kept as the lowest version of golang that we support
pull: always
environment:
GO111MODULE: on
@@ -122,7 +122,7 @@ steps:
path: /go
- name: build-backend-arm64
- image: golang:1.18
+ image: golang:1.19
environment:
GO111MODULE: on
GOPROXY: https://goproxy.io
@@ -138,7 +138,7 @@ steps:
path: /go
- name: build-backend-windows
- image: golang:1.18
+ image: golang:1.19
environment:
GO111MODULE: on
GOPROXY: https://goproxy.io
@@ -153,7 +153,7 @@ steps:
path: /go
- name: build-backend-386
- image: golang:1.18
+ image: golang:1.19
environment:
GO111MODULE: on
GOPROXY: https://goproxy.io
@@ -230,11 +230,16 @@ services:
MINIO_ACCESS_KEY: 123456
MINIO_SECRET_KEY: 12345678
+ - name: smtpimap
+ image: tabascoterrier/docker-imap-devel:latest
+ pull: always
+
steps:
- name: fetch-tags
image: docker:git
pull: always
commands:
+ - git config --global --add safe.directory /drone/src
- git fetch --tags --force
when:
event:
@@ -242,7 +247,7 @@ steps:
- pull_request
- name: deps-backend
- image: golang:1.18
+ image: golang:1.19
pull: always
commands:
- make deps-backend
@@ -330,7 +335,7 @@ steps:
image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env
user: gitea
commands:
- - timeout -s ABRT 40m make test-mysql8-migration test-mysql8
+ - timeout -s ABRT 50m make test-mysql8-migration test-mysql8
environment:
GOPROXY: https://goproxy.io
TAGS: bindata
@@ -359,7 +364,7 @@ steps:
path: /go
- name: generate-coverage
- image: golang:1.18
+ image: golang:1.19
commands:
- make coverage
environment:
@@ -427,6 +432,7 @@ steps:
image: docker:git
pull: always
commands:
+ - git config --global --add safe.directory /drone/src
- git fetch --tags --force
when:
event:
@@ -434,7 +440,7 @@ steps:
- pull_request
- name: deps-backend
- image: golang:1.18
+ image: golang:1.19
pull: always
commands:
- make deps-backend
@@ -467,7 +473,7 @@ steps:
image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env
user: gitea
commands:
- - timeout -s ABRT 40m make test-sqlite-migration test-sqlite
+ - timeout -s ABRT 50m make test-sqlite-migration test-sqlite
environment:
GOPROXY: https://goproxy.io
TAGS: bindata gogit sqlite sqlite_unlock_notify
@@ -483,7 +489,7 @@ steps:
image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env
user: gitea
commands:
- - timeout -s ABRT 40m make test-pgsql-migration test-pgsql
+ - timeout -s ABRT 50m make test-pgsql-migration test-pgsql
environment:
GOPROXY: https://goproxy.io
TAGS: bindata gogit
@@ -496,6 +502,78 @@ steps:
- name: deps
path: /go
+---
+kind: pipeline
+type: docker
+name: testing-e2e
+
+platform:
+ os: linux
+ arch: amd64
+
+depends_on:
+ - compliance
+
+trigger:
+ event:
+ - pull_request
+
+volumes:
+ - name: deps
+ temp: {}
+
+services:
+ - name: pgsql
+ pull: default
+ image: postgres:10
+ environment:
+ POSTGRES_DB: testgitea-e2e
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_INITDB_ARGS: --encoding=UTF8 --lc-collate='en_US.UTF-8' --lc-ctype='en_US.UTF-8'
+
+steps:
+ - name: deps-frontend
+ image: node:18
+ pull: always
+ commands:
+ - make deps-frontend
+
+ - name: build-frontend
+ image: node:18
+ commands:
+ - make frontend
+ depends_on: [deps-frontend]
+
+ - name: deps-backend
+ image: golang:1.18
+ pull: always
+ commands:
+ - make deps-backend
+ volumes:
+ - name: deps
+ path: /go
+
+ # TODO: We should probably build all dependencies into a test image
+ - name: test-e2e
+ image: mcr.microsoft.com/playwright:v1.29.0-focal
+ commands:
+ - curl -sLO https://go.dev/dl/go1.19.linux-amd64.tar.gz && tar -C /usr/local -xzf go1.19.linux-amd64.tar.gz
+ - groupadd --gid 1001 gitea && useradd -m --gid 1001 --uid 1001 gitea
+ - apt-get -qq update && apt-get -qqy install build-essential
+ - export TEST_PGSQL_SCHEMA=''
+ - ./build/test-env-prepare.sh
+ - su gitea bash -c "export PATH=$PATH:/usr/local/go/bin && timeout -s ABRT 40m make test-e2e-pgsql"
+ environment:
+ GOPROXY: https://goproxy.io
+ GOSUMDB: sum.golang.org
+ USE_REPO_TEST_DIR: 1
+ TEST_PGSQL_DBNAME: 'testgitea-e2e'
+ DEBIAN_FRONTEND: noninteractive
+ depends_on: [build-frontend, deps-backend]
+ volumes:
+ - name: deps
+ path: /go
+
---
kind: pipeline
name: update_translations
@@ -526,7 +604,7 @@ steps:
from_secret: crowdin_key
- name: update
- image: alpine:3.13
+ image: alpine:3.17
pull: always
commands:
- ./build/update-locales.sh
@@ -542,6 +620,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
@@ -576,7 +656,7 @@ trigger:
steps:
- name: download
- image: golang:1.18
+ image: golang:1.19
pull: always
commands:
- timeout -s ABRT 40m make generate-license generate-gitignore
@@ -586,12 +666,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
@@ -628,16 +710,17 @@ steps:
image: docker:git
pull: always
commands:
+ - git config --global --add safe.directory /drone/src
- git fetch --tags --force
- name: deps-frontend
- image: node:16
+ image: node:18
pull: always
commands:
- make deps-frontend
- name: deps-backend
- image: golang:1.18
+ image: golang:1.19
pull: always
commands:
- make deps-backend
@@ -646,15 +729,17 @@ steps:
path: /go
- name: static
- image: techknowlogick/xgo:go-1.18.x
+ image: techknowlogick/xgo:go-1.19.x
pull: always
commands:
- - curl -sL https://deb.nodesource.com/setup_16.x | bash - && apt-get install -y nodejs
+ # Upgrade to node 18 once https://github.com/techknowlogick/xgo/issues/163 is resolved
+ - curl -sL https://deb.nodesource.com/setup_16.x | bash - && apt-get -qqy install nodejs
- export PATH=$PATH:$GOPATH/bin
- make release
environment:
GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not
TAGS: bindata sqlite sqlite_unlock_notify
+ DEBIAN_FRONTEND: noninteractive
volumes:
- name: deps
path: /go
@@ -746,16 +831,17 @@ steps:
image: docker:git
pull: always
commands:
+ - git config --global --add safe.directory /drone/src
- git fetch --tags --force
- name: deps-frontend
- image: node:16
+ image: node:18
pull: always
commands:
- make deps-frontend
- name: deps-backend
- image: golang:1.18
+ image: golang:1.19
pull: always
commands:
- make deps-backend
@@ -764,15 +850,17 @@ steps:
path: /go
- name: static
- image: techknowlogick/xgo:go-1.18.x
+ image: techknowlogick/xgo:go-1.19.x
pull: always
commands:
- - curl -sL https://deb.nodesource.com/setup_16.x | bash - && apt-get install -y nodejs
+ # Upgrade to node 18 once https://github.com/techknowlogick/xgo/issues/163 is resolved
+ - curl -sL https://deb.nodesource.com/setup_16.x | bash - && apt-get -qqy install nodejs
- export PATH=$PATH:$GOPATH/bin
- make release
environment:
GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not
TAGS: bindata sqlite sqlite_unlock_notify
+ DEBIAN_FRONTEND: noninteractive
depends_on: [fetch-tags]
volumes:
- name: deps
@@ -844,10 +932,8 @@ trigger:
steps:
- name: build-docs
- image: plugins/hugo:latest
- pull: always
+ image: golang:1.19
commands:
- - apk add --no-cache make bash curl
- cd docs
- make trans-copy clean build
@@ -891,6 +977,7 @@ steps:
image: docker:git
pull: always
commands:
+ - git config --global --add safe.directory /drone/src
- git fetch --tags --force
- name: publish
@@ -954,6 +1041,7 @@ steps:
image: docker:git
pull: always
commands:
+ - git config --global --add safe.directory /drone/src
- git fetch --tags --force
- name: publish
@@ -1016,6 +1104,7 @@ steps:
image: docker:git
pull: always
commands:
+ - git config --global --add safe.directory /drone/src
- git fetch --tags --force
- name: publish
@@ -1112,6 +1201,7 @@ steps:
image: docker:git
pull: always
commands:
+ - git config --global --add safe.directory /drone/src
- git fetch --tags --force
- name: publish
@@ -1175,6 +1265,7 @@ steps:
image: docker:git
pull: always
commands:
+ - git config --global --add safe.directory /drone/src
- git fetch --tags --force
- name: publish
@@ -1237,6 +1328,7 @@ steps:
image: docker:git
pull: always
commands:
+ - git config --global --add safe.directory /drone/src
- git fetch --tags --force
- name: publish
diff --git a/.editorconfig b/.editorconfig
index 0f8603e5a29e7..c0946ac9975cd 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -26,6 +26,3 @@ indent_style = tab
[*.svg]
insert_final_newline = false
-
-[*.md]
-trim_trailing_whitespace = false
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index 77d9dc1228eef..0000000000000
--- a/.eslintrc
+++ /dev/null
@@ -1,458 +0,0 @@
-root: true
-reportUnusedDisableDirectives: true
-
-ignorePatterns:
- - /web_src/js/vendor
-
-parserOptions:
- sourceType: module
- ecmaVersion: latest
-
-plugins:
- - eslint-plugin-unicorn
- - eslint-plugin-import
- - eslint-plugin-vue
- - eslint-plugin-html
-
-extends:
- - plugin:vue/recommended
-
-env:
- es2021: true
- node: true
-
-globals:
- __webpack_public_path__: true
-
-settings:
- html/html-extensions: [".tmpl"]
-
-overrides:
- - files: ["web_src/**/*.js", "web_src/**/*.vue", "templates/**/*.tmpl"]
- env:
- browser: true
- node: false
- - files: ["templates/**/*.tmpl"]
- rules:
- no-tabs: [0]
- indent: [2, tab, {SwitchCase: 1}]
- - files: ["web_src/**/*worker.js"]
- env:
- worker: true
- rules:
- no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top]
- - files: ["build/generate-images.js"]
- 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]
-
-rules:
- accessor-pairs: [2]
- array-bracket-newline: [0]
- array-bracket-spacing: [2, never]
- array-callback-return: [0]
- array-element-newline: [0]
- arrow-body-style: [0]
- arrow-parens: [2, always]
- arrow-spacing: [2, {before: true, after: true}]
- block-scoped-var: [2]
- brace-style: [2, 1tbs, {allowSingleLine: true}]
- camelcase: [0]
- capitalized-comments: [0]
- class-methods-use-this: [0]
- comma-dangle: [2, only-multiline]
- comma-spacing: [2, {before: false, after: true}]
- comma-style: [2, last]
- complexity: [0]
- computed-property-spacing: [2, never]
- consistent-return: [0]
- consistent-this: [0]
- constructor-super: [2]
- curly: [0]
- default-case-last: [2]
- default-case: [0]
- default-param-last: [0]
- dot-location: [2, property]
- dot-notation: [0]
- eol-last: [2]
- eqeqeq: [2]
- for-direction: [2]
- func-call-spacing: [2, never]
- func-name-matching: [2]
- func-names: [0]
- func-style: [0]
- function-call-argument-newline: [0]
- function-paren-newline: [0]
- generator-star-spacing: [0]
- getter-return: [2]
- grouped-accessor-pairs: [2]
- guard-for-in: [0]
- id-blacklist: [0]
- id-length: [0]
- id-match: [0]
- implicit-arrow-linebreak: [0]
- import/default: [0]
- import/dynamic-import-chunkname: [0]
- import/export: [2]
- import/exports-last: [0]
- import/extensions: [2, always, {ignorePackages: true}]
- import/first: [2]
- import/group-exports: [0]
- import/max-dependencies: [0]
- import/named: [2]
- import/namespace: [0]
- import/newline-after-import: [0]
- import/no-absolute-path: [0]
- import/no-amd: [0]
- import/no-anonymous-default-export: [0]
- import/no-commonjs: [0]
- import/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}]
- import/no-default-export: [0]
- import/no-deprecated: [0]
- import/no-dynamic-require: [0]
- import/no-extraneous-dependencies: [2]
- import/no-import-module-exports: [0]
- import/no-internal-modules: [0]
- import/no-mutable-exports: [2]
- import/no-named-as-default-member: [0]
- import/no-named-as-default: [2]
- import/no-named-default: [0]
- import/no-named-export: [0]
- import/no-namespace: [0]
- import/no-nodejs-modules: [0]
- import/no-relative-packages: [0]
- import/no-relative-parent-imports: [0]
- import/no-restricted-paths: [0]
- import/no-self-import: [2]
- import/no-unassigned-import: [0]
- import/no-unresolved: [2, {commonjs: true}]
- import/no-unused-modules: [2, {unusedExports: true}]
- import/no-useless-path-segments: [2, {commonjs: true}]
- import/no-webpack-loader-syntax: [2]
- import/order: [0]
- import/prefer-default-export: [0]
- import/unambiguous: [0]
- indent: [2, 2, {SwitchCase: 1}]
- init-declarations: [0]
- key-spacing: [2]
- keyword-spacing: [2]
- line-comment-position: [0]
- linebreak-style: [2, unix]
- lines-around-comment: [0]
- lines-between-class-members: [0]
- max-classes-per-file: [0]
- max-depth: [0]
- max-len: [0]
- max-lines-per-function: [0]
- max-lines: [0]
- max-nested-callbacks: [0]
- max-params: [0]
- max-statements-per-line: [0]
- max-statements: [0]
- multiline-comment-style: [2, separate-lines]
- multiline-ternary: [0]
- new-cap: [0]
- new-parens: [2]
- newline-per-chained-call: [0]
- no-alert: [0]
- no-array-constructor: [2]
- no-async-promise-executor: [2]
- no-await-in-loop: [0]
- no-bitwise: [0]
- no-buffer-constructor: [0]
- no-caller: [2]
- no-case-declarations: [2]
- no-class-assign: [2]
- no-compare-neg-zero: [2]
- no-cond-assign: [2, except-parens]
- no-confusing-arrow: [0]
- no-console: [1, {allow: [info, warn, error]}]
- no-const-assign: [2]
- no-constant-condition: [0]
- no-constructor-return: [2]
- no-continue: [0]
- no-control-regex: [0]
- no-debugger: [1]
- no-delete-var: [2]
- no-div-regex: [0]
- no-dupe-args: [2]
- no-dupe-class-members: [2]
- no-dupe-else-if: [2]
- no-dupe-keys: [2]
- no-duplicate-case: [2]
- no-duplicate-imports: [2]
- no-else-return: [2]
- no-empty-character-class: [2]
- no-empty-function: [0]
- no-empty-pattern: [2]
- no-empty: [2, {allowEmptyCatch: true}]
- no-eq-null: [2]
- no-eval: [2]
- no-ex-assign: [2]
- no-extend-native: [2]
- no-extra-bind: [2]
- no-extra-boolean-cast: [2]
- no-extra-label: [0]
- no-extra-parens: [0]
- no-extra-semi: [2]
- no-fallthrough: [2]
- no-floating-decimal: [0]
- no-func-assign: [2]
- no-global-assign: [2]
- no-implicit-coercion: [0]
- no-implicit-globals: [0]
- no-implied-eval: [2]
- no-import-assign: [2]
- no-inline-comments: [0]
- no-inner-declarations: [2]
- no-invalid-regexp: [2]
- no-invalid-this: [0]
- no-irregular-whitespace: [2]
- no-iterator: [2]
- no-label-var: [2]
- no-labels: [2]
- no-lone-blocks: [2]
- no-lonely-if: [0]
- no-loop-func: [0]
- no-loss-of-precision: [2]
- no-magic-numbers: [0]
- no-misleading-character-class: [2]
- no-mixed-operators: [0]
- no-mixed-spaces-and-tabs: [2]
- no-multi-assign: [0]
- no-multi-spaces: [2, {ignoreEOLComments: true, exceptions: {Property: true}}]
- no-multi-str: [2]
- no-negated-condition: [0]
- no-nested-ternary: [0]
- no-new-func: [2]
- no-new-object: [2]
- no-new-symbol: [2]
- no-new-wrappers: [2]
- no-new: [0]
- no-nonoctal-decimal-escape: [2]
- no-obj-calls: [2]
- no-octal-escape: [2]
- no-octal: [2]
- no-param-reassign: [0]
- no-plusplus: [0]
- no-promise-executor-return: [0]
- no-proto: [2]
- no-prototype-builtins: [2]
- no-redeclare: [2]
- no-regex-spaces: [2]
- no-restricted-exports: [0]
- no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, self, status, statusbar, stop, toolbar, top]
- no-restricted-imports: [0]
- no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement]
- no-return-assign: [0]
- no-return-await: [0]
- no-script-url: [2]
- no-self-assign: [2, {props: true}]
- no-self-compare: [2]
- no-sequences: [2]
- no-setter-return: [2]
- no-shadow-restricted-names: [2]
- no-shadow: [0]
- no-sparse-arrays: [2]
- no-tabs: [2]
- no-template-curly-in-string: [2]
- no-ternary: [0]
- no-this-before-super: [2]
- no-throw-literal: [2]
- no-trailing-spaces: [2]
- no-undef-init: [2]
- no-undef: [2, {typeof: true}]
- no-undefined: [0]
- no-underscore-dangle: [0]
- no-unexpected-multiline: [2]
- no-unmodified-loop-condition: [2]
- no-unneeded-ternary: [0]
- no-unreachable-loop: [2]
- no-unreachable: [2]
- no-unsafe-finally: [2]
- no-unsafe-negation: [2]
- no-unused-expressions: [2]
- no-unused-labels: [2]
- no-unused-private-class-members: [2]
- no-unused-vars: [2, {args: all, argsIgnorePattern: ^_, varsIgnorePattern: ^_, caughtErrorsIgnorePattern: ^_, destructuredArrayIgnorePattern: ^_, ignoreRestSiblings: false}]
- no-use-before-define: [2, nofunc]
- no-useless-backreference: [0]
- no-useless-call: [2]
- no-useless-catch: [2]
- no-useless-computed-key: [2]
- no-useless-concat: [2]
- no-useless-constructor: [2]
- no-useless-escape: [2]
- no-useless-rename: [2]
- no-useless-return: [2]
- no-var: [2]
- no-void: [2]
- no-warning-comments: [0]
- no-whitespace-before-property: [2]
- no-with: [2]
- nonblock-statement-body-position: [2]
- object-curly-newline: [0]
- object-curly-spacing: [2, never]
- object-shorthand: [2, always]
- one-var-declaration-per-line: [0]
- one-var: [0]
- operator-assignment: [2, always]
- operator-linebreak: [2, after]
- padded-blocks: [2, never]
- padding-line-between-statements: [0]
- prefer-arrow-callback: [2, {allowNamedFunctions: true, allowUnboundThis: true}]
- prefer-const: [2, {destructuring: all}]
- prefer-destructuring: [0]
- prefer-exponentiation-operator: [2]
- prefer-named-capture-group: [0]
- prefer-numeric-literals: [2]
- prefer-object-has-own: [0]
- prefer-object-spread: [0]
- prefer-promise-reject-errors: [2, {allowEmptyReject: false}]
- prefer-regex-literals: [2]
- prefer-rest-params: [2]
- prefer-spread: [2]
- prefer-template: [2]
- quote-props: [0]
- quotes: [2, single, {avoidEscape: true, allowTemplateLiterals: true}]
- radix: [2, as-needed]
- require-atomic-updates: [0]
- require-await: [0]
- require-unicode-regexp: [0]
- require-yield: [2]
- rest-spread-spacing: [2, never]
- semi-spacing: [2, {before: false, after: true}]
- semi-style: [2, last]
- semi: [2, always, {omitLastInOneLineBlock: true}]
- sort-imports: [0]
- sort-keys: [0]
- sort-vars: [0]
- space-before-blocks: [2, always]
- space-in-parens: [2, never]
- space-infix-ops: [2]
- space-unary-ops: [2]
- spaced-comment: [2, always]
- strict: [0]
- switch-colon-spacing: [2]
- symbol-description: [2]
- template-curly-spacing: [2, never]
- template-tag-spacing: [2, never]
- unicode-bom: [2, never]
- unicorn/better-regex: [0]
- unicorn/catch-error-name: [0]
- unicorn/consistent-destructuring: [2]
- unicorn/consistent-function-scoping: [2]
- unicorn/custom-error-definition: [0]
- unicorn/empty-brace-spaces: [2]
- unicorn/error-message: [0]
- unicorn/escape-case: [0]
- unicorn/expiring-todo-comments: [0]
- unicorn/explicit-length-check: [0]
- unicorn/filename-case: [0]
- unicorn/import-index: [0]
- unicorn/import-style: [0]
- unicorn/new-for-builtins: [2]
- unicorn/no-abusive-eslint-disable: [0]
- unicorn/no-array-for-each: [2]
- unicorn/no-array-instanceof: [0]
- unicorn/no-array-method-this-argument: [2]
- unicorn/no-array-push-push: [2]
- unicorn/no-await-expression-member: [0]
- unicorn/no-console-spaces: [0]
- unicorn/no-document-cookie: [2]
- unicorn/no-empty-file: [2]
- unicorn/no-fn-reference-in-iterator: [0]
- unicorn/no-for-loop: [0]
- unicorn/no-hex-escape: [0]
- unicorn/no-invalid-remove-event-listener: [2]
- unicorn/no-keyword-prefix: [0]
- unicorn/no-lonely-if: [2]
- unicorn/no-nested-ternary: [0]
- unicorn/no-new-array: [0]
- unicorn/no-new-buffer: [0]
- unicorn/no-null: [0]
- unicorn/no-object-as-default-parameter: [2]
- unicorn/no-process-exit: [0]
- unicorn/no-reduce: [2]
- unicorn/no-static-only-class: [2]
- unicorn/no-thenable: [2]
- unicorn/no-this-assignment: [2]
- unicorn/no-unreadable-array-destructuring: [0]
- unicorn/no-unsafe-regex: [0]
- unicorn/no-unused-properties: [2]
- unicorn/no-useless-fallback-in-spread: [2]
- unicorn/no-useless-length-check: [2]
- unicorn/no-useless-promise-resolve-reject: [2]
- unicorn/no-useless-spread: [2]
- unicorn/no-useless-undefined: [0]
- unicorn/no-zero-fractions: [2]
- unicorn/number-literal-case: [0]
- unicorn/numeric-separators-style: [0]
- unicorn/prefer-add-event-listener: [2]
- unicorn/prefer-array-find: [2]
- unicorn/prefer-array-flat-map: [2]
- unicorn/prefer-array-flat: [2]
- unicorn/prefer-array-index-of: [2]
- unicorn/prefer-array-some: [2]
- unicorn/prefer-at: [0]
- unicorn/prefer-code-point: [2]
- unicorn/prefer-dataset: [2]
- unicorn/prefer-date-now: [2]
- unicorn/prefer-default-parameters: [0]
- unicorn/prefer-event-key: [2]
- unicorn/prefer-export-from: [2]
- unicorn/prefer-includes: [2]
- unicorn/prefer-json-parse-buffer: [0]
- unicorn/prefer-math-trunc: [2]
- unicorn/prefer-modern-dom-apis: [0]
- unicorn/prefer-module: [2]
- unicorn/prefer-negative-index: [2]
- unicorn/prefer-node-append: [0]
- unicorn/prefer-node-protocol: [0]
- unicorn/prefer-node-remove: [0]
- unicorn/prefer-number-properties: [0]
- unicorn/prefer-object-from-entries: [2]
- unicorn/prefer-object-has-own: [0]
- unicorn/prefer-optional-catch-binding: [2]
- unicorn/prefer-prototype-methods: [0]
- unicorn/prefer-query-selector: [0]
- unicorn/prefer-reflect-apply: [0]
- unicorn/prefer-regexp-test: [2]
- unicorn/prefer-replace-all: [0]
- unicorn/prefer-set-has: [0]
- unicorn/prefer-spread: [0]
- unicorn/prefer-starts-ends-with: [2]
- unicorn/prefer-string-slice: [0]
- unicorn/prefer-switch: [0]
- unicorn/prefer-ternary: [0]
- unicorn/prefer-text-content: [2]
- unicorn/prefer-top-level-await: [0]
- unicorn/prefer-trim-start-end: [2]
- unicorn/prefer-type-error: [0]
- unicorn/prevent-abbreviations: [0]
- unicorn/relative-url-style: [2]
- unicorn/require-array-join-separator: [2]
- unicorn/require-number-to-fixed-digits-argument: [2]
- unicorn/require-post-message-target-origin: [0]
- unicorn/string-content: [0]
- unicorn/template-indent: [2]
- unicorn/text-encoding-identifier-case: [0]
- unicorn/throw-new-error: [2]
- use-isnan: [2]
- valid-typeof: [2, {requireStringLiterals: true}]
- vars-on-top: [0]
- vue/attributes-order: [0]
- vue/component-definition-name-casing: [0]
- vue/html-closing-bracket-spacing: [0]
- vue/max-attributes-per-line: [0]
- vue/one-component-per-file: [0]
- wrap-iife: [2, inside]
- wrap-regex: [0]
- yield-star-spacing: [2, after]
- yoda: [2, never]
diff --git a/.eslintrc.yaml b/.eslintrc.yaml
new file mode 100644
index 0000000000000..32e7ea70c858c
--- /dev/null
+++ b/.eslintrc.yaml
@@ -0,0 +1,536 @@
+root: true
+reportUnusedDisableDirectives: true
+
+ignorePatterns:
+ - /web_src/js/vendor
+
+parserOptions:
+ sourceType: module
+ ecmaVersion: latest
+
+plugins:
+ - eslint-plugin-unicorn
+ - eslint-plugin-import
+ - eslint-plugin-jquery
+ - eslint-plugin-sonarjs
+
+env:
+ es2022: true
+ node: true
+
+globals:
+ __webpack_public_path__: true
+
+overrides:
+ - files: ["web_src/**/*.js", "docs/**/*.js"]
+ env:
+ browser: true
+ node: false
+ - files: ["web_src/**/*worker.js"]
+ env:
+ worker: true
+ rules:
+ no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top]
+ - files: ["build/generate-images.js"]
+ rules:
+ import/no-unresolved: [0]
+ import/no-extraneous-dependencies: [0]
+ - files: ["*.config.js"]
+ rules:
+ import/no-unused-modules: [0]
+
+rules:
+ accessor-pairs: [2]
+ array-bracket-newline: [0]
+ array-bracket-spacing: [2, never]
+ array-callback-return: [2, {checkForEach: true}]
+ array-element-newline: [0]
+ arrow-body-style: [0]
+ arrow-parens: [2, always]
+ arrow-spacing: [2, {before: true, after: true}]
+ block-scoped-var: [2]
+ brace-style: [2, 1tbs, {allowSingleLine: true}]
+ camelcase: [0]
+ capitalized-comments: [0]
+ class-methods-use-this: [0]
+ comma-dangle: [2, only-multiline]
+ comma-spacing: [2, {before: false, after: true}]
+ comma-style: [2, last]
+ complexity: [0]
+ computed-property-spacing: [2, never]
+ consistent-return: [0]
+ consistent-this: [0]
+ constructor-super: [2]
+ curly: [0]
+ default-case-last: [2]
+ default-case: [0]
+ default-param-last: [0]
+ dot-location: [2, property]
+ dot-notation: [0]
+ eol-last: [2]
+ eqeqeq: [2]
+ for-direction: [2]
+ func-call-spacing: [2, never]
+ func-name-matching: [2]
+ func-names: [0]
+ func-style: [0]
+ function-call-argument-newline: [0]
+ function-paren-newline: [0]
+ generator-star-spacing: [0]
+ getter-return: [2]
+ grouped-accessor-pairs: [2]
+ guard-for-in: [0]
+ id-blacklist: [0]
+ id-length: [0]
+ id-match: [0]
+ implicit-arrow-linebreak: [0]
+ import/default: [0]
+ import/dynamic-import-chunkname: [0]
+ import/export: [2]
+ import/exports-last: [0]
+ import/extensions: [2, always, {ignorePackages: true}]
+ import/first: [2]
+ import/group-exports: [0]
+ import/max-dependencies: [0]
+ import/named: [2]
+ import/namespace: [0]
+ import/newline-after-import: [0]
+ import/no-absolute-path: [0]
+ import/no-amd: [0]
+ import/no-anonymous-default-export: [0]
+ import/no-commonjs: [0]
+ import/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}]
+ import/no-default-export: [0]
+ import/no-deprecated: [0]
+ import/no-dynamic-require: [0]
+ import/no-extraneous-dependencies: [2]
+ import/no-import-module-exports: [0]
+ import/no-internal-modules: [0]
+ import/no-mutable-exports: [0]
+ import/no-named-as-default-member: [0]
+ import/no-named-as-default: [2]
+ import/no-named-default: [0]
+ import/no-named-export: [0]
+ import/no-namespace: [0]
+ import/no-nodejs-modules: [0]
+ import/no-relative-packages: [0]
+ import/no-relative-parent-imports: [0]
+ import/no-restricted-paths: [0]
+ import/no-self-import: [2]
+ import/no-unassigned-import: [0]
+ import/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}]
+ import/no-unused-modules: [2, {unusedExports: true}]
+ import/no-useless-path-segments: [2, {commonjs: true}]
+ import/no-webpack-loader-syntax: [2]
+ import/order: [0]
+ import/prefer-default-export: [0]
+ import/unambiguous: [0]
+ indent: [2, 2, {SwitchCase: 1}]
+ init-declarations: [0]
+ jquery/no-ajax-events: [2]
+ jquery/no-ajax: [0]
+ jquery/no-animate: [2]
+ jquery/no-attr: [0]
+ jquery/no-bind: [2]
+ jquery/no-class: [0]
+ jquery/no-clone: [2]
+ jquery/no-closest: [0]
+ jquery/no-css: [0]
+ jquery/no-data: [0]
+ jquery/no-deferred: [2]
+ jquery/no-delegate: [2]
+ jquery/no-each: [0]
+ jquery/no-extend: [2]
+ jquery/no-fade: [0]
+ jquery/no-filter: [0]
+ jquery/no-find: [0]
+ jquery/no-global-eval: [2]
+ jquery/no-grep: [2]
+ jquery/no-has: [2]
+ jquery/no-hide: [0]
+ jquery/no-html: [0]
+ jquery/no-in-array: [2]
+ jquery/no-is-array: [2]
+ jquery/no-is-function: [2]
+ jquery/no-is: [0]
+ jquery/no-load: [2]
+ jquery/no-map: [0]
+ jquery/no-merge: [2]
+ jquery/no-param: [2]
+ jquery/no-parent: [0]
+ jquery/no-parents: [0]
+ jquery/no-parse-html: [2]
+ jquery/no-prop: [0]
+ jquery/no-proxy: [2]
+ jquery/no-ready: [0]
+ jquery/no-serialize: [2]
+ jquery/no-show: [0]
+ jquery/no-size: [2]
+ jquery/no-sizzle: [0]
+ jquery/no-slide: [0]
+ jquery/no-submit: [0]
+ jquery/no-text: [0]
+ jquery/no-toggle: [0]
+ jquery/no-trigger: [0]
+ jquery/no-trim: [2]
+ jquery/no-val: [0]
+ jquery/no-when: [2]
+ jquery/no-wrap: [2]
+ key-spacing: [2]
+ keyword-spacing: [2]
+ line-comment-position: [0]
+ linebreak-style: [2, unix]
+ lines-around-comment: [0]
+ lines-between-class-members: [0]
+ logical-assignment-operators: [0]
+ max-classes-per-file: [0]
+ max-depth: [0]
+ max-len: [0]
+ max-lines-per-function: [0]
+ max-lines: [0]
+ max-nested-callbacks: [0]
+ max-params: [0]
+ max-statements-per-line: [0]
+ max-statements: [0]
+ multiline-comment-style: [2, separate-lines]
+ multiline-ternary: [0]
+ new-cap: [0]
+ new-parens: [2]
+ newline-per-chained-call: [0]
+ no-alert: [0]
+ no-array-constructor: [2]
+ no-async-promise-executor: [0]
+ no-await-in-loop: [0]
+ no-bitwise: [0]
+ no-buffer-constructor: [0]
+ no-caller: [2]
+ no-case-declarations: [2]
+ no-class-assign: [2]
+ no-compare-neg-zero: [2]
+ no-cond-assign: [2, except-parens]
+ no-confusing-arrow: [0]
+ no-console: [1, {allow: [debug, info, warn, error]}]
+ no-const-assign: [2]
+ no-constant-binary-expression: [2]
+ no-constant-condition: [0]
+ no-constructor-return: [2]
+ no-continue: [0]
+ no-control-regex: [0]
+ no-debugger: [1]
+ no-delete-var: [2]
+ no-div-regex: [0]
+ no-dupe-args: [2]
+ no-dupe-class-members: [2]
+ no-dupe-else-if: [2]
+ no-dupe-keys: [2]
+ no-duplicate-case: [2]
+ no-duplicate-imports: [2]
+ no-else-return: [2]
+ no-empty-character-class: [2]
+ no-empty-function: [0]
+ no-empty-pattern: [2]
+ no-empty-static-block: [2]
+ no-empty: [2, {allowEmptyCatch: true}]
+ no-eq-null: [2]
+ no-eval: [2]
+ no-ex-assign: [2]
+ no-extend-native: [2]
+ no-extra-bind: [2]
+ no-extra-boolean-cast: [2]
+ no-extra-label: [0]
+ no-extra-parens: [0]
+ no-extra-semi: [2]
+ no-fallthrough: [2]
+ no-floating-decimal: [0]
+ no-func-assign: [2]
+ no-global-assign: [2]
+ no-implicit-coercion: [2]
+ no-implicit-globals: [0]
+ no-implied-eval: [2]
+ no-import-assign: [2]
+ no-inline-comments: [0]
+ no-inner-declarations: [2]
+ no-invalid-regexp: [2]
+ no-invalid-this: [0]
+ no-irregular-whitespace: [2]
+ no-iterator: [2]
+ no-label-var: [2]
+ no-labels: [0] # handled by no-restricted-syntax
+ no-lone-blocks: [2]
+ no-lonely-if: [0]
+ no-loop-func: [0]
+ no-loss-of-precision: [2]
+ no-magic-numbers: [0]
+ no-misleading-character-class: [2]
+ no-mixed-operators: [0]
+ no-mixed-spaces-and-tabs: [2]
+ no-multi-assign: [0]
+ no-multi-spaces: [2, {ignoreEOLComments: true, exceptions: {Property: true}}]
+ no-multi-str: [2]
+ no-negated-condition: [0]
+ no-nested-ternary: [0]
+ no-new-func: [2]
+ no-new-native-nonconstructor: [2]
+ no-new-object: [2]
+ no-new-symbol: [2]
+ no-new-wrappers: [2]
+ no-new: [0]
+ no-nonoctal-decimal-escape: [2]
+ no-obj-calls: [2]
+ no-octal-escape: [2]
+ no-octal: [2]
+ no-param-reassign: [0]
+ no-plusplus: [0]
+ no-promise-executor-return: [0]
+ no-proto: [2]
+ no-prototype-builtins: [2]
+ no-redeclare: [2]
+ no-regex-spaces: [2]
+ no-restricted-exports: [0]
+ no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, self, status, statusbar, stop, toolbar, top, __dirname, __filename]
+ no-restricted-imports: [0]
+ no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement]
+ no-return-assign: [0]
+ no-return-await: [0]
+ no-script-url: [2]
+ no-self-assign: [2, {props: true}]
+ no-self-compare: [2]
+ no-sequences: [2]
+ no-setter-return: [2]
+ no-shadow-restricted-names: [2]
+ no-shadow: [0]
+ no-sparse-arrays: [2]
+ no-tabs: [2]
+ no-template-curly-in-string: [2]
+ no-ternary: [0]
+ no-this-before-super: [2]
+ no-throw-literal: [2]
+ no-trailing-spaces: [2]
+ no-undef-init: [2]
+ no-undef: [2, {typeof: true}]
+ no-undefined: [0]
+ no-underscore-dangle: [0]
+ no-unexpected-multiline: [2]
+ no-unmodified-loop-condition: [2]
+ no-unneeded-ternary: [0]
+ no-unreachable-loop: [2]
+ no-unreachable: [2]
+ no-unsafe-finally: [2]
+ no-unsafe-negation: [2]
+ no-unused-expressions: [2]
+ no-unused-labels: [2]
+ no-unused-private-class-members: [2]
+ no-unused-vars: [2, {args: all, argsIgnorePattern: ^_, varsIgnorePattern: ^_, caughtErrorsIgnorePattern: ^_, destructuredArrayIgnorePattern: ^_, ignoreRestSiblings: false}]
+ no-use-before-define: [2, {functions: false, classes: true, variables: true, allowNamedExports: true}]
+ no-useless-backreference: [2]
+ no-useless-call: [2]
+ no-useless-catch: [2]
+ no-useless-computed-key: [2]
+ no-useless-concat: [2]
+ no-useless-constructor: [2]
+ no-useless-escape: [2]
+ no-useless-rename: [2]
+ no-useless-return: [2]
+ no-var: [2]
+ no-void: [2]
+ no-warning-comments: [0]
+ no-whitespace-before-property: [2]
+ no-with: [0] # handled by no-restricted-syntax
+ nonblock-statement-body-position: [2]
+ object-curly-newline: [0]
+ object-curly-spacing: [2, never]
+ object-shorthand: [2, always]
+ one-var-declaration-per-line: [0]
+ one-var: [0]
+ operator-assignment: [2, always]
+ operator-linebreak: [2, after]
+ padded-blocks: [2, never]
+ padding-line-between-statements: [0]
+ prefer-arrow-callback: [2, {allowNamedFunctions: true, allowUnboundThis: true}]
+ prefer-const: [2, {destructuring: all, ignoreReadBeforeAssign: true}]
+ prefer-destructuring: [0]
+ prefer-exponentiation-operator: [2]
+ prefer-named-capture-group: [0]
+ prefer-numeric-literals: [2]
+ prefer-object-has-own: [0]
+ prefer-object-spread: [2]
+ prefer-promise-reject-errors: [2, {allowEmptyReject: false}]
+ prefer-regex-literals: [2]
+ prefer-rest-params: [2]
+ prefer-spread: [2]
+ prefer-template: [2]
+ quote-props: [0]
+ quotes: [2, single, {avoidEscape: true, allowTemplateLiterals: true}]
+ radix: [2, as-needed]
+ require-atomic-updates: [0]
+ require-await: [0]
+ require-unicode-regexp: [0]
+ require-yield: [2]
+ rest-spread-spacing: [2, never]
+ semi-spacing: [2, {before: false, after: true}]
+ semi-style: [2, last]
+ semi: [2, always, {omitLastInOneLineBlock: true}]
+ sonarjs/cognitive-complexity: [0]
+ sonarjs/elseif-without-else: [0]
+ sonarjs/max-switch-cases: [0]
+ sonarjs/no-all-duplicated-branches: [2]
+ sonarjs/no-collapsible-if: [0]
+ sonarjs/no-collection-size-mischeck: [2]
+ sonarjs/no-duplicate-string: [0]
+ sonarjs/no-duplicated-branches: [0]
+ sonarjs/no-element-overwrite: [2]
+ sonarjs/no-empty-collection: [2]
+ sonarjs/no-extra-arguments: [2]
+ sonarjs/no-gratuitous-expressions: [2]
+ sonarjs/no-identical-conditions: [2]
+ sonarjs/no-identical-expressions: [2]
+ sonarjs/no-identical-functions: [2, 5]
+ sonarjs/no-ignored-return: [2]
+ sonarjs/no-inverted-boolean-check: [2]
+ sonarjs/no-nested-switch: [0]
+ sonarjs/no-nested-template-literals: [0]
+ sonarjs/no-one-iteration-loop: [2]
+ sonarjs/no-redundant-boolean: [2]
+ sonarjs/no-redundant-jump: [0]
+ sonarjs/no-same-line-conditional: [2]
+ sonarjs/no-small-switch: [0]
+ sonarjs/no-unused-collection: [2]
+ sonarjs/no-use-of-empty-return-value: [2]
+ sonarjs/no-useless-catch: [2]
+ sonarjs/non-existent-operator: [2]
+ sonarjs/prefer-immediate-return: [0]
+ sonarjs/prefer-object-literal: [0]
+ sonarjs/prefer-single-boolean-return: [0]
+ sonarjs/prefer-while: [2]
+ sort-imports: [0]
+ sort-keys: [0]
+ sort-vars: [0]
+ space-before-blocks: [2, always]
+ space-in-parens: [2, never]
+ space-infix-ops: [2]
+ space-unary-ops: [2]
+ spaced-comment: [2, always]
+ strict: [0]
+ switch-colon-spacing: [2]
+ symbol-description: [2]
+ template-curly-spacing: [2, never]
+ template-tag-spacing: [2, never]
+ unicode-bom: [2, never]
+ unicorn/better-regex: [0]
+ unicorn/catch-error-name: [0]
+ unicorn/consistent-destructuring: [2]
+ unicorn/consistent-function-scoping: [2]
+ unicorn/custom-error-definition: [0]
+ unicorn/empty-brace-spaces: [2]
+ unicorn/error-message: [0]
+ unicorn/escape-case: [0]
+ unicorn/expiring-todo-comments: [0]
+ unicorn/explicit-length-check: [0]
+ unicorn/filename-case: [0]
+ unicorn/import-index: [0]
+ unicorn/import-style: [0]
+ unicorn/new-for-builtins: [2]
+ unicorn/no-abusive-eslint-disable: [0]
+ unicorn/no-array-for-each: [2]
+ unicorn/no-array-instanceof: [0]
+ unicorn/no-array-method-this-argument: [2]
+ unicorn/no-array-push-push: [2]
+ unicorn/no-await-expression-member: [0]
+ unicorn/no-console-spaces: [0]
+ unicorn/no-document-cookie: [2]
+ unicorn/no-empty-file: [2]
+ unicorn/no-fn-reference-in-iterator: [0]
+ unicorn/no-for-loop: [0]
+ unicorn/no-hex-escape: [0]
+ unicorn/no-invalid-remove-event-listener: [2]
+ unicorn/no-keyword-prefix: [0]
+ unicorn/no-lonely-if: [2]
+ unicorn/no-negated-condition: [0]
+ unicorn/no-nested-ternary: [0]
+ unicorn/no-new-array: [0]
+ unicorn/no-new-buffer: [0]
+ unicorn/no-null: [0]
+ unicorn/no-object-as-default-parameter: [0]
+ unicorn/no-process-exit: [0]
+ unicorn/no-reduce: [2]
+ unicorn/no-static-only-class: [2]
+ unicorn/no-thenable: [2]
+ unicorn/no-this-assignment: [2]
+ unicorn/no-typeof-undefined: [2]
+ unicorn/no-unnecessary-await: [2]
+ unicorn/no-unreadable-array-destructuring: [0]
+ unicorn/no-unreadable-iife: [2]
+ unicorn/no-unsafe-regex: [0]
+ unicorn/no-unused-properties: [2]
+ unicorn/no-useless-fallback-in-spread: [2]
+ unicorn/no-useless-length-check: [2]
+ unicorn/no-useless-promise-resolve-reject: [2]
+ unicorn/no-useless-spread: [2]
+ unicorn/no-useless-switch-case: [2]
+ unicorn/no-useless-undefined: [0]
+ unicorn/no-zero-fractions: [2]
+ unicorn/number-literal-case: [0]
+ unicorn/numeric-separators-style: [0]
+ unicorn/prefer-add-event-listener: [2]
+ unicorn/prefer-array-find: [2]
+ unicorn/prefer-array-flat-map: [2]
+ unicorn/prefer-array-flat: [2]
+ unicorn/prefer-array-index-of: [2]
+ unicorn/prefer-array-some: [2]
+ unicorn/prefer-at: [0]
+ unicorn/prefer-code-point: [0]
+ unicorn/prefer-dataset: [2]
+ unicorn/prefer-date-now: [2]
+ unicorn/prefer-default-parameters: [0]
+ unicorn/prefer-event-key: [2]
+ unicorn/prefer-event-target: [2]
+ unicorn/prefer-export-from: [2]
+ unicorn/prefer-includes: [2]
+ unicorn/prefer-json-parse-buffer: [0]
+ unicorn/prefer-logical-operator-over-ternary: [2]
+ unicorn/prefer-math-trunc: [2]
+ unicorn/prefer-modern-dom-apis: [0]
+ unicorn/prefer-modern-math-apis: [2]
+ unicorn/prefer-module: [2]
+ unicorn/prefer-native-coercion-functions: [2]
+ unicorn/prefer-negative-index: [2]
+ unicorn/prefer-node-append: [0]
+ unicorn/prefer-node-protocol: [2]
+ unicorn/prefer-node-remove: [0]
+ unicorn/prefer-number-properties: [0]
+ unicorn/prefer-object-from-entries: [2]
+ unicorn/prefer-object-has-own: [0]
+ unicorn/prefer-optional-catch-binding: [2]
+ unicorn/prefer-prototype-methods: [0]
+ unicorn/prefer-query-selector: [0]
+ unicorn/prefer-reflect-apply: [0]
+ unicorn/prefer-regexp-test: [2]
+ unicorn/prefer-replace-all: [0]
+ unicorn/prefer-set-has: [0]
+ unicorn/prefer-set-size: [2]
+ unicorn/prefer-spread: [0]
+ unicorn/prefer-starts-ends-with: [2]
+ unicorn/prefer-string-slice: [0]
+ unicorn/prefer-switch: [0]
+ unicorn/prefer-ternary: [0]
+ unicorn/prefer-text-content: [2]
+ unicorn/prefer-top-level-await: [0]
+ unicorn/prefer-trim-start-end: [2]
+ unicorn/prefer-type-error: [0]
+ unicorn/prevent-abbreviations: [0]
+ unicorn/relative-url-style: [2]
+ unicorn/require-array-join-separator: [2]
+ unicorn/require-number-to-fixed-digits-argument: [2]
+ unicorn/require-post-message-target-origin: [0]
+ unicorn/string-content: [0]
+ unicorn/switch-case-braces: [0]
+ unicorn/template-indent: [2]
+ unicorn/text-encoding-identifier-case: [0]
+ unicorn/throw-new-error: [2]
+ use-isnan: [2]
+ valid-typeof: [2, {requireStringLiterals: true}]
+ vars-on-top: [0]
+ wrap-iife: [2, inside]
+ wrap-regex: [0]
+ yield-star-spacing: [2, after]
+ yoda: [2, never]
diff --git a/.gitattributes b/.gitattributes
index 12c45dbc6a078..34482b9e8cba7 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,7 +1,6 @@
* text=auto eol=lf
*.tmpl linguist-language=Handlebars
-/.eslintrc linguist-language=YAML
-/.stylelintrc linguist-language=YAML
+/assets/*.json linguist-generated
/public/vendor/** -text -eol linguist-vendored
/vendor/** -text -eol linguist-vendored
/web_src/fomantic/build/** linguist-generated
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index e79cc9d4328d5..9bb5bb8e886a9 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,10 +1,10 @@
-blank_issues_enabled: true
+blank_issues_enabled: false
contact_links:
- name: Security Concern
url: https://tinyurl.com/security-gitea
about: For security concerns, please send a mail to security@gitea.io instead of opening a public issue.
- name: Discord Server
- url: https://discord.gg/gitea
+ url: https://discord.gg/Gitea
about: Please ask questions and discuss configuration or deployment problems here.
- name: Discourse Forum
url: https://discourse.gitea.io
diff --git a/.gitignore b/.gitignore
index eab92b49ad189..1ce2a87611e81 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,21 +63,14 @@ cpu.out
/indexers
/log
/public/img/avatar
-/integrations/gitea-integration-mysql
-/integrations/gitea-integration-mysql8
-/integrations/gitea-integration-pgsql
-/integrations/gitea-integration-sqlite
-/integrations/gitea-integration-mssql
-/integrations/indexers-mysql
-/integrations/indexers-mysql8
-/integrations/indexers-pgsql
-/integrations/indexers-sqlite
-/integrations/indexers-mssql
-/integrations/sqlite.ini
-/integrations/mysql.ini
-/integrations/mysql8.ini
-/integrations/pgsql.ini
-/integrations/mssql.ini
+/tests/integration/gitea-integration-*
+/tests/integration/indexers-*
+/tests/e2e/gitea-e2e-*
+/tests/e2e/indexers-*
+/tests/e2e/reports
+/tests/e2e/test-artifacts
+/tests/e2e/test-snapshots
+/tests/*.ini
/node_modules
/yarn.lock
/yarn-error.log
@@ -102,6 +95,7 @@ cpu.out
!/web_src/fomantic/build/themes/default/assets/fonts/outline-icons.woff2
/VERSION
/.air
+/.go-licenses
# Snapcraft
snap/.snapcraft/
diff --git a/.gitpod.yml b/.gitpod.yml
new file mode 100644
index 0000000000000..0b6ad1f30ec7a
--- /dev/null
+++ b/.gitpod.yml
@@ -0,0 +1,42 @@
+tasks:
+ - name: Setup
+ init: |
+ cp -r contrib/ide/vscode .vscode
+ make deps
+ make build
+ command: |
+ gp sync-done setup
+ exit 0
+ - name: Run frontend
+ command: |
+ gp sync-await setup
+ make watch-frontend
+ - name: Run backend
+ command: |
+ gp sync-await setup
+ mkdir -p custom/conf/
+ echo -e "[server]\nROOT_URL=$(gp url 3000)/" > custom/conf/app.ini
+ echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini
+ export TAGS="sqlite sqlite_unlock_notify"
+ make watch-backend
+ - name: Run docs
+ before: sudo bash -c "$(grep 'https://github.com/gohugoio/hugo/releases/download' Makefile | tr -d '\')" # install hugo
+ command: cd docs && make clean update && hugo server -D -F --baseUrl $(gp url 1313) --liveReloadPort=443 --appendPort=false --bind=0.0.0.0
+
+vscode:
+ extensions:
+ - editorconfig.editorconfig
+ - dbaeumer.vscode-eslint
+ - golang.go
+ - stylelint.vscode-stylelint
+ - DavidAnson.vscode-markdownlint
+ - johnsoncodehk.volar
+ - ms-azuretools.vscode-docker
+ - zixuanchen.vitest-explorer
+ - alexcvzz.vscode-sqlite
+
+ports:
+ - name: Gitea
+ port: 3000
+ - name: Docs
+ port: 1313
diff --git a/.golangci.yml b/.golangci.yml
index 8e31d0cbc4d6b..7635e83a37260 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,30 +1,34 @@
linters:
enable:
- - gosimple
- - deadcode
- - typecheck
- - govet
- - errcheck
- - staticcheck
- - unused
- - structcheck
- - varcheck
+ - bidichk
+ # - deadcode # deprecated - https://github.com/golangci/golangci-lint/issues/1841
+ - depguard
- dupl
- #- gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time.
- - gofmt
- - misspell
+ - errcheck
- gocritic
- - bidichk
+ # - gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time.
+ - gofmt
+ - gofumpt
+ - gosimple
+ - govet
- ineffassign
+ - nakedret
+ - nolintlint
- revive
- - gofumpt
- - depguard
+ - staticcheck
+ # - structcheck # deprecated - https://github.com/golangci/golangci-lint/issues/1841
+ - stylecheck
+ - typecheck
+ - unconvert
+ - unused
+ # - varcheck # deprecated - https://github.com/golangci/golangci-lint/issues/1841
+ # - wastedassign # disabled - https://github.com/golangci/golangci-lint/issues/2649
enable-all: false
disable-all: true
fast: false
run:
- go: 1.18
+ go: 1.19
timeout: 10m
skip-dirs:
- node_modules
@@ -32,6 +36,10 @@ run:
- web_src
linters-settings:
+ stylecheck:
+ checks: ["all", "-ST1005", "-ST1003"]
+ nakedret:
+ max-func-lines: 0
gocritic:
disabled-checks:
- ifElseChain
@@ -66,17 +74,20 @@ linters-settings:
- name: modifies-value-receiver
gofumpt:
extra-rules: true
- lang-version: "1.18"
+ lang-version: "1.19"
depguard:
- # TODO: use depguard to replace import checks in gitea-vet
list-type: denylist
# Check the list against standard lib.
include-go-root: true
packages-with-error-message:
- encoding/json: "use gitea's modules/json instead of encoding/json"
- github.com/unknwon/com: "use gitea's util and replacements"
+ - io/ioutil: "use os or io instead"
+ - golang.org/x/exp: "it's experimental and unreliable."
issues:
+ max-issues-per-linter: 0
+ max-same-issues: 0
exclude-rules:
# Exclude some linters from running on tests files.
- path: _test\.go
@@ -137,9 +148,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
@@ -162,6 +170,7 @@ issues:
- path: models/user/openid.go
linters:
- golint
- - linters:
- - staticcheck
- text: "strings.Title is deprecated: The rule Title uses for word boundaries does not handle Unicode punctuation properly. Use golang.org/x/text/cases instead."
+ - path: models/user/badge.go
+ linters:
+ - revive
+ text: "exported: type name will be used as user.UserBadge by other packages, and that stutters; consider calling this Badge"
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
new file mode 100644
index 0000000000000..7ccdd53e89793
--- /dev/null
+++ b/.markdownlint.yaml
@@ -0,0 +1,18 @@
+commands-show-output: false
+fenced-code-language: false
+first-line-h1: false
+header-increment: false
+line-length: {code_blocks: false, tables: false, stern: true, line_length: -1}
+no-alt-text: false
+no-bare-urls: false
+no-blanks-blockquote: false
+no-duplicate-header: {allow_different_nesting: true}
+no-emphasis-as-header: false
+no-empty-links: false
+no-hard-tabs: {code_blocks: false}
+no-inline-html: false
+no-space-in-code: false
+no-space-in-emphasis: false
+no-trailing-punctuation: false
+no-trailing-spaces: {br_spaces: 0}
+single-h1: false
diff --git a/.spectral.yaml b/.spectral.yaml
new file mode 100644
index 0000000000000..e547eea57d899
--- /dev/null
+++ b/.spectral.yaml
@@ -0,0 +1,12 @@
+extends: [[spectral:oas, all]]
+
+rules:
+ info-contact: off
+ oas2-api-host: off
+ oas2-parameter-description: off
+ oas2-schema: off
+ oas2-valid-schema-example: off
+ openapi-tags: off
+ operation-description: off
+ operation-singular-tag: off
+ operation-tag-defined: off
diff --git a/.stylelintrc b/.stylelintrc
deleted file mode 100644
index 9bad55d371e5c..0000000000000
--- a/.stylelintrc
+++ /dev/null
@@ -1,32 +0,0 @@
-extends: stylelint-config-standard
-
-overrides:
- - files: ["**/*.less"]
- customSyntax: postcss-less
-
-rules:
- alpha-value-notation: null
- at-rule-empty-line-before: null
- block-closing-brace-empty-line-before: null
- color-function-notation: null
- color-hex-length: null
- comment-empty-line-before: null
- declaration-block-no-redundant-longhand-properties: null
- declaration-block-single-line-max-declarations: null
- declaration-empty-line-before: null
- function-no-unknown: null
- hue-degree-notation: null
- indentation: 2
- max-line-length: null
- no-descending-specificity: null
- no-invalid-position-at-import-rule: null
- number-leading-zero: never
- number-max-precision: null
- property-no-vendor-prefix: null
- rule-empty-line-before: null
- selector-class-pattern: null
- selector-id-pattern: null
- selector-pseudo-element-colon-notation: double
- shorthand-property-no-redundant-values: true
- string-quotes: null
- value-no-vendor-prefix: null
diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml
new file mode 100644
index 0000000000000..d51a08bf8e596
--- /dev/null
+++ b/.stylelintrc.yaml
@@ -0,0 +1,44 @@
+extends: stylelint-config-standard
+
+plugins:
+ - stylelint-declaration-strict-value
+
+overrides:
+ - files: ["**/*.less"]
+ customSyntax: postcss-less
+ - files: ["**/*.less"]
+ rules:
+ scale-unlimited/declaration-strict-value: [color, {
+ ignoreValues: /^(inherit|transparent|unset|initial)$/
+ }]
+ - files: ["**/chroma/*", "**/codemirror/*", "**/standalone/*", "**/console/*"]
+ rules:
+ scale-unlimited/declaration-strict-value: null
+
+rules:
+ alpha-value-notation: null
+ at-rule-empty-line-before: null
+ block-closing-brace-empty-line-before: null
+ color-function-notation: null
+ color-hex-length: null
+ comment-empty-line-before: null
+ declaration-block-no-redundant-longhand-properties: null
+ declaration-block-single-line-max-declarations: null
+ declaration-empty-line-before: null
+ function-no-unknown: null
+ hue-degree-notation: null
+ import-notation: string
+ indentation: 2
+ max-line-length: null
+ no-descending-specificity: null
+ no-invalid-position-at-import-rule: null
+ number-leading-zero: never
+ number-max-precision: null
+ property-no-vendor-prefix: null
+ rule-empty-line-before: null
+ selector-class-pattern: null
+ selector-id-pattern: null
+ selector-pseudo-element-colon-notation: double
+ shorthand-property-no-redundant-values: true
+ string-quotes: null
+ value-no-vendor-prefix: null
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cfbbc99b10268..1e31b0008ad74 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,824 @@ 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.18.1](https://github.com/go-gitea/gitea/releases/tag/v1.18.1) - 2023-01-17
+
+* API
+ * Add `sync_on_commit` option for push mirrors api (#22271) (#22292)
+* BUGFIXES
+ * Update `github.com/zeripath/zapx/v15` (#22485)
+ * Fix pull request API field `closed_at` always being `null` (#22482) (#22483)
+ * Fix container blob mount (#22226) (#22476)
+ * Fix error when calculating repository size (#22392) (#22474)
+ * Fix Operator does not exist bug on explore page with ONLY_SHOW_RELEVANT_REPOS (#22454) (#22472)
+ * Fix environments for KaTeX and error reporting (#22453) (#22473)
+ * Remove the netgo tag for Windows build (#22467) (#22468)
+ * Fix migration from GitBucket (#22477) (#22465)
+ * Prevent panic on looking at api "git" endpoints for empty repos (#22457) (#22458)
+ * Fix PR status layout on mobile (#21547) (#22441)
+ * Fix wechatwork webhook sends empty content in PR review (#21762) (#22440)
+ * Remove duplicate "Actions" label in mobile view (#21974) (#22439)
+ * Fix leaving organization bug on user settings -> orgs (#21983) (#22438)
+ * Fixed colour transparency regex matching in project board sorting (#22092) (#22437)
+ * Correctly handle select on multiple channels in Queues (#22146) (#22428)
+ * Prepend refs/heads/ to issue template refs (#20461) (#22427)
+ * Restore function to "Show more" buttons (#22399) (#22426)
+ * Continue GCing other repos on error in one repo (#22422) (#22425)
+ * Allow HOST has no port (#22280) (#22409)
+ * Fix omit avatar_url in discord payload when empty (#22393) (#22394)
+ * Don't display stop watch top bar icon when disabled and hidden when click other place (#22374) (#22387)
+ * Don't lookup mail server when using sendmail (#22300) (#22383)
+ * Fix gravatar disable bug (#22337)
+ * Fix update settings table on install (#22326) (#22327)
+ * Fix sitemap (#22272) (#22320)
+ * Fix code search title translation (#22285) (#22316)
+ * Fix due date rendering the wrong date in issue (#22302) (#22306)
+ * Fix get system setting bug when enabled redis cache (#22298)
+ * Fix bug of DisableGravatar default value (#22297)
+ * Fix key signature error page (#22229) (#22230)
+* TESTING
+ * Remove test session cache to reduce possible concurrent problem (#22199) (#22429)
+* MISC
+ * Restore previous official review when an official review is deleted (#22449) (#22460)
+ * Log STDERR of external renderer when it fails (#22442) (#22444)
+
+## [1.18.0](https://github.com/go-gitea/gitea/releases/tag/v1.18.0) - 2022-12-29
+
+* SECURITY
+ * Remove ReverseProxy authentication from the API (#22219) (#22251)
+ * Support Go Vulnerability Management (#21139)
+ * Forbid HTML string tooltips (#20935)
+* BREAKING
+ * Rework mailer settings (#18982)
+ * Remove U2F support (#20141)
+ * Refactor `i18n` to `locale` (#20153)
+ * Enable contenthash in filename for dynamic assets (#20813)
+* FEATURES
+ * Add color previews in markdown (#21474)
+ * Allow package version sorting (#21453)
+ * Add support for Chocolatey/NuGet v2 API (#21393)
+ * Add API endpoint to get changed files of a PR (#21177)
+ * Add filetree on left of diff view (#21012)
+ * Support Issue forms and PR forms (#20987)
+ * Add support for Vagrant packages (#20930)
+ * Add support for `npm unpublish` (#20688)
+ * Add badge capabilities to users (#20607)
+ * Add issue filter for Author (#20578)
+ * Add KaTeX rendering to Markdown. (#20571)
+ * Add support for Pub packages (#20560)
+ * Support localized README (#20508)
+ * Add support mCaptcha as captcha provider (#20458)
+ * Add team member invite by email (#20307)
+ * Added email notification option to receive all own messages (#20179)
+ * Switch Unicode Escaping to a VSCode-like system (#19990)
+ * Add user/organization code search (#19977)
+ * Only show relevant repositories on explore page (#19361)
+ * User keypairs and HTTP signatures for ActivityPub federation using go-ap (#19133)
+ * Add sitemap support (#18407)
+ * Allow creation of OAuth2 applications for orgs (#18084)
+ * Add system setting table with cache and also add cache supports for user setting (#18058)
+ * Add pages to view watched repos and subscribed issues/PRs (#17156)
+ * Support Proxy protocol (#12527)
+ * Implement sync push mirror on commit (#19411)
+* API
+ * Allow empty assignees on pull request edit (#22150) (#22214)
+ * Make external issue tracker regexp configurable via API (#21338)
+ * Add name field for org api (#21270)
+ * Show teams with no members if user is admin (#21204)
+ * Add latest commit's SHA to content response (#20398)
+ * Add allow_rebase_update, default_delete_branch_after_merge to repository api response (#20079)
+ * Add new endpoints for push mirrors management (#19841)
+* ENHANCEMENTS
+ * Add setting to disable the git apply step in test patch (#22130) (#22170)
+ * Multiple improvements for comment edit diff (#21990) (#22007)
+ * Fix button in branch list, avoid unexpected page jump before restore branch actually done (#21562) (#21928)
+ * Fix flex layout for repo list icons (#21896) (#21920)
+ * Fix vertical align of committer avatar rendered by email address (#21884) (#21918)
+ * Fix setting HTTP headers after write (#21833) (#21877)
+ * Color and Style enhancements (#21784, #21799) (#21868)
+ * Ignore line anchor links with leading zeroes (#21728) (#21776)
+ * Quick fixes monaco-editor error: "vs.editor.nullLanguage" (#21734) (#21738)
+ * Use CSS color-scheme instead of invert (#21616) (#21623)
+ * Respect user's locale when rendering the date range in the repo activity page (#21410)
+ * Change `commits-table` column width (#21564)
+ * Refactor git command arguments and make all arguments to be safe to be used (#21535)
+ * CSS color enhancements (#21534)
+ * Add link to user profile in markdown mention only if user exists (#21533, #21554)
+ * Add option to skip index dirs (#21501)
+ * Diff file tree tweaks (#21446)
+ * Localize all timestamps (#21440)
+ * Add `code` highlighting in issue titles (#21432)
+ * Use Name instead of DisplayName in LFS Lock (#21415)
+ * Consolidate more CSS colors into variables (#21402)
+ * Redirect to new repository owner (#21398)
+ * Use ISO date format instead of hard-coded English date format for date range in repo activity page (#21396)
+ * Use weighted algorithm for string matching when finding files in repo (#21370)
+ * Show private data in feeds (#21369)
+ * Refactor parseTreeEntries, speed up tree list (#21368)
+ * Add GET and DELETE endpoints for Docker blob uploads (#21367)
+ * Add nicer error handling on template compile errors (#21350)
+ * Add `stat` to `ToCommit` function for speed (#21337)
+ * Support instance-wide OAuth2 applications (#21335)
+ * Record OAuth client type at registration (#21316)
+ * Add new CSS variables --color-accent and --color-small-accent (#21305)
+ * Improve error descriptions for unauthorized_client (#21292)
+ * Case-insensitive "find files in repo" (#21269)
+ * Consolidate more CSS rules, fix inline code on arc-green (#21260)
+ * Log real ip of requests from ssh (#21216)
+ * Save files in local storage as group readable (#21198)
+ * Enable fluid page layout on medium size viewports (#21178)
+ * File header tweaks (#21175)
+ * Added missing headers on user packages page (#21172)
+ * Display image digest for container packages (#21170)
+ * Skip dirty check for team forms (#21154)
+ * Keep path when creating a new branch (#21153)
+ * Remove fomantic image module (#21145)
+ * Make labels clickable in the comments section. (#21137)
+ * Sort branches and tags by date descending (#21136)
+ * Better repo API unit checks (#21130)
+ * Improve commit status icons (#21124)
+ * Limit length of repo description and repo url input fields (#21119)
+ * Show .editorconfig errors in frontend (#21088)
+ * Allow poster to choose reviewers (#21084)
+ * Remove black labels and CSS cleanup (#21003)
+ * Make e-mail sanity check more precise (#20991)
+ * Use native inputs in whitespace dropdown (#20980)
+ * Enhance package date display (#20928)
+ * Display total blob size of a package version (#20927)
+ * Show language name on hover (#20923)
+ * Show instructions for all generic package files (#20917)
+ * Refactor AssertExistsAndLoadBean to use generics (#20797)
+ * Move the official website link at the footer of gitea (#20777)
+ * Add support for full name in reverse proxy auth (#20776)
+ * Remove useless JS operation for relative time tooltips (#20756)
+ * Replace some icons with SVG (#20741)
+ * Change commit status icons to SVG (#20736)
+ * Improve single repo action for issue and pull requests (#20730)
+ * Allow multiple files in generic packages (#20661)
+ * Add option to create new issue from /issues page (#20650)
+ * Background color of private list-items updated (#20630)
+ * Added search input field to issue filter (#20623)
+ * Increase default item listing size `ISSUE_PAGING_NUM` to 20 (#20547)
+ * Modify milestone search keywords to be case insensitive again (#20513)
+ * Show hint to link package to repo when viewing empty repo package list (#20504)
+ * Add Tar ZSTD support (#20493)
+ * Make code review checkboxes clickable (#20481)
+ * Add "X-Gitea-Object-Type" header for GET `/raw/` & `/media/` API (#20438)
+ * Display project in issue list (#20434)
+ * Prepend commit message to template content when opening a new PR (#20429)
+ * Replace fomantic popup module with tippy.js (#20428)
+ * Allow to specify colors for text in markup (#20363)
+ * Allow access to the Public Organization Member lists with minimal permissions (#20330)
+ * Use default values when provided values are empty (#20318)
+ * Vertical align navbar avatar at middle (#20302)
+ * Delete cancel button in repo creation page (#21381)
+ * Include login_name in adminCreateUser response (#20283)
+ * fix: icon margin in user/settings/repos (#20281)
+ * Remove blue text on migrate page (#20273)
+ * Modify milestone search keywords to be case insensitive (#20266)
+ * Move some files into models' sub packages (#20262)
+ * Add tooltip to repo icons in explore page (#20241)
+ * Remove deprecated licenses (#20222)
+ * Webhook for Wiki changes (#20219)
+ * Share HTML template renderers and create a watcher framework (#20218)
+ * Allow enable LDAP source and disable user sync via CLI (#20206)
+ * Adds a checkbox to select all issues/PRs (#20177)
+ * Refactor `i18n` to `locale` (#20153)
+ * Disable status checks in template if none found (#20088)
+ * Allow manager logging to set SQL (#20064)
+ * Add order by for assignee no sort issue (#20053)
+ * Take a stab at porting existing components to Vue3 (#20044)
+ * Add doctor command to write commit-graphs (#20007)
+ * Add support for authentication based on reverse proxy email (#19949)
+ * Enable spellcheck for EasyMDE, use contenteditable mode (#19776)
+ * Allow specifying SECRET_KEY_URI, similar to INTERNAL_TOKEN_URI (#19663)
+ * Rework mailer settings (#18982)
+ * Add option to purge users (#18064)
+ * Add author search input (#21246)
+ * Make rss/atom identifier globally unique (#21550)
+* BUGFIXES
+ * Auth interface return error when verify failure (#22119) (#22259)
+ * Use complete SHA to create and query commit status (#22244) (#22257)
+ * Update bleve and zapx to fix unaligned atomic (#22031) (#22218)
+ * Prevent panic in doctor command when running default checks (#21791) (#21807)
+ * Load GitRepo in API before deleting issue (#21720) (#21796)
+ * Ignore line anchor links with leading zeroes (#21728) (#21776)
+ * Set last login when activating account (#21731) (#21755)
+ * Fix UI language switching bug (#21597) (#21749)
+ * Quick fixes monaco-editor error: "vs.editor.nullLanguage" (#21734) (#21738)
+ * Allow local package identifiers for PyPI packages (#21690) (#21727)
+ * Deal with markdown template without metadata (#21639) (#21654)
+ * Fix opaque background on mermaid diagrams (#21642) (#21652)
+ * Fix repository adoption on Windows (#21646) (#21650)
+ * Sync git hooks when config file path changed (#21619) (#21626)
+ * Fix 500 on PR files API (#21602) (#21607)
+ * Fix `Timestamp.IsZero` (#21593) (#21603)
+ * Fix viewing user subscriptions (#21482)
+ * Fix mermaid-related bugs (#21431)
+ * Fix branch dropdown shifting on page load (#21428)
+ * Fix default theme-auto selector when nologin (#21346)
+ * Fix and improve incorrect error messages (#21342)
+ * Fix formatted link for PR review notifications to matrix (#21319)
+ * Center-aligning content of WebAuthN page (#21127)
+ * Remove follow from commits by file (#20765)
+ * Fix commit status popup (#20737)
+ * Fix init mail render logic (#20704)
+ * Use correct page size for link header pagination (#20546)
+ * Preserve unix socket file (#20499)
+ * Use tippy.js for context popup (#20393)
+ * Add missing parameter for error in log message (#20144)
+ * Do not allow organisation owners add themselves as collaborator (#20043)
+ * Rework file highlight rendering and fix yaml copy-paste (#19967)
+ * Improve code diff highlight, fix incorrect rendered diff result (#19958)
+* TESTING
+ * Improve OAuth integration tests (#21390)
+ * Add playwright tests (#20123)
+* BUILD
+ * Switch to building with go1.19 (#20695)
+ * Update JS dependencies, adjust eslint (#20659)
+ * Add more linters to improve code readability (#19989)
+
+## [1.17.4](https://github.com/go-gitea/gitea/releases/tag/v1.17.4) - 2022-12-21
+
+* SECURITY
+ * Do not allow Ghost access to limited visible user/org (#21849) (#21875)
+ * Fix package access for admins and inactive users (#21580) (#21592)
+* ENHANCEMENTS
+ * Fix button in branch list, avoid unexpected page jump before restore branch actually done (#21562) (#21927)
+ * Fix vertical align of committer avatar rendered by email address (#21884) (#21919)
+ * Fix setting HTTP headers after write (#21833) (#21874)
+ * Ignore line anchor links with leading zeroes (#21728) (#21777)
+ * Enable Monaco automaticLayout (#21516)
+* BUGFIXES
+ * Do not list active repositories as unadopted (#22034) (#22167)
+ * Correctly handle moved files in apply patch (#22118) (#22136)
+ * Fix condition for is_internal (#22095) (#22131)
+ * Fix permission check on issue/pull lock (#22114)
+ * Fix sorting admin user list by last login (#22081) (#22106)
+ * Workaround for container registry push/pull errors (#21862) (#22069)
+ * Fix issue/PR numbers (#22037) (#22045)
+ * Handle empty author names (#21902) (#22028)
+ * Fix ListBranches to handle empty case (#21921) (#22025)
+ * Fix enabling partial clones on 1.17 (#21809)
+ * Prevent panic in doctor command when running default checks (#21791) (#21808)
+ * Upgrade golang.org/x/crypto (#21792) (#21794)
+ * Init git module before database migration (#21764) (#21766)
+ * Set last login when activating account (#21731) (#21754)
+ * Add HEAD fix to gitea doctor (#21352) (#21751)
+ * Fix UI language switching bug (#21597) (#21748)
+ * Remove semver compatible flag and change pypi to an array of test cases (#21708) (#21729)
+ * Allow local package identifiers for PyPI packages (#21690) (#21726)
+ * Fix repository adoption on Windows (#21646) (#21651)
+ * Sync git hooks when config file path changed (#21619) (#21625)
+ * Added check for disabled Packages (#21540) (#21614)
+ * Fix `Timestamp.IsZero` (#21593) (#21604)
+ * Fix issues count bug (#21600)
+ * Support binary deploy in npm packages (#21589)
+ * Update milestone counters when issue is deleted (#21459) (#21586)
+ * SessionUser protection against nil pointer dereference (#21581)
+ * Case-insensitive NuGet symbol file GUID (#21409) (#21575)
+ * Suppress `ExternalLoginUserNotExist` error (#21504) (#21572)
+ * Prevent Authorization header for presigned LFS urls (#21531) (#21569)
+ * Update binding to fix bugs (#21560)
+ * Fix generating compare link (#21519) (#21530)
+ * Ignore error when retrieving changed PR review files (#21487) (#21524)
+ * Fix incorrect notification commit url (#21479) (#21483)
+ * Display total commit count in hook message (#21400) (#21481)
+ * Enforce grouped NuGet search results (#21442) (#21480)
+ * Return 404 when user is not found on avatar (#21476) (#21477)
+ * Normalize NuGet package version on upload (#22186) (#22201)
+* MISC
+ * Check for zero time instant in TimeStamp.IsZero() (#22171) (#22173)
+ * Fix warn in database structs sync (#22111)
+ * Allow for resolution of NPM registry paths that match upstream (#21568) (#21723)
+
+## [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
+ * Correctly escape within tribute.js (#20831) (#20832)
+* ENHANCEMENTS
+ * Add support for NuGet API keys (#20721) (#20734)
+ * Display project in issue list (#20583)
+ * Add disable download source configuration (#20548) (#20579)
+ * Add username check to doctor (#20140) (#20671)
+ * Enable Wire 2 for Internal SSH Server (#20616) (#20617)
+* BUGFIXES
+ * Use the total issue count for UI (#20785) (#20827)
+ * Add proxy host into allow list (#20798) (#20819)
+ * Add missing translation for queue flush workers (#20791) (#20792)
+ * Improve comment header for mobile (#20781) (#20789)
+ * Fix git.Init for doctor sub-command (#20782) (#20783)
+ * Check webhooks slice length before calling xorm (#20642) (#20768)
+ * Remove manual rollback for failed generated repositories (#20639) (#20762)
+ * Use correct field name in npm template (#20675) (#20760)
+ * Keep download count on Container tag overwrite (#20728) (#20735)
+ * Fix v220 migration to be compatible for MSSQL 2008 r2 (#20702) (#20707)
+ * Use request timeout for git service rpc (#20689) (#20693)
+ * Send correct NuGet status codes (#20647) (#20677)
+ * Use correct context to get package content (#20673) (#20676)
+ * Fix the JS error "EventSource is not defined" caused by some non-standard browsers (#20584) (#20663)
+ * Add default commit messages to PR for squash merge (#20618) (#20645)
+ * Fix package upload for files >32mb (#20622) (#20635)
+ * Fix the new-line copy-paste for rendered code (#20612)
+ * Clean up and fix clone button script (#20415 & #20600) (#20599)
+ * Fix default merge style (#20564) (#20565)
+ * Add repository condition for issue count (#20454) (#20496)
+ * Make branch icon stand out more (#20726) (#20774)
+ * Fix loading button with invalid form (#20754) (#20759)
+ * Fix SecToTime edge-cases (#20610) (#20611)
+ * Executable check always returns true for windows (#20637) (#20835)
+ * Check issue labels slice length before calling xorm Insert (#20655) (#20836)
+ * Fix owners cannot create organization repos bug (#20841) (#20854)
+ * Prevent 500 is head repo does not have PullRequest unit in IsUserAllowedToUpdate (#20839) (#20848)
+
+## [1.17.0](https://github.com/go-gitea/gitea/releases/tag/v1.17.0) - 2022-07-30
+
+* BREAKING
+ * Require go1.18 for Gitea 1.17 (#19918)
+ * Make AppDataPath absolute against the AppWorkPath if it is not (#19815)
+ * Nuke the incorrect permission report on /api/v1/notifications (#19761)
+ * Refactor git module, make Gitea use internal git config (#19732)
+ * Remove `RequireHighlightJS` field, update plantuml example. (#19615)
+ * Increase minimal required git version to 2.0 (#19577)
+ * Add a directory prefix `gitea-src-VERSION` to release-tar-file (#19396)
+ * Use "main" as default branch name (#19354)
+ * Make cron task no notice on success (#19221)
+ * Add pam account authorization check (#19040)
+ * Show messages for users if the ROOT_URL is wrong, show JavaScript errors (#18971)
+ * Refactor mirror code & fix StartToMirror (#18904)
+ * Remove deprecated SSH ciphers from default (#18697)
+ * Add the possibility to allow the user to have a favicon which differs from the main logo (#18542)
+ * Update reserved usernames list (#18438)
+ * Support custom ACME provider (#18340)
+ * Change initial TrustModel to committer (#18335)
+ * Update HTTP status codes (#18063)
+ * Upgrade Alpine from 3.13 to 3.15 (#18050)
+ * Restrict email address validation (#17688)
+ * Refactor Router Logger (#17308)
+* SECURITY
+ * Use git.HOME_PATH for Git HOME directory (#20114) (#20293)
+ * Add write check for creating Commit Statuses (#20332) (#20333)
+ * Remove deprecated SSH ciphers from default (#18697)
+* FEDERATION
+ * Return statistic information for nodeinfo (#19561)
+ * Add Webfinger endpoint (#19462)
+ * Store the foreign ID of issues during migration (#18446)
+* FEATURES
+ * Automatically render wiki TOC (#19873)
+ * Adding button to link accounts from user settings (#19792)
+ * Allow set default merge style while creating repo (#19751)
+ * Auto merge pull requests when all checks succeeded (#9307 & #19648)
+ * Improve reviewing PR UX (#19612)
+ * Add support for rendering console output with colors (#19497)
+ * Add Helm Chart registry (#19406)
+ * Add Goroutine stack inspector to admin/monitor (#19207)
+ * RSS/Atom support for Orgs & Repos (#17714 & #19055)
+ * Add button for issue deletion (#19032)
+ * Allow to mark files in a PR as viewed (#19007)
+ * Add Index to comment for migrations and mirroring (#18806)
+ * Add health check endpoint (#18465)
+ * Add packagist webhook (#18224)
+ * Add "Allow edits from maintainer" feature (#18002)
+ * Add apply-patch, basic revert and cherry-pick functionality (#17902)
+ * Add Package Registry (#16510)
+ * Add LDAP group sync to Teams (#16299)
+ * Pause queues (#15928)
+ * Added auto-save whitespace behavior if it changed manually (#15566)
+ * Find files in repo (#15028)
+ * Provide configuration to allow camo-media proxying (#12802)
+* API
+ * Add endpoint to serve blob or LFS file content (#19689)
+ * Add endpoint to check if team has repo access (#19540)
+ * More commit info (#19252)
+ * Allow to create file on empty repo (#19224)
+ * Allow removing issues (#18879)
+ * Add endpoint to query collaborators permission for a repository (#18761)
+ * Return primary language and repository language stats API URL (#18396)
+ * Implement http signatures support for the API (#17565)
+* ENHANCEMENTS
+ * Make notification bell more prominent on mobile (#20108, #20236, #20251) (#20269)
+ * Adjust max-widths for the repository file table (#20243) (#20247)
+ * Display full name (#20171) (#20246)
+ * Add dbconsistency checks for Stopwatches (#20010)
+ * Add fetch.writeCommitGraph to gitconfig (#20006)
+ * Add fgprof pprof profiler (#20005)
+ * Move agit dependency (#19998)
+ * Empty log queue on flush and close (#19994)
+ * Remove tab/TabName usage where it's not needed (#19973)
+ * Improve file header on mobile (#19945)
+ * Move issues related files into models/issues (#19931)
+ * Add breaking email restrictions checker in doctor (#19903)
+ * Improve UX on modal for deleting an access token (#19894)
+ * Add alt text to logo (#19892)
+ * Move some code into models/git (#19879)
+ * Remove customized (unmaintained) dropdown, improve aria a11y for dropdown (#19861)
+ * Make user profile image show full image on mobile (#19840)
+ * Replace blue button and label classes with primary (#19763)
+ * Remove fomantic progress module (#19760)
+ * Allows repo search to match against "owner/repo" pattern strings (#19754)
+ * Move org functions (#19753)
+ * Move almost all functions' parameter db.Engine to context.Context (#19748)
+ * Show source/target branches on PR's list (#19747)
+ * Use http.StatusTemporaryRedirect(307) when serve avatar directly (#19739)
+ * Add doctor orphan check for orphaned pull requests without an existing base repo (#19731)
+ * Make Ctrl+Enter (quick submit) work for issue comment and wiki editor (#19729)
+ * Update go-chi/cache to utilize Ping() (#19719)
+ * Improve commit list/view on mobile (#19712)
+ * Move some repository related code into sub package (#19711)
+ * Use a better OlderThan for DeleteInactiveUsers (#19693)
+ * Introduce eslint-plugin-jquery (#19690)
+ * Tidy up `
` template (#19678)
+ * Calculate filename hash only once (#19654)
+ * Simplify `IsVendor` (#19626)
+ * Add "Reference" section to Issue view sidebar (#19609)
+ * Only set CanColorStdout / CanColorStderr to true if the stdout/stderr is a terminal (#19581)
+ * Use for a repo action one database transaction (#19576)
+ * Simplify loops to copy (#19569)
+ * Added X-Mailer header to outgoing emails (#19562)
+ * use middleware to open gitRepo (#19559)
+ * Mute link in diff header (#19556)
+ * Improve UI on mobile (#19546)
+ * Fix Pull Request comment filename word breaks (#19535)
+ * Permalink files In PR diff (#19534)
+ * PullService lock via pullID (#19520)
+ * Make repository file list useable on mobile (#19515)
+ * more context for models (#19511)
+ * Refactor readme file renderer (#19502)
+ * By default force vertical tabs on mobile (#19486)
+ * Github style following followers (#19482)
+ * Improve action table indices (#19472)
+ * Use horizontal tabs for repo header on mobile (#19468)
+ * pass gitRepo down since its used for main repo and wiki (#19461)
+ * Admin should not delete himself (#19423)
+ * Use queue instead of memory queue in webhook send service (#19390)
+ * Simplify the code to get issue count (#19380)
+ * Add commit status popup to issuelist (#19375)
+ * Add RSS Feed buttons to Repo, User and Org pages (#19370)
+ * Add logic to switch between source/rendered on Markdown (#19356)
+ * Move some helper files out of models (#19355)
+ * Move access and repo permission to models/perm/access (#19350)
+ * Disallow selecting the text of buttons (#19330)
+ * Allow custom redirect for landing page (#19324)
+ * Remove dependent on session auth for api/v1 routers (#19321)
+ * Never use /api/v1 from Gitea UI Pages (#19318)
+ * Remove legacy unmaintained packages, refactor to support change default locale (#19308)
+ * Move milestone to models/issues/ (#19278)
+ * Configure OpenSSH log level via Environment in Docker (#19274)
+ * Move reaction to models/issues/ (#19264)
+ * Make git.OpenRepository accept Context (#19260)
+ * Move some issue methods as functions (#19255)
+ * Show last cron messages on monitor page (#19223)
+ * New cron task: delete old system notices (#19219)
+ * Add Redis Sentinel Authentication Support (#19213)
+ * Add auto logging of goroutine pid label (#19212)
+ * Set OpenGraph title to DisplayName in profile pages (#19206)
+ * Add pprof labels in processes and for lifecycles (#19202)
+ * Let web and API routes have different auth methods group (#19168)
+ * Move init repository related functions to modules (#19159)
+ * Feeds: render markdown to html (#19058)
+ * Allow users to self-request a PR review (#19030)
+ * Allow render HTML with css/js external links (#19017)
+ * Fix script compatiable with OpenWrt (#19000)
+ * Support ignore all santize for external renderer (#18984)
+ * Add note to GPG key response if user has no keys (#18961)
+ * Improve Stopwatch behavior (#18930)
+ * Improve mirror iterator (#18928)
+ * Uncapitalize errors (#18915)
+ * Prevent Stats Indexer reporting error if repo dir missing (#18870)
+ * Refactor SecToTime() function (#18863)
+ * Replace deprecated String.prototype.substr() with String.prototype.slice() (#18796)
+ * Move deletebeans into models/db (#18781)
+ * Fix display time of milestones (#18753)
+ * Add config option to disable "Update branch by rebase" (#18745)
+ * Display template path of current page in dev mode (#18717)
+ * Add number in queue status to monitor page (#18712)
+ * Change git.cmd to RunWithContext (#18693)
+ * Refactor i18n, use Locale to provide i18n/translation related functions (#18648)
+ * Delete old git.NewCommand() and use it as git.NewCommandContext() (#18552)
+ * Move organization related structs into sub package (#18518)
+ * Warn at startup if the provided `SCRIPT_TYPE` is not on the PATH (#18467)
+ * Use `CryptoRandomBytes` instead of `CryptoRandomString` (#18439)
+ * Use explicit jQuery import, remove unused eslint globals (#18435)
+ * Allow to filter repositories by language in explore, user and organization repositories lists (#18430)
+ * Use base32 for 2FA scratch token (#18384)
+ * Unexport var git.GlobalCommandArgs (#18376)
+ * Don't underline commit status icon on hover (#18372)
+ * Always use git command but not os.Command (#18363)
+ * Switch to non-deprecation setting (#18358)
+ * Set the LastModified header for raw files (#18356)
+ * Refactor jwt.StandardClaims to RegisteredClaims (#18344)
+ * Enable deprecation error for v1.17.0 (#18341)
+ * Refactor httplib (#18338)
+ * Limit max-height of CodeMirror editors for issue comment and wiki (#18271)
+ * Validate migration files (#18203)
+ * Format with gofumpt (#18184)
+ * Allow custom default merge message with .gitea/default_merge_message/_TEMPLATE.md (#18177)
+ * Prettify number of issues (#17760)
+ * Add a "admin user generate-access-token" subcommand (#17722)
+ * Custom regexp external issues (#17624)
+ * Add smtp password to install page (#17564)
+ * Add config options to hide issue events (#17414)
+ * Prevent double click new issue/pull/comment button (#16157)
+ * Show issue assignee on project board (#15232)
+* BUGFIXES
+ * WebAuthn CredentialID field needs to be increased in size (#20530) (#20555)
+ * Ensure that all unmerged files are merged when conflict checking (#20528) (#20536)
+ * Stop logging EOFs and exit(1)s in ssh handler (#20476) (#20529)
+ * Add labels to two buttons that were missing them (#20419) (#20524)
+ * Fix ROOT_URL detection for URLs without trailing slash (#20502) (#20503)
+ * Dismiss prior pull reviews if done via web in review dismiss (#20197) (#20407)
+ * Allow RSA 2047 bit keys (#20272) (#20396)
+ * Add missing return for when topic isn't found (#20351) (#20395)
+ * Fix commit status icon when in subdirectory (#20285) (#20385)
+ * Initialize cron last (#20373) (#20384)
+ * Set target on create release with existing tag (#20381) (#20382)
+ * Update xorm.io/xorm to fix a interpreting db column sizes issue on 32bit systems (#20371) (#20372)
+ * Make sure `repo_dir` is an empty directory or doesn't exist before 'dump-repo' (#20205) (#20370)
+ * Prevent context deadline error propagation in GetCommitsInfo (#20346) (#20361)
+ * Correctly handle draft releases without a tag (#20314) (#20335)
+ * Prevent "empty" scrollbars on Firefox (#20294) (#20308)
+ * Refactor SSH init code, fix directory creation for TrustedUserCAKeys file (#20299) (#20306)
+ * Bump goldmark to v1.4.13 (#20300) (#20301)
+ * Do not create empty ".ssh" directory when loading config (#20289) (#20298)
+ * Fix NPE when using non-numeric (#20277) (#20278)
+ * Store read access in access for team repositories (#20275) (#20276)
+ * EscapeFilter the group dn membership (#20200) (#20254)
+ * Only show Followers that current user can access (#20220) (#20252)
+ * Update Bluemonday to v1.0.19 (#20199) (#20209)
+ * Refix indices on actions table (#20158) (#20198)
+ * Check if project has the same repository id with issue when assign project to issue (#20133) (#20188)
+ * Fix remove file on initial comment (#20127) (#20128)
+ * Catch the error before the response is processed by goth (#20000) (#20102)
+ * Dashboard feed respect setting.UI.FeedPagingNum again (#20094) (#20099)
+ * Alter hook_task TEXT fields to LONGTEXT (#20038) (#20041)
+ * Respond with a 401 on git push when password isn't changed yet (#20026) (#20027)
+ * Return 404 when tag is broken (#20017) (#20024)
+ * Alter hook_task TEXT fields to LONGTEXT (#20038) (#20041)
+ * Respond with a 401 on git push when password isn't changed yet (#20026) (#20027)
+ * Return 404 when tag is broken (#20017) (#20024)
+ * Write Commit-Graphs in RepositoryDumper (#20004)
+ * Use DisplayName() instead of FullName in Oauth Provider (#19991)
+ * Don't buffer doctor logger (#19982)
+ * Always try to fetch repo for mirrors (#19975)
+ * Uppercase first languages letters (#19965)
+ * Fix cli command restore-repo: "units" should be parsed as StringSlice (#19953)
+ * Ensure minimum mirror interval is reported on settings page (#19895)
+ * Exclude Archived repos from Dashboard Milestones (#19882)
+ * gitconfig: set safe.directory = * (#19870)
+ * Prevent NPE on update mirror settings (#19864)
+ * Only return valid stopwatches to the EventSource (#19863)
+ * Prevent NPE whilst migrating if there is a team request review (#19855)
+ * Fix inconsistency in doctor output (#19836)
+ * Fix release tag for webhook (#19830)
+ * Add title attribute to dependencies in sidebar (#19807)
+ * Estimate Action Count in Statistics (#19775)
+ * Do not update user stars numbers unless fix is specified (#19750)
+ * Improved ref comment link when origin is body/title (#19741)
+ * Fix nodeinfo caching and prevent NPE if cache non-existent (#19721)
+ * Fix duplicate entry error when add team member (#19702)
+ * Fix sending empty notifications (#19589)
+ * Update image URL for Discord webhook (#19536)
+ * Don't let repo clone URL overflow (#19517)
+ * Allow commit status popup on /pulls page (#19507)
+ * Fix two UI bugs: JS error in imagediff.js, 500 error in diff/compare.tmpl (#19494)
+ * Fix logging of Transfer API (#19456)
+ * Fix panic in teams API when requesting members (#19360)
+ * Refactor CSRF protection modules, make sure CSRF tokens can be up-to-date. (#19337)
+ * An attempt to sync a non-mirror repo must give 400 (Bad Request) (#19300)
+ * Move checks for pulls before merge into own function (#19271)
+ * Fix `contrib/upgrade.sh` (#19222)
+ * Set the default branch for repositories generated from templates (#19136)
+ * Fix EasyMDE error when input Enter (#19004)
+ * Don't clean up hardcoded `tmp` (#18983)
+ * Delete related notifications on issue deletion too (#18953)
+ * Fix trace log to show value instead of pointers (#18926)
+ * Fix behavior or checkbox submission. (#18851)
+ * Add `ContextUser` (#18798)
+ * Fix some mirror bugs (#18649)
+ * Quote MAKE to prevent path expansion with space error (#18622)
+ * Preserve users if restoring a repository on the same Gitea instance (#18604)
+ * Fix non-ASCII search on database (#18437)
+ * Automatically pause queue if index service is unavailable (#15066)
+* TESTING
+ * Allow postgres integration tests to run over unix pipe (#19875)
+ * Prevent intermittent NPE in queue tests (#19301)
+ * Add test for importing pull requests in gitea uploader for migrations (#18752)
+ * Remove redundant comparison in repo dump/restore (#18660)
+ * More repo dump/restore tests, including pull requests (#18621)
+ * Add test coverage for original author conversion during migrations (#18506)
+* TRANSLATION
+ * Update issue_no_dependencies description (#19112)
+ * Refactor webhooks i18n (#18380)
+* BUILD
+ * Use alpine 3.16 (#19797)
+ * Require node 14.0 (#19451)
+* DOCS
+ * Update documents (git/fomantic/db, etc) (#19868)
+ * Update the ROOT documentation and error messages (#19832)
+ * Update document to use FHS `/usr/local/bin/gitea` instead of `/app/...` for Docker (#19794)
+ * Update documentation to disable duration settings with -1 instead of 0 (#19647)
+ * Add warning to set SENDMAIL_ARGS to -- (#19102)
+ * Update nginx reverse proxy docs (#18922)
+ * Add example to render html files (#18736)
+ * Make SSH passtrough documentation better (#18687)
+ * Changelog 1.16.0 & 1.15.11 (#18468 & #18455) (#18470)
+ * Update the SSH passthrough documentation (#18366)
+ * Add `contrib/upgrade.sh` (#18286)
+* MISC
+ * Fix aria for logo (#19955)
+ * In code search, get code unit accessible repos in one (main) query (#19764)
+ * Add tooltip to pending PR comments (#19662)
+ * Improve sync performance for pull-mirrors (#19125)
+ * Improve dashboard's repo list performance (#18963)
+ * Avoid database lookups for `DescriptionHTML` (#18924)
+ * Remove CodeMirror dependencies (#18911)
+ * Disable unnecessary mirroring elements (#18527)
+ * Disable unnecessary OpenID/OAuth2 elements (#18491)
+ * Disable unnecessary GitHooks elements (#18485)
+ * Change some logging levels (#18421)
+ * Prevent showing webauthn error for every time visiting `/user/settings/security` (#18385)
+ * Use correct translation key for errors (#18342)
+
+## [1.16.9](https://github.com/go-gitea/gitea/releases/tag/v1.16.9) - 2022-07-12
+
+* SECURITY
+ * Add write check for creating Commit status (#20332) (#20334)
+ * Check for permission when fetching user controlled issues (#20133) (#20196)
+* BUGFIXES
+ * Hide notify mail setting ui if not enabled (#20138) (#20337)
+ * Add write check for creating Commit status (#20332) (#20334)
+ * Only show Followers that current user can access (#20220) (#20253)
+ * Release page show all tags in compare dropdown (#20070) (#20071)
+ * Fix permission check for delete tag (#19985) (#20001)
+ * Only log non ErrNotExist errors in git.GetNote (#19884) (#19905)
+ * Use exact search instead of fuzzy search for branch filter dropdown (#19885) (#19893)
+ * Set Setpgid on child git processes (#19865) (#19881)
+ * Import git from alpine 3.16 repository as 2.30.4 is needed for `safe.directory = '*'` to work but alpine 3.13 has 2.30.3 (#19876)
+ * Ensure responses are context.ResponseWriters (#19843) (#19859)
+ * Fix incorrect usage of `Count` function (#19850)
+ * Fix raw endpoint PDF file headers (#19825) (#19826)
+ * Make WIP prefixes case insensitive, e.g. allow `Draft` as a WIP prefix (#19780) (#19811)
+ * Don't return 500 on NotificationUnreadCount (#19802)
+ * Prevent NPE when cache service is disabled (#19703) (#19783)
+ * Detect truncated utf-8 characters at the end of content as still representing utf-8 (#19773) (#19774)
+ * Fix doctor pq: syntax error at or near "." quote user table name (#19765) (#19770)
+ * Fix bug with assigneees (#19757)
+
+## [1.16.8](https://github.com/go-gitea/gitea/releases/tag/v1.16.8) - 2022-05-16
+
+* ENHANCEMENTS
+ * Add doctor check/fix for bogus action rows (#19656) (#19669)
+ * Make .cs highlighting legible on dark themes. (#19604) (#19605)
+* BUGFIXES
+ * Fix oauth setting list bug (#19681)
+ * Delete user related oauth stuff on user deletion too (#19677) (#19680)
+ * Fix new release from tags list UI (#19670) (#19673)
+ * Prevent NPE when checking repo units if the user is nil (#19625) (#19630)
+ * GetFeeds must always discard actions with dangling repo_id (#19598) (#19629)
+ * Call MultipartForm.RemoveAll when request finishes (#19606) (#19607)
+ * Avoid MoreThanOne error when creating a branch whose name conflicts with other ref names (#19557) (#19591)
+ * Fix sending empty notifications (#19589) (#19590)
+ * Ignore DNS error when doing migration allow/block check (#19566) (#19567)
+ * Fix issue overview for teams (#19652) (#19653)
+
+## [1.16.7](https://github.com/go-gitea/gitea/releases/tag/v1.16.7) - 2022-05-02
+
+* SECURITY
+ * Escape git fetch remote (#19487) (#19490)
+* BUGFIXES
+ * Don't overwrite err with nil (#19572) (#19574)
+ * On Migrations, only write commit-graph if wiki clone was successful (#19563) (#19568)
+ * Respect DefaultUserIsRestricted system default when creating new user (#19310) (#19560)
+ * Don't error when branch's commit doesn't exist (#19547) (#19548)
+ * Support `hostname:port` to pass host matcher's check (#19543) (#19544)
+ * Prevent intermittent race in attribute reader close (#19537) (#19539)
+ * Fix 64-bit atomic operations on 32-bit machines (#19531) (#19532)
+ * Prevent dangling archiver goroutine (#19516) (#19526)
+ * Fix migrate release from github (#19510) (#19523)
+ * When view _Siderbar or _Footer, just display once (#19501) (#19522)
+ * Fix blame page select range error and some typos (#19503)
+ * Fix name of doctor fix "authorized-keys" in hints (#19464) (#19484)
+ * User specific repoID or xorm builder conditions for issue search (#19475) (#19476)
+ * Prevent dangling cat-file calls (goroutine alternative) (#19454) (#19466)
+ * RepoAssignment ensure to close before overwrite (#19449) (#19460)
+ * Set correct PR status on 3way on conflict checking (#19457) (#19458)
+ * Mark TemplateLoading error as "UnprocessableEntity" (#19445) (#19446)
+
+## [1.16.6](https://github.com/go-gitea/gitea/releases/tag/v1.16.6) - 2022-04-20
+
+* ENHANCEMENTS
+ * Only request write when necessary (#18657) (#19422)
+ * Disable service worker by default (#18914) (#19342)
+* BUGFIXES
+ * When dumping trim the standard suffices instead of a random suffix (#19440) (#19447)
+ * Fix DELETE request for non-existent public key (#19443) (#19444)
+ * Don't panic on ErrEmailInvalid (#19441) (#19442)
+ * Add uploadpack.allowAnySHA1InWant to allow --filter=blob:none with older git clients (#19430) (#19438)
+ * Warn on SSH connection for incorrect configuration (#19317) (#19437)
+ * Search Issues via API, dont show 500 if filter result in empty list (#19244) (#19436)
+ * When updating mirror repo intervals by API reschedule next update too (#19429) (#19433)
+ * Fix nil error when some pages are rendered outside request context (#19427) (#19428)
+ * Fix double blob-hunk on diff page (#19404) (#19405)
+ * Don't allow merging PR's which are being conflict checked (#19357) (#19358)
+ * Fix middleware function's placements (#19377) (#19378)
+ * Fix invalid CSRF token bug, make sure CSRF tokens can be up-to-date (#19338)
+ * Restore user autoregistration with email addresses (#19261) (#19312)
+ * Move checks for pulls before merge into own function (#19271) (#19277)
+ * Granular webhook events in editHook (#19251) (#19257)
+ * Only send webhook events to active system webhooks and only deliver to active hooks (#19234) (#19248)
+ * Use full output of git show-ref --tags to get tags for PushUpdateAddTag (#19235) (#19236)
+ * Touch mirrors on even on fail to update (#19217) (#19233)
+ * Hide sensitive content on admin panel progress monitor (#19218 & #19226) (#19231)
+ * Fix clone url JS error for the empty repo page (#19209)
+ * Bump goldmark to v1.4.11 (#19201) (#19203)
+* TESTING
+ * Prevent intermittent failures in RepoIndexerTest (#19225 #19229) (#19228)
+* BUILD
+ * Revert the minimal golang version requirement from 1.17 to 1.16 and add a warning in Makefile (#19319)
+* MISC
+ * Performance improvement for add team user when org has more than 1000 repositories (#19227) (#19289)
+ * Check go and nodejs version by go.mod and package.json (#19197) (#19254)
+
## [1.16.5](https://github.com/go-gitea/gitea/releases/tag/v1.16.5) - 2022-03-23
* BREAKING
@@ -55,12 +873,12 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
* Don't show context cancelled errors in attribute reader (#19006) (#19027)
* Fix update hint bug (#18996) (#19002)
* MISC
- * Fix potential assignee query for repo (#18994) (#18999)
+ * Fix potential assignee query for repo (#18994) (#18999)
## [1.16.3](https://github.com/go-gitea/gitea/releases/tag/v1.16.3) - 2022-03-02
* SECURITY
- * Git backend ignore replace objects (#18979) (#18980)
+ * Git backend ignore replace objects (#18979) (#18980)
* ENHANCEMENTS
* Adjust error for already locked db and prevent level db lock on malformed connstr (#18923) (#18938)
* BUGFIXES
@@ -93,7 +911,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
* Immediately Hammer if second kill is sent (#18823) (#18826)
* Allow mermaid render error to wrap (#18791)
* BUGFIXES
- * Fix ldap user sync missed email in email_address table (#18786) (#18876)
+ * Fix ldap user sync missed email in email_address table (#18786) (#18876)
* Update assignees check to include any writing team and change org sidebar (#18680) (#18873)
* Don't report signal: killed errors in serviceRPC (#18850) (#18865)
* Fix bug where certain LDAP settings were reverted (#18859)
@@ -592,6 +1410,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
* Fix SVG side by side comparison link (#17375) (#17391)
## [1.15.4](https://github.com/go-gitea/gitea/releases/tag/v1.15.4) - 2021-10-08
+
* BUGFIXES
* Raw file API: don't try to interpret 40char filenames as commit SHA (#17185) (#17272)
* Don't allow merged PRs to be reopened (#17192) (#17271)
@@ -1238,7 +2057,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
* Add size to Save function (#15264) (#15270)
* Monaco improvements (#15333) (#15345)
* Support .mailmap in code activity stats (#15009)
- * Sort release attachments by name (#15008)
+ * Sort release attachments by name (#15008)
* Add ui.explore settings to control view of explore pages (#14094)
* Make internal SSH server host key path configurable (#14918)
* Hide resync all ssh principals when using internal ssh server (#14904)
@@ -1533,6 +2352,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
* Return original URL of Repositories (#13885) (#13886)
## [1.13.0](https://github.com/go-gitea/gitea/releases/tag/v1.13.0) - 2020-12-01
+
* SECURITY
* Add Allow-/Block-List for Migrate & Mirrors (#13610) (#13776)
* Prevent git operations for inactive users (#13527) (#13536)
@@ -2446,6 +3266,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
* Blacklist manifest.json & milestones user (#10292) (#10293)
## [1.11.0](https://github.com/go-gitea/gitea/releases/tag/v1.11.0) - 2020-02-10
+
* BREAKING
* Fix followers and following tabs in profile (#10202) (#10203)
* Make CertFile and KeyFile relative to CustomPath (#9868) (#9874)
@@ -2898,7 +3719,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.io).
This is a re-tag version of v1.10.5 and also explicitly built with Go 1.13.
-WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be used.
+WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should __not__ be used.
## [1.10.5](https://github.com/go-gitea/gitea/releases/tag/v1.10.5) - 2020-03-06
@@ -2919,6 +3740,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Ensure that 2fa is checked on reset-password (#9857) (#9877)
## [1.10.3](https://github.com/go-gitea/gitea/releases/tag/v1.10.3) - 2020-01-17
+
* SECURITY
* Hide credentials when submitting migration (#9102) (#9704)
* Never allow an empty password to validate (#9682) (#9684)
@@ -2937,6 +3759,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Branches not at ref commit ID should not be listed as Merged (#9614) (#9639)
## [1.10.2](https://github.com/go-gitea/gitea/releases/tag/v1.10.2) - 2020-01-02
+
* BUGFIXES
* Allow only specific Columns to be updated on Issue via API (#9539) (#9580)
* Add ErrReactionAlreadyExist error (#9550) (#9564)
@@ -2957,6 +3780,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix File Edit: Author/Committer interchanged (#9297) (#9300)
## [1.10.1](https://github.com/go-gitea/gitea/releases/tag/v1.10.1) - 2019-12-05
+
* BUGFIXES
* Fix max length check and limit in multiple repo forms (#9148) (#9204)
* Properly fix displaying virtual session provider in admin panel (#9137) (#9203)
@@ -2978,6 +3802,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Shadow password correctly for session config (#8984) (#9002)
## [1.10.0](https://github.com/go-gitea/gitea/releases/tag/v1.10.0) - 2019-11-13
+
* BREAKING
* Fix deadline on update issue or PR via API (#8698)
* Hide some user information via API if user doesn't have enough permission (#8655) (#8657)
@@ -3275,6 +4100,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix Statuses API only shows first 10 statuses: Add paging and extend API GetCommitStatuses (#7141)
## [1.9.6](https://github.com/go-gitea/gitea/releases/tag/v1.9.6) - 2019-11-13
+
* BUGFIXES
* Allow to merge if file path contains " or \ (#8629) (#8772)
* Fix 500 when edit hook (#8782) (#8790)
@@ -3283,6 +4109,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Add Close() method to gogitRepository (#8901) (#8958)
## [1.9.5](https://github.com/go-gitea/gitea/releases/tag/v1.9.5) - 2019-10-30
+
* BREAKING
* Hide some user information via API if user doesn't have enough permission (#8655) (#8658)
* BUGFIXES
@@ -3307,6 +4134,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Update heatmap fixtures to restore tests (#8615) (#8617)
## [1.9.4](https://github.com/go-gitea/gitea/releases/tag/v1.9.4) - 2019-10-08
+
* BUGFIXES
* Highlight issue references (#8101) (#8404)
* Fix bug when migrating a private repository #7917 (#8403)
@@ -3333,6 +4161,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Make show private icon when repo avatar set (#8144) (#8175)
## [1.9.3](https://github.com/go-gitea/gitea/releases/tag/v1.9.3) - 2019-09-06
+
* BUGFIXES
* Fix go get from a private repository with Go 1.13 (#8100)
* Strict name matching for Repository.GetTagID() (#8082)
@@ -3348,6 +4177,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Keep blame view buttons sequence consistent with normal view when viewing a file (#8007) (#8009)
## [1.9.2](https://github.com/go-gitea/gitea/releases/tag/v1.9.2) - 2019-08-22
+
* BUGFIXES
* Fix wrong sender when send slack webhook (#7918) (#7924)
* Upload support text/plain; charset=utf8 (#7899)
@@ -3362,6 +4192,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Drone/docker: prepare multi-arch release + provide arm64 image (#7571) (#7884)
## [1.9.1](https://github.com/go-gitea/gitea/releases/tag/v1.9.1) - 2019-08-14
+
* BREAKING
* Add pagination for admin api get orgs and fix only list public orgs bug (#7742) (#7752)
* SECURITY
@@ -3389,6 +4220,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Correct wrong datetime format for git (#7689) (#7690)
## [1.9.0](https://github.com/go-gitea/gitea/releases/tag/v1.9.0) - 2019-07-30
+
* BREAKING
* Better logging (#6038) (#6095)
* SECURITY
@@ -3745,6 +4577,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Added docker example for backup (#5846)
## [1.8.3](https://github.com/go-gitea/gitea/releases/tag/v1.8.3) - 2019-06-17
+
* BUGFIXES
* Always set userID on LFS authentication (#7224) (Part of #6993)
* Fix LFS Locks over SSH (#6999) (#7223)
@@ -3755,6 +4588,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix GCArgs load from ini (#7156) (#7157)
## [1.8.2](https://github.com/go-gitea/gitea/releases/tag/v1.8.2) - 2019-05-29
+
* BUGFIXES
* Fix possbile mysql invalid connnection error (#7051) (#7071)
* Handle invalid administrator username on install page (#7060) (#7063)
@@ -3770,6 +4604,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix wrong init dependency on markup extensions (#7038) (#7074)
## [1.8.1](https://github.com/go-gitea/gitea/releases/tag/v1.8.1) - 2019-05-08
+
* BUGFIXES
* Fix 404 when sending pull requests in some situations (#6871) (#6873)
* Enforce osusergo build tag for releases (#6862) (#6869)
@@ -3796,6 +4631,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix config ui error about cache ttl (#6861) (#6865)
## [1.8.0](https://github.com/go-gitea/gitea/releases/tag/v1.8.0) - 2019-04-20
+
* SECURITY
* Prevent remote code execution vulnerability with mirror repo URL settings (#6593) (#6594)
* Resolve 2FA bypass on API (#6676) (#6674)
@@ -4030,18 +4866,21 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Migrate database if app.ini found (#5290)
## [1.7.6](https://github.com/go-gitea/gitea/releases/tag/v1.7.6) - 2019-04-12
+
* SECURITY
* Prevent remote code execution vulnerability with mirror repo URL settings (#6593) (#6595)
* BUGFIXES
* Allow resend of confirmation email when logged in (#6482) (#6487)
## [1.7.5](https://github.com/go-gitea/gitea/releases/tag/v1.7.5) - 2019-03-27
+
* BUGFIXES
* Fix unitTypeCode not being used in accessLevelUnit (#6419) (#6423)
* Fix bug where manifest.json was being requested without cookies and continuously creating new sessions (#6372) (#6383)
* Fix ParsePatch function to work with quoted diff --git strings (#6323) (#6332)
## [1.7.4](https://github.com/go-gitea/gitea/releases/tag/v1.7.4) - 2019-03-12
+
* SECURITY
* Fix potential XSS vulnerability in repository description. (#6306) (#6308)
* BUGFIXES
@@ -4051,6 +4890,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix displaying dashboard even if required to change password (#6214) (#6215)
## [1.7.3](https://github.com/go-gitea/gitea/releases/tag/v1.7.3) - 2019-02-27
+
* BUGFIXES
* Fix server 500 when trying to migrate to an already existing repository (#6188) (#6197)
* Load Issue attributes for API /repos/{owner}/{repo}/issues/{index} (#6122) (#6185)
@@ -4065,6 +4905,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Recover panic in orgmode.Render if bad orgfile (#4982) (#5903) (#6097)
## [1.7.2](https://github.com/go-gitea/gitea/releases/tag/v1.7.2) - 2019-02-14
+
* BUGFIXES
* Remove all CommitStatus when a repo is deleted (#5940) (#5941)
* Fix notifications on pushing with deploy keys by setting hook environment variables (#5935) (#5944)
@@ -4081,6 +4922,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* In basic auth check for tokens before call UserSignIn (#5725) (#6083)
## [1.7.1](https://github.com/go-gitea/gitea/releases/tag/v1.7.1) - 2019-01-31
+
* SECURITY
* Disable redirect for i18n (#5910) (#5916)
* Only allow local login if password is non-empty (#5906) (#5908)
@@ -4102,6 +4944,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Include Go toolchain to --version (#5832) (#5830)
## [1.7.0](https://github.com/go-gitea/gitea/releases/tag/v1.7.0) - 2019-01-22
+
* SECURITY
* Do not display the raw OpenID error in the UI (#5705) (#5712)
* When redirecting clean the path to avoid redirecting to external site (#5669) (#5679)
@@ -4258,18 +5101,21 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Only chown directories during docker setup if necessary. Fix #4425 (#5064)
## [1.6.4](https://github.com/go-gitea/gitea/releases/tag/v1.6.4) - 2019-01-15
+
* BUGFIX
* Fix SSH key now can be reused as public key after deleting as deploy key (#5671) (#5685)
* When redirecting clean the path to avoid redirecting to external site (#5669) (#5703)
* Fix to use correct value for "MSpan Structures Obtained" (#5706) (#5715)
## [1.6.3](https://github.com/go-gitea/gitea/releases/tag/v1.6.3) - 2019-01-04
+
* SECURITY
* Prevent DeleteFilePost doing arbitrary deletion (#5631)
* BUGFIX
* Fix wrong text getting saved on editing second comment on an issue (#5608)
## [1.6.2](https://github.com/go-gitea/gitea/releases/tag/v1.6.2) - 2018-12-21
+
* SECURITY
* Sanitize uploaded file names (#5571) (#5573)
* HTMLEncode user added text (#5570) (#5575)
@@ -4284,6 +5130,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix empty wiki (#5504) (#5508)
## [1.6.1](https://github.com/go-gitea/gitea/releases/tag/v1.6.1) - 2018-12-08
+
* BUGFIXES
* Fix dependent issue searching when gitea is run in subpath (#5392) (#5400)
* API: '/orgs/:org/repos': return private repos with read access (#5393)
@@ -4294,6 +5141,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix topic name length on database (#5493) (#5495)
## [1.6.0](https://github.com/go-gitea/gitea/releases/tag/v1.6.0) - 2018-11-22
+
* BREAKING
* Respect email privacy option in user search via API (#4512)
* Simply remove tidb and deps (#3993)
@@ -4447,10 +5295,12 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix translation (#4355)
## [1.5.3](https://github.com/go-gitea/gitea/releases/tag/v1.5.3) - 2018-10-31
+
* SECURITY
* Fix remote command execution vulnerability in upstream library (#5177) (#5196)
## [1.5.2](https://github.com/go-gitea/gitea/releases/tag/v1.5.2) - 2018-10-09
+
* SECURITY
* Enforce token on api routes (#4840) (#4905)
* BUGFIXES
@@ -4467,6 +5317,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix trimming of markup section names (#4864)
## [1.5.1](https://github.com/go-gitea/gitea/releases/tag/v1.5.1) - 2018-09-03
+
* SECURITY
* Don't disclose emails of all users when sending out emails (#4784)
* Improve URL validation for external wiki and external issues (#4710) (#4740)
@@ -4481,6 +5332,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix incorrect caption of webhook setting (#4701) (#4718)
## [1.5.0](https://github.com/go-gitea/gitea/releases/tag/v1.5.0) - 2018-08-10
+
* SECURITY
* Check that repositories can only be migrated to own user or organizations (#4366) (#4370)
* Limit uploaded avatar image-size to 4096px x 3072px by default (#4353)
@@ -4544,6 +5396,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Sign release binaries (#4188)
## [1.4.3](https://github.com/go-gitea/gitea/releases/tag/v1.4.3) - 2018-06-26
+
* SECURITY
* HTML-escape plain-text READMEs (#4192) (#4214)
* Fix open redirect vulnerability on login screen (#4312) (#4312)
@@ -4556,6 +5409,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix webhook type conflation (#4285) (#4285)
## [1.4.2](https://github.com/go-gitea/gitea/releases/tag/v1.4.2) - 2018-06-04
+
* BUGFIXES
* Adjust z-index for floating labels (#3939) (#3950)
* Add missing token validation on application settings page (#3976) #3978
@@ -4571,6 +5425,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Respository's home page not updated after first push (#4075)
## [1.4.1](https://github.com/go-gitea/gitea/releases/tag/v1.4.1) - 2018-05-03
+
* BREAKING
* Add "error" as reserved username (#3882) (#3886)
* SECURITY
@@ -4588,6 +5443,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Show clipboard button if disable HTTP of git protocol (#3773) (#3774)
## [1.4.0](https://github.com/go-gitea/gitea/releases/tag/v1.4.0) - 2018-03-25
+
* BREAKING
* Drop deprecated GOGS\_WORK\_DIR use (#2946)
* Fix API status code for hook creation (#2814)
@@ -4707,6 +5563,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Add owner to delete repo message (#2886)
## [1.3.1](https://github.com/go-gitea/gitea/releases/tag/v1.3.1) - 2017-12-08
+
* BUGFIXES
* Sanitize logs for mirror sync (#3057, #3082) (#3078)
* Fix missing branch in release bug (#3108) (#3117)
@@ -4717,6 +5574,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix missing password length check when change password (#3039) (#3071)
## [1.3.0](https://github.com/go-gitea/gitea/releases/tag/v1.3.0) - 2017-11-29
+
* BREAKING
* Make URL scheme unambiguous (#2408)
* FEATURES
@@ -4944,11 +5802,13 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Added vendor dir for js/css libs; Documented sources (#1484) (#2241)
## [1.2.3](https://github.com/go-gitea/gitea/releases/tag/v1.2.3) - 2017-11-03
+
* BUGFIXES
* Only require one email when validating GPG key (#2266, #2467, #2663) (#2788)
* Fix order of comments (#2835) (#2839)
## [1.2.2](https://github.com/go-gitea/gitea/releases/tag/v1.2.2) - 2017-10-26
+
* BUGFIXES
* Add checks for commits with missing author and time (#2771) (#2785)
* Fix sending mail with a non-latin display name (#2559) (#2783)
@@ -4957,6 +5817,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix emojify image URL (#2769) (#2773)
## [1.2.1](https://github.com/go-gitea/gitea/releases/tag/v1.2.1) - 2017-10-16
+
* BUGFIXES
* Fix PR, milestone and label functionality if issue unit is disabled (#2710) (#2714)
* Fix plain readme didn't render correctly on repo home page (#2705) (#2712)
@@ -4965,6 +5826,7 @@ WARNING: v1.10.5 is incorrectly tagged targeting 1.12-dev and should **not** be
* Fix slice out of bounds error in mailer (#2479) (#2696)
## [1.2.0](https://github.com/go-gitea/gitea/releases/tag/v1.2.0) - 2017-10-10
+
* SECURITY
* Sanitation fix from Gogs (#1461)
* BREAKING
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 45344a4d7bbb5..fbf2a331dd480 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -81,13 +81,15 @@ Here's how to run the test suite:
|``make lint-frontend`` | lint frontend files |
|``make lint-backend`` | lint backend files |
-- run test code (Suggest run in Linux)
+- run test code (Suggest run in Linux)
| | |
| :------------------------------------- | :----------------------------------------------- |
|``make test[\#TestSpecificName]`` | run unit test |
-|``make test-sqlite[\#TestSpecificName]``| run [integration](integrations) test for SQLite |
-|[More details about integrations](integrations/README.md) |
+|``make test-sqlite[\#TestSpecificName]``| run [integration](tests/integration) test for SQLite |
+|[More details about integration tests](tests/integration/README.md) |
+|``make test-e2e-sqlite[\#TestSpecificFileName]``| run [end-to-end](tests/e2e) test for SQLite |
+|[More details about e2e tests](tests/e2e/README.md) |
## Vendoring
@@ -127,14 +129,14 @@ the *[How to get faster PR reviews](https://github.com/kubernetes/community/blob
it has lots of useful tips for any project you may want to contribute.
Some of the key points:
-* Make small pull requests. The smaller, the faster to review and the
+- Make small pull requests. The smaller, the faster to review and the
more likely it will be merged soon.
-* Don't make changes unrelated to your PR. Maybe there are typos on
+- Don't make changes unrelated to your PR. Maybe there are typos on
some comments, maybe refactoring would be welcome on a function... but
if that is not related to your PR, please make *another* PR for that.
-* Split big pull requests into multiple small ones. An incremental change
+- Split big pull requests into multiple small ones. An incremental change
will be faster to review than a huge PR.
-* Use the first comment as a summary explainer of your PR and you should keep this up-to-date as the PR evolves.
+- Use the first comment as a summary explainer of your PR and you should keep this up-to-date as the PR evolves.
If your PR could cause a breaking change you must add a BREAKING section to this comment e.g.:
@@ -144,9 +146,20 @@ 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)
+For imports you should use the following format (*without* the comments)
+
```go
import (
// stdlib
@@ -167,25 +180,36 @@ import (
To maintain understandable code and avoid circular dependencies it is important to have a good structure of the code. The Gitea code is divided into the following parts:
-- **integration:** Integrations tests
- **models:** Contains the data structures used by xorm to construct database tables. It also contains supporting functions to query and update the database. Dependencies to other code in Gitea should be avoided although some modules might be needed (for example for logging).
- **models/fixtures:** Sample model data used in integration tests.
- **models/migrations:** Handling of database migrations between versions. PRs that changes a database structure shall also have a migration step.
-- **modules:** Different modules to handle specific functionality in Gitea.
+- **modules:** Different modules to handle specific functionality in Gitea. Shall only depend on other modules but not other packages (models, services).
- **public:** Frontend files (javascript, images, css, etc.)
-- **routers:** Handling of server requests. As it uses other Gitea packages to serve the request, other packages (models, modules or services) shall not depend on routers
+- **routers:** Handling of server requests. As it uses other Gitea packages to serve the request, other packages (models, modules or services) shall not depend on routers.
- **services:** Support functions for common routing operations. Uses models and modules to handle the request.
- **templates:** Golang templates for generating the html output.
+- **tests/e2e:** End to end tests
+- **tests/integration:** Integration tests
+- **tests/gitea-repositories-meta:** Sample repos used in integration tests. Adding a new repo requires editing `models/fixtures/repositories.yml` and `models/fixtures/repo_unit.yml` to match.
+- **tests/gitea-lfs-meta:** Sample LFS objects used in integration tests. Adding a new object requires editing `models/fixtures/lfs_meta_object.yml` to match.
- **vendor:** External code that Gitea depends on.
+## Documentation
+
+If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated.
+
## API v1
The API is documented by [swagger](http://try.gitea.io/api/swagger) and is based on [GitHub API v3](https://developer.github.com/v3/).
-Thus, Gitea´s API should use the same endpoints and fields as GitHub´s API as far as possible, unless there are good reasons to deviate.
-If Gitea provides functionality that GitHub does not, a new endpoint can be created.
+
+Thus, Gitea´s API should use the same endpoints and fields as GitHub´s API as far as possible, unless there are good reasons to deviate.
+
+If Gitea provides functionality that GitHub does not, a new endpoint can be created.
+
If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields.
Updating an existing API should not remove existing fields unless there is a really good reason to do so.
+
The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to APIv2 (which is currently not planned).
All expected results (errors, success, fail messages) should be documented
@@ -194,49 +218,33 @@ All expected results (errors, success, fail messages) should be documented
All JSON input types must be defined as a struct in [modules/structs/](modules/structs/)
([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91))
and referenced in
-[routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go).
+[routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go).
+
They can then be used like the following:
([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318)).
All JSON responses must be defined as a struct in [modules/structs/](modules/structs/)
([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68))
and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/)
-([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16))
+([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16))
+
They can be used like the following:
([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279))
In general, HTTP methods are chosen as follows:
- * **GET** endpoints return requested object and status **OK (200)**
- * **DELETE** endpoints return status **No Content (204)**
- * **POST** endpoints return status **Created (201)**, used to **create** new objects (e.g. a User)
- * **PUT** endpoints return status **No Content (204)**, used to **add/assign** existing Objects (e.g. User) to something (e.g. Org-Team)
- * **PATCH** endpoints return changed object and status **OK (200)**, used to **edit/change** an existing object
-An endpoint which changes/edits an object expects all fields to be optional (except ones to identify the object, which are required).
-### Endpoints returning lists should
- * support pagination (`page` & `limit` options in query)
- * set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444))
-
-## Large Character Comments
-
-Throughout the codebase there are large-text comments for sections of code, e.g.:
-
-```go
-// __________ .__
-// \______ \ _______ _|__| ______ _ __
-// | _// __ \ \/ / |/ __ \ \/ \/ /
-// | | \ ___/\ /| \ ___/\ /
-// |____|_ /\___ >\_/ |__|\___ >\/\_/
-// \/ \/ \/
-```
+- **GET** endpoints return requested object and status **OK (200)**
+- **DELETE** endpoints return status **No Content (204)**
+- **POST** endpoints return status **Created (201)**, used to **create** new objects (e.g. a User)
+- **PUT** endpoints return status **No Content (204)**, used to **add/assign** existing Objects (e.g. User) to something (e.g. Org-Team)
+- **PATCH** endpoints return changed object and status **OK (200)**, used to **edit/change** an existing object
-These were created using the `figlet` tool with the `graffiti` font.
+An endpoint which changes/edits an object expects all fields to be optional (except ones to identify the object, which are required).
-A simple way of creating these is to use the following:
+### Endpoints returning lists should
-```bash
-figlet -f graffiti Review | sed -e's+^+// +' - | xclip -sel clip -in
-```
+- support pagination (`page` & `limit` options in query)
+- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444))
## Backports and Frontports
@@ -368,35 +376,35 @@ and lead the development of Gitea.
To honor the past owners, here's the history of the owners and the time
they served:
-* 2022-01-01 ~ 2022-12-31 - https://github.com/go-gitea/gitea/issues/17872
- * [Lunny Xiao](https://gitea.com/lunny)
- * [Matti Ranta](https://gitea.com/techknowlogick)
- * [Andrew Thornton](https://gitea.com/zeripath)
+- 2022-01-01 ~ 2022-12-31 - https://github.com/go-gitea/gitea/issues/17872
+ - [Lunny Xiao](https://gitea.com/lunny)
+ - [Matti Ranta](https://gitea.com/techknowlogick)
+ - [Andrew Thornton](https://gitea.com/zeripath)
-* 2021-01-01 ~ 2021-12-31 - https://github.com/go-gitea/gitea/issues/13801
- * [Lunny Xiao](https://gitea.com/lunny)
- * [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks)
- * [Matti Ranta](https://gitea.com/techknowlogick)
+- 2021-01-01 ~ 2021-12-31 - https://github.com/go-gitea/gitea/issues/13801
+ - [Lunny Xiao](https://gitea.com/lunny)
+ - [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks)
+ - [Matti Ranta](https://gitea.com/techknowlogick)
-* 2020-01-01 ~ 2020-12-31 - https://github.com/go-gitea/gitea/issues/9230
- * [Lunny Xiao](https://gitea.com/lunny)
- * [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks)
- * [Matti Ranta](https://gitea.com/techknowlogick)
+- 2020-01-01 ~ 2020-12-31 - https://github.com/go-gitea/gitea/issues/9230
+ - [Lunny Xiao](https://gitea.com/lunny)
+ - [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks)
+ - [Matti Ranta](https://gitea.com/techknowlogick)
-* 2019-01-01 ~ 2019-12-31 - https://github.com/go-gitea/gitea/issues/5572
- * [Lunny Xiao](https://github.com/lunny)
- * [Lauris Bukšis-Haberkorns](https://github.com/lafriks)
- * [Matti Ranta](https://github.com/techknowlogick)
+- 2019-01-01 ~ 2019-12-31 - https://github.com/go-gitea/gitea/issues/5572
+ - [Lunny Xiao](https://github.com/lunny)
+ - [Lauris Bukšis-Haberkorns](https://github.com/lafriks)
+ - [Matti Ranta](https://github.com/techknowlogick)
-* 2018-01-01 ~ 2018-12-31 - https://github.com/go-gitea/gitea/issues/3255
- * [Lunny Xiao](https://github.com/lunny)
- * [Lauris Bukšis-Haberkorns](https://github.com/lafriks)
- * [Kim Carlbäcker](https://github.com/bkcsoft)
+- 2018-01-01 ~ 2018-12-31 - https://github.com/go-gitea/gitea/issues/3255
+ - [Lunny Xiao](https://github.com/lunny)
+ - [Lauris Bukšis-Haberkorns](https://github.com/lafriks)
+ - [Kim Carlbäcker](https://github.com/bkcsoft)
-* 2016-11-04 ~ 2017-12-31
- * [Lunny Xiao](https://github.com/lunny)
- * [Thomas Boerger](https://github.com/tboerger)
- * [Kim Carlbäcker](https://github.com/bkcsoft)
+- 2016-11-04 ~ 2017-12-31
+ - [Lunny Xiao](https://github.com/lunny)
+ - [Thomas Boerger](https://github.com/tboerger)
+ - [Kim Carlbäcker](https://github.com/bkcsoft)
## Versions
@@ -413,29 +421,29 @@ be reviewed by two maintainers and must pass the automatic tests.
## Releasing Gitea
-* Let $vmaj, $vmin and $vpat be Major, Minor and Patch version numbers, $vpat should be rc1, rc2, 0, 1, ...... $vmaj.$vmin will be kept the same as milestones on github or gitea in future.
-* Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody against in about serval hours.
-* If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps:
- * Create `-dev` tag as `git tag -s -F release.notes v$vmaj.$vmin.0-dev` and push the tag as `git push origin v$vmaj.$vmin.0-dev`.
- * When CI has finished building tag then you have to create a new branch named `release/v$vmaj.$vmin`
-* If it is bugfix version create PR for changelog on branch `release/v$vmaj.$vmin` and wait till it is reviewed and merged.
-* Add a tag as `git tag -s -F release.notes v$vmaj.$vmin.$`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`.
-* And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.)
-* If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version.
-* Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release.
-* Verify all release assets were correctly published through CI on dl.gitea.io and GitHub releases. Once ACKed:
- * bump the version of https://dl.gitea.io/gitea/version.json
- * merge the blog post PR
- * announce the release in discord `#announcements`
+- Let $vmaj, $vmin and $vpat be Major, Minor and Patch version numbers, $vpat should be rc1, rc2, 0, 1, ...... $vmaj.$vmin will be kept the same as milestones on github or gitea in future.
+- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody against in about serval hours.
+- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps:
+ - Create `-dev` tag as `git tag -s -F release.notes v$vmaj.$vmin.0-dev` and push the tag as `git push origin v$vmaj.$vmin.0-dev`.
+ - When CI has finished building tag then you have to create a new branch named `release/v$vmaj.$vmin`
+- If it is bugfix version create PR for changelog on branch `release/v$vmaj.$vmin` and wait till it is reviewed and merged.
+- Add a tag as `git tag -s -F release.notes v$vmaj.$vmin.$`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`.
+- And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.)
+- If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version.
+- Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release.
+- Verify all release assets were correctly published through CI on dl.gitea.io and GitHub releases. Once ACKed:
+ - bump the version of https://dl.gitea.io/gitea/version.json
+ - merge the blog post PR
+ - announce the release in discord `#announcements`
## Copyright
Code that you contribute should use the standard copyright header:
```
-// 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.
+// Copyright The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
```
Files in the repository contain copyright from the year they are added
diff --git a/Dockerfile b/Dockerfile
index 973d93b784cca..3ee474bb3425b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
#Build stage
-FROM golang:1.18-alpine3.15 AS build-env
+FROM golang:1.19-alpine3.17 AS build-env
ARG GOPROXY
ENV GOPROXY ${GOPROXY:-direct}
@@ -23,7 +23,7 @@ RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
# Begin env-to-ini build
RUN go build contrib/environment-to-ini/environment-to-ini.go
-FROM alpine:3.15
+FROM alpine:3.17
LABEL maintainer="maintainers@gitea.io"
EXPOSE 22 3000
diff --git a/Dockerfile.rootless b/Dockerfile.rootless
index 27e898c58e46c..a43a63fa10c78 100644
--- a/Dockerfile.rootless
+++ b/Dockerfile.rootless
@@ -1,5 +1,5 @@
#Build stage
-FROM golang:1.18-alpine3.15 AS build-env
+FROM golang:1.19-alpine3.17 AS build-env
ARG GOPROXY
ENV GOPROXY ${GOPROXY:-direct}
@@ -23,7 +23,7 @@ RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
# Begin env-to-ini build
RUN go build contrib/environment-to-ini/environment-to-ini.go
-FROM alpine:3.15
+FROM alpine:3.17
LABEL maintainer="maintainers@gitea.io"
EXPOSE 2222 3000
@@ -31,6 +31,7 @@ EXPOSE 2222 3000
RUN apk --no-cache add \
bash \
ca-certificates \
+ dumb-init \
gettext \
git \
curl \
@@ -62,12 +63,12 @@ ENV GITEA_CUSTOM /var/lib/gitea/custom
ENV GITEA_TEMP /tmp/gitea
ENV TMPDIR /tmp/gitea
-#TODO add to docs the ability to define the ini to load (usefull to test and revert a config)
+#TODO add to docs the ability to define the ini to load (useful to test and revert a config)
ENV GITEA_APP_INI /etc/gitea/app.ini
ENV HOME "/var/lib/gitea/git"
VOLUME ["/var/lib/gitea", "/etc/gitea"]
WORKDIR /var/lib/gitea
-ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
+ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/docker-entrypoint.sh"]
CMD []
diff --git a/MAINTAINERS b/MAINTAINERS
index bbcdde333aebd..74196d4bd860c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -44,5 +44,7 @@ Janis Estelmann (@KN4CK3R)
Steven Kriegler (@justusbunsi)
Jimmy Praet (@jpraet)
Leon Hofmeister (@delvh)
-Gusted (@singuliere)
+Wim (@42wim)
+Xinyu Zhou (@xin-u)
+Jason Song (@wolfogre)
+Yarden Shoham (@yardenshoham)
diff --git a/Makefile b/Makefile
index 5ed50a67382cd..4d7c507875529 100644
--- a/Makefile
+++ b/Makefile
@@ -17,24 +17,25 @@ else
DIST := dist
DIST_DIRS := $(DIST)/binaries $(DIST)/release
IMPORT := code.gitea.io/gitea
-export GO111MODULE=on
GO ?= go
SHASUM ?= shasum -a 256
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
COMMA := ,
-XGO_VERSION := go-1.18.x
+XGO_VERSION := go-1.19.x
-AIR_PACKAGE ?= github.com/cosmtrek/air@v1.29.0
-EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.4.0
-ERRCHECK_PACKAGE ?= github.com/kisielk/errcheck@v1.6.0
-GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.3.0
-GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.44.2
+AIR_PACKAGE ?= github.com/cosmtrek/air@v1.40.4
+EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.6.0
+ERRCHECK_PACKAGE ?= github.com/kisielk/errcheck@v1.6.2
+GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.4.0
+GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1
GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10
MISSPELL_PACKAGE ?= github.com/client9/misspell/cmd/misspell@v0.3.4
-SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.29.0
+SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.3
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
+GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.5.0
+GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@latest
DOCKER_IMAGE ?= gitea/gitea
DOCKER_TAG ?= latest
@@ -99,7 +100,8 @@ LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(G
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
-GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/models/migrations code.gitea.io/gitea/integrations/migration-test code.gitea.io/gitea/integrations,$(shell $(GO) list ./... | grep -v /vendor/))
+GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
+GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
FOMANTIC_WORK_DIR := web_src/fomantic
@@ -111,25 +113,39 @@ WEBPACK_DEST_ENTRIES := public/js public/css public/fonts public/img/webpack pub
BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))
+GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
+
SVG_DEST_DIR := public/img/svg
AIR_TMP_DIR := .air
+GO_LICENSE_TMP_DIR := .go-licenses
+GO_LICENSE_FILE := assets/go-licenses.json
+
TAGS ?=
TAGS_SPLIT := $(subst $(COMMA), ,$(TAGS))
TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
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)
+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 integrations models modules routers build services tools
+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)
+GO_SOURCES += $(GENERATED_GO_DEST)
+GO_SOURCES_NO_BINDATA := $(GO_SOURCES)
ifeq ($(filter $(TAGS_SPLIT),bindata),bindata)
GO_SOURCES += $(BINDATA_DEST)
+ GENERATED_GO_DEST += $(BINDATA_DEST)
+endif
+
+# Force installation of playwright dependencies by setting this flag
+ifdef DEPS_PLAYWRIGHT
+ PLAYWRIGHT_FLAGS += --with-deps
endif
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
@@ -183,6 +199,7 @@ help:
@echo " - test test everything"
@echo " - test-frontend test frontend files"
@echo " - test-backend test backend files"
+ @echo " - test-e2e[\#TestSpecificName] test end to end using playwright"
@echo " - webpack build webpack files"
@echo " - svg build svg files"
@echo " - fomantic build fomantic files"
@@ -194,16 +211,18 @@ help:
@echo " - generate-swagger generate the swagger spec from code comments"
@echo " - swagger-validate check if the swagger spec is valid"
@echo " - golangci-lint run golangci-lint linter"
+ @echo " - go-licenses regenerate go licenses"
@echo " - vet examines Go source code and reports suspicious constructs"
+ @echo " - tidy run go mod tidy"
@echo " - test[\#TestSpecificName] run unit test"
@echo " - test-sqlite[\#TestSpecificName] run integration test for sqlite"
@echo " - pr# build and start gitea from a PR with integration test data loaded"
.PHONY: go-check
go-check:
- $(eval MIN_GO_VERSION_STR := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
- $(eval MIN_GO_VERSION := $(shell printf "%03d%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
- $(eval GO_VERSION := $(shell printf "%03d%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9.]+' | tr '.' ' ');))
+ $(eval MIN_GO_VERSION_STR := $(shell grep -Eo '^go\s+[0-9]+\.[0-9]+' go.mod | cut -d' ' -f2))
+ $(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' ')))
+ $(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');))
@if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \
echo "Gitea requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \
exit 1; \
@@ -236,14 +255,33 @@ clean:
$(GO) clean -i ./...
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \
integrations*.test \
- integrations/gitea-integration-pgsql/ integrations/gitea-integration-mysql/ integrations/gitea-integration-mysql8/ integrations/gitea-integration-sqlite/ \
- integrations/gitea-integration-mssql/ integrations/indexers-mysql/ integrations/indexers-mysql8/ integrations/indexers-pgsql integrations/indexers-sqlite \
- integrations/indexers-mssql integrations/mysql.ini integrations/mysql8.ini integrations/pgsql.ini integrations/mssql.ini man/
+ e2e*.test \
+ tests/integration/gitea-integration-pgsql/ tests/integration/gitea-integration-mysql/ tests/integration/gitea-integration-mysql8/ tests/integration/gitea-integration-sqlite/ \
+ tests/integration/gitea-integration-mssql/ tests/integration/indexers-mysql/ tests/integration/indexers-mysql8/ tests/integration/indexers-pgsql tests/integration/indexers-sqlite \
+ tests/integration/indexers-mssql tests/mysql.ini tests/mysql8.ini tests/pgsql.ini tests/mssql.ini man/ \
+ tests/e2e/gitea-e2e-pgsql/ tests/e2e/gitea-e2e-mysql/ tests/e2e/gitea-e2e-mysql8/ tests/e2e/gitea-e2e-sqlite/ \
+ tests/e2e/gitea-e2e-mssql/ tests/e2e/indexers-mysql/ tests/e2e/indexers-mysql8/ tests/e2e/indexers-pgsql/ tests/e2e/indexers-sqlite/ \
+ tests/e2e/indexers-mssql/ tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
.PHONY: fmt
fmt:
- @echo "Running gitea-fmt (with gofumpt)..."
- @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:
@@ -261,7 +299,9 @@ TAGS_PREREQ := $(TAGS_EVIDENCE)
endif
.PHONY: generate-swagger
-generate-swagger:
+generate-swagger: $(SWAGGER_SPEC)
+
+$(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA)
$(GO) run $(SWAGGER_PACKAGE) generate spec -x "$(SWAGGER_EXCLUDE)" -o './$(SWAGGER_SPEC)'
$(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)'
$(SED_INPLACE) $(SWAGGER_NEWLINE_COMMAND) './$(SWAGGER_SPEC)'
@@ -286,16 +326,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
-
.PHONY: checks
checks: checks-frontend checks-backend
@@ -303,15 +333,17 @@ checks: checks-frontend checks-backend
checks-frontend: lockfile-check svg-check
.PHONY: checks-backend
-checks-backend: gomod-check swagger-check swagger-validate
+checks-backend: tidy-check swagger-check fmt-check misspell-check swagger-validate security-check
.PHONY: lint
lint: lint-frontend lint-backend
.PHONY: lint-frontend
lint-frontend: node_modules
- npx eslint --color --max-warnings=0 web_src/js build templates *.config.js docs/assets/js
+ npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js docs/assets/js tests/e2e
npx stylelint --color --max-warnings=0 web_src/less
+ npx spectral lint -q -F hint $(SWAGGER_SPEC)
+ npx markdownlint docs *.md
.PHONY: lint-backend
lint-backend: golangci-lint vet editorconfig-checker
@@ -327,7 +359,7 @@ watch-frontend: node-check node_modules
.PHONY: watch-backend
watch-backend: go-check
- $(GO) run $(AIR_PACKAGE) -c .air.toml
+ GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml
.PHONY: test
test: test-frontend test-backend
@@ -335,11 +367,11 @@ test: test-frontend test-backend
.PHONY: test-backend
test-backend:
@echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
- @$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_PACKAGES)
+ @$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES)
.PHONY: test-frontend
test-frontend: node_modules
- @NODE_OPTIONS="--experimental-vm-modules --no-warnings" npx jest --color
+ npx vitest
.PHONY: test-check
test-check:
@@ -356,54 +388,62 @@ test-check:
.PHONY: test\#%
test\#%:
@echo "Running go test with -tags '$(TEST_TAGS)'..."
- @$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_PACKAGES)
+ @$(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES)
.PHONY: coverage
coverage:
grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' coverage.out > coverage-bodged.out
grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' integration.coverage.out > integration.coverage-bodged.out
- GO111MODULE=on $(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all || (echo "gocovmerge failed"; echo "integration.coverage.out"; cat integration.coverage.out; echo "coverage.out"; cat coverage.out; exit 1)
+ $(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all
.PHONY: unit-test-coverage
unit-test-coverage:
@echo "Running unit-test-coverage $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..."
- @$(GO) test $(GOTESTFLAGS) -timeout=20m -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
+ @$(GO) test $(GOTESTFLAGS) -timeout=20m -tags='$(TEST_TAGS)' -cover -coverprofile coverage.out $(GO_TEST_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
-.PHONY: vendor
-vendor:
- $(GO) mod tidy && $(GO) mod vendor
+.PHONY: tidy
+tidy:
+ $(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
+ $(GO) mod tidy -compat=$(MIN_GO_VERSION)
+ @$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
-.PHONY: gomod-check
-gomod-check:
- @$(GO) mod tidy
- @diff=$$(git diff go.sum); \
+vendor: go.mod go.sum
+ $(GO) mod vendor
+ @touch vendor
+
+.PHONY: tidy-check
+tidy-check: tidy
+ @diff=$$(git diff go.mod go.sum $(GO_LICENSE_FILE)); \
if [ -n "$$diff" ]; then \
- echo "Please run '$(GO) mod tidy' and commit the result:"; \
+ echo "Please run 'make tidy' and commit the result:"; \
echo "$${diff}"; \
exit 1; \
fi
+.PHONY: go-licenses
+go-licenses: $(GO_LICENSE_FILE)
+
+$(GO_LICENSE_FILE): go.mod go.sum
+ -$(GO) run $(GO_LICENSES_PACKAGE) save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null
+ $(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE)
+ @rm -rf $(GO_LICENSE_TMP_DIR)
+
generate-ini-sqlite:
sed -e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
- integrations/sqlite.ini.tmpl > integrations/sqlite.ini
+ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
+ -e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
+ tests/sqlite.ini.tmpl > tests/sqlite.ini
.PHONY: test-sqlite
test-sqlite: integrations.sqlite.test generate-ini-sqlite
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./integrations.sqlite.test
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test
.PHONY: test-sqlite\#%
test-sqlite\#%: integrations.sqlite.test generate-ini-sqlite
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*)
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*)
.PHONY: test-sqlite-migration
-test-sqlite-migration: migrations.sqlite.test migrations.individual.sqlite.test generate-ini-sqlite
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.sqlite.test
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.individual.sqlite.test
-
-.PHONY: test-sqlite-migration\#%
-test-sqlite-migration\#%: migrations.sqlite.test migrations.individual.sqlite.test generate-ini-sqlite
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.individual.sqlite.test -test.run $(subst .,/,$*)
-
+test-sqlite-migration: migrations.sqlite.test migrations.individual.sqlite.test
generate-ini-mysql:
sed -e 's|{{TEST_MYSQL_HOST}}|${TEST_MYSQL_HOST}|g' \
@@ -411,20 +451,20 @@ generate-ini-mysql:
-e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|g' \
-e 's|{{TEST_MYSQL_PASSWORD}}|${TEST_MYSQL_PASSWORD}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
- integrations/mysql.ini.tmpl > integrations/mysql.ini
+ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
+ -e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
+ tests/mysql.ini.tmpl > tests/mysql.ini
.PHONY: test-mysql
test-mysql: integrations.mysql.test generate-ini-mysql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./integrations.mysql.test
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test
.PHONY: test-mysql\#%
test-mysql\#%: integrations.mysql.test generate-ini-mysql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*)
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*)
.PHONY: test-mysql-migration
-test-mysql-migration: migrations.mysql.test migrations.individual.mysql.test generate-ini-mysql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./migrations.mysql.test
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./migrations.individual.mysql.test
+test-mysql-migration: migrations.mysql.test migrations.individual.mysql.test
generate-ini-mysql8:
sed -e 's|{{TEST_MYSQL8_HOST}}|${TEST_MYSQL8_HOST}|g' \
@@ -432,20 +472,20 @@ generate-ini-mysql8:
-e 's|{{TEST_MYSQL8_USERNAME}}|${TEST_MYSQL8_USERNAME}|g' \
-e 's|{{TEST_MYSQL8_PASSWORD}}|${TEST_MYSQL8_PASSWORD}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
- integrations/mysql8.ini.tmpl > integrations/mysql8.ini
+ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
+ -e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
+ tests/mysql8.ini.tmpl > tests/mysql8.ini
.PHONY: test-mysql8
test-mysql8: integrations.mysql8.test generate-ini-mysql8
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql8.ini ./integrations.mysql8.test
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini ./integrations.mysql8.test
.PHONY: test-mysql8\#%
test-mysql8\#%: integrations.mysql8.test generate-ini-mysql8
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql8.ini ./integrations.mysql8.test -test.run $(subst .,/,$*)
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini ./integrations.mysql8.test -test.run $(subst .,/,$*)
.PHONY: test-mysql8-migration
-test-mysql8-migration: migrations.mysql8.test migrations.individual.mysql8.test generate-ini-mysql8
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql8.ini ./migrations.mysql8.test
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql8.ini ./migrations.individual.mysql8.test
+test-mysql8-migration: migrations.mysql8.test migrations.individual.mysql8.test
generate-ini-pgsql:
sed -e 's|{{TEST_PGSQL_HOST}}|${TEST_PGSQL_HOST}|g' \
@@ -454,20 +494,20 @@ generate-ini-pgsql:
-e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \
-e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
- integrations/pgsql.ini.tmpl > integrations/pgsql.ini
+ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
+ -e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
+ tests/pgsql.ini.tmpl > tests/pgsql.ini
.PHONY: test-pgsql
test-pgsql: integrations.pgsql.test generate-ini-pgsql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./integrations.pgsql.test
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test
.PHONY: test-pgsql\#%
test-pgsql\#%: integrations.pgsql.test generate-ini-pgsql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*)
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*)
.PHONY: test-pgsql-migration
-test-pgsql-migration: migrations.pgsql.test migrations.individual.pgsql.test generate-ini-pgsql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./migrations.pgsql.test
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./migrations.individual.pgsql.test
+test-pgsql-migration: migrations.pgsql.test migrations.individual.pgsql.test
generate-ini-mssql:
sed -e 's|{{TEST_MSSQL_HOST}}|${TEST_MSSQL_HOST}|g' \
@@ -475,105 +515,205 @@ generate-ini-mssql:
-e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|g' \
-e 's|{{TEST_MSSQL_PASSWORD}}|${TEST_MSSQL_PASSWORD}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
- integrations/mssql.ini.tmpl > integrations/mssql.ini
+ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
+ -e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
+ tests/mssql.ini.tmpl > tests/mssql.ini
.PHONY: test-mssql
test-mssql: integrations.mssql.test generate-ini-mssql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./integrations.mssql.test
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test
.PHONY: test-mssql\#%
test-mssql\#%: integrations.mssql.test generate-ini-mssql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*)
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*)
.PHONY: test-mssql-migration
-test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test generate-ini-mssql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./migrations.mssql.test -test.failfast
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./migrations.individual.mssql.test -test.failfast
+test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
+
+.PHONY: playwright
+playwright: $(PLAYWRIGHT_DIR)
+ npm install --no-save @playwright/test
+ npx playwright install $(PLAYWRIGHT_FLAGS)
+
+.PHONY: test-e2e%
+test-e2e%: TEST_TYPE ?= e2e
+ # Clear display env variable. Otherwise, chromium tests can fail.
+ DISPLAY=
+
+.PHONY: test-e2e
+test-e2e: test-e2e-sqlite
+
+.PHONY: test-e2e-sqlite
+test-e2e-sqlite: playwright e2e.sqlite.test generate-ini-sqlite
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test
+
+.PHONY: test-e2e-sqlite\#%
+test-e2e-sqlite\#%: playwright e2e.sqlite.test generate-ini-sqlite
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test -test.run TestE2e/$*
+
+.PHONY: test-e2e-mysql
+test-e2e-mysql: playwright e2e.mysql.test generate-ini-mysql
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test
+
+.PHONY: test-e2e-mysql\#%
+test-e2e-mysql\#%: playwright e2e.mysql.test generate-ini-mysql
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test -test.run TestE2e/$*
+
+.PHONY: test-e2e-mysql8
+test-e2e-mysql8: playwright e2e.mysql8.test generate-ini-mysql8
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini ./e2e.mysql8.test
+
+.PHONY: test-e2e-mysql8\#%
+test-e2e-mysql8\#%: playwright e2e.mysql8.test generate-ini-mysql8
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini ./e2e.mysql8.test -test.run TestE2e/$*
+
+.PHONY: test-e2e-pgsql
+test-e2e-pgsql: playwright e2e.pgsql.test generate-ini-pgsql
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test
+
+.PHONY: test-e2e-pgsql\#%
+test-e2e-pgsql\#%: playwright e2e.pgsql.test generate-ini-pgsql
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test -test.run TestE2e/$*
+
+.PHONY: test-e2e-mssql
+test-e2e-mssql: playwright e2e.mssql.test generate-ini-mssql
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test
+
+.PHONY: test-e2e-mssql\#%
+test-e2e-mssql\#%: playwright e2e.mssql.test generate-ini-mssql
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test -test.run TestE2e/$*
.PHONY: bench-sqlite
bench-sqlite: integrations.sqlite.test generate-ini-sqlite
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
.PHONY: bench-mysql
bench-mysql: integrations.mysql.test generate-ini-mysql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./integrations.mysql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
.PHONY: bench-mssql
bench-mssql: integrations.mssql.test generate-ini-mssql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./integrations.mssql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
.PHONY: bench-pgsql
bench-pgsql: integrations.pgsql.test generate-ini-pgsql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
.PHONY: integration-test-coverage
integration-test-coverage: integrations.cover.test generate-ini-mysql
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out
.PHONY: integration-test-coverage-sqlite
integration-test-coverage-sqlite: integrations.cover.sqlite.test generate-ini-sqlite
- GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out
integrations.mysql.test: git-check $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.mysql.test
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mysql.test
integrations.mysql8.test: git-check $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.mysql8.test
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mysql8.test
integrations.pgsql.test: git-check $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.pgsql.test
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.pgsql.test
integrations.mssql.test: git-check $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.mssql.test
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mssql.test
integrations.sqlite.test: git-check $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.sqlite.test -tags '$(TEST_TAGS)'
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.sqlite.test -tags '$(TEST_TAGS)'
integrations.cover.test: git-check $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -coverpkg $(shell echo $(GO_PACKAGES) | tr ' ' ',') -o integrations.cover.test
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -coverpkg $(shell echo $(GO_TEST_PACKAGES) | tr ' ' ',') -o integrations.cover.test
integrations.cover.sqlite.test: git-check $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -coverpkg $(shell echo $(GO_PACKAGES) | tr ' ' ',') -o integrations.cover.sqlite.test -tags '$(TEST_TAGS)'
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -coverpkg $(shell echo $(GO_TEST_PACKAGES) | tr ' ' ',') -o integrations.cover.sqlite.test -tags '$(TEST_TAGS)'
.PHONY: migrations.mysql.test
-migrations.mysql.test: $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.mysql.test
+migrations.mysql.test: $(GO_SOURCES) generate-ini-mysql
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mysql.test
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./migrations.mysql.test
.PHONY: migrations.mysql8.test
-migrations.mysql8.test: $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.mysql8.test
+migrations.mysql8.test: $(GO_SOURCES) generate-ini-mysql8
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mysql8.test
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini ./migrations.mysql8.test
.PHONY: migrations.pgsql.test
-migrations.pgsql.test: $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.pgsql.test
+migrations.pgsql.test: $(GO_SOURCES) generate-ini-pgsql
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.pgsql.test
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./migrations.pgsql.test
.PHONY: migrations.mssql.test
-migrations.mssql.test: $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.mssql.test
+migrations.mssql.test: $(GO_SOURCES) generate-ini-mssql
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mssql.test
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./migrations.mssql.test
.PHONY: migrations.sqlite.test
-migrations.sqlite.test: $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)'
+migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)'
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./migrations.sqlite.test
.PHONY: migrations.individual.mysql.test
migrations.individual.mysql.test: $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/models/migrations -o migrations.individual.mysql.test
+ for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
+ done
.PHONY: migrations.individual.mysql8.test
migrations.individual.mysql8.test: $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/models/migrations -o migrations.individual.mysql8.test
+ for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
+ done
+
+.PHONY: migrations.individual.mysql8.test\#%
+migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
.PHONY: migrations.individual.pgsql.test
migrations.individual.pgsql.test: $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/models/migrations -o migrations.individual.pgsql.test
+ for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
+ done
+
+.PHONY: migrations.individual.pgsql.test\#%
+migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
+
.PHONY: migrations.individual.mssql.test
-migrations.individual.mssql.test: $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/models/migrations -o migrations.individual.mssql.test
+migrations.individual.mssql.test: $(GO_SOURCES) generate-ini-mssql
+ for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg -test.failfast; \
+ done
+
+.PHONY: migrations.individual.mssql.test\#%
+migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
.PHONY: migrations.individual.sqlite.test
-migrations.individual.sqlite.test: $(GO_SOURCES)
- $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/models/migrations -o migrations.individual.sqlite.test -tags '$(TEST_TAGS)'
+migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
+ for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \
+ done
+
+.PHONY: migrations.individual.sqlite.test\#%
+migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
+ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
+
+e2e.mysql.test: $(GO_SOURCES)
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mysql.test
+
+e2e.mysql8.test: $(GO_SOURCES)
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mysql8.test
+
+e2e.pgsql.test: $(GO_SOURCES)
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.pgsql.test
+
+e2e.mssql.test: $(GO_SOURCES)
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mssql.test
+
+e2e.sqlite.test: $(GO_SOURCES)
+ $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.sqlite.test -tags '$(TEST_TAGS)'
.PHONY: check
check: test
@@ -589,49 +729,67 @@ build: frontend backend
frontend: $(WEBPACK_DEST)
.PHONY: backend
-backend: go-check generate $(EXECUTABLE)
+backend: go-check generate-backend $(EXECUTABLE)
+# We generate the backend before the frontend in case we in future we want to generate things in the frontend from generated files in backend
.PHONY: generate
-generate: $(TAGS_PREREQ)
+generate: generate-backend
+
+.PHONY: generate-backend
+generate-backend: $(TAGS_PREREQ) generate-go
+
+.PHONY: generate-go
+generate-go: $(TAGS_PREREQ)
@echo "Running go generate..."
@CC= GOOS= GOARCH= $(GO) generate -tags '$(TAGS)' $(GO_PACKAGES)
+.PHONY: security-check
+security-check:
+ go run $(GOVULNCHECK_PACKAGE) -v ./...
+
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
.PHONY: release
-release: frontend generate release-windows release-linux release-darwin release-copy release-compress vendor release-sources release-docs release-check
+release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-docs release-check
$(DIST_DIRS):
mkdir -p $(DIST_DIRS)
.PHONY: release-windows
release-windows: | $(DIST_DIRS)
- CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
+ CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
ifeq (,$(findstring gogit,$(TAGS)))
- CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'netgo osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
+ CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
endif
-ifeq ($(CI),drone)
+ifeq ($(CI),true)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-linux
release-linux: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) .
-ifeq ($(CI),drone)
+ifeq ($(CI),true)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-darwin
release-darwin: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
-ifeq ($(CI),drone)
+ifeq ($(CI),true)
+ cp /build/* $(DIST)/binaries
+endif
+
+.PHONY: release-freebsd
+release-freebsd: | $(DIST_DIRS)
+ CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
+ifeq ($(CI),true)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-copy
release-copy: | $(DIST_DIRS)
- cd $(DIST); for file in `find /build -type f -name "*"`; do cp $${file} ./release/; done;
+ cd $(DIST); for file in `find . -type f -name "*"`; do cp $${file} ./release/; done;
.PHONY: release-check
release-check: | $(DIST_DIRS)
@@ -646,7 +804,9 @@ release-sources: | $(DIST_DIRS)
echo $(VERSION) > $(STORED_VERSION_FILE)
# bsdtar needs a ^ to prevent matching subdirectories
$(eval EXCL := --exclude=$(shell tar --help | grep -q bsdtar && echo "^")./)
- tar $(addprefix $(EXCL),$(TAR_EXCLUDES)) -czf $(DIST)/release/gitea-src-$(VERSION).tar.gz .
+# use transform to a add a release-folder prefix; in bsdtar the transform parameter equivalent is -s
+ $(eval TRANSFORM := $(shell tar --help | grep -q bsdtar && echo "-s '/^./gitea-src-$(VERSION)/'" || echo "--transform 's|^./|gitea-src-$(VERSION)/|'"))
+ tar $(addprefix $(EXCL),$(TAR_EXCLUDES)) $(TRANSFORM) -czf $(DIST)/release/gitea-src-$(VERSION).tar.gz .
rm -f $(STORED_VERSION_FILE)
.PHONY: release-docs
@@ -678,6 +838,8 @@ deps-backend:
$(GO) install $(MISSPELL_PACKAGE)
$(GO) install $(SWAGGER_PACKAGE)
$(GO) install $(XGO_PACKAGE)
+ $(GO) install $(GO_LICENSES_PACKAGE)
+ $(GO) install $(GOVULNCHECK_PACKAGE)
node_modules: package-lock.json
npm install --no-save
@@ -696,8 +858,8 @@ fomantic:
cd $(FOMANTIC_WORK_DIR) && npm install --no-save
cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config
cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/
- cp -f web_src/js/vendor/dropdown.js $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/definitions/modules
cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build
+ $(SED_INPLACE) -e 's/\r//g' $(FOMANTIC_WORK_DIR)/build/semantic.css $(FOMANTIC_WORK_DIR)/build/semantic.js
rm -f $(FOMANTIC_WORK_DIR)/build/*.min.*
.PHONY: webpack
@@ -747,15 +909,15 @@ update-translations:
.PHONY: generate-license
generate-license:
- GO111MODULE=on $(GO) run build/generate-licenses.go
+ $(GO) run build/generate-licenses.go
.PHONY: generate-gitignore
generate-gitignore:
- GO111MODULE=on $(GO) run build/generate-gitignores.go
+ $(GO) run build/generate-gitignores.go
.PHONY: generate-images
generate-images: | node_modules
- npm install --no-save --no-package-lock fabric@4 imagemin-zopfli@7
+ npm install --no-save --no-package-lock fabric@5 imagemin-zopfli@7
node build/generate-images.js $(TAGS)
.PHONY: generate-manpage
@@ -764,7 +926,7 @@ generate-manpage:
@mkdir -p man/man1/ man/man5
@./gitea docs --man > man/man1/gitea.1
@gzip -9 man/man1/gitea.1 && echo man/man1/gitea.1.gz created
- @#TODO A smal script witch format config-cheat-sheet.en-us.md nicely to suit as config man page
+ @#TODO A small script that formats config-cheat-sheet.en-us.md nicely for use as a config man page
.PHONY: pr\#%
pr\#%: clean-all
diff --git a/README.md b/README.md
index 84c0524ed49bb..19703d85dd77e 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,12 @@
+
+
+
@@ -45,21 +51,21 @@
- View the chinese version of this document
+ View this document in Chinese
## Purpose
The goal of this project is to make the easiest, fastest, and most
painless way of setting up a self-hosted Git service.
-Using Go, this can be done with an independent binary distribution across
-**all platforms** which Go supports, including Linux, macOS, and Windows
-on x86, amd64, ARM and PowerPC architectures.
-Want to try it before doing anything else?
-Do it [with the online demo](https://try.gitea.io/)!
+
+As Gitea is written in Go, it works across **all** the platforms and
+architectures that are supported by Go, including Linux, macOS, and
+Windows on x86, amd64, ARM and PowerPC architectures.
+You can try it out using [the online demo](https://try.gitea.io/).
This project has been
[forked](https://blog.gitea.io/2016/12/welcome-to-gitea/) from
-[Gogs](https://gogs.io) since 2016.11 but changed a lot.
+[Gogs](https://gogs.io) since November of 2016, but a lot has changed.
## Building
@@ -73,7 +79,7 @@ or if SQLite support is required:
The `build` target is split into two sub-targets:
-- `make backend` which requires [Go 1.17](https://go.dev/dl/) or greater.
+- `make backend` which requires [Go Stable](https://go.dev/dl/), required version is defined in [go.mod](/go.mod).
- `make frontend` which requires [Node.js LTS](https://nodejs.org/en/download/) or greater and Internet connectivity to download npm dependencies.
When building from the official source tarballs which include pre-built frontend files, the `frontend` target will not be triggered, making it possible to build without Node.js and Internet connectivity.
@@ -100,9 +106,9 @@ NOTES:
## Translating
-Translations are done through Crowdin. If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there.
+Translations are done through Crowdin. If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there.
-You can also just create an issue for adding a language or ask on discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty but we hope fo fill it as questions pop up.
+You can also just create an issue for adding a language or ask on discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty but we hope to fill it as questions pop up.
https://docs.gitea.io/en-us/translation-guidelines/
@@ -113,15 +119,17 @@ https://docs.gitea.io/en-us/translation-guidelines/
For more information and instructions about how to install Gitea, please look at our [documentation](https://docs.gitea.io/en-us/).
If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://discourse.gitea.io/).
-We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea).
-The hugo-based documentation theme is hosted at [gitea/theme](https://gitea.com/gitea/theme).
+We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea).
+
+The Hugo-based documentation theme is hosted at [gitea/theme](https://gitea.com/gitea/theme).
+
The official Gitea CLI is developed at [gitea/tea](https://gitea.com/gitea/tea).
## Authors
-* [Maintainers](https://github.com/orgs/go-gitea/people)
-* [Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
-* [Translators](options/locale/TRANSLATORS)
+- [Maintainers](https://github.com/orgs/go-gitea/people)
+- [Contributors](https://github.com/go-gitea/gitea/graphs/contributors)
+- [Translators](options/locale/TRANSLATORS)
## Backers
@@ -143,6 +151,7 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
+
## FAQ
@@ -161,6 +170,7 @@ See the [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) file
for the full license text.
## Screenshots
+
Looking for an overview of the interface? Check it out!
|![Dashboard](https://dl.gitea.io/screenshots/home_timeline.png)|![User Profile](https://dl.gitea.io/screenshots/user_profile.png)|![Global Issues](https://dl.gitea.io/screenshots/global_issues.png)|
diff --git a/README_ZH.md b/README_ZH.md
index 904b3a4fb6373..0e58ad6d4a46e 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -33,6 +33,12 @@
+
+
+
@@ -45,7 +51,7 @@
- View the english version of this document
+ View this document in English
## 目标
diff --git a/SECURITY.md b/SECURITY.md
index 9846a94f7e835..ef98a2a8ac688 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,10 +1,83 @@
# Reporting security issues
-The Gitea maintainers take security seriously.
+The Gitea maintainers take security seriously.
+
If you discover a security issue, please bring it to their attention right away!
-### Reporting a Vulnerability
+## Reporting a Vulnerability
Please **DO NOT** file a public issue, instead send your report privately to `security@gitea.io`.
+## Protecting Security Information
+
+Due to the sensitive nature of security information, you can use below GPG public key encrypt your mail body.
+
+The PGP key is valid until June 24, 2024.
+
+```
+Key ID: 6FCD2D5B
+Key Type: RSA
+Expires: 6/24/2024
+Key Size: 4096/4096
+Fingerprint: 3DE0 3D1E 144A 7F06 9359 99DC AAFD 2381 6FCD 2D5B
+```
+
+UserID: Gitea Security
+
+```
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBGK1Z/4BEADFMqXA9DeeChmSxUjF0Be5sq99ZUhgrZjcN/wOzz0wuCJZC0l8
+4uC+d6mfv7JpJYlzYzOK97/x5UguKHkYNZ6mm1G9KHaXmoIBDLKDzfPdJopVNv2r
+OajijaE0uMCnMjadlg5pbhMLRQG8a9J32yyaz7ZEAw72Ab31fvvcA53NkuqO4j2w
+k7dtFQzhbNOYV0VffQT90WDZdalYHB1JHyEQ+70U9OjVD5ggNYSzX98Eu3Hjn7V7
+kqFrcAxr5TE1elf0IXJcuBJtFzQSTUGlQldKOHtGTGgGjj9r/FFAE5ioBgVD05bV
+rEEgIMM/GqYaG/nbNpWE6P3mEc2Mnn3pZaRJL0LuF26TLjnqEcMMDp5iIhLdFzXR
+3tMdtKgQFu+Mtzs3ipwWARYgHyU09RJsI2HeBx7RmZO/Xqrec763Z7zdJ7SpCn0Z
+q+pHZl24JYR0Kf3T/ZiOC0cGd2QJqpJtg5J6S/OqfX9NH6MsCczO8pUC1N/aHH2X
+CTme2nF56izORqDWKoiICteL3GpYsCV9nyCidcCmoQsS+DKvE86YhIhVIVWGRY2F
+lzpAjnN9/KLtQroutrm+Ft0mdjDiJUeFVl1cOHDhoyfCsQh62HumoyZoZvqzQd6e
+AbN11nq6aViMe2Q3je1AbiBnRnQSHxt1Tc8X4IshO3MQK1Sk7oPI6LA5oQARAQAB
+tCJHaXRlYSBTZWN1cml0eSA8c2VjdXJpdHlAZ2l0ZWEuaW8+iQJXBBMBCABBFiEE
+PeA9HhRKfwaTWZncqv0jgW/NLVsFAmK1Z/4CGwMFCQPCZwAFCwkIBwICIgIGFQoJ
+CAsCBBYCAwECHgcCF4AACgkQqv0jgW/NLVvnyxAAhxyNnWzw/rQO2qhzqicmZM94
+njSbOg+U2qMBvCdaqCQQeC+uaMmMzkDPanUUmLcyCkWqfCjPNjeSXAkE9npepVJI
+4HtmgxZQ94OU/h3CLbft+9GVRzUkVI29TSYGdvNtV2/BkNGoFFnKWQr119um0o6A
+bgha2Uy5uY8o3ZIoiKkiHRaEoWIjjeBxJxYAojsZY4YElUmsQ3ik2joG6rhFesTa
+ofVt/bL8G2xzpOG26WGIxBbqf2qjV6OtZ0hu/vtTPHeIWMLq0Mz0V3PEDQWfkGPE
+i2RYxxYDs2xzJhSQWqTNVLSq0m5xTJnbHhQPfdCX4C2jvFKgLdfmytQq49S7jiJb
+Z03HVOZ/PsyBlQfH9xJi06R5yQCMEA8h8Z5r3/NXW09kQ6OFRe6xshoTcxZGRPTo
+srhwr3uPbmCRh+YEl7qBLU6+BC5k8IRTZXqhrj/aPJu3MxgbgwV8u3vLoFSXM2lb
+a61FgeCQ0O7lkgVswwF0RppCaH9Ul3ZDapet/vCRg4NVwm9zOI/8q/Vj0FKA1GDR
+JhRu8+Ce8zlFL65D34t+PprAzSeTlbv9um3x/ZIjCco7EEKSBylt+AZj/VyA6+e5
+kjOQwRRc6dFJWBcorsSI2dG+H+QMF7ZabzmeCcz1v9HjLOPzYHoZAHhCmSppWTvX
+AJy6+lhfW2OUTqQeYSi5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
+T+p07yCSSoSlmnJHCQmwh4vfg1blyz0zZ4vkIhtpHsEgc+ZAG+WQXSsJ2iRz+eSN
+GwoOQl4XC3n+QWkc1ws+btr48+6UqXIQU+F8TPQyx/PIgi2nZXJB7f5+mjCqsk46
+XvH4nTr4kJjuqMSR/++wvre2qNQRa/q/dTsK0OaN/mJsdX6Oi+aGNaQJUhIG7F+E
+ZDMkn/O6xnwWNzy/+bpg43qH/Gk0eakOmz5NmQLRkV58SZLiJvuCUtkttf6CyhnX
+03OcWaajv5W8qA39dBYQgDrrPbBWUnwfO3yMveqhwV4JjDoe8sPAyn1NwzakNYqP
+RzsWyLrLS7R7J9s3FkZXhQw/QQcsaSMcGNQO047dm1P83N8JY5aEpiRo9zSWjoiw
+qoExANj5lUTZPe8M50lI182FrcjAN7dClO3QI6pg7wy0erMxfFly3j8UQ91ysS9T
+s+GsP9I3cmWWQcKYxWHtE8xTXnNCVPFZQj2nwhJzae8ypfOtulBRA3dUKWGKuDH/
+axFENhUsT397aOU3qkP/od4a64JyNIEo4CTTSPVeWd7njsGqli2U3A4xL2CcyYvt
+D/MWcMBGEoLSNTswwKdom4FaJpn5KThnK/T0bQcmJblJhoCtppXisbexZnCpuS0x
+Zdlm2T14KJ3LABEBAAGJAjwEGAEIACYWIQQ94D0eFEp/BpNZmdyq/SOBb80tWwUC
+YrVn/gIbDAUJA8JnAAAKCRCq/SOBb80tWyTBD/9AGpW6QoDF7zYjHAozH9S5RGCA
+Y7E82dG/0xmFUwPprAG0BKmmgU6TiipyVGmKIXGYYYU92pMnbvXkYQMoa+WJNncN
+D3fY52UeXeffTf4cFpStlzi9xgYtOLhFamzYu/4xhkjOX+xhOSXscCiFRyT8cF3B
+O6c5BHU+Zj0/rGPgOyPUbx7l7B9MubB/41nNX35k08e+8T3wtWDb4XF+15HnRfva
+6fblO8wgU25Orv2Rm1jnKGa9DxJ8nE40IMrqDapENtDuL+zKJsvR0+ptWvEyL56U
+GtJJG5un6mXiLKuRQT0DEv4MdZRHDgDstDnqcbEiazVEbUuvhZZob6lRY2A19m1+
+7zfnDxkhqCA1RCnv4fdvcPdCMMFHwLpdhjgW0aI/uwgwrvsEz5+JRlnLvdQHlPAg
+q7l2fGcBSpz9U0ayyfRPjPntsNCtZl1UDxGLeciPkZhyG84zEWQbk/j52ZpRN+Ik
+ALpRLa8RBFmFSmXDUmwQrmm1EmARyQXwweKU31hf8ZGbCp2lPuRYm1LuGiirXSVP
+GysjRAJgW+VRpBKOzFQoUAUbReVWSaCwT8s17THzf71DdDb6CTj31jMLLYWwBpA/
+i73DgobDZMIGEZZC1EKqza8eh11xfyHFzGec03tbh+lIen+5IiRtWiEWkDS9ll0G
+zgS/ZdziCvdAutqnGA==
+=gZWO
+-----END PGP PUBLIC KEY BLOCK-----
+
+```
+
Security reports are greatly appreciated and we will publicly thank you for it, although we keep your name confidential if you request it.
diff --git a/assets/emoji.json b/assets/emoji.json
index d28c33cff0284..5a1ff98a46afc 100644
--- a/assets/emoji.json
+++ b/assets/emoji.json
@@ -1 +1 @@
-[{"emoji":"👍","aliases":["+1","thumbsup"]},{"emoji":"👎","aliases":["-1","thumbsdown"]},{"emoji":"💯","aliases":["100"]},{"emoji":"🔢","aliases":["1234"]},{"emoji":"🥇","aliases":["1st_place_medal"]},{"emoji":"🥈","aliases":["2nd_place_medal"]},{"emoji":"🥉","aliases":["3rd_place_medal"]},{"emoji":"🎱","aliases":["8ball"]},{"emoji":"🅰️","aliases":["a"]},{"emoji":"🆎","aliases":["ab"]},{"emoji":"🧮","aliases":["abacus"]},{"emoji":"🔤","aliases":["abc"]},{"emoji":"🔡","aliases":["abcd"]},{"emoji":"🉑","aliases":["accept"]},{"emoji":"🩹","aliases":["adhesive_bandage"]},{"emoji":"🧑","aliases":["adult"]},{"emoji":"🚡","aliases":["aerial_tramway"]},{"emoji":"🇦🇫","aliases":["afghanistan"]},{"emoji":"✈️","aliases":["airplane"]},{"emoji":"🇦🇽","aliases":["aland_islands"]},{"emoji":"⏰","aliases":["alarm_clock"]},{"emoji":"🇦🇱","aliases":["albania"]},{"emoji":"⚗️","aliases":["alembic"]},{"emoji":"🇩🇿","aliases":["algeria"]},{"emoji":"👽","aliases":["alien"]},{"emoji":"🚑","aliases":["ambulance"]},{"emoji":"🇦🇸","aliases":["american_samoa"]},{"emoji":"🏺","aliases":["amphora"]},{"emoji":"⚓","aliases":["anchor"]},{"emoji":"🇦🇩","aliases":["andorra"]},{"emoji":"👼","aliases":["angel"]},{"emoji":"💢","aliases":["anger"]},{"emoji":"🇦🇴","aliases":["angola"]},{"emoji":"😠","aliases":["angry"]},{"emoji":"🇦🇮","aliases":["anguilla"]},{"emoji":"😧","aliases":["anguished"]},{"emoji":"🐜","aliases":["ant"]},{"emoji":"🇦🇶","aliases":["antarctica"]},{"emoji":"🇦🇬","aliases":["antigua_barbuda"]},{"emoji":"🍎","aliases":["apple"]},{"emoji":"♒","aliases":["aquarius"]},{"emoji":"🇦🇷","aliases":["argentina"]},{"emoji":"♈","aliases":["aries"]},{"emoji":"🇦🇲","aliases":["armenia"]},{"emoji":"◀️","aliases":["arrow_backward"]},{"emoji":"⏬","aliases":["arrow_double_down"]},{"emoji":"⏫","aliases":["arrow_double_up"]},{"emoji":"⬇️","aliases":["arrow_down"]},{"emoji":"🔽","aliases":["arrow_down_small"]},{"emoji":"▶️","aliases":["arrow_forward"]},{"emoji":"⤵️","aliases":["arrow_heading_down"]},{"emoji":"⤴️","aliases":["arrow_heading_up"]},{"emoji":"⬅️","aliases":["arrow_left"]},{"emoji":"↙️","aliases":["arrow_lower_left"]},{"emoji":"↘️","aliases":["arrow_lower_right"]},{"emoji":"➡️","aliases":["arrow_right"]},{"emoji":"↪️","aliases":["arrow_right_hook"]},{"emoji":"⬆️","aliases":["arrow_up"]},{"emoji":"↕️","aliases":["arrow_up_down"]},{"emoji":"🔼","aliases":["arrow_up_small"]},{"emoji":"↖️","aliases":["arrow_upper_left"]},{"emoji":"↗️","aliases":["arrow_upper_right"]},{"emoji":"🔃","aliases":["arrows_clockwise"]},{"emoji":"🔄","aliases":["arrows_counterclockwise"]},{"emoji":"🎨","aliases":["art"]},{"emoji":"🚛","aliases":["articulated_lorry"]},{"emoji":"🛰️","aliases":["artificial_satellite"]},{"emoji":"🧑🎨","aliases":["artist"]},{"emoji":"🇦🇼","aliases":["aruba"]},{"emoji":"🇦🇨","aliases":["ascension_island"]},{"emoji":"*️⃣","aliases":["asterisk"]},{"emoji":"😲","aliases":["astonished"]},{"emoji":"🧑🚀","aliases":["astronaut"]},{"emoji":"👟","aliases":["athletic_shoe"]},{"emoji":"🏧","aliases":["atm"]},{"emoji":"⚛️","aliases":["atom_symbol"]},{"emoji":"🇦🇺","aliases":["australia"]},{"emoji":"🇦🇹","aliases":["austria"]},{"emoji":"🛺","aliases":["auto_rickshaw"]},{"emoji":"🥑","aliases":["avocado"]},{"emoji":"🪓","aliases":["axe"]},{"emoji":"🇦🇿","aliases":["azerbaijan"]},{"emoji":"🅱️","aliases":["b"]},{"emoji":"👶","aliases":["baby"]},{"emoji":"🍼","aliases":["baby_bottle"]},{"emoji":"🐤","aliases":["baby_chick"]},{"emoji":"🚼","aliases":["baby_symbol"]},{"emoji":"🔙","aliases":["back"]},{"emoji":"🥓","aliases":["bacon"]},{"emoji":"🦡","aliases":["badger"]},{"emoji":"🏸","aliases":["badminton"]},{"emoji":"🥯","aliases":["bagel"]},{"emoji":"🛄","aliases":["baggage_claim"]},{"emoji":"🥖","aliases":["baguette_bread"]},{"emoji":"🇧🇸","aliases":["bahamas"]},{"emoji":"🇧🇭","aliases":["bahrain"]},{"emoji":"⚖️","aliases":["balance_scale"]},{"emoji":"👨🦲","aliases":["bald_man"]},{"emoji":"👩🦲","aliases":["bald_woman"]},{"emoji":"🩰","aliases":["ballet_shoes"]},{"emoji":"🎈","aliases":["balloon"]},{"emoji":"🗳️","aliases":["ballot_box"]},{"emoji":"☑️","aliases":["ballot_box_with_check"]},{"emoji":"🎍","aliases":["bamboo"]},{"emoji":"🍌","aliases":["banana"]},{"emoji":"‼️","aliases":["bangbang"]},{"emoji":"🇧🇩","aliases":["bangladesh"]},{"emoji":"🪕","aliases":["banjo"]},{"emoji":"🏦","aliases":["bank"]},{"emoji":"📊","aliases":["bar_chart"]},{"emoji":"🇧🇧","aliases":["barbados"]},{"emoji":"💈","aliases":["barber"]},{"emoji":"⚾","aliases":["baseball"]},{"emoji":"🧺","aliases":["basket"]},{"emoji":"🏀","aliases":["basketball"]},{"emoji":"🦇","aliases":["bat"]},{"emoji":"🛀","aliases":["bath"]},{"emoji":"🛁","aliases":["bathtub"]},{"emoji":"🔋","aliases":["battery"]},{"emoji":"🏖️","aliases":["beach_umbrella"]},{"emoji":"🐻","aliases":["bear"]},{"emoji":"🧔","aliases":["bearded_person"]},{"emoji":"🛏️","aliases":["bed"]},{"emoji":"🐝","aliases":["bee","honeybee"]},{"emoji":"🍺","aliases":["beer"]},{"emoji":"🍻","aliases":["beers"]},{"emoji":"🔰","aliases":["beginner"]},{"emoji":"🇧🇾","aliases":["belarus"]},{"emoji":"🇧🇪","aliases":["belgium"]},{"emoji":"🇧🇿","aliases":["belize"]},{"emoji":"🔔","aliases":["bell"]},{"emoji":"🛎️","aliases":["bellhop_bell"]},{"emoji":"🇧🇯","aliases":["benin"]},{"emoji":"🍱","aliases":["bento"]},{"emoji":"🇧🇲","aliases":["bermuda"]},{"emoji":"🧃","aliases":["beverage_box"]},{"emoji":"🇧🇹","aliases":["bhutan"]},{"emoji":"🚴","aliases":["bicyclist"]},{"emoji":"🚲","aliases":["bike"]},{"emoji":"🚴♂️","aliases":["biking_man"]},{"emoji":"🚴♀️","aliases":["biking_woman"]},{"emoji":"👙","aliases":["bikini"]},{"emoji":"🧢","aliases":["billed_cap"]},{"emoji":"☣️","aliases":["biohazard"]},{"emoji":"🐦","aliases":["bird"]},{"emoji":"🎂","aliases":["birthday"]},{"emoji":"⚫","aliases":["black_circle"]},{"emoji":"🏴","aliases":["black_flag"]},{"emoji":"🖤","aliases":["black_heart"]},{"emoji":"🃏","aliases":["black_joker"]},{"emoji":"⬛","aliases":["black_large_square"]},{"emoji":"◾","aliases":["black_medium_small_square"]},{"emoji":"◼️","aliases":["black_medium_square"]},{"emoji":"✒️","aliases":["black_nib"]},{"emoji":"▪️","aliases":["black_small_square"]},{"emoji":"🔲","aliases":["black_square_button"]},{"emoji":"👱♂️","aliases":["blond_haired_man"]},{"emoji":"👱","aliases":["blond_haired_person"]},{"emoji":"👱♀️","aliases":["blond_haired_woman","blonde_woman"]},{"emoji":"🌼","aliases":["blossom"]},{"emoji":"🐡","aliases":["blowfish"]},{"emoji":"📘","aliases":["blue_book"]},{"emoji":"🚙","aliases":["blue_car"]},{"emoji":"💙","aliases":["blue_heart"]},{"emoji":"🟦","aliases":["blue_square"]},{"emoji":"😊","aliases":["blush"]},{"emoji":"🐗","aliases":["boar"]},{"emoji":"⛵","aliases":["boat","sailboat"]},{"emoji":"🇧🇴","aliases":["bolivia"]},{"emoji":"💣","aliases":["bomb"]},{"emoji":"🦴","aliases":["bone"]},{"emoji":"📖","aliases":["book","open_book"]},{"emoji":"🔖","aliases":["bookmark"]},{"emoji":"📑","aliases":["bookmark_tabs"]},{"emoji":"📚","aliases":["books"]},{"emoji":"💥","aliases":["boom","collision"]},{"emoji":"👢","aliases":["boot"]},{"emoji":"🇧🇦","aliases":["bosnia_herzegovina"]},{"emoji":"🇧🇼","aliases":["botswana"]},{"emoji":"⛹️♂️","aliases":["bouncing_ball_man","basketball_man"]},{"emoji":"⛹️","aliases":["bouncing_ball_person"]},{"emoji":"⛹️♀️","aliases":["bouncing_ball_woman","basketball_woman"]},{"emoji":"💐","aliases":["bouquet"]},{"emoji":"🇧🇻","aliases":["bouvet_island"]},{"emoji":"🙇","aliases":["bow"]},{"emoji":"🏹","aliases":["bow_and_arrow"]},{"emoji":"🙇♂️","aliases":["bowing_man"]},{"emoji":"🙇♀️","aliases":["bowing_woman"]},{"emoji":"🥣","aliases":["bowl_with_spoon"]},{"emoji":"🎳","aliases":["bowling"]},{"emoji":"🥊","aliases":["boxing_glove"]},{"emoji":"👦","aliases":["boy"]},{"emoji":"🧠","aliases":["brain"]},{"emoji":"🇧🇷","aliases":["brazil"]},{"emoji":"🍞","aliases":["bread"]},{"emoji":"🤱","aliases":["breast_feeding"]},{"emoji":"🧱","aliases":["bricks"]},{"emoji":"🌉","aliases":["bridge_at_night"]},{"emoji":"💼","aliases":["briefcase"]},{"emoji":"🇮🇴","aliases":["british_indian_ocean_territory"]},{"emoji":"🇻🇬","aliases":["british_virgin_islands"]},{"emoji":"🥦","aliases":["broccoli"]},{"emoji":"💔","aliases":["broken_heart"]},{"emoji":"🧹","aliases":["broom"]},{"emoji":"🟤","aliases":["brown_circle"]},{"emoji":"🤎","aliases":["brown_heart"]},{"emoji":"🟫","aliases":["brown_square"]},{"emoji":"🇧🇳","aliases":["brunei"]},{"emoji":"🐛","aliases":["bug"]},{"emoji":"🏗️","aliases":["building_construction"]},{"emoji":"💡","aliases":["bulb"]},{"emoji":"🇧🇬","aliases":["bulgaria"]},{"emoji":"🚅","aliases":["bullettrain_front"]},{"emoji":"🚄","aliases":["bullettrain_side"]},{"emoji":"🇧🇫","aliases":["burkina_faso"]},{"emoji":"🌯","aliases":["burrito"]},{"emoji":"🇧🇮","aliases":["burundi"]},{"emoji":"🚌","aliases":["bus"]},{"emoji":"🕴️","aliases":["business_suit_levitating"]},{"emoji":"🚏","aliases":["busstop"]},{"emoji":"👤","aliases":["bust_in_silhouette"]},{"emoji":"👥","aliases":["busts_in_silhouette"]},{"emoji":"🧈","aliases":["butter"]},{"emoji":"🦋","aliases":["butterfly"]},{"emoji":"🌵","aliases":["cactus"]},{"emoji":"🍰","aliases":["cake"]},{"emoji":"📆","aliases":["calendar"]},{"emoji":"🤙","aliases":["call_me_hand"]},{"emoji":"📲","aliases":["calling"]},{"emoji":"🇰🇭","aliases":["cambodia"]},{"emoji":"🐫","aliases":["camel"]},{"emoji":"📷","aliases":["camera"]},{"emoji":"📸","aliases":["camera_flash"]},{"emoji":"🇨🇲","aliases":["cameroon"]},{"emoji":"🏕️","aliases":["camping"]},{"emoji":"🇨🇦","aliases":["canada"]},{"emoji":"🇮🇨","aliases":["canary_islands"]},{"emoji":"♋","aliases":["cancer"]},{"emoji":"🕯️","aliases":["candle"]},{"emoji":"🍬","aliases":["candy"]},{"emoji":"🥫","aliases":["canned_food"]},{"emoji":"🛶","aliases":["canoe"]},{"emoji":"🇨🇻","aliases":["cape_verde"]},{"emoji":"🔠","aliases":["capital_abcd"]},{"emoji":"♑","aliases":["capricorn"]},{"emoji":"🚗","aliases":["car","red_car"]},{"emoji":"🗃️","aliases":["card_file_box"]},{"emoji":"📇","aliases":["card_index"]},{"emoji":"🗂️","aliases":["card_index_dividers"]},{"emoji":"🇧🇶","aliases":["caribbean_netherlands"]},{"emoji":"🎠","aliases":["carousel_horse"]},{"emoji":"🥕","aliases":["carrot"]},{"emoji":"🤸","aliases":["cartwheeling"]},{"emoji":"🐱","aliases":["cat"]},{"emoji":"🐈","aliases":["cat2"]},{"emoji":"🇰🇾","aliases":["cayman_islands"]},{"emoji":"💿","aliases":["cd"]},{"emoji":"🇨🇫","aliases":["central_african_republic"]},{"emoji":"🇪🇦","aliases":["ceuta_melilla"]},{"emoji":"🇹🇩","aliases":["chad"]},{"emoji":"⛓️","aliases":["chains"]},{"emoji":"🪑","aliases":["chair"]},{"emoji":"🍾","aliases":["champagne"]},{"emoji":"💹","aliases":["chart"]},{"emoji":"📉","aliases":["chart_with_downwards_trend"]},{"emoji":"📈","aliases":["chart_with_upwards_trend"]},{"emoji":"🏁","aliases":["checkered_flag"]},{"emoji":"🧀","aliases":["cheese"]},{"emoji":"🍒","aliases":["cherries"]},{"emoji":"🌸","aliases":["cherry_blossom"]},{"emoji":"♟️","aliases":["chess_pawn"]},{"emoji":"🌰","aliases":["chestnut"]},{"emoji":"🐔","aliases":["chicken"]},{"emoji":"🧒","aliases":["child"]},{"emoji":"🚸","aliases":["children_crossing"]},{"emoji":"🇨🇱","aliases":["chile"]},{"emoji":"🐿️","aliases":["chipmunk"]},{"emoji":"🍫","aliases":["chocolate_bar"]},{"emoji":"🥢","aliases":["chopsticks"]},{"emoji":"🇨🇽","aliases":["christmas_island"]},{"emoji":"🎄","aliases":["christmas_tree"]},{"emoji":"⛪","aliases":["church"]},{"emoji":"🎦","aliases":["cinema"]},{"emoji":"🎪","aliases":["circus_tent"]},{"emoji":"🌇","aliases":["city_sunrise"]},{"emoji":"🌆","aliases":["city_sunset"]},{"emoji":"🏙️","aliases":["cityscape"]},{"emoji":"🆑","aliases":["cl"]},{"emoji":"🗜️","aliases":["clamp"]},{"emoji":"👏","aliases":["clap"]},{"emoji":"🎬","aliases":["clapper"]},{"emoji":"🏛️","aliases":["classical_building"]},{"emoji":"🧗","aliases":["climbing"]},{"emoji":"🧗♂️","aliases":["climbing_man"]},{"emoji":"🧗♀️","aliases":["climbing_woman"]},{"emoji":"🥂","aliases":["clinking_glasses"]},{"emoji":"📋","aliases":["clipboard"]},{"emoji":"🇨🇵","aliases":["clipperton_island"]},{"emoji":"🕐","aliases":["clock1"]},{"emoji":"🕙","aliases":["clock10"]},{"emoji":"🕥","aliases":["clock1030"]},{"emoji":"🕚","aliases":["clock11"]},{"emoji":"🕦","aliases":["clock1130"]},{"emoji":"🕛","aliases":["clock12"]},{"emoji":"🕧","aliases":["clock1230"]},{"emoji":"🕜","aliases":["clock130"]},{"emoji":"🕑","aliases":["clock2"]},{"emoji":"🕝","aliases":["clock230"]},{"emoji":"🕒","aliases":["clock3"]},{"emoji":"🕞","aliases":["clock330"]},{"emoji":"🕓","aliases":["clock4"]},{"emoji":"🕟","aliases":["clock430"]},{"emoji":"🕔","aliases":["clock5"]},{"emoji":"🕠","aliases":["clock530"]},{"emoji":"🕕","aliases":["clock6"]},{"emoji":"🕡","aliases":["clock630"]},{"emoji":"🕖","aliases":["clock7"]},{"emoji":"🕢","aliases":["clock730"]},{"emoji":"🕗","aliases":["clock8"]},{"emoji":"🕣","aliases":["clock830"]},{"emoji":"🕘","aliases":["clock9"]},{"emoji":"🕤","aliases":["clock930"]},{"emoji":"📕","aliases":["closed_book"]},{"emoji":"🔐","aliases":["closed_lock_with_key"]},{"emoji":"🌂","aliases":["closed_umbrella"]},{"emoji":"☁️","aliases":["cloud"]},{"emoji":"🌩️","aliases":["cloud_with_lightning"]},{"emoji":"⛈️","aliases":["cloud_with_lightning_and_rain"]},{"emoji":"🌧️","aliases":["cloud_with_rain"]},{"emoji":"🌨️","aliases":["cloud_with_snow"]},{"emoji":"🤡","aliases":["clown_face"]},{"emoji":"♣️","aliases":["clubs"]},{"emoji":"🇨🇳","aliases":["cn"]},{"emoji":"🧥","aliases":["coat"]},{"emoji":"🍸","aliases":["cocktail"]},{"emoji":"🥥","aliases":["coconut"]},{"emoji":"🇨🇨","aliases":["cocos_islands"]},{"emoji":"☕","aliases":["coffee"]},{"emoji":"⚰️","aliases":["coffin"]},{"emoji":"🥶","aliases":["cold_face"]},{"emoji":"😰","aliases":["cold_sweat"]},{"emoji":"🇨🇴","aliases":["colombia"]},{"emoji":"☄️","aliases":["comet"]},{"emoji":"🇰🇲","aliases":["comoros"]},{"emoji":"🧭","aliases":["compass"]},{"emoji":"💻","aliases":["computer"]},{"emoji":"🖱️","aliases":["computer_mouse"]},{"emoji":"🎊","aliases":["confetti_ball"]},{"emoji":"😖","aliases":["confounded"]},{"emoji":"😕","aliases":["confused"]},{"emoji":"🇨🇬","aliases":["congo_brazzaville"]},{"emoji":"🇨🇩","aliases":["congo_kinshasa"]},{"emoji":"㊗️","aliases":["congratulations"]},{"emoji":"🚧","aliases":["construction"]},{"emoji":"👷","aliases":["construction_worker"]},{"emoji":"👷♂️","aliases":["construction_worker_man"]},{"emoji":"👷♀️","aliases":["construction_worker_woman"]},{"emoji":"🎛️","aliases":["control_knobs"]},{"emoji":"🏪","aliases":["convenience_store"]},{"emoji":"🧑🍳","aliases":["cook"]},{"emoji":"🇨🇰","aliases":["cook_islands"]},{"emoji":"🍪","aliases":["cookie"]},{"emoji":"🆒","aliases":["cool"]},{"emoji":"©️","aliases":["copyright"]},{"emoji":"🌽","aliases":["corn"]},{"emoji":"🇨🇷","aliases":["costa_rica"]},{"emoji":"🇨🇮","aliases":["cote_divoire"]},{"emoji":"🛋️","aliases":["couch_and_lamp"]},{"emoji":"👫","aliases":["couple"]},{"emoji":"💑","aliases":["couple_with_heart"]},{"emoji":"👨❤️👨","aliases":["couple_with_heart_man_man"]},{"emoji":"👩❤️👨","aliases":["couple_with_heart_woman_man"]},{"emoji":"👩❤️👩","aliases":["couple_with_heart_woman_woman"]},{"emoji":"💏","aliases":["couplekiss"]},{"emoji":"👨❤️💋👨","aliases":["couplekiss_man_man"]},{"emoji":"👩❤️💋👨","aliases":["couplekiss_man_woman"]},{"emoji":"👩❤️💋👩","aliases":["couplekiss_woman_woman"]},{"emoji":"🐮","aliases":["cow"]},{"emoji":"🐄","aliases":["cow2"]},{"emoji":"🤠","aliases":["cowboy_hat_face"]},{"emoji":"🦀","aliases":["crab"]},{"emoji":"🖍️","aliases":["crayon"]},{"emoji":"💳","aliases":["credit_card"]},{"emoji":"🌙","aliases":["crescent_moon"]},{"emoji":"🦗","aliases":["cricket"]},{"emoji":"🏏","aliases":["cricket_game"]},{"emoji":"🇭🇷","aliases":["croatia"]},{"emoji":"🐊","aliases":["crocodile"]},{"emoji":"🥐","aliases":["croissant"]},{"emoji":"🤞","aliases":["crossed_fingers"]},{"emoji":"🎌","aliases":["crossed_flags"]},{"emoji":"⚔️","aliases":["crossed_swords"]},{"emoji":"👑","aliases":["crown"]},{"emoji":"😢","aliases":["cry"]},{"emoji":"😿","aliases":["crying_cat_face"]},{"emoji":"🔮","aliases":["crystal_ball"]},{"emoji":"🇨🇺","aliases":["cuba"]},{"emoji":"🥒","aliases":["cucumber"]},{"emoji":"🥤","aliases":["cup_with_straw"]},{"emoji":"🧁","aliases":["cupcake"]},{"emoji":"💘","aliases":["cupid"]},{"emoji":"🇨🇼","aliases":["curacao"]},{"emoji":"🥌","aliases":["curling_stone"]},{"emoji":"👨🦱","aliases":["curly_haired_man"]},{"emoji":"👩🦱","aliases":["curly_haired_woman"]},{"emoji":"➰","aliases":["curly_loop"]},{"emoji":"💱","aliases":["currency_exchange"]},{"emoji":"🍛","aliases":["curry"]},{"emoji":"🤬","aliases":["cursing_face"]},{"emoji":"🍮","aliases":["custard"]},{"emoji":"🛃","aliases":["customs"]},{"emoji":"🥩","aliases":["cut_of_meat"]},{"emoji":"🌀","aliases":["cyclone"]},{"emoji":"🇨🇾","aliases":["cyprus"]},{"emoji":"🇨🇿","aliases":["czech_republic"]},{"emoji":"🗡️","aliases":["dagger"]},{"emoji":"👯","aliases":["dancers"]},{"emoji":"👯♂️","aliases":["dancing_men"]},{"emoji":"👯♀️","aliases":["dancing_women"]},{"emoji":"🍡","aliases":["dango"]},{"emoji":"🕶️","aliases":["dark_sunglasses"]},{"emoji":"🎯","aliases":["dart"]},{"emoji":"💨","aliases":["dash"]},{"emoji":"📅","aliases":["date"]},{"emoji":"🇩🇪","aliases":["de"]},{"emoji":"🧏♂️","aliases":["deaf_man"]},{"emoji":"🧏","aliases":["deaf_person"]},{"emoji":"🧏♀️","aliases":["deaf_woman"]},{"emoji":"🌳","aliases":["deciduous_tree"]},{"emoji":"🦌","aliases":["deer"]},{"emoji":"🇩🇰","aliases":["denmark"]},{"emoji":"🏬","aliases":["department_store"]},{"emoji":"🏚️","aliases":["derelict_house"]},{"emoji":"🏜️","aliases":["desert"]},{"emoji":"🏝️","aliases":["desert_island"]},{"emoji":"🖥️","aliases":["desktop_computer"]},{"emoji":"🕵️","aliases":["detective"]},{"emoji":"💠","aliases":["diamond_shape_with_a_dot_inside"]},{"emoji":"♦️","aliases":["diamonds"]},{"emoji":"🇩🇬","aliases":["diego_garcia"]},{"emoji":"😞","aliases":["disappointed"]},{"emoji":"😥","aliases":["disappointed_relieved"]},{"emoji":"🤿","aliases":["diving_mask"]},{"emoji":"🪔","aliases":["diya_lamp"]},{"emoji":"💫","aliases":["dizzy"]},{"emoji":"😵","aliases":["dizzy_face"]},{"emoji":"🇩🇯","aliases":["djibouti"]},{"emoji":"🧬","aliases":["dna"]},{"emoji":"🚯","aliases":["do_not_litter"]},{"emoji":"🐶","aliases":["dog"]},{"emoji":"🐕","aliases":["dog2"]},{"emoji":"💵","aliases":["dollar"]},{"emoji":"🎎","aliases":["dolls"]},{"emoji":"🐬","aliases":["dolphin","flipper"]},{"emoji":"🇩🇲","aliases":["dominica"]},{"emoji":"🇩🇴","aliases":["dominican_republic"]},{"emoji":"🚪","aliases":["door"]},{"emoji":"🍩","aliases":["doughnut"]},{"emoji":"🕊️","aliases":["dove"]},{"emoji":"🐉","aliases":["dragon"]},{"emoji":"🐲","aliases":["dragon_face"]},{"emoji":"👗","aliases":["dress"]},{"emoji":"🐪","aliases":["dromedary_camel"]},{"emoji":"🤤","aliases":["drooling_face"]},{"emoji":"🩸","aliases":["drop_of_blood"]},{"emoji":"💧","aliases":["droplet"]},{"emoji":"🥁","aliases":["drum"]},{"emoji":"🦆","aliases":["duck"]},{"emoji":"🥟","aliases":["dumpling"]},{"emoji":"📀","aliases":["dvd"]},{"emoji":"📧","aliases":["e-mail"]},{"emoji":"🦅","aliases":["eagle"]},{"emoji":"👂","aliases":["ear"]},{"emoji":"🌾","aliases":["ear_of_rice"]},{"emoji":"🦻","aliases":["ear_with_hearing_aid"]},{"emoji":"🌍","aliases":["earth_africa"]},{"emoji":"🌎","aliases":["earth_americas"]},{"emoji":"🌏","aliases":["earth_asia"]},{"emoji":"🇪🇨","aliases":["ecuador"]},{"emoji":"🥚","aliases":["egg"]},{"emoji":"🍆","aliases":["eggplant"]},{"emoji":"🇪🇬","aliases":["egypt"]},{"emoji":"8️⃣","aliases":["eight"]},{"emoji":"✴️","aliases":["eight_pointed_black_star"]},{"emoji":"✳️","aliases":["eight_spoked_asterisk"]},{"emoji":"⏏️","aliases":["eject_button"]},{"emoji":"🇸🇻","aliases":["el_salvador"]},{"emoji":"🔌","aliases":["electric_plug"]},{"emoji":"🐘","aliases":["elephant"]},{"emoji":"🧝","aliases":["elf"]},{"emoji":"🧝♂️","aliases":["elf_man"]},{"emoji":"🧝♀️","aliases":["elf_woman"]},{"emoji":"✉️","aliases":["email","envelope"]},{"emoji":"🔚","aliases":["end"]},{"emoji":"🏴","aliases":["england"]},{"emoji":"📩","aliases":["envelope_with_arrow"]},{"emoji":"🇬🇶","aliases":["equatorial_guinea"]},{"emoji":"🇪🇷","aliases":["eritrea"]},{"emoji":"🇪🇸","aliases":["es"]},{"emoji":"🇪🇪","aliases":["estonia"]},{"emoji":"🇪🇹","aliases":["ethiopia"]},{"emoji":"🇪🇺","aliases":["eu","european_union"]},{"emoji":"💶","aliases":["euro"]},{"emoji":"🏰","aliases":["european_castle"]},{"emoji":"🏤","aliases":["european_post_office"]},{"emoji":"🌲","aliases":["evergreen_tree"]},{"emoji":"❗","aliases":["exclamation","heavy_exclamation_mark"]},{"emoji":"🤯","aliases":["exploding_head"]},{"emoji":"😑","aliases":["expressionless"]},{"emoji":"👁️","aliases":["eye"]},{"emoji":"👁️🗨️","aliases":["eye_speech_bubble"]},{"emoji":"👓","aliases":["eyeglasses"]},{"emoji":"👀","aliases":["eyes"]},{"emoji":"🤕","aliases":["face_with_head_bandage"]},{"emoji":"🤒","aliases":["face_with_thermometer"]},{"emoji":"🤦","aliases":["facepalm"]},{"emoji":"🏭","aliases":["factory"]},{"emoji":"🧑🏭","aliases":["factory_worker"]},{"emoji":"🧚","aliases":["fairy"]},{"emoji":"🧚♂️","aliases":["fairy_man"]},{"emoji":"🧚♀️","aliases":["fairy_woman"]},{"emoji":"🧆","aliases":["falafel"]},{"emoji":"🇫🇰","aliases":["falkland_islands"]},{"emoji":"🍂","aliases":["fallen_leaf"]},{"emoji":"👪","aliases":["family"]},{"emoji":"👨👦","aliases":["family_man_boy"]},{"emoji":"👨👦👦","aliases":["family_man_boy_boy"]},{"emoji":"👨👧","aliases":["family_man_girl"]},{"emoji":"👨👧👦","aliases":["family_man_girl_boy"]},{"emoji":"👨👧👧","aliases":["family_man_girl_girl"]},{"emoji":"👨👨👦","aliases":["family_man_man_boy"]},{"emoji":"👨👨👦👦","aliases":["family_man_man_boy_boy"]},{"emoji":"👨👨👧","aliases":["family_man_man_girl"]},{"emoji":"👨👨👧👦","aliases":["family_man_man_girl_boy"]},{"emoji":"👨👨👧👧","aliases":["family_man_man_girl_girl"]},{"emoji":"👨👩👦","aliases":["family_man_woman_boy"]},{"emoji":"👨👩👦👦","aliases":["family_man_woman_boy_boy"]},{"emoji":"👨👩👧","aliases":["family_man_woman_girl"]},{"emoji":"👨👩👧👦","aliases":["family_man_woman_girl_boy"]},{"emoji":"👨👩👧👧","aliases":["family_man_woman_girl_girl"]},{"emoji":"👩👦","aliases":["family_woman_boy"]},{"emoji":"👩👦👦","aliases":["family_woman_boy_boy"]},{"emoji":"👩👧","aliases":["family_woman_girl"]},{"emoji":"👩👧👦","aliases":["family_woman_girl_boy"]},{"emoji":"👩👧👧","aliases":["family_woman_girl_girl"]},{"emoji":"👩👩👦","aliases":["family_woman_woman_boy"]},{"emoji":"👩👩👦👦","aliases":["family_woman_woman_boy_boy"]},{"emoji":"👩👩👧","aliases":["family_woman_woman_girl"]},{"emoji":"👩👩👧👦","aliases":["family_woman_woman_girl_boy"]},{"emoji":"👩👩👧👧","aliases":["family_woman_woman_girl_girl"]},{"emoji":"🧑🌾","aliases":["farmer"]},{"emoji":"🇫🇴","aliases":["faroe_islands"]},{"emoji":"⏩","aliases":["fast_forward"]},{"emoji":"📠","aliases":["fax"]},{"emoji":"😨","aliases":["fearful"]},{"emoji":"🐾","aliases":["feet","paw_prints"]},{"emoji":"🕵️♀️","aliases":["female_detective"]},{"emoji":"♀️","aliases":["female_sign"]},{"emoji":"🎡","aliases":["ferris_wheel"]},{"emoji":"⛴️","aliases":["ferry"]},{"emoji":"🏑","aliases":["field_hockey"]},{"emoji":"🇫🇯","aliases":["fiji"]},{"emoji":"🗄️","aliases":["file_cabinet"]},{"emoji":"📁","aliases":["file_folder"]},{"emoji":"📽️","aliases":["film_projector"]},{"emoji":"🎞️","aliases":["film_strip"]},{"emoji":"🇫🇮","aliases":["finland"]},{"emoji":"🔥","aliases":["fire"]},{"emoji":"🚒","aliases":["fire_engine"]},{"emoji":"🧯","aliases":["fire_extinguisher"]},{"emoji":"🧨","aliases":["firecracker"]},{"emoji":"🧑🚒","aliases":["firefighter"]},{"emoji":"🎆","aliases":["fireworks"]},{"emoji":"🌓","aliases":["first_quarter_moon"]},{"emoji":"🌛","aliases":["first_quarter_moon_with_face"]},{"emoji":"🐟","aliases":["fish"]},{"emoji":"🍥","aliases":["fish_cake"]},{"emoji":"🎣","aliases":["fishing_pole_and_fish"]},{"emoji":"🤛","aliases":["fist_left"]},{"emoji":"👊","aliases":["fist_oncoming","facepunch","punch"]},{"emoji":"✊","aliases":["fist_raised","fist"]},{"emoji":"🤜","aliases":["fist_right"]},{"emoji":"5️⃣","aliases":["five"]},{"emoji":"🎏","aliases":["flags"]},{"emoji":"🦩","aliases":["flamingo"]},{"emoji":"🔦","aliases":["flashlight"]},{"emoji":"🥿","aliases":["flat_shoe"]},{"emoji":"⚜️","aliases":["fleur_de_lis"]},{"emoji":"🛬","aliases":["flight_arrival"]},{"emoji":"🛫","aliases":["flight_departure"]},{"emoji":"💾","aliases":["floppy_disk"]},{"emoji":"🎴","aliases":["flower_playing_cards"]},{"emoji":"😳","aliases":["flushed"]},{"emoji":"🥏","aliases":["flying_disc"]},{"emoji":"🛸","aliases":["flying_saucer"]},{"emoji":"🌫️","aliases":["fog"]},{"emoji":"🌁","aliases":["foggy"]},{"emoji":"🦶","aliases":["foot"]},{"emoji":"🏈","aliases":["football"]},{"emoji":"👣","aliases":["footprints"]},{"emoji":"🍴","aliases":["fork_and_knife"]},{"emoji":"🥠","aliases":["fortune_cookie"]},{"emoji":"⛲","aliases":["fountain"]},{"emoji":"🖋️","aliases":["fountain_pen"]},{"emoji":"4️⃣","aliases":["four"]},{"emoji":"🍀","aliases":["four_leaf_clover"]},{"emoji":"🦊","aliases":["fox_face"]},{"emoji":"🇫🇷","aliases":["fr"]},{"emoji":"🖼️","aliases":["framed_picture"]},{"emoji":"🆓","aliases":["free"]},{"emoji":"🇬🇫","aliases":["french_guiana"]},{"emoji":"🇵🇫","aliases":["french_polynesia"]},{"emoji":"🇹🇫","aliases":["french_southern_territories"]},{"emoji":"🍳","aliases":["fried_egg"]},{"emoji":"🍤","aliases":["fried_shrimp"]},{"emoji":"🍟","aliases":["fries"]},{"emoji":"🐸","aliases":["frog"]},{"emoji":"😦","aliases":["frowning"]},{"emoji":"☹️","aliases":["frowning_face"]},{"emoji":"🙍♂️","aliases":["frowning_man"]},{"emoji":"🙍","aliases":["frowning_person"]},{"emoji":"🙍♀️","aliases":["frowning_woman"]},{"emoji":"⛽","aliases":["fuelpump"]},{"emoji":"🌕","aliases":["full_moon"]},{"emoji":"🌝","aliases":["full_moon_with_face"]},{"emoji":"⚱️","aliases":["funeral_urn"]},{"emoji":"🇬🇦","aliases":["gabon"]},{"emoji":"🇬🇲","aliases":["gambia"]},{"emoji":"🎲","aliases":["game_die"]},{"emoji":"🧄","aliases":["garlic"]},{"emoji":"🇬🇧","aliases":["gb","uk"]},{"emoji":"⚙️","aliases":["gear"]},{"emoji":"💎","aliases":["gem"]},{"emoji":"♊","aliases":["gemini"]},{"emoji":"🧞","aliases":["genie"]},{"emoji":"🧞♂️","aliases":["genie_man"]},{"emoji":"🧞♀️","aliases":["genie_woman"]},{"emoji":"🇬🇪","aliases":["georgia"]},{"emoji":"🇬🇭","aliases":["ghana"]},{"emoji":"👻","aliases":["ghost"]},{"emoji":"🇬🇮","aliases":["gibraltar"]},{"emoji":"🎁","aliases":["gift"]},{"emoji":"💝","aliases":["gift_heart"]},{"emoji":"🦒","aliases":["giraffe"]},{"emoji":"👧","aliases":["girl"]},{"emoji":"🌐","aliases":["globe_with_meridians"]},{"emoji":"🧤","aliases":["gloves"]},{"emoji":"🥅","aliases":["goal_net"]},{"emoji":"🐐","aliases":["goat"]},{"emoji":"🥽","aliases":["goggles"]},{"emoji":"⛳","aliases":["golf"]},{"emoji":"🏌️","aliases":["golfing"]},{"emoji":"🏌️♂️","aliases":["golfing_man"]},{"emoji":"🏌️♀️","aliases":["golfing_woman"]},{"emoji":"🦍","aliases":["gorilla"]},{"emoji":"🍇","aliases":["grapes"]},{"emoji":"🇬🇷","aliases":["greece"]},{"emoji":"🍏","aliases":["green_apple"]},{"emoji":"📗","aliases":["green_book"]},{"emoji":"🟢","aliases":["green_circle"]},{"emoji":"💚","aliases":["green_heart"]},{"emoji":"🥗","aliases":["green_salad"]},{"emoji":"🟩","aliases":["green_square"]},{"emoji":"🇬🇱","aliases":["greenland"]},{"emoji":"🇬🇩","aliases":["grenada"]},{"emoji":"❕","aliases":["grey_exclamation"]},{"emoji":"❔","aliases":["grey_question"]},{"emoji":"😬","aliases":["grimacing"]},{"emoji":"😁","aliases":["grin"]},{"emoji":"😀","aliases":["grinning"]},{"emoji":"🇬🇵","aliases":["guadeloupe"]},{"emoji":"🇬🇺","aliases":["guam"]},{"emoji":"💂","aliases":["guard"]},{"emoji":"💂♂️","aliases":["guardsman"]},{"emoji":"💂♀️","aliases":["guardswoman"]},{"emoji":"🇬🇹","aliases":["guatemala"]},{"emoji":"🇬🇬","aliases":["guernsey"]},{"emoji":"🦮","aliases":["guide_dog"]},{"emoji":"🇬🇳","aliases":["guinea"]},{"emoji":"🇬🇼","aliases":["guinea_bissau"]},{"emoji":"🎸","aliases":["guitar"]},{"emoji":"🔫","aliases":["gun"]},{"emoji":"🇬🇾","aliases":["guyana"]},{"emoji":"💇","aliases":["haircut"]},{"emoji":"💇♂️","aliases":["haircut_man"]},{"emoji":"💇♀️","aliases":["haircut_woman"]},{"emoji":"🇭🇹","aliases":["haiti"]},{"emoji":"🍔","aliases":["hamburger"]},{"emoji":"🔨","aliases":["hammer"]},{"emoji":"⚒️","aliases":["hammer_and_pick"]},{"emoji":"🛠️","aliases":["hammer_and_wrench"]},{"emoji":"🐹","aliases":["hamster"]},{"emoji":"✋","aliases":["hand","raised_hand"]},{"emoji":"🤭","aliases":["hand_over_mouth"]},{"emoji":"👜","aliases":["handbag"]},{"emoji":"🤾","aliases":["handball_person"]},{"emoji":"🤝","aliases":["handshake"]},{"emoji":"💩","aliases":["hankey","poop","shit"]},{"emoji":"#️⃣","aliases":["hash"]},{"emoji":"🐥","aliases":["hatched_chick"]},{"emoji":"🐣","aliases":["hatching_chick"]},{"emoji":"🎧","aliases":["headphones"]},{"emoji":"🧑⚕️","aliases":["health_worker"]},{"emoji":"🙉","aliases":["hear_no_evil"]},{"emoji":"🇭🇲","aliases":["heard_mcdonald_islands"]},{"emoji":"❤️","aliases":["heart"]},{"emoji":"💟","aliases":["heart_decoration"]},{"emoji":"😍","aliases":["heart_eyes"]},{"emoji":"😻","aliases":["heart_eyes_cat"]},{"emoji":"💓","aliases":["heartbeat"]},{"emoji":"💗","aliases":["heartpulse"]},{"emoji":"♥️","aliases":["hearts"]},{"emoji":"✔️","aliases":["heavy_check_mark"]},{"emoji":"➗","aliases":["heavy_division_sign"]},{"emoji":"💲","aliases":["heavy_dollar_sign"]},{"emoji":"❣️","aliases":["heavy_heart_exclamation"]},{"emoji":"➖","aliases":["heavy_minus_sign"]},{"emoji":"✖️","aliases":["heavy_multiplication_x"]},{"emoji":"➕","aliases":["heavy_plus_sign"]},{"emoji":"🦔","aliases":["hedgehog"]},{"emoji":"🚁","aliases":["helicopter"]},{"emoji":"🌿","aliases":["herb"]},{"emoji":"🌺","aliases":["hibiscus"]},{"emoji":"🔆","aliases":["high_brightness"]},{"emoji":"👠","aliases":["high_heel"]},{"emoji":"🥾","aliases":["hiking_boot"]},{"emoji":"🛕","aliases":["hindu_temple"]},{"emoji":"🦛","aliases":["hippopotamus"]},{"emoji":"🔪","aliases":["hocho","knife"]},{"emoji":"🕳️","aliases":["hole"]},{"emoji":"🇭🇳","aliases":["honduras"]},{"emoji":"🍯","aliases":["honey_pot"]},{"emoji":"🇭🇰","aliases":["hong_kong"]},{"emoji":"🐴","aliases":["horse"]},{"emoji":"🏇","aliases":["horse_racing"]},{"emoji":"🏥","aliases":["hospital"]},{"emoji":"🥵","aliases":["hot_face"]},{"emoji":"🌶️","aliases":["hot_pepper"]},{"emoji":"🌭","aliases":["hotdog"]},{"emoji":"🏨","aliases":["hotel"]},{"emoji":"♨️","aliases":["hotsprings"]},{"emoji":"⌛","aliases":["hourglass"]},{"emoji":"⏳","aliases":["hourglass_flowing_sand"]},{"emoji":"🏠","aliases":["house"]},{"emoji":"🏡","aliases":["house_with_garden"]},{"emoji":"🏘️","aliases":["houses"]},{"emoji":"🤗","aliases":["hugs"]},{"emoji":"🇭🇺","aliases":["hungary"]},{"emoji":"😯","aliases":["hushed"]},{"emoji":"🍨","aliases":["ice_cream"]},{"emoji":"🧊","aliases":["ice_cube"]},{"emoji":"🏒","aliases":["ice_hockey"]},{"emoji":"⛸️","aliases":["ice_skate"]},{"emoji":"🍦","aliases":["icecream"]},{"emoji":"🇮🇸","aliases":["iceland"]},{"emoji":"🆔","aliases":["id"]},{"emoji":"🉐","aliases":["ideograph_advantage"]},{"emoji":"👿","aliases":["imp"]},{"emoji":"📥","aliases":["inbox_tray"]},{"emoji":"📨","aliases":["incoming_envelope"]},{"emoji":"🇮🇳","aliases":["india"]},{"emoji":"🇮🇩","aliases":["indonesia"]},{"emoji":"♾️","aliases":["infinity"]},{"emoji":"ℹ️","aliases":["information_source"]},{"emoji":"😇","aliases":["innocent"]},{"emoji":"⁉️","aliases":["interrobang"]},{"emoji":"📱","aliases":["iphone"]},{"emoji":"🇮🇷","aliases":["iran"]},{"emoji":"🇮🇶","aliases":["iraq"]},{"emoji":"🇮🇪","aliases":["ireland"]},{"emoji":"🇮🇲","aliases":["isle_of_man"]},{"emoji":"🇮🇱","aliases":["israel"]},{"emoji":"🇮🇹","aliases":["it"]},{"emoji":"🏮","aliases":["izakaya_lantern","lantern"]},{"emoji":"🎃","aliases":["jack_o_lantern"]},{"emoji":"🇯🇲","aliases":["jamaica"]},{"emoji":"🗾","aliases":["japan"]},{"emoji":"🏯","aliases":["japanese_castle"]},{"emoji":"👺","aliases":["japanese_goblin"]},{"emoji":"👹","aliases":["japanese_ogre"]},{"emoji":"👖","aliases":["jeans"]},{"emoji":"🇯🇪","aliases":["jersey"]},{"emoji":"🧩","aliases":["jigsaw"]},{"emoji":"🇯🇴","aliases":["jordan"]},{"emoji":"😂","aliases":["joy"]},{"emoji":"😹","aliases":["joy_cat"]},{"emoji":"🕹️","aliases":["joystick"]},{"emoji":"🇯🇵","aliases":["jp"]},{"emoji":"🧑⚖️","aliases":["judge"]},{"emoji":"🤹","aliases":["juggling_person"]},{"emoji":"🕋","aliases":["kaaba"]},{"emoji":"🦘","aliases":["kangaroo"]},{"emoji":"🇰🇿","aliases":["kazakhstan"]},{"emoji":"🇰🇪","aliases":["kenya"]},{"emoji":"🔑","aliases":["key"]},{"emoji":"⌨️","aliases":["keyboard"]},{"emoji":"🔟","aliases":["keycap_ten"]},{"emoji":"🛴","aliases":["kick_scooter"]},{"emoji":"👘","aliases":["kimono"]},{"emoji":"🇰🇮","aliases":["kiribati"]},{"emoji":"💋","aliases":["kiss"]},{"emoji":"😗","aliases":["kissing"]},{"emoji":"😽","aliases":["kissing_cat"]},{"emoji":"😚","aliases":["kissing_closed_eyes"]},{"emoji":"😘","aliases":["kissing_heart"]},{"emoji":"😙","aliases":["kissing_smiling_eyes"]},{"emoji":"🪁","aliases":["kite"]},{"emoji":"🥝","aliases":["kiwi_fruit"]},{"emoji":"🧎♂️","aliases":["kneeling_man"]},{"emoji":"🧎","aliases":["kneeling_person"]},{"emoji":"🧎♀️","aliases":["kneeling_woman"]},{"emoji":"🐨","aliases":["koala"]},{"emoji":"🈁","aliases":["koko"]},{"emoji":"🇽🇰","aliases":["kosovo"]},{"emoji":"🇰🇷","aliases":["kr"]},{"emoji":"🇰🇼","aliases":["kuwait"]},{"emoji":"🇰🇬","aliases":["kyrgyzstan"]},{"emoji":"🥼","aliases":["lab_coat"]},{"emoji":"🏷️","aliases":["label"]},{"emoji":"🥍","aliases":["lacrosse"]},{"emoji":"🐞","aliases":["lady_beetle"]},{"emoji":"🇱🇦","aliases":["laos"]},{"emoji":"🔵","aliases":["large_blue_circle"]},{"emoji":"🔷","aliases":["large_blue_diamond"]},{"emoji":"🔶","aliases":["large_orange_diamond"]},{"emoji":"🌗","aliases":["last_quarter_moon"]},{"emoji":"🌜","aliases":["last_quarter_moon_with_face"]},{"emoji":"✝️","aliases":["latin_cross"]},{"emoji":"🇱🇻","aliases":["latvia"]},{"emoji":"😆","aliases":["laughing","satisfied","laugh"]},{"emoji":"🥬","aliases":["leafy_green"]},{"emoji":"🍃","aliases":["leaves"]},{"emoji":"🇱🇧","aliases":["lebanon"]},{"emoji":"📒","aliases":["ledger"]},{"emoji":"🛅","aliases":["left_luggage"]},{"emoji":"↔️","aliases":["left_right_arrow"]},{"emoji":"🗨️","aliases":["left_speech_bubble"]},{"emoji":"↩️","aliases":["leftwards_arrow_with_hook"]},{"emoji":"🦵","aliases":["leg"]},{"emoji":"🍋","aliases":["lemon"]},{"emoji":"♌","aliases":["leo"]},{"emoji":"🐆","aliases":["leopard"]},{"emoji":"🇱🇸","aliases":["lesotho"]},{"emoji":"🎚️","aliases":["level_slider"]},{"emoji":"🇱🇷","aliases":["liberia"]},{"emoji":"♎","aliases":["libra"]},{"emoji":"🇱🇾","aliases":["libya"]},{"emoji":"🇱🇮","aliases":["liechtenstein"]},{"emoji":"🚈","aliases":["light_rail"]},{"emoji":"🔗","aliases":["link"]},{"emoji":"🦁","aliases":["lion"]},{"emoji":"👄","aliases":["lips"]},{"emoji":"💄","aliases":["lipstick"]},{"emoji":"🇱🇹","aliases":["lithuania"]},{"emoji":"🦎","aliases":["lizard"]},{"emoji":"🦙","aliases":["llama"]},{"emoji":"🦞","aliases":["lobster"]},{"emoji":"🔒","aliases":["lock"]},{"emoji":"🔏","aliases":["lock_with_ink_pen"]},{"emoji":"🍭","aliases":["lollipop"]},{"emoji":"➿","aliases":["loop"]},{"emoji":"🧴","aliases":["lotion_bottle"]},{"emoji":"🧘","aliases":["lotus_position"]},{"emoji":"🧘♂️","aliases":["lotus_position_man"]},{"emoji":"🧘♀️","aliases":["lotus_position_woman"]},{"emoji":"🔊","aliases":["loud_sound"]},{"emoji":"📢","aliases":["loudspeaker"]},{"emoji":"🏩","aliases":["love_hotel"]},{"emoji":"💌","aliases":["love_letter"]},{"emoji":"🤟","aliases":["love_you_gesture"]},{"emoji":"🔅","aliases":["low_brightness"]},{"emoji":"🧳","aliases":["luggage"]},{"emoji":"🇱🇺","aliases":["luxembourg"]},{"emoji":"🤥","aliases":["lying_face"]},{"emoji":"Ⓜ️","aliases":["m"]},{"emoji":"🇲🇴","aliases":["macau"]},{"emoji":"🇲🇰","aliases":["macedonia"]},{"emoji":"🇲🇬","aliases":["madagascar"]},{"emoji":"🔍","aliases":["mag"]},{"emoji":"🔎","aliases":["mag_right"]},{"emoji":"🧙","aliases":["mage"]},{"emoji":"🧙♂️","aliases":["mage_man"]},{"emoji":"🧙♀️","aliases":["mage_woman"]},{"emoji":"🧲","aliases":["magnet"]},{"emoji":"🀄","aliases":["mahjong"]},{"emoji":"📫","aliases":["mailbox"]},{"emoji":"📪","aliases":["mailbox_closed"]},{"emoji":"📬","aliases":["mailbox_with_mail"]},{"emoji":"📭","aliases":["mailbox_with_no_mail"]},{"emoji":"🇲🇼","aliases":["malawi"]},{"emoji":"🇲🇾","aliases":["malaysia"]},{"emoji":"🇲🇻","aliases":["maldives"]},{"emoji":"🕵️♂️","aliases":["male_detective"]},{"emoji":"♂️","aliases":["male_sign"]},{"emoji":"🇲🇱","aliases":["mali"]},{"emoji":"🇲🇹","aliases":["malta"]},{"emoji":"👨","aliases":["man"]},{"emoji":"👨🎨","aliases":["man_artist"]},{"emoji":"👨🚀","aliases":["man_astronaut"]},{"emoji":"🤸♂️","aliases":["man_cartwheeling"]},{"emoji":"👨🍳","aliases":["man_cook"]},{"emoji":"🕺","aliases":["man_dancing"]},{"emoji":"🤦♂️","aliases":["man_facepalming"]},{"emoji":"👨🏭","aliases":["man_factory_worker"]},{"emoji":"👨🌾","aliases":["man_farmer"]},{"emoji":"👨🚒","aliases":["man_firefighter"]},{"emoji":"👨⚕️","aliases":["man_health_worker"]},{"emoji":"👨🦽","aliases":["man_in_manual_wheelchair"]},{"emoji":"👨🦼","aliases":["man_in_motorized_wheelchair"]},{"emoji":"👨⚖️","aliases":["man_judge"]},{"emoji":"🤹♂️","aliases":["man_juggling"]},{"emoji":"👨🔧","aliases":["man_mechanic"]},{"emoji":"👨💼","aliases":["man_office_worker"]},{"emoji":"👨✈️","aliases":["man_pilot"]},{"emoji":"🤾♂️","aliases":["man_playing_handball"]},{"emoji":"🤽♂️","aliases":["man_playing_water_polo"]},{"emoji":"👨🔬","aliases":["man_scientist"]},{"emoji":"🤷♂️","aliases":["man_shrugging"]},{"emoji":"👨🎤","aliases":["man_singer"]},{"emoji":"👨🎓","aliases":["man_student"]},{"emoji":"👨🏫","aliases":["man_teacher"]},{"emoji":"👨💻","aliases":["man_technologist"]},{"emoji":"👲","aliases":["man_with_gua_pi_mao"]},{"emoji":"👨🦯","aliases":["man_with_probing_cane"]},{"emoji":"👳♂️","aliases":["man_with_turban"]},{"emoji":"🥭","aliases":["mango"]},{"emoji":"👞","aliases":["mans_shoe","shoe"]},{"emoji":"🕰️","aliases":["mantelpiece_clock"]},{"emoji":"🦽","aliases":["manual_wheelchair"]},{"emoji":"🍁","aliases":["maple_leaf"]},{"emoji":"🇲🇭","aliases":["marshall_islands"]},{"emoji":"🥋","aliases":["martial_arts_uniform"]},{"emoji":"🇲🇶","aliases":["martinique"]},{"emoji":"😷","aliases":["mask"]},{"emoji":"💆","aliases":["massage"]},{"emoji":"💆♂️","aliases":["massage_man"]},{"emoji":"💆♀️","aliases":["massage_woman"]},{"emoji":"🧉","aliases":["mate"]},{"emoji":"🇲🇷","aliases":["mauritania"]},{"emoji":"🇲🇺","aliases":["mauritius"]},{"emoji":"🇾🇹","aliases":["mayotte"]},{"emoji":"🍖","aliases":["meat_on_bone"]},{"emoji":"🧑🔧","aliases":["mechanic"]},{"emoji":"🦾","aliases":["mechanical_arm"]},{"emoji":"🦿","aliases":["mechanical_leg"]},{"emoji":"🎖️","aliases":["medal_military"]},{"emoji":"🏅","aliases":["medal_sports"]},{"emoji":"⚕️","aliases":["medical_symbol"]},{"emoji":"📣","aliases":["mega"]},{"emoji":"🍈","aliases":["melon"]},{"emoji":"📝","aliases":["memo","pencil"]},{"emoji":"🤼♂️","aliases":["men_wrestling"]},{"emoji":"🕎","aliases":["menorah"]},{"emoji":"🚹","aliases":["mens"]},{"emoji":"🧜♀️","aliases":["mermaid"]},{"emoji":"🧜♂️","aliases":["merman"]},{"emoji":"🧜","aliases":["merperson"]},{"emoji":"🤘","aliases":["metal"]},{"emoji":"🚇","aliases":["metro"]},{"emoji":"🇲🇽","aliases":["mexico"]},{"emoji":"🦠","aliases":["microbe"]},{"emoji":"🇫🇲","aliases":["micronesia"]},{"emoji":"🎤","aliases":["microphone"]},{"emoji":"🔬","aliases":["microscope"]},{"emoji":"🖕","aliases":["middle_finger","fu"]},{"emoji":"🥛","aliases":["milk_glass"]},{"emoji":"🌌","aliases":["milky_way"]},{"emoji":"🚐","aliases":["minibus"]},{"emoji":"💽","aliases":["minidisc"]},{"emoji":"📴","aliases":["mobile_phone_off"]},{"emoji":"🇲🇩","aliases":["moldova"]},{"emoji":"🇲🇨","aliases":["monaco"]},{"emoji":"🤑","aliases":["money_mouth_face"]},{"emoji":"💸","aliases":["money_with_wings"]},{"emoji":"💰","aliases":["moneybag"]},{"emoji":"🇲🇳","aliases":["mongolia"]},{"emoji":"🐒","aliases":["monkey"]},{"emoji":"🐵","aliases":["monkey_face"]},{"emoji":"🧐","aliases":["monocle_face"]},{"emoji":"🚝","aliases":["monorail"]},{"emoji":"🇲🇪","aliases":["montenegro"]},{"emoji":"🇲🇸","aliases":["montserrat"]},{"emoji":"🌔","aliases":["moon","waxing_gibbous_moon"]},{"emoji":"🥮","aliases":["moon_cake"]},{"emoji":"🇲🇦","aliases":["morocco"]},{"emoji":"🎓","aliases":["mortar_board"]},{"emoji":"🕌","aliases":["mosque"]},{"emoji":"🦟","aliases":["mosquito"]},{"emoji":"🛥️","aliases":["motor_boat"]},{"emoji":"🛵","aliases":["motor_scooter"]},{"emoji":"🏍️","aliases":["motorcycle"]},{"emoji":"🦼","aliases":["motorized_wheelchair"]},{"emoji":"🛣️","aliases":["motorway"]},{"emoji":"🗻","aliases":["mount_fuji"]},{"emoji":"⛰️","aliases":["mountain"]},{"emoji":"🚵","aliases":["mountain_bicyclist"]},{"emoji":"🚵♂️","aliases":["mountain_biking_man"]},{"emoji":"🚵♀️","aliases":["mountain_biking_woman"]},{"emoji":"🚠","aliases":["mountain_cableway"]},{"emoji":"🚞","aliases":["mountain_railway"]},{"emoji":"🏔️","aliases":["mountain_snow"]},{"emoji":"🐭","aliases":["mouse"]},{"emoji":"🐁","aliases":["mouse2"]},{"emoji":"🎥","aliases":["movie_camera"]},{"emoji":"🗿","aliases":["moyai"]},{"emoji":"🇲🇿","aliases":["mozambique"]},{"emoji":"🤶","aliases":["mrs_claus"]},{"emoji":"💪","aliases":["muscle"]},{"emoji":"🍄","aliases":["mushroom"]},{"emoji":"🎹","aliases":["musical_keyboard"]},{"emoji":"🎵","aliases":["musical_note"]},{"emoji":"🎼","aliases":["musical_score"]},{"emoji":"🔇","aliases":["mute"]},{"emoji":"🇲🇲","aliases":["myanmar"]},{"emoji":"💅","aliases":["nail_care"]},{"emoji":"📛","aliases":["name_badge"]},{"emoji":"🇳🇦","aliases":["namibia"]},{"emoji":"🏞️","aliases":["national_park"]},{"emoji":"🇳🇷","aliases":["nauru"]},{"emoji":"🤢","aliases":["nauseated_face"]},{"emoji":"🧿","aliases":["nazar_amulet"]},{"emoji":"👔","aliases":["necktie"]},{"emoji":"❎","aliases":["negative_squared_cross_mark"]},{"emoji":"🇳🇵","aliases":["nepal"]},{"emoji":"🤓","aliases":["nerd_face"]},{"emoji":"🇳🇱","aliases":["netherlands"]},{"emoji":"😐","aliases":["neutral_face"]},{"emoji":"🆕","aliases":["new"]},{"emoji":"🇳🇨","aliases":["new_caledonia"]},{"emoji":"🌑","aliases":["new_moon"]},{"emoji":"🌚","aliases":["new_moon_with_face"]},{"emoji":"🇳🇿","aliases":["new_zealand"]},{"emoji":"📰","aliases":["newspaper"]},{"emoji":"🗞️","aliases":["newspaper_roll"]},{"emoji":"⏭️","aliases":["next_track_button"]},{"emoji":"🆖","aliases":["ng"]},{"emoji":"🇳🇮","aliases":["nicaragua"]},{"emoji":"🇳🇪","aliases":["niger"]},{"emoji":"🇳🇬","aliases":["nigeria"]},{"emoji":"🌃","aliases":["night_with_stars"]},{"emoji":"9️⃣","aliases":["nine"]},{"emoji":"🇳🇺","aliases":["niue"]},{"emoji":"🔕","aliases":["no_bell"]},{"emoji":"🚳","aliases":["no_bicycles"]},{"emoji":"⛔","aliases":["no_entry"]},{"emoji":"🚫","aliases":["no_entry_sign"]},{"emoji":"🙅","aliases":["no_good"]},{"emoji":"🙅♂️","aliases":["no_good_man","ng_man"]},{"emoji":"🙅♀️","aliases":["no_good_woman","ng_woman"]},{"emoji":"📵","aliases":["no_mobile_phones"]},{"emoji":"😶","aliases":["no_mouth"]},{"emoji":"🚷","aliases":["no_pedestrians"]},{"emoji":"🚭","aliases":["no_smoking"]},{"emoji":"🚱","aliases":["non-potable_water"]},{"emoji":"🇳🇫","aliases":["norfolk_island"]},{"emoji":"🇰🇵","aliases":["north_korea"]},{"emoji":"🇲🇵","aliases":["northern_mariana_islands"]},{"emoji":"🇳🇴","aliases":["norway"]},{"emoji":"👃","aliases":["nose"]},{"emoji":"📓","aliases":["notebook"]},{"emoji":"📔","aliases":["notebook_with_decorative_cover"]},{"emoji":"🎶","aliases":["notes"]},{"emoji":"🔩","aliases":["nut_and_bolt"]},{"emoji":"⭕","aliases":["o"]},{"emoji":"🅾️","aliases":["o2"]},{"emoji":"🌊","aliases":["ocean"]},{"emoji":"🐙","aliases":["octopus"]},{"emoji":"🍢","aliases":["oden"]},{"emoji":"🏢","aliases":["office"]},{"emoji":"🧑💼","aliases":["office_worker"]},{"emoji":"🛢️","aliases":["oil_drum"]},{"emoji":"🆗","aliases":["ok"]},{"emoji":"👌","aliases":["ok_hand"]},{"emoji":"🙆♂️","aliases":["ok_man"]},{"emoji":"🙆","aliases":["ok_person"]},{"emoji":"🙆♀️","aliases":["ok_woman"]},{"emoji":"🗝️","aliases":["old_key"]},{"emoji":"🧓","aliases":["older_adult"]},{"emoji":"👴","aliases":["older_man"]},{"emoji":"👵","aliases":["older_woman"]},{"emoji":"🕉️","aliases":["om"]},{"emoji":"🇴🇲","aliases":["oman"]},{"emoji":"🔛","aliases":["on"]},{"emoji":"🚘","aliases":["oncoming_automobile"]},{"emoji":"🚍","aliases":["oncoming_bus"]},{"emoji":"🚔","aliases":["oncoming_police_car"]},{"emoji":"🚖","aliases":["oncoming_taxi"]},{"emoji":"1️⃣","aliases":["one"]},{"emoji":"🩱","aliases":["one_piece_swimsuit"]},{"emoji":"🧅","aliases":["onion"]},{"emoji":"📂","aliases":["open_file_folder"]},{"emoji":"👐","aliases":["open_hands"]},{"emoji":"😮","aliases":["open_mouth"]},{"emoji":"☂️","aliases":["open_umbrella"]},{"emoji":"⛎","aliases":["ophiuchus"]},{"emoji":"📙","aliases":["orange_book"]},{"emoji":"🟠","aliases":["orange_circle"]},{"emoji":"🧡","aliases":["orange_heart"]},{"emoji":"🟧","aliases":["orange_square"]},{"emoji":"🦧","aliases":["orangutan"]},{"emoji":"☦️","aliases":["orthodox_cross"]},{"emoji":"🦦","aliases":["otter"]},{"emoji":"📤","aliases":["outbox_tray"]},{"emoji":"🦉","aliases":["owl"]},{"emoji":"🐂","aliases":["ox"]},{"emoji":"🦪","aliases":["oyster"]},{"emoji":"📦","aliases":["package"]},{"emoji":"📄","aliases":["page_facing_up"]},{"emoji":"📃","aliases":["page_with_curl"]},{"emoji":"📟","aliases":["pager"]},{"emoji":"🖌️","aliases":["paintbrush"]},{"emoji":"🇵🇰","aliases":["pakistan"]},{"emoji":"🇵🇼","aliases":["palau"]},{"emoji":"🇵🇸","aliases":["palestinian_territories"]},{"emoji":"🌴","aliases":["palm_tree"]},{"emoji":"🤲","aliases":["palms_up_together"]},{"emoji":"🇵🇦","aliases":["panama"]},{"emoji":"🥞","aliases":["pancakes"]},{"emoji":"🐼","aliases":["panda_face"]},{"emoji":"📎","aliases":["paperclip"]},{"emoji":"🖇️","aliases":["paperclips"]},{"emoji":"🇵🇬","aliases":["papua_new_guinea"]},{"emoji":"🪂","aliases":["parachute"]},{"emoji":"🇵🇾","aliases":["paraguay"]},{"emoji":"⛱️","aliases":["parasol_on_ground"]},{"emoji":"🅿️","aliases":["parking"]},{"emoji":"🦜","aliases":["parrot"]},{"emoji":"〽️","aliases":["part_alternation_mark"]},{"emoji":"⛅","aliases":["partly_sunny"]},{"emoji":"🥳","aliases":["partying_face"]},{"emoji":"🛳️","aliases":["passenger_ship"]},{"emoji":"🛂","aliases":["passport_control"]},{"emoji":"⏸️","aliases":["pause_button"]},{"emoji":"☮️","aliases":["peace_symbol"]},{"emoji":"🍑","aliases":["peach"]},{"emoji":"🦚","aliases":["peacock"]},{"emoji":"🥜","aliases":["peanuts"]},{"emoji":"🍐","aliases":["pear"]},{"emoji":"🖊️","aliases":["pen"]},{"emoji":"✏️","aliases":["pencil2"]},{"emoji":"🐧","aliases":["penguin"]},{"emoji":"😔","aliases":["pensive"]},{"emoji":"🧑🤝🧑","aliases":["people_holding_hands"]},{"emoji":"🎭","aliases":["performing_arts"]},{"emoji":"😣","aliases":["persevere"]},{"emoji":"🧑🦲","aliases":["person_bald"]},{"emoji":"🧑🦱","aliases":["person_curly_hair"]},{"emoji":"🤺","aliases":["person_fencing"]},{"emoji":"🧑🦽","aliases":["person_in_manual_wheelchair"]},{"emoji":"🧑🦼","aliases":["person_in_motorized_wheelchair"]},{"emoji":"🤵","aliases":["person_in_tuxedo"]},{"emoji":"🧑🦰","aliases":["person_red_hair"]},{"emoji":"🧑🦳","aliases":["person_white_hair"]},{"emoji":"🧑🦯","aliases":["person_with_probing_cane"]},{"emoji":"👳","aliases":["person_with_turban"]},{"emoji":"👰","aliases":["person_with_veil"]},{"emoji":"🇵🇪","aliases":["peru"]},{"emoji":"🧫","aliases":["petri_dish"]},{"emoji":"🇵🇭","aliases":["philippines"]},{"emoji":"☎️","aliases":["phone","telephone"]},{"emoji":"⛏️","aliases":["pick"]},{"emoji":"🥧","aliases":["pie"]},{"emoji":"🐷","aliases":["pig"]},{"emoji":"🐖","aliases":["pig2"]},{"emoji":"🐽","aliases":["pig_nose"]},{"emoji":"💊","aliases":["pill"]},{"emoji":"🧑✈️","aliases":["pilot"]},{"emoji":"🤏","aliases":["pinching_hand"]},{"emoji":"🍍","aliases":["pineapple"]},{"emoji":"🏓","aliases":["ping_pong"]},{"emoji":"🏴☠️","aliases":["pirate_flag"]},{"emoji":"♓","aliases":["pisces"]},{"emoji":"🇵🇳","aliases":["pitcairn_islands"]},{"emoji":"🍕","aliases":["pizza"]},{"emoji":"🛐","aliases":["place_of_worship"]},{"emoji":"🍽️","aliases":["plate_with_cutlery"]},{"emoji":"⏯️","aliases":["play_or_pause_button"]},{"emoji":"🥺","aliases":["pleading_face"]},{"emoji":"👇","aliases":["point_down"]},{"emoji":"👈","aliases":["point_left"]},{"emoji":"👉","aliases":["point_right"]},{"emoji":"☝️","aliases":["point_up"]},{"emoji":"👆","aliases":["point_up_2"]},{"emoji":"🇵🇱","aliases":["poland"]},{"emoji":"🚓","aliases":["police_car"]},{"emoji":"👮","aliases":["police_officer","cop"]},{"emoji":"👮♂️","aliases":["policeman"]},{"emoji":"👮♀️","aliases":["policewoman"]},{"emoji":"🐩","aliases":["poodle"]},{"emoji":"🍿","aliases":["popcorn"]},{"emoji":"🇵🇹","aliases":["portugal"]},{"emoji":"🏣","aliases":["post_office"]},{"emoji":"📯","aliases":["postal_horn"]},{"emoji":"📮","aliases":["postbox"]},{"emoji":"🚰","aliases":["potable_water"]},{"emoji":"🥔","aliases":["potato"]},{"emoji":"👝","aliases":["pouch"]},{"emoji":"🍗","aliases":["poultry_leg"]},{"emoji":"💷","aliases":["pound"]},{"emoji":"😾","aliases":["pouting_cat"]},{"emoji":"🙎","aliases":["pouting_face"]},{"emoji":"🙎♂️","aliases":["pouting_man"]},{"emoji":"🙎♀️","aliases":["pouting_woman"]},{"emoji":"🙏","aliases":["pray"]},{"emoji":"📿","aliases":["prayer_beads"]},{"emoji":"🤰","aliases":["pregnant_woman"]},{"emoji":"🥨","aliases":["pretzel"]},{"emoji":"⏮️","aliases":["previous_track_button"]},{"emoji":"🤴","aliases":["prince"]},{"emoji":"👸","aliases":["princess"]},{"emoji":"🖨️","aliases":["printer"]},{"emoji":"🦯","aliases":["probing_cane"]},{"emoji":"🇵🇷","aliases":["puerto_rico"]},{"emoji":"🟣","aliases":["purple_circle"]},{"emoji":"💜","aliases":["purple_heart"]},{"emoji":"🟪","aliases":["purple_square"]},{"emoji":"👛","aliases":["purse"]},{"emoji":"📌","aliases":["pushpin"]},{"emoji":"🚮","aliases":["put_litter_in_its_place"]},{"emoji":"🇶🇦","aliases":["qatar"]},{"emoji":"❓","aliases":["question"]},{"emoji":"🐰","aliases":["rabbit"]},{"emoji":"🐇","aliases":["rabbit2"]},{"emoji":"🦝","aliases":["raccoon"]},{"emoji":"🐎","aliases":["racehorse"]},{"emoji":"🏎️","aliases":["racing_car"]},{"emoji":"📻","aliases":["radio"]},{"emoji":"🔘","aliases":["radio_button"]},{"emoji":"☢️","aliases":["radioactive"]},{"emoji":"😡","aliases":["rage","pout"]},{"emoji":"🚃","aliases":["railway_car"]},{"emoji":"🛤️","aliases":["railway_track"]},{"emoji":"🌈","aliases":["rainbow"]},{"emoji":"🏳️🌈","aliases":["rainbow_flag"]},{"emoji":"🤚","aliases":["raised_back_of_hand"]},{"emoji":"🤨","aliases":["raised_eyebrow"]},{"emoji":"🖐️","aliases":["raised_hand_with_fingers_splayed"]},{"emoji":"🙌","aliases":["raised_hands"]},{"emoji":"🙋","aliases":["raising_hand"]},{"emoji":"🙋♂️","aliases":["raising_hand_man"]},{"emoji":"🙋♀️","aliases":["raising_hand_woman"]},{"emoji":"🐏","aliases":["ram"]},{"emoji":"🍜","aliases":["ramen"]},{"emoji":"🐀","aliases":["rat"]},{"emoji":"🪒","aliases":["razor"]},{"emoji":"🧾","aliases":["receipt"]},{"emoji":"⏺️","aliases":["record_button"]},{"emoji":"♻️","aliases":["recycle"]},{"emoji":"🔴","aliases":["red_circle"]},{"emoji":"🧧","aliases":["red_envelope"]},{"emoji":"👨🦰","aliases":["red_haired_man"]},{"emoji":"👩🦰","aliases":["red_haired_woman"]},{"emoji":"🟥","aliases":["red_square"]},{"emoji":"®️","aliases":["registered"]},{"emoji":"☺️","aliases":["relaxed"]},{"emoji":"😌","aliases":["relieved"]},{"emoji":"🎗️","aliases":["reminder_ribbon"]},{"emoji":"🔁","aliases":["repeat"]},{"emoji":"🔂","aliases":["repeat_one"]},{"emoji":"⛑️","aliases":["rescue_worker_helmet"]},{"emoji":"🚻","aliases":["restroom"]},{"emoji":"🇷🇪","aliases":["reunion"]},{"emoji":"💞","aliases":["revolving_hearts"]},{"emoji":"⏪","aliases":["rewind"]},{"emoji":"🦏","aliases":["rhinoceros"]},{"emoji":"🎀","aliases":["ribbon"]},{"emoji":"🍚","aliases":["rice"]},{"emoji":"🍙","aliases":["rice_ball"]},{"emoji":"🍘","aliases":["rice_cracker"]},{"emoji":"🎑","aliases":["rice_scene"]},{"emoji":"🗯️","aliases":["right_anger_bubble"]},{"emoji":"💍","aliases":["ring"]},{"emoji":"🪐","aliases":["ringed_planet"]},{"emoji":"🤖","aliases":["robot"]},{"emoji":"🚀","aliases":["rocket"]},{"emoji":"🤣","aliases":["rofl"]},{"emoji":"🙄","aliases":["roll_eyes"]},{"emoji":"🧻","aliases":["roll_of_paper"]},{"emoji":"🎢","aliases":["roller_coaster"]},{"emoji":"🇷🇴","aliases":["romania"]},{"emoji":"🐓","aliases":["rooster"]},{"emoji":"🌹","aliases":["rose"]},{"emoji":"🏵️","aliases":["rosette"]},{"emoji":"🚨","aliases":["rotating_light"]},{"emoji":"📍","aliases":["round_pushpin"]},{"emoji":"🚣","aliases":["rowboat"]},{"emoji":"🚣♂️","aliases":["rowing_man"]},{"emoji":"🚣♀️","aliases":["rowing_woman"]},{"emoji":"🇷🇺","aliases":["ru"]},{"emoji":"🏉","aliases":["rugby_football"]},{"emoji":"🏃","aliases":["runner","running"]},{"emoji":"🏃♂️","aliases":["running_man"]},{"emoji":"🎽","aliases":["running_shirt_with_sash"]},{"emoji":"🏃♀️","aliases":["running_woman"]},{"emoji":"🇷🇼","aliases":["rwanda"]},{"emoji":"🈂️","aliases":["sa"]},{"emoji":"🧷","aliases":["safety_pin"]},{"emoji":"🦺","aliases":["safety_vest"]},{"emoji":"♐","aliases":["sagittarius"]},{"emoji":"🍶","aliases":["sake"]},{"emoji":"🧂","aliases":["salt"]},{"emoji":"🇼🇸","aliases":["samoa"]},{"emoji":"🇸🇲","aliases":["san_marino"]},{"emoji":"👡","aliases":["sandal"]},{"emoji":"🥪","aliases":["sandwich"]},{"emoji":"🎅","aliases":["santa"]},{"emoji":"🇸🇹","aliases":["sao_tome_principe"]},{"emoji":"🥻","aliases":["sari"]},{"emoji":"📡","aliases":["satellite"]},{"emoji":"🇸🇦","aliases":["saudi_arabia"]},{"emoji":"🧖♂️","aliases":["sauna_man"]},{"emoji":"🧖","aliases":["sauna_person"]},{"emoji":"🧖♀️","aliases":["sauna_woman"]},{"emoji":"🦕","aliases":["sauropod"]},{"emoji":"🎷","aliases":["saxophone"]},{"emoji":"🧣","aliases":["scarf"]},{"emoji":"🏫","aliases":["school"]},{"emoji":"🎒","aliases":["school_satchel"]},{"emoji":"🧑🔬","aliases":["scientist"]},{"emoji":"✂️","aliases":["scissors"]},{"emoji":"🦂","aliases":["scorpion"]},{"emoji":"♏","aliases":["scorpius"]},{"emoji":"🏴","aliases":["scotland"]},{"emoji":"😱","aliases":["scream"]},{"emoji":"🙀","aliases":["scream_cat"]},{"emoji":"📜","aliases":["scroll"]},{"emoji":"💺","aliases":["seat"]},{"emoji":"㊙️","aliases":["secret"]},{"emoji":"🙈","aliases":["see_no_evil"]},{"emoji":"🌱","aliases":["seedling"]},{"emoji":"🤳","aliases":["selfie"]},{"emoji":"🇸🇳","aliases":["senegal"]},{"emoji":"🇷🇸","aliases":["serbia"]},{"emoji":"🐕🦺","aliases":["service_dog"]},{"emoji":"7️⃣","aliases":["seven"]},{"emoji":"🇸🇨","aliases":["seychelles"]},{"emoji":"🥘","aliases":["shallow_pan_of_food"]},{"emoji":"☘️","aliases":["shamrock"]},{"emoji":"🦈","aliases":["shark"]},{"emoji":"🍧","aliases":["shaved_ice"]},{"emoji":"🐑","aliases":["sheep"]},{"emoji":"🐚","aliases":["shell"]},{"emoji":"🛡️","aliases":["shield"]},{"emoji":"⛩️","aliases":["shinto_shrine"]},{"emoji":"🚢","aliases":["ship"]},{"emoji":"👕","aliases":["shirt","tshirt"]},{"emoji":"🛍️","aliases":["shopping"]},{"emoji":"🛒","aliases":["shopping_cart"]},{"emoji":"🩳","aliases":["shorts"]},{"emoji":"🚿","aliases":["shower"]},{"emoji":"🦐","aliases":["shrimp"]},{"emoji":"🤷","aliases":["shrug"]},{"emoji":"🤫","aliases":["shushing_face"]},{"emoji":"🇸🇱","aliases":["sierra_leone"]},{"emoji":"📶","aliases":["signal_strength"]},{"emoji":"🇸🇬","aliases":["singapore"]},{"emoji":"🧑🎤","aliases":["singer"]},{"emoji":"🇸🇽","aliases":["sint_maarten"]},{"emoji":"6️⃣","aliases":["six"]},{"emoji":"🔯","aliases":["six_pointed_star"]},{"emoji":"🛹","aliases":["skateboard"]},{"emoji":"🎿","aliases":["ski"]},{"emoji":"⛷️","aliases":["skier"]},{"emoji":"💀","aliases":["skull"]},{"emoji":"☠️","aliases":["skull_and_crossbones"]},{"emoji":"🦨","aliases":["skunk"]},{"emoji":"🛷","aliases":["sled"]},{"emoji":"😴","aliases":["sleeping"]},{"emoji":"🛌","aliases":["sleeping_bed"]},{"emoji":"😪","aliases":["sleepy"]},{"emoji":"🙁","aliases":["slightly_frowning_face"]},{"emoji":"🙂","aliases":["slightly_smiling_face"]},{"emoji":"🎰","aliases":["slot_machine"]},{"emoji":"🦥","aliases":["sloth"]},{"emoji":"🇸🇰","aliases":["slovakia"]},{"emoji":"🇸🇮","aliases":["slovenia"]},{"emoji":"🛩️","aliases":["small_airplane"]},{"emoji":"🔹","aliases":["small_blue_diamond"]},{"emoji":"🔸","aliases":["small_orange_diamond"]},{"emoji":"🔺","aliases":["small_red_triangle"]},{"emoji":"🔻","aliases":["small_red_triangle_down"]},{"emoji":"😄","aliases":["smile"]},{"emoji":"😸","aliases":["smile_cat"]},{"emoji":"😃","aliases":["smiley"]},{"emoji":"😺","aliases":["smiley_cat"]},{"emoji":"🥰","aliases":["smiling_face_with_three_hearts"]},{"emoji":"😈","aliases":["smiling_imp"]},{"emoji":"😏","aliases":["smirk"]},{"emoji":"😼","aliases":["smirk_cat"]},{"emoji":"🚬","aliases":["smoking"]},{"emoji":"🐌","aliases":["snail"]},{"emoji":"🐍","aliases":["snake"]},{"emoji":"🤧","aliases":["sneezing_face"]},{"emoji":"🏂","aliases":["snowboarder"]},{"emoji":"❄️","aliases":["snowflake"]},{"emoji":"⛄","aliases":["snowman"]},{"emoji":"☃️","aliases":["snowman_with_snow"]},{"emoji":"🧼","aliases":["soap"]},{"emoji":"😭","aliases":["sob"]},{"emoji":"⚽","aliases":["soccer"]},{"emoji":"🧦","aliases":["socks"]},{"emoji":"🥎","aliases":["softball"]},{"emoji":"🇸🇧","aliases":["solomon_islands"]},{"emoji":"🇸🇴","aliases":["somalia"]},{"emoji":"🔜","aliases":["soon"]},{"emoji":"🆘","aliases":["sos"]},{"emoji":"🔉","aliases":["sound"]},{"emoji":"🇿🇦","aliases":["south_africa"]},{"emoji":"🇬🇸","aliases":["south_georgia_south_sandwich_islands"]},{"emoji":"🇸🇸","aliases":["south_sudan"]},{"emoji":"👾","aliases":["space_invader"]},{"emoji":"♠️","aliases":["spades"]},{"emoji":"🍝","aliases":["spaghetti"]},{"emoji":"❇️","aliases":["sparkle"]},{"emoji":"🎇","aliases":["sparkler"]},{"emoji":"✨","aliases":["sparkles"]},{"emoji":"💖","aliases":["sparkling_heart"]},{"emoji":"🙊","aliases":["speak_no_evil"]},{"emoji":"🔈","aliases":["speaker"]},{"emoji":"🗣️","aliases":["speaking_head"]},{"emoji":"💬","aliases":["speech_balloon"]},{"emoji":"🚤","aliases":["speedboat"]},{"emoji":"🕷️","aliases":["spider"]},{"emoji":"🕸️","aliases":["spider_web"]},{"emoji":"🗓️","aliases":["spiral_calendar"]},{"emoji":"🗒️","aliases":["spiral_notepad"]},{"emoji":"🧽","aliases":["sponge"]},{"emoji":"🥄","aliases":["spoon"]},{"emoji":"🦑","aliases":["squid"]},{"emoji":"🇱🇰","aliases":["sri_lanka"]},{"emoji":"🇧🇱","aliases":["st_barthelemy"]},{"emoji":"🇸🇭","aliases":["st_helena"]},{"emoji":"🇰🇳","aliases":["st_kitts_nevis"]},{"emoji":"🇱🇨","aliases":["st_lucia"]},{"emoji":"🇲🇫","aliases":["st_martin"]},{"emoji":"🇵🇲","aliases":["st_pierre_miquelon"]},{"emoji":"🇻🇨","aliases":["st_vincent_grenadines"]},{"emoji":"🏟️","aliases":["stadium"]},{"emoji":"🧍♂️","aliases":["standing_man"]},{"emoji":"🧍","aliases":["standing_person"]},{"emoji":"🧍♀️","aliases":["standing_woman"]},{"emoji":"⭐","aliases":["star"]},{"emoji":"🌟","aliases":["star2"]},{"emoji":"☪️","aliases":["star_and_crescent"]},{"emoji":"✡️","aliases":["star_of_david"]},{"emoji":"🤩","aliases":["star_struck"]},{"emoji":"🌠","aliases":["stars"]},{"emoji":"🚉","aliases":["station"]},{"emoji":"🗽","aliases":["statue_of_liberty"]},{"emoji":"🚂","aliases":["steam_locomotive"]},{"emoji":"🩺","aliases":["stethoscope"]},{"emoji":"🍲","aliases":["stew"]},{"emoji":"⏹️","aliases":["stop_button"]},{"emoji":"🛑","aliases":["stop_sign"]},{"emoji":"⏱️","aliases":["stopwatch"]},{"emoji":"📏","aliases":["straight_ruler"]},{"emoji":"🍓","aliases":["strawberry"]},{"emoji":"😛","aliases":["stuck_out_tongue"]},{"emoji":"😝","aliases":["stuck_out_tongue_closed_eyes"]},{"emoji":"😜","aliases":["stuck_out_tongue_winking_eye"]},{"emoji":"🧑🎓","aliases":["student"]},{"emoji":"🎙️","aliases":["studio_microphone"]},{"emoji":"🥙","aliases":["stuffed_flatbread"]},{"emoji":"🇸🇩","aliases":["sudan"]},{"emoji":"🌥️","aliases":["sun_behind_large_cloud"]},{"emoji":"🌦️","aliases":["sun_behind_rain_cloud"]},{"emoji":"🌤️","aliases":["sun_behind_small_cloud"]},{"emoji":"🌞","aliases":["sun_with_face"]},{"emoji":"🌻","aliases":["sunflower"]},{"emoji":"😎","aliases":["sunglasses"]},{"emoji":"☀️","aliases":["sunny"]},{"emoji":"🌅","aliases":["sunrise"]},{"emoji":"🌄","aliases":["sunrise_over_mountains"]},{"emoji":"🦸","aliases":["superhero"]},{"emoji":"🦸♂️","aliases":["superhero_man"]},{"emoji":"🦸♀️","aliases":["superhero_woman"]},{"emoji":"🦹","aliases":["supervillain"]},{"emoji":"🦹♂️","aliases":["supervillain_man"]},{"emoji":"🦹♀️","aliases":["supervillain_woman"]},{"emoji":"🏄","aliases":["surfer"]},{"emoji":"🏄♂️","aliases":["surfing_man"]},{"emoji":"🏄♀️","aliases":["surfing_woman"]},{"emoji":"🇸🇷","aliases":["suriname"]},{"emoji":"🍣","aliases":["sushi"]},{"emoji":"🚟","aliases":["suspension_railway"]},{"emoji":"🇸🇯","aliases":["svalbard_jan_mayen"]},{"emoji":"🦢","aliases":["swan"]},{"emoji":"🇸🇿","aliases":["swaziland"]},{"emoji":"😓","aliases":["sweat"]},{"emoji":"💦","aliases":["sweat_drops"]},{"emoji":"😅","aliases":["sweat_smile"]},{"emoji":"🇸🇪","aliases":["sweden"]},{"emoji":"🍠","aliases":["sweet_potato"]},{"emoji":"🩲","aliases":["swim_brief"]},{"emoji":"🏊","aliases":["swimmer"]},{"emoji":"🏊♂️","aliases":["swimming_man"]},{"emoji":"🏊♀️","aliases":["swimming_woman"]},{"emoji":"🇨🇭","aliases":["switzerland"]},{"emoji":"🔣","aliases":["symbols"]},{"emoji":"🕍","aliases":["synagogue"]},{"emoji":"🇸🇾","aliases":["syria"]},{"emoji":"💉","aliases":["syringe"]},{"emoji":"🦖","aliases":["t-rex"]},{"emoji":"🌮","aliases":["taco"]},{"emoji":"🎉","aliases":["tada","hooray"]},{"emoji":"🇹🇼","aliases":["taiwan"]},{"emoji":"🇹🇯","aliases":["tajikistan"]},{"emoji":"🥡","aliases":["takeout_box"]},{"emoji":"🎋","aliases":["tanabata_tree"]},{"emoji":"🍊","aliases":["tangerine","orange","mandarin"]},{"emoji":"🇹🇿","aliases":["tanzania"]},{"emoji":"♉","aliases":["taurus"]},{"emoji":"🚕","aliases":["taxi"]},{"emoji":"🍵","aliases":["tea"]},{"emoji":"🧑🏫","aliases":["teacher"]},{"emoji":"🧑💻","aliases":["technologist"]},{"emoji":"🧸","aliases":["teddy_bear"]},{"emoji":"📞","aliases":["telephone_receiver"]},{"emoji":"🔭","aliases":["telescope"]},{"emoji":"🎾","aliases":["tennis"]},{"emoji":"⛺","aliases":["tent"]},{"emoji":"🧪","aliases":["test_tube"]},{"emoji":"🇹🇭","aliases":["thailand"]},{"emoji":"🌡️","aliases":["thermometer"]},{"emoji":"🤔","aliases":["thinking"]},{"emoji":"💭","aliases":["thought_balloon"]},{"emoji":"🧵","aliases":["thread"]},{"emoji":"3️⃣","aliases":["three"]},{"emoji":"🎫","aliases":["ticket"]},{"emoji":"🎟️","aliases":["tickets"]},{"emoji":"🐯","aliases":["tiger"]},{"emoji":"🐅","aliases":["tiger2"]},{"emoji":"⏲️","aliases":["timer_clock"]},{"emoji":"🇹🇱","aliases":["timor_leste"]},{"emoji":"💁♂️","aliases":["tipping_hand_man","sassy_man"]},{"emoji":"💁","aliases":["tipping_hand_person","information_desk_person"]},{"emoji":"💁♀️","aliases":["tipping_hand_woman","sassy_woman"]},{"emoji":"😫","aliases":["tired_face"]},{"emoji":"™️","aliases":["tm"]},{"emoji":"🇹🇬","aliases":["togo"]},{"emoji":"🚽","aliases":["toilet"]},{"emoji":"🇹🇰","aliases":["tokelau"]},{"emoji":"🗼","aliases":["tokyo_tower"]},{"emoji":"🍅","aliases":["tomato"]},{"emoji":"🇹🇴","aliases":["tonga"]},{"emoji":"👅","aliases":["tongue"]},{"emoji":"🧰","aliases":["toolbox"]},{"emoji":"🦷","aliases":["tooth"]},{"emoji":"🔝","aliases":["top"]},{"emoji":"🎩","aliases":["tophat"]},{"emoji":"🌪️","aliases":["tornado"]},{"emoji":"🇹🇷","aliases":["tr"]},{"emoji":"🖲️","aliases":["trackball"]},{"emoji":"🚜","aliases":["tractor"]},{"emoji":"🚥","aliases":["traffic_light"]},{"emoji":"🚋","aliases":["train"]},{"emoji":"🚆","aliases":["train2"]},{"emoji":"🚊","aliases":["tram"]},{"emoji":"🚩","aliases":["triangular_flag_on_post"]},{"emoji":"📐","aliases":["triangular_ruler"]},{"emoji":"🔱","aliases":["trident"]},{"emoji":"🇹🇹","aliases":["trinidad_tobago"]},{"emoji":"🇹🇦","aliases":["tristan_da_cunha"]},{"emoji":"😤","aliases":["triumph"]},{"emoji":"🚎","aliases":["trolleybus"]},{"emoji":"🏆","aliases":["trophy"]},{"emoji":"🍹","aliases":["tropical_drink"]},{"emoji":"🐠","aliases":["tropical_fish"]},{"emoji":"🚚","aliases":["truck"]},{"emoji":"🎺","aliases":["trumpet"]},{"emoji":"🌷","aliases":["tulip"]},{"emoji":"🥃","aliases":["tumbler_glass"]},{"emoji":"🇹🇳","aliases":["tunisia"]},{"emoji":"🦃","aliases":["turkey"]},{"emoji":"🇹🇲","aliases":["turkmenistan"]},{"emoji":"🇹🇨","aliases":["turks_caicos_islands"]},{"emoji":"🐢","aliases":["turtle"]},{"emoji":"🇹🇻","aliases":["tuvalu"]},{"emoji":"📺","aliases":["tv"]},{"emoji":"🔀","aliases":["twisted_rightwards_arrows"]},{"emoji":"2️⃣","aliases":["two"]},{"emoji":"💕","aliases":["two_hearts"]},{"emoji":"👬","aliases":["two_men_holding_hands"]},{"emoji":"👭","aliases":["two_women_holding_hands"]},{"emoji":"🈹","aliases":["u5272"]},{"emoji":"🈴","aliases":["u5408"]},{"emoji":"🈺","aliases":["u55b6"]},{"emoji":"🈯","aliases":["u6307"]},{"emoji":"🈷️","aliases":["u6708"]},{"emoji":"🈶","aliases":["u6709"]},{"emoji":"🈵","aliases":["u6e80"]},{"emoji":"🈚","aliases":["u7121"]},{"emoji":"🈸","aliases":["u7533"]},{"emoji":"🈲","aliases":["u7981"]},{"emoji":"🈳","aliases":["u7a7a"]},{"emoji":"🇺🇬","aliases":["uganda"]},{"emoji":"🇺🇦","aliases":["ukraine"]},{"emoji":"☔","aliases":["umbrella"]},{"emoji":"😒","aliases":["unamused"]},{"emoji":"🔞","aliases":["underage"]},{"emoji":"🦄","aliases":["unicorn"]},{"emoji":"🇦🇪","aliases":["united_arab_emirates"]},{"emoji":"🇺🇳","aliases":["united_nations"]},{"emoji":"🔓","aliases":["unlock"]},{"emoji":"🆙","aliases":["up"]},{"emoji":"🙃","aliases":["upside_down_face"]},{"emoji":"🇺🇾","aliases":["uruguay"]},{"emoji":"🇺🇸","aliases":["us"]},{"emoji":"🇺🇲","aliases":["us_outlying_islands"]},{"emoji":"🇻🇮","aliases":["us_virgin_islands"]},{"emoji":"🇺🇿","aliases":["uzbekistan"]},{"emoji":"✌️","aliases":["v"]},{"emoji":"🧛","aliases":["vampire"]},{"emoji":"🧛♂️","aliases":["vampire_man"]},{"emoji":"🧛♀️","aliases":["vampire_woman"]},{"emoji":"🇻🇺","aliases":["vanuatu"]},{"emoji":"🇻🇦","aliases":["vatican_city"]},{"emoji":"🇻🇪","aliases":["venezuela"]},{"emoji":"🚦","aliases":["vertical_traffic_light"]},{"emoji":"📼","aliases":["vhs"]},{"emoji":"📳","aliases":["vibration_mode"]},{"emoji":"📹","aliases":["video_camera"]},{"emoji":"🎮","aliases":["video_game"]},{"emoji":"🇻🇳","aliases":["vietnam"]},{"emoji":"🎻","aliases":["violin"]},{"emoji":"♍","aliases":["virgo"]},{"emoji":"🌋","aliases":["volcano"]},{"emoji":"🏐","aliases":["volleyball"]},{"emoji":"🤮","aliases":["vomiting_face"]},{"emoji":"🆚","aliases":["vs"]},{"emoji":"🖖","aliases":["vulcan_salute"]},{"emoji":"🧇","aliases":["waffle"]},{"emoji":"🏴","aliases":["wales"]},{"emoji":"🚶","aliases":["walking"]},{"emoji":"🚶♂️","aliases":["walking_man"]},{"emoji":"🚶♀️","aliases":["walking_woman"]},{"emoji":"🇼🇫","aliases":["wallis_futuna"]},{"emoji":"🌘","aliases":["waning_crescent_moon"]},{"emoji":"🌖","aliases":["waning_gibbous_moon"]},{"emoji":"⚠️","aliases":["warning"]},{"emoji":"🗑️","aliases":["wastebasket"]},{"emoji":"⌚","aliases":["watch"]},{"emoji":"🐃","aliases":["water_buffalo"]},{"emoji":"🤽","aliases":["water_polo"]},{"emoji":"🍉","aliases":["watermelon"]},{"emoji":"👋","aliases":["wave"]},{"emoji":"〰️","aliases":["wavy_dash"]},{"emoji":"🌒","aliases":["waxing_crescent_moon"]},{"emoji":"🚾","aliases":["wc"]},{"emoji":"😩","aliases":["weary"]},{"emoji":"💒","aliases":["wedding"]},{"emoji":"🏋️","aliases":["weight_lifting"]},{"emoji":"🏋️♂️","aliases":["weight_lifting_man"]},{"emoji":"🏋️♀️","aliases":["weight_lifting_woman"]},{"emoji":"🇪🇭","aliases":["western_sahara"]},{"emoji":"🐳","aliases":["whale"]},{"emoji":"🐋","aliases":["whale2"]},{"emoji":"☸️","aliases":["wheel_of_dharma"]},{"emoji":"♿","aliases":["wheelchair"]},{"emoji":"✅","aliases":["white_check_mark"]},{"emoji":"⚪","aliases":["white_circle"]},{"emoji":"🏳️","aliases":["white_flag"]},{"emoji":"💮","aliases":["white_flower"]},{"emoji":"👨🦳","aliases":["white_haired_man"]},{"emoji":"👩🦳","aliases":["white_haired_woman"]},{"emoji":"🤍","aliases":["white_heart"]},{"emoji":"⬜","aliases":["white_large_square"]},{"emoji":"◽","aliases":["white_medium_small_square"]},{"emoji":"◻️","aliases":["white_medium_square"]},{"emoji":"▫️","aliases":["white_small_square"]},{"emoji":"🔳","aliases":["white_square_button"]},{"emoji":"🥀","aliases":["wilted_flower"]},{"emoji":"🎐","aliases":["wind_chime"]},{"emoji":"🌬️","aliases":["wind_face"]},{"emoji":"🍷","aliases":["wine_glass"]},{"emoji":"😉","aliases":["wink"]},{"emoji":"🐺","aliases":["wolf"]},{"emoji":"👩","aliases":["woman"]},{"emoji":"👩🎨","aliases":["woman_artist"]},{"emoji":"👩🚀","aliases":["woman_astronaut"]},{"emoji":"🤸♀️","aliases":["woman_cartwheeling"]},{"emoji":"👩🍳","aliases":["woman_cook"]},{"emoji":"💃","aliases":["woman_dancing","dancer"]},{"emoji":"🤦♀️","aliases":["woman_facepalming"]},{"emoji":"👩🏭","aliases":["woman_factory_worker"]},{"emoji":"👩🌾","aliases":["woman_farmer"]},{"emoji":"👩🚒","aliases":["woman_firefighter"]},{"emoji":"👩⚕️","aliases":["woman_health_worker"]},{"emoji":"👩🦽","aliases":["woman_in_manual_wheelchair"]},{"emoji":"👩🦼","aliases":["woman_in_motorized_wheelchair"]},{"emoji":"👩⚖️","aliases":["woman_judge"]},{"emoji":"🤹♀️","aliases":["woman_juggling"]},{"emoji":"👩🔧","aliases":["woman_mechanic"]},{"emoji":"👩💼","aliases":["woman_office_worker"]},{"emoji":"👩✈️","aliases":["woman_pilot"]},{"emoji":"🤾♀️","aliases":["woman_playing_handball"]},{"emoji":"🤽♀️","aliases":["woman_playing_water_polo"]},{"emoji":"👩🔬","aliases":["woman_scientist"]},{"emoji":"🤷♀️","aliases":["woman_shrugging"]},{"emoji":"👩🎤","aliases":["woman_singer"]},{"emoji":"👩🎓","aliases":["woman_student"]},{"emoji":"👩🏫","aliases":["woman_teacher"]},{"emoji":"👩💻","aliases":["woman_technologist"]},{"emoji":"🧕","aliases":["woman_with_headscarf"]},{"emoji":"👩🦯","aliases":["woman_with_probing_cane"]},{"emoji":"👳♀️","aliases":["woman_with_turban"]},{"emoji":"👚","aliases":["womans_clothes"]},{"emoji":"👒","aliases":["womans_hat"]},{"emoji":"🤼♀️","aliases":["women_wrestling"]},{"emoji":"🚺","aliases":["womens"]},{"emoji":"🥴","aliases":["woozy_face"]},{"emoji":"🗺️","aliases":["world_map"]},{"emoji":"😟","aliases":["worried"]},{"emoji":"🔧","aliases":["wrench"]},{"emoji":"🤼","aliases":["wrestling"]},{"emoji":"✍️","aliases":["writing_hand"]},{"emoji":"❌","aliases":["x"]},{"emoji":"🧶","aliases":["yarn"]},{"emoji":"🥱","aliases":["yawning_face"]},{"emoji":"🟡","aliases":["yellow_circle"]},{"emoji":"💛","aliases":["yellow_heart"]},{"emoji":"🟨","aliases":["yellow_square"]},{"emoji":"🇾🇪","aliases":["yemen"]},{"emoji":"💴","aliases":["yen"]},{"emoji":"☯️","aliases":["yin_yang"]},{"emoji":"🪀","aliases":["yo_yo"]},{"emoji":"😋","aliases":["yum"]},{"emoji":"🇿🇲","aliases":["zambia"]},{"emoji":"🤪","aliases":["zany_face"]},{"emoji":"⚡","aliases":["zap"]},{"emoji":"🦓","aliases":["zebra"]},{"emoji":"0️⃣","aliases":["zero"]},{"emoji":"🇿🇼","aliases":["zimbabwe"]},{"emoji":"🤐","aliases":["zipper_mouth_face"]},{"emoji":"🧟","aliases":["zombie"]},{"emoji":"🧟♂️","aliases":["zombie_man"]},{"emoji":"🧟♀️","aliases":["zombie_woman"]},{"emoji":"💤","aliases":["zzz"]}]
\ No newline at end of file
+[{"emoji":"👍","aliases":["+1","thumbsup"]},{"emoji":"👎","aliases":["-1","thumbsdown"]},{"emoji":"💯","aliases":["100"]},{"emoji":"🔢","aliases":["1234"]},{"emoji":"🥇","aliases":["1st_place_medal"]},{"emoji":"🥈","aliases":["2nd_place_medal"]},{"emoji":"🥉","aliases":["3rd_place_medal"]},{"emoji":"🎱","aliases":["8ball"]},{"emoji":"🅰️","aliases":["a"]},{"emoji":"🆎","aliases":["ab"]},{"emoji":"🧮","aliases":["abacus"]},{"emoji":"🔤","aliases":["abc"]},{"emoji":"🔡","aliases":["abcd"]},{"emoji":"🉑","aliases":["accept"]},{"emoji":"🪗","aliases":["accordion"]},{"emoji":"🩹","aliases":["adhesive_bandage"]},{"emoji":"🧑","aliases":["adult"]},{"emoji":"🚡","aliases":["aerial_tramway"]},{"emoji":"🇦🇫","aliases":["afghanistan"]},{"emoji":"✈️","aliases":["airplane"]},{"emoji":"🇦🇽","aliases":["aland_islands"]},{"emoji":"⏰","aliases":["alarm_clock"]},{"emoji":"🇦🇱","aliases":["albania"]},{"emoji":"⚗️","aliases":["alembic"]},{"emoji":"🇩🇿","aliases":["algeria"]},{"emoji":"👽","aliases":["alien"]},{"emoji":"🚑","aliases":["ambulance"]},{"emoji":"🇦🇸","aliases":["american_samoa"]},{"emoji":"🏺","aliases":["amphora"]},{"emoji":"🫀","aliases":["anatomical_heart"]},{"emoji":"⚓","aliases":["anchor"]},{"emoji":"🇦🇩","aliases":["andorra"]},{"emoji":"👼","aliases":["angel"]},{"emoji":"💢","aliases":["anger"]},{"emoji":"🇦🇴","aliases":["angola"]},{"emoji":"😠","aliases":["angry"]},{"emoji":"🇦🇮","aliases":["anguilla"]},{"emoji":"😧","aliases":["anguished"]},{"emoji":"🐜","aliases":["ant"]},{"emoji":"🇦🇶","aliases":["antarctica"]},{"emoji":"🇦🇬","aliases":["antigua_barbuda"]},{"emoji":"🍎","aliases":["apple"]},{"emoji":"♒","aliases":["aquarius"]},{"emoji":"🇦🇷","aliases":["argentina"]},{"emoji":"♈","aliases":["aries"]},{"emoji":"🇦🇲","aliases":["armenia"]},{"emoji":"◀️","aliases":["arrow_backward"]},{"emoji":"⏬","aliases":["arrow_double_down"]},{"emoji":"⏫","aliases":["arrow_double_up"]},{"emoji":"⬇️","aliases":["arrow_down"]},{"emoji":"🔽","aliases":["arrow_down_small"]},{"emoji":"▶️","aliases":["arrow_forward"]},{"emoji":"⤵️","aliases":["arrow_heading_down"]},{"emoji":"⤴️","aliases":["arrow_heading_up"]},{"emoji":"⬅️","aliases":["arrow_left"]},{"emoji":"↙️","aliases":["arrow_lower_left"]},{"emoji":"↘️","aliases":["arrow_lower_right"]},{"emoji":"➡️","aliases":["arrow_right"]},{"emoji":"↪️","aliases":["arrow_right_hook"]},{"emoji":"⬆️","aliases":["arrow_up"]},{"emoji":"↕️","aliases":["arrow_up_down"]},{"emoji":"🔼","aliases":["arrow_up_small"]},{"emoji":"↖️","aliases":["arrow_upper_left"]},{"emoji":"↗️","aliases":["arrow_upper_right"]},{"emoji":"🔃","aliases":["arrows_clockwise"]},{"emoji":"🔄","aliases":["arrows_counterclockwise"]},{"emoji":"🎨","aliases":["art"]},{"emoji":"🚛","aliases":["articulated_lorry"]},{"emoji":"🛰️","aliases":["artificial_satellite"]},{"emoji":"🧑🎨","aliases":["artist"]},{"emoji":"🇦🇼","aliases":["aruba"]},{"emoji":"🇦🇨","aliases":["ascension_island"]},{"emoji":"*️⃣","aliases":["asterisk"]},{"emoji":"😲","aliases":["astonished"]},{"emoji":"🧑🚀","aliases":["astronaut"]},{"emoji":"👟","aliases":["athletic_shoe"]},{"emoji":"🏧","aliases":["atm"]},{"emoji":"⚛️","aliases":["atom_symbol"]},{"emoji":"🇦🇺","aliases":["australia"]},{"emoji":"🇦🇹","aliases":["austria"]},{"emoji":"🛺","aliases":["auto_rickshaw"]},{"emoji":"🥑","aliases":["avocado"]},{"emoji":"🪓","aliases":["axe"]},{"emoji":"🇦🇿","aliases":["azerbaijan"]},{"emoji":"🅱️","aliases":["b"]},{"emoji":"👶","aliases":["baby"]},{"emoji":"🍼","aliases":["baby_bottle"]},{"emoji":"🐤","aliases":["baby_chick"]},{"emoji":"🚼","aliases":["baby_symbol"]},{"emoji":"🔙","aliases":["back"]},{"emoji":"🥓","aliases":["bacon"]},{"emoji":"🦡","aliases":["badger"]},{"emoji":"🏸","aliases":["badminton"]},{"emoji":"🥯","aliases":["bagel"]},{"emoji":"🛄","aliases":["baggage_claim"]},{"emoji":"🥖","aliases":["baguette_bread"]},{"emoji":"🇧🇸","aliases":["bahamas"]},{"emoji":"🇧🇭","aliases":["bahrain"]},{"emoji":"⚖️","aliases":["balance_scale"]},{"emoji":"👨🦲","aliases":["bald_man"]},{"emoji":"👩🦲","aliases":["bald_woman"]},{"emoji":"🩰","aliases":["ballet_shoes"]},{"emoji":"🎈","aliases":["balloon"]},{"emoji":"🗳️","aliases":["ballot_box"]},{"emoji":"☑️","aliases":["ballot_box_with_check"]},{"emoji":"🎍","aliases":["bamboo"]},{"emoji":"🍌","aliases":["banana"]},{"emoji":"‼️","aliases":["bangbang"]},{"emoji":"🇧🇩","aliases":["bangladesh"]},{"emoji":"🪕","aliases":["banjo"]},{"emoji":"🏦","aliases":["bank"]},{"emoji":"📊","aliases":["bar_chart"]},{"emoji":"🇧🇧","aliases":["barbados"]},{"emoji":"💈","aliases":["barber"]},{"emoji":"⚾","aliases":["baseball"]},{"emoji":"🧺","aliases":["basket"]},{"emoji":"🏀","aliases":["basketball"]},{"emoji":"🦇","aliases":["bat"]},{"emoji":"🛀","aliases":["bath"]},{"emoji":"🛁","aliases":["bathtub"]},{"emoji":"🔋","aliases":["battery"]},{"emoji":"🏖️","aliases":["beach_umbrella"]},{"emoji":"🫘","aliases":["beans"]},{"emoji":"🐻","aliases":["bear"]},{"emoji":"🧔","aliases":["bearded_person"]},{"emoji":"🦫","aliases":["beaver"]},{"emoji":"🛏️","aliases":["bed"]},{"emoji":"🐝","aliases":["bee","honeybee"]},{"emoji":"🍺","aliases":["beer"]},{"emoji":"🍻","aliases":["beers"]},{"emoji":"🪲","aliases":["beetle"]},{"emoji":"🔰","aliases":["beginner"]},{"emoji":"🇧🇾","aliases":["belarus"]},{"emoji":"🇧🇪","aliases":["belgium"]},{"emoji":"🇧🇿","aliases":["belize"]},{"emoji":"🔔","aliases":["bell"]},{"emoji":"🫑","aliases":["bell_pepper"]},{"emoji":"🛎️","aliases":["bellhop_bell"]},{"emoji":"🇧🇯","aliases":["benin"]},{"emoji":"🍱","aliases":["bento"]},{"emoji":"🇧🇲","aliases":["bermuda"]},{"emoji":"🧃","aliases":["beverage_box"]},{"emoji":"🇧🇹","aliases":["bhutan"]},{"emoji":"🚴","aliases":["bicyclist"]},{"emoji":"🚲","aliases":["bike"]},{"emoji":"🚴♂️","aliases":["biking_man"]},{"emoji":"🚴♀️","aliases":["biking_woman"]},{"emoji":"👙","aliases":["bikini"]},{"emoji":"🧢","aliases":["billed_cap"]},{"emoji":"☣️","aliases":["biohazard"]},{"emoji":"🐦","aliases":["bird"]},{"emoji":"🎂","aliases":["birthday"]},{"emoji":"🦬","aliases":["bison"]},{"emoji":"🫦","aliases":["biting_lip"]},{"emoji":"🐈⬛","aliases":["black_cat"]},{"emoji":"⚫","aliases":["black_circle"]},{"emoji":"🏴","aliases":["black_flag"]},{"emoji":"🖤","aliases":["black_heart"]},{"emoji":"🃏","aliases":["black_joker"]},{"emoji":"⬛","aliases":["black_large_square"]},{"emoji":"◾","aliases":["black_medium_small_square"]},{"emoji":"◼️","aliases":["black_medium_square"]},{"emoji":"✒️","aliases":["black_nib"]},{"emoji":"▪️","aliases":["black_small_square"]},{"emoji":"🔲","aliases":["black_square_button"]},{"emoji":"👱♂️","aliases":["blond_haired_man"]},{"emoji":"👱","aliases":["blond_haired_person"]},{"emoji":"👱♀️","aliases":["blond_haired_woman","blonde_woman"]},{"emoji":"🌼","aliases":["blossom"]},{"emoji":"🐡","aliases":["blowfish"]},{"emoji":"📘","aliases":["blue_book"]},{"emoji":"🚙","aliases":["blue_car"]},{"emoji":"💙","aliases":["blue_heart"]},{"emoji":"🟦","aliases":["blue_square"]},{"emoji":"🫐","aliases":["blueberries"]},{"emoji":"😊","aliases":["blush"]},{"emoji":"🐗","aliases":["boar"]},{"emoji":"⛵","aliases":["boat","sailboat"]},{"emoji":"🇧🇴","aliases":["bolivia"]},{"emoji":"💣","aliases":["bomb"]},{"emoji":"🦴","aliases":["bone"]},{"emoji":"📖","aliases":["book","open_book"]},{"emoji":"🔖","aliases":["bookmark"]},{"emoji":"📑","aliases":["bookmark_tabs"]},{"emoji":"📚","aliases":["books"]},{"emoji":"💥","aliases":["boom","collision"]},{"emoji":"🪃","aliases":["boomerang"]},{"emoji":"👢","aliases":["boot"]},{"emoji":"🇧🇦","aliases":["bosnia_herzegovina"]},{"emoji":"🇧🇼","aliases":["botswana"]},{"emoji":"⛹️♂️","aliases":["bouncing_ball_man","basketball_man"]},{"emoji":"⛹️","aliases":["bouncing_ball_person"]},{"emoji":"⛹️♀️","aliases":["bouncing_ball_woman","basketball_woman"]},{"emoji":"💐","aliases":["bouquet"]},{"emoji":"🇧🇻","aliases":["bouvet_island"]},{"emoji":"🙇","aliases":["bow"]},{"emoji":"🏹","aliases":["bow_and_arrow"]},{"emoji":"🙇♂️","aliases":["bowing_man"]},{"emoji":"🙇♀️","aliases":["bowing_woman"]},{"emoji":"🥣","aliases":["bowl_with_spoon"]},{"emoji":"🎳","aliases":["bowling"]},{"emoji":"🥊","aliases":["boxing_glove"]},{"emoji":"👦","aliases":["boy"]},{"emoji":"🧠","aliases":["brain"]},{"emoji":"🇧🇷","aliases":["brazil"]},{"emoji":"🍞","aliases":["bread"]},{"emoji":"🤱","aliases":["breast_feeding"]},{"emoji":"🧱","aliases":["bricks"]},{"emoji":"🌉","aliases":["bridge_at_night"]},{"emoji":"💼","aliases":["briefcase"]},{"emoji":"🇮🇴","aliases":["british_indian_ocean_territory"]},{"emoji":"🇻🇬","aliases":["british_virgin_islands"]},{"emoji":"🥦","aliases":["broccoli"]},{"emoji":"💔","aliases":["broken_heart"]},{"emoji":"🧹","aliases":["broom"]},{"emoji":"🟤","aliases":["brown_circle"]},{"emoji":"🤎","aliases":["brown_heart"]},{"emoji":"🟫","aliases":["brown_square"]},{"emoji":"🇧🇳","aliases":["brunei"]},{"emoji":"🧋","aliases":["bubble_tea"]},{"emoji":"🫧","aliases":["bubbles"]},{"emoji":"🪣","aliases":["bucket"]},{"emoji":"🐛","aliases":["bug"]},{"emoji":"🏗️","aliases":["building_construction"]},{"emoji":"💡","aliases":["bulb"]},{"emoji":"🇧🇬","aliases":["bulgaria"]},{"emoji":"🚅","aliases":["bullettrain_front"]},{"emoji":"🚄","aliases":["bullettrain_side"]},{"emoji":"🇧🇫","aliases":["burkina_faso"]},{"emoji":"🌯","aliases":["burrito"]},{"emoji":"🇧🇮","aliases":["burundi"]},{"emoji":"🚌","aliases":["bus"]},{"emoji":"🕴️","aliases":["business_suit_levitating"]},{"emoji":"🚏","aliases":["busstop"]},{"emoji":"👤","aliases":["bust_in_silhouette"]},{"emoji":"👥","aliases":["busts_in_silhouette"]},{"emoji":"🧈","aliases":["butter"]},{"emoji":"🦋","aliases":["butterfly"]},{"emoji":"🌵","aliases":["cactus"]},{"emoji":"🍰","aliases":["cake"]},{"emoji":"📆","aliases":["calendar"]},{"emoji":"🤙","aliases":["call_me_hand"]},{"emoji":"📲","aliases":["calling"]},{"emoji":"🇰🇭","aliases":["cambodia"]},{"emoji":"🐫","aliases":["camel"]},{"emoji":"📷","aliases":["camera"]},{"emoji":"📸","aliases":["camera_flash"]},{"emoji":"🇨🇲","aliases":["cameroon"]},{"emoji":"🏕️","aliases":["camping"]},{"emoji":"🇨🇦","aliases":["canada"]},{"emoji":"🇮🇨","aliases":["canary_islands"]},{"emoji":"♋","aliases":["cancer"]},{"emoji":"🕯️","aliases":["candle"]},{"emoji":"🍬","aliases":["candy"]},{"emoji":"🥫","aliases":["canned_food"]},{"emoji":"🛶","aliases":["canoe"]},{"emoji":"🇨🇻","aliases":["cape_verde"]},{"emoji":"🔠","aliases":["capital_abcd"]},{"emoji":"♑","aliases":["capricorn"]},{"emoji":"🚗","aliases":["car","red_car"]},{"emoji":"🗃️","aliases":["card_file_box"]},{"emoji":"📇","aliases":["card_index"]},{"emoji":"🗂️","aliases":["card_index_dividers"]},{"emoji":"🇧🇶","aliases":["caribbean_netherlands"]},{"emoji":"🎠","aliases":["carousel_horse"]},{"emoji":"🪚","aliases":["carpentry_saw"]},{"emoji":"🥕","aliases":["carrot"]},{"emoji":"🤸","aliases":["cartwheeling"]},{"emoji":"🐱","aliases":["cat"]},{"emoji":"🐈","aliases":["cat2"]},{"emoji":"🇰🇾","aliases":["cayman_islands"]},{"emoji":"💿","aliases":["cd"]},{"emoji":"🇨🇫","aliases":["central_african_republic"]},{"emoji":"🇪🇦","aliases":["ceuta_melilla"]},{"emoji":"🇹🇩","aliases":["chad"]},{"emoji":"⛓️","aliases":["chains"]},{"emoji":"🪑","aliases":["chair"]},{"emoji":"🍾","aliases":["champagne"]},{"emoji":"💹","aliases":["chart"]},{"emoji":"📉","aliases":["chart_with_downwards_trend"]},{"emoji":"📈","aliases":["chart_with_upwards_trend"]},{"emoji":"🏁","aliases":["checkered_flag"]},{"emoji":"🧀","aliases":["cheese"]},{"emoji":"🍒","aliases":["cherries"]},{"emoji":"🌸","aliases":["cherry_blossom"]},{"emoji":"♟️","aliases":["chess_pawn"]},{"emoji":"🌰","aliases":["chestnut"]},{"emoji":"🐔","aliases":["chicken"]},{"emoji":"🧒","aliases":["child"]},{"emoji":"🚸","aliases":["children_crossing"]},{"emoji":"🇨🇱","aliases":["chile"]},{"emoji":"🐿️","aliases":["chipmunk"]},{"emoji":"🍫","aliases":["chocolate_bar"]},{"emoji":"🥢","aliases":["chopsticks"]},{"emoji":"🇨🇽","aliases":["christmas_island"]},{"emoji":"🎄","aliases":["christmas_tree"]},{"emoji":"⛪","aliases":["church"]},{"emoji":"🎦","aliases":["cinema"]},{"emoji":"🎪","aliases":["circus_tent"]},{"emoji":"🌇","aliases":["city_sunrise"]},{"emoji":"🌆","aliases":["city_sunset"]},{"emoji":"🏙️","aliases":["cityscape"]},{"emoji":"🆑","aliases":["cl"]},{"emoji":"🗜️","aliases":["clamp"]},{"emoji":"👏","aliases":["clap"]},{"emoji":"🎬","aliases":["clapper"]},{"emoji":"🏛️","aliases":["classical_building"]},{"emoji":"🧗","aliases":["climbing"]},{"emoji":"🧗♂️","aliases":["climbing_man"]},{"emoji":"🧗♀️","aliases":["climbing_woman"]},{"emoji":"🥂","aliases":["clinking_glasses"]},{"emoji":"📋","aliases":["clipboard"]},{"emoji":"🇨🇵","aliases":["clipperton_island"]},{"emoji":"🕐","aliases":["clock1"]},{"emoji":"🕙","aliases":["clock10"]},{"emoji":"🕥","aliases":["clock1030"]},{"emoji":"🕚","aliases":["clock11"]},{"emoji":"🕦","aliases":["clock1130"]},{"emoji":"🕛","aliases":["clock12"]},{"emoji":"🕧","aliases":["clock1230"]},{"emoji":"🕜","aliases":["clock130"]},{"emoji":"🕑","aliases":["clock2"]},{"emoji":"🕝","aliases":["clock230"]},{"emoji":"🕒","aliases":["clock3"]},{"emoji":"🕞","aliases":["clock330"]},{"emoji":"🕓","aliases":["clock4"]},{"emoji":"🕟","aliases":["clock430"]},{"emoji":"🕔","aliases":["clock5"]},{"emoji":"🕠","aliases":["clock530"]},{"emoji":"🕕","aliases":["clock6"]},{"emoji":"🕡","aliases":["clock630"]},{"emoji":"🕖","aliases":["clock7"]},{"emoji":"🕢","aliases":["clock730"]},{"emoji":"🕗","aliases":["clock8"]},{"emoji":"🕣","aliases":["clock830"]},{"emoji":"🕘","aliases":["clock9"]},{"emoji":"🕤","aliases":["clock930"]},{"emoji":"📕","aliases":["closed_book"]},{"emoji":"🔐","aliases":["closed_lock_with_key"]},{"emoji":"🌂","aliases":["closed_umbrella"]},{"emoji":"☁️","aliases":["cloud"]},{"emoji":"🌩️","aliases":["cloud_with_lightning"]},{"emoji":"⛈️","aliases":["cloud_with_lightning_and_rain"]},{"emoji":"🌧️","aliases":["cloud_with_rain"]},{"emoji":"🌨️","aliases":["cloud_with_snow"]},{"emoji":"🤡","aliases":["clown_face"]},{"emoji":"♣️","aliases":["clubs"]},{"emoji":"🇨🇳","aliases":["cn"]},{"emoji":"🧥","aliases":["coat"]},{"emoji":"🪳","aliases":["cockroach"]},{"emoji":"🍸","aliases":["cocktail"]},{"emoji":"🥥","aliases":["coconut"]},{"emoji":"🇨🇨","aliases":["cocos_islands"]},{"emoji":"☕","aliases":["coffee"]},{"emoji":"⚰️","aliases":["coffin"]},{"emoji":"🪙","aliases":["coin"]},{"emoji":"🥶","aliases":["cold_face"]},{"emoji":"😰","aliases":["cold_sweat"]},{"emoji":"🇨🇴","aliases":["colombia"]},{"emoji":"☄️","aliases":["comet"]},{"emoji":"🇰🇲","aliases":["comoros"]},{"emoji":"🧭","aliases":["compass"]},{"emoji":"💻","aliases":["computer"]},{"emoji":"🖱️","aliases":["computer_mouse"]},{"emoji":"🎊","aliases":["confetti_ball"]},{"emoji":"😖","aliases":["confounded"]},{"emoji":"😕","aliases":["confused"]},{"emoji":"🇨🇬","aliases":["congo_brazzaville"]},{"emoji":"🇨🇩","aliases":["congo_kinshasa"]},{"emoji":"㊗️","aliases":["congratulations"]},{"emoji":"🚧","aliases":["construction"]},{"emoji":"👷","aliases":["construction_worker"]},{"emoji":"👷♂️","aliases":["construction_worker_man"]},{"emoji":"👷♀️","aliases":["construction_worker_woman"]},{"emoji":"🎛️","aliases":["control_knobs"]},{"emoji":"🏪","aliases":["convenience_store"]},{"emoji":"🧑🍳","aliases":["cook"]},{"emoji":"🇨🇰","aliases":["cook_islands"]},{"emoji":"🍪","aliases":["cookie"]},{"emoji":"🆒","aliases":["cool"]},{"emoji":"©️","aliases":["copyright"]},{"emoji":"🪸","aliases":["coral"]},{"emoji":"🌽","aliases":["corn"]},{"emoji":"🇨🇷","aliases":["costa_rica"]},{"emoji":"🇨🇮","aliases":["cote_divoire"]},{"emoji":"🛋️","aliases":["couch_and_lamp"]},{"emoji":"👫","aliases":["couple"]},{"emoji":"💑","aliases":["couple_with_heart"]},{"emoji":"👨❤️👨","aliases":["couple_with_heart_man_man"]},{"emoji":"👩❤️👨","aliases":["couple_with_heart_woman_man"]},{"emoji":"👩❤️👩","aliases":["couple_with_heart_woman_woman"]},{"emoji":"💏","aliases":["couplekiss"]},{"emoji":"👨❤️💋👨","aliases":["couplekiss_man_man"]},{"emoji":"👩❤️💋👨","aliases":["couplekiss_man_woman"]},{"emoji":"👩❤️💋👩","aliases":["couplekiss_woman_woman"]},{"emoji":"🐮","aliases":["cow"]},{"emoji":"🐄","aliases":["cow2"]},{"emoji":"🤠","aliases":["cowboy_hat_face"]},{"emoji":"🦀","aliases":["crab"]},{"emoji":"🖍️","aliases":["crayon"]},{"emoji":"💳","aliases":["credit_card"]},{"emoji":"🌙","aliases":["crescent_moon"]},{"emoji":"🦗","aliases":["cricket"]},{"emoji":"🏏","aliases":["cricket_game"]},{"emoji":"🇭🇷","aliases":["croatia"]},{"emoji":"🐊","aliases":["crocodile"]},{"emoji":"🥐","aliases":["croissant"]},{"emoji":"🤞","aliases":["crossed_fingers"]},{"emoji":"🎌","aliases":["crossed_flags"]},{"emoji":"⚔️","aliases":["crossed_swords"]},{"emoji":"👑","aliases":["crown"]},{"emoji":"🩼","aliases":["crutch"]},{"emoji":"😢","aliases":["cry"]},{"emoji":"😿","aliases":["crying_cat_face"]},{"emoji":"🔮","aliases":["crystal_ball"]},{"emoji":"🇨🇺","aliases":["cuba"]},{"emoji":"🥒","aliases":["cucumber"]},{"emoji":"🥤","aliases":["cup_with_straw"]},{"emoji":"🧁","aliases":["cupcake"]},{"emoji":"💘","aliases":["cupid"]},{"emoji":"🇨🇼","aliases":["curacao"]},{"emoji":"🥌","aliases":["curling_stone"]},{"emoji":"👨🦱","aliases":["curly_haired_man"]},{"emoji":"👩🦱","aliases":["curly_haired_woman"]},{"emoji":"➰","aliases":["curly_loop"]},{"emoji":"💱","aliases":["currency_exchange"]},{"emoji":"🍛","aliases":["curry"]},{"emoji":"🤬","aliases":["cursing_face"]},{"emoji":"🍮","aliases":["custard"]},{"emoji":"🛃","aliases":["customs"]},{"emoji":"🥩","aliases":["cut_of_meat"]},{"emoji":"🌀","aliases":["cyclone"]},{"emoji":"🇨🇾","aliases":["cyprus"]},{"emoji":"🇨🇿","aliases":["czech_republic"]},{"emoji":"🗡️","aliases":["dagger"]},{"emoji":"👯","aliases":["dancers"]},{"emoji":"👯♂️","aliases":["dancing_men"]},{"emoji":"👯♀️","aliases":["dancing_women"]},{"emoji":"🍡","aliases":["dango"]},{"emoji":"🕶️","aliases":["dark_sunglasses"]},{"emoji":"🎯","aliases":["dart"]},{"emoji":"💨","aliases":["dash"]},{"emoji":"📅","aliases":["date"]},{"emoji":"🇩🇪","aliases":["de"]},{"emoji":"🧏♂️","aliases":["deaf_man"]},{"emoji":"🧏","aliases":["deaf_person"]},{"emoji":"🧏♀️","aliases":["deaf_woman"]},{"emoji":"🌳","aliases":["deciduous_tree"]},{"emoji":"🦌","aliases":["deer"]},{"emoji":"🇩🇰","aliases":["denmark"]},{"emoji":"🏬","aliases":["department_store"]},{"emoji":"🏚️","aliases":["derelict_house"]},{"emoji":"🏜️","aliases":["desert"]},{"emoji":"🏝️","aliases":["desert_island"]},{"emoji":"🖥️","aliases":["desktop_computer"]},{"emoji":"🕵️","aliases":["detective"]},{"emoji":"💠","aliases":["diamond_shape_with_a_dot_inside"]},{"emoji":"♦️","aliases":["diamonds"]},{"emoji":"🇩🇬","aliases":["diego_garcia"]},{"emoji":"😞","aliases":["disappointed"]},{"emoji":"😥","aliases":["disappointed_relieved"]},{"emoji":"🥸","aliases":["disguised_face"]},{"emoji":"🤿","aliases":["diving_mask"]},{"emoji":"🪔","aliases":["diya_lamp"]},{"emoji":"💫","aliases":["dizzy"]},{"emoji":"😵","aliases":["dizzy_face"]},{"emoji":"🇩🇯","aliases":["djibouti"]},{"emoji":"🧬","aliases":["dna"]},{"emoji":"🚯","aliases":["do_not_litter"]},{"emoji":"🦤","aliases":["dodo"]},{"emoji":"🐶","aliases":["dog"]},{"emoji":"🐕","aliases":["dog2"]},{"emoji":"💵","aliases":["dollar"]},{"emoji":"🎎","aliases":["dolls"]},{"emoji":"🐬","aliases":["dolphin","flipper"]},{"emoji":"🇩🇲","aliases":["dominica"]},{"emoji":"🇩🇴","aliases":["dominican_republic"]},{"emoji":"🚪","aliases":["door"]},{"emoji":"🫥","aliases":["dotted_line_face"]},{"emoji":"🍩","aliases":["doughnut"]},{"emoji":"🕊️","aliases":["dove"]},{"emoji":"🐉","aliases":["dragon"]},{"emoji":"🐲","aliases":["dragon_face"]},{"emoji":"👗","aliases":["dress"]},{"emoji":"🐪","aliases":["dromedary_camel"]},{"emoji":"🤤","aliases":["drooling_face"]},{"emoji":"🩸","aliases":["drop_of_blood"]},{"emoji":"💧","aliases":["droplet"]},{"emoji":"🥁","aliases":["drum"]},{"emoji":"🦆","aliases":["duck"]},{"emoji":"🥟","aliases":["dumpling"]},{"emoji":"📀","aliases":["dvd"]},{"emoji":"🦅","aliases":["eagle"]},{"emoji":"👂","aliases":["ear"]},{"emoji":"🌾","aliases":["ear_of_rice"]},{"emoji":"🦻","aliases":["ear_with_hearing_aid"]},{"emoji":"🌍","aliases":["earth_africa"]},{"emoji":"🌎","aliases":["earth_americas"]},{"emoji":"🌏","aliases":["earth_asia"]},{"emoji":"🇪🇨","aliases":["ecuador"]},{"emoji":"🥚","aliases":["egg"]},{"emoji":"🍆","aliases":["eggplant"]},{"emoji":"🇪🇬","aliases":["egypt"]},{"emoji":"8️⃣","aliases":["eight"]},{"emoji":"✴️","aliases":["eight_pointed_black_star"]},{"emoji":"✳️","aliases":["eight_spoked_asterisk"]},{"emoji":"⏏️","aliases":["eject_button"]},{"emoji":"🇸🇻","aliases":["el_salvador"]},{"emoji":"🔌","aliases":["electric_plug"]},{"emoji":"🐘","aliases":["elephant"]},{"emoji":"🛗","aliases":["elevator"]},{"emoji":"🧝","aliases":["elf"]},{"emoji":"🧝♂️","aliases":["elf_man"]},{"emoji":"🧝♀️","aliases":["elf_woman"]},{"emoji":"📧","aliases":["email","e-mail"]},{"emoji":"🪹","aliases":["empty_nest"]},{"emoji":"🔚","aliases":["end"]},{"emoji":"🏴","aliases":["england"]},{"emoji":"✉️","aliases":["envelope"]},{"emoji":"📩","aliases":["envelope_with_arrow"]},{"emoji":"🇬🇶","aliases":["equatorial_guinea"]},{"emoji":"🇪🇷","aliases":["eritrea"]},{"emoji":"🇪🇸","aliases":["es"]},{"emoji":"🇪🇪","aliases":["estonia"]},{"emoji":"🇪🇹","aliases":["ethiopia"]},{"emoji":"🇪🇺","aliases":["eu","european_union"]},{"emoji":"💶","aliases":["euro"]},{"emoji":"🏰","aliases":["european_castle"]},{"emoji":"🏤","aliases":["european_post_office"]},{"emoji":"🌲","aliases":["evergreen_tree"]},{"emoji":"❗","aliases":["exclamation","heavy_exclamation_mark"]},{"emoji":"🤯","aliases":["exploding_head"]},{"emoji":"😑","aliases":["expressionless"]},{"emoji":"👁️","aliases":["eye"]},{"emoji":"👁️🗨️","aliases":["eye_speech_bubble"]},{"emoji":"👓","aliases":["eyeglasses"]},{"emoji":"👀","aliases":["eyes"]},{"emoji":"😮💨","aliases":["face_exhaling"]},{"emoji":"🥹","aliases":["face_holding_back_tears"]},{"emoji":"😶🌫️","aliases":["face_in_clouds"]},{"emoji":"🫤","aliases":["face_with_diagonal_mouth"]},{"emoji":"🤕","aliases":["face_with_head_bandage"]},{"emoji":"🫢","aliases":["face_with_open_eyes_and_hand_over_mouth"]},{"emoji":"🫣","aliases":["face_with_peeking_eye"]},{"emoji":"😵💫","aliases":["face_with_spiral_eyes"]},{"emoji":"🤒","aliases":["face_with_thermometer"]},{"emoji":"🤦","aliases":["facepalm"]},{"emoji":"🏭","aliases":["factory"]},{"emoji":"🧑🏭","aliases":["factory_worker"]},{"emoji":"🧚","aliases":["fairy"]},{"emoji":"🧚♂️","aliases":["fairy_man"]},{"emoji":"🧚♀️","aliases":["fairy_woman"]},{"emoji":"🧆","aliases":["falafel"]},{"emoji":"🇫🇰","aliases":["falkland_islands"]},{"emoji":"🍂","aliases":["fallen_leaf"]},{"emoji":"👪","aliases":["family"]},{"emoji":"👨👦","aliases":["family_man_boy"]},{"emoji":"👨👦👦","aliases":["family_man_boy_boy"]},{"emoji":"👨👧","aliases":["family_man_girl"]},{"emoji":"👨👧👦","aliases":["family_man_girl_boy"]},{"emoji":"👨👧👧","aliases":["family_man_girl_girl"]},{"emoji":"👨👨👦","aliases":["family_man_man_boy"]},{"emoji":"👨👨👦👦","aliases":["family_man_man_boy_boy"]},{"emoji":"👨👨👧","aliases":["family_man_man_girl"]},{"emoji":"👨👨👧👦","aliases":["family_man_man_girl_boy"]},{"emoji":"👨👨👧👧","aliases":["family_man_man_girl_girl"]},{"emoji":"👨👩👦","aliases":["family_man_woman_boy"]},{"emoji":"👨👩👦👦","aliases":["family_man_woman_boy_boy"]},{"emoji":"👨👩👧","aliases":["family_man_woman_girl"]},{"emoji":"👨👩👧👦","aliases":["family_man_woman_girl_boy"]},{"emoji":"👨👩👧👧","aliases":["family_man_woman_girl_girl"]},{"emoji":"👩👦","aliases":["family_woman_boy"]},{"emoji":"👩👦👦","aliases":["family_woman_boy_boy"]},{"emoji":"👩👧","aliases":["family_woman_girl"]},{"emoji":"👩👧👦","aliases":["family_woman_girl_boy"]},{"emoji":"👩👧👧","aliases":["family_woman_girl_girl"]},{"emoji":"👩👩👦","aliases":["family_woman_woman_boy"]},{"emoji":"👩👩👦👦","aliases":["family_woman_woman_boy_boy"]},{"emoji":"👩👩👧","aliases":["family_woman_woman_girl"]},{"emoji":"👩👩👧👦","aliases":["family_woman_woman_girl_boy"]},{"emoji":"👩👩👧👧","aliases":["family_woman_woman_girl_girl"]},{"emoji":"🧑🌾","aliases":["farmer"]},{"emoji":"🇫🇴","aliases":["faroe_islands"]},{"emoji":"⏩","aliases":["fast_forward"]},{"emoji":"📠","aliases":["fax"]},{"emoji":"😨","aliases":["fearful"]},{"emoji":"🪶","aliases":["feather"]},{"emoji":"🐾","aliases":["feet","paw_prints"]},{"emoji":"🕵️♀️","aliases":["female_detective"]},{"emoji":"♀️","aliases":["female_sign"]},{"emoji":"🎡","aliases":["ferris_wheel"]},{"emoji":"⛴️","aliases":["ferry"]},{"emoji":"🏑","aliases":["field_hockey"]},{"emoji":"🇫🇯","aliases":["fiji"]},{"emoji":"🗄️","aliases":["file_cabinet"]},{"emoji":"📁","aliases":["file_folder"]},{"emoji":"📽️","aliases":["film_projector"]},{"emoji":"🎞️","aliases":["film_strip"]},{"emoji":"🇫🇮","aliases":["finland"]},{"emoji":"🔥","aliases":["fire"]},{"emoji":"🚒","aliases":["fire_engine"]},{"emoji":"🧯","aliases":["fire_extinguisher"]},{"emoji":"🧨","aliases":["firecracker"]},{"emoji":"🧑🚒","aliases":["firefighter"]},{"emoji":"🎆","aliases":["fireworks"]},{"emoji":"🌓","aliases":["first_quarter_moon"]},{"emoji":"🌛","aliases":["first_quarter_moon_with_face"]},{"emoji":"🐟","aliases":["fish"]},{"emoji":"🍥","aliases":["fish_cake"]},{"emoji":"🎣","aliases":["fishing_pole_and_fish"]},{"emoji":"🤛","aliases":["fist_left"]},{"emoji":"👊","aliases":["fist_oncoming","facepunch","punch"]},{"emoji":"✊","aliases":["fist_raised","fist"]},{"emoji":"🤜","aliases":["fist_right"]},{"emoji":"5️⃣","aliases":["five"]},{"emoji":"🎏","aliases":["flags"]},{"emoji":"🦩","aliases":["flamingo"]},{"emoji":"🔦","aliases":["flashlight"]},{"emoji":"🥿","aliases":["flat_shoe"]},{"emoji":"🫓","aliases":["flatbread"]},{"emoji":"⚜️","aliases":["fleur_de_lis"]},{"emoji":"🛬","aliases":["flight_arrival"]},{"emoji":"🛫","aliases":["flight_departure"]},{"emoji":"💾","aliases":["floppy_disk"]},{"emoji":"🎴","aliases":["flower_playing_cards"]},{"emoji":"😳","aliases":["flushed"]},{"emoji":"🪰","aliases":["fly"]},{"emoji":"🥏","aliases":["flying_disc"]},{"emoji":"🛸","aliases":["flying_saucer"]},{"emoji":"🌫️","aliases":["fog"]},{"emoji":"🌁","aliases":["foggy"]},{"emoji":"🫕","aliases":["fondue"]},{"emoji":"🦶","aliases":["foot"]},{"emoji":"🏈","aliases":["football"]},{"emoji":"👣","aliases":["footprints"]},{"emoji":"🍴","aliases":["fork_and_knife"]},{"emoji":"🥠","aliases":["fortune_cookie"]},{"emoji":"⛲","aliases":["fountain"]},{"emoji":"🖋️","aliases":["fountain_pen"]},{"emoji":"4️⃣","aliases":["four"]},{"emoji":"🍀","aliases":["four_leaf_clover"]},{"emoji":"🦊","aliases":["fox_face"]},{"emoji":"🇫🇷","aliases":["fr"]},{"emoji":"🖼️","aliases":["framed_picture"]},{"emoji":"🆓","aliases":["free"]},{"emoji":"🇬🇫","aliases":["french_guiana"]},{"emoji":"🇵🇫","aliases":["french_polynesia"]},{"emoji":"🇹🇫","aliases":["french_southern_territories"]},{"emoji":"🍳","aliases":["fried_egg"]},{"emoji":"🍤","aliases":["fried_shrimp"]},{"emoji":"🍟","aliases":["fries"]},{"emoji":"🐸","aliases":["frog"]},{"emoji":"😦","aliases":["frowning"]},{"emoji":"☹️","aliases":["frowning_face"]},{"emoji":"🙍♂️","aliases":["frowning_man"]},{"emoji":"🙍","aliases":["frowning_person"]},{"emoji":"🙍♀️","aliases":["frowning_woman"]},{"emoji":"⛽","aliases":["fuelpump"]},{"emoji":"🌕","aliases":["full_moon"]},{"emoji":"🌝","aliases":["full_moon_with_face"]},{"emoji":"⚱️","aliases":["funeral_urn"]},{"emoji":"🇬🇦","aliases":["gabon"]},{"emoji":"🇬🇲","aliases":["gambia"]},{"emoji":"🎲","aliases":["game_die"]},{"emoji":"🧄","aliases":["garlic"]},{"emoji":"🇬🇧","aliases":["gb","uk"]},{"emoji":"⚙️","aliases":["gear"]},{"emoji":"💎","aliases":["gem"]},{"emoji":"♊","aliases":["gemini"]},{"emoji":"🧞","aliases":["genie"]},{"emoji":"🧞♂️","aliases":["genie_man"]},{"emoji":"🧞♀️","aliases":["genie_woman"]},{"emoji":"🇬🇪","aliases":["georgia"]},{"emoji":"🇬🇭","aliases":["ghana"]},{"emoji":"👻","aliases":["ghost"]},{"emoji":"🇬🇮","aliases":["gibraltar"]},{"emoji":"🎁","aliases":["gift"]},{"emoji":"💝","aliases":["gift_heart"]},{"emoji":"🦒","aliases":["giraffe"]},{"emoji":"👧","aliases":["girl"]},{"emoji":"🌐","aliases":["globe_with_meridians"]},{"emoji":"🧤","aliases":["gloves"]},{"emoji":"🥅","aliases":["goal_net"]},{"emoji":"🐐","aliases":["goat"]},{"emoji":"🥽","aliases":["goggles"]},{"emoji":"⛳","aliases":["golf"]},{"emoji":"🏌️","aliases":["golfing"]},{"emoji":"🏌️♂️","aliases":["golfing_man"]},{"emoji":"🏌️♀️","aliases":["golfing_woman"]},{"emoji":"🦍","aliases":["gorilla"]},{"emoji":"🍇","aliases":["grapes"]},{"emoji":"🇬🇷","aliases":["greece"]},{"emoji":"🍏","aliases":["green_apple"]},{"emoji":"📗","aliases":["green_book"]},{"emoji":"🟢","aliases":["green_circle"]},{"emoji":"💚","aliases":["green_heart"]},{"emoji":"🥗","aliases":["green_salad"]},{"emoji":"🟩","aliases":["green_square"]},{"emoji":"🇬🇱","aliases":["greenland"]},{"emoji":"🇬🇩","aliases":["grenada"]},{"emoji":"❕","aliases":["grey_exclamation"]},{"emoji":"❔","aliases":["grey_question"]},{"emoji":"😬","aliases":["grimacing"]},{"emoji":"😁","aliases":["grin"]},{"emoji":"😀","aliases":["grinning"]},{"emoji":"🇬🇵","aliases":["guadeloupe"]},{"emoji":"🇬🇺","aliases":["guam"]},{"emoji":"💂","aliases":["guard"]},{"emoji":"💂♂️","aliases":["guardsman"]},{"emoji":"💂♀️","aliases":["guardswoman"]},{"emoji":"🇬🇹","aliases":["guatemala"]},{"emoji":"🇬🇬","aliases":["guernsey"]},{"emoji":"🦮","aliases":["guide_dog"]},{"emoji":"🇬🇳","aliases":["guinea"]},{"emoji":"🇬🇼","aliases":["guinea_bissau"]},{"emoji":"🎸","aliases":["guitar"]},{"emoji":"🔫","aliases":["gun"]},{"emoji":"🇬🇾","aliases":["guyana"]},{"emoji":"💇","aliases":["haircut"]},{"emoji":"💇♂️","aliases":["haircut_man"]},{"emoji":"💇♀️","aliases":["haircut_woman"]},{"emoji":"🇭🇹","aliases":["haiti"]},{"emoji":"🍔","aliases":["hamburger"]},{"emoji":"🔨","aliases":["hammer"]},{"emoji":"⚒️","aliases":["hammer_and_pick"]},{"emoji":"🛠️","aliases":["hammer_and_wrench"]},{"emoji":"🪬","aliases":["hamsa"]},{"emoji":"🐹","aliases":["hamster"]},{"emoji":"✋","aliases":["hand","raised_hand"]},{"emoji":"🤭","aliases":["hand_over_mouth"]},{"emoji":"🫰","aliases":["hand_with_index_finger_and_thumb_crossed"]},{"emoji":"👜","aliases":["handbag"]},{"emoji":"🤾","aliases":["handball_person"]},{"emoji":"🤝","aliases":["handshake"]},{"emoji":"💩","aliases":["hankey","poop","shit"]},{"emoji":"#️⃣","aliases":["hash"]},{"emoji":"🐥","aliases":["hatched_chick"]},{"emoji":"🐣","aliases":["hatching_chick"]},{"emoji":"🎧","aliases":["headphones"]},{"emoji":"🪦","aliases":["headstone"]},{"emoji":"🧑⚕️","aliases":["health_worker"]},{"emoji":"🙉","aliases":["hear_no_evil"]},{"emoji":"🇭🇲","aliases":["heard_mcdonald_islands"]},{"emoji":"❤️","aliases":["heart"]},{"emoji":"💟","aliases":["heart_decoration"]},{"emoji":"😍","aliases":["heart_eyes"]},{"emoji":"😻","aliases":["heart_eyes_cat"]},{"emoji":"🫶","aliases":["heart_hands"]},{"emoji":"❤️🔥","aliases":["heart_on_fire"]},{"emoji":"💓","aliases":["heartbeat"]},{"emoji":"💗","aliases":["heartpulse"]},{"emoji":"♥️","aliases":["hearts"]},{"emoji":"✔️","aliases":["heavy_check_mark"]},{"emoji":"➗","aliases":["heavy_division_sign"]},{"emoji":"💲","aliases":["heavy_dollar_sign"]},{"emoji":"🟰","aliases":["heavy_equals_sign"]},{"emoji":"❣️","aliases":["heavy_heart_exclamation"]},{"emoji":"➖","aliases":["heavy_minus_sign"]},{"emoji":"✖️","aliases":["heavy_multiplication_x"]},{"emoji":"➕","aliases":["heavy_plus_sign"]},{"emoji":"🦔","aliases":["hedgehog"]},{"emoji":"🚁","aliases":["helicopter"]},{"emoji":"🌿","aliases":["herb"]},{"emoji":"🌺","aliases":["hibiscus"]},{"emoji":"🔆","aliases":["high_brightness"]},{"emoji":"👠","aliases":["high_heel"]},{"emoji":"🥾","aliases":["hiking_boot"]},{"emoji":"🛕","aliases":["hindu_temple"]},{"emoji":"🦛","aliases":["hippopotamus"]},{"emoji":"🔪","aliases":["hocho","knife"]},{"emoji":"🕳️","aliases":["hole"]},{"emoji":"🇭🇳","aliases":["honduras"]},{"emoji":"🍯","aliases":["honey_pot"]},{"emoji":"🇭🇰","aliases":["hong_kong"]},{"emoji":"🪝","aliases":["hook"]},{"emoji":"🐴","aliases":["horse"]},{"emoji":"🏇","aliases":["horse_racing"]},{"emoji":"🏥","aliases":["hospital"]},{"emoji":"🥵","aliases":["hot_face"]},{"emoji":"🌶️","aliases":["hot_pepper"]},{"emoji":"🌭","aliases":["hotdog"]},{"emoji":"🏨","aliases":["hotel"]},{"emoji":"♨️","aliases":["hotsprings"]},{"emoji":"⌛","aliases":["hourglass"]},{"emoji":"⏳","aliases":["hourglass_flowing_sand"]},{"emoji":"🏠","aliases":["house"]},{"emoji":"🏡","aliases":["house_with_garden"]},{"emoji":"🏘️","aliases":["houses"]},{"emoji":"🤗","aliases":["hugs"]},{"emoji":"🇭🇺","aliases":["hungary"]},{"emoji":"😯","aliases":["hushed"]},{"emoji":"🛖","aliases":["hut"]},{"emoji":"🍨","aliases":["ice_cream"]},{"emoji":"🧊","aliases":["ice_cube"]},{"emoji":"🏒","aliases":["ice_hockey"]},{"emoji":"⛸️","aliases":["ice_skate"]},{"emoji":"🍦","aliases":["icecream"]},{"emoji":"🇮🇸","aliases":["iceland"]},{"emoji":"🆔","aliases":["id"]},{"emoji":"🪪","aliases":["identification_card"]},{"emoji":"🉐","aliases":["ideograph_advantage"]},{"emoji":"👿","aliases":["imp"]},{"emoji":"📥","aliases":["inbox_tray"]},{"emoji":"📨","aliases":["incoming_envelope"]},{"emoji":"🫵","aliases":["index_pointing_at_the_viewer"]},{"emoji":"🇮🇳","aliases":["india"]},{"emoji":"🇮🇩","aliases":["indonesia"]},{"emoji":"♾️","aliases":["infinity"]},{"emoji":"ℹ️","aliases":["information_source"]},{"emoji":"😇","aliases":["innocent"]},{"emoji":"⁉️","aliases":["interrobang"]},{"emoji":"📱","aliases":["iphone"]},{"emoji":"🇮🇷","aliases":["iran"]},{"emoji":"🇮🇶","aliases":["iraq"]},{"emoji":"🇮🇪","aliases":["ireland"]},{"emoji":"🇮🇲","aliases":["isle_of_man"]},{"emoji":"🇮🇱","aliases":["israel"]},{"emoji":"🇮🇹","aliases":["it"]},{"emoji":"🏮","aliases":["izakaya_lantern","lantern"]},{"emoji":"🎃","aliases":["jack_o_lantern"]},{"emoji":"🇯🇲","aliases":["jamaica"]},{"emoji":"🗾","aliases":["japan"]},{"emoji":"🏯","aliases":["japanese_castle"]},{"emoji":"👺","aliases":["japanese_goblin"]},{"emoji":"👹","aliases":["japanese_ogre"]},{"emoji":"🫙","aliases":["jar"]},{"emoji":"👖","aliases":["jeans"]},{"emoji":"🇯🇪","aliases":["jersey"]},{"emoji":"🧩","aliases":["jigsaw"]},{"emoji":"🇯🇴","aliases":["jordan"]},{"emoji":"😂","aliases":["joy"]},{"emoji":"😹","aliases":["joy_cat"]},{"emoji":"🕹️","aliases":["joystick"]},{"emoji":"🇯🇵","aliases":["jp"]},{"emoji":"🧑⚖️","aliases":["judge"]},{"emoji":"🤹","aliases":["juggling_person"]},{"emoji":"🕋","aliases":["kaaba"]},{"emoji":"🦘","aliases":["kangaroo"]},{"emoji":"🇰🇿","aliases":["kazakhstan"]},{"emoji":"🇰🇪","aliases":["kenya"]},{"emoji":"🔑","aliases":["key"]},{"emoji":"⌨️","aliases":["keyboard"]},{"emoji":"🔟","aliases":["keycap_ten"]},{"emoji":"🛴","aliases":["kick_scooter"]},{"emoji":"👘","aliases":["kimono"]},{"emoji":"🇰🇮","aliases":["kiribati"]},{"emoji":"💋","aliases":["kiss"]},{"emoji":"😗","aliases":["kissing"]},{"emoji":"😽","aliases":["kissing_cat"]},{"emoji":"😚","aliases":["kissing_closed_eyes"]},{"emoji":"😘","aliases":["kissing_heart"]},{"emoji":"😙","aliases":["kissing_smiling_eyes"]},{"emoji":"🪁","aliases":["kite"]},{"emoji":"🥝","aliases":["kiwi_fruit"]},{"emoji":"🧎♂️","aliases":["kneeling_man"]},{"emoji":"🧎","aliases":["kneeling_person"]},{"emoji":"🧎♀️","aliases":["kneeling_woman"]},{"emoji":"🪢","aliases":["knot"]},{"emoji":"🐨","aliases":["koala"]},{"emoji":"🈁","aliases":["koko"]},{"emoji":"🇽🇰","aliases":["kosovo"]},{"emoji":"🇰🇷","aliases":["kr"]},{"emoji":"🇰🇼","aliases":["kuwait"]},{"emoji":"🇰🇬","aliases":["kyrgyzstan"]},{"emoji":"🥼","aliases":["lab_coat"]},{"emoji":"🏷️","aliases":["label"]},{"emoji":"🥍","aliases":["lacrosse"]},{"emoji":"🪜","aliases":["ladder"]},{"emoji":"🐞","aliases":["lady_beetle"]},{"emoji":"🇱🇦","aliases":["laos"]},{"emoji":"🔵","aliases":["large_blue_circle"]},{"emoji":"🔷","aliases":["large_blue_diamond"]},{"emoji":"🔶","aliases":["large_orange_diamond"]},{"emoji":"🌗","aliases":["last_quarter_moon"]},{"emoji":"🌜","aliases":["last_quarter_moon_with_face"]},{"emoji":"✝️","aliases":["latin_cross"]},{"emoji":"🇱🇻","aliases":["latvia"]},{"emoji":"😆","aliases":["laughing","satisfied","laugh"]},{"emoji":"🥬","aliases":["leafy_green"]},{"emoji":"🍃","aliases":["leaves"]},{"emoji":"🇱🇧","aliases":["lebanon"]},{"emoji":"📒","aliases":["ledger"]},{"emoji":"🛅","aliases":["left_luggage"]},{"emoji":"↔️","aliases":["left_right_arrow"]},{"emoji":"🗨️","aliases":["left_speech_bubble"]},{"emoji":"↩️","aliases":["leftwards_arrow_with_hook"]},{"emoji":"🫲","aliases":["leftwards_hand"]},{"emoji":"🦵","aliases":["leg"]},{"emoji":"🍋","aliases":["lemon"]},{"emoji":"♌","aliases":["leo"]},{"emoji":"🐆","aliases":["leopard"]},{"emoji":"🇱🇸","aliases":["lesotho"]},{"emoji":"🎚️","aliases":["level_slider"]},{"emoji":"🇱🇷","aliases":["liberia"]},{"emoji":"♎","aliases":["libra"]},{"emoji":"🇱🇾","aliases":["libya"]},{"emoji":"🇱🇮","aliases":["liechtenstein"]},{"emoji":"🚈","aliases":["light_rail"]},{"emoji":"🔗","aliases":["link"]},{"emoji":"🦁","aliases":["lion"]},{"emoji":"👄","aliases":["lips"]},{"emoji":"💄","aliases":["lipstick"]},{"emoji":"🇱🇹","aliases":["lithuania"]},{"emoji":"🦎","aliases":["lizard"]},{"emoji":"🦙","aliases":["llama"]},{"emoji":"🦞","aliases":["lobster"]},{"emoji":"🔒","aliases":["lock"]},{"emoji":"🔏","aliases":["lock_with_ink_pen"]},{"emoji":"🍭","aliases":["lollipop"]},{"emoji":"🪘","aliases":["long_drum"]},{"emoji":"➿","aliases":["loop"]},{"emoji":"🧴","aliases":["lotion_bottle"]},{"emoji":"🪷","aliases":["lotus"]},{"emoji":"🧘","aliases":["lotus_position"]},{"emoji":"🧘♂️","aliases":["lotus_position_man"]},{"emoji":"🧘♀️","aliases":["lotus_position_woman"]},{"emoji":"🔊","aliases":["loud_sound"]},{"emoji":"📢","aliases":["loudspeaker"]},{"emoji":"🏩","aliases":["love_hotel"]},{"emoji":"💌","aliases":["love_letter"]},{"emoji":"🤟","aliases":["love_you_gesture"]},{"emoji":"🪫","aliases":["low_battery"]},{"emoji":"🔅","aliases":["low_brightness"]},{"emoji":"🧳","aliases":["luggage"]},{"emoji":"🫁","aliases":["lungs"]},{"emoji":"🇱🇺","aliases":["luxembourg"]},{"emoji":"🤥","aliases":["lying_face"]},{"emoji":"Ⓜ️","aliases":["m"]},{"emoji":"🇲🇴","aliases":["macau"]},{"emoji":"🇲🇰","aliases":["macedonia"]},{"emoji":"🇲🇬","aliases":["madagascar"]},{"emoji":"🔍","aliases":["mag"]},{"emoji":"🔎","aliases":["mag_right"]},{"emoji":"🧙","aliases":["mage"]},{"emoji":"🧙♂️","aliases":["mage_man"]},{"emoji":"🧙♀️","aliases":["mage_woman"]},{"emoji":"🪄","aliases":["magic_wand"]},{"emoji":"🧲","aliases":["magnet"]},{"emoji":"🀄","aliases":["mahjong"]},{"emoji":"📫","aliases":["mailbox"]},{"emoji":"📪","aliases":["mailbox_closed"]},{"emoji":"📬","aliases":["mailbox_with_mail"]},{"emoji":"📭","aliases":["mailbox_with_no_mail"]},{"emoji":"🇲🇼","aliases":["malawi"]},{"emoji":"🇲🇾","aliases":["malaysia"]},{"emoji":"🇲🇻","aliases":["maldives"]},{"emoji":"🕵️♂️","aliases":["male_detective"]},{"emoji":"♂️","aliases":["male_sign"]},{"emoji":"🇲🇱","aliases":["mali"]},{"emoji":"🇲🇹","aliases":["malta"]},{"emoji":"🦣","aliases":["mammoth"]},{"emoji":"👨","aliases":["man"]},{"emoji":"👨🎨","aliases":["man_artist"]},{"emoji":"👨🚀","aliases":["man_astronaut"]},{"emoji":"🧔♂️","aliases":["man_beard"]},{"emoji":"🤸♂️","aliases":["man_cartwheeling"]},{"emoji":"👨🍳","aliases":["man_cook"]},{"emoji":"🕺","aliases":["man_dancing"]},{"emoji":"🤦♂️","aliases":["man_facepalming"]},{"emoji":"👨🏭","aliases":["man_factory_worker"]},{"emoji":"👨🌾","aliases":["man_farmer"]},{"emoji":"👨🍼","aliases":["man_feeding_baby"]},{"emoji":"👨🚒","aliases":["man_firefighter"]},{"emoji":"👨⚕️","aliases":["man_health_worker"]},{"emoji":"👨🦽","aliases":["man_in_manual_wheelchair"]},{"emoji":"👨🦼","aliases":["man_in_motorized_wheelchair"]},{"emoji":"🤵♂️","aliases":["man_in_tuxedo"]},{"emoji":"👨⚖️","aliases":["man_judge"]},{"emoji":"🤹♂️","aliases":["man_juggling"]},{"emoji":"👨🔧","aliases":["man_mechanic"]},{"emoji":"👨💼","aliases":["man_office_worker"]},{"emoji":"👨✈️","aliases":["man_pilot"]},{"emoji":"🤾♂️","aliases":["man_playing_handball"]},{"emoji":"🤽♂️","aliases":["man_playing_water_polo"]},{"emoji":"👨🔬","aliases":["man_scientist"]},{"emoji":"🤷♂️","aliases":["man_shrugging"]},{"emoji":"👨🎤","aliases":["man_singer"]},{"emoji":"👨🎓","aliases":["man_student"]},{"emoji":"👨🏫","aliases":["man_teacher"]},{"emoji":"👨💻","aliases":["man_technologist"]},{"emoji":"👲","aliases":["man_with_gua_pi_mao"]},{"emoji":"👨🦯","aliases":["man_with_probing_cane"]},{"emoji":"👳♂️","aliases":["man_with_turban"]},{"emoji":"👰♂️","aliases":["man_with_veil"]},{"emoji":"🥭","aliases":["mango"]},{"emoji":"👞","aliases":["mans_shoe","shoe"]},{"emoji":"🕰️","aliases":["mantelpiece_clock"]},{"emoji":"🦽","aliases":["manual_wheelchair"]},{"emoji":"🍁","aliases":["maple_leaf"]},{"emoji":"🇲🇭","aliases":["marshall_islands"]},{"emoji":"🥋","aliases":["martial_arts_uniform"]},{"emoji":"🇲🇶","aliases":["martinique"]},{"emoji":"😷","aliases":["mask"]},{"emoji":"💆","aliases":["massage"]},{"emoji":"💆♂️","aliases":["massage_man"]},{"emoji":"💆♀️","aliases":["massage_woman"]},{"emoji":"🧉","aliases":["mate"]},{"emoji":"🇲🇷","aliases":["mauritania"]},{"emoji":"🇲🇺","aliases":["mauritius"]},{"emoji":"🇾🇹","aliases":["mayotte"]},{"emoji":"🍖","aliases":["meat_on_bone"]},{"emoji":"🧑🔧","aliases":["mechanic"]},{"emoji":"🦾","aliases":["mechanical_arm"]},{"emoji":"🦿","aliases":["mechanical_leg"]},{"emoji":"🎖️","aliases":["medal_military"]},{"emoji":"🏅","aliases":["medal_sports"]},{"emoji":"⚕️","aliases":["medical_symbol"]},{"emoji":"📣","aliases":["mega"]},{"emoji":"🍈","aliases":["melon"]},{"emoji":"🫠","aliases":["melting_face"]},{"emoji":"📝","aliases":["memo","pencil"]},{"emoji":"🤼♂️","aliases":["men_wrestling"]},{"emoji":"❤️🩹","aliases":["mending_heart"]},{"emoji":"🕎","aliases":["menorah"]},{"emoji":"🚹","aliases":["mens"]},{"emoji":"🧜♀️","aliases":["mermaid"]},{"emoji":"🧜♂️","aliases":["merman"]},{"emoji":"🧜","aliases":["merperson"]},{"emoji":"🤘","aliases":["metal"]},{"emoji":"🚇","aliases":["metro"]},{"emoji":"🇲🇽","aliases":["mexico"]},{"emoji":"🦠","aliases":["microbe"]},{"emoji":"🇫🇲","aliases":["micronesia"]},{"emoji":"🎤","aliases":["microphone"]},{"emoji":"🔬","aliases":["microscope"]},{"emoji":"🖕","aliases":["middle_finger","fu"]},{"emoji":"🪖","aliases":["military_helmet"]},{"emoji":"🥛","aliases":["milk_glass"]},{"emoji":"🌌","aliases":["milky_way"]},{"emoji":"🚐","aliases":["minibus"]},{"emoji":"💽","aliases":["minidisc"]},{"emoji":"🪞","aliases":["mirror"]},{"emoji":"🪩","aliases":["mirror_ball"]},{"emoji":"📴","aliases":["mobile_phone_off"]},{"emoji":"🇲🇩","aliases":["moldova"]},{"emoji":"🇲🇨","aliases":["monaco"]},{"emoji":"🤑","aliases":["money_mouth_face"]},{"emoji":"💸","aliases":["money_with_wings"]},{"emoji":"💰","aliases":["moneybag"]},{"emoji":"🇲🇳","aliases":["mongolia"]},{"emoji":"🐒","aliases":["monkey"]},{"emoji":"🐵","aliases":["monkey_face"]},{"emoji":"🧐","aliases":["monocle_face"]},{"emoji":"🚝","aliases":["monorail"]},{"emoji":"🇲🇪","aliases":["montenegro"]},{"emoji":"🇲🇸","aliases":["montserrat"]},{"emoji":"🌔","aliases":["moon","waxing_gibbous_moon"]},{"emoji":"🥮","aliases":["moon_cake"]},{"emoji":"🇲🇦","aliases":["morocco"]},{"emoji":"🎓","aliases":["mortar_board"]},{"emoji":"🕌","aliases":["mosque"]},{"emoji":"🦟","aliases":["mosquito"]},{"emoji":"🛥️","aliases":["motor_boat"]},{"emoji":"🛵","aliases":["motor_scooter"]},{"emoji":"🏍️","aliases":["motorcycle"]},{"emoji":"🦼","aliases":["motorized_wheelchair"]},{"emoji":"🛣️","aliases":["motorway"]},{"emoji":"🗻","aliases":["mount_fuji"]},{"emoji":"⛰️","aliases":["mountain"]},{"emoji":"🚵","aliases":["mountain_bicyclist"]},{"emoji":"🚵♂️","aliases":["mountain_biking_man"]},{"emoji":"🚵♀️","aliases":["mountain_biking_woman"]},{"emoji":"🚠","aliases":["mountain_cableway"]},{"emoji":"🚞","aliases":["mountain_railway"]},{"emoji":"🏔️","aliases":["mountain_snow"]},{"emoji":"🐭","aliases":["mouse"]},{"emoji":"🐁","aliases":["mouse2"]},{"emoji":"🪤","aliases":["mouse_trap"]},{"emoji":"🎥","aliases":["movie_camera"]},{"emoji":"🗿","aliases":["moyai"]},{"emoji":"🇲🇿","aliases":["mozambique"]},{"emoji":"🤶","aliases":["mrs_claus"]},{"emoji":"💪","aliases":["muscle"]},{"emoji":"🍄","aliases":["mushroom"]},{"emoji":"🎹","aliases":["musical_keyboard"]},{"emoji":"🎵","aliases":["musical_note"]},{"emoji":"🎼","aliases":["musical_score"]},{"emoji":"🔇","aliases":["mute"]},{"emoji":"🧑🎄","aliases":["mx_claus"]},{"emoji":"🇲🇲","aliases":["myanmar"]},{"emoji":"💅","aliases":["nail_care"]},{"emoji":"📛","aliases":["name_badge"]},{"emoji":"🇳🇦","aliases":["namibia"]},{"emoji":"🏞️","aliases":["national_park"]},{"emoji":"🇳🇷","aliases":["nauru"]},{"emoji":"🤢","aliases":["nauseated_face"]},{"emoji":"🧿","aliases":["nazar_amulet"]},{"emoji":"👔","aliases":["necktie"]},{"emoji":"❎","aliases":["negative_squared_cross_mark"]},{"emoji":"🇳🇵","aliases":["nepal"]},{"emoji":"🤓","aliases":["nerd_face"]},{"emoji":"🪺","aliases":["nest_with_eggs"]},{"emoji":"🪆","aliases":["nesting_dolls"]},{"emoji":"🇳🇱","aliases":["netherlands"]},{"emoji":"😐","aliases":["neutral_face"]},{"emoji":"🆕","aliases":["new"]},{"emoji":"🇳🇨","aliases":["new_caledonia"]},{"emoji":"🌑","aliases":["new_moon"]},{"emoji":"🌚","aliases":["new_moon_with_face"]},{"emoji":"🇳🇿","aliases":["new_zealand"]},{"emoji":"📰","aliases":["newspaper"]},{"emoji":"🗞️","aliases":["newspaper_roll"]},{"emoji":"⏭️","aliases":["next_track_button"]},{"emoji":"🆖","aliases":["ng"]},{"emoji":"🇳🇮","aliases":["nicaragua"]},{"emoji":"🇳🇪","aliases":["niger"]},{"emoji":"🇳🇬","aliases":["nigeria"]},{"emoji":"🌃","aliases":["night_with_stars"]},{"emoji":"9️⃣","aliases":["nine"]},{"emoji":"🥷","aliases":["ninja"]},{"emoji":"🇳🇺","aliases":["niue"]},{"emoji":"🔕","aliases":["no_bell"]},{"emoji":"🚳","aliases":["no_bicycles"]},{"emoji":"⛔","aliases":["no_entry"]},{"emoji":"🚫","aliases":["no_entry_sign"]},{"emoji":"🙅","aliases":["no_good"]},{"emoji":"🙅♂️","aliases":["no_good_man","ng_man"]},{"emoji":"🙅♀️","aliases":["no_good_woman","ng_woman"]},{"emoji":"📵","aliases":["no_mobile_phones"]},{"emoji":"😶","aliases":["no_mouth"]},{"emoji":"🚷","aliases":["no_pedestrians"]},{"emoji":"🚭","aliases":["no_smoking"]},{"emoji":"🚱","aliases":["non-potable_water"]},{"emoji":"🇳🇫","aliases":["norfolk_island"]},{"emoji":"🇰🇵","aliases":["north_korea"]},{"emoji":"🇲🇵","aliases":["northern_mariana_islands"]},{"emoji":"🇳🇴","aliases":["norway"]},{"emoji":"👃","aliases":["nose"]},{"emoji":"📓","aliases":["notebook"]},{"emoji":"📔","aliases":["notebook_with_decorative_cover"]},{"emoji":"🎶","aliases":["notes"]},{"emoji":"🔩","aliases":["nut_and_bolt"]},{"emoji":"⭕","aliases":["o"]},{"emoji":"🅾️","aliases":["o2"]},{"emoji":"🌊","aliases":["ocean"]},{"emoji":"🐙","aliases":["octopus"]},{"emoji":"🍢","aliases":["oden"]},{"emoji":"🏢","aliases":["office"]},{"emoji":"🧑💼","aliases":["office_worker"]},{"emoji":"🛢️","aliases":["oil_drum"]},{"emoji":"🆗","aliases":["ok"]},{"emoji":"👌","aliases":["ok_hand"]},{"emoji":"🙆♂️","aliases":["ok_man"]},{"emoji":"🙆","aliases":["ok_person"]},{"emoji":"🙆♀️","aliases":["ok_woman"]},{"emoji":"🗝️","aliases":["old_key"]},{"emoji":"🧓","aliases":["older_adult"]},{"emoji":"👴","aliases":["older_man"]},{"emoji":"👵","aliases":["older_woman"]},{"emoji":"🫒","aliases":["olive"]},{"emoji":"🕉️","aliases":["om"]},{"emoji":"🇴🇲","aliases":["oman"]},{"emoji":"🔛","aliases":["on"]},{"emoji":"🚘","aliases":["oncoming_automobile"]},{"emoji":"🚍","aliases":["oncoming_bus"]},{"emoji":"🚔","aliases":["oncoming_police_car"]},{"emoji":"🚖","aliases":["oncoming_taxi"]},{"emoji":"1️⃣","aliases":["one"]},{"emoji":"🩱","aliases":["one_piece_swimsuit"]},{"emoji":"🧅","aliases":["onion"]},{"emoji":"📂","aliases":["open_file_folder"]},{"emoji":"👐","aliases":["open_hands"]},{"emoji":"😮","aliases":["open_mouth"]},{"emoji":"☂️","aliases":["open_umbrella"]},{"emoji":"⛎","aliases":["ophiuchus"]},{"emoji":"📙","aliases":["orange_book"]},{"emoji":"🟠","aliases":["orange_circle"]},{"emoji":"🧡","aliases":["orange_heart"]},{"emoji":"🟧","aliases":["orange_square"]},{"emoji":"🦧","aliases":["orangutan"]},{"emoji":"☦️","aliases":["orthodox_cross"]},{"emoji":"🦦","aliases":["otter"]},{"emoji":"📤","aliases":["outbox_tray"]},{"emoji":"🦉","aliases":["owl"]},{"emoji":"🐂","aliases":["ox"]},{"emoji":"🦪","aliases":["oyster"]},{"emoji":"📦","aliases":["package"]},{"emoji":"📄","aliases":["page_facing_up"]},{"emoji":"📃","aliases":["page_with_curl"]},{"emoji":"📟","aliases":["pager"]},{"emoji":"🖌️","aliases":["paintbrush"]},{"emoji":"🇵🇰","aliases":["pakistan"]},{"emoji":"🇵🇼","aliases":["palau"]},{"emoji":"🇵🇸","aliases":["palestinian_territories"]},{"emoji":"🫳","aliases":["palm_down_hand"]},{"emoji":"🌴","aliases":["palm_tree"]},{"emoji":"🫴","aliases":["palm_up_hand"]},{"emoji":"🤲","aliases":["palms_up_together"]},{"emoji":"🇵🇦","aliases":["panama"]},{"emoji":"🥞","aliases":["pancakes"]},{"emoji":"🐼","aliases":["panda_face"]},{"emoji":"📎","aliases":["paperclip"]},{"emoji":"🖇️","aliases":["paperclips"]},{"emoji":"🇵🇬","aliases":["papua_new_guinea"]},{"emoji":"🪂","aliases":["parachute"]},{"emoji":"🇵🇾","aliases":["paraguay"]},{"emoji":"⛱️","aliases":["parasol_on_ground"]},{"emoji":"🅿️","aliases":["parking"]},{"emoji":"🦜","aliases":["parrot"]},{"emoji":"〽️","aliases":["part_alternation_mark"]},{"emoji":"⛅","aliases":["partly_sunny"]},{"emoji":"🥳","aliases":["partying_face"]},{"emoji":"🛳️","aliases":["passenger_ship"]},{"emoji":"🛂","aliases":["passport_control"]},{"emoji":"⏸️","aliases":["pause_button"]},{"emoji":"☮️","aliases":["peace_symbol"]},{"emoji":"🍑","aliases":["peach"]},{"emoji":"🦚","aliases":["peacock"]},{"emoji":"🥜","aliases":["peanuts"]},{"emoji":"🍐","aliases":["pear"]},{"emoji":"🖊️","aliases":["pen"]},{"emoji":"✏️","aliases":["pencil2"]},{"emoji":"🐧","aliases":["penguin"]},{"emoji":"😔","aliases":["pensive"]},{"emoji":"🧑🤝🧑","aliases":["people_holding_hands"]},{"emoji":"🫂","aliases":["people_hugging"]},{"emoji":"🎭","aliases":["performing_arts"]},{"emoji":"😣","aliases":["persevere"]},{"emoji":"🧑🦲","aliases":["person_bald"]},{"emoji":"🧑🦱","aliases":["person_curly_hair"]},{"emoji":"🧑🍼","aliases":["person_feeding_baby"]},{"emoji":"🤺","aliases":["person_fencing"]},{"emoji":"🧑🦽","aliases":["person_in_manual_wheelchair"]},{"emoji":"🧑🦼","aliases":["person_in_motorized_wheelchair"]},{"emoji":"🤵","aliases":["person_in_tuxedo"]},{"emoji":"🧑🦰","aliases":["person_red_hair"]},{"emoji":"🧑🦳","aliases":["person_white_hair"]},{"emoji":"🫅","aliases":["person_with_crown"]},{"emoji":"🧑🦯","aliases":["person_with_probing_cane"]},{"emoji":"👳","aliases":["person_with_turban"]},{"emoji":"👰","aliases":["person_with_veil"]},{"emoji":"🇵🇪","aliases":["peru"]},{"emoji":"🧫","aliases":["petri_dish"]},{"emoji":"🇵🇭","aliases":["philippines"]},{"emoji":"☎️","aliases":["phone","telephone"]},{"emoji":"⛏️","aliases":["pick"]},{"emoji":"🛻","aliases":["pickup_truck"]},{"emoji":"🥧","aliases":["pie"]},{"emoji":"🐷","aliases":["pig"]},{"emoji":"🐖","aliases":["pig2"]},{"emoji":"🐽","aliases":["pig_nose"]},{"emoji":"💊","aliases":["pill"]},{"emoji":"🧑✈️","aliases":["pilot"]},{"emoji":"🪅","aliases":["pinata"]},{"emoji":"🤌","aliases":["pinched_fingers"]},{"emoji":"🤏","aliases":["pinching_hand"]},{"emoji":"🍍","aliases":["pineapple"]},{"emoji":"🏓","aliases":["ping_pong"]},{"emoji":"🏴☠️","aliases":["pirate_flag"]},{"emoji":"♓","aliases":["pisces"]},{"emoji":"🇵🇳","aliases":["pitcairn_islands"]},{"emoji":"🍕","aliases":["pizza"]},{"emoji":"🪧","aliases":["placard"]},{"emoji":"🛐","aliases":["place_of_worship"]},{"emoji":"🍽️","aliases":["plate_with_cutlery"]},{"emoji":"⏯️","aliases":["play_or_pause_button"]},{"emoji":"🛝","aliases":["playground_slide"]},{"emoji":"🥺","aliases":["pleading_face"]},{"emoji":"🪠","aliases":["plunger"]},{"emoji":"👇","aliases":["point_down"]},{"emoji":"👈","aliases":["point_left"]},{"emoji":"👉","aliases":["point_right"]},{"emoji":"☝️","aliases":["point_up"]},{"emoji":"👆","aliases":["point_up_2"]},{"emoji":"🇵🇱","aliases":["poland"]},{"emoji":"🐻❄️","aliases":["polar_bear"]},{"emoji":"🚓","aliases":["police_car"]},{"emoji":"👮","aliases":["police_officer","cop"]},{"emoji":"👮♂️","aliases":["policeman"]},{"emoji":"👮♀️","aliases":["policewoman"]},{"emoji":"🐩","aliases":["poodle"]},{"emoji":"🍿","aliases":["popcorn"]},{"emoji":"🇵🇹","aliases":["portugal"]},{"emoji":"🏣","aliases":["post_office"]},{"emoji":"📯","aliases":["postal_horn"]},{"emoji":"📮","aliases":["postbox"]},{"emoji":"🚰","aliases":["potable_water"]},{"emoji":"🥔","aliases":["potato"]},{"emoji":"🪴","aliases":["potted_plant"]},{"emoji":"👝","aliases":["pouch"]},{"emoji":"🍗","aliases":["poultry_leg"]},{"emoji":"💷","aliases":["pound"]},{"emoji":"🫗","aliases":["pouring_liquid"]},{"emoji":"😾","aliases":["pouting_cat"]},{"emoji":"🙎","aliases":["pouting_face"]},{"emoji":"🙎♂️","aliases":["pouting_man"]},{"emoji":"🙎♀️","aliases":["pouting_woman"]},{"emoji":"🙏","aliases":["pray"]},{"emoji":"📿","aliases":["prayer_beads"]},{"emoji":"🫃","aliases":["pregnant_man"]},{"emoji":"🫄","aliases":["pregnant_person"]},{"emoji":"🤰","aliases":["pregnant_woman"]},{"emoji":"🥨","aliases":["pretzel"]},{"emoji":"⏮️","aliases":["previous_track_button"]},{"emoji":"🤴","aliases":["prince"]},{"emoji":"👸","aliases":["princess"]},{"emoji":"🖨️","aliases":["printer"]},{"emoji":"🦯","aliases":["probing_cane"]},{"emoji":"🇵🇷","aliases":["puerto_rico"]},{"emoji":"🟣","aliases":["purple_circle"]},{"emoji":"💜","aliases":["purple_heart"]},{"emoji":"🟪","aliases":["purple_square"]},{"emoji":"👛","aliases":["purse"]},{"emoji":"📌","aliases":["pushpin"]},{"emoji":"🚮","aliases":["put_litter_in_its_place"]},{"emoji":"🇶🇦","aliases":["qatar"]},{"emoji":"❓","aliases":["question"]},{"emoji":"🐰","aliases":["rabbit"]},{"emoji":"🐇","aliases":["rabbit2"]},{"emoji":"🦝","aliases":["raccoon"]},{"emoji":"🐎","aliases":["racehorse"]},{"emoji":"🏎️","aliases":["racing_car"]},{"emoji":"📻","aliases":["radio"]},{"emoji":"🔘","aliases":["radio_button"]},{"emoji":"☢️","aliases":["radioactive"]},{"emoji":"😡","aliases":["rage","pout"]},{"emoji":"🚃","aliases":["railway_car"]},{"emoji":"🛤️","aliases":["railway_track"]},{"emoji":"🌈","aliases":["rainbow"]},{"emoji":"🏳️🌈","aliases":["rainbow_flag"]},{"emoji":"🤚","aliases":["raised_back_of_hand"]},{"emoji":"🤨","aliases":["raised_eyebrow"]},{"emoji":"🖐️","aliases":["raised_hand_with_fingers_splayed"]},{"emoji":"🙌","aliases":["raised_hands"]},{"emoji":"🙋","aliases":["raising_hand"]},{"emoji":"🙋♂️","aliases":["raising_hand_man"]},{"emoji":"🙋♀️","aliases":["raising_hand_woman"]},{"emoji":"🐏","aliases":["ram"]},{"emoji":"🍜","aliases":["ramen"]},{"emoji":"🐀","aliases":["rat"]},{"emoji":"🪒","aliases":["razor"]},{"emoji":"🧾","aliases":["receipt"]},{"emoji":"⏺️","aliases":["record_button"]},{"emoji":"♻️","aliases":["recycle"]},{"emoji":"🔴","aliases":["red_circle"]},{"emoji":"🧧","aliases":["red_envelope"]},{"emoji":"👨🦰","aliases":["red_haired_man"]},{"emoji":"👩🦰","aliases":["red_haired_woman"]},{"emoji":"🟥","aliases":["red_square"]},{"emoji":"®️","aliases":["registered"]},{"emoji":"☺️","aliases":["relaxed"]},{"emoji":"😌","aliases":["relieved"]},{"emoji":"🎗️","aliases":["reminder_ribbon"]},{"emoji":"🔁","aliases":["repeat"]},{"emoji":"🔂","aliases":["repeat_one"]},{"emoji":"⛑️","aliases":["rescue_worker_helmet"]},{"emoji":"🚻","aliases":["restroom"]},{"emoji":"🇷🇪","aliases":["reunion"]},{"emoji":"💞","aliases":["revolving_hearts"]},{"emoji":"⏪","aliases":["rewind"]},{"emoji":"🦏","aliases":["rhinoceros"]},{"emoji":"🎀","aliases":["ribbon"]},{"emoji":"🍚","aliases":["rice"]},{"emoji":"🍙","aliases":["rice_ball"]},{"emoji":"🍘","aliases":["rice_cracker"]},{"emoji":"🎑","aliases":["rice_scene"]},{"emoji":"🗯️","aliases":["right_anger_bubble"]},{"emoji":"🫱","aliases":["rightwards_hand"]},{"emoji":"💍","aliases":["ring"]},{"emoji":"🛟","aliases":["ring_buoy"]},{"emoji":"🪐","aliases":["ringed_planet"]},{"emoji":"🤖","aliases":["robot"]},{"emoji":"🪨","aliases":["rock"]},{"emoji":"🚀","aliases":["rocket"]},{"emoji":"🤣","aliases":["rofl"]},{"emoji":"🙄","aliases":["roll_eyes"]},{"emoji":"🧻","aliases":["roll_of_paper"]},{"emoji":"🎢","aliases":["roller_coaster"]},{"emoji":"🛼","aliases":["roller_skate"]},{"emoji":"🇷🇴","aliases":["romania"]},{"emoji":"🐓","aliases":["rooster"]},{"emoji":"🌹","aliases":["rose"]},{"emoji":"🏵️","aliases":["rosette"]},{"emoji":"🚨","aliases":["rotating_light"]},{"emoji":"📍","aliases":["round_pushpin"]},{"emoji":"🚣","aliases":["rowboat"]},{"emoji":"🚣♂️","aliases":["rowing_man"]},{"emoji":"🚣♀️","aliases":["rowing_woman"]},{"emoji":"🇷🇺","aliases":["ru"]},{"emoji":"🏉","aliases":["rugby_football"]},{"emoji":"🏃","aliases":["runner","running"]},{"emoji":"🏃♂️","aliases":["running_man"]},{"emoji":"🎽","aliases":["running_shirt_with_sash"]},{"emoji":"🏃♀️","aliases":["running_woman"]},{"emoji":"🇷🇼","aliases":["rwanda"]},{"emoji":"🈂️","aliases":["sa"]},{"emoji":"🧷","aliases":["safety_pin"]},{"emoji":"🦺","aliases":["safety_vest"]},{"emoji":"♐","aliases":["sagittarius"]},{"emoji":"🍶","aliases":["sake"]},{"emoji":"🧂","aliases":["salt"]},{"emoji":"🫡","aliases":["saluting_face"]},{"emoji":"🇼🇸","aliases":["samoa"]},{"emoji":"🇸🇲","aliases":["san_marino"]},{"emoji":"👡","aliases":["sandal"]},{"emoji":"🥪","aliases":["sandwich"]},{"emoji":"🎅","aliases":["santa"]},{"emoji":"🇸🇹","aliases":["sao_tome_principe"]},{"emoji":"🥻","aliases":["sari"]},{"emoji":"📡","aliases":["satellite"]},{"emoji":"🇸🇦","aliases":["saudi_arabia"]},{"emoji":"🧖♂️","aliases":["sauna_man"]},{"emoji":"🧖","aliases":["sauna_person"]},{"emoji":"🧖♀️","aliases":["sauna_woman"]},{"emoji":"🦕","aliases":["sauropod"]},{"emoji":"🎷","aliases":["saxophone"]},{"emoji":"🧣","aliases":["scarf"]},{"emoji":"🏫","aliases":["school"]},{"emoji":"🎒","aliases":["school_satchel"]},{"emoji":"🧑🔬","aliases":["scientist"]},{"emoji":"✂️","aliases":["scissors"]},{"emoji":"🦂","aliases":["scorpion"]},{"emoji":"♏","aliases":["scorpius"]},{"emoji":"🏴","aliases":["scotland"]},{"emoji":"😱","aliases":["scream"]},{"emoji":"🙀","aliases":["scream_cat"]},{"emoji":"🪛","aliases":["screwdriver"]},{"emoji":"📜","aliases":["scroll"]},{"emoji":"🦭","aliases":["seal"]},{"emoji":"💺","aliases":["seat"]},{"emoji":"㊙️","aliases":["secret"]},{"emoji":"🙈","aliases":["see_no_evil"]},{"emoji":"🌱","aliases":["seedling"]},{"emoji":"🤳","aliases":["selfie"]},{"emoji":"🇸🇳","aliases":["senegal"]},{"emoji":"🇷🇸","aliases":["serbia"]},{"emoji":"🐕🦺","aliases":["service_dog"]},{"emoji":"7️⃣","aliases":["seven"]},{"emoji":"🪡","aliases":["sewing_needle"]},{"emoji":"🇸🇨","aliases":["seychelles"]},{"emoji":"🥘","aliases":["shallow_pan_of_food"]},{"emoji":"☘️","aliases":["shamrock"]},{"emoji":"🦈","aliases":["shark"]},{"emoji":"🍧","aliases":["shaved_ice"]},{"emoji":"🐑","aliases":["sheep"]},{"emoji":"🐚","aliases":["shell"]},{"emoji":"🛡️","aliases":["shield"]},{"emoji":"⛩️","aliases":["shinto_shrine"]},{"emoji":"🚢","aliases":["ship"]},{"emoji":"👕","aliases":["shirt","tshirt"]},{"emoji":"🛍️","aliases":["shopping"]},{"emoji":"🛒","aliases":["shopping_cart"]},{"emoji":"🩳","aliases":["shorts"]},{"emoji":"🚿","aliases":["shower"]},{"emoji":"🦐","aliases":["shrimp"]},{"emoji":"🤷","aliases":["shrug"]},{"emoji":"🤫","aliases":["shushing_face"]},{"emoji":"🇸🇱","aliases":["sierra_leone"]},{"emoji":"📶","aliases":["signal_strength"]},{"emoji":"🇸🇬","aliases":["singapore"]},{"emoji":"🧑🎤","aliases":["singer"]},{"emoji":"🇸🇽","aliases":["sint_maarten"]},{"emoji":"6️⃣","aliases":["six"]},{"emoji":"🔯","aliases":["six_pointed_star"]},{"emoji":"🛹","aliases":["skateboard"]},{"emoji":"🎿","aliases":["ski"]},{"emoji":"⛷️","aliases":["skier"]},{"emoji":"💀","aliases":["skull"]},{"emoji":"☠️","aliases":["skull_and_crossbones"]},{"emoji":"🦨","aliases":["skunk"]},{"emoji":"🛷","aliases":["sled"]},{"emoji":"😴","aliases":["sleeping"]},{"emoji":"🛌","aliases":["sleeping_bed"]},{"emoji":"😪","aliases":["sleepy"]},{"emoji":"🙁","aliases":["slightly_frowning_face"]},{"emoji":"🙂","aliases":["slightly_smiling_face"]},{"emoji":"🎰","aliases":["slot_machine"]},{"emoji":"🦥","aliases":["sloth"]},{"emoji":"🇸🇰","aliases":["slovakia"]},{"emoji":"🇸🇮","aliases":["slovenia"]},{"emoji":"🛩️","aliases":["small_airplane"]},{"emoji":"🔹","aliases":["small_blue_diamond"]},{"emoji":"🔸","aliases":["small_orange_diamond"]},{"emoji":"🔺","aliases":["small_red_triangle"]},{"emoji":"🔻","aliases":["small_red_triangle_down"]},{"emoji":"😄","aliases":["smile"]},{"emoji":"😸","aliases":["smile_cat"]},{"emoji":"😃","aliases":["smiley"]},{"emoji":"😺","aliases":["smiley_cat"]},{"emoji":"🥲","aliases":["smiling_face_with_tear"]},{"emoji":"🥰","aliases":["smiling_face_with_three_hearts"]},{"emoji":"😈","aliases":["smiling_imp"]},{"emoji":"😏","aliases":["smirk"]},{"emoji":"😼","aliases":["smirk_cat"]},{"emoji":"🚬","aliases":["smoking"]},{"emoji":"🐌","aliases":["snail"]},{"emoji":"🐍","aliases":["snake"]},{"emoji":"🤧","aliases":["sneezing_face"]},{"emoji":"🏂","aliases":["snowboarder"]},{"emoji":"❄️","aliases":["snowflake"]},{"emoji":"⛄","aliases":["snowman"]},{"emoji":"☃️","aliases":["snowman_with_snow"]},{"emoji":"🧼","aliases":["soap"]},{"emoji":"😭","aliases":["sob"]},{"emoji":"⚽","aliases":["soccer"]},{"emoji":"🧦","aliases":["socks"]},{"emoji":"🥎","aliases":["softball"]},{"emoji":"🇸🇧","aliases":["solomon_islands"]},{"emoji":"🇸🇴","aliases":["somalia"]},{"emoji":"🔜","aliases":["soon"]},{"emoji":"🆘","aliases":["sos"]},{"emoji":"🔉","aliases":["sound"]},{"emoji":"🇿🇦","aliases":["south_africa"]},{"emoji":"🇬🇸","aliases":["south_georgia_south_sandwich_islands"]},{"emoji":"🇸🇸","aliases":["south_sudan"]},{"emoji":"👾","aliases":["space_invader"]},{"emoji":"♠️","aliases":["spades"]},{"emoji":"🍝","aliases":["spaghetti"]},{"emoji":"❇️","aliases":["sparkle"]},{"emoji":"🎇","aliases":["sparkler"]},{"emoji":"✨","aliases":["sparkles"]},{"emoji":"💖","aliases":["sparkling_heart"]},{"emoji":"🙊","aliases":["speak_no_evil"]},{"emoji":"🔈","aliases":["speaker"]},{"emoji":"🗣️","aliases":["speaking_head"]},{"emoji":"💬","aliases":["speech_balloon"]},{"emoji":"🚤","aliases":["speedboat"]},{"emoji":"🕷️","aliases":["spider"]},{"emoji":"🕸️","aliases":["spider_web"]},{"emoji":"🗓️","aliases":["spiral_calendar"]},{"emoji":"🗒️","aliases":["spiral_notepad"]},{"emoji":"🧽","aliases":["sponge"]},{"emoji":"🥄","aliases":["spoon"]},{"emoji":"🦑","aliases":["squid"]},{"emoji":"🇱🇰","aliases":["sri_lanka"]},{"emoji":"🇧🇱","aliases":["st_barthelemy"]},{"emoji":"🇸🇭","aliases":["st_helena"]},{"emoji":"🇰🇳","aliases":["st_kitts_nevis"]},{"emoji":"🇱🇨","aliases":["st_lucia"]},{"emoji":"🇲🇫","aliases":["st_martin"]},{"emoji":"🇵🇲","aliases":["st_pierre_miquelon"]},{"emoji":"🇻🇨","aliases":["st_vincent_grenadines"]},{"emoji":"🏟️","aliases":["stadium"]},{"emoji":"🧍♂️","aliases":["standing_man"]},{"emoji":"🧍","aliases":["standing_person"]},{"emoji":"🧍♀️","aliases":["standing_woman"]},{"emoji":"⭐","aliases":["star"]},{"emoji":"🌟","aliases":["star2"]},{"emoji":"☪️","aliases":["star_and_crescent"]},{"emoji":"✡️","aliases":["star_of_david"]},{"emoji":"🤩","aliases":["star_struck"]},{"emoji":"🌠","aliases":["stars"]},{"emoji":"🚉","aliases":["station"]},{"emoji":"🗽","aliases":["statue_of_liberty"]},{"emoji":"🚂","aliases":["steam_locomotive"]},{"emoji":"🩺","aliases":["stethoscope"]},{"emoji":"🍲","aliases":["stew"]},{"emoji":"⏹️","aliases":["stop_button"]},{"emoji":"🛑","aliases":["stop_sign"]},{"emoji":"⏱️","aliases":["stopwatch"]},{"emoji":"📏","aliases":["straight_ruler"]},{"emoji":"🍓","aliases":["strawberry"]},{"emoji":"😛","aliases":["stuck_out_tongue"]},{"emoji":"😝","aliases":["stuck_out_tongue_closed_eyes"]},{"emoji":"😜","aliases":["stuck_out_tongue_winking_eye"]},{"emoji":"🧑🎓","aliases":["student"]},{"emoji":"🎙️","aliases":["studio_microphone"]},{"emoji":"🥙","aliases":["stuffed_flatbread"]},{"emoji":"🇸🇩","aliases":["sudan"]},{"emoji":"🌥️","aliases":["sun_behind_large_cloud"]},{"emoji":"🌦️","aliases":["sun_behind_rain_cloud"]},{"emoji":"🌤️","aliases":["sun_behind_small_cloud"]},{"emoji":"🌞","aliases":["sun_with_face"]},{"emoji":"🌻","aliases":["sunflower"]},{"emoji":"😎","aliases":["sunglasses"]},{"emoji":"☀️","aliases":["sunny"]},{"emoji":"🌅","aliases":["sunrise"]},{"emoji":"🌄","aliases":["sunrise_over_mountains"]},{"emoji":"🦸","aliases":["superhero"]},{"emoji":"🦸♂️","aliases":["superhero_man"]},{"emoji":"🦸♀️","aliases":["superhero_woman"]},{"emoji":"🦹","aliases":["supervillain"]},{"emoji":"🦹♂️","aliases":["supervillain_man"]},{"emoji":"🦹♀️","aliases":["supervillain_woman"]},{"emoji":"🏄","aliases":["surfer"]},{"emoji":"🏄♂️","aliases":["surfing_man"]},{"emoji":"🏄♀️","aliases":["surfing_woman"]},{"emoji":"🇸🇷","aliases":["suriname"]},{"emoji":"🍣","aliases":["sushi"]},{"emoji":"🚟","aliases":["suspension_railway"]},{"emoji":"🇸🇯","aliases":["svalbard_jan_mayen"]},{"emoji":"🦢","aliases":["swan"]},{"emoji":"🇸🇿","aliases":["swaziland"]},{"emoji":"😓","aliases":["sweat"]},{"emoji":"💦","aliases":["sweat_drops"]},{"emoji":"😅","aliases":["sweat_smile"]},{"emoji":"🇸🇪","aliases":["sweden"]},{"emoji":"🍠","aliases":["sweet_potato"]},{"emoji":"🩲","aliases":["swim_brief"]},{"emoji":"🏊","aliases":["swimmer"]},{"emoji":"🏊♂️","aliases":["swimming_man"]},{"emoji":"🏊♀️","aliases":["swimming_woman"]},{"emoji":"🇨🇭","aliases":["switzerland"]},{"emoji":"🔣","aliases":["symbols"]},{"emoji":"🕍","aliases":["synagogue"]},{"emoji":"🇸🇾","aliases":["syria"]},{"emoji":"💉","aliases":["syringe"]},{"emoji":"🦖","aliases":["t-rex"]},{"emoji":"🌮","aliases":["taco"]},{"emoji":"🎉","aliases":["tada","hooray"]},{"emoji":"🇹🇼","aliases":["taiwan"]},{"emoji":"🇹🇯","aliases":["tajikistan"]},{"emoji":"🥡","aliases":["takeout_box"]},{"emoji":"🫔","aliases":["tamale"]},{"emoji":"🎋","aliases":["tanabata_tree"]},{"emoji":"🍊","aliases":["tangerine","orange","mandarin"]},{"emoji":"🇹🇿","aliases":["tanzania"]},{"emoji":"♉","aliases":["taurus"]},{"emoji":"🚕","aliases":["taxi"]},{"emoji":"🍵","aliases":["tea"]},{"emoji":"🧑🏫","aliases":["teacher"]},{"emoji":"🫖","aliases":["teapot"]},{"emoji":"🧑💻","aliases":["technologist"]},{"emoji":"🧸","aliases":["teddy_bear"]},{"emoji":"📞","aliases":["telephone_receiver"]},{"emoji":"🔭","aliases":["telescope"]},{"emoji":"🎾","aliases":["tennis"]},{"emoji":"⛺","aliases":["tent"]},{"emoji":"🧪","aliases":["test_tube"]},{"emoji":"🇹🇭","aliases":["thailand"]},{"emoji":"🌡️","aliases":["thermometer"]},{"emoji":"🤔","aliases":["thinking"]},{"emoji":"🩴","aliases":["thong_sandal"]},{"emoji":"💭","aliases":["thought_balloon"]},{"emoji":"🧵","aliases":["thread"]},{"emoji":"3️⃣","aliases":["three"]},{"emoji":"🎫","aliases":["ticket"]},{"emoji":"🎟️","aliases":["tickets"]},{"emoji":"🐯","aliases":["tiger"]},{"emoji":"🐅","aliases":["tiger2"]},{"emoji":"⏲️","aliases":["timer_clock"]},{"emoji":"🇹🇱","aliases":["timor_leste"]},{"emoji":"💁♂️","aliases":["tipping_hand_man","sassy_man"]},{"emoji":"💁","aliases":["tipping_hand_person","information_desk_person"]},{"emoji":"💁♀️","aliases":["tipping_hand_woman","sassy_woman"]},{"emoji":"😫","aliases":["tired_face"]},{"emoji":"™️","aliases":["tm"]},{"emoji":"🇹🇬","aliases":["togo"]},{"emoji":"🚽","aliases":["toilet"]},{"emoji":"🇹🇰","aliases":["tokelau"]},{"emoji":"🗼","aliases":["tokyo_tower"]},{"emoji":"🍅","aliases":["tomato"]},{"emoji":"🇹🇴","aliases":["tonga"]},{"emoji":"👅","aliases":["tongue"]},{"emoji":"🧰","aliases":["toolbox"]},{"emoji":"🦷","aliases":["tooth"]},{"emoji":"🪥","aliases":["toothbrush"]},{"emoji":"🔝","aliases":["top"]},{"emoji":"🎩","aliases":["tophat"]},{"emoji":"🌪️","aliases":["tornado"]},{"emoji":"🇹🇷","aliases":["tr"]},{"emoji":"🖲️","aliases":["trackball"]},{"emoji":"🚜","aliases":["tractor"]},{"emoji":"🚥","aliases":["traffic_light"]},{"emoji":"🚋","aliases":["train"]},{"emoji":"🚆","aliases":["train2"]},{"emoji":"🚊","aliases":["tram"]},{"emoji":"🏳️⚧️","aliases":["transgender_flag"]},{"emoji":"⚧️","aliases":["transgender_symbol"]},{"emoji":"🚩","aliases":["triangular_flag_on_post"]},{"emoji":"📐","aliases":["triangular_ruler"]},{"emoji":"🔱","aliases":["trident"]},{"emoji":"🇹🇹","aliases":["trinidad_tobago"]},{"emoji":"🇹🇦","aliases":["tristan_da_cunha"]},{"emoji":"😤","aliases":["triumph"]},{"emoji":"🧌","aliases":["troll"]},{"emoji":"🚎","aliases":["trolleybus"]},{"emoji":"🏆","aliases":["trophy"]},{"emoji":"🍹","aliases":["tropical_drink"]},{"emoji":"🐠","aliases":["tropical_fish"]},{"emoji":"🚚","aliases":["truck"]},{"emoji":"🎺","aliases":["trumpet"]},{"emoji":"🌷","aliases":["tulip"]},{"emoji":"🥃","aliases":["tumbler_glass"]},{"emoji":"🇹🇳","aliases":["tunisia"]},{"emoji":"🦃","aliases":["turkey"]},{"emoji":"🇹🇲","aliases":["turkmenistan"]},{"emoji":"🇹🇨","aliases":["turks_caicos_islands"]},{"emoji":"🐢","aliases":["turtle"]},{"emoji":"🇹🇻","aliases":["tuvalu"]},{"emoji":"📺","aliases":["tv"]},{"emoji":"🔀","aliases":["twisted_rightwards_arrows"]},{"emoji":"2️⃣","aliases":["two"]},{"emoji":"💕","aliases":["two_hearts"]},{"emoji":"👬","aliases":["two_men_holding_hands"]},{"emoji":"👭","aliases":["two_women_holding_hands"]},{"emoji":"🈹","aliases":["u5272"]},{"emoji":"🈴","aliases":["u5408"]},{"emoji":"🈺","aliases":["u55b6"]},{"emoji":"🈯","aliases":["u6307"]},{"emoji":"🈷️","aliases":["u6708"]},{"emoji":"🈶","aliases":["u6709"]},{"emoji":"🈵","aliases":["u6e80"]},{"emoji":"🈚","aliases":["u7121"]},{"emoji":"🈸","aliases":["u7533"]},{"emoji":"🈲","aliases":["u7981"]},{"emoji":"🈳","aliases":["u7a7a"]},{"emoji":"🇺🇬","aliases":["uganda"]},{"emoji":"🇺🇦","aliases":["ukraine"]},{"emoji":"☔","aliases":["umbrella"]},{"emoji":"😒","aliases":["unamused"]},{"emoji":"🔞","aliases":["underage"]},{"emoji":"🦄","aliases":["unicorn"]},{"emoji":"🇦🇪","aliases":["united_arab_emirates"]},{"emoji":"🇺🇳","aliases":["united_nations"]},{"emoji":"🔓","aliases":["unlock"]},{"emoji":"🆙","aliases":["up"]},{"emoji":"🙃","aliases":["upside_down_face"]},{"emoji":"🇺🇾","aliases":["uruguay"]},{"emoji":"🇺🇸","aliases":["us"]},{"emoji":"🇺🇲","aliases":["us_outlying_islands"]},{"emoji":"🇻🇮","aliases":["us_virgin_islands"]},{"emoji":"🇺🇿","aliases":["uzbekistan"]},{"emoji":"✌️","aliases":["v"]},{"emoji":"🧛","aliases":["vampire"]},{"emoji":"🧛♂️","aliases":["vampire_man"]},{"emoji":"🧛♀️","aliases":["vampire_woman"]},{"emoji":"🇻🇺","aliases":["vanuatu"]},{"emoji":"🇻🇦","aliases":["vatican_city"]},{"emoji":"🇻🇪","aliases":["venezuela"]},{"emoji":"🚦","aliases":["vertical_traffic_light"]},{"emoji":"📼","aliases":["vhs"]},{"emoji":"📳","aliases":["vibration_mode"]},{"emoji":"📹","aliases":["video_camera"]},{"emoji":"🎮","aliases":["video_game"]},{"emoji":"🇻🇳","aliases":["vietnam"]},{"emoji":"🎻","aliases":["violin"]},{"emoji":"♍","aliases":["virgo"]},{"emoji":"🌋","aliases":["volcano"]},{"emoji":"🏐","aliases":["volleyball"]},{"emoji":"🤮","aliases":["vomiting_face"]},{"emoji":"🆚","aliases":["vs"]},{"emoji":"🖖","aliases":["vulcan_salute"]},{"emoji":"🧇","aliases":["waffle"]},{"emoji":"🏴","aliases":["wales"]},{"emoji":"🚶","aliases":["walking"]},{"emoji":"🚶♂️","aliases":["walking_man"]},{"emoji":"🚶♀️","aliases":["walking_woman"]},{"emoji":"🇼🇫","aliases":["wallis_futuna"]},{"emoji":"🌘","aliases":["waning_crescent_moon"]},{"emoji":"🌖","aliases":["waning_gibbous_moon"]},{"emoji":"⚠️","aliases":["warning"]},{"emoji":"🗑️","aliases":["wastebasket"]},{"emoji":"⌚","aliases":["watch"]},{"emoji":"🐃","aliases":["water_buffalo"]},{"emoji":"🤽","aliases":["water_polo"]},{"emoji":"🍉","aliases":["watermelon"]},{"emoji":"👋","aliases":["wave"]},{"emoji":"〰️","aliases":["wavy_dash"]},{"emoji":"🌒","aliases":["waxing_crescent_moon"]},{"emoji":"🚾","aliases":["wc"]},{"emoji":"😩","aliases":["weary"]},{"emoji":"💒","aliases":["wedding"]},{"emoji":"🏋️","aliases":["weight_lifting"]},{"emoji":"🏋️♂️","aliases":["weight_lifting_man"]},{"emoji":"🏋️♀️","aliases":["weight_lifting_woman"]},{"emoji":"🇪🇭","aliases":["western_sahara"]},{"emoji":"🐳","aliases":["whale"]},{"emoji":"🐋","aliases":["whale2"]},{"emoji":"🛞","aliases":["wheel"]},{"emoji":"☸️","aliases":["wheel_of_dharma"]},{"emoji":"♿","aliases":["wheelchair"]},{"emoji":"✅","aliases":["white_check_mark"]},{"emoji":"⚪","aliases":["white_circle"]},{"emoji":"🏳️","aliases":["white_flag"]},{"emoji":"💮","aliases":["white_flower"]},{"emoji":"👨🦳","aliases":["white_haired_man"]},{"emoji":"👩🦳","aliases":["white_haired_woman"]},{"emoji":"🤍","aliases":["white_heart"]},{"emoji":"⬜","aliases":["white_large_square"]},{"emoji":"◽","aliases":["white_medium_small_square"]},{"emoji":"◻️","aliases":["white_medium_square"]},{"emoji":"▫️","aliases":["white_small_square"]},{"emoji":"🔳","aliases":["white_square_button"]},{"emoji":"🥀","aliases":["wilted_flower"]},{"emoji":"🎐","aliases":["wind_chime"]},{"emoji":"🌬️","aliases":["wind_face"]},{"emoji":"🪟","aliases":["window"]},{"emoji":"🍷","aliases":["wine_glass"]},{"emoji":"😉","aliases":["wink"]},{"emoji":"🐺","aliases":["wolf"]},{"emoji":"👩","aliases":["woman"]},{"emoji":"👩🎨","aliases":["woman_artist"]},{"emoji":"👩🚀","aliases":["woman_astronaut"]},{"emoji":"🧔♀️","aliases":["woman_beard"]},{"emoji":"🤸♀️","aliases":["woman_cartwheeling"]},{"emoji":"👩🍳","aliases":["woman_cook"]},{"emoji":"💃","aliases":["woman_dancing","dancer"]},{"emoji":"🤦♀️","aliases":["woman_facepalming"]},{"emoji":"👩🏭","aliases":["woman_factory_worker"]},{"emoji":"👩🌾","aliases":["woman_farmer"]},{"emoji":"👩🍼","aliases":["woman_feeding_baby"]},{"emoji":"👩🚒","aliases":["woman_firefighter"]},{"emoji":"👩⚕️","aliases":["woman_health_worker"]},{"emoji":"👩🦽","aliases":["woman_in_manual_wheelchair"]},{"emoji":"👩🦼","aliases":["woman_in_motorized_wheelchair"]},{"emoji":"🤵♀️","aliases":["woman_in_tuxedo"]},{"emoji":"👩⚖️","aliases":["woman_judge"]},{"emoji":"🤹♀️","aliases":["woman_juggling"]},{"emoji":"👩🔧","aliases":["woman_mechanic"]},{"emoji":"👩💼","aliases":["woman_office_worker"]},{"emoji":"👩✈️","aliases":["woman_pilot"]},{"emoji":"🤾♀️","aliases":["woman_playing_handball"]},{"emoji":"🤽♀️","aliases":["woman_playing_water_polo"]},{"emoji":"👩🔬","aliases":["woman_scientist"]},{"emoji":"🤷♀️","aliases":["woman_shrugging"]},{"emoji":"👩🎤","aliases":["woman_singer"]},{"emoji":"👩🎓","aliases":["woman_student"]},{"emoji":"👩🏫","aliases":["woman_teacher"]},{"emoji":"👩💻","aliases":["woman_technologist"]},{"emoji":"🧕","aliases":["woman_with_headscarf"]},{"emoji":"👩🦯","aliases":["woman_with_probing_cane"]},{"emoji":"👳♀️","aliases":["woman_with_turban"]},{"emoji":"👰♀️","aliases":["woman_with_veil","bride_with_veil"]},{"emoji":"👚","aliases":["womans_clothes"]},{"emoji":"👒","aliases":["womans_hat"]},{"emoji":"🤼♀️","aliases":["women_wrestling"]},{"emoji":"🚺","aliases":["womens"]},{"emoji":"🪵","aliases":["wood"]},{"emoji":"🥴","aliases":["woozy_face"]},{"emoji":"🗺️","aliases":["world_map"]},{"emoji":"🪱","aliases":["worm"]},{"emoji":"😟","aliases":["worried"]},{"emoji":"🔧","aliases":["wrench"]},{"emoji":"🤼","aliases":["wrestling"]},{"emoji":"✍️","aliases":["writing_hand"]},{"emoji":"❌","aliases":["x"]},{"emoji":"🩻","aliases":["x_ray"]},{"emoji":"🧶","aliases":["yarn"]},{"emoji":"🥱","aliases":["yawning_face"]},{"emoji":"🟡","aliases":["yellow_circle"]},{"emoji":"💛","aliases":["yellow_heart"]},{"emoji":"🟨","aliases":["yellow_square"]},{"emoji":"🇾🇪","aliases":["yemen"]},{"emoji":"💴","aliases":["yen"]},{"emoji":"☯️","aliases":["yin_yang"]},{"emoji":"🪀","aliases":["yo_yo"]},{"emoji":"😋","aliases":["yum"]},{"emoji":"🇿🇲","aliases":["zambia"]},{"emoji":"🤪","aliases":["zany_face"]},{"emoji":"⚡","aliases":["zap"]},{"emoji":"🦓","aliases":["zebra"]},{"emoji":"0️⃣","aliases":["zero"]},{"emoji":"🇿🇼","aliases":["zimbabwe"]},{"emoji":"🤐","aliases":["zipper_mouth_face"]},{"emoji":"🧟","aliases":["zombie"]},{"emoji":"🧟♂️","aliases":["zombie_man"]},{"emoji":"🧟♀️","aliases":["zombie_woman"]},{"emoji":"💤","aliases":["zzz"]}]
\ No newline at end of file
diff --git a/assets/favicon.svg b/assets/favicon.svg
new file mode 100644
index 0000000000000..9df6b83b564bd
--- /dev/null
+++ b/assets/favicon.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/go-licenses.json b/assets/go-licenses.json
new file mode 100644
index 0000000000000..30597388638f4
--- /dev/null
+++ b/assets/go-licenses.json
@@ -0,0 +1,977 @@
+[
+ {
+ "name": "cloud.google.com/go/compute/metadata",
+ "path": "cloud.google.com/go/compute/metadata/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "code.gitea.io/gitea/modules/lfs",
+ "path": "code.gitea.io/gitea/modules/lfs/LICENSE",
+ "licenseText": "Copyright (c) 2016 The Gitea Authors\nCopyright (c) GitHub, Inc. and LFS Test Server contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "code.gitea.io/sdk/gitea",
+ "path": "code.gitea.io/sdk/gitea/LICENSE",
+ "licenseText": "Copyright (c) 2016 The Gitea Authors\nCopyright (c) 2014 The Gogs Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "codeberg.org/gusted/mcaptcha",
+ "path": "codeberg.org/gusted/mcaptcha/LICENSE",
+ "licenseText": "Copyright © 2022 William Zijl\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "git.sr.ht/~mariusor/go-xsd-duration",
+ "path": "git.sr.ht/~mariusor/go-xsd-duration/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2019 Go xsd:duration\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "gitea.com/go-chi/binding",
+ "path": "gitea.com/go-chi/binding/LICENSE",
+ "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "gitea.com/go-chi/cache",
+ "path": "gitea.com/go-chi/cache/LICENSE",
+ "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "gitea.com/go-chi/captcha",
+ "path": "gitea.com/go-chi/captcha/LICENSE",
+ "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "gitea.com/go-chi/session",
+ "path": "gitea.com/go-chi/session/LICENSE",
+ "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "gitea.com/lunny/dingtalk_webhook",
+ "path": "gitea.com/lunny/dingtalk_webhook/LICENSE",
+ "licenseText": "Copyright (c) 2016 The Gitea Authors\nCopyright (c) 2015 The Gogs Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "gitea.com/lunny/levelqueue",
+ "path": "gitea.com/lunny/levelqueue/LICENSE",
+ "licenseText": "Copyright (c) 2019 Lunny Xiao\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/42wim/sshsig",
+ "path": "github.com/42wim/sshsig/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/Azure/go-ntlmssp",
+ "path": "github.com/Azure/go-ntlmssp/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Microsoft\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/NYTimes/gziphandler",
+ "path": "github.com/NYTimes/gziphandler/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2016-2017 The New York Times Company\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/RoaringBitmap/roaring",
+ "path": "github.com/RoaringBitmap/roaring/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2016 by the authors\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n================================================================================\n\nPortions of runcontainer.go are from the Go standard library, which is licensed\nunder:\n\nCopyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\n copyright notice, this list of conditions and the following disclaimer\n in the documentation and/or other materials provided with the\n distribution.\n * Neither the name of Google Inc. nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/alecthomas/chroma/v2",
+ "path": "github.com/alecthomas/chroma/v2/COPYING",
+ "licenseText": "Copyright (C) 2017 Alec Thomas\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/andybalholm/brotli",
+ "path": "github.com/andybalholm/brotli/LICENSE",
+ "licenseText": "Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/anmitsu/go-shlex",
+ "path": "github.com/anmitsu/go-shlex/LICENSE",
+ "licenseText": "Copyright (c) anmitsu \u003canmitsu.s@gmail.com\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/aymerick/douceur",
+ "path": "github.com/aymerick/douceur/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Aymerick JEHANNE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
+ },
+ {
+ "name": "github.com/beorn7/perks/quantile",
+ "path": "github.com/beorn7/perks/quantile/LICENSE",
+ "licenseText": "Copyright (C) 2013 Blake Mizerany\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/bits-and-blooms/bitset",
+ "path": "github.com/bits-and-blooms/bitset/LICENSE",
+ "licenseText": "Copyright (c) 2014 Will Fitzgerald. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/blevesearch/bleve/v2",
+ "path": "github.com/blevesearch/bleve/v2/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/blevesearch/bleve_index_api",
+ "path": "github.com/blevesearch/bleve_index_api/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/blevesearch/geo",
+ "path": "github.com/blevesearch/geo/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/blevesearch/go-porterstemmer",
+ "path": "github.com/blevesearch/go-porterstemmer/LICENSE",
+ "licenseText": "Copyright (c) 2013 Charles Iliya Krempeaux \u003ccharles@reptile.ca\u003e :: http://changelog.ca/\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/blevesearch/gtreap",
+ "path": "github.com/blevesearch/gtreap/LICENSE",
+ "licenseText": "Copyright (C) 2012 Steve Yen\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
+ },
+ {
+ "name": "github.com/blevesearch/mmap-go",
+ "path": "github.com/blevesearch/mmap-go/LICENSE",
+ "licenseText": "Copyright (c) 2011, Evan Shaw \u003cedsrzf@gmail.com\u003e\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n * Neither the name of the copyright holder nor the\n names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL \u003cCOPYRIGHT HOLDER\u003e BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n"
+ },
+ {
+ "name": "github.com/blevesearch/scorch_segment_api/v2",
+ "path": "github.com/blevesearch/scorch_segment_api/v2/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/blevesearch/segment",
+ "path": "github.com/blevesearch/segment/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/blevesearch/snowballstem",
+ "path": "github.com/blevesearch/snowballstem/COPYING",
+ "licenseText": "Copyright (c) 2001, Dr Martin Porter\nCopyright (c) 2004,2005, Richard Boulton\nCopyright (c) 2013, Yoshiki Shibukawa\nCopyright (c) 2006,2007,2009,2010,2011,2014-2019, Olly Betts\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n 1. Redistributions of source code must retain the above copyright notice,\n this list of conditions and the following disclaimer.\n 2. Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n 3. Neither the name of the Snowball project nor the names of its contributors\n may be used to endorse or promote products derived from this software\n without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/blevesearch/upsidedown_store_api",
+ "path": "github.com/blevesearch/upsidedown_store_api/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/blevesearch/vellum",
+ "path": "github.com/blevesearch/vellum/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/blevesearch/vellum/levenshtein",
+ "path": "github.com/blevesearch/vellum/levenshtein/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n"
+ },
+ {
+ "name": "github.com/blevesearch/zapx/v11",
+ "path": "github.com/blevesearch/zapx/v11/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/blevesearch/zapx/v12",
+ "path": "github.com/blevesearch/zapx/v12/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/blevesearch/zapx/v13",
+ "path": "github.com/blevesearch/zapx/v13/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/blevesearch/zapx/v14",
+ "path": "github.com/blevesearch/zapx/v14/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/blevesearch/zapx/v15",
+ "path": "github.com/blevesearch/zapx/v15/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/boombuler/barcode",
+ "path": "github.com/boombuler/barcode/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Florian Sundermann\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/bradfitz/gomemcache/memcache",
+ "path": "github.com/bradfitz/gomemcache/memcache/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/buildkite/terminal-to-html/v3",
+ "path": "github.com/buildkite/terminal-to-html/v3/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2019 Keith Pitt, Tim Lucas, Michael Pearson\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/caddyserver/certmagic",
+ "path": "github.com/caddyserver/certmagic/LICENSE.txt",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/cention-sany/utf7",
+ "path": "github.com/cention-sany/utf7/LICENSE",
+ "licenseText": "Copyright (c) 2013 The Go-IMAP Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n\n * Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the\n distribution.\n\n * Neither the name of the go-imap project nor the names of its\n contributors may be used to endorse or promote products derived\n from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/cespare/xxhash/v2",
+ "path": "github.com/cespare/xxhash/v2/LICENSE.txt",
+ "licenseText": "Copyright (c) 2016 Caleb Spare\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/chi-middleware/proxy",
+ "path": "github.com/chi-middleware/proxy/LICENSE",
+ "licenseText": "Copyright (c) 2020 Lauris BH\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/couchbase/go-couchbase",
+ "path": "github.com/couchbase/go-couchbase/LICENSE",
+ "licenseText": "Copyright (c) 2013 Couchbase, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/couchbase/gomemcached",
+ "path": "github.com/couchbase/gomemcached/LICENSE",
+ "licenseText": "Copyright (c) 2013 Dustin Sallings\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/couchbase/goutils",
+ "path": "github.com/couchbase/goutils/LICENSE.md",
+ "licenseText": "Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n"
+ },
+ {
+ "name": "github.com/cpuguy83/go-md2man/v2/md2man",
+ "path": "github.com/cpuguy83/go-md2man/v2/md2man/LICENSE.md",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Brian Goff\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/davecgh/go-spew/spew",
+ "path": "github.com/davecgh/go-spew/spew/LICENSE",
+ "licenseText": "ISC License\n\nCopyright (c) 2012-2016 Dave Collins \u003cdave@davec.name\u003e\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/denisenkom/go-mssqldb",
+ "path": "github.com/denisenkom/go-mssqldb/LICENSE.txt",
+ "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/dgryski/go-rendezvous",
+ "path": "github.com/dgryski/go-rendezvous/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2017-2020 Damian Gryski \u003cdamian@gryski.com\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/dimiro1/reply",
+ "path": "github.com/dimiro1/reply/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) Discourse\nCopyright (c) Claudemiro\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/djherbis/buffer",
+ "path": "github.com/djherbis/buffer/LICENSE.txt",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Dustin H\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/djherbis/nio/v3",
+ "path": "github.com/djherbis/nio/v3/LICENSE.txt",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Dustin H\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/dlclark/regexp2",
+ "path": "github.com/dlclark/regexp2/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) Doug Clark\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/dsnet/compress",
+ "path": "github.com/dsnet/compress/LICENSE.md",
+ "licenseText": "Copyright © 2015, Joe Tsai and The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n* Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation and/or\nother materials provided with the distribution.\n* Neither the copyright holder nor the names of its contributors may be used to\nendorse or promote products derived from this software without specific prior\nwritten permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/dustin/go-humanize",
+ "path": "github.com/dustin/go-humanize/LICENSE",
+ "licenseText": "Copyright (c) 2005-2008 Dustin Sallings \u003cdustin@spy.net\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\u003chttp://www.opensource.org/licenses/mit-license.php\u003e\n"
+ },
+ {
+ "name": "github.com/editorconfig/editorconfig-core-go/v2",
+ "path": "github.com/editorconfig/editorconfig-core-go/v2/LICENSE",
+ "licenseText": "MIT License\nCopyright (c) 2016 The Editorconfig Team\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/emersion/go-imap",
+ "path": "github.com/emersion/go-imap/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 The Go-IMAP Authors\nCopyright (c) 2016 emersion\nCopyright (c) 2016 Proton Technologies AG\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/emersion/go-sasl",
+ "path": "github.com/emersion/go-sasl/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 emersion\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/ethantkoenig/rupture",
+ "path": "github.com/ethantkoenig/rupture/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2018 Ethan Koenig\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/felixge/fgprof",
+ "path": "github.com/felixge/fgprof/LICENSE.txt",
+ "licenseText": "The MIT License (MIT)\nCopyright © 2020 Felix Geisendörfer \u003cfelix@felixge.de\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/fsnotify/fsnotify",
+ "path": "github.com/fsnotify/fsnotify/LICENSE",
+ "licenseText": "Copyright © 2012 The Go Authors. All rights reserved.\nCopyright © fsnotify Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n* Redistributions in binary form must reproduce the above copyright notice, this\n list of conditions and the following disclaimer in the documentation and/or\n other materials provided with the distribution.\n* Neither the name of Google Inc. nor the names of its contributors may be used\n to endorse or promote products derived from this software without specific\n prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/fxamacker/cbor/v2",
+ "path": "github.com/fxamacker/cbor/v2/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2019-present Faye Amacker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
+ },
+ {
+ "name": "github.com/gliderlabs/ssh",
+ "path": "github.com/gliderlabs/ssh/LICENSE",
+ "licenseText": "Copyright (c) 2016 Glider Labs. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Glider Labs nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/go-ap/activitypub",
+ "path": "github.com/go-ap/activitypub/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2017 Golang ActitvityPub\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/go-ap/errors",
+ "path": "github.com/go-ap/errors/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2019 Golang ActitvityPub\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/go-ap/jsonld",
+ "path": "github.com/go-ap/jsonld/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2017 Marius Orcsik\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/go-asn1-ber/asn1-ber",
+ "path": "github.com/go-asn1-ber/asn1-ber/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)\nPortions copyright (c) 2015-2016 go-asn1-ber Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/go-chi/chi/v5",
+ "path": "github.com/go-chi/chi/v5/LICENSE",
+ "licenseText": "Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc.\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/go-chi/cors",
+ "path": "github.com/go-chi/cors/LICENSE",
+ "licenseText": "Copyright (c) 2014 Olivier Poitrey \u003crs@dailymotion.com\u003e\nCopyright (c) 2016-Present https://github.com/go-chi authors\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/go-enry/go-enry/v2",
+ "path": "github.com/go-enry/go-enry/v2/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/go-fed/httpsig",
+ "path": "github.com/go-fed/httpsig/LICENSE",
+ "licenseText": "BSD 3-Clause License\n\nCopyright (c) 2018, go-fed\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/go-ldap/ldap/v3",
+ "path": "github.com/go-ldap/ldap/v3/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)\nPortions copyright (c) 2015-2016 go-ldap Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/go-redis/redis/v8",
+ "path": "github.com/go-redis/redis/v8/LICENSE",
+ "licenseText": "Copyright (c) 2013 The github.com/go-redis/redis Authors.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/go-sql-driver/mysql",
+ "path": "github.com/go-sql-driver/mysql/LICENSE",
+ "licenseText": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n means each individual or legal entity that creates, contributes to\n the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n means the combination of the Contributions of others (if any) used\n by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n means Source Code Form to which the initial Contributor has attached\n the notice in Exhibit A, the Executable Form of such Source Code\n Form, and Modifications of such Source Code Form, in each case\n including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n (a) that the initial Contributor has attached the notice described\n in Exhibit B to the Covered Software; or\n\n (b) that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the\n terms of a Secondary License.\n\n1.6. \"Executable Form\"\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n means a work that combines Covered Software with other material, in \n a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n means this document.\n\n1.9. \"Licensable\"\n means having the right to grant, to the maximum extent possible,\n whether at the time of the initial grant or subsequently, any and\n all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n means any of the following:\n\n (a) any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered\n Software; or\n\n (b) any new file in Source Code Form that contains any Covered\n Software.\n\n1.11. \"Patent Claims\" of a Contributor\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the\n License, by the making, using, selling, offering for sale, having\n made, import, or transfer of either its Contributions or its\n Contributor Version.\n\n1.12. \"Secondary License\"\n means either the GNU General Public License, Version 2.0, the GNU\n Lesser General Public License, Version 2.1, the GNU Affero General\n Public License, Version 3.0, or any later versions of those\n licenses.\n\n1.13. \"Source Code Form\"\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that\n controls, is controlled by, or is under common control with You. For\n purposes of this definition, \"control\" means (a) the power, direct\n or indirect, to cause the direction or management of such entity,\n whether by contract or otherwise, or (b) ownership of more than\n fifty percent (50%) of the outstanding shares or beneficial\n ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n for sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n or\n\n(b) for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n Form, as described in Section 3.1, and You must inform recipients of\n the Executable Form how they can obtain a copy of such Source Code\n Form by reasonable means in a timely manner, at a charge no more\n than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter\n the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n* *\n* 6. Disclaimer of Warranty *\n* ------------------------- *\n* *\n* Covered Software is provided under this License on an \"as is\" *\n* basis, without warranty of any kind, either expressed, implied, or *\n* statutory, including, without limitation, warranties that the *\n* Covered Software is free of defects, merchantable, fit for a *\n* particular purpose or non-infringing. The entire risk as to the *\n* quality and performance of the Covered Software is with You. *\n* Should any Covered Software prove defective in any respect, You *\n* (not any Contributor) assume the cost of any necessary servicing, *\n* repair, or correction. This disclaimer of warranty constitutes an *\n* essential part of this License. No use of any Covered Software is *\n* authorized under this License except under this disclaimer. *\n* *\n************************************************************************\n\n************************************************************************\n* *\n* 7. Limitation of Liability *\n* -------------------------- *\n* *\n* Under no circumstances and under no legal theory, whether tort *\n* (including negligence), contract, or otherwise, shall any *\n* Contributor, or anyone who distributes Covered Software as *\n* permitted above, be liable to You for any direct, indirect, *\n* special, incidental, or consequential damages of any character *\n* including, without limitation, damages for lost profits, loss of *\n* goodwill, work stoppage, computer failure or malfunction, or any *\n* and all other commercial damages or losses, even if such party *\n* shall have been informed of the possibility of such damages. This *\n* limitation of liability shall not apply to liability for death or *\n* personal injury resulting from such party's negligence to the *\n* extent applicable law prohibits such limitation. Some *\n* jurisdictions do not allow the exclusion or limitation of *\n* incidental or consequential damages, so this exclusion and *\n* limitation may not apply to You. *\n* *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n This Source Code Form is subject to the terms of the Mozilla Public\n License, v. 2.0. If a copy of the MPL was not distributed with this\n file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n This Source Code Form is \"Incompatible With Secondary Licenses\", as\n defined by the Mozilla Public License, v. 2.0.\n"
+ },
+ {
+ "name": "github.com/go-testfixtures/testfixtures/v3",
+ "path": "github.com/go-testfixtures/testfixtures/v3/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Andrey Nering\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/go-webauthn/revoke",
+ "path": "github.com/go-webauthn/revoke/LICENSE",
+ "licenseText": "BSD 2-Clause License\n\nCopyright (c) 2022, go-webauthn\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\nCopyright (c) 2014 CloudFlare Inc.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\nRedistributions of source code must retain the above copyright notice,\nthis list of conditions and the following disclaimer.\n\nRedistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation\nand/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED\nTO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/go-webauthn/webauthn",
+ "path": "github.com/go-webauthn/webauthn/LICENSE",
+ "licenseText": "Copyright (c) 2017 Duo Security, Inc. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\nCopyright (c) 2021-2022 github.com/go-webauthn/webauthn authors.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the\nfollowing conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following\n disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following\n disclaimer in the documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\nTHIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+ },
+ {
+ "name": "github.com/gobwas/glob",
+ "path": "github.com/gobwas/glob/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Sergey Kamardin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
+ },
+ {
+ "name": "github.com/goccy/go-json",
+ "path": "github.com/goccy/go-json/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2020 Masaaki Goshima\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/gogs/chardet",
+ "path": "github.com/gogs/chardet/LICENSE",
+ "licenseText": "Copyright (c) 2012 chardet Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\nPartial of the Software is derived from ICU project. See icu-license.html for\nlicense of the derivative portions.\n"
+ },
+ {
+ "name": "github.com/gogs/cron",
+ "path": "github.com/gogs/cron/LICENSE",
+ "licenseText": "Copyright (C) 2012 Rob Figueiredo\nAll Rights Reserved.\n\nMIT LICENSE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/gogs/go-gogs-client",
+ "path": "github.com/gogs/go-gogs-client/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Go Git Service\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
+ },
+ {
+ "name": "github.com/golang-jwt/jwt/v4",
+ "path": "github.com/golang-jwt/jwt/v4/LICENSE",
+ "licenseText": "Copyright (c) 2012 Dave Grijalva\nCopyright (c) 2021 golang-jwt maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n"
+ },
+ {
+ "name": "github.com/golang-sql/civil",
+ "path": "github.com/golang-sql/civil/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/golang-sql/sqlexp",
+ "path": "github.com/golang-sql/sqlexp/LICENSE",
+ "licenseText": "Copyright (c) 2017 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/golang/geo",
+ "path": "github.com/golang/geo/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/golang/protobuf",
+ "path": "github.com/golang/protobuf/LICENSE",
+ "licenseText": "Copyright 2010 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n"
+ },
+ {
+ "name": "github.com/golang/snappy",
+ "path": "github.com/golang/snappy/LICENSE",
+ "licenseText": "Copyright (c) 2011 The Snappy-Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/google/go-github/v45/github",
+ "path": "github.com/google/go-github/v45/github/LICENSE",
+ "licenseText": "Copyright (c) 2013 The go-github AUTHORS. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/google/go-querystring/query",
+ "path": "github.com/google/go-querystring/query/LICENSE",
+ "licenseText": "Copyright (c) 2013 Google. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/google/go-tpm",
+ "path": "github.com/google/go-tpm/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/google/pprof/profile",
+ "path": "github.com/google/pprof/profile/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/google/uuid",
+ "path": "github.com/google/uuid/LICENSE",
+ "licenseText": "Copyright (c) 2009,2014 Google Inc. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/gorilla/css/scanner",
+ "path": "github.com/gorilla/css/scanner/LICENSE",
+ "licenseText": "Copyright (c) 2013, Gorilla web toolkit\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n Redistributions in binary form must reproduce the above copyright notice, this\n list of conditions and the following disclaimer in the documentation and/or\n other materials provided with the distribution.\n\n Neither the name of the {organization} nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/gorilla/feeds",
+ "path": "github.com/gorilla/feeds/LICENSE",
+ "licenseText": "Copyright (c) 2013-2018 The Gorilla Feeds Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/gorilla/mux",
+ "path": "github.com/gorilla/mux/LICENSE",
+ "licenseText": "Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/gorilla/securecookie",
+ "path": "github.com/gorilla/securecookie/LICENSE",
+ "licenseText": "Copyright (c) 2012 Rodrigo Moraes. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/gorilla/sessions",
+ "path": "github.com/gorilla/sessions/LICENSE",
+ "licenseText": "Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\t * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/hashicorp/errwrap",
+ "path": "github.com/hashicorp/errwrap/LICENSE",
+ "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. “Contributor”\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. “Contributor Version”\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution”\n\n means Covered Software of a particular Contributor.\n\n1.4. “Covered Software”\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. “Incompatible With Secondary Licenses”\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of version\n 1.1 or earlier of the License, but not also under the terms of a\n Secondary License.\n\n1.6. “Executable Form”\n\n means any form of the work other than Source Code Form.\n\n1.7. “Larger Work”\n\n means a work that combines Covered Software with other material, in a separate\n file or files, that is not Covered Software.\n\n1.8. “License”\n\n means this document.\n\n1.9. “Licensable”\n\n means having the right to grant, to the maximum extent possible, whether at the\n time of the initial grant or subsequently, any and all of the rights conveyed by\n this License.\n\n1.10. “Modifications”\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to, deletion\n from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor\n\n means any patent claim(s), including without limitation, method, process,\n and apparatus claims, in any patent Licensable by such Contributor that\n would be infringed, but for the grant of the License, by the making,\n using, selling, offering for sale, having made, import, or transfer of\n either its Contributions or its Contributor Version.\n\n1.12. “Secondary License”\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form”\n\n means the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”)\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, “You” includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, “control” means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-exclusive license:\n\n a. under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or as\n part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its Contributions\n or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution become\n effective for each Contribution on the date the Contributor first distributes\n such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under this\n License. No additional rights or licenses will be implied from the distribution\n or licensing of Covered Software under this License. Notwithstanding Section\n 2.1(b) above, no patent license is granted by a Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; or\n\n b. for infringements caused by: (i) Your and any other third party’s\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n c. under Patent Claims infringed by Covered Software in the absence of its\n Contributions.\n\n This License does not grant any rights in the trademarks, service marks, or\n logos of any Contributor (except as may be necessary to comply with the\n notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this License\n (see Section 10.2) or under the terms of a Secondary License (if permitted\n under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its Contributions\n are its original creation(s) or it has sufficient rights to grant the\n rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under applicable\n copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under the\n terms of this License. You must inform recipients that the Source Code Form\n of the Covered Software is governed by the terms of this License, and how\n they can obtain a copy of this License. You may not attempt to alter or\n restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this License,\n or sublicense it under different terms, provided that the license for\n the Executable Form does not attempt to limit or alter the recipients’\n rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for the\n Covered Software. If the Larger Work is a combination of Covered Software\n with a work governed by one or more Secondary Licenses, and the Covered\n Software is not Incompatible With Secondary Licenses, this License permits\n You to additionally distribute such Covered Software under the terms of\n such Secondary License(s), so that the recipient of the Larger Work may, at\n their option, further distribute the Covered Software under the terms of\n either this License or such Secondary License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices (including\n copyright notices, patent notices, disclaimers of warranty, or limitations\n of liability) contained within the Source Code Form of the Covered\n Software, except that You may alter any license notices to the extent\n required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on behalf\n of any Contributor. You must make it absolutely clear that any such\n warranty, support, indemnity, or liability obligation is offered by You\n alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute, judicial\n order, or regulation then You must: (a) comply with the terms of this License\n to the maximum extent possible; and (b) describe the limitations and the code\n they affect. Such description must be placed in a text file included with all\n distributions of the Covered Software under this License. Except to the\n extent prohibited by statute or regulation, such description must be\n sufficiently detailed for a recipient of ordinary skill to be able to\n understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing basis,\n if such Contributor fails to notify You of the non-compliance by some\n reasonable means prior to 60 days after You have come back into compliance.\n Moreover, Your grants from a particular Contributor are reinstated on an\n ongoing basis if such Contributor notifies You of the non-compliance by\n some reasonable means, this is the first time You have received notice of\n non-compliance with this License from such Contributor, and You become\n compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions, counter-claims,\n and cross-claims) alleging that a Contributor Version directly or\n indirectly infringes any patent, then the rights granted to You by any and\n all Contributors for the Covered Software under Section 2.1 of this License\n shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an “as is” basis, without\n warranty of any kind, either expressed, implied, or statutory, including,\n without limitation, warranties that the Covered Software is free of defects,\n merchantable, fit for a particular purpose or non-infringing. The entire\n risk as to the quality and performance of the Covered Software is with You.\n Should any Covered Software prove defective in any respect, You (not any\n Contributor) assume the cost of any necessary servicing, repair, or\n correction. This disclaimer of warranty constitutes an essential part of this\n License. No use of any Covered Software is authorized under this License\n except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from such\n party’s negligence to the extent applicable law prohibits such limitation.\n Some jurisdictions do not allow the exclusion or limitation of incidental or\n consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts of\n a jurisdiction where the defendant maintains its principal place of business\n and such litigation shall be governed by laws of that jurisdiction, without\n reference to its conflict-of-law provisions. Nothing in this Section shall\n prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject matter\n hereof. If any provision of this License is held to be unenforceable, such\n provision shall be reformed only to the extent necessary to make it\n enforceable. Any law or regulation which provides that the language of a\n contract shall be construed against the drafter shall not be used to construe\n this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version of\n the License under which You originally received the Covered Software, or\n under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a modified\n version of this License if you rename the license and remove any\n references to the name of the license steward (except to note that such\n modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses\n If You choose to distribute Source Code Form that is Incompatible With\n Secondary Licenses under the terms of this version of the License, the\n notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then\nYou may include the notice in a location (such as a LICENSE file in a relevant\ndirectory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice\n\n This Source Code Form is “Incompatible\n With Secondary Licenses”, as defined by\n the Mozilla Public License, v. 2.0.\n\n"
+ },
+ {
+ "name": "github.com/hashicorp/go-cleanhttp",
+ "path": "github.com/hashicorp/go-cleanhttp/LICENSE",
+ "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the terms of\n a Secondary License.\n\n1.6. \"Executable Form\"\n\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n\n means a work that combines Covered Software with other material, in a\n separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n\n means this document.\n\n1.9. \"Licensable\"\n\n means having the right to grant, to the maximum extent possible, whether\n at the time of the initial grant or subsequently, any and all of the\n rights conveyed by this License.\n\n1.10. \"Modifications\"\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. \"Patent Claims\" of a Contributor\n\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the License,\n by the making, using, selling, offering for sale, having made, import,\n or transfer of either its Contributions or its Contributor Version.\n\n1.12. \"Secondary License\"\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. \"Source Code Form\"\n\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, \"control\" means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-exclusive license:\n\n a. under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution\n become effective for each Contribution on the date the Contributor first\n distributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under\n this License. No additional rights or licenses will be implied from the\n distribution or licensing of Covered Software under this License.\n Notwithstanding Section 2.1(b) above, no patent license is granted by a\n Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; or\n\n b. for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n c. under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\n This License does not grant any rights in the trademarks, service marks,\n or logos of any Contributor (except as may be necessary to comply with\n the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this\n License (see Section 10.2) or under the terms of a Secondary License (if\n permitted under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its\n Contributions are its original creation(s) or it has sufficient rights to\n grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under\n applicable copyright doctrines of fair use, fair dealing, or other\n equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under\n the terms of this License. You must inform recipients that the Source\n Code Form of the Covered Software is governed by the terms of this\n License, and how they can obtain a copy of this License. You may not\n attempt to alter or restrict the recipients' rights in the Source Code\n Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter the\n recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for\n the Covered Software. If the Larger Work is a combination of Covered\n Software with a work governed by one or more Secondary Licenses, and the\n Covered Software is not Incompatible With Secondary Licenses, this\n License permits You to additionally distribute such Covered Software\n under the terms of such Secondary License(s), so that the recipient of\n the Larger Work may, at their option, further distribute the Covered\n Software under the terms of either this License or such Secondary\n License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices\n (including copyright notices, patent notices, disclaimers of warranty, or\n limitations of liability) contained within the Source Code Form of the\n Covered Software, except that You may alter any license notices to the\n extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on\n behalf of any Contributor. You must make it absolutely clear that any\n such warranty, support, indemnity, or liability obligation is offered by\n You alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute,\n judicial order, or regulation then You must: (a) comply with the terms of\n this License to the maximum extent possible; and (b) describe the\n limitations and the code they affect. Such description must be placed in a\n text file included with all distributions of the Covered Software under\n this License. Except to the extent prohibited by statute or regulation,\n such description must be sufficiently detailed for a recipient of ordinary\n skill to be able to understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing\n basis, if such Contributor fails to notify You of the non-compliance by\n some reasonable means prior to 60 days after You have come back into\n compliance. Moreover, Your grants from a particular Contributor are\n reinstated on an ongoing basis if such Contributor notifies You of the\n non-compliance by some reasonable means, this is the first time You have\n received notice of non-compliance with this License from such\n Contributor, and You become compliant prior to 30 days after Your receipt\n of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions,\n counter-claims, and cross-claims) alleging that a Contributor Version\n directly or indirectly infringes any patent, then the rights granted to\n You by any and all Contributors for the Covered Software under Section\n 2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an \"as is\" basis,\n without warranty of any kind, either expressed, implied, or statutory,\n including, without limitation, warranties that the Covered Software is free\n of defects, merchantable, fit for a particular purpose or non-infringing.\n The entire risk as to the quality and performance of the Covered Software\n is with You. Should any Covered Software prove defective in any respect,\n You (not any Contributor) assume the cost of any necessary servicing,\n repair, or correction. This disclaimer of warranty constitutes an essential\n part of this License. No use of any Covered Software is authorized under\n this License except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from\n such party's negligence to the extent applicable law prohibits such\n limitation. Some jurisdictions do not allow the exclusion or limitation of\n incidental or consequential damages, so this exclusion and limitation may\n not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts\n of a jurisdiction where the defendant maintains its principal place of\n business and such litigation shall be governed by laws of that\n jurisdiction, without reference to its conflict-of-law provisions. Nothing\n in this Section shall prevent a party's ability to bring cross-claims or\n counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject\n matter hereof. If any provision of this License is held to be\n unenforceable, such provision shall be reformed only to the extent\n necessary to make it enforceable. Any law or regulation which provides that\n the language of a contract shall be construed against the drafter shall not\n be used to construe this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version\n of the License under which You originally received the Covered Software,\n or under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a\n modified version of this License if you rename the license and remove\n any references to the name of the license steward (except to note that\n such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\n Licenses If You choose to distribute Source Code Form that is\n Incompatible With Secondary Licenses under the terms of this version of\n the License, the notice described in Exhibit B of this License must be\n attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file,\nthen You may include the notice in a location (such as a LICENSE file in a\nrelevant directory) where a recipient would be likely to look for such a\nnotice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n\n This Source Code Form is \"Incompatible\n With Secondary Licenses\", as defined by\n the Mozilla Public License, v. 2.0.\n\n"
+ },
+ {
+ "name": "github.com/hashicorp/go-multierror",
+ "path": "github.com/hashicorp/go-multierror/LICENSE",
+ "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. “Contributor”\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. “Contributor Version”\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution”\n\n means Covered Software of a particular Contributor.\n\n1.4. “Covered Software”\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. “Incompatible With Secondary Licenses”\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of version\n 1.1 or earlier of the License, but not also under the terms of a\n Secondary License.\n\n1.6. “Executable Form”\n\n means any form of the work other than Source Code Form.\n\n1.7. “Larger Work”\n\n means a work that combines Covered Software with other material, in a separate\n file or files, that is not Covered Software.\n\n1.8. “License”\n\n means this document.\n\n1.9. “Licensable”\n\n means having the right to grant, to the maximum extent possible, whether at the\n time of the initial grant or subsequently, any and all of the rights conveyed by\n this License.\n\n1.10. “Modifications”\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to, deletion\n from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor\n\n means any patent claim(s), including without limitation, method, process,\n and apparatus claims, in any patent Licensable by such Contributor that\n would be infringed, but for the grant of the License, by the making,\n using, selling, offering for sale, having made, import, or transfer of\n either its Contributions or its Contributor Version.\n\n1.12. “Secondary License”\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form”\n\n means the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”)\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, “You” includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, “control” means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-exclusive license:\n\n a. under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or as\n part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its Contributions\n or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution become\n effective for each Contribution on the date the Contributor first distributes\n such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under this\n License. No additional rights or licenses will be implied from the distribution\n or licensing of Covered Software under this License. Notwithstanding Section\n 2.1(b) above, no patent license is granted by a Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; or\n\n b. for infringements caused by: (i) Your and any other third party’s\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n c. under Patent Claims infringed by Covered Software in the absence of its\n Contributions.\n\n This License does not grant any rights in the trademarks, service marks, or\n logos of any Contributor (except as may be necessary to comply with the\n notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this License\n (see Section 10.2) or under the terms of a Secondary License (if permitted\n under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its Contributions\n are its original creation(s) or it has sufficient rights to grant the\n rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under applicable\n copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under the\n terms of this License. You must inform recipients that the Source Code Form\n of the Covered Software is governed by the terms of this License, and how\n they can obtain a copy of this License. You may not attempt to alter or\n restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this License,\n or sublicense it under different terms, provided that the license for\n the Executable Form does not attempt to limit or alter the recipients’\n rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for the\n Covered Software. If the Larger Work is a combination of Covered Software\n with a work governed by one or more Secondary Licenses, and the Covered\n Software is not Incompatible With Secondary Licenses, this License permits\n You to additionally distribute such Covered Software under the terms of\n such Secondary License(s), so that the recipient of the Larger Work may, at\n their option, further distribute the Covered Software under the terms of\n either this License or such Secondary License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices (including\n copyright notices, patent notices, disclaimers of warranty, or limitations\n of liability) contained within the Source Code Form of the Covered\n Software, except that You may alter any license notices to the extent\n required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on behalf\n of any Contributor. You must make it absolutely clear that any such\n warranty, support, indemnity, or liability obligation is offered by You\n alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute, judicial\n order, or regulation then You must: (a) comply with the terms of this License\n to the maximum extent possible; and (b) describe the limitations and the code\n they affect. Such description must be placed in a text file included with all\n distributions of the Covered Software under this License. Except to the\n extent prohibited by statute or regulation, such description must be\n sufficiently detailed for a recipient of ordinary skill to be able to\n understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing basis,\n if such Contributor fails to notify You of the non-compliance by some\n reasonable means prior to 60 days after You have come back into compliance.\n Moreover, Your grants from a particular Contributor are reinstated on an\n ongoing basis if such Contributor notifies You of the non-compliance by\n some reasonable means, this is the first time You have received notice of\n non-compliance with this License from such Contributor, and You become\n compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions, counter-claims,\n and cross-claims) alleging that a Contributor Version directly or\n indirectly infringes any patent, then the rights granted to You by any and\n all Contributors for the Covered Software under Section 2.1 of this License\n shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an “as is” basis, without\n warranty of any kind, either expressed, implied, or statutory, including,\n without limitation, warranties that the Covered Software is free of defects,\n merchantable, fit for a particular purpose or non-infringing. The entire\n risk as to the quality and performance of the Covered Software is with You.\n Should any Covered Software prove defective in any respect, You (not any\n Contributor) assume the cost of any necessary servicing, repair, or\n correction. This disclaimer of warranty constitutes an essential part of this\n License. No use of any Covered Software is authorized under this License\n except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from such\n party’s negligence to the extent applicable law prohibits such limitation.\n Some jurisdictions do not allow the exclusion or limitation of incidental or\n consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts of\n a jurisdiction where the defendant maintains its principal place of business\n and such litigation shall be governed by laws of that jurisdiction, without\n reference to its conflict-of-law provisions. Nothing in this Section shall\n prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject matter\n hereof. If any provision of this License is held to be unenforceable, such\n provision shall be reformed only to the extent necessary to make it\n enforceable. Any law or regulation which provides that the language of a\n contract shall be construed against the drafter shall not be used to construe\n this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version of\n the License under which You originally received the Covered Software, or\n under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a modified\n version of this License if you rename the license and remove any\n references to the name of the license steward (except to note that such\n modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses\n If You choose to distribute Source Code Form that is Incompatible With\n Secondary Licenses under the terms of this version of the License, the\n notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then\nYou may include the notice in a location (such as a LICENSE file in a relevant\ndirectory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice\n\n This Source Code Form is “Incompatible\n With Secondary Licenses”, as defined by\n the Mozilla Public License, v. 2.0.\n"
+ },
+ {
+ "name": "github.com/hashicorp/go-retryablehttp",
+ "path": "github.com/hashicorp/go-retryablehttp/LICENSE",
+ "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the terms of\n a Secondary License.\n\n1.6. \"Executable Form\"\n\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n\n means a work that combines Covered Software with other material, in a\n separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n\n means this document.\n\n1.9. \"Licensable\"\n\n means having the right to grant, to the maximum extent possible, whether\n at the time of the initial grant or subsequently, any and all of the\n rights conveyed by this License.\n\n1.10. \"Modifications\"\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. \"Patent Claims\" of a Contributor\n\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the License,\n by the making, using, selling, offering for sale, having made, import,\n or transfer of either its Contributions or its Contributor Version.\n\n1.12. \"Secondary License\"\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. \"Source Code Form\"\n\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, \"control\" means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-exclusive license:\n\n a. under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution\n become effective for each Contribution on the date the Contributor first\n distributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under\n this License. No additional rights or licenses will be implied from the\n distribution or licensing of Covered Software under this License.\n Notwithstanding Section 2.1(b) above, no patent license is granted by a\n Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; or\n\n b. for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n c. under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\n This License does not grant any rights in the trademarks, service marks,\n or logos of any Contributor (except as may be necessary to comply with\n the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this\n License (see Section 10.2) or under the terms of a Secondary License (if\n permitted under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its\n Contributions are its original creation(s) or it has sufficient rights to\n grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under\n applicable copyright doctrines of fair use, fair dealing, or other\n equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under\n the terms of this License. You must inform recipients that the Source\n Code Form of the Covered Software is governed by the terms of this\n License, and how they can obtain a copy of this License. You may not\n attempt to alter or restrict the recipients' rights in the Source Code\n Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter the\n recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for\n the Covered Software. If the Larger Work is a combination of Covered\n Software with a work governed by one or more Secondary Licenses, and the\n Covered Software is not Incompatible With Secondary Licenses, this\n License permits You to additionally distribute such Covered Software\n under the terms of such Secondary License(s), so that the recipient of\n the Larger Work may, at their option, further distribute the Covered\n Software under the terms of either this License or such Secondary\n License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices\n (including copyright notices, patent notices, disclaimers of warranty, or\n limitations of liability) contained within the Source Code Form of the\n Covered Software, except that You may alter any license notices to the\n extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on\n behalf of any Contributor. You must make it absolutely clear that any\n such warranty, support, indemnity, or liability obligation is offered by\n You alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute,\n judicial order, or regulation then You must: (a) comply with the terms of\n this License to the maximum extent possible; and (b) describe the\n limitations and the code they affect. Such description must be placed in a\n text file included with all distributions of the Covered Software under\n this License. Except to the extent prohibited by statute or regulation,\n such description must be sufficiently detailed for a recipient of ordinary\n skill to be able to understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing\n basis, if such Contributor fails to notify You of the non-compliance by\n some reasonable means prior to 60 days after You have come back into\n compliance. Moreover, Your grants from a particular Contributor are\n reinstated on an ongoing basis if such Contributor notifies You of the\n non-compliance by some reasonable means, this is the first time You have\n received notice of non-compliance with this License from such\n Contributor, and You become compliant prior to 30 days after Your receipt\n of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions,\n counter-claims, and cross-claims) alleging that a Contributor Version\n directly or indirectly infringes any patent, then the rights granted to\n You by any and all Contributors for the Covered Software under Section\n 2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an \"as is\" basis,\n without warranty of any kind, either expressed, implied, or statutory,\n including, without limitation, warranties that the Covered Software is free\n of defects, merchantable, fit for a particular purpose or non-infringing.\n The entire risk as to the quality and performance of the Covered Software\n is with You. Should any Covered Software prove defective in any respect,\n You (not any Contributor) assume the cost of any necessary servicing,\n repair, or correction. This disclaimer of warranty constitutes an essential\n part of this License. No use of any Covered Software is authorized under\n this License except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from\n such party's negligence to the extent applicable law prohibits such\n limitation. Some jurisdictions do not allow the exclusion or limitation of\n incidental or consequential damages, so this exclusion and limitation may\n not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts\n of a jurisdiction where the defendant maintains its principal place of\n business and such litigation shall be governed by laws of that\n jurisdiction, without reference to its conflict-of-law provisions. Nothing\n in this Section shall prevent a party's ability to bring cross-claims or\n counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject\n matter hereof. If any provision of this License is held to be\n unenforceable, such provision shall be reformed only to the extent\n necessary to make it enforceable. Any law or regulation which provides that\n the language of a contract shall be construed against the drafter shall not\n be used to construe this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version\n of the License under which You originally received the Covered Software,\n or under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a\n modified version of this License if you rename the license and remove\n any references to the name of the license steward (except to note that\n such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\n Licenses If You choose to distribute Source Code Form that is\n Incompatible With Secondary Licenses under the terms of this version of\n the License, the notice described in Exhibit B of this License must be\n attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file,\nthen You may include the notice in a location (such as a LICENSE file in a\nrelevant directory) where a recipient would be likely to look for such a\nnotice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n\n This Source Code Form is \"Incompatible\n With Secondary Licenses\", as defined by\n the Mozilla Public License, v. 2.0.\n\n"
+ },
+ {
+ "name": "github.com/hashicorp/go-version",
+ "path": "github.com/hashicorp/go-version/LICENSE",
+ "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. “Contributor”\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. “Contributor Version”\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution”\n\n means Covered Software of a particular Contributor.\n\n1.4. “Covered Software”\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. “Incompatible With Secondary Licenses”\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of version\n 1.1 or earlier of the License, but not also under the terms of a\n Secondary License.\n\n1.6. “Executable Form”\n\n means any form of the work other than Source Code Form.\n\n1.7. “Larger Work”\n\n means a work that combines Covered Software with other material, in a separate\n file or files, that is not Covered Software.\n\n1.8. “License”\n\n means this document.\n\n1.9. “Licensable”\n\n means having the right to grant, to the maximum extent possible, whether at the\n time of the initial grant or subsequently, any and all of the rights conveyed by\n this License.\n\n1.10. “Modifications”\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to, deletion\n from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor\n\n means any patent claim(s), including without limitation, method, process,\n and apparatus claims, in any patent Licensable by such Contributor that\n would be infringed, but for the grant of the License, by the making,\n using, selling, offering for sale, having made, import, or transfer of\n either its Contributions or its Contributor Version.\n\n1.12. “Secondary License”\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form”\n\n means the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”)\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, “You” includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, “control” means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-exclusive license:\n\n a. under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or as\n part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its Contributions\n or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution become\n effective for each Contribution on the date the Contributor first distributes\n such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under this\n License. No additional rights or licenses will be implied from the distribution\n or licensing of Covered Software under this License. Notwithstanding Section\n 2.1(b) above, no patent license is granted by a Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; or\n\n b. for infringements caused by: (i) Your and any other third party’s\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n c. under Patent Claims infringed by Covered Software in the absence of its\n Contributions.\n\n This License does not grant any rights in the trademarks, service marks, or\n logos of any Contributor (except as may be necessary to comply with the\n notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this License\n (see Section 10.2) or under the terms of a Secondary License (if permitted\n under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its Contributions\n are its original creation(s) or it has sufficient rights to grant the\n rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under applicable\n copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under the\n terms of this License. You must inform recipients that the Source Code Form\n of the Covered Software is governed by the terms of this License, and how\n they can obtain a copy of this License. You may not attempt to alter or\n restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this License,\n or sublicense it under different terms, provided that the license for\n the Executable Form does not attempt to limit or alter the recipients’\n rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for the\n Covered Software. If the Larger Work is a combination of Covered Software\n with a work governed by one or more Secondary Licenses, and the Covered\n Software is not Incompatible With Secondary Licenses, this License permits\n You to additionally distribute such Covered Software under the terms of\n such Secondary License(s), so that the recipient of the Larger Work may, at\n their option, further distribute the Covered Software under the terms of\n either this License or such Secondary License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices (including\n copyright notices, patent notices, disclaimers of warranty, or limitations\n of liability) contained within the Source Code Form of the Covered\n Software, except that You may alter any license notices to the extent\n required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on behalf\n of any Contributor. You must make it absolutely clear that any such\n warranty, support, indemnity, or liability obligation is offered by You\n alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute, judicial\n order, or regulation then You must: (a) comply with the terms of this License\n to the maximum extent possible; and (b) describe the limitations and the code\n they affect. Such description must be placed in a text file included with all\n distributions of the Covered Software under this License. Except to the\n extent prohibited by statute or regulation, such description must be\n sufficiently detailed for a recipient of ordinary skill to be able to\n understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing basis,\n if such Contributor fails to notify You of the non-compliance by some\n reasonable means prior to 60 days after You have come back into compliance.\n Moreover, Your grants from a particular Contributor are reinstated on an\n ongoing basis if such Contributor notifies You of the non-compliance by\n some reasonable means, this is the first time You have received notice of\n non-compliance with this License from such Contributor, and You become\n compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions, counter-claims,\n and cross-claims) alleging that a Contributor Version directly or\n indirectly infringes any patent, then the rights granted to You by any and\n all Contributors for the Covered Software under Section 2.1 of this License\n shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an “as is” basis, without\n warranty of any kind, either expressed, implied, or statutory, including,\n without limitation, warranties that the Covered Software is free of defects,\n merchantable, fit for a particular purpose or non-infringing. The entire\n risk as to the quality and performance of the Covered Software is with You.\n Should any Covered Software prove defective in any respect, You (not any\n Contributor) assume the cost of any necessary servicing, repair, or\n correction. This disclaimer of warranty constitutes an essential part of this\n License. No use of any Covered Software is authorized under this License\n except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from such\n party’s negligence to the extent applicable law prohibits such limitation.\n Some jurisdictions do not allow the exclusion or limitation of incidental or\n consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts of\n a jurisdiction where the defendant maintains its principal place of business\n and such litigation shall be governed by laws of that jurisdiction, without\n reference to its conflict-of-law provisions. Nothing in this Section shall\n prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject matter\n hereof. If any provision of this License is held to be unenforceable, such\n provision shall be reformed only to the extent necessary to make it\n enforceable. Any law or regulation which provides that the language of a\n contract shall be construed against the drafter shall not be used to construe\n this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version of\n the License under which You originally received the Covered Software, or\n under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a modified\n version of this License if you rename the license and remove any\n references to the name of the license steward (except to note that such\n modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses\n If You choose to distribute Source Code Form that is Incompatible With\n Secondary Licenses under the terms of this version of the License, the\n notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then\nYou may include the notice in a location (such as a LICENSE file in a relevant\ndirectory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice\n\n This Source Code Form is “Incompatible\n With Secondary Licenses”, as defined by\n the Mozilla Public License, v. 2.0.\n\n"
+ },
+ {
+ "name": "github.com/hashicorp/golang-lru",
+ "path": "github.com/hashicorp/golang-lru/LICENSE",
+ "licenseText": "Copyright (c) 2014 HashiCorp, Inc.\n\nMozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the terms of\n a Secondary License.\n\n1.6. \"Executable Form\"\n\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n\n means a work that combines Covered Software with other material, in a\n separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n\n means this document.\n\n1.9. \"Licensable\"\n\n means having the right to grant, to the maximum extent possible, whether\n at the time of the initial grant or subsequently, any and all of the\n rights conveyed by this License.\n\n1.10. \"Modifications\"\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. \"Patent Claims\" of a Contributor\n\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the License,\n by the making, using, selling, offering for sale, having made, import,\n or transfer of either its Contributions or its Contributor Version.\n\n1.12. \"Secondary License\"\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. \"Source Code Form\"\n\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, \"control\" means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-exclusive license:\n\n a. under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution\n become effective for each Contribution on the date the Contributor first\n distributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under\n this License. No additional rights or licenses will be implied from the\n distribution or licensing of Covered Software under this License.\n Notwithstanding Section 2.1(b) above, no patent license is granted by a\n Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; or\n\n b. for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n c. under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\n This License does not grant any rights in the trademarks, service marks,\n or logos of any Contributor (except as may be necessary to comply with\n the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this\n License (see Section 10.2) or under the terms of a Secondary License (if\n permitted under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its\n Contributions are its original creation(s) or it has sufficient rights to\n grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under\n applicable copyright doctrines of fair use, fair dealing, or other\n equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under\n the terms of this License. You must inform recipients that the Source\n Code Form of the Covered Software is governed by the terms of this\n License, and how they can obtain a copy of this License. You may not\n attempt to alter or restrict the recipients' rights in the Source Code\n Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter the\n recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for\n the Covered Software. If the Larger Work is a combination of Covered\n Software with a work governed by one or more Secondary Licenses, and the\n Covered Software is not Incompatible With Secondary Licenses, this\n License permits You to additionally distribute such Covered Software\n under the terms of such Secondary License(s), so that the recipient of\n the Larger Work may, at their option, further distribute the Covered\n Software under the terms of either this License or such Secondary\n License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices\n (including copyright notices, patent notices, disclaimers of warranty, or\n limitations of liability) contained within the Source Code Form of the\n Covered Software, except that You may alter any license notices to the\n extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on\n behalf of any Contributor. You must make it absolutely clear that any\n such warranty, support, indemnity, or liability obligation is offered by\n You alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute,\n judicial order, or regulation then You must: (a) comply with the terms of\n this License to the maximum extent possible; and (b) describe the\n limitations and the code they affect. Such description must be placed in a\n text file included with all distributions of the Covered Software under\n this License. Except to the extent prohibited by statute or regulation,\n such description must be sufficiently detailed for a recipient of ordinary\n skill to be able to understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing\n basis, if such Contributor fails to notify You of the non-compliance by\n some reasonable means prior to 60 days after You have come back into\n compliance. Moreover, Your grants from a particular Contributor are\n reinstated on an ongoing basis if such Contributor notifies You of the\n non-compliance by some reasonable means, this is the first time You have\n received notice of non-compliance with this License from such\n Contributor, and You become compliant prior to 30 days after Your receipt\n of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions,\n counter-claims, and cross-claims) alleging that a Contributor Version\n directly or indirectly infringes any patent, then the rights granted to\n You by any and all Contributors for the Covered Software under Section\n 2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an \"as is\" basis,\n without warranty of any kind, either expressed, implied, or statutory,\n including, without limitation, warranties that the Covered Software is free\n of defects, merchantable, fit for a particular purpose or non-infringing.\n The entire risk as to the quality and performance of the Covered Software\n is with You. Should any Covered Software prove defective in any respect,\n You (not any Contributor) assume the cost of any necessary servicing,\n repair, or correction. This disclaimer of warranty constitutes an essential\n part of this License. No use of any Covered Software is authorized under\n this License except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from\n such party's negligence to the extent applicable law prohibits such\n limitation. Some jurisdictions do not allow the exclusion or limitation of\n incidental or consequential damages, so this exclusion and limitation may\n not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts\n of a jurisdiction where the defendant maintains its principal place of\n business and such litigation shall be governed by laws of that\n jurisdiction, without reference to its conflict-of-law provisions. Nothing\n in this Section shall prevent a party's ability to bring cross-claims or\n counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject\n matter hereof. If any provision of this License is held to be\n unenforceable, such provision shall be reformed only to the extent\n necessary to make it enforceable. Any law or regulation which provides that\n the language of a contract shall be construed against the drafter shall not\n be used to construe this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version\n of the License under which You originally received the Covered Software,\n or under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a\n modified version of this License if you rename the license and remove\n any references to the name of the license steward (except to note that\n such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\n Licenses If You choose to distribute Source Code Form that is\n Incompatible With Secondary Licenses under the terms of this version of\n the License, the notice described in Exhibit B of this License must be\n attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file,\nthen You may include the notice in a location (such as a LICENSE file in a\nrelevant directory) where a recipient would be likely to look for such a\nnotice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n\n This Source Code Form is \"Incompatible\n With Secondary Licenses\", as defined by\n the Mozilla Public License, v. 2.0.\n"
+ },
+ {
+ "name": "github.com/huandu/xstrings",
+ "path": "github.com/huandu/xstrings/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Huan Du\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
+ },
+ {
+ "name": "github.com/jaytaylor/html2text",
+ "path": "github.com/jaytaylor/html2text/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Jay Taylor\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
+ },
+ {
+ "name": "github.com/jhillyerd/enmime",
+ "path": "github.com/jhillyerd/enmime/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2012-2016 James Hillyerd, All Rights Reserved\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/josharian/intern",
+ "path": "github.com/josharian/intern/license.md",
+ "licenseText": "MIT License\n\nCopyright (c) 2019 Josh Bleecher Snyder\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/json-iterator/go",
+ "path": "github.com/json-iterator/go/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2016 json-iterator\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/kballard/go-shellquote",
+ "path": "github.com/kballard/go-shellquote/LICENSE",
+ "licenseText": "Copyright (C) 2014 Kevin Ballard\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\nDAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE\nOR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/keybase/go-crypto",
+ "path": "github.com/keybase/go-crypto/LICENSE",
+ "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/klauspost/compress",
+ "path": "github.com/klauspost/compress/LICENSE",
+ "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\nCopyright (c) 2019 Klaus Post. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n------------------\n\nFiles: gzhttp/*\n\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2016-2017 The New York Times Company\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n------------------\n\nFiles: s2/cmd/internal/readahead/*\n\nThe MIT License (MIT)\n\nCopyright (c) 2015 Klaus Post\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n---------------------\nFiles: snappy/*\nFiles: internal/snapref/*\n\nCopyright (c) 2011 The Snappy-Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n-----------------\n\nFiles: s2/cmd/internal/filepathx/*\n\nCopyright 2016 The filepathx Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/klauspost/compress/internal/snapref",
+ "path": "github.com/klauspost/compress/internal/snapref/LICENSE",
+ "licenseText": "Copyright (c) 2011 The Snappy-Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/klauspost/compress/s2",
+ "path": "github.com/klauspost/compress/s2/LICENSE",
+ "licenseText": "Copyright (c) 2011 The Snappy-Go Authors. All rights reserved.\nCopyright (c) 2019 Klaus Post. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/klauspost/compress/zstd/internal/xxhash",
+ "path": "github.com/klauspost/compress/zstd/internal/xxhash/LICENSE.txt",
+ "licenseText": "Copyright (c) 2016 Caleb Spare\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/klauspost/cpuid/v2",
+ "path": "github.com/klauspost/cpuid/v2/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Klaus Post\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
+ },
+ {
+ "name": "github.com/klauspost/pgzip",
+ "path": "github.com/klauspost/pgzip/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2014 Klaus Post\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/lib/pq",
+ "path": "github.com/lib/pq/LICENSE.md",
+ "licenseText": "Copyright (c) 2011-2013, 'pq' Contributors\nPortions Copyright (C) 2011 Blake Mizerany\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/libdns/libdns",
+ "path": "github.com/libdns/libdns/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2020 Matthew Holt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/mailru/easyjson",
+ "path": "github.com/mailru/easyjson/LICENSE",
+ "licenseText": "Copyright (c) 2016 Mail.Ru Group\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/markbates/going/defaults",
+ "path": "github.com/markbates/going/defaults/LICENSE.txt",
+ "licenseText": "Copyright (c) 2014 Mark Bates\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/markbates/goth",
+ "path": "github.com/markbates/goth/LICENSE.txt",
+ "licenseText": "Copyright (c) 2014 Mark Bates\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/mattn/go-isatty",
+ "path": "github.com/mattn/go-isatty/LICENSE",
+ "licenseText": "Copyright (c) Yasuhiro MATSUMOTO \u003cmattn.jp@gmail.com\u003e\n\nMIT License (Expat)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/mattn/go-runewidth",
+ "path": "github.com/mattn/go-runewidth/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Yasuhiro Matsumoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/matttproud/golang_protobuf_extensions/pbutil",
+ "path": "github.com/matttproud/golang_protobuf_extensions/pbutil/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/mholt/acmez",
+ "path": "github.com/mholt/acmez/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/mholt/archiver/v3",
+ "path": "github.com/mholt/archiver/v3/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2016 Matthew Holt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
+ },
+ {
+ "name": "github.com/microcosm-cc/bluemonday",
+ "path": "github.com/microcosm-cc/bluemonday/LICENSE.md",
+ "licenseText": "SPDX short identifier: BSD-3-Clause\nhttps://opensource.org/licenses/BSD-3-Clause\n\nCopyright (c) 2014, David Kitchen \u003cdavid@buro9.com\u003e\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the organisation (Microcosm) nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/miekg/dns",
+ "path": "github.com/miekg/dns/LICENSE",
+ "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nAs this is fork of the official Go code the same license applies.\nExtensions of the original work are copyright (c) 2011 Miek Gieben\n"
+ },
+ {
+ "name": "github.com/minio/md5-simd",
+ "path": "github.com/minio/md5-simd/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/minio/minio-go/v7",
+ "path": "github.com/minio/minio-go/v7/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/minio/sha256-simd",
+ "path": "github.com/minio/sha256-simd/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/mitchellh/mapstructure",
+ "path": "github.com/mitchellh/mapstructure/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Mitchell Hashimoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/modern-go/concurrent",
+ "path": "github.com/modern-go/concurrent/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/modern-go/reflect2",
+ "path": "github.com/modern-go/reflect2/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/nfnt/resize",
+ "path": "github.com/nfnt/resize/LICENSE",
+ "licenseText": "Copyright (c) 2012, Jan Schlicht \u003cjan.schlicht@gmail.com\u003e\n\nPermission to use, copy, modify, and/or distribute this software for any purpose\nwith or without fee is hereby granted, provided that the above copyright notice\nand this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\nOF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER\nTORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF\nTHIS SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/niklasfasching/go-org/org",
+ "path": "github.com/niklasfasching/go-org/org/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2018 Niklas Fasching\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/nwaples/rardecode",
+ "path": "github.com/nwaples/rardecode/LICENSE",
+ "licenseText": "Copyright (c) 2015, Nicholas Waples\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/olekukonko/tablewriter",
+ "path": "github.com/olekukonko/tablewriter/LICENSE.md",
+ "licenseText": "Copyright (C) 2014 by Oleku Konko\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/oliamb/cutter",
+ "path": "github.com/oliamb/cutter/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Olivier Amblet\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/olivere/elastic/v7",
+ "path": "github.com/olivere/elastic/v7/LICENSE",
+ "licenseText": "The MIT License (MIT)\nCopyright © 2012-2015 Oliver Eilhard\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the “Software”), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\nIN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/olivere/elastic/v7/uritemplates",
+ "path": "github.com/olivere/elastic/v7/uritemplates/LICENSE",
+ "licenseText": "Copyright (c) 2013 Joshua Tacoma\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/pierrec/lz4/v4",
+ "path": "github.com/pierrec/lz4/v4/LICENSE",
+ "licenseText": "Copyright (c) 2015, Pierre Curto\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of xxHash nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n"
+ },
+ {
+ "name": "github.com/pkg/errors",
+ "path": "github.com/pkg/errors/LICENSE",
+ "licenseText": "Copyright (c) 2015, Dave Cheney \u003cdave@cheney.net\u003e\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/pmezard/go-difflib/difflib",
+ "path": "github.com/pmezard/go-difflib/difflib/LICENSE",
+ "licenseText": "Copyright (c) 2013, Patrick Mezard\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n The names of its contributors may not be used to endorse or promote\nproducts derived from this software without specific prior written\npermission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\nTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A\nPARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED\nTO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/pquerna/otp",
+ "path": "github.com/pquerna/otp/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/prometheus/client_golang/prometheus",
+ "path": "github.com/prometheus/client_golang/prometheus/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/prometheus/client_model/go",
+ "path": "github.com/prometheus/client_model/go/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/prometheus/common",
+ "path": "github.com/prometheus/common/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/prometheus/procfs",
+ "path": "github.com/prometheus/procfs/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/rivo/uniseg",
+ "path": "github.com/rivo/uniseg/LICENSE.txt",
+ "licenseText": "MIT License\n\nCopyright (c) 2019 Oliver Kuederle\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/rs/xid",
+ "path": "github.com/rs/xid/LICENSE",
+ "licenseText": "Copyright (c) 2015 Olivier Poitrey \u003crs@dailymotion.com\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/russross/blackfriday/v2",
+ "path": "github.com/russross/blackfriday/v2/LICENSE.txt",
+ "licenseText": "Blackfriday is distributed under the Simplified BSD License:\n\n\u003e Copyright © 2011 Russ Ross\n\u003e All rights reserved.\n\u003e\n\u003e Redistribution and use in source and binary forms, with or without\n\u003e modification, are permitted provided that the following conditions\n\u003e are met:\n\u003e\n\u003e 1. Redistributions of source code must retain the above copyright\n\u003e notice, this list of conditions and the following disclaimer.\n\u003e\n\u003e 2. Redistributions in binary form must reproduce the above\n\u003e copyright notice, this list of conditions and the following\n\u003e disclaimer in the documentation and/or other materials provided with\n\u003e the distribution.\n\u003e\n\u003e THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\u003e \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n\u003e LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\n\u003e FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\n\u003e COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,\n\u003e INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,\n\u003e BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n\u003e LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n\u003e CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\n\u003e LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\n\u003e ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n\u003e POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/santhosh-tekuri/jsonschema/v5",
+ "path": "github.com/santhosh-tekuri/jsonschema/v5/LICENSE",
+ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability."
+ },
+ {
+ "name": "github.com/sergi/go-diff/diffmatchpatch",
+ "path": "github.com/sergi/go-diff/diffmatchpatch/LICENSE",
+ "licenseText": "Copyright (c) 2012-2016 The go-diff Authors. All rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n\n"
+ },
+ {
+ "name": "github.com/ssor/bom",
+ "path": "github.com/ssor/bom/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2017 Asher\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/stretchr/testify/assert",
+ "path": "github.com/stretchr/testify/assert/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/syndtr/goleveldb/leveldb",
+ "path": "github.com/syndtr/goleveldb/leveldb/LICENSE",
+ "licenseText": "Copyright 2012 Suryandaru Triandana \u003csyndtr@gmail.com\u003e\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/tstranex/u2f",
+ "path": "github.com/tstranex/u2f/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 The Go FIDO U2F Library Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/ulikunitz/xz",
+ "path": "github.com/ulikunitz/xz/LICENSE",
+ "licenseText": "Copyright (c) 2014-2022 Ulrich Kunitz\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* My name, Ulrich Kunitz, may not be used to endorse or promote products\n derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "github.com/unknwon/com",
+ "path": "github.com/unknwon/com/LICENSE",
+ "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
+ },
+ {
+ "name": "github.com/unrolled/render",
+ "path": "github.com/unrolled/render/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Cory Jacobsen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "github.com/urfave/cli",
+ "path": "github.com/urfave/cli/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2016 Jeremy Saenz \u0026 Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/valyala/fastjson",
+ "path": "github.com/valyala/fastjson/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2018 Aliaksandr Valialkin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
+ },
+ {
+ "name": "github.com/x448/float16",
+ "path": "github.com/x448/float16/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
+ },
+ {
+ "name": "github.com/xanzy/go-gitlab",
+ "path": "github.com/xanzy/go-gitlab/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "github.com/yohcop/openid-go",
+ "path": "github.com/yohcop/openid-go/LICENSE",
+ "licenseText": "Copyright 2015 Yohann Coppel\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
+ },
+ {
+ "name": "github.com/yuin/goldmark-highlighting/v2",
+ "path": "github.com/yuin/goldmark-highlighting/v2/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2019 Yusuke Inuzuka\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/yuin/goldmark-meta",
+ "path": "github.com/yuin/goldmark-meta/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2019 Yusuke Inuzuka\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/yuin/goldmark",
+ "path": "github.com/yuin/goldmark/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2019 Yusuke Inuzuka\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "go.etcd.io/bbolt",
+ "path": "go.etcd.io/bbolt/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Ben Johnson\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "go.jolheiser.com/hcaptcha",
+ "path": "go.jolheiser.com/hcaptcha/LICENSE",
+ "licenseText": "Copyright 2020 John Olheiser\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
+ },
+ {
+ "name": "go.jolheiser.com/pwn",
+ "path": "go.jolheiser.com/pwn/LICENSE",
+ "licenseText": "Copyright 2020 John Olheiser\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
+ },
+ {
+ "name": "go.uber.org/atomic",
+ "path": "go.uber.org/atomic/LICENSE.txt",
+ "licenseText": "Copyright (c) 2016 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "go.uber.org/multierr",
+ "path": "go.uber.org/multierr/LICENSE.txt",
+ "licenseText": "Copyright (c) 2017-2021 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "go.uber.org/zap",
+ "path": "go.uber.org/zap/LICENSE.txt",
+ "licenseText": "Copyright (c) 2016-2017 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "golang.org/x/crypto",
+ "path": "golang.org/x/crypto/LICENSE",
+ "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "golang.org/x/mod/semver",
+ "path": "golang.org/x/mod/semver/LICENSE",
+ "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "golang.org/x/net",
+ "path": "golang.org/x/net/LICENSE",
+ "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "golang.org/x/oauth2",
+ "path": "golang.org/x/oauth2/LICENSE",
+ "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "golang.org/x/sys",
+ "path": "golang.org/x/sys/LICENSE",
+ "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "golang.org/x/text",
+ "path": "golang.org/x/text/LICENSE",
+ "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "golang.org/x/time/rate",
+ "path": "golang.org/x/time/rate/LICENSE",
+ "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "google.golang.org/protobuf",
+ "path": "google.golang.org/protobuf/LICENSE",
+ "licenseText": "Copyright (c) 2018 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "gopkg.in/gomail.v2",
+ "path": "gopkg.in/gomail.v2/LICENSE",
+ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Alexandre Cesaro\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
+ },
+ {
+ "name": "gopkg.in/ini.v1",
+ "path": "gopkg.in/ini.v1/LICENSE",
+ "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright 2014 Unknwon\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "gopkg.in/yaml.v2",
+ "path": "gopkg.in/yaml.v2/LICENSE",
+ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
+ },
+ {
+ "name": "gopkg.in/yaml.v3",
+ "path": "gopkg.in/yaml.v3/LICENSE",
+ "licenseText": "\nThis project is covered by two different licenses: MIT and Apache.\n\n#### MIT License ####\n\nThe following files were ported to Go from C files of libyaml, and thus\nare still covered by their original MIT license, with the additional\ncopyright staring in 2011 when the project was ported over:\n\n apic.go emitterc.go parserc.go readerc.go scannerc.go\n writerc.go yamlh.go yamlprivateh.go\n\nCopyright (c) 2006-2010 Kirill Simonov\nCopyright (c) 2006-2011 Kirill Simonov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n### Apache License ###\n\nAll the remaining project files are covered by the Apache license:\n\nCopyright (c) 2011-2019 Canonical Ltd\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
+ },
+ {
+ "name": "mvdan.cc/xurls/v2",
+ "path": "mvdan.cc/xurls/v2/LICENSE",
+ "licenseText": "Copyright (c) 2015, Daniel Martí. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of the copyright holder nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "strk.kbt.io/projects/go/libravatar",
+ "path": "strk.kbt.io/projects/go/libravatar/LICENSE",
+ "licenseText": "Copyright (c) 2016 Sandro Santilli \u003cstrk@kbt.io\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
+ },
+ {
+ "name": "xorm.io/builder",
+ "path": "xorm.io/builder/LICENSE",
+ "licenseText": "Copyright (c) 2016 The Xorm Authors\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the {organization} nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ },
+ {
+ "name": "xorm.io/xorm",
+ "path": "xorm.io/xorm/LICENSE",
+ "licenseText": "Copyright (c) 2013 - 2015 The Xorm Authors\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the {organization} nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+ }
+]
\ No newline at end of file
diff --git a/build.go b/build.go
index aa561413407bc..d2e724a7f70c5 100644
--- a/build.go
+++ b/build.go
@@ -1,9 +1,8 @@
// 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.
+// SPDX-License-Identifier: MIT
+
//go:build vendor
-// +build vendor
package main
diff --git a/build/code-batch-process.go b/build/code-batch-process.go
index b2290af771573..9cc47442b2252 100644
--- a/build/code-batch-process.go
+++ b/build/code-batch-process.go
@@ -1,9 +1,7 @@
// 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.
+// SPDX-License-Identifier: MIT
//go:build ignore
-// +build ignore
package main
@@ -21,7 +19,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.
@@ -62,7 +60,7 @@ func newFileCollector(fileFilter string, batchSize int) (*fileCollector, error)
"build",
"cmd",
"contrib",
- "integrations",
+ "tests",
"models",
"modules",
"routers",
@@ -72,8 +70,8 @@ func newFileCollector(fileFilter string, batchSize int) (*fileCollector, error)
co.includePatterns = append(co.includePatterns, regexp.MustCompile(`.*\.go$`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`.*\bbindata\.go$`))
- co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`integrations/gitea-repositories-meta`))
- co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`integrations/migration-test`))
+ co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/gitea-repositories-meta`))
+ co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/integration/migration-test`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`modules/git/tests`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/fixtures`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/migrations/fixtures`))
@@ -196,7 +194,6 @@ Options:
Commands:
%[1]s gofmt ...
- %[1]s misspell ...
Arguments:
{file-list} the file list
@@ -207,6 +204,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 == "" {
@@ -229,9 +237,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
}
@@ -270,10 +278,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 5d051b2726153..c9fc2a27b4ab7 100644
--- a/build/codeformat/formatimports.go
+++ b/build/codeformat/formatimports.go
@@ -1,13 +1,11 @@
// 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.
+// SPDX-License-Identifier: MIT
package codeformat
import (
"bytes"
"errors"
- "fmt"
"io"
"os"
"sort"
@@ -159,7 +157,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 +181,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/build/codeformat/formatimports_test.go b/build/codeformat/formatimports_test.go
index 3db90cad7b6a6..c66181d351311 100644
--- a/build/codeformat/formatimports_test.go
+++ b/build/codeformat/formatimports_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package codeformat
diff --git a/build/generate-bindata.go b/build/generate-bindata.go
index 7fdf9d761610e..2fcb7c2f2a088 100644
--- a/build/generate-bindata.go
+++ b/build/generate-bindata.go
@@ -1,9 +1,7 @@
// 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.
+// SPDX-License-Identifier: MIT
//go:build ignore
-// +build ignore
package main
@@ -34,11 +32,15 @@ func needsUpdate(dir, filename string) (bool, []byte) {
hasher := sha1.New()
- err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+ err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
- _, _ = hasher.Write([]byte(info.Name()))
+ info, err := d.Info()
+ if err != nil {
+ return err
+ }
+ _, _ = hasher.Write([]byte(d.Name()))
_, _ = hasher.Write([]byte(info.ModTime().String()))
_, _ = hasher.Write([]byte(strconv.FormatInt(info.Size(), 16)))
return nil
diff --git a/build/generate-emoji.go b/build/generate-emoji.go
index 2f3536342d41c..60350336c1e01 100644
--- a/build/generate-emoji.go
+++ b/build/generate-emoji.go
@@ -1,10 +1,8 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2015 Kenneth Shaw
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
//go:build ignore
-// +build ignore
package main
@@ -27,7 +25,7 @@ import (
const (
gemojiURL = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
- maxUnicodeVersion = 12
+ maxUnicodeVersion = 14
)
var flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out")
@@ -191,6 +189,10 @@ func generate() ([]byte, error) {
}
}
+ sort.Slice(data, func(i, j int) bool {
+ return data[i].Aliases[0] < data[j].Aliases[0]
+ })
+
// add header
str := replacer.Replace(fmt.Sprintf(hdr, gemojiURL, data))
@@ -210,13 +212,12 @@ func generate() ([]byte, error) {
const hdr = `
// 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.
+// SPDX-License-Identifier: MIT
+
package emoji
-// Code generated by gen.go. DO NOT EDIT.
+// Code generated by build/generate-emoji.go. DO NOT EDIT.
// Sourced from %s
-//
var GemojiData = %#v
`
diff --git a/build/generate-gitignores.go b/build/generate-gitignores.go
index 0f7d719d40b18..1e09c83a6a716 100644
--- a/build/generate-gitignores.go
+++ b/build/generate-gitignores.go
@@ -1,5 +1,4 @@
//go:build ignore
-// +build ignore
package main
diff --git a/build/generate-go-licenses.go b/build/generate-go-licenses.go
new file mode 100644
index 0000000000000..ec8fc82a71d68
--- /dev/null
+++ b/build/generate-go-licenses.go
@@ -0,0 +1,81 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+//go:build ignore
+
+package main
+
+import (
+ "encoding/json"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+)
+
+// regexp is based on go-license, excluding README and NOTICE
+// https://github.com/google/go-licenses/blob/master/licenses/find.go
+var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
+
+type LicenseEntry struct {
+ Name string `json:"name"`
+ Path string `json:"path"`
+ LicenseText string `json:"licenseText"`
+}
+
+func main() {
+ base, out := os.Args[1], os.Args[2]
+
+ paths := []string{}
+ err := filepath.WalkDir(base, func(path string, entry fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if entry.IsDir() || !licenseRe.MatchString(entry.Name()) {
+ return nil
+ }
+ paths = append(paths, path)
+ return nil
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ sort.Strings(paths)
+
+ entries := []LicenseEntry{}
+ for _, path := range paths {
+ licenseText, err := os.ReadFile(path)
+ if err != nil {
+ panic(err)
+ }
+
+ path := strings.Replace(path, base+string(os.PathSeparator), "", 1)
+ name := filepath.Dir(path)
+
+ // There might be a bug somewhere in go-licenses that sometimes interprets the
+ // root package as "." and sometimes as "code.gitea.io/gitea". Workaround by
+ // removing both of them for the sake of stable output.
+ if name == "." || name == "code.gitea.io/gitea" {
+ continue
+ }
+
+ entries = append(entries, LicenseEntry{
+ Name: name,
+ Path: path,
+ LicenseText: string(licenseText),
+ })
+ }
+
+ jsonBytes, err := json.MarshalIndent(entries, "", " ")
+ if err != nil {
+ panic(err)
+ }
+
+ err = os.WriteFile(out, jsonBytes, 0o644)
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/build/generate-images.js b/build/generate-images.js
index b8284b1be2fc2..e5744526db61f 100755
--- a/build/generate-images.js
+++ b/build/generate-images.js
@@ -1,13 +1,8 @@
+#!/usr/bin/env node
import imageminZopfli from 'imagemin-zopfli';
import {optimize} from 'svgo';
import {fabric} from 'fabric';
-import fs from 'fs';
-import {resolve, dirname} from 'path';
-import {fileURLToPath} from 'url';
-
-const {readFile, writeFile} = fs.promises;
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const logoFile = resolve(__dirname, '../assets/logo.svg');
+import {readFile, writeFile} from 'node:fs/promises';
function exit(err) {
if (err) console.error(err);
@@ -22,8 +17,10 @@ function loadSvg(svg) {
});
}
-async function generate(svg, outputFile, {size, bg}) {
- if (outputFile.endsWith('.svg')) {
+async function generate(svg, path, {size, bg}) {
+ const outputFile = new URL(path, import.meta.url);
+
+ if (String(outputFile).endsWith('.svg')) {
const {data} = optimize(svg, {
plugins: [
'preset-default',
@@ -68,17 +65,18 @@ async function generate(svg, outputFile, {size, bg}) {
async function main() {
const gitea = process.argv.slice(2).includes('gitea');
- const svg = await readFile(logoFile, 'utf8');
+ const logoSvg = await readFile(new URL('../assets/logo.svg', import.meta.url), 'utf8');
+ const faviconSvg = await readFile(new URL('../assets/favicon.svg', import.meta.url), 'utf8');
await Promise.all([
- generate(svg, resolve(__dirname, '../public/img/logo.svg'), {size: 32}),
- generate(svg, resolve(__dirname, '../public/img/logo.png'), {size: 512}),
- generate(svg, resolve(__dirname, '../public/img/favicon.png'), {size: 180}),
- generate(svg, resolve(__dirname, '../public/img/avatar_default.png'), {size: 200}),
- generate(svg, resolve(__dirname, '../public/img/apple-touch-icon.png'), {size: 180, bg: true}),
- gitea && generate(svg, resolve(__dirname, '../public/img/gitea.svg'), {size: 32}),
+ generate(logoSvg, '../public/img/logo.svg', {size: 32}),
+ generate(logoSvg, '../public/img/logo.png', {size: 512}),
+ generate(faviconSvg, '../public/img/favicon.svg', {size: 32}),
+ generate(faviconSvg, '../public/img/favicon.png', {size: 180}),
+ generate(logoSvg, '../public/img/avatar_default.png', {size: 200}),
+ generate(logoSvg, '../public/img/apple-touch-icon.png', {size: 180, bg: true}),
+ gitea && generate(logoSvg, '../public/img/gitea.svg', {size: 32}),
]);
}
main().then(exit).catch(exit);
-
diff --git a/build/generate-licenses.go b/build/generate-licenses.go
index 0f9b9f369fec2..9a111bc811150 100644
--- a/build/generate-licenses.go
+++ b/build/generate-licenses.go
@@ -1,5 +1,4 @@
//go:build ignore
-// +build ignore
package main
@@ -40,6 +39,14 @@ func main() {
defer util.Remove(file.Name())
+ if err := os.RemoveAll(destination); err != nil {
+ log.Fatalf("Cannot clean destination folder: %v", err)
+ }
+
+ if err := os.MkdirAll(destination, 0o755); err != nil {
+ log.Fatalf("Cannot create destination: %v", err)
+ }
+
req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Fatalf("Failed to download archive. %s", err)
diff --git a/build/generate-svg.js b/build/generate-svg.js
index 29b7d476938e9..31e65b8a75796 100755
--- a/build/generate-svg.js
+++ b/build/generate-svg.js
@@ -1,13 +1,14 @@
+#!/usr/bin/env node
import fastGlob from 'fast-glob';
import {optimize} from 'svgo';
-import {resolve, parse, dirname} from 'path';
-import fs from 'fs';
-import {fileURLToPath} from 'url';
+import {parse} from 'node:path';
+import {readFile, writeFile, mkdir} from 'node:fs/promises';
+import {fileURLToPath} from 'node:url';
-const {readFile, writeFile, mkdir} = fs.promises;
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const glob = (pattern) => fastGlob.sync(pattern, {cwd: resolve(__dirname), absolute: true});
-const outputDir = resolve(__dirname, '../public/img/svg');
+const glob = (pattern) => fastGlob.sync(pattern, {
+ cwd: fileURLToPath(new URL('..', import.meta.url)),
+ absolute: true,
+});
function exit(err) {
if (err) console.error(err);
@@ -16,7 +17,6 @@ function exit(err) {
async function processFile(file, {prefix, fullName} = {}) {
let name;
-
if (fullName) {
name = fullName;
} else {
@@ -35,7 +35,8 @@ async function processFile(file, {prefix, fullName} = {}) {
{name: 'addAttributesToSVGElement', params: {attributes: [{'width': '16'}, {'height': '16'}, {'aria-hidden': 'true'}]}},
],
});
- await writeFile(resolve(outputDir, `${name}.svg`), data);
+
+ await writeFile(fileURLToPath(new URL(`../public/img/svg/${name}.svg`, import.meta.url)), data);
}
function processFiles(pattern, opts) {
@@ -44,15 +45,14 @@ function processFiles(pattern, opts) {
async function main() {
try {
- await mkdir(outputDir);
+ await mkdir(fileURLToPath(new URL('../public/img/svg', import.meta.url)), {recursive: true});
} catch {}
await Promise.all([
- ...processFiles('../node_modules/@primer/octicons/build/svg/*-16.svg', {prefix: 'octicon'}),
- ...processFiles('../web_src/svg/*.svg'),
- ...processFiles('../public/img/gitea.svg', {fullName: 'gitea-gitea'}),
+ ...processFiles('node_modules/@primer/octicons/build/svg/*-16.svg', {prefix: 'octicon'}),
+ ...processFiles('web_src/svg/*.svg'),
+ ...processFiles('public/img/gitea.svg', {fullName: 'gitea-gitea'}),
]);
}
main().then(exit).catch(exit);
-
diff --git a/build/gitea-format-imports.go b/build/gitea-format-imports.go
deleted file mode 100644
index 67c8397b2d9a9..0000000000000
--- a/build/gitea-format-imports.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// 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.
-
-//go:build ignore
-// +build ignore
-
-package main
-
-import (
- "log"
- "os"
-
- "code.gitea.io/gitea/build/codeformat"
-)
-
-func main() {
- if len(os.Args) <= 1 {
- log.Fatalf("Usage: gitea-format-imports [files...]")
- }
-
- for _, file := range os.Args[1:] {
- if err := codeformat.FormatGoImports(file); err != nil {
- log.Fatalf("can not format file %s, err=%v", file, err)
- }
- }
-}
diff --git a/build/gocovmerge.go b/build/gocovmerge.go
index 1d2652129f9f5..c6f74ed85cd3d 100644
--- a/build/gocovmerge.go
+++ b/build/gocovmerge.go
@@ -1,13 +1,11 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright (c) 2015, Wade Simmons
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
// gocovmerge takes the results from multiple `go test -coverprofile` runs and
// merges them into one profile
//go:build ignore
-// +build ignore
package main
diff --git a/cmd/admin.go b/cmd/admin.go
index e4a254c613909..a662011f9c079 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -1,7 +1,6 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package cmd
@@ -13,10 +12,10 @@ import (
"strings"
"text/tabwriter"
- "code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/auth"
+ auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/graceful"
@@ -25,6 +24,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
+ "code.gitea.io/gitea/modules/util"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/auth/source/smtp"
@@ -114,6 +114,10 @@ var (
Name: "access-token",
Usage: "Generate access token for the user",
},
+ cli.BoolFlag{
+ Name: "restricted",
+ Usage: "Make a restricted user account",
+ },
},
}
@@ -151,6 +155,10 @@ var (
Name: "email,e",
Usage: "Email of the user to delete",
},
+ cli.BoolFlag{
+ Name: "purge",
+ Usage: "Purge user, all their repositories, organizations and comments",
+ },
},
Action: runDeleteUser,
}
@@ -485,7 +493,7 @@ func runChangePassword(c *cli.Context) error {
return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords")
}
uname := c.String("username")
- user, err := user_model.GetUserByName(uname)
+ user, err := user_model.GetUserByName(ctx, uname)
if err != nil {
return err
}
@@ -551,7 +559,7 @@ func runCreateUser(c *cli.Context) error {
// If this is the first user being created.
// Take it as the admin and don't force a password update.
- if n := user_model.CountUsers(); n == 0 {
+ if n := user_model.CountUsers(nil); n == 0 {
changePassword = false
}
@@ -559,27 +567,36 @@ func runCreateUser(c *cli.Context) error {
changePassword = c.Bool("must-change-password")
}
+ restricted := util.OptionalBoolNone
+
+ if c.IsSet("restricted") {
+ restricted = util.OptionalBoolOf(c.Bool("restricted"))
+ }
+
u := &user_model.User{
Name: username,
Email: c.String("email"),
Passwd: password,
- IsActive: true,
IsAdmin: c.Bool("admin"),
MustChangePassword: changePassword,
- Theme: setting.UI.DefaultTheme,
}
- if err := user_model.CreateUser(u); err != nil {
- return fmt.Errorf("CreateUser: %v", err)
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsActive: util.OptionalBoolTrue,
+ IsRestricted: restricted,
+ }
+
+ if err := user_model.CreateUser(u, overwriteDefault); err != nil {
+ return fmt.Errorf("CreateUser: %w", err)
}
if c.Bool("access-token") {
- t := &models.AccessToken{
+ t := &auth_model.AccessToken{
Name: "gitea-admin",
UID: u.ID,
}
- if err := models.NewAccessToken(t); err != nil {
+ if err := auth_model.NewAccessToken(t); err != nil {
return err
}
@@ -613,9 +630,10 @@ func runListUsers(c *cli.Context) error {
}
}
} else {
- fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\n")
+ twofa := user_model.UserList(users).GetTwoFaStatus()
+ fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n")
for _, u := range users {
- fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin)
+ fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID])
}
}
@@ -645,9 +663,9 @@ func runDeleteUser(c *cli.Context) error {
if c.IsSet("email") {
user, err = user_model.GetUserByEmail(c.String("email"))
} else if c.IsSet("username") {
- user, err = user_model.GetUserByName(c.String("username"))
+ user, err = user_model.GetUserByName(ctx, c.String("username"))
} else {
- user, err = user_model.GetUserByID(c.Int64("id"))
+ user, err = user_model.GetUserByID(ctx, c.Int64("id"))
}
if err != nil {
return err
@@ -660,7 +678,7 @@ func runDeleteUser(c *cli.Context) error {
return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
}
- return user_service.DeleteUser(user)
+ return user_service.DeleteUser(ctx, user, c.Bool("purge"))
}
func runGenerateAccessToken(c *cli.Context) error {
@@ -675,17 +693,17 @@ func runGenerateAccessToken(c *cli.Context) error {
return err
}
- user, err := user_model.GetUserByName(c.String("username"))
+ user, err := user_model.GetUserByName(ctx, c.String("username"))
if err != nil {
return err
}
- t := &models.AccessToken{
+ t := &auth_model.AccessToken{
Name: c.String("token-name"),
UID: user.ID,
}
- if err := models.NewAccessToken(t); err != nil {
+ if err := auth_model.NewAccessToken(t); err != nil {
return err
}
@@ -708,15 +726,15 @@ func runRepoSyncReleases(_ *cli.Context) error {
log.Trace("Synchronizing repository releases (this may take a while)")
for page := 1; ; page++ {
- repos, count, err := models.SearchRepositoryByName(&models.SearchRepoOptions{
+ repos, count, err := repo_model.SearchRepositoryByName(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
- PageSize: models.RepositoryListDefaultPageSize,
+ PageSize: repo_model.RepositoryListDefaultPageSize,
Page: page,
},
Private: true,
})
if err != nil {
- return fmt.Errorf("SearchRepositoryByName: %v", err)
+ return fmt.Errorf("SearchRepositoryByName: %w", err)
}
if len(repos) == 0 {
break
@@ -759,9 +777,10 @@ func runRepoSyncReleases(_ *cli.Context) error {
}
func getReleaseCount(id int64) (int64, error) {
- return models.GetReleaseCountByRepoID(
+ return repo_model.GetReleaseCountByRepoID(
+ db.DefaultContext,
id,
- models.FindReleasesOptions{
+ repo_model.FindReleasesOptions{
IncludeTags: true,
},
)
@@ -824,8 +843,8 @@ func runAddOauth(c *cli.Context) error {
return err
}
- return auth.CreateSource(&auth.Source{
- Type: auth.OAuth2,
+ return auth_model.CreateSource(&auth_model.Source{
+ Type: auth_model.OAuth2,
Name: c.String("name"),
IsActive: true,
Cfg: parseOAuth2Config(c),
@@ -844,7 +863,7 @@ func runUpdateOauth(c *cli.Context) error {
return err
}
- source, err := auth.GetSourceByID(c.Int64("id"))
+ source, err := auth_model.GetSourceByID(c.Int64("id"))
if err != nil {
return err
}
@@ -924,14 +943,14 @@ func runUpdateOauth(c *cli.Context) error {
oAuth2Config.CustomURLMapping = customURLMapping
source.Cfg = oAuth2Config
- return auth.UpdateSource(source)
+ return auth_model.UpdateSource(source)
}
func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
if c.IsSet("auth-type") {
conf.Auth = c.String("auth-type")
validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"}
- if !contains(validAuthTypes, strings.ToUpper(c.String("auth-type"))) {
+ if !util.SliceContainsString(validAuthTypes, strings.ToUpper(c.String("auth-type"))) {
return errors.New("Auth must be one of PLAIN/LOGIN/CRAM-MD5")
}
conf.Auth = c.String("auth-type")
@@ -995,8 +1014,8 @@ func runAddSMTP(c *cli.Context) error {
smtpConfig.Auth = "PLAIN"
}
- return auth.CreateSource(&auth.Source{
- Type: auth.SMTP,
+ return auth_model.CreateSource(&auth_model.Source{
+ Type: auth_model.SMTP,
Name: c.String("name"),
IsActive: active,
Cfg: &smtpConfig,
@@ -1015,7 +1034,7 @@ func runUpdateSMTP(c *cli.Context) error {
return err
}
- source, err := auth.GetSourceByID(c.Int64("id"))
+ source, err := auth_model.GetSourceByID(c.Int64("id"))
if err != nil {
return err
}
@@ -1036,7 +1055,7 @@ func runUpdateSMTP(c *cli.Context) error {
source.Cfg = smtpConfig
- return auth.UpdateSource(source)
+ return auth_model.UpdateSource(source)
}
func runListAuth(c *cli.Context) error {
@@ -1047,7 +1066,7 @@ func runListAuth(c *cli.Context) error {
return err
}
- authSources, err := auth.Sources()
+ authSources, err := auth_model.Sources()
if err != nil {
return err
}
@@ -1085,7 +1104,7 @@ func runDeleteAuth(c *cli.Context) error {
return err
}
- source, err := auth.GetSourceByID(c.Int64("id"))
+ source, err := auth_model.GetSourceByID(c.Int64("id"))
if err != nil {
return err
}
diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go
index ec86b2c671d47..91276f221ff7c 100644
--- a/cmd/admin_auth_ldap.go
+++ b/cmd/admin_auth_ldap.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
@@ -34,6 +33,10 @@ var (
Name: "not-active",
Usage: "Deactivate the authentication source.",
},
+ cli.BoolFlag{
+ Name: "active",
+ Usage: "Activate the authentication source.",
+ },
cli.StringFlag{
Name: "security-protocol",
Usage: "Security protocol name.",
@@ -117,6 +120,10 @@ var (
Name: "synchronize-users",
Usage: "Enable user synchronization.",
},
+ cli.BoolFlag{
+ Name: "disable-synchronize-users",
+ Usage: "Disable user synchronization.",
+ },
cli.UintFlag{
Name: "page-size",
Usage: "Search page size.",
@@ -183,9 +190,15 @@ func parseAuthSource(c *cli.Context, authSource *auth.Source) {
if c.IsSet("not-active") {
authSource.IsActive = !c.Bool("not-active")
}
+ if c.IsSet("active") {
+ authSource.IsActive = c.Bool("active")
+ }
if c.IsSet("synchronize-users") {
authSource.IsSyncEnabled = c.Bool("synchronize-users")
}
+ if c.IsSet("disable-synchronize-users") {
+ authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users")
+ }
}
// parseLdapConfig assigns values on config according to command line flags.
diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go
index f050b536fdf50..65f53aaf4e9e4 100644
--- a/cmd/admin_auth_ldap_test.go
+++ b/cmd/admin_auth_ldap_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
@@ -858,6 +857,36 @@ func TestUpdateLdapBindDn(t *testing.T) {
},
errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
},
+ // case 24
+ {
+ args: []string{
+ "ldap-test",
+ "--id", "24",
+ "--name", "ldap (via Bind DN) flip 'active' and 'user sync' attributes",
+ "--active",
+ "--disable-synchronize-users",
+ },
+ id: 24,
+ existingAuthSource: &auth.Source{
+ Type: auth.LDAP,
+ IsActive: false,
+ IsSyncEnabled: true,
+ Cfg: &ldap.Source{
+ Name: "ldap (via Bind DN) flip 'active' and 'user sync' attributes",
+ Enabled: true,
+ },
+ },
+ authSource: &auth.Source{
+ Type: auth.LDAP,
+ Name: "ldap (via Bind DN) flip 'active' and 'user sync' attributes",
+ IsActive: true,
+ IsSyncEnabled: false,
+ Cfg: &ldap.Source{
+ Name: "ldap (via Bind DN) flip 'active' and 'user sync' attributes",
+ Enabled: true,
+ },
+ },
+ },
}
for n, c := range cases {
@@ -1221,6 +1250,33 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
},
errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM",
},
+ // case 20
+ {
+ args: []string{
+ "ldap-test",
+ "--id", "20",
+ "--name", "ldap (simple auth) flip 'active' attribute",
+ "--active",
+ },
+ id: 20,
+ existingAuthSource: &auth.Source{
+ Type: auth.DLDAP,
+ IsActive: false,
+ Cfg: &ldap.Source{
+ Name: "ldap (simple auth) flip 'active' attribute",
+ Enabled: true,
+ },
+ },
+ authSource: &auth.Source{
+ Type: auth.DLDAP,
+ Name: "ldap (simple auth) flip 'active' attribute",
+ IsActive: true,
+ Cfg: &ldap.Source{
+ Name: "ldap (simple auth) flip 'active' attribute",
+ Enabled: true,
+ },
+ },
+ },
}
for n, c := range cases {
diff --git a/cmd/cert.go b/cmd/cert.go
index 162c4171bfb29..816659023ca93 100644
--- a/cmd/cert.go
+++ b/cmd/cert.go
@@ -1,8 +1,7 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package cmd
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 3846a8690020f..493519e1359a9 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
// Package cmd provides subcommands to the gitea binary - such as "web" or
// "admin".
@@ -68,7 +67,7 @@ Ensure you are running in the correct environment or set the correct configurati
If this is the intended configuration file complete the [database] section.`, setting.CustomConf)
}
if err := db.InitEngine(ctx); err != nil {
- return fmt.Errorf("unable to initialise the database using the configuration in %q. Error: %v", setting.CustomConf, err)
+ return fmt.Errorf("unable to initialize the database using the configuration in %q. Error: %w", setting.CustomConf, err)
}
return nil
}
diff --git a/cmd/convert.go b/cmd/convert.go
index 6d4d99a255040..b9ed9f1627422 100644
--- a/cmd/convert.go
+++ b/cmd/convert.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
diff --git a/cmd/docs.go b/cmd/docs.go
index 073c57497305f..901d0abd1c55d 100644
--- a/cmd/docs.go
+++ b/cmd/docs.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
diff --git a/cmd/doctor.go b/cmd/doctor.go
index 10f62f32c16b2..ceb6e3fbabe5a 100644
--- a/cmd/doctor.go
+++ b/cmd/doctor.go
@@ -1,10 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
import (
+ "errors"
"fmt"
golog "log"
"os"
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/migrations"
+ migrate_base "code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/modules/doctor"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -24,8 +25,8 @@ import (
// CmdDoctor represents the available doctor sub-command.
var CmdDoctor = cli.Command{
Name: "doctor",
- Usage: "Diagnose problems",
- Description: "A command to diagnose problems with the current Gitea instance according to the given configuration.",
+ Usage: "Diagnose and optionally fix problems",
+ Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
Action: runDoctor,
Flags: []cli.Flag{
cli.BoolFlag{
@@ -113,7 +114,7 @@ func runRecreateTable(ctx *cli.Context) error {
if err != nil {
return err
}
- recreateTables := migrations.RecreateTables(beans...)
+ recreateTables := migrate_base.RecreateTables(beans...)
return db.InitEngineWithMigration(stdCtx, func(x *xorm.Engine) error {
if err := migrations.EnsureUpToDate(x); err != nil {
@@ -123,20 +124,11 @@ func runRecreateTable(ctx *cli.Context) error {
})
}
-func runDoctor(ctx *cli.Context) error {
- // Silence the default loggers
- log.DelNamedLogger("console")
- log.DelNamedLogger(log.DEFAULT)
-
- stdCtx, cancel := installSignals()
- defer cancel()
-
- // Now setup our own
+func setDoctorLogger(ctx *cli.Context) {
logFile := ctx.String("log-file")
if !ctx.IsSet("log-file") {
logFile = "doctor.log"
}
-
colorize := log.CanColorStdout
if ctx.IsSet("color") {
colorize = ctx.Bool("color")
@@ -144,11 +136,50 @@ func runDoctor(ctx *cli.Context) error {
if len(logFile) == 0 {
log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"NONE","stacktracelevel":"NONE","colorize":%t}`, colorize))
- } else if logFile == "-" {
+ return
+ }
+
+ defer func() {
+ recovered := recover()
+ if recovered == nil {
+ return
+ }
+
+ err, ok := recovered.(error)
+ if !ok {
+ panic(recovered)
+ }
+ if errors.Is(err, os.ErrPermission) {
+ fmt.Fprintf(os.Stderr, "ERROR: Unable to write logs to provided file due to permissions error: %s\n %v\n", logFile, err)
+ } else {
+ fmt.Fprintf(os.Stderr, "ERROR: Unable to write logs to provided file: %s\n %v\n", logFile, err)
+ }
+ fmt.Fprintf(os.Stderr, "WARN: Logging will be disabled\n Use `--log-file` to configure log file location\n")
+ log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"NONE","stacktracelevel":"NONE","colorize":%t}`, colorize))
+ }()
+
+ if logFile == "-" {
log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"trace","stacktracelevel":"NONE","colorize":%t}`, colorize))
} else {
log.NewLogger(1000, "doctor", "file", fmt.Sprintf(`{"filename":%q,"level":"trace","stacktracelevel":"NONE"}`, logFile))
}
+}
+
+func runDoctor(ctx *cli.Context) error {
+ stdCtx, cancel := installSignals()
+ defer cancel()
+
+ // Silence the default loggers
+ log.DelNamedLogger("console")
+ log.DelNamedLogger(log.DEFAULT)
+
+ // Now setup our own
+ setDoctorLogger(ctx)
+
+ colorize := log.CanColorStdout
+ if ctx.IsSet("color") {
+ colorize = ctx.Bool("color")
+ }
// Finally redirect the default golog to here
golog.SetFlags(0)
@@ -203,7 +234,7 @@ func runDoctor(ctx *cli.Context) error {
// Now we can set up our own logger to return information about what the doctor is doing
if err := log.NewNamedLogger("doctorouter",
- 1000,
+ 0,
"console",
"console",
fmt.Sprintf(`{"level":"INFO","stacktracelevel":"NONE","colorize":%t,"flags":-1}`, colorize)); err != nil {
diff --git a/cmd/dump.go b/cmd/dump.go
index 418042559874c..f40ddbac23e83 100644
--- a/cmd/dump.go
+++ b/cmd/dump.go
@@ -1,12 +1,12 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package cmd
import (
"fmt"
+ "io"
"os"
"path"
"path/filepath"
@@ -21,14 +21,25 @@ import (
"code.gitea.io/gitea/modules/util"
"gitea.com/go-chi/session"
- archiver "github.com/mholt/archiver/v3"
+ "github.com/mholt/archiver/v3"
"github.com/urfave/cli"
)
-func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error {
+func addReader(w archiver.Writer, r io.ReadCloser, info os.FileInfo, customName string, verbose bool) error {
if verbose {
- log.Info("Adding file %s\n", filePath)
+ log.Info("Adding file %s", customName)
}
+
+ return w.Write(archiver.File{
+ FileInfo: archiver.FileInfo{
+ FileInfo: info,
+ CustomName: customName,
+ },
+ ReadCloser: r,
+ })
+}
+
+func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error {
file, err := os.Open(absPath)
if err != nil {
return err
@@ -39,13 +50,7 @@ func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error {
return err
}
- return w.Write(archiver.File{
- FileInfo: archiver.FileInfo{
- FileInfo: fileInfo,
- CustomName: filePath,
- },
- ReadCloser: file,
- })
+ return addReader(w, file, fileInfo, filePath, verbose)
}
func isSubdir(upper, lower string) (bool, error) {
@@ -86,7 +91,7 @@ func (o outputType) String() string {
}
var outputTypeEnum = &outputType{
- Enum: []string{"zip", "rar", "tar", "sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4"},
+ Enum: []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"},
Default: "zip",
}
@@ -136,6 +141,14 @@ It can be used for backup and capture Gitea server image to send to maintainer`,
Name: "skip-attachment-data",
Usage: "Skip attachment data",
},
+ cli.BoolFlag{
+ Name: "skip-package-data",
+ Usage: "Skip package data",
+ },
+ cli.BoolFlag{
+ Name: "skip-index",
+ Usage: "Skip bleve index data",
+ },
cli.GenericFlag{
Name: "type",
Value: outputTypeEnum,
@@ -160,7 +173,12 @@ func runDump(ctx *cli.Context) error {
fatal("Deleting default logger failed. Can not write to stdout: %v", err)
}
} else {
- fileName = strings.TrimSuffix(fileName, path.Ext(fileName))
+ for _, suffix := range outputTypeEnum.Enum {
+ if strings.HasSuffix(fileName, "."+suffix) {
+ fileName = strings.TrimSuffix(fileName, "."+suffix)
+ break
+ }
+ }
fileName += "." + outType
}
setting.LoadFromExisting()
@@ -236,13 +254,7 @@ func runDump(ctx *cli.Context) error {
return err
}
- return w.Write(archiver.File{
- FileInfo: archiver.FileInfo{
- FileInfo: info,
- CustomName: path.Join("data", "lfs", objPath),
- },
- ReadCloser: object,
- })
+ return addReader(w, object, info, path.Join("data", "lfs", objPath), verbose)
}); err != nil {
fatal("Failed to dump LFS objects: %v", err)
}
@@ -318,9 +330,15 @@ 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)
+ excludes = append(excludes, setting.Packages.Path)
excludes = append(excludes, setting.LogRootPath)
excludes = append(excludes, absFileName)
if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {
@@ -336,17 +354,24 @@ func runDump(ctx *cli.Context) error {
return err
}
- return w.Write(archiver.File{
- FileInfo: archiver.FileInfo{
- FileInfo: info,
- CustomName: path.Join("data", "attachments", objPath),
- },
- ReadCloser: object,
- })
+ return addReader(w, object, info, path.Join("data", "attachments", objPath), verbose)
}); err != nil {
fatal("Failed to dump attachments: %v", err)
}
+ if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
+ log.Info("Skip dumping package data")
+ } else if err := storage.Packages.IterateObjects(func(objPath string, object storage.Object) error {
+ info, err := object.Stat()
+ if err != nil {
+ return err
+ }
+
+ return addReader(w, object, info, path.Join("data", "packages", objPath), verbose)
+ }); err != nil {
+ fatal("Failed to dump packages: %v", err)
+ }
+
// Doesn't check if LogRootPath exists before processing --skip-log intentionally,
// ensuring that it's clear the dump is skipped whether the directory's initialized
// yet or not.
@@ -384,15 +409,6 @@ func runDump(ctx *cli.Context) error {
return nil
}
-func contains(slice []string, s string) bool {
- for _, v := range slice {
- if v == s {
- return true
- }
- }
- return false
-}
-
// addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath
func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeAbsPath []string, verbose bool) error {
absPath, err := filepath.Abs(absPath)
@@ -413,7 +429,7 @@ func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeA
currentAbsPath := path.Join(absPath, file.Name())
currentInsidePath := path.Join(insidePath, file.Name())
if file.IsDir() {
- if !contains(excludeAbsPath, currentAbsPath) {
+ if !util.SliceContainsString(excludeAbsPath, currentAbsPath) {
if err := addFile(w, currentInsidePath, currentAbsPath, false); err != nil {
return err
}
@@ -422,8 +438,23 @@ func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeA
}
}
} else {
- if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil {
- return err
+ // only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
+ shouldAdd := file.Mode().IsRegular()
+ if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
+ target, err := filepath.EvalSymlinks(currentAbsPath)
+ if err != nil {
+ return err
+ }
+ targetStat, err := os.Stat(target)
+ if err != nil {
+ return err
+ }
+ shouldAdd = targetStat.Mode().IsRegular()
+ }
+ if shouldAdd {
+ if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil {
+ return err
+ }
}
}
}
diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go
index e980af30110aa..b7b9b3ccc734b 100644
--- a/cmd/dump_repo.go
+++ b/cmd/dump_repo.go
@@ -1,19 +1,22 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
+ "fmt"
+ "os"
"strings"
- "code.gitea.io/gitea/modules/convert"
+ "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/migrations"
"github.com/urfave/cli"
@@ -83,6 +86,11 @@ func runDumpRepository(ctx *cli.Context) error {
return err
}
+ // migrations.GiteaLocalUploader depends on git module
+ if err := git.InitSimple(context.Background()); err != nil {
+ return err
+ }
+
log.Info("AppPath: %s", setting.AppPath)
log.Info("AppWorkPath: %s", setting.AppWorkPath)
log.Info("Custom path: %s", setting.CustomPath)
@@ -128,7 +136,9 @@ func runDumpRepository(ctx *cli.Context) error {
} else {
units := strings.Split(ctx.String("units"), ",")
for _, unit := range units {
- switch strings.ToLower(unit) {
+ switch strings.ToLower(strings.TrimSpace(unit)) {
+ case "":
+ continue
case "wiki":
opts.Wiki = true
case "issues":
@@ -145,13 +155,29 @@ func runDumpRepository(ctx *cli.Context) error {
opts.Comments = true
case "pull_requests":
opts.PullRequests = true
+ default:
+ return errors.New("invalid unit: " + unit)
}
}
}
+ // the repo_dir will be removed if error occurs in DumpRepository
+ // make sure the directory doesn't exist or is empty, prevent from deleting user files
+ repoDir := ctx.String("repo_dir")
+ if exists, err := util.IsExist(repoDir); err != nil {
+ return fmt.Errorf("unable to stat repo_dir %q: %w", repoDir, err)
+ } else if exists {
+ if isDir, _ := util.IsDir(repoDir); !isDir {
+ return fmt.Errorf("repo_dir %q already exists but it's not a directory", repoDir)
+ }
+ if dir, _ := os.ReadDir(repoDir); len(dir) > 0 {
+ return fmt.Errorf("repo_dir %q is not empty", repoDir)
+ }
+ }
+
if err := migrations.DumpRepository(
context.Background(),
- ctx.String("repo_dir"),
+ repoDir,
ctx.String("owner_name"),
opts,
); err != nil {
diff --git a/cmd/embedded.go b/cmd/embedded.go
index 2930e5d3077d1..118781895ecae 100644
--- a/cmd/embedded.go
+++ b/cmd/embedded.go
@@ -1,9 +1,7 @@
// 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.
+// SPDX-License-Identifier: MIT
//go:build bindata
-// +build bindata
package cmd
@@ -124,7 +122,7 @@ func initEmbeddedExtractor(c *cli.Context) error {
sections["public"] = §ion{Path: "public", Names: public.AssetNames, IsDir: public.AssetIsDir, Asset: public.Asset}
sections["options"] = §ion{Path: "options", Names: options.AssetNames, IsDir: options.AssetIsDir, Asset: options.Asset}
- sections["templates"] = §ion{Path: "templates", Names: templates.AssetNames, IsDir: templates.AssetIsDir, Asset: templates.Asset}
+ sections["templates"] = §ion{Path: "templates", Names: templates.BuiltinAssetNames, IsDir: templates.BuiltinAssetIsDir, Asset: templates.BuiltinAsset}
for _, sec := range sections {
assets = append(assets, buildAssetList(sec, pats, c)...)
@@ -187,11 +185,11 @@ func runViewDo(c *cli.Context) error {
data, err := assets[0].Section.Asset(assets[0].Name)
if err != nil {
- return fmt.Errorf("%s: %v", assets[0].Path, err)
+ return fmt.Errorf("%s: %w", assets[0].Path, err)
}
if _, err = os.Stdout.Write(data); err != nil {
- return fmt.Errorf("%s: %v", assets[0].Path, err)
+ return fmt.Errorf("%s: %w", assets[0].Path, err)
}
return nil
@@ -252,11 +250,11 @@ func extractAsset(d string, a asset, overwrite, rename bool) error {
data, err := a.Section.Asset(a.Name)
if err != nil {
- return fmt.Errorf("%s: %v", a.Path, err)
+ return fmt.Errorf("%s: %w", a.Path, err)
}
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
- return fmt.Errorf("%s: %v", dir, err)
+ return fmt.Errorf("%s: %w", dir, err)
}
perms := os.ModePerm & 0o666
@@ -264,7 +262,7 @@ func extractAsset(d string, a asset, overwrite, rename bool) error {
fi, err := os.Lstat(dest)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
- return fmt.Errorf("%s: %v", dest, err)
+ return fmt.Errorf("%s: %w", dest, err)
}
} else if !overwrite && !rename {
fmt.Printf("%s already exists; skipped.\n", dest)
@@ -273,7 +271,7 @@ func extractAsset(d string, a asset, overwrite, rename bool) error {
return fmt.Errorf("%s already exists, but it's not a regular file", dest)
} else if rename {
if err := util.Rename(dest, dest+".bak"); err != nil {
- return fmt.Errorf("Error creating backup for %s: %v", dest, err)
+ return fmt.Errorf("Error creating backup for %s: %w", dest, err)
}
// Attempt to respect file permissions mask (even if user:group will be set anew)
perms = fi.Mode()
@@ -281,12 +279,12 @@ func extractAsset(d string, a asset, overwrite, rename bool) error {
file, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms)
if err != nil {
- return fmt.Errorf("%s: %v", dest, err)
+ return fmt.Errorf("%s: %w", dest, err)
}
defer file.Close()
if _, err = file.Write(data); err != nil {
- return fmt.Errorf("%s: %v", dest, err)
+ return fmt.Errorf("%s: %w", dest, err)
}
fmt.Println(dest)
@@ -326,7 +324,7 @@ func getPatterns(args []string) ([]glob.Glob, error) {
pat := make([]glob.Glob, len(args))
for i := range args {
if g, err := glob.Compile(args[i], '/'); err != nil {
- return nil, fmt.Errorf("'%s': Invalid glob pattern: %v", args[i], err)
+ return nil, fmt.Errorf("'%s': Invalid glob pattern: %w", args[i], err)
} else {
pat[i] = g
}
diff --git a/cmd/embedded_stub.go b/cmd/embedded_stub.go
index 0e9e3e6ec3e14..874df06f9d7fc 100644
--- a/cmd/embedded_stub.go
+++ b/cmd/embedded_stub.go
@@ -1,9 +1,7 @@
// 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.
+// SPDX-License-Identifier: MIT
//go:build !bindata
-// +build !bindata
package cmd
diff --git a/cmd/generate.go b/cmd/generate.go
index 35c77a815b1d9..f72ee16390486 100644
--- a/cmd/generate.go
+++ b/cmd/generate.go
@@ -1,7 +1,6 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package cmd
diff --git a/cmd/hook.go b/cmd/hook.go
index 2b62b61ca6fb0..228b79f7f014a 100644
--- a/cmd/hook.go
+++ b/cmd/hook.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
@@ -15,9 +14,9 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/private"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -162,7 +161,7 @@ func (n *nilWriter) WriteString(s string) (int, error) {
}
func runHookPreReceive(c *cli.Context) error {
- if os.Getenv(models.EnvIsInternal) == "true" {
+ if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
ctx, cancel := installSignals()
@@ -180,12 +179,12 @@ Gitea or set your environment appropriately.`, "")
}
// the environment is set by serv command
- isWiki := os.Getenv(models.EnvRepoIsWiki) == "true"
- username := os.Getenv(models.EnvRepoUsername)
- reponame := os.Getenv(models.EnvRepoName)
- userID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
- prID, _ := strconv.ParseInt(os.Getenv(models.EnvPRID), 10, 64)
- deployKeyID, _ := strconv.ParseInt(os.Getenv(models.EnvDeployKeyID), 10, 64)
+ isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
+ username := os.Getenv(repo_module.EnvRepoUsername)
+ reponame := os.Getenv(repo_module.EnvRepoName)
+ userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
+ prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64)
+ deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64)
hookOptions := private.HookOptions{
UserID: userID,
@@ -218,9 +217,9 @@ Gitea or set your environment appropriately.`, "")
}
}
- supportProcRecive := false
+ supportProcReceive := false
if git.CheckGitVersionAtLeast("2.29") == nil {
- supportProcRecive = true
+ supportProcReceive = true
}
for scanner.Scan() {
@@ -241,9 +240,9 @@ Gitea or set your environment appropriately.`, "")
lastline++
// If the ref is a branch or tag, check if it's protected
- // if supportProcRecive all ref should be checked because
+ // if supportProcReceive all ref should be checked because
// permission check was delayed
- if supportProcRecive || strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
+ if supportProcReceive || strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
oldCommitIDs[count] = oldCommitID
newCommitIDs[count] = newCommitID
refFullNames[count] = refFullName
@@ -308,18 +307,18 @@ func runHookPostReceive(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
+ setup("hooks/post-receive.log", c.Bool("debug"))
+
// First of all run update-server-info no matter what
if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil {
- return fmt.Errorf("Failed to call 'git update-server-info': %v", err)
+ return fmt.Errorf("Failed to call 'git update-server-info': %w", err)
}
// Now if we're an internal don't do anything else
- if os.Getenv(models.EnvIsInternal) == "true" {
+ if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
- setup("hooks/post-receive.log", c.Bool("debug"))
-
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
return fail(`Rejecting changes as Gitea environment not set.
@@ -343,11 +342,11 @@ Gitea or set your environment appropriately.`, "")
}
// the environment is set by serv command
- repoUser := os.Getenv(models.EnvRepoUsername)
- isWiki := os.Getenv(models.EnvRepoIsWiki) == "true"
- repoName := os.Getenv(models.EnvRepoName)
- pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
- pusherName := os.Getenv(models.EnvPusherName)
+ repoUser := os.Getenv(repo_module.EnvRepoUsername)
+ isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
+ repoName := os.Getenv(repo_module.EnvRepoName)
+ pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
+ pusherName := os.Getenv(repo_module.EnvPusherName)
hookOptions := private.HookOptions{
UserName: pusherName,
@@ -503,10 +502,10 @@ Gitea or set your environment appropriately.`, "")
}
reader := bufio.NewReader(os.Stdin)
- repoUser := os.Getenv(models.EnvRepoUsername)
- repoName := os.Getenv(models.EnvRepoName)
- pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
- pusherName := os.Getenv(models.EnvPusherName)
+ repoUser := os.Getenv(repo_module.EnvRepoUsername)
+ repoName := os.Getenv(repo_module.EnvRepoName)
+ pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
+ pusherName := os.Getenv(repo_module.EnvPusherName)
// 1. Version and features negotiation.
// S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n)
@@ -792,7 +791,7 @@ func writeDataPktLine(out io.Writer, data []byte) error {
if err != nil {
return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
}
- if 4 != lr {
+ if lr != 4 {
return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
}
diff --git a/cmd/hook_test.go b/cmd/hook_test.go
index 92c7e82a9ac79..fe1f072a6f794 100644
--- a/cmd/hook_test.go
+++ b/cmd/hook_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
diff --git a/cmd/keys.go b/cmd/keys.go
index 684aca64e22ab..74dc1cc68c6c3 100644
--- a/cmd/keys.go
+++ b/cmd/keys.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package cmd
diff --git a/cmd/mailer.go b/cmd/mailer.go
index 35fcb302f8970..af6613f159602 100644
--- a/cmd/mailer.go
+++ b/cmd/mailer.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
diff --git a/cmd/main_test.go b/cmd/main_test.go
new file mode 100644
index 0000000000000..9aacdf7bbae10
--- /dev/null
+++ b/cmd/main_test.go
@@ -0,0 +1,22 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+func init() {
+ setting.SetCustomPathAndConf("", "", "")
+ setting.LoadForTest()
+}
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m, &unittest.TestOptions{
+ GiteaRootPath: "..",
+ })
+}
diff --git a/cmd/manager.go b/cmd/manager.go
index 03fe23aa9e3f0..cdfe509075f3b 100644
--- a/cmd/manager.go
+++ b/cmd/manager.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
@@ -82,7 +81,7 @@ var (
},
cli.BoolFlag{
Name: "no-system",
- Usage: "Do not show system proceses",
+ Usage: "Do not show system processes",
},
cli.BoolFlag{
Name: "stacktraces",
diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go
index 0043ea1e52ad4..d49675ce87093 100644
--- a/cmd/manager_logging.go
+++ b/cmd/manager_logging.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
@@ -174,6 +173,18 @@ var (
Action: runAddSMTPLogger,
},
},
+ }, {
+ Name: "log-sql",
+ Usage: "Set LogSQL",
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "debug",
+ }, cli.BoolFlag{
+ Name: "off",
+ Usage: "Switch off SQL logging",
+ },
+ },
+ Action: runSetLogSQL,
},
},
}
@@ -381,3 +392,18 @@ func runReleaseReopenLogging(c *cli.Context) error {
fmt.Fprintln(os.Stdout, msg)
return nil
}
+
+func runSetLogSQL(c *cli.Context) error {
+ ctx, cancel := installSignals()
+ defer cancel()
+ setup("manager", c.Bool("debug"))
+
+ statusCode, msg := private.SetLogSQL(ctx, !c.Bool("off"))
+ switch statusCode {
+ case http.StatusInternalServerError:
+ return fail("InternalServerError", msg)
+ }
+
+ fmt.Fprintln(os.Stdout, msg)
+ return nil
+}
diff --git a/cmd/migrate.go b/cmd/migrate.go
index 49a13adeb5672..2546fca21d0a5 100644
--- a/cmd/migrate.go
+++ b/cmd/migrate.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package cmd
diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go
index f9770d50e74c8..0b8ebe7c8dcd1 100644
--- a/cmd/migrate_storage.go
+++ b/cmd/migrate_storage.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
@@ -9,12 +8,14 @@ import (
"fmt"
"strings"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/migrations"
+ packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
+ packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
@@ -25,13 +26,13 @@ import (
var CmdMigrateStorage = cli.Command{
Name: "migrate-storage",
Usage: "Migrate the storage",
- Description: "This is a command for migrating storage.",
+ Description: "Copies stored files from storage configured in app.ini to parameter-configured storage",
Action: runMigrateStorage,
Flags: []cli.Flag{
cli.StringFlag{
Name: "type, t",
Value: "",
- Usage: "Kinds of files to migrate, currently only 'attachments' is supported",
+ Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages'",
},
cli.StringFlag{
Name: "storage, s",
@@ -80,34 +81,50 @@ var CmdMigrateStorage = cli.Command{
},
}
-func migrateAttachments(dstStorage storage.ObjectStorage) error {
- return repo_model.IterateAttachment(func(attach *repo_model.Attachment) error {
+func migrateAttachments(ctx context.Context, dstStorage storage.ObjectStorage) error {
+ return db.Iterate(ctx, nil, func(ctx context.Context, attach *repo_model.Attachment) error {
_, err := storage.Copy(dstStorage, attach.RelativePath(), storage.Attachments, attach.RelativePath())
return err
})
}
-func migrateLFS(dstStorage storage.ObjectStorage) error {
- return models.IterateLFS(func(mo *models.LFSMetaObject) error {
+func migrateLFS(ctx context.Context, dstStorage storage.ObjectStorage) error {
+ return db.Iterate(ctx, nil, func(ctx context.Context, mo *git_model.LFSMetaObject) error {
_, err := storage.Copy(dstStorage, mo.RelativePath(), storage.LFS, mo.RelativePath())
return err
})
}
-func migrateAvatars(dstStorage storage.ObjectStorage) error {
- return user_model.IterateUser(func(user *user_model.User) error {
+func migrateAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error {
+ return db.Iterate(ctx, nil, func(ctx context.Context, user *user_model.User) error {
_, err := storage.Copy(dstStorage, user.CustomAvatarRelativePath(), storage.Avatars, user.CustomAvatarRelativePath())
return err
})
}
-func migrateRepoAvatars(dstStorage storage.ObjectStorage) error {
- return repo_model.IterateRepository(func(repo *repo_model.Repository) error {
+func migrateRepoAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error {
+ return db.Iterate(ctx, nil, func(ctx context.Context, repo *repo_model.Repository) error {
_, err := storage.Copy(dstStorage, repo.CustomAvatarRelativePath(), storage.RepoAvatars, repo.CustomAvatarRelativePath())
return err
})
}
+func migrateRepoArchivers(ctx context.Context, dstStorage storage.ObjectStorage) error {
+ return db.Iterate(ctx, nil, func(ctx context.Context, archiver *repo_model.RepoArchiver) error {
+ p := archiver.RelativePath()
+ _, err := storage.Copy(dstStorage, p, storage.RepoArchives, p)
+ return err
+ })
+}
+
+func migratePackages(ctx context.Context, dstStorage storage.ObjectStorage) error {
+ return db.Iterate(ctx, nil, func(ctx context.Context, pb *packages_model.PackageBlob) error {
+ p := packages_module.KeyToRelativePath(packages_module.BlobHash256Key(pb.HashSHA256))
+ _, err := storage.Copy(dstStorage, p, storage.Packages, p)
+ return err
+ })
+}
+
func runMigrateStorage(ctx *cli.Context) error {
stdCtx, cancel := installSignals()
defer cancel()
@@ -127,8 +144,6 @@ func runMigrateStorage(ctx *cli.Context) error {
return err
}
- goCtx := context.Background()
-
if err := storage.Init(); err != nil {
return err
}
@@ -145,13 +160,13 @@ func runMigrateStorage(ctx *cli.Context) error {
return nil
}
dstStorage, err = storage.NewLocalStorage(
- goCtx,
+ stdCtx,
storage.LocalStorageConfig{
Path: p,
})
case string(storage.MinioStorageType):
dstStorage, err = storage.NewMinioStorage(
- goCtx,
+ stdCtx,
storage.MinioStorageConfig{
Endpoint: ctx.String("minio-endpoint"),
AccessKeyID: ctx.String("minio-access-key-id"),
@@ -162,35 +177,29 @@ func runMigrateStorage(ctx *cli.Context) error {
UseSSL: ctx.Bool("minio-use-ssl"),
})
default:
- return fmt.Errorf("Unsupported storage type: %s", ctx.String("storage"))
+ return fmt.Errorf("unsupported storage type: %s", ctx.String("storage"))
}
if err != nil {
return err
}
+ migratedMethods := map[string]func(context.Context, storage.ObjectStorage) error{
+ "attachments": migrateAttachments,
+ "lfs": migrateLFS,
+ "avatars": migrateAvatars,
+ "repo-avatars": migrateRepoAvatars,
+ "repo-archivers": migrateRepoArchivers,
+ "packages": migratePackages,
+ }
+
tp := strings.ToLower(ctx.String("type"))
- switch tp {
- case "attachments":
- if err := migrateAttachments(dstStorage); err != nil {
- return err
- }
- case "lfs":
- if err := migrateLFS(dstStorage); err != nil {
- return err
- }
- case "avatars":
- if err := migrateAvatars(dstStorage); err != nil {
+ if m, ok := migratedMethods[tp]; ok {
+ if err := m(stdCtx, dstStorage); err != nil {
return err
}
- case "repo-avatars":
- if err := migrateRepoAvatars(dstStorage); err != nil {
- return err
- }
- default:
- return fmt.Errorf("Unsupported storage: %s", ctx.String("type"))
+ log.Info("%s files have successfully been copied to the new storage.", tp)
+ return nil
}
- log.Warn("All files have been copied to the new placement but old files are still on the original placement.")
-
- return nil
+ return fmt.Errorf("unsupported storage: %s", ctx.String("type"))
}
diff --git a/cmd/migrate_storage_test.go b/cmd/migrate_storage_test.go
new file mode 100644
index 0000000000000..aae366c0cf524
--- /dev/null
+++ b/cmd/migrate_storage_test.go
@@ -0,0 +1,73 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "context"
+ "os"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ packages_module "code.gitea.io/gitea/modules/packages"
+ "code.gitea.io/gitea/modules/storage"
+ packages_service "code.gitea.io/gitea/services/packages"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMigratePackages(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ creator := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+ content := "package main\n\nfunc main() {\nfmt.Println(\"hi\")\n}\n"
+ buf, err := packages_module.CreateHashedBufferFromReader(strings.NewReader(content), 1024)
+ assert.NoError(t, err)
+ defer buf.Close()
+
+ v, f, err := packages_service.CreatePackageAndAddFile(&packages_service.PackageCreationInfo{
+ PackageInfo: packages_service.PackageInfo{
+ Owner: creator,
+ PackageType: packages.TypeGeneric,
+ Name: "test",
+ Version: "1.0.0",
+ },
+ Creator: creator,
+ SemverCompatible: true,
+ VersionProperties: map[string]string{},
+ }, &packages_service.PackageFileCreationInfo{
+ PackageFileInfo: packages_service.PackageFileInfo{
+ Filename: "a.go",
+ },
+ Creator: creator,
+ Data: buf,
+ IsLead: true,
+ })
+ assert.NoError(t, err)
+ assert.NotNil(t, v)
+ assert.NotNil(t, f)
+
+ ctx := context.Background()
+
+ p := t.TempDir()
+
+ dstStorage, err := storage.NewLocalStorage(
+ ctx,
+ storage.LocalStorageConfig{
+ Path: p,
+ })
+ assert.NoError(t, err)
+
+ err = migratePackages(ctx, dstStorage)
+ assert.NoError(t, err)
+
+ entries, err := os.ReadDir(p)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 2, len(entries))
+ assert.EqualValues(t, "01", entries[0].Name())
+ assert.EqualValues(t, "tmp", entries[1].Name())
+}
diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go
index f0b01e7984c7f..23932f821c286 100644
--- a/cmd/restore_repo.go
+++ b/cmd/restore_repo.go
@@ -1,12 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
import (
"errors"
"net/http"
+ "strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
@@ -55,13 +55,16 @@ func runRestoreRepository(c *cli.Context) error {
defer cancel()
setting.LoadFromExisting()
-
+ var units []string
+ if s := c.String("units"); s != "" {
+ units = strings.Split(s, ",")
+ }
statusCode, errStr := private.RestoreRepo(
ctx,
c.String("repo_dir"),
c.String("owner_name"),
c.String("repo_name"),
- c.StringSlice("units"),
+ units,
c.Bool("validation"),
)
if statusCode == http.StatusOK {
diff --git a/cmd/serv.go b/cmd/serv.go
index b106d40d28d8f..346c918b184db 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -1,11 +1,11 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package cmd
import (
+ "context"
"fmt"
"net/http"
"net/url"
@@ -16,14 +16,16 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
+ git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/pprof"
"code.gitea.io/gitea/modules/private"
+ "code.gitea.io/gitea/modules/process"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/lfs"
@@ -40,7 +42,7 @@ const (
var CmdServ = cli.Command{
Name: "serv",
Usage: "This command should only be called by SSH shell",
- Description: `Serv provide access auth for repositories`,
+ Description: "Serv provides access auth for repositories",
Action: runServ,
Flags: []cli.Flag{
cli.BoolFlag{
@@ -63,6 +65,21 @@ func setup(logPath string, debug bool) {
if debug {
setting.RunMode = "dev"
}
+
+ // Check if setting.RepoRootPath exists. It could be the case that it doesn't exist, this can happen when
+ // `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection.
+ if _, err := os.Stat(setting.RepoRootPath); err != nil {
+ if os.IsNotExist(err) {
+ _ = fail("Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath)
+ } else {
+ _ = fail("Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err)
+ }
+ return
+ }
+
+ if err := git.InitSimple(context.Background()); err != nil {
+ _ = fail("Failed to init git", "Failed to init git, err: %v", err)
+ }
}
var (
@@ -78,12 +95,12 @@ var (
func fail(userMessage, logMessage string, args ...interface{}) error {
// There appears to be a chance to cause a zombie process and failure to read the Exit status
// if nothing is outputted on stdout.
- fmt.Fprintln(os.Stdout, "")
- fmt.Fprintln(os.Stderr, "Gitea:", userMessage)
+ _, _ = fmt.Fprintln(os.Stdout, "")
+ _, _ = fmt.Fprintln(os.Stderr, "Gitea:", userMessage)
if len(logMessage) > 0 {
if !setting.IsProd {
- fmt.Fprintf(os.Stderr, logMessage+"\n", args...)
+ _, _ = fmt.Fprintf(os.Stderr, logMessage+"\n", args...)
}
}
ctx, cancel := installSignals()
@@ -235,17 +252,6 @@ func runServ(c *cli.Context) error {
}
return fail("Internal Server Error", "%s", err.Error())
}
- os.Setenv(models.EnvRepoIsWiki, strconv.FormatBool(results.IsWiki))
- os.Setenv(models.EnvRepoName, results.RepoName)
- os.Setenv(models.EnvRepoUsername, results.OwnerName)
- os.Setenv(models.EnvPusherName, results.UserName)
- os.Setenv(models.EnvPusherEmail, results.UserEmail)
- os.Setenv(models.EnvPusherID, strconv.FormatInt(results.UserID, 10))
- os.Setenv(models.EnvRepoID, strconv.FormatInt(results.RepoID, 10))
- os.Setenv(models.EnvPRID, fmt.Sprintf("%d", 0))
- os.Setenv(models.EnvDeployKeyID, fmt.Sprintf("%d", results.DeployKeyID))
- os.Setenv(models.EnvKeyID, fmt.Sprintf("%d", results.KeyID))
- os.Setenv(models.EnvAppURL, setting.AppURL)
// LFS token authentication
if verb == lfsAuthenticateVerb {
@@ -269,7 +275,7 @@ func runServ(c *cli.Context) error {
return fail("Internal error", "Failed to sign JWT token: %v", err)
}
- tokenAuthentication := &models.LFSTokenResponse{
+ tokenAuthentication := &git_model.LFSTokenResponse{
Header: make(map[string]string),
Href: url,
}
@@ -296,19 +302,29 @@ func runServ(c *cli.Context) error {
gitcmd = exec.CommandContext(ctx, verb, repoPath)
}
- // Check if setting.RepoRootPath exists. It could be the case that it doesn't exist, this can happen when
- // `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection.
- if _, err := os.Stat(setting.RepoRootPath); err != nil {
- if os.IsNotExist(err) {
- return fail("Incorrect configuration.",
- "Directory `[repository]` `ROOT` was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository]` `ROOT` an absolute value.")
- }
- }
-
+ process.SetSysProcAttribute(gitcmd)
gitcmd.Dir = setting.RepoRootPath
gitcmd.Stdout = os.Stdout
gitcmd.Stdin = os.Stdin
gitcmd.Stderr = os.Stderr
+ gitcmd.Env = append(gitcmd.Env, os.Environ()...)
+ gitcmd.Env = append(gitcmd.Env,
+ repo_module.EnvRepoIsWiki+"="+strconv.FormatBool(results.IsWiki),
+ repo_module.EnvRepoName+"="+results.RepoName,
+ repo_module.EnvRepoUsername+"="+results.OwnerName,
+ repo_module.EnvPusherName+"="+results.UserName,
+ repo_module.EnvPusherEmail+"="+results.UserEmail,
+ repo_module.EnvPusherID+"="+strconv.FormatInt(results.UserID, 10),
+ repo_module.EnvRepoID+"="+strconv.FormatInt(results.RepoID, 10),
+ repo_module.EnvPRID+"="+fmt.Sprintf("%d", 0),
+ repo_module.EnvDeployKeyID+"="+fmt.Sprintf("%d", results.DeployKeyID),
+ repo_module.EnvKeyID+"="+fmt.Sprintf("%d", results.KeyID),
+ repo_module.EnvAppURL+"="+setting.AppURL,
+ )
+ // to avoid breaking, here only use the minimal environment variables for the "gitea serv" command.
+ // it could be re-considered whether to use the same git.CommonGitCmdEnvs() as "git" command later.
+ gitcmd.Env = append(gitcmd.Env, git.CommonCmdServEnvs()...)
+
if err = gitcmd.Run(); err != nil {
return fail("Internal error", "Failed to execute git command: %v", err)
}
diff --git a/cmd/web.go b/cmd/web.go
index 8c7c026172745..49a03356158e2 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -1,6 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package cmd
@@ -21,6 +20,7 @@ import (
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/install"
+ "github.com/felixge/fgprof"
"github.com/urfave/cli"
ini "gopkg.in/ini.v1"
)
@@ -75,7 +75,7 @@ func runHTTPRedirector() {
http.Redirect(w, r, target, http.StatusTemporaryRedirect)
})
- err := runHTTP("tcp", source, "HTTP Redirector", handler)
+ err := runHTTP("tcp", source, "HTTP Redirector", handler, setting.RedirectorUseProxyProtocol)
if err != nil {
log.Fatal("Failed to start port redirection: %v", err)
}
@@ -125,8 +125,10 @@ func runWeb(ctx *cli.Context) error {
return err
}
}
- c := install.Routes()
+ installCtx, cancel := context.WithCancel(graceful.GetManager().HammerContext())
+ c := install.Routes(installCtx)
err := listen(c, false)
+ cancel()
if err != nil {
log.Critical("Unable to open listener for installer. Is Gitea already running?")
graceful.GetManager().DoGracefulShutdown()
@@ -145,9 +147,11 @@ func runWeb(ctx *cli.Context) error {
if setting.EnablePprof {
go func() {
+ http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
_, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true)
+ // The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it.
log.Info("Starting pprof server on localhost:6060")
- log.Info("%v", http.ListenAndServe("localhost:6060", nil))
+ log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil))
finished()
}()
}
@@ -172,7 +176,7 @@ func runWeb(ctx *cli.Context) error {
}
// Set up Chi routes
- c := routers.NormalRoutes()
+ c := routers.NormalRoutes(graceful.GetManager().HammerContext())
err := listen(c, true)
<-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid())
@@ -198,7 +202,7 @@ func setPort(port string) error {
defaultLocalURL += ":" + setting.HTTPPort + "/"
// Save LOCAL_ROOT_URL if port changed
- setting.CreateOrAppendToCustomConf(func(cfg *ini.File) {
+ setting.CreateOrAppendToCustomConf("server.LOCAL_ROOT_URL", func(cfg *ini.File) {
cfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL)
})
}
@@ -228,40 +232,38 @@ func listen(m http.Handler, handleRedirector bool) error {
if handleRedirector {
NoHTTPRedirector()
}
- err = runHTTP("tcp", listenAddr, "Web", m)
+ err = runHTTP("tcp", listenAddr, "Web", m, setting.UseProxyProtocol)
case setting.HTTPS:
if setting.EnableAcme {
err = runACME(listenAddr, m)
break
- } else {
- if handleRedirector {
- if setting.RedirectOtherPort {
- go runHTTPRedirector()
- } else {
- NoHTTPRedirector()
- }
+ }
+ if handleRedirector {
+ if setting.RedirectOtherPort {
+ go runHTTPRedirector()
+ } else {
+ NoHTTPRedirector()
}
- err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, m)
}
+ err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, m, setting.UseProxyProtocol, setting.ProxyProtocolTLSBridging)
case setting.FCGI:
if handleRedirector {
NoHTTPRedirector()
}
- err = runFCGI("tcp", listenAddr, "FCGI Web", m)
+ err = runFCGI("tcp", listenAddr, "FCGI Web", m, setting.UseProxyProtocol)
case setting.HTTPUnix:
if handleRedirector {
NoHTTPRedirector()
}
- err = runHTTP("unix", listenAddr, "Web", m)
+ err = runHTTP("unix", listenAddr, "Web", m, setting.UseProxyProtocol)
case setting.FCGIUnix:
if handleRedirector {
NoHTTPRedirector()
}
- err = runFCGI("unix", listenAddr, "Web", m)
+ err = runFCGI("unix", listenAddr, "Web", m, setting.UseProxyProtocol)
default:
log.Fatal("Invalid protocol: %s", setting.Protocol)
}
-
if err != nil {
log.Critical("Failed to start server: %v", err)
}
diff --git a/cmd/web_acme.go b/cmd/web_acme.go
index 7dbeb14a0e64f..90e4a02764b67 100644
--- a/cmd/web_acme.go
+++ b/cmd/web_acme.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
@@ -66,7 +65,7 @@ func runACME(listenAddr string, m http.Handler) error {
log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
}
}
- myACME := certmagic.NewACMEManager(magic, certmagic.ACMEManager{
+ myACME := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
CA: setting.AcmeURL,
TrustedRoots: certPool,
Email: setting.AcmeEmail,
@@ -113,14 +112,14 @@ func runACME(listenAddr string, m http.Handler) error {
log.Info("Running Let's Encrypt handler on %s", setting.HTTPAddr+":"+setting.PortToRedirect)
// all traffic coming into HTTP will be redirect to HTTPS automatically (LE HTTP-01 validation happens here)
- err := runHTTP("tcp", setting.HTTPAddr+":"+setting.PortToRedirect, "Let's Encrypt HTTP Challenge", myACME.HTTPChallengeHandler(http.HandlerFunc(runLetsEncryptFallbackHandler)))
+ err := runHTTP("tcp", setting.HTTPAddr+":"+setting.PortToRedirect, "Let's Encrypt HTTP Challenge", myACME.HTTPChallengeHandler(http.HandlerFunc(runLetsEncryptFallbackHandler)), setting.RedirectorUseProxyProtocol)
if err != nil {
log.Fatal("Failed to start the Let's Encrypt handler on port %s: %v", setting.PortToRedirect, err)
}
}()
}
- return runHTTPSWithTLSConfig("tcp", listenAddr, "Web", tlsConfig, m)
+ return runHTTPSWithTLSConfig("tcp", listenAddr, "Web", tlsConfig, m, setting.UseProxyProtocol, setting.ProxyProtocolTLSBridging)
}
func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) {
diff --git a/cmd/web_graceful.go b/cmd/web_graceful.go
index 1618208c557eb..996537be3b59a 100644
--- a/cmd/web_graceful.go
+++ b/cmd/web_graceful.go
@@ -1,6 +1,5 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package cmd
@@ -15,8 +14,8 @@ import (
"code.gitea.io/gitea/modules/setting"
)
-func runHTTP(network, listenAddr, name string, m http.Handler) error {
- return graceful.HTTPListenAndServe(network, listenAddr, name, m)
+func runHTTP(network, listenAddr, name string, m http.Handler, useProxyProtocol bool) error {
+ return graceful.HTTPListenAndServe(network, listenAddr, name, m, useProxyProtocol)
}
// NoHTTPRedirector tells our cleanup routine that we will not be using a fallback http redirector
@@ -36,7 +35,7 @@ func NoInstallListener() {
graceful.GetManager().InformCleanup()
}
-func runFCGI(network, listenAddr, name string, m http.Handler) error {
+func runFCGI(network, listenAddr, name string, m http.Handler, useProxyProtocol bool) error {
// This needs to handle stdin as fcgi point
fcgiServer := graceful.NewServer(network, listenAddr, name)
@@ -47,7 +46,7 @@ func runFCGI(network, listenAddr, name string, m http.Handler) error {
}
m.ServeHTTP(resp, req)
}))
- })
+ }, useProxyProtocol)
if err != nil {
log.Fatal("Failed to start FCGI main server: %v", err)
}
diff --git a/cmd/web_https.go b/cmd/web_https.go
index b0910ca04000f..70d35cd40d84a 100644
--- a/cmd/web_https.go
+++ b/cmd/web_https.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cmd
@@ -129,14 +128,14 @@ var (
defaultCiphersChaChaFirst = append(defaultCiphersChaCha, defaultCiphersAES...)
)
-// runHTTPs listens on the provided network address and then calls
+// runHTTPS listens on the provided network address and then calls
// Serve to handle requests on incoming TLS connections.
//
// Filenames containing a certificate and matching private key for the server must
// be provided. If the certificate is signed by a certificate authority, the
// certFile should be the concatenation of the server's certificate followed by the
// CA's certificate.
-func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handler) error {
+func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handler, useProxyProtocol, proxyProtocolTLSBridging bool) error {
tlsConfig := &tls.Config{}
if tlsConfig.NextProtos == nil {
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
@@ -184,9 +183,9 @@ func runHTTPS(network, listenAddr, name, certFile, keyFile string, m http.Handle
return err
}
- return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m)
+ return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m, useProxyProtocol, proxyProtocolTLSBridging)
}
-func runHTTPSWithTLSConfig(network, listenAddr, name string, tlsConfig *tls.Config, m http.Handler) error {
- return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m)
+func runHTTPSWithTLSConfig(network, listenAddr, name string, tlsConfig *tls.Config, m http.Handler, useProxyProtocol, proxyProtocolTLSBridging bool) error {
+ return graceful.HTTPListenAndServeTLSConfig(network, listenAddr, name, tlsConfig, m, useProxyProtocol, proxyProtocolTLSBridging)
}
diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go
index ccda03fa92555..b502c15cecd61 100644
--- a/contrib/environment-to-ini/environment-to-ini.go
+++ b/contrib/environment-to-ini/environment-to-ini.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package main
diff --git a/contrib/fixtures/fixture_generation.go b/contrib/fixtures/fixture_generation.go
index 66ff5c54e31be..210e0aae0676f 100644
--- a/contrib/fixtures/fixture_generation.go
+++ b/contrib/fixtures/fixture_generation.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package main
diff --git a/contrib/gitea-monitoring-mixin/dashboards/overview.libsonnet b/contrib/gitea-monitoring-mixin/dashboards/overview.libsonnet
index 3e2513c4cfcae..31b7d4f9b2e81 100644
--- a/contrib/gitea-monitoring-mixin/dashboards/overview.libsonnet
+++ b/contrib/gitea-monitoring-mixin/dashboards/overview.libsonnet
@@ -29,7 +29,7 @@ local addIssueLabelsOverrides(labels) =
grafanaDashboards+:: {
- local giteaSelector = 'job="$job", instance="$instance"',
+ local giteaSelector = 'job=~"$job", instance=~"$instance"',
local giteaStatsPanel =
grafana.statPanel.new(
'Gitea stats',
@@ -399,25 +399,31 @@ local addIssueLabelsOverrides(labels) =
.addTemplate(
{
hide: 0,
- label: null,
+ label: 'job',
name: 'job',
options: [],
+ datasource: '$datasource',
query: 'label_values(gitea_organizations, job)',
refresh: 1,
regex: '',
type: 'query',
+ multi: true,
+ allValue: '.+'
},
)
.addTemplate(
{
hide: 0,
- label: null,
+ label: 'instance',
name: 'instance',
options: [],
+ datasource: '$datasource',
query: 'label_values(gitea_organizations{job="$job"}, instance)',
refresh: 1,
regex: '',
type: 'query',
+ multi: true,
+ allValue: '.+'
},
)
.addTemplate(
diff --git a/contrib/init/gentoo/gitea b/contrib/init/gentoo/gitea
index e423eae54ddc6..db904e7bbba58 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 42ccf88af8386..b31a4a8c689e7 100644
--- a/contrib/pr/checkout.go
+++ b/contrib/pr/checkout.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package main
@@ -14,7 +13,6 @@ import (
"fmt"
"log"
"net/http"
- "net/url"
"os"
"os/exec"
"os/user"
@@ -24,15 +22,17 @@ import (
"strconv"
"time"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
gitea_git "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/external"
+ repo_module "code.gitea.io/gitea/modules/repository"
"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"
@@ -61,11 +61,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"
@@ -80,7 +76,6 @@ func runPR() {
setting.RunUser = curUser.Username
log.Printf("[PR] Loading fixtures data ...\n")
- gitea_git.CheckLFSVersion()
//models.LoadConfigs()
/*
setting.Database.Type = "sqlite3"
@@ -111,14 +106,14 @@ func runPR() {
}
unittest.LoadFixtures()
util.RemoveAll(setting.RepoRootPath)
- util.RemoveAll(models.LocalCopyPath())
- unittest.CopyDir(path.Join(curDir, "integrations/gitea-repositories-meta"), setting.RepoRootPath)
+ util.RemoveAll(repo_module.LocalCopyPath())
+ unittest.CopyDir(path.Join(curDir, "tests/gitea-repositories-meta"), setting.RepoRootPath)
log.Printf("[PR] Setting up router\n")
// routers.GlobalInit()
external.RegisterRenderers()
- markup.Init()
- c := routers.NormalRoutes()
+ markup.Init(markup_service.ProcessorHelper())
+ c := routers.NormalRoutes(graceful.GetManager().HammerContext())
log.Printf("[PR] Ready for testing !\n")
log.Printf("[PR] Login with user1, user2, user3, ... with pass: password\n")
diff --git a/contrib/systemd/gitea.service b/contrib/systemd/gitea.service
index d6a4377ec8091..d205c6ee8ba6e 100644
--- a/contrib/systemd/gitea.service
+++ b/contrib/systemd/gitea.service
@@ -49,12 +49,8 @@ After=network.target
###
[Service]
-# Modify these two values and uncomment them if you have
-# repos with lots of files and get an HTTP error 500 because
-# of that
-###
-#LimitMEMLOCK=infinity
-#LimitNOFILE=65535
+# Uncomment the next line if you have repos with lots of files and get a HTTP 500 error because of that
+# LimitNOFILE=524288:524288
RestartSec=2s
Type=simple
User=git
@@ -78,6 +74,13 @@ Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
#AmbientCapabilities=CAP_NET_BIND_SERVICE
###
+# In some cases, when using CapabilityBoundingSet and AmbientCapabilities option, you may want to
+# set the following value to false to allow capabilities to be applied on gitea process. The following
+# value if set to true sandboxes gitea service and prevent any processes from running with privileges
+# in the host user namespace.
+###
+#PrivateUsers=false
+###
[Install]
WantedBy=multi-user.target
diff --git a/contrib/upgrade.sh b/contrib/upgrade.sh
index 9a5e903b6b68d..3a98c277d6b33 100755
--- a/contrib/upgrade.sh
+++ b/contrib/upgrade.sh
@@ -24,7 +24,8 @@
function giteacmd {
if [[ $sudocmd = "su" ]]; then
- "$sudocmd" - "$giteauser" -c "$giteabin" --config "$giteaconf" --work-path "$giteahome" "$@"
+ # `-c` only accept one string as argument.
+ "$sudocmd" - "$giteauser" -c "$(printf "%q " "$giteabin" "--config" "$giteaconf" "--work-path" "$giteahome" "$@")"
else
"$sudocmd" --user "$giteauser" "$giteabin" --config "$giteaconf" --work-path "$giteahome" "$@"
fi
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 4b5c2b1022725..eca1184ff98f4 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -7,6 +7,38 @@
;; see https://docs.gitea.io/en-us/config-cheat-sheet/ for additional documentation.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Default Configuration (non-`app.ini` configuration)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;; These values are environment-dependent but form the basis of a lot of values. They will be
+;; reported as part of the default configuration when running `gitea --help` or on start-up. The order they are emitted there is slightly different but we will list them here in the order they are set-up.
+;;
+;; - _`AppPath`_: This is the absolute path of the running gitea binary.
+;; - _`AppWorkPath`_: This refers to "working path" of the `gitea` binary. It is determined by using the first set thing in the following hierarchy:
+;; - The `--work-path` flag passed to the binary
+;; - The environment variable `$GITEA_WORK_DIR`
+;; - A built-in value set at build time (see building from source)
+;; - Otherwise it defaults to the directory of the _`AppPath`_
+;; - If any of the above are relative paths then they are made absolute against the
+;; the directory of the _`AppPath`_
+;; - _`CustomPath`_: This is the base directory for custom templates and other options.
+;; It is determined by using the first set thing in the following hierarchy:
+;; - The `--custom-path` flag passed to the binary
+;; - The environment variable `$GITEA_CUSTOM`
+;; - A built-in value set at build time (see building from source)
+;; - Otherwise it defaults to _`AppWorkPath`_`/custom`
+;; - If any of the above are relative paths then they are made absolute against the
+;; the directory of the _`AppWorkPath`_
+;; - _`CustomConf`_: This is the path to the `app.ini` file.
+;; - The `--config` flag passed to the binary
+;; - A built-in value set at build time (see building from source)
+;; - Otherwise it defaults to _`CustomPath`_`/conf/app.ini`
+;; - If any of the above are relative paths then they are made absolute against the
+;; the directory of the _`CustomPath`_
+;;
+;; In addition there is _`StaticRootPath`_ which can be set as a built-in at build time, but will otherwise default to _`AppWorkPath`_
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; General Settings
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -26,9 +58,21 @@ RUN_MODE = ; prod
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
-;; The protocol the server listens on. One of 'http', 'https', 'unix' or 'fcgi'. Defaults to 'http'
+;; The protocol the server listens on. One of 'http', 'https', 'http+unix', 'fcgi' or 'fcgi+unix'. Defaults to 'http'
;PROTOCOL = http
;;
+;; Expect PROXY protocol headers on connections
+;USE_PROXY_PROTOCOL = false
+;;
+;; Use PROXY protocol in TLS Bridging mode
+;PROXY_PROTOCOL_TLS_BRIDGING = false
+;;
+; Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
+;PROXY_PROTOCOL_HEADER_TIMEOUT=5s
+;;
+; Accept PROXY protocol headers with UNKNOWN type
+;PROXY_PROTOCOL_ACCEPT_UNKNOWN=false
+;;
;; Set the domain for the server
;DOMAIN = localhost
;;
@@ -39,6 +83,8 @@ RUN_MODE = ; prod
;STATIC_URL_PREFIX =
;;
;; The address to listen on. Either a IPv4/IPv6 address or the path to a unix socket.
+;; If PROTOCOL is set to `http+unix` or `fcgi+unix`, this should be the name of the Unix socket file to use.
+;; Relative paths will be made absolute against the _`AppWorkPath`_.
;HTTP_ADDR = 0.0.0.0
;;
;; The port to listen on. Leave empty when using a unix socket.
@@ -51,6 +97,8 @@ RUN_MODE = ; prod
;REDIRECT_OTHER_PORT = false
;PORT_TO_REDIRECT = 80
;;
+;; expect PROXY protocol header on connections to https redirector.
+;REDIRECTOR_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s
;; Minimum and maximum supported TLS versions
;SSL_MIN_VERSION=TLSv1.2
;SSL_MAX_VERSION=
@@ -61,7 +109,7 @@ RUN_MODE = ; prod
;; SSL Cipher Suites
;SSL_CIPHER_SUITES=; Will default to "ecdhe_ecdsa_with_aes_256_gcm_sha384,ecdhe_rsa_with_aes_256_gcm_sha384,ecdhe_ecdsa_with_aes_128_gcm_sha256,ecdhe_rsa_with_aes_128_gcm_sha256,ecdhe_ecdsa_with_chacha20_poly1305,ecdhe_rsa_with_chacha20_poly1305" if aes is supported by hardware, otherwise chacha will be first.
;;
-;; Timeout for any write to the connection. (Set to 0 to disable all timeouts.)
+;; Timeout for any write to the connection. (Set to -1 to disable all timeouts.)
;PER_WRITE_TIMEOUT = 30s
;;
;; Timeout per Kb written to connections.
@@ -76,13 +124,19 @@ RUN_MODE = ; prod
;; Do not set this variable if PROTOCOL is set to 'unix'.
;LOCAL_ROOT_URL = %(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/
;;
+;; When making local connections pass the PROXY protocol header.
+;LOCAL_USE_PROXY_PROTOCOL = %(USE_PROXY_PROTOCOL)s
+;;
;; Disable SSH feature when not available
;DISABLE_SSH = false
;;
;; Whether to use the builtin SSH server or not.
;START_SSH_SERVER = false
;;
-;; Username to use for the builtin SSH server.
+;; Expect PROXY protocol header on connections to the built-in SSH server
+;SSH_SERVER_USE_PROXY_PROTOCOL = false
+;;
+;; Username to use for the builtin SSH server. If blank, then it is the value of RUN_USER.
;BUILTIN_SSH_SERVER_USER = %(RUN_USER)s
;;
;; Domain name to be exposed in clone URL
@@ -125,7 +179,7 @@ RUN_MODE = ; prod
;;
;; For the built-in SSH server, choose the keypair to offer as the host key
;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
-;; relative paths are made absolute relative to the APP_DATA_PATH
+;; relative paths are made absolute relative to the %(APP_DATA_PATH)s
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
;;
;; Directory to create temporary files in when testing public keys using ssh-keygen,
@@ -163,7 +217,7 @@ RUN_MODE = ; prod
;; Enable exposure of SSH clone URL to anonymous visitors, default is false
;SSH_EXPOSE_ANONYMOUS = false
;;
-;; Timeout for any write to ssh connections. (Set to 0 to disable all timeouts.)
+;; Timeout for any write to ssh connections. (Set to -1 to disable all timeouts.)
;; Will default to the PER_WRITE_TIMEOUT.
;SSH_PER_WRITE_TIMEOUT = 30s
;;
@@ -221,10 +275,10 @@ RUN_MODE = ; prod
;;
;; Root directory containing templates and static files.
;; default is the path where Gitea is executed
-;STATIC_ROOT_PATH =
+;STATIC_ROOT_PATH = ; Will default to the built-in value _`StaticRootPath`_
;;
;; Default path for App data
-;APP_DATA_PATH = data
+;APP_DATA_PATH = data ; relative paths will be made absolute with _`AppWorkPath`_
;;
;; Enable gzip compression for runtime-generated content, static resources excluded
;ENABLE_GZIP = false
@@ -235,7 +289,7 @@ RUN_MODE = ; prod
;ENABLE_PPROF = false
;;
;; PPROF_DATA_PATH, use an absolute path when you start gitea as service
-;PPROF_DATA_PATH = data/tmp/pprof
+;PPROF_DATA_PATH = data/tmp/pprof ; Path is relative to _`AppWorkPath`_
;;
;; Landing page, can be "home", "explore", "organizations", "login", or any URL such as "/org/repo" or even "https://anotherwebsite.com"
;; The "login" choice is not a security measure but just a UI flow change, use REQUIRE_SIGNIN_VIEW to force users to log in.
@@ -313,6 +367,7 @@ USER = root
;DB_TYPE = sqlite3
;PATH= ; defaults to data/gitea.db
;SQLITE_TIMEOUT = ; Query timeout defaults to: 500
+;SQLITE_JOURNAL_MODE = ; defaults to sqlite database default (often DELETE), can be used to enable WAL mode. https://www.sqlite.org/pragma.html#pragma_journal_mode
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
@@ -348,6 +403,9 @@ LOG_SQL = false ; if unset defaults to true
;;
;; Database maximum number of open connections, default is 0 meaning no maximum
;MAX_OPEN_CONNS = 0
+;;
+;; Whether execute database models migrations automatically
+;AUTO_MIGRATION = true
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -358,14 +416,19 @@ LOG_SQL = false ; if unset defaults to true
;; Whether the installer is disabled (set to true to disable the installer)
INSTALL_LOCK = false
;;
-;; Global secret key that will be used - if blank will be regenerated.
+;; Global secret key that will be used
+;; This key is VERY IMPORTANT. If you lose it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
SECRET_KEY =
;;
+;; Alternative location to specify secret key, instead of this file; you cannot specify both this and SECRET_KEY, and must pick one
+;; This key is VERY IMPORTANT. If you lose it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
+;SECRET_KEY_URI = file:/etc/gitea/secret_key
+;;
;; Secret used to validate communication within Gitea binary.
INTERNAL_TOKEN=
;;
-;; Instead of defining internal token in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: file:/etc/gitea/internal_token)
-;INTERNAL_TOKEN_URI = ;e.g. /etc/gitea/internal_token
+;; Alternative location to specify internal token, instead of this file; you cannot specify both this and INTERNAL_TOKEN, and must pick one
+;INTERNAL_TOKEN_URI = file:/etc/gitea/internal_token
;;
;; How long to remember that a user is logged in before requiring relogin (in days)
;LOGIN_REMEMBER_DAYS = 7
@@ -376,9 +439,10 @@ INTERNAL_TOKEN=
;; Name of cookie used to store authentication information.
;COOKIE_REMEMBER_NAME = gitea_incredible
;;
-;; Reverse proxy authentication header name of user name and email
+;; Reverse proxy authentication header name of user name, email, and full name
;REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER
;REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL
+;REVERSE_PROXY_AUTHENTICATION_FULL_NAME = X-WEBAUTH-FULLNAME
;;
;; Interpret X-Forwarded-For header or the X-Real-IP header and set this as the remote IP for the request
;REVERSE_PROXY_LIMIT = 1
@@ -398,6 +462,7 @@ INTERNAL_TOKEN=
;; By modifying the Gitea database, users can gain Gitea administrator privileges.
;; It also enables them to access other resources available to the user on the operating system that is running the Gitea instance and perform arbitrary actions in the name of the Gitea OS user.
;; WARNING: This maybe harmful to you website or your operating system.
+;; WARNING: Setting this to true does not change existing hooks in git repos; adjust it before if necessary.
;DISABLE_GIT_HOOKS = true
;;
;; Set to true to disable webhooks feature.
@@ -474,20 +539,6 @@ ENABLE = true
;; Maximum length of oauth2 token/cookie stored on server
;MAX_TOKEN_LENGTH = 32767
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-[U2F]
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;
-;; NOTE: THE DEFAULT VALUES HERE WILL NEED TO BE CHANGED
-;; Two Factor authentication with security keys
-;; https://developers.yubico.com/U2F/App_ID.html
-;;
-;; DEPRECATED - this only applies to previously registered security keys using the U2F standard
-APP_ID = ; e.g. http://localhost:3000/
-
-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[log]
@@ -616,7 +667,10 @@ ROUTER = console
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; The path of git executable. If empty, Gitea searches through the PATH environment.
-PATH =
+;PATH =
+;;
+;; The HOME directory for Git
+;HOME_PATH = %(APP_DATA_PATH)s/home
;;
;; Disables highlight of added and removed changes
;DISABLE_DIFF_HIGHLIGHT = false
@@ -641,6 +695,7 @@ PATH =
;GC_ARGS =
;;
;; If use git wire protocol version 2 when git version >= 2.18, default is true, set to false when you always want git wire protocol version 1
+;; To enable this for Git over SSH when using a OpenSSH server, add `AcceptEnv GIT_PROTOCOL` to your sshd_config file.
;ENABLE_AUTO_GIT_WIRE_PROTOCOL = true
;;
;; Respond to pushes to a non-default branch with a URL for creating a Pull Request (if the repository has them enabled)
@@ -702,13 +757,19 @@ PATH =
;ENABLE_REVERSE_PROXY_AUTHENTICATION = false
;ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false
;ENABLE_REVERSE_PROXY_EMAIL = false
+;ENABLE_REVERSE_PROXY_FULL_NAME = false
;;
;; Enable captcha validation for registration
;ENABLE_CAPTCHA = false
;;
-;; Type of captcha you want to use. Options: image, recaptcha, hcaptcha
+;; Enable this to require captcha validation for login
+;REQUIRE_CAPTCHA_FOR_LOGIN = false
+;;
+;; Type of captcha you want to use. Options: image, recaptcha, hcaptcha, mcaptcha.
;CAPTCHA_TYPE = image
;;
+;; Change this to use recaptcha.net or other recaptcha service
+;RECAPTCHA_URL = https://www.google.com/recaptcha/
;; Enable recaptcha to use Google's recaptcha service
;; Go to https://www.google.com/recaptcha/admin to sign up for a key
;RECAPTCHA_SECRET =
@@ -718,8 +779,13 @@ PATH =
;HCAPTCHA_SECRET =
;HCAPTCHA_SITEKEY =
;;
-;; Change this to use recaptcha.net or other recaptcha service
-;RECAPTCHA_URL = https://www.google.com/recaptcha/
+;; Change this to use demo.mcaptcha.org or your self-hosted mcaptcha.org instance.
+;MCAPTCHA_URL = https://demo.mcaptcha.org
+;;
+;; Go to your configured mCaptcha instance and register a sitekey
+;; and use your account's secret.
+;MCAPTCHA_SECRET =
+;MCAPTCHA_SITEKEY =
;;
;; Default value for KeepEmailPrivate
;; Each new user will get the value of this setting copied into their profile
@@ -812,7 +878,8 @@ PATH =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[repository]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Root path for storing all repository data. It must be an absolute path. By default, it is stored in a sub-directory of `APP_DATA_PATH`.
+;; Root path for storing all repository data. By default, it is set to %(APP_DATA_PATH)s/gitea-repositories.
+;; A relative path is interpreted as _`AppWorkPath`_/%(ROOT)s
;ROOT =
;;
;; The script type this server supports. Usually this is `bash`, but some users report that only `sh` is available.
@@ -860,7 +927,7 @@ PATH =
;USE_COMPAT_SSH_URI = false
;;
;; Close issues as long as a commit on any branch marks it as fixed
-;; Comma separated list of globally disabled repo units. Allowed values: repo.issues, repo.ext_issues, repo.pulls, repo.wiki, repo.ext_wiki
+;; Comma separated list of globally disabled repo units. Allowed values: repo.issues, repo.ext_issues, repo.pulls, repo.wiki, repo.ext_wiki, repo.projects
;DISABLED_REPO_UNITS =
;;
;; Comma separated list of default repo units. Allowed values: repo.code, repo.releases, repo.issues, repo.pulls, repo.wiki, repo.projects.
@@ -887,6 +954,12 @@ PATH =
;; Allow deletion of unadopted repositories
;ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES = false
+;; Don't allow download source archive files from UI
+;DISABLE_DOWNLOAD_SOURCE_ARCHIVES = false
+
+;; Allow fork repositories without maximum number limit
+;ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT = true
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[repository.editor]
@@ -937,7 +1010,7 @@ PATH =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
-;; List of prefixes used in Pull Request title to mark them as Work In Progress
+;; List of prefixes used in Pull Request title to mark them as Work In Progress (matched in a case-insensitive manner)
;WORK_IN_PROGRESS_PREFIXES = WIP:,[WIP]
;;
;; List of keywords used in Pull Request comments to automatically close a related issue
@@ -946,6 +1019,9 @@ PATH =
;; List of keywords used in Pull Request comments to automatically reopen a related issue
;REOPEN_KEYWORDS = reopen,reopens,reopened
;;
+;; Set default merge style for repository creating, valid options: merge, rebase, rebase-merge, squash
+;DEFAULT_MERGE_STYLE = merge
+;;
;; In the default merge message for squash commits include at most this many commits
;DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT = 50
;;
@@ -963,6 +1039,9 @@ PATH =
;;
;; Add co-authored-by and co-committed-by trailers if committer does not match author
;ADD_CO_COMMITTER_TRAILERS = true
+;;
+;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply
+;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1071,6 +1150,9 @@ PATH =
;; allow request with credentials
;ALLOW_CREDENTIALS = false
;;
+;; headers to permit
+;HEADERS = Content-Type,User-Agent
+;;
;; set X-FRAME-OPTIONS header
;X_FRAME_OPTIONS = SAMEORIGIN
@@ -1084,7 +1166,7 @@ PATH =
;EXPLORE_PAGING_NUM = 20
;;
;; Number of issues that are displayed on one page
-;ISSUE_PAGING_NUM = 10
+;ISSUE_PAGING_NUM = 20
;;
;; Number of maximum commits displayed in one activity feed
;FEED_MAX_COMMIT_NUM = 5
@@ -1092,6 +1174,9 @@ PATH =
;; Number of items that are displayed in home feed
;FEED_PAGING_NUM = 20
;;
+;; Number of items that are displayed in a single subsitemap
+;SITEMAP_PAGING_NUM = 20
+;;
;; Number of maximum commits displayed in commit graph.
;GRAPH_MAX_COMMIT_NUM = 100
;;
@@ -1133,6 +1218,10 @@ PATH =
;;
;; Whether to enable a Service Worker to cache frontend assets
;USE_SERVICE_WORKER = false
+;;
+;; Whether to only show relevant repos on the explore page when no keyword is specified and default sorting is used.
+;; A repo is considered irrelevant if it's a fork or if it has no metadata (no description, no icon, no topic).
+;ONLY_SHOW_RELEVANT_REPOS = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1177,7 +1266,7 @@ PATH =
;;
;; Control how often the notification endpoint is polled to update the notification
;; The timeout will increase to MAX_TIMEOUT in TIMEOUT_STEPs if the notification count is unchanged
-;; Set MIN_TIMEOUT to 0 to turn off
+;; Set MIN_TIMEOUT to -1 to turn off
;MIN_TIMEOUT = 10s
;MAX_TIMEOUT = 60s
;TIMEOUT_STEP = 10s
@@ -1227,6 +1316,9 @@ PATH =
;; List of file extensions that should be rendered/edited as Markdown
;; Separate the extensions with a comma. To render files without any extension as markdown, just put a comma
;FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd
+;;
+;; Enables math inline and block detection
+;ENABLE_MATH = true
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1237,7 +1329,7 @@ PATH =
;; Define allowed algorithms and their minimum key length (use -1 to disable a type)
;ED25519 = 256
;ECDSA = 256
-;RSA = 2048
+;RSA = 2047 ; we allow 2047 here because an otherwise valid 2048 bit RSA key can be reported as having 2047 bit length
;DSA = -1 ; set to 1024 to switch on
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1253,7 +1345,7 @@ PATH =
;ISSUE_INDEXER_TYPE = bleve
;;
;; Issue indexer storage path, available when ISSUE_INDEXER_TYPE is bleve
-;ISSUE_INDEXER_PATH = indexers/issues.bleve
+;ISSUE_INDEXER_PATH = indexers/issues.bleve ; Relative paths will be made absolute against _`AppWorkPath`_.
;;
;; Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch
;ISSUE_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
@@ -1262,7 +1354,7 @@ PATH =
;ISSUE_INDEXER_NAME = gitea_issues
;;
;; Timeout the indexer if it takes longer than this to start.
-;; Set to zero to disable timeout.
+;; Set to -1 to disable timeout.
;STARTUP_TIMEOUT = 30s
;;
;; Issue indexer queue, currently support: channel, levelqueue or redis, default is levelqueue (deprecated - use [queue.issue_indexer])
@@ -1271,7 +1363,7 @@ PATH =
;; When ISSUE_INDEXER_QUEUE_TYPE is levelqueue, this will be the path where the queue will be saved.
;; This can be overridden by `ISSUE_INDEXER_QUEUE_CONN_STR`.
;; default is queues/common
-;ISSUE_INDEXER_QUEUE_DIR = queues/common; **DEPRECATED** use settings in `[queue.issue_indexer]`.
+;ISSUE_INDEXER_QUEUE_DIR = queues/common; **DEPRECATED** use settings in `[queue.issue_indexer]`. Relative paths will be made absolute against `%(APP_DATA_PATH)s`.
;;
;; When `ISSUE_INDEXER_QUEUE_TYPE` is `redis`, this will store the redis connection string.
;; When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this is a directory or additional options of
@@ -1327,7 +1419,7 @@ PATH =
;TYPE = persistable-channel
;;
;; data-dir for storing persistable queues and level queues, individual queues will default to `queues/common` meaning the queue is shared.
-;DATADIR = queues/
+;DATADIR = queues/ ; Relative paths will be made absolute against `%(APP_DATA_PATH)s`.
;;
;; Default queue length before a channel queue will block
;LENGTH = 20
@@ -1494,6 +1586,11 @@ PATH =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
+;; NOTICE: this section is for Gitea 1.18 and later. If you are using Gitea 1.17 or older,
+;; please refer to
+;; https://github.com/go-gitea/gitea/blob/release/v1.17/custom/conf/app.example.ini
+;; https://github.com/go-gitea/gitea/blob/release/v1.17/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+;;
;ENABLED = false
;;
;; Buffer length of channel, keep it as it is if you don't know what it is.
@@ -1502,30 +1599,42 @@ PATH =
;; Prefix displayed before subject in mail
;SUBJECT_PREFIX =
;;
-;; Mail server
-;; Gmail: smtp.gmail.com:587
-;; QQ: smtp.qq.com:465
-;; As per RFC 8314 using Implicit TLS/SMTPS on port 465 (if supported) is recommended,
-;; otherwise STARTTLS on port 587 should be used.
-;HOST =
-;;
-;; Disable HELO operation when hostnames are different.
-;DISABLE_HELO =
-;;
-;; Custom hostname for HELO operation, if no value is provided, one is retrieved from system.
+;; Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy".
+;; - sendmail: use the operating system's `sendmail` command instead of SMTP. This is common on Linux systems.
+;; - dummy: send email messages to the log as a testing phase.
+;; If your provider does not explicitly say which protocol it uses but does provide a port,
+;; you can set SMTP_PORT instead and this will be inferred.
+;; (Before 1.18, see the notice, this was controlled via MAILER_TYPE and IS_TLS_ENABLED.)
+;PROTOCOL =
+;;
+;; Mail server address, e.g. smtp.gmail.com.
+;; For smtp+unix, this should be a path to a unix socket instead.
+;; (Before 1.18, see the notice, this was combined with SMTP_PORT as HOST.)
+;SMTP_ADDR =
+;;
+;; Mail server port. Common ports are:
+;; 25: insecure SMTP
+;; 465: SMTP Secure
+;; 587: StartTLS
+;; If no protocol is specified, it will be inferred by this setting.
+;; (Before 1.18, this was combined with SMTP_ADDR as HOST.)
+;SMTP_PORT =
+;;
+;; Enable HELO operation. Defaults to true.
+;ENABLE_HELO = true
+;;
+;; Custom hostname for HELO operation.
+;; If no value is provided, one is retrieved from system.
;HELO_HOSTNAME =
;;
-;; Whether or not to skip verification of certificates; `true` to disable verification. This option is unsafe. Consider adding the certificate to the system trust store instead.
-;SKIP_VERIFY = false
-;;
-;; Use client certificate
-;USE_CERTIFICATE = false
-;CERT_FILE = custom/mailer/cert.pem
-;KEY_FILE = custom/mailer/key.pem
+;; If set to `true`, completely ignores server certificate validation errors.
+;; This option is unsafe. Consider adding the certificate to the system trust store instead.
+;FORCE_TRUST_SERVER_CERT = false
;;
-;; Should SMTP connect with TLS, (if port ends with 465 TLS will always be used.)
-;; If this is false but STARTTLS is supported the connection will be upgraded to TLS opportunistically.
-;IS_TLS_ENABLED = false
+;; Use client certificate in connection.
+;USE_CLIENT_CERT = false
+;CLIENT_CERT_FILE = custom/mailer/cert.pem
+;CLIENT_KEY_FILE = custom/mailer/key.pem
;;
;; Mail from address, RFC 5322. This can be just an email address, or the `"Name" ` format
;FROM =
@@ -1533,19 +1642,15 @@ PATH =
;; Sometimes it is helpful to use a different address on the envelope. Set this to use ENVELOPE_FROM as the from on the envelope. Set to `<>` to send an empty address.
;ENVELOPE_FROM =
;;
-;; Mailer user name and password
-;; Please Note: Authentication is only supported when the SMTP server communication is encrypted with TLS (this can be via STARTTLS) or `HOST=localhost`.
+;; Mailer user name and password, if required by provider.
;USER =
;;
;; Use PASSWD = `your password` for quoting if you use special characters in the password.
;PASSWD =
;;
-;; Send mails as plain text
+;; Send mails only in plain text, without HTML alternative
;SEND_AS_PLAIN_TEXT = false
;;
-;; Set Mailer Type (either SMTP, sendmail or dummy to just send to the log)
-;MAILER_TYPE = smtp
-;;
;; Specify an alternative sendmail binary
;SENDMAIL_PATH = sendmail
;;
@@ -1559,6 +1664,47 @@ PATH =
;; convert \r\n to \n for Sendmail
;SENDMAIL_CONVERT_CRLF = true
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;[email.incoming]
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;; Enable handling of incoming emails.
+;ENABLED = false
+;;
+;; The email address including the %{token} placeholder that will be replaced per user/action.
+;; Example: incoming+%{token}@example.com
+;; The placeholder must appear in the user part of the address (before the @).
+;REPLY_TO_ADDRESS =
+;;
+;; IMAP server host
+;HOST =
+;;
+;; IMAP server port
+;PORT =
+;;
+;; Username of the receiving account
+;USERNAME =
+;;
+;; Password of the receiving account
+;PASSWORD =
+;;
+;; Whether the IMAP server uses TLS.
+;USE_TLS = false
+;;
+;; If set to true, completely ignores server certificate validation errors. This option is unsafe.
+;SKIP_TLS_VERIFY = true
+;;
+;; The mailbox name where incoming mail will end up.
+;MAILBOX = INBOX
+;;
+;; Whether handled messages should be deleted from the mailbox.
+;DELETE_HANDLED_MESSAGE = true
+;;
+;; Maximum size of a message to handle. Bigger messages are ignored. Set to 0 to allow every size.
+;MAXIMUM_MESSAGE_SIZE = 10485760
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[cache]
@@ -1581,7 +1727,7 @@ PATH =
;HOST =
;;
;; Time to keep items in cache if not used, default is 16 hours.
-;; Setting it to 0 disables caching
+;; Setting it to -1 disables caching
;ITEM_TTL = 16h
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -1595,7 +1741,7 @@ PATH =
;ENABLED = true
;;
;; Time to keep items in cache if not used, default is 8760 hours.
-;; Setting it to 0 disables caching
+;; Setting it to -1 disables caching
;ITEM_TTL = 8760h
;;
;; Only enable the cache when repository's commits count great than
@@ -1607,7 +1753,8 @@ PATH =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
-;; Either "memory", "file", or "redis", default is "memory"
+;; Either "memory", "file", "redis", "db", "mysql", "couchbase", "memcache" or "postgres"
+;; Default is "memory". "db" will reuse the configuration in [database]
;PROVIDER = memory
;;
;; Provider config options
@@ -1615,7 +1762,7 @@ PATH =
;; file: session file path, e.g. `data/sessions`
;; redis: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180
;; mysql: go-sql-driver/mysql dsn config string, e.g. `root:password@/session_table`
-;PROVIDER_CONFIG = data/sessions
+;PROVIDER_CONFIG = data/sessions ; Relative paths will be made absolute against _`AppWorkPath`_.
;;
;; Session cookie name
;COOKIE_NAME = i_like_gitea
@@ -1681,7 +1828,7 @@ PATH =
;ENABLED = true
;;
;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
-;ALLOWED_TYPES = .docx,.gif,.gz,.jpeg,.jpg,.mp4,.log,.pdf,.png,.pptx,.txt,.xlsx,.zip
+;ALLOWED_TYPES = .csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip
;;
;; Max size of each file. Defaults to 4MB
;MAX_SIZE = 4
@@ -2047,7 +2194,7 @@ PATH =
;[cron.update_checker]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;ENABLED = false
+;ENABLED = true
;RUN_AT_START = false
;ENABLE_SUCCESS_NOTICE = false
;SCHEDULE = @every 168h
@@ -2066,6 +2213,28 @@ PATH =
;SCHEDULE = @every 168h
;OLDER_THAN = 8760h
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Garbage collect LFS pointers in repositories
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;[cron.gc_lfs]
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;ENABLED = false
+;; Garbage collect LFS pointers in repositories (default false)
+;RUN_AT_START = false
+;; Interval as a duration between each gc run (default every 24h)
+;SCHEDULE = @every 24h
+;; Only attempt to garbage collect LFSMetaObjects older than this (default 7 days)
+;OLDER_THAN = 168h
+;; Only attempt to garbage collect LFSMetaObjects that have not been attempted to be garbage collected for this long (default 3 days)
+;LAST_UPDATED_MORE_THAN_AGO = 72h
+; Minimum number of stale LFSMetaObjects to check per repo. Set to `0` to always check all.
+;NUMBER_TO_CHECK_PER_REPO = 100
+;Check at least this proportion of LFSMetaObjects per repo. (This may cause all stale LFSMetaObjects to be checked.)
+;PROPORTION_TO_CHECK_PER_REPO = 0.6
+
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Git Operation timeout in seconds
@@ -2085,7 +2254,7 @@ PATH =
;[mirror]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Enables the mirror functionality. Set to **false** to disable all mirrors.
+;; Enables the mirror functionality. Set to **false** to disable all mirrors. Pre-existing mirrors remain valid but won't be updated; may be converted to regular repo.
;ENABLED = true
;; Disable the creation of **new** pull mirrors. Pre-existing mirrors remain valid. Will be ignored if `mirror.ENABLED` is `false`.
;DISABLE_NEW_PULL = false
@@ -2101,7 +2270,7 @@ PATH =
;[api]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Enables Swagger. True or false; default is true.
+;; Enables the API documentation endpoints (/api/swagger, /api/v1/swagger, …). True or false.
;ENABLE_SWAGGER = true
;; Max number of items in a page
;MAX_RESPONSE_ITEMS = 50
@@ -2109,7 +2278,7 @@ PATH =
;DEFAULT_PAGING_NUM = 30
;; Default and maximum number of items per page for git trees api
;DEFAULT_GIT_TREES_PER_PAGE = 1000
-;; Default size of a blob returned by the blobs API (default is 10MiB)
+;; Default max size of a blob returned by the blobs API (default is 10MiB)
;DEFAULT_MAX_BLOB_SIZE = 10485760
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2118,8 +2287,8 @@ PATH =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; The first locale will be used as the default if user browser's language doesn't match any locale in the list.
-;LANGS = en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,uk-UA,ja-JP,es-ES,pt-BR,pt-PT,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR,el-GR,fa-IR,hu-HU,id-ID,ml-IN
-;NAMES = English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,Українська,日本語,español,português do Brasil,Português de Portugal,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어,ελληνικά,فارسی,magyar nyelv,bahasa Indonesia,മലയാളം
+;LANGS = en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,uk-UA,ja-JP,es-ES,pt-BR,pt-PT,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sv-SE,ko-KR,el-GR,fa-IR,hu-HU,id-ID,ml-IN
+;NAMES = English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,Français,Nederlands,Latviešu,Русский,Українська,日本語,Español,Português do Brasil,Português de Portugal,Polski,Български,Italiano,Suomi,Türkçe,Čeština,Српски,Svenska,한국어,Ελληνικά,فارسی,Magyar nyelv,Bahasa Indonesia,മലയാളം
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2139,7 +2308,10 @@ PATH =
;SHOW_FOOTER_VERSION = true
;; Show template execution time in the footer
;SHOW_FOOTER_TEMPLATE_LOAD_TIME = true
-
+;; Generate sitemap. Defaults to `true`.
+;ENABLE_SITEMAP = true
+;; Enable/Disable RSS/Atom feed
+;ENABLE_FEED = true
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2175,8 +2347,11 @@ PATH =
;RENDER_COMMAND = "asciidoc --out-file=- -"
;; Don't pass the file on STDIN, pass the filename as argument instead.
;IS_INPUT_FILE = false
-; Don't filter html tags and attributes if true
-;DISABLE_SANITIZER = false
+;; How the content will be rendered.
+;; * sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in [markup.sanitizer.*] .
+;; * no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code.
+;; * iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page.
+;RENDER_CONTENT_MODE=sanitized
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2222,13 +2397,16 @@ PATH =
;;
;; Allowed domains for migrating, default is blank. Blank means everything will be allowed.
;; Multiple domains could be separated by commas.
+;; Wildcard is supported: "github.com, *.github.com"
;ALLOWED_DOMAINS =
;;
;; Blocklist for migrating, default is blank. Multiple domains could be separated by commas.
;; When ALLOWED_DOMAINS is not blank, this option has a higher priority to deny domains.
+;; Wildcard is supported.
;BLOCKED_DOMAINS =
;;
;; Allow private addresses defined by RFC 1918, RFC 1122, RFC 4632 and RFC 4291 (false by default)
+;; If a domain is allowed by ALLOWED_DOMAINS, this option will be ignored.
;ALLOW_LOCALNETWORKS = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2238,7 +2416,27 @@ PATH =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Enable/Disable federation capabilities
-; ENABLED = true
+;ENABLED = false
+;;
+;; Enable/Disable user statistics for nodeinfo if federation is enabled
+;SHARE_USER_STATISTICS = true
+;;
+;; Maximum federation request and response size (MB)
+;MAX_SIZE = 4
+;;
+;; WARNING: Changing the settings below can break federation.
+;;
+;; HTTP signature algorithms
+;ALGORITHMS = rsa-sha256, rsa-sha512, ed25519
+;;
+;; HTTP signature digest algorithm
+;DIGEST_ALGORITHM = SHA-256
+;;
+;; GET headers for federation requests
+;GET_HEADERS = (request-target), Date
+;;
+;; POST headers for federation requests
+;POST_HEADERS = (request-target), Date, Digest
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -2251,6 +2449,35 @@ PATH =
;;
;; Path for chunked uploads. Defaults to APP_DATA_PATH + `tmp/package-upload`
;CHUNKED_UPLOAD_PATH = tmp/package-upload
+;;
+;; Maximum count of package versions a single owner can have (`-1` means no limits)
+;LIMIT_TOTAL_OWNER_COUNT = -1
+;; Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_TOTAL_OWNER_SIZE = -1
+;; Maximum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_COMPOSER = -1
+;; Maximum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_CONAN = -1
+;; Maximum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_CONTAINER = -1
+;; Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_GENERIC = -1
+;; Maximum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_HELM = -1
+;; Maximum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_MAVEN = -1
+;; Maximum size of a npm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_NPM = -1
+;; Maximum size of a NuGet upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_NUGET = -1
+;; Maximum size of a Pub upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_PUB = -1
+;; Maximum size of a PyPI upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_PYPI = -1
+;; Maximum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_RUBYGEMS = -1
+;; Maximum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+;LIMIT_SIZE_VAGRANT = -1
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/docker/root/etc/s6/openssh/setup b/docker/root/etc/s6/openssh/setup
index f7843050c170e..f43544d655272 100755
--- a/docker/root/etc/s6/openssh/setup
+++ b/docker/root/etc/s6/openssh/setup
@@ -14,11 +14,6 @@ if [ ! -f /data/ssh/ssh_host_rsa_key ]; then
ssh-keygen -t rsa -b 2048 -f /data/ssh/ssh_host_rsa_key -N "" > /dev/null
fi
-if [ ! -f /data/ssh/ssh_host_dsa_key ]; then
- echo "Generating /data/ssh/ssh_host_dsa_key..."
- ssh-keygen -t dsa -f /data/ssh/ssh_host_dsa_key -N "" > /dev/null
-fi
-
if [ ! -f /data/ssh/ssh_host_ecdsa_key ]; then
echo "Generating /data/ssh/ssh_host_ecdsa_key..."
ssh-keygen -t ecdsa -b 256 -f /data/ssh/ssh_host_ecdsa_key -N "" > /dev/null
@@ -36,19 +31,15 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
fi
-if [ -e /data/ssh/ssh_host_dsa_cert ]; then
- SSH_DSA_CERT=${SSH_DSA_CERT:-"/data/ssh/ssh_host_dsa_cert"}
-fi
-
if [ -d /etc/ssh ]; then
SSH_PORT=${SSH_PORT:-"22"} \
SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \
SSH_ED25519_CERT="${SSH_ED25519_CERT:+"HostCertificate "}${SSH_ED25519_CERT}" \
SSH_RSA_CERT="${SSH_RSA_CERT:+"HostCertificate "}${SSH_RSA_CERT}" \
SSH_ECDSA_CERT="${SSH_ECDSA_CERT:+"HostCertificate "}${SSH_ECDSA_CERT}" \
- SSH_DSA_CERT="${SSH_DSA_CERT:+"HostCertificate "}${SSH_DSA_CERT}" \
SSH_MAX_STARTUPS="${SSH_MAX_STARTUPS:+"MaxStartups "}${SSH_MAX_STARTUPS}" \
SSH_MAX_SESSIONS="${SSH_MAX_SESSIONS:+"MaxSessions "}${SSH_MAX_SESSIONS}" \
+ SSH_INCLUDE_FILE="${SSH_INCLUDE_FILE:+"Include "}${SSH_INCLUDE_FILE}" \
SSH_LOG_LEVEL=${SSH_LOG_LEVEL:-"INFO"} \
envsubst < /etc/templates/sshd_config > /etc/ssh/sshd_config
diff --git a/docker/root/etc/templates/sshd_config b/docker/root/etc/templates/sshd_config
index 6f1a3630459ad..033c434658676 100644
--- a/docker/root/etc/templates/sshd_config
+++ b/docker/root/etc/templates/sshd_config
@@ -16,8 +16,6 @@ HostKey /data/ssh/ssh_host_rsa_key
${SSH_RSA_CERT}
HostKey /data/ssh/ssh_host_ecdsa_key
${SSH_ECDSA_CERT}
-HostKey /data/ssh/ssh_host_dsa_key
-${SSH_DSA_CERT}
AuthorizedKeysFile .ssh/authorized_keys
AuthorizedPrincipalsFile .ssh/authorized_principals
@@ -41,3 +39,5 @@ Banner none
Subsystem sftp /usr/lib/ssh/sftp-server
AcceptEnv GIT_PROTOCOL
+
+${SSH_INCLUDE_FILE}
diff --git a/docker/root/usr/local/bin/gitea b/docker/root/usr/local/bin/gitea
index 8a8f17bc4ecd8..24d3f91eb15de 100644
--- a/docker/root/usr/local/bin/gitea
+++ b/docker/root/usr/local/bin/gitea
@@ -13,5 +13,3 @@ CUSTOM_PATH="/data/gitea"
# Provide docker defaults
GITEA_WORK_DIR="${GITEA_WORK_DIR:-$WORK_DIR}" GITEA_CUSTOM="${GITEA_CUSTOM:-$CUSTOM_PATH}" exec -a "$0" "$GITEA" $CONF_ARG "$@"
-
-
diff --git a/docker/rootless/usr/local/bin/docker-setup.sh b/docker/rootless/usr/local/bin/docker-setup.sh
index 47645726c4b08..feab02a3793c1 100755
--- a/docker/rootless/usr/local/bin/docker-setup.sh
+++ b/docker/rootless/usr/local/bin/docker-setup.sh
@@ -5,7 +5,7 @@ mkdir -p ${HOME} && chmod 0700 ${HOME}
if [ ! -w ${HOME} ]; then echo "${HOME} is not writable"; exit 1; fi
# Prepare custom folder
-mkdir -p ${GITEA_CUSTOM} && chmod 0500 ${GITEA_CUSTOM}
+mkdir -p ${GITEA_CUSTOM} && chmod 0700 ${GITEA_CUSTOM}
# Prepare temp folder
mkdir -p ${GITEA_TEMP} && chmod 0700 ${GITEA_TEMP}
diff --git a/docs/.gitignore b/docs/.gitignore
index 9cd1408bd249f..271adbb1da107 100644
--- a/docs/.gitignore
+++ b/docs/.gitignore
@@ -2,3 +2,6 @@ public/
templates/swagger/v1_json.tmpl
themes/
resources/
+
+# Temporary lock file while building
+/.hugo_build.lock
diff --git a/docs/Makefile b/docs/Makefile
index 68afe03e75fe0..f47ad4de3ca68 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -2,6 +2,8 @@ THEME := themes/gitea
PUBLIC := public
ARCHIVE := https://dl.gitea.io/theme/master.tar.gz
+HUGO_PACKAGE := github.com/gohugoio/hugo@v0.82.0
+
.PHONY: all
all: build
@@ -11,19 +13,19 @@ clean:
.PHONY: trans-copy
trans-copy:
- @bash scripts/trans-copy
+ bash scripts/trans-copy.sh
.PHONY: server
server: $(THEME)
- hugo server
+ go run $(HUGO_PACKAGE) server
.PHONY: build
build: $(THEME)
- hugo --cleanDestinationDir
+ go run $(HUGO_PACKAGE) --cleanDestinationDir
.PHONY: build-offline
build-offline: $(THEME)
- hugo --baseURL="/" --cleanDestinationDir
+ go run $(HUGO_PACKAGE) --baseURL="/" --cleanDestinationDir
.PHONY: update
update: $(THEME)
diff --git a/docs/config.yaml b/docs/config.yaml
index e2180daa272f6..0b47c0ffd83f5 100644
--- a/docs/config.yaml
+++ b/docs/config.yaml
@@ -18,10 +18,13 @@ params:
description: Git with a cup of tea
author: The Gitea Authors
website: https://docs.gitea.io
- version: 1.16.5
- minGoVersion: 1.17
- goVersion: 1.18
- minNodeVersion: 12.17
+ version: 1.18.1
+ minGoVersion: 1.18
+ goVersion: 1.19
+ minNodeVersion: 16
+ search: nav
+ repo: "https://github.com/go-gitea/gitea"
+ docContentPath: "docs/content"
outputs:
home:
diff --git a/docs/content/doc/advanced/clone-filter.en-us.md b/docs/content/doc/advanced/clone-filter.en-us.md
index ba2fdf104cc89..58675d2e941df 100644
--- a/docs/content/doc/advanced/clone-filter.en-us.md
+++ b/docs/content/doc/advanced/clone-filter.en-us.md
@@ -30,7 +30,6 @@ see Git version of the server.
By default, clone filters are enabled, unless `DISABLE_PARTIAL_CLONE` under
`[git]` is set to `true`.
-
See [GitHub blog post: Get up to speed with partial clone](https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/)
for common use cases of clone filters (blobless and treeless clones), and
[GitLab docs for partial clone](https://docs.gitlab.com/ee/topics/git/partial_clone.html)
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 9d70269bf2fd4..295fa713a4e69 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -27,23 +27,56 @@ accurately recorded in [app.example.ini](https://github.com/go-gitea/gitea/blob/
(s/main/\). Any string in the format `%(X)s` is a feature powered
by [ini](https://github.com/go-ini/ini/#recursive-values), for reading values recursively.
+In the default values below, a value in the form `$XYZ` refers to an environment variable. (However, see `environment-to-ini`.) Values in the form _`XxYyZz`_ refer to values listed as part of the default configuration. These notation forms will not work in your own `app.ini` file and are only listed here as documentation.
+
Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
**Note:** A full restart is required for Gitea configuration changes to take effect.
{{< toc >}}
+## Default Configuration (non-`app.ini` configuration)
+
+These values are environment-dependent but form the basis of a lot of values. They will be
+reported as part of the default configuration when running `gitea --help` or on start-up. The order they are emitted there is slightly different but we will list them here in the order they are set-up.
+
+- _`AppPath`_: This is the absolute path of the running gitea binary.
+- _`AppWorkPath`_: This refers to "working path" of the `gitea` binary. It is determined by using the first set thing in the following hierarchy:
+ - The `--work-path` flag passed to the binary
+ - The environment variable `$GITEA_WORK_DIR`
+ - A built-in value set at build time (see building from source)
+ - Otherwise it defaults to the directory of the _`AppPath`_
+ - If any of the above are relative paths then they are made absolute against the
+the directory of the _`AppPath`_
+- _`CustomPath`_: This is the base directory for custom templates and other options.
+It is determined by using the first set thing in the following hierarchy:
+ - The `--custom-path` flag passed to the binary
+ - The environment variable `$GITEA_CUSTOM`
+ - A built-in value set at build time (see building from source)
+ - Otherwise it defaults to _`AppWorkPath`_`/custom`
+ - If any of the above are relative paths then they are made absolute against the
+the directory of the _`AppWorkPath`_
+- _`CustomConf`_: This is the path to the `app.ini` file.
+ - The `--config` flag passed to the binary
+ - A built-in value set at build time (see building from source)
+ - Otherwise it defaults to _`CustomPath`_`/conf/app.ini`
+ - If any of the above are relative paths then they are made absolute against the
+the directory of the _`CustomPath`_
+
+In addition there is _`StaticRootPath`_ which can be set as a built-in at build time, but will otherwise default to _`AppWorkPath`_
+
## Overall (`DEFAULT`)
- `APP_NAME`: **Gitea: Git with a cup of tea**: Application name, used in the page title.
-- `RUN_USER`: **git**: The user Gitea will run as. This should be a dedicated system
- (non-user) account. Setting this incorrectly will cause Gitea to not start.
+- `RUN_USER`: **_current OS username_/`$USER`/`$USERNAME` e.g. git**: The user Gitea will run as.
+ This should be a dedicated system (non-user) account. Setting this incorrectly will cause Gitea
+ to not start.
- `RUN_MODE`: **prod**: Application run mode, affects performance and debugging. Either "dev", "prod" or "test".
## Repository (`repository`)
-- `ROOT`: **data/gitea-repositories/**: Root path for storing all repository data. It must be
- an absolute path. By default it is stored in a sub-directory of `APP_DATA_PATH`.
+- `ROOT`: **%(APP_DATA_PATH)s/gitea-repositories**: Root path for storing all repository data.
+ A relative path is interpreted as **_`AppWorkPath`_/%(ROOT)s**.
- `SCRIPT_TYPE`: **bash**: The script type this server supports. Usually this is `bash`,
but some users report that only `sh` is available.
- `DETECTED_CHARSETS_ORDER`: **UTF-8, UTF-16BE, UTF-16LE, UTF-32BE, UTF-32LE, ISO-8859, windows-1252, ISO-8859, windows-1250, ISO-8859, ISO-8859, ISO-8859, windows-1253, ISO-8859, windows-1255, ISO-8859, windows-1251, windows-1256, KOI8-R, ISO-8859, windows-1254, Shift_JIS, GB18030, EUC-JP, EUC-KR, Big5, ISO-2022, ISO-2022, ISO-2022, IBM424_rtl, IBM424_ltr, IBM420_rtl, IBM420_ltr**: Tie-break order of detected charsets - if the detected charsets have equal confidence, charsets earlier in the list will be chosen in preference to those later. Adding `defaults` will place the unnamed charsets at that point.
@@ -78,6 +111,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `DEFAULT_BRANCH`: **main**: Default branch name of all repositories.
- `ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES`: **false**: Allow non-admin users to adopt unadopted repositories
- `ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES`: **false**: Allow non-admin users to delete unadopted repositories
+- `DISABLE_DOWNLOAD_SOURCE_ARCHIVES`: **false**: Don't allow download source archive files from UI
+- `ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT`: **true**: Allow fork repositories without maximum number limit
### Repository - Editor (`repository.editor`)
@@ -87,11 +122,12 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
### Repository - Pull Request (`repository.pull-request`)
- `WORK_IN_PROGRESS_PREFIXES`: **WIP:,\[WIP\]**: List of prefixes used in Pull Request
- title to mark them as Work In Progress
+ title to mark them as Work In Progress. These are matched in a case-insensitive manner.
- `CLOSE_KEYWORDS`: **close**, **closes**, **closed**, **fix**, **fixes**, **fixed**, **resolve**, **resolves**, **resolved**: List of
keywords used in Pull Request comments to automatically close a related issue
- `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: List of keywords used in Pull Request comments to automatically reopen
a related issue
+- `DEFAULT_MERGE_STYLE`: **merge**: Set default merge style for repository creating, valid options: `merge`, `rebase`, `rebase-merge`, `squash`
- `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: In the default merge message for squash commits include at most this many commits. Set to `-1` to include all commits
- `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: In the default merge message for squash commits limit the size of the commit messages. Set to `-1` to have no limit. Only used if `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES` is `true`.
- `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: In the default merge message for squash commits walk all commits to include all authors in the Co-authored-by otherwise just use those in the limited list
@@ -99,6 +135,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY`: **true**: In default merge messages only include approvers who are officially allowed to review.
- `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`: **false**: In default squash-merge messages include the commit message of all commits comprising the pull request.
- `ADD_CO_COMMITTER_TRAILERS`: **true**: Add co-authored-by and co-committed-by trailers to merge commit messages if committer does not match author.
+- `TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY`: **false**: PR patches are tested using a three-way merge method to discover if there are conflicts. If this setting is set to **true**, conflicting patches will be retested using `git apply` - This was the previous behaviour in 1.18 (and earlier) but is somewhat inefficient. Please report if you find that this setting is required.
### Repository - Issue (`repository.issue`)
@@ -129,9 +166,9 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `always`: Always sign
- Options other than `never` and `always` can be combined as a comma separated list.
- `DEFAULT_TRUST_MODEL`: **collaborator**: \[collaborator, committer, collaboratorcommitter\]: The default trust model used for verifying commits.
- - `collaborator`: Trust signatures signed by keys of collaborators.
- - `committer`: Trust signatures that match committers (This matches GitHub and will force Gitea signed commits to have Gitea as the committer).
- - `collaboratorcommitter`: Trust signatures signed by keys of collaborators which match the committer.
+ - `collaborator`: Trust signatures signed by keys of collaborators.
+ - `committer`: Trust signatures that match committers (This matches GitHub and will force Gitea signed commits to have Gitea as the committer).
+ - `collaboratorcommitter`: Trust signatures signed by keys of collaborators which match the committer.
- `WIKI`: **never**: \[never, pubkey, twofa, always, parentsigned\]: Sign commits to wiki.
- `CRUD_ACTIONS`: **pubkey, twofa, parentsigned**: \[never, pubkey, twofa, parentsigned, always\]: Sign CRUD actions.
- Options as above, with the addition of:
@@ -151,6 +188,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
Configuration for set the expected MIME type based on file extensions of downloadable files. Configuration presents in key-value pairs and file extensions starts with leading `.`.
The following configuration set `Content-Type: application/vnd.android.package-archive` header when downloading files with `.apk` file extension.
+
```ini
.apk=application/vnd.android.package-archive
```
@@ -164,15 +202,17 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `METHODS`: **GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS**: list of methods allowed to request
- `MAX_AGE`: **10m**: max time to cache response
- `ALLOW_CREDENTIALS`: **false**: allow request with credentials
+- `HEADERS`: **Content-Type,User-Agent**: additional headers that are permitted in requests
- `X_FRAME_OPTIONS`: **SAMEORIGIN**: Set the `X-Frame-Options` header value.
## UI (`ui`)
- `EXPLORE_PAGING_NUM`: **20**: Number of repositories that are shown in one explore page.
-- `ISSUE_PAGING_NUM`: **10**: Number of issues that are shown in one page (for all pages that list issues).
+- `ISSUE_PAGING_NUM`: **20**: Number of issues that are shown in one page (for all pages that list issues, milestones, projects).
- `MEMBERS_PAGING_NUM`: **20**: Number of members that are shown in organization members.
- `FEED_MAX_COMMIT_NUM`: **5**: Number of maximum commits shown in one activity feed.
- `FEED_PAGING_NUM`: **20**: Number of items that are displayed in home feed.
+- `SITEMAP_PAGING_NUM`: **20**: Number of items that are displayed in a single subsitemap.
- `GRAPH_MAX_COMMIT_NUM`: **100**: Number of maximum commits shown in the commit graph.
- `CODE_COMMENT_LINES`: **4**: Number of line of codes shown for a code comment.
- `DEFAULT_THEME`: **auto**: \[auto, gitea, arc-green\]: Set the default theme for the Gitea install.
@@ -190,6 +230,8 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `DEFAULT_SHOW_FULL_NAME`: **false**: Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
- `SEARCH_REPO_DESCRIPTION`: **true**: Whether to search within description at repository search on explore page.
- `USE_SERVICE_WORKER`: **false**: Whether to enable a Service Worker to cache frontend assets.
+- `ONLY_SHOW_RELEVANT_REPOS`: **false** Whether to only show relevant repos on the explore page when no keyword is specified and default sorting is used.
+ A repo is considered irrelevant if it's a fork or if it has no metadata (no description, no icon, no topic).
### UI - Admin (`ui.admin`)
@@ -198,6 +240,10 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `NOTICE_PAGING_NUM`: **25**: Number of notices that are shown in one page.
- `ORG_PAGING_NUM`: **50**: Number of organizations that are shown in one page.
+### UI - User (`ui.user`)
+
+- `REPO_PAGING_NUM`: **15**: Number of repos that are shown in one page.
+
### UI - Metadata (`ui.meta`)
- `AUTHOR`: **Gitea - Git with a cup of tea**: Author meta tag of the homepage.
@@ -206,7 +252,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
### UI - Notification (`ui.notification`)
-- `MIN_TIMEOUT`: **10s**: These options control how often notification endpoint is polled to update the notification count. On page load the notification count will be checked after `MIN_TIMEOUT`. The timeout will increase to `MAX_TIMEOUT` by `TIMEOUT_STEP` if the notification count is unchanged. Set MIN_TIMEOUT to 0 to turn off.
+- `MIN_TIMEOUT`: **10s**: These options control how often notification endpoint is polled to update the notification count. On page load the notification count will be checked after `MIN_TIMEOUT`. The timeout will increase to `MAX_TIMEOUT` by `TIMEOUT_STEP` if the notification count is unchanged. Set MIN_TIMEOUT to -1 to turn off.
- `MAX_TIMEOUT`: **60s**.
- `TIMEOUT_STEP`: **10s**.
- `EVENT_SOURCE_UPDATE_TIME`: **10s**: This setting determines how often the database is queried to update notification counts. If the browser client supports `EventSource` and `SharedWorker`, a `SharedWorker` will be used in preference to polling notification endpoint. Set to **-1** to disable the `EventSource`.
@@ -230,10 +276,16 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional
URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are
always displayed
+- `ENABLE_MATH`: **true**: Enables detection of `\(...\)`, `\[...\]`, `$...$` and `$$...$$` blocks as math blocks.
## Server (`server`)
+- `APP_DATA_PATH`: **_`AppWorkPath`_/data**: This is the default root path for storing data.
- `PROTOCOL`: **http**: \[http, https, fcgi, http+unix, fcgi+unix\]
+- `USE_PROXY_PROTOCOL`: **false**: Expect PROXY protocol headers on connections
+- `PROXY_PROTOCOL_TLS_BRIDGING`: **false**: When protocol is https, expect PROXY protocol headers after TLS negotiation.
+- `PROXY_PROTOCOL_HEADER_TIMEOUT`: **5s**: Timeout to wait for PROXY protocol header (set to 0 to have no timeout)
+- `PROXY_PROTOCOL_ACCEPT_UNKNOWN`: **false**: Accept PROXY protocol headers with Unknown type.
- `DOMAIN`: **localhost**: Domain name of this server.
- `ROOT_URL`: **%(PROTOCOL)s://%(DOMAIN)s:%(HTTP\_PORT)s/**:
Overwrite the automatically generated public URL.
@@ -243,14 +295,19 @@ The following configuration set `Content-Type: application/vnd.android.package-a
This includes CSS files, images, JS files and web fonts.
Avatar images are dynamic resources and still served by Gitea.
The option can be just a different path, as in `/static`, or another domain, as in `https://cdn.example.com`.
- Requests are then made as `%(ROOT_URL)s/static/css/index.css` and `https://cdn.example.com/css/index.css` respective.
+ Requests are then made as `%(ROOT_URL)s/static/assets/css/index.css` or `https://cdn.example.com/assets/css/index.css` respectively.
The static files are located in the `public/` directory of the Gitea source repository.
+ You can proxy the STATIC_URL_PREFIX requests to Gitea server to serve the static
+ assets, or copy the manually built Gitea assets from `$GITEA_BUILD/public` to
+ the assets location, eg: `/var/www/assets`, make sure `$STATIC_URL_PREFIX/assets/css/index.css`
+ points to `/var/www/assets/css/index.css`.
+
- `HTTP_ADDR`: **0.0.0.0**: HTTP listen address.
- - If `PROTOCOL` is set to `fcgi`, Gitea will listen for FastCGI requests on TCP socket
+ - If `PROTOCOL` is set to `fcgi`, Gitea will listen for FastCGI requests on TCP socket
defined by `HTTP_ADDR` and `HTTP_PORT` configuration settings.
- - If `PROTOCOL` is set to `http+unix` or `fcgi+unix`, this should be the name of the Unix socket file to use. Relative paths will be made absolute against the AppWorkPath.
+ - If `PROTOCOL` is set to `http+unix` or `fcgi+unix`, this should be the name of the Unix socket file to use. Relative paths will be made absolute against the _`AppWorkPath`_.
- `HTTP_PORT`: **3000**: HTTP listen port.
- - If `PROTOCOL` is set to `fcgi`, Gitea will listen for FastCGI requests on TCP socket
+ - If `PROTOCOL` is set to `fcgi`, Gitea will listen for FastCGI requests on TCP socket
defined by `HTTP_ADDR` and `HTTP_PORT` configuration settings.
- `UNIX_SOCKET_PERMISSION`: **666**: Permissions for the Unix socket.
- `LOCAL_ROOT_URL`: **%(PROTOCOL)s://%(HTTP_ADDR)s:%(HTTP_PORT)s/**: Local
@@ -258,14 +315,17 @@ The following configuration set `Content-Type: application/vnd.android.package-a
most cases you do not need to change the default value. Alter it only if
your SSH server node is not the same as HTTP node. Do not set this variable
if `PROTOCOL` is set to `http+unix`.
-- `PER_WRITE_TIMEOUT`: **30s**: Timeout for any write to the connection. (Set to 0 to
+- `LOCAL_USE_PROXY_PROTOCOL`: **%(USE_PROXY_PROTOCOL)s**: When making local connections pass the PROXY protocol header.
+ This should be set to false if the local connection will go through the proxy.
+- `PER_WRITE_TIMEOUT`: **30s**: Timeout for any write to the connection. (Set to -1 to
disable all timeouts.)
- `PER_WRITE_PER_KB_TIMEOUT`: **10s**: Timeout per Kb written to connections.
- `DISABLE_SSH`: **false**: Disable SSH feature when it's not available.
- `START_SSH_SERVER`: **false**: When enabled, use the built-in SSH server.
+- `SSH_SERVER_USE_PROXY_PROTOCOL`: **false**: Expect PROXY protocol header on connections to the built-in SSH Server.
- `BUILTIN_SSH_SERVER_USER`: **%(RUN_USER)s**: Username to use for the built-in SSH Server.
-- `SSH_USER`: **%(BUILTIN_SSH_SERVER_USER)**: SSH username displayed in clone URLs. This is only for people who configure the SSH server themselves; in most cases, you want to leave this blank and modify the `BUILTIN_SSH_SERVER_USER`.
+- `SSH_USER`: **%(BUILTIN_SSH_SERVER_USER)s**: SSH username displayed in clone URLs. This is only for people who configure the SSH server themselves; in most cases, you want to leave this blank and modify the `BUILTIN_SSH_SERVER_USER`.
- `SSH_DOMAIN`: **%(DOMAIN)s**: Domain name of this server, used for displayed clone URL.
- `SSH_PORT`: **22**: SSH port displayed in clone URL.
- `SSH_LISTEN_HOST`: **0.0.0.0**: Listen address for the built-in SSH server.
@@ -287,35 +347,36 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `SSH_KEYGEN_PATH`: **ssh-keygen**: Path to ssh-keygen, default is 'ssh-keygen' which means the shell is responsible for finding out which one to call.
- `SSH_EXPOSE_ANONYMOUS`: **false**: Enable exposure of SSH clone URL to anonymous visitors, default is false.
- `SSH_PER_WRITE_TIMEOUT`: **30s**: Timeout for any write to the SSH connections. (Set to
- 0 to disable all timeouts.)
+ -1 to disable all timeouts.)
- `SSH_PER_WRITE_PER_KB_TIMEOUT`: **10s**: Timeout per Kb written to SSH connections.
- `MINIMUM_KEY_SIZE_CHECK`: **true**: Indicate whether to check minimum key size with corresponding type.
- `OFFLINE_MODE`: **false**: Disables use of CDN for static files and Gravatar for profile pictures.
-- `CERT_FILE`: **https/cert.pem**: Cert file path used for HTTPS. When chaining, the server certificate must come first, then intermediate CA certificates (if any). This is ignored if `ENABLE_ACME=true`. From 1.11 paths are relative to `CUSTOM_PATH`.
-- `KEY_FILE`: **https/key.pem**: Key file path used for HTTPS. This is ignored if `ENABLE_ACME=true`. From 1.11 paths are relative to `CUSTOM_PATH`.
-- `STATIC_ROOT_PATH`: **./**: Upper level of template and static files path.
-- `APP_DATA_PATH`: **data** (**/data/gitea** on docker): Default path for application data.
+- `CERT_FILE`: **https/cert.pem**: Cert file path used for HTTPS. When chaining, the server certificate must come first, then intermediate CA certificates (if any). This is ignored if `ENABLE_ACME=true`. Paths are relative to `CUSTOM_PATH`.
+- `KEY_FILE`: **https/key.pem**: Key file path used for HTTPS. This is ignored if `ENABLE_ACME=true`. Paths are relative to `CUSTOM_PATH`.
+- `STATIC_ROOT_PATH`: **_`StaticRootPath`_**: Upper level of template and static files path.
+- `APP_DATA_PATH`: **data** (**/data/gitea** on docker): Default path for application data. Relative paths will be made absolute against _`AppWorkPath`_.
- `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars. Note that this cache is disabled when `RUN_MODE` is "dev".
- `ENABLE_GZIP`: **false**: Enable gzip compression for runtime-generated content, static resources excluded.
-- `ENABLE_PPROF`: **false**: Application profiling (memory and cpu). For "web" command it listens on localhost:6060. For "serv" command it dumps to disk at `PPROF_DATA_PATH` as `(cpuprofile|memprofile)__`
-- `PPROF_DATA_PATH`: **data/tmp/pprof**: `PPROF_DATA_PATH`, use an absolute path when you start Gitea as service
+- `ENABLE_PPROF`: **false**: Application profiling (memory and cpu). For "web" command it listens on `localhost:6060`. For "serv" command it dumps to disk at `PPROF_DATA_PATH` as `(cpuprofile|memprofile)__`
+- `PPROF_DATA_PATH`: **_`AppWorkPath`_/data/tmp/pprof**: `PPROF_DATA_PATH`, use an absolute path when you start Gitea as service
- `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore, organizations, login, **custom**\]. Where custom would instead be any URL such as "/org/repo" or even `https://anotherwebsite.com`
- `LFS_START_SERVER`: **false**: Enables Git LFS support.
-- `LFS_CONTENT_PATH`: **%(APP_DATA_PATH)/lfs**: Default LFS content path. (if it is on local storage.) **DEPRECATED** use settings in `[lfs]`.
+- `LFS_CONTENT_PATH`: **%(APP_DATA_PATH)s/lfs**: Default LFS content path. (if it is on local storage.) **DEPRECATED** use settings in `[lfs]`.
- `LFS_JWT_SECRET`: **\**: LFS authentication secret, change this a unique string.
- `LFS_HTTP_AUTH_EXPIRY`: **20m**: LFS authentication validity period in time.Duration, pushes taking longer than this may fail.
- `LFS_MAX_FILE_SIZE`: **0**: Maximum allowed LFS file size in bytes (Set to 0 for no limit).
- `LFS_LOCKS_PAGING_NUM`: **50**: Maximum number of LFS Locks returned per page.
- `REDIRECT_OTHER_PORT`: **false**: If true and `PROTOCOL` is https, allows redirecting http requests on `PORT_TO_REDIRECT` to the https port Gitea listens on.
+- `REDIRECTOR_USE_PROXY_PROTOCOL`: **%(USE_PROXY_PROTOCOL)s**: expect PROXY protocol header on connections to https redirector.
- `PORT_TO_REDIRECT`: **80**: Port for the http redirection service to listen on. Used when `REDIRECT_OTHER_PORT` is true.
- `SSL_MIN_VERSION`: **TLSv1.2**: Set the minimum version of ssl support.
- `SSL_MAX_VERSION`: **\**: Set the maximum version of ssl support.
- `SSL_CURVE_PREFERENCES`: **X25519,P256**: Set the preferred curves,
- `SSL_CIPHER_SUITES`: **ecdhe_ecdsa_with_aes_256_gcm_sha384,ecdhe_rsa_with_aes_256_gcm_sha384,ecdhe_ecdsa_with_aes_128_gcm_sha256,ecdhe_rsa_with_aes_128_gcm_sha256,ecdhe_ecdsa_with_chacha20_poly1305,ecdhe_rsa_with_chacha20_poly1305**: Set the preferred cipher suites.
- - If there is not hardware support for AES suites by default the cha cha suites will be preferred over the AES suites
- - supported suites as of go 1.17 are:
+ - If there is no hardware support for AES suites, by default the ChaCha suites will be preferred over the AES suites.
+ - supported suites as of Go 1.18 are:
- TLS 1.0 - 1.2 cipher suites
- "rsa_with_rc4_128_sha"
- "rsa_with_3des_ede_cbc_sha"
@@ -368,17 +429,18 @@ The following configuration set `Content-Type: application/vnd.android.package-a
(e.g. `ALTER USER user SET SEARCH_PATH = schema_name,"$user",public;`).
- `SSL_MODE`: **disable**: SSL/TLS encryption mode for connecting to the database. This option is only applied for PostgreSQL and MySQL.
- Valid values for MySQL:
- - `true`: Enable TLS with verification of the database server certificate against its root certificate. When selecting this option make sure that the root certificate required to validate the database server certificate (e.g. the CA certificate) is on the system certificate store of both the database and Gitea servers. See your system documentation for instructions on how to add a CA certificate to the certificate store.
- - `false`: Disable TLS.
- - `disable`: Alias for `false`, for compatibility with PostgreSQL.
- - `skip-verify`: Enable TLS without database server certificate verification. Use this option if you have self-signed or invalid certificate on the database server.
- - `prefer`: Enable TLS with fallback to non-TLS connection.
+ - `true`: Enable TLS with verification of the database server certificate against its root certificate. When selecting this option make sure that the root certificate required to validate the database server certificate (e.g. the CA certificate) is on the system certificate store of both the database and Gitea servers. See your system documentation for instructions on how to add a CA certificate to the certificate store.
+ - `false`: Disable TLS.
+ - `disable`: Alias for `false`, for compatibility with PostgreSQL.
+ - `skip-verify`: Enable TLS without database server certificate verification. Use this option if you have self-signed or invalid certificate on the database server.
+ - `prefer`: Enable TLS with fallback to non-TLS connection.
- Valid values for PostgreSQL:
- - `disable`: Disable TLS.
- - `require`: Enable TLS without any verifications.
- - `verify-ca`: Enable TLS with verification of the database server certificate against its root certificate.
- - `verify-full`: Enable TLS and verify the database server name matches the given certificate in either the `Common Name` or `Subject Alternative Name` fields.
+ - `disable`: Disable TLS.
+ - `require`: Enable TLS without any verifications.
+ - `verify-ca`: Enable TLS with verification of the database server certificate against its root certificate.
+ - `verify-full`: Enable TLS and verify the database server name matches the given certificate in either the `Common Name` or `Subject Alternative Name` fields.
- `SQLITE_TIMEOUT`: **500**: Query timeout for SQLite3 only.
+- `SQLITE_JOURNAL_MODE`: **""**: Change journal mode for SQlite3. Can be used to enable [WAL mode](https://www.sqlite.org/wal.html) when high load causes write congestion. See [SQlite3 docs](https://www.sqlite.org/pragma.html#pragma_journal_mode) for possible values. Defaults to the default for the database file, often DELETE.
- `ITERATE_BUFFER_SIZE`: **50**: Internal buffer size for iterating.
- `CHARSET`: **utf8mb4**: For MySQL only, either "utf8" or "utf8mb4". NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this.
- `PATH`: **data/gitea.db**: For SQLite3 only, the database file path.
@@ -388,6 +450,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `MAX_OPEN_CONNS` **0**: Database maximum open connections - default is 0, meaning there is no limit.
- `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`.
- `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071).
+- `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically.
Please see #8540 & #8273 for further discussion of the appropriate values for `MAX_OPEN_CONNS`, `MAX_IDLE_CONNS` & `CONN_MAX_LIFETIME` and their
relation to port exhaustion.
@@ -397,10 +460,10 @@ relation to port exhaustion.
- `ISSUE_INDEXER_TYPE`: **bleve**: Issue indexer type, currently supported: `bleve`, `db` or `elasticsearch`.
- `ISSUE_INDEXER_CONN_STR`: ****: Issue indexer connection string, available when ISSUE_INDEXER_TYPE is elasticsearch. i.e. http://elastic:changeme@localhost:9200
- `ISSUE_INDEXER_NAME`: **gitea_issues**: Issue indexer name, available when ISSUE_INDEXER_TYPE is elasticsearch
-- `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search; available when ISSUE_INDEXER_TYPE is bleve and elasticsearch.
+- `ISSUE_INDEXER_PATH`: **indexers/issues.bleve**: Index file used for issue search; available when ISSUE_INDEXER_TYPE is bleve and elasticsearch. Relative paths will be made absolute against _`AppWorkPath`_.
- The next 4 configuration values are deprecated and should be set in `queue.issue_indexer` however are kept for backwards compatibility:
- `ISSUE_INDEXER_QUEUE_TYPE`: **levelqueue**: Issue indexer queue, currently supports:`channel`, `levelqueue`, `redis`. **DEPRECATED** use settings in `[queue.issue_indexer]`.
-- `ISSUE_INDEXER_QUEUE_DIR`: **queues/common**: When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this will be the path where the queue will be saved. **DEPRECATED** use settings in `[queue.issue_indexer]`.
+- `ISSUE_INDEXER_QUEUE_DIR`: **queues/common**: When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this will be the path where the queue will be saved. **DEPRECATED** use settings in `[queue.issue_indexer]`. Relative paths will be made absolute against `%(APP_DATA_PATH)s`.
- `ISSUE_INDEXER_QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: When `ISSUE_INDEXER_QUEUE_TYPE` is `redis`, this will store the redis connection string. When `ISSUE_INDEXER_QUEUE_TYPE` is `levelqueue`, this is a directory or additional options of the form `leveldb://path/to/db?option=value&....`, and overrides `ISSUE_INDEXER_QUEUE_DIR`. **DEPRECATED** use settings in `[queue.issue_indexer]`.
- `ISSUE_INDEXER_QUEUE_BATCH_NUMBER`: **20**: Batch queue number. **DEPRECATED** use settings in `[queue.issue_indexer]`.
@@ -415,14 +478,14 @@ relation to port exhaustion.
- `REPO_INDEXER_EXCLUDE_VENDORED`: **true**: Exclude vendored files from index.
- `UPDATE_BUFFER_LEN`: **20**: Buffer length of index request. **DEPRECATED** use settings in `[queue.issue_indexer]`.
- `MAX_FILE_SIZE`: **1048576**: Maximum size in bytes of files to be indexed.
-- `STARTUP_TIMEOUT`: **30s**: If the indexer takes longer than this timeout to start - fail. (This timeout will be added to the hammer time above for child processes - as bleve will not start until the previous parent is shutdown.) Set to zero to never timeout.
+- `STARTUP_TIMEOUT`: **30s**: If the indexer takes longer than this timeout to start - fail. (This timeout will be added to the hammer time above for child processes - as bleve will not start until the previous parent is shutdown.) Set to -1 to never timeout.
## Queue (`queue` and `queue.*`)
Configuration at `[queue]` will set defaults for queues with overrides for individual queues at `[queue.*]`. (However see below.)
- `TYPE`: **persistable-channel**: General queue type, currently support: `persistable-channel` (uses a LevelDB internally), `channel`, `level`, `redis`, `dummy`
-- `DATADIR`: **queues/**: Base DataDir for storing persistent and level queues. `DATADIR` for individual queues can be set in `queue.name` sections but will default to `DATADIR/`**`common`**. (Previously each queue would default to `DATADIR/`**`name`**.)
+- `DATADIR`: **queues/**: Base DataDir for storing persistent and level queues. `DATADIR` for individual queues can be set in `queue.name` sections but will default to `DATADIR/`**`common`**. (Previously each queue would default to `DATADIR/`**`name`**.) Relative paths will be made absolute against `%(APP_DATA_PATH)s`.
- `LENGTH`: **20**: Maximal queue size before channel queues block
- `BATCH_LENGTH`: **20**: Batch data before passing to the handler
- `CONN_STR`: **redis://127.0.0.1:6379/0**: Connection string for the redis queue type. Options can be set using query params. Similarly LevelDB options can also be set using: **leveldb://relative/path?option=value** or **leveldb:///absolute/path?option=value**, and will override `DATADIR`
@@ -433,11 +496,11 @@ Configuration at `[queue]` will set defaults for queues with overrides for indiv
- `MAX_ATTEMPTS`: **10**: Maximum number of attempts to create the wrapped queue
- `TIMEOUT`: **GRACEFUL_HAMMER_TIME + 30s**: Timeout the creation of the wrapped queue if it takes longer than this to create.
- Queues by default come with a dynamically scaling worker pool. The following settings configure this:
-- `WORKERS`: **0** (v1.14 and before: **1**): Number of initial workers for the queue.
+- `WORKERS`: **0**: Number of initial workers for the queue.
- `MAX_WORKERS`: **10**: Maximum number of worker go-routines for the queue.
- `BLOCK_TIMEOUT`: **1s**: If the queue blocks for this time, boost the number of workers - the `BLOCK_TIMEOUT` will then be doubled before boosting again whilst the boost is ongoing.
- `BOOST_TIMEOUT`: **5m**: Boost workers will timeout after this long.
-- `BOOST_WORKERS`: **1** (v1.14 and before: **5**): This many workers will be added to the worker pool if there is a boost.
+- `BOOST_WORKERS`: **1**: This many workers will be added to the worker pool if there is a boost.
Gitea creates the following non-unique queues:
@@ -478,7 +541,8 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
## Security (`security`)
- `INSTALL_LOCK`: **false**: Controls access to the installation page. When set to "true", the installation page is not accessible.
-- `SECRET_KEY`: **\**: Global secret key. This should be changed.
+- `SECRET_KEY`: **\**: Global secret key. This key is VERY IMPORTANT, if you lost it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
+- `SECRET_KEY_URI`: ****: Instead of defining SECRET_KEY, this option can be used to use the key stored in a file (example value: `file:/etc/gitea/secret_key`). It shouldn't be lost like SECRET_KEY.
- `LOGIN_REMEMBER_DAYS`: **7**: Cookie lifetime, in days.
- `COOKIE_USERNAME`: **gitea\_awesome**: Name of the cookie used to store the current username.
- `COOKIE_REMEMBER_NAME`: **gitea\_incredible**: Name of cookie used to store authentication
@@ -487,6 +551,8 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
authentication.
- `REVERSE_PROXY_AUTHENTICATION_EMAIL`: **X-WEBAUTH-EMAIL**: Header name for reverse proxy
authentication provided email.
+- `REVERSE_PROXY_AUTHENTICATION_FULL_NAME`: **X-WEBAUTH-FULLNAME**: Header name for reverse proxy
+ authentication provided full name.
- `REVERSE_PROXY_LIMIT`: **1**: Interpret X-Forwarded-For header or the X-Real-IP header and set this as the remote IP for the request.
Number of trusted proxy count. Set to zero to not use these headers.
- `REVERSE_PROXY_TRUSTED_PROXIES`: **127.0.0.0/8,::1/128**: List of IP addresses and networks separated by comma of trusted proxy servers. Use `*` to trust all.
@@ -497,29 +563,30 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
It also enables them to access other resources available to the user on the operating system that is running the
Gitea instance and perform arbitrary actions in the name of the Gitea OS user.
This maybe harmful to you website or your operating system.
+ Setting this to true does not change existing hooks in git repos; adjust it before if necessary.
- `DISABLE_WEBHOOKS`: **false**: Set to `true` to disable webhooks feature.
- `ONLY_ALLOW_PUSH_IF_GITEA_ENVIRONMENT_SET`: **true**: Set to `false` to allow local users to push to gitea-repositories without setting up the Gitea environment. This is not recommended and if you want local users to push to Gitea repositories you should set the environment appropriately.
- `IMPORT_LOCAL_PATHS`: **false**: Set to `false` to prevent all users (including admin) from importing local path on server.
- `INTERNAL_TOKEN`: **\**: Secret used to validate communication within Gitea binary.
-- `INTERNAL_TOKEN_URI`: ****: Instead of defining internal token in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`)
+- `INTERNAL_TOKEN_URI`: ****: Instead of defining INTERNAL_TOKEN in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`)
- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, scrypt, bcrypt\], argon2 will spend more memory than others.
- `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie.
- `MIN_PASSWORD_LENGTH`: **6**: Minimum password length for new users.
- `PASSWORD_COMPLEXITY`: **off**: Comma separated list of character classes required to pass minimum complexity. If left empty or no valid values are specified, checking is disabled (off):
- - lower - use one or more lower latin characters
- - upper - use one or more upper latin characters
- - digit - use one or more digits
- - spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~``
- - off - do not check password complexity
+ - lower - use one or more lower latin characters
+ - upper - use one or more upper latin characters
+ - digit - use one or more digits
+ - spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~``
+ - off - do not check password complexity
- `PASSWORD_CHECK_PWN`: **false**: Check [HaveIBeenPwned](https://haveibeenpwned.com/Passwords) to see if a password has been exposed.
- `SUCCESSFUL_TOKENS_CACHE_SIZE`: **20**: Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations. This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
## Camo (`camo`)
- `ENABLED`: **false**: Enable media proxy, we support images only at the moment.
-- `SERVER_URL`: ****: url of camo server, it **is required** if camo is enabled.
-- `HMAC_KEY`: ****: Provide the HMAC key for encoding urls, it **is required** if camo is enabled.
-- `ALLWAYS`: **false**: Set to true to use camo for https too lese only non https urls are proxyed
+- `SERVER_URL`: ****: URL of camo server, it **is required** if camo is enabled.
+- `HMAC_KEY`: ****: Provide the HMAC key for encoding URLs, it **is required** if camo is enabled.
+- `ALLWAYS`: **false**: Set to true to use camo for both HTTP and HTTPS content, otherwise only non-HTTPS URLs are proxied
## OpenID (`openid`)
@@ -532,18 +599,18 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
## OAuth2 Client (`oauth2_client`)
-- `REGISTER_EMAIL_CONFIRM`: *[service]* **REGISTER\_EMAIL\_CONFIRM**: Set this to enable or disable email confirmation of OAuth2 auto-registration. (Overwrites the REGISTER\_EMAIL\_CONFIRM setting of the `[service]` section)
+- `REGISTER_EMAIL_CONFIRM`: _[service]_ **REGISTER\_EMAIL\_CONFIRM**: Set this to enable or disable email confirmation of OAuth2 auto-registration. (Overwrites the REGISTER\_EMAIL\_CONFIRM setting of the `[service]` section)
- `OPENID_CONNECT_SCOPES`: **\**: List of additional openid connect scopes. (`openid` is implicitly added)
- `ENABLE_AUTO_REGISTRATION`: **false**: Automatically create user accounts for new oauth2 users.
- `USERNAME`: **nickname**: The source of the username for new oauth2 accounts:
- - userid - use the userid / sub attribute
- - nickname - use the nickname attribute
- - email - use the username part of the email attribute
+ - userid - use the userid / sub attribute
+ - nickname - use the nickname attribute
+ - email - use the username part of the email attribute
- `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login.
- `ACCOUNT_LINKING`: **login**: How to handle if an account / email already exists:
- - disabled - show an error
- - login - show an account linking login
- - auto - automatically link with the account (Please be aware that this will grant access to an existing account just because the same username or email is provided. You must make sure that this does not cause issues with your authentication providers.)
+ - disabled - show an error
+ - login - show an account linking login
+ - auto - automatically link with the account (Please be aware that this will grant access to an existing account just because the same username or email is provided. You must make sure that this does not cause issues with your authentication providers.)
## Service (`service`)
@@ -571,15 +638,21 @@ Certain queues have defaults that override the defaults set in `[queue]` (this o
for reverse authentication.
- `ENABLE_REVERSE_PROXY_EMAIL`: **false**: Enable this to allow to auto-registration with a
provided email rather than a generated email.
+- `ENABLE_REVERSE_PROXY_FULL_NAME`: **false**: Enable this to allow to auto-registration with a
+ provided full name for the user.
- `ENABLE_CAPTCHA`: **false**: Enable this to use captcha validation for registration.
+- `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: Enable this to require captcha validation for login. You also must enable `ENABLE_CAPTCHA`.
- `REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA`: **false**: Enable this to force captcha validation
- even for External Accounts (i.e. GitHub, OpenID Connect, etc). You must `ENABLE_CAPTCHA` also.
-- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha\]
+ even for External Accounts (i.e. GitHub, OpenID Connect, etc). You also must enable `ENABLE_CAPTCHA`.
+- `CAPTCHA_TYPE`: **image**: \[image, recaptcha, hcaptcha, mcaptcha\]
- `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha.
- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha.
- `RECAPTCHA_URL`: **https://www.google.com/recaptcha/**: Set the recaptcha url - allows the use of recaptcha net.
- `HCAPTCHA_SECRET`: **""**: Sign up at https://www.hcaptcha.com/ to get a secret for hcaptcha.
- `HCAPTCHA_SITEKEY`: **""**: Sign up at https://www.hcaptcha.com/ to get a sitekey for hcaptcha.
+- `MCAPTCHA_SECRET`: **""**: Go to your mCaptcha instance to get a secret for mCaptcha.
+- `MCAPTCHA_SITEKEY`: **""**: Go to your mCaptcha instance to get a sitekey for mCaptcha.
+- `MCAPTCHA_URL` **https://demo.mcaptcha.org/**: Set the mCaptcha URL.
- `DEFAULT_KEEP_EMAIL_PRIVATE`: **false**: By default set users to keep their email address private.
- `DEFAULT_ALLOW_CREATE_ORGANIZATION`: **true**: Allow new users to create organizations by default.
- `DEFAULT_USER_IS_RESTRICTED`: **false**: Give new users restricted permissions by default
@@ -618,18 +691,18 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
- `ED25519`: **256**
- `ECDSA`: **256**
-- `RSA`: **2048**
+- `RSA`: **2047**: We set 2047 here because an otherwise valid 2048 RSA key can be reported as 2047 length.
- `DSA`: **-1**: DSA is now disabled by default. Set to **1024** to re-enable but ensure you may need to reconfigure your SSHD provider
## Webhook (`webhook`)
- `QUEUE_LENGTH`: **1000**: Hook task queue length. Use caution when editing this value.
- `DELIVER_TIMEOUT`: **5**: Delivery timeout (sec) for shooting webhooks.
-- `ALLOWED_HOST_LIST`: **external**: Since 1.15.7. Default to `*` for 1.15.x, `external` for 1.16 and later. Webhook can only call allowed hosts for security reasons. Comma separated list.
+- `ALLOWED_HOST_LIST`: **external**: Webhook can only call allowed hosts for security reasons. Comma separated list.
- Built-in networks:
- `loopback`: 127.0.0.0/8 for IPv4 and ::1/128 for IPv6, localhost is included.
- `private`: RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and RFC 4193 (FC00::/7). Also called LAN/Intranet.
- - `external`: A valid non-private unicast IP, you can access all hosts on public internet.
+ - `external`: A valid non-private unicast IP, you can access all hosts on public internet.
- `*`: All hosts are allowed.
- CIDR list: `1.2.3.0/8` for IPv4 and `2001:db8::/32` for IPv6
- Wildcard hosts: `*.mydomain.com`, `192.168.100.*`
@@ -640,42 +713,56 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
## Mailer (`mailer`)
+⚠️ This section is for Gitea 1.18 and later. If you are using Gitea 1.17 or older,
+please refer to
+[Gitea 1.17 app.ini example](https://github.com/go-gitea/gitea/blob/release/v1.17/custom/conf/app.example.ini)
+and
+[Gitea 1.17 configuration document](https://github.com/go-gitea/gitea/blob/release/v1.17/docs/content/doc/advanced/config-cheat-sheet.en-us.md)
+
- `ENABLED`: **false**: Enable to use a mail service.
-- `DISABLE_HELO`: **\**: Disable HELO operation.
-- `HELO_HOSTNAME`: **\**: Custom hostname for HELO operation.
-- `HOST`: **\**: SMTP mail host address and port (example: smtp.gitea.io:587).
- - As per RFC 8314, if supported, Implicit TLS/SMTPS on port 465 is recommended, otherwise opportunistic TLS via STARTTLS on port 587 should be used.
-- `IS_TLS_ENABLED` : **false** : Forcibly use TLS to connect even if not on a default SMTPS port.
- - Note, if the port ends with `465` Implicit TLS/SMTPS/SMTP over TLS will be used despite this setting.
- - Otherwise if `IS_TLS_ENABLED=false` and the server supports `STARTTLS` this will be used. Thus if `STARTTLS` is preferred you should set `IS_TLS_ENABLED=false`.
-- `FROM`: **\**: Mail from address, RFC 5322. This can be just an email address, or
- the "Name" \ format.
-- `ENVELOPE_FROM`: **\**: Address set as the From address on the SMTP mail envelope. Set to `<>` to send an empty address.
+- `PROTOCOL`: **\**: Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._
+ - SMTP family, if your provider does not explicitly say which protocol it uses but does provide a port, you can set SMTP_PORT instead and this will be inferred.
+ - **sendmail** Use the operating system's `sendmail` command instead of SMTP. This is common on Linux systems.
+ - **dummy** Send email messages to the log as a testing phase.
+ - Note that enabling sendmail will ignore all other `mailer` settings except `ENABLED`, `FROM`, `SUBJECT_PREFIX` and `SENDMAIL_PATH`.
+ - Enabling dummy will ignore all settings except `ENABLED`, `SUBJECT_PREFIX` and `FROM`.
+- `SMTP_ADDR`: **\**: Mail server address. e.g. smtp.gmail.com. For smtp+unix, this should be a path to a unix socket instead. _Before 1.18, this was combined with `SMTP_PORT` under the name `HOST`._
+- `SMTP_PORT`: **\**: Mail server port. If no protocol is specified, it will be inferred by this setting. Common ports are listed below. _Before 1.18, this was combined with `SMTP_ADDR` under the name `HOST`._
+ - 25: insecure SMTP
+ - 465: SMTP Secure
+ - 587: StartTLS
+- `USE_CLIENT_CERT`: **false**: Use client certificate for TLS/SSL.
+- `CLIENT_CERT_FILE`: **custom/mailer/cert.pem**: Client certificate file.
+- `CLIENT_KEY_FILE`: **custom/mailer/key.pem**: Client key file.
+- `FORCE_TRUST_SERVER_CERT`: **false**: If set to `true`, completely ignores server certificate validation errors. This option is unsafe. Consider adding the certificate to the system trust store instead.
- `USER`: **\**: Username of mailing user (usually the sender's e-mail address).
- `PASSWD`: **\**: Password of mailing user. Use \`your password\` for quoting if you use special characters in the password.
- - Please note: authentication is only supported when the SMTP server communication is encrypted with TLS (this can be via `STARTTLS`) or `HOST=localhost`. See [Email Setup]({{< relref "doc/usage/email-setup.en-us.md" >}}) for more information.
-- `SEND_AS_PLAIN_TEXT`: **false**: Send mails as plain text.
-- `SKIP_VERIFY`: **false**: Whether or not to skip verification of certificates; `true` to disable verification.
- - **Warning:** This option is unsafe. Consider adding the certificate to the system trust store instead.
- - **Note:** Gitea only supports SMTP with STARTTLS.
-- `USE_CERTIFICATE`: **false**: Use client certificate.
-- `CERT_FILE`: **custom/mailer/cert.pem**
-- `KEY_FILE`: **custom/mailer/key.pem**
+ - Please note: authentication is only supported when the SMTP server communication is encrypted with TLS (this can be via `STARTTLS`) or SMTP host is localhost. See [Email Setup]({{< relref "doc/usage/email-setup.en-us.md" >}}) for more information.
+- `ENABLE_HELO`: **true**: Enable HELO operation.
+- `HELO_HOSTNAME`: **(retrieved from system)**: HELO hostname.
+- `FROM`: **\**: Mail from address, RFC 5322. This can be just an email address, or the "Name" \ format.
+- `ENVELOPE_FROM`: **\**: Address set as the From address on the SMTP mail envelope. Set to `<>` to send an empty address.
- `SUBJECT_PREFIX`: **\**: Prefix to be placed before e-mail subject lines.
-- `MAILER_TYPE`: **smtp**: \[smtp, sendmail, dummy\]
- - **smtp** Use SMTP to send mail
- - **sendmail** Use the operating system's `sendmail` command instead of SMTP.
- This is common on Linux systems.
- - **dummy** Send email messages to the log as a testing phase.
- - Note that enabling sendmail will ignore all other `mailer` settings except `ENABLED`,
- `FROM`, `SUBJECT_PREFIX` and `SENDMAIL_PATH`.
- - Enabling dummy will ignore all settings except `ENABLED`, `SUBJECT_PREFIX` and `FROM`.
-- `SENDMAIL_PATH`: **sendmail**: The location of sendmail on the operating system (can be
- command or full path).
-- `SENDMAIL_ARGS`: **_empty_**: Specify any extra sendmail arguments. (NOTE: you should be aware that email addresses can look like options - if your `sendmail` command takes options you must set the option terminator `--`)
+- `SENDMAIL_PATH`: **sendmail**: The location of sendmail on the operating system (can be command or full path).
+- `SENDMAIL_ARGS`: **\**: Specify any extra sendmail arguments. (NOTE: you should be aware that email addresses can look like options - if your `sendmail` command takes options you must set the option terminator `--`)
- `SENDMAIL_TIMEOUT`: **5m**: default timeout for sending email through sendmail
- `SENDMAIL_CONVERT_CRLF`: **true**: Most versions of sendmail prefer LF line endings rather than CRLF line endings. Set this to false if your version of sendmail requires CRLF line endings.
- `SEND_BUFFER_LEN`: **100**: Buffer length of mailing queue. **DEPRECATED** use `LENGTH` in `[queue.mailer]`
+- `SEND_AS_PLAIN_TEXT`: **false**: Send mails only in plain text, without HTML alternative.
+
+## Incoming Email (`email.incoming`)
+
+- `ENABLED`: **false**: Enable handling of incoming emails.
+- `REPLY_TO_ADDRESS`: **\**: The email address including the `%{token}` placeholder that will be replaced per user/action. Example: `incoming+%{token}@example.com`. The placeholder must appear in the user part of the address (before the `@`).
+- `HOST`: **\**: IMAP server host.
+- `PORT`: **\**: IMAP server port.
+- `USERNAME`: **\**: Username of the receiving account.
+- `PASSWORD`: **\**: Password of the receiving account.
+- `USE_TLS`: **false**: Whether the IMAP server uses TLS.
+- `SKIP_TLS_VERIFY`: **false**: If set to `true`, completely ignores server certificate validation errors. This option is unsafe.
+- `MAILBOX`: **INBOX**: The mailbox name where incoming mail will end up.
+- `DELETE_HANDLED_MESSAGE`: **true**: Whether handled messages should be deleted from the mailbox.
+- `MAXIMUM_MESSAGE_SIZE`: **10485760**: Maximum size of a message to handle. Bigger messages are ignored. Set to 0 to allow every size.
## Cache (`cache`)
@@ -683,21 +770,21 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
- `ADAPTER`: **memory**: Cache engine adapter, either `memory`, `redis`, `twoqueue` or `memcache`. (`twoqueue` represents a size limited LRU cache.)
- `INTERVAL`: **60**: Garbage Collection interval (sec), for memory and twoqueue cache only.
- `HOST`: **\**: Connection string for `redis` and `memcache`. For `twoqueue` sets configuration for the queue.
- - Redis: `redis://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s`
- - Memcache: `127.0.0.1:9090;127.0.0.1:9091`
- - TwoQueue LRU cache: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` representing the maximum number of objects stored in the cache.
-- `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to 0 disables caching.
+ - Redis: `redis://:macaron@127.0.0.1:6379/0?pool_size=100&idle_timeout=180s`
+ - Memcache: `127.0.0.1:9090;127.0.0.1:9091`
+ - TwoQueue LRU cache: `{"size":50000,"recent_ratio":0.25,"ghost_ratio":0.5}` or `50000` representing the maximum number of objects stored in the cache.
+- `ITEM_TTL`: **16h**: Time to keep items in cache if not used, Setting it to -1 disables caching.
## Cache - LastCommitCache settings (`cache.last_commit`)
- `ENABLED`: **true**: Enable the cache.
-- `ITEM_TTL`: **8760h**: Time to keep items in cache if not used, Setting it to 0 disables caching.
+- `ITEM_TTL`: **8760h**: Time to keep items in cache if not used, Setting it to -1 disables caching.
- `COMMITS_COUNT`: **1000**: Only enable the cache when repository's commits count great than.
## Session (`session`)
-- `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, db, mysql, couchbase, memcache, postgres\].
-- `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string.
+- `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]`
+- `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_.
- `COOKIE_SECURE`: **false**: Enable this to force using HTTPS for all session access.
- `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID.
- `GC_INTERVAL_TIME`: **86400**: GC interval in seconds.
@@ -709,9 +796,9 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
- `GRAVATAR_SOURCE`: **gravatar**: Can be `gravatar`, `duoshuo` or anything like
`http://cn.gravatar.com/avatar/`.
-- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only.
+- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only. **DEPRECATED [v1.18+]** moved to database. Use admin panel to configure.
- `ENABLE_FEDERATED_AVATAR`: **false**: Enable support for federated avatars (see
- [http://www.libravatar.org](http://www.libravatar.org)).
+ [http://www.libravatar.org](http://www.libravatar.org)). **DEPRECATED [v1.18+]** moved to database. Use admin panel to configure.
- `AVATAR_STORAGE_TYPE`: **default**: Storage type defined in `[storage.xxx]`. Default is `default` which will read `[storage]` if no section `[storage]` will be a type `local`.
- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files.
@@ -728,7 +815,6 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
- image = default image will be used (which is set in `REPOSITORY_AVATAR_FALLBACK_IMAGE`)
- `REPOSITORY_AVATAR_FALLBACK_IMAGE`: **/img/repo_default.png**: Image used as default repository avatar (if `REPOSITORY_AVATAR_FALLBACK` is set to image and none was uploaded)
-
## Project (`project`)
Default templates for project boards:
@@ -739,7 +825,7 @@ Default templates for project boards:
## Issue and pull request attachments (`attachment`)
- `ENABLED`: **true**: Whether issue and pull request attachments are enabled.
-- `ALLOWED_TYPES`: **.docx,.gif,.gz,.jpeg,.jpg,mp4,.log,.pdf,.png,.pptx,.txt,.xlsx,.zip**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
+- `ALLOWED_TYPES`: **.csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
- `MAX_SIZE`: **4**: Maximum size (MB).
- `MAX_FILES`: **5**: Maximum number of attachments that can be uploaded at once.
- `STORAGE_TYPE`: **local**: Storage type for attachments, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]`
@@ -761,13 +847,15 @@ Default templates for project boards:
- `STACKTRACE_LEVEL`: **None**: Default log level at which to log create stack traces. \[Trace, Debug, Info, Warn, Error, Critical, Fatal, None\]
- `ENABLE_SSH_LOG`: **false**: save ssh log to log file
- `ENABLE_XORM_LOG`: **true**: Set whether to perform XORM logging. Please note SQL statement logging can be disabled by setting `LOG_SQL` to false in the `[database]` section.
-
+
### Router Log (`log`)
+
- `DISABLE_ROUTER_LOG`: **false**: Mute printing of the router log.
- `ROUTER`: **console**: The mode or name of the log the router should log to. (If you set this to `,` it will log to default Gitea logger.)
NB: You must have `DISABLE_ROUTER_LOG` set to `false` for this option to take effect. Configure each mode in per mode log subsections `\[log.modename.router\]`.
### Access Log (`log`)
+
- `ENABLE_ACCESS_LOG`: **false**: Creates an access.log in NCSA common log format, or as per the following template
- `ACCESS`: **file**: Logging mode for the access logger, use a comma to separate values. Configure each mode in per mode log subsections `\[log.modename.access\]`. By default the file mode will log to `$ROOT_PATH/access.log`. (If you set this to `,` it will log to the default Gitea logger.)
- `ACCESS_LOG_TEMPLATE`: **`{{.Ctx.RemoteAddr}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}\" \"{{.Ctx.Req.UserAgent}}"`**: Sets the template used to create the access log.
@@ -784,9 +872,9 @@ Default templates for project boards:
- `STACKTRACE_LEVEL`: **log.STACKTRACE_LEVEL**: Sets the log level at which to log stack traces.
- `MODE`: **name**: Sets the mode of this sublogger - Defaults to the provided subsection name. This allows you to have two different file loggers at different levels.
- `EXPRESSION`: **""**: A regular expression to match either the function name, file or message. Defaults to empty. Only log messages that match the expression will be saved in the logger.
-- `FLAGS`: **stdflags**: A comma separated string representing the log flags. Defaults to `stdflags` which represents the prefix: `2009/01/23 01:23:23 ...a/b/c/d.go:23:runtime.Caller() [I]: message`. `none` means don't prefix log lines. See `modules/log/base.go` for more information.
+- `FLAGS`: **stdflags**: A comma separated string representing the log flags. Defaults to `stdflags` which represents the prefix: `2009/01/23 01:23:23 ...a/b/c/d.go:23:runtime.Caller() [I]: message`. `none` means don't prefix log lines. See `modules/log/flags.go` for more information.
- `PREFIX`: **""**: An additional prefix for every log line in this logger. Defaults to empty.
-- `COLORIZE`: **false**: Colorize the log lines by default
+- `COLORIZE`: **false**: Whether to colorize the log lines
### Console log mode (`log.console`, `log.console.*`, or `MODE=console`)
@@ -825,9 +913,9 @@ Default templates for project boards:
- `NOTICE_ON_SUCCESS`: **false**: Set to true to switch on success notices.
- `SCHEDULE` accept formats
- - Full crontab specs, e.g. `* * * * * ?`
- - Descriptors, e.g. `@midnight`, `@every 1h30m` ...
- - See more: [cron decument](https://pkg.go.dev/github.com/gogs/cron@v0.0.0-20171120032916-9f6c956d3e14)
+ - Full crontab specs, e.g. `* * * * * ?`
+ - Descriptors, e.g. `@midnight`, `@every 1h30m` ...
+ - See more: [cron documentation](https://pkg.go.dev/github.com/gogs/cron@v0.0.0-20171120032916-9f6c956d3e14)
### Basic cron tasks - enabled by default
@@ -884,6 +972,7 @@ Default templates for project boards:
### Extended cron tasks (not enabled by default)
#### Cron - Garbage collect all repositories ('cron.git_gc_repos')
+
- `ENABLED`: **false**: Enable service.
- `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED).
- `SCHEDULE`: **@every 72h**: Cron syntax for scheduling repository archive cleanup, e.g. `@every 1h`.
@@ -892,36 +981,42 @@ Default templates for project boards:
- `ARGS`: **\**: Arguments for command `git gc`, e.g. `--aggressive --auto`. The default value is same with [git] -> GC_ARGS
#### Cron - Update the '.ssh/authorized_keys' file with Gitea SSH keys ('cron.resync_all_sshkeys')
+
- `ENABLED`: **false**: Enable service.
- `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED).
- `NOTICE_ON_SUCCESS`: **false**: Set to true to switch on success notices.
- `SCHEDULE`: **@every 72h**: Cron syntax for scheduling repository archive cleanup, e.g. `@every 1h`.
#### Cron - Resynchronize pre-receive, update and post-receive hooks of all repositories ('cron.resync_all_hooks')
+
- `ENABLED`: **false**: Enable service.
- `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED).
- `NOTICE_ON_SUCCESS`: **false**: Set to true to switch on success notices.
- `SCHEDULE`: **@every 72h**: Cron syntax for scheduling repository archive cleanup, e.g. `@every 1h`.
#### Cron - Reinitialize all missing Git repositories for which records exist ('cron.reinit_missing_repos')
+
- `ENABLED`: **false**: Enable service.
- `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED).
- `NOTICE_ON_SUCCESS`: **false**: Set to true to switch on success notices.
- `SCHEDULE`: **@every 72h**: Cron syntax for scheduling repository archive cleanup, e.g. `@every 1h`.
#### Cron - Delete all repositories missing their Git files ('cron.delete_missing_repos')
+
- `ENABLED`: **false**: Enable service.
- `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED).
- `NOTICE_ON_SUCCESS`: **false**: Set to true to switch on success notices.
- `SCHEDULE`: **@every 72h**: Cron syntax for scheduling repository archive cleanup, e.g. `@every 1h`.
#### Cron - Delete generated repository avatars ('cron.delete_generated_repository_avatars')
+
- `ENABLED`: **false**: Enable service.
- `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED).
- `NOTICE_ON_SUCCESS`: **false**: Set to true to switch on success notices.
- `SCHEDULE`: **@every 72h**: Cron syntax for scheduling repository archive cleanup, e.g. `@every 1h`.
#### Cron - Delete all old actions from database ('cron.delete_old_actions')
+
- `ENABLED`: **false**: Enable service.
- `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED).
- `NOTICE_ON_SUCCESS`: **false**: Set to true to switch on success notices.
@@ -929,22 +1024,36 @@ Default templates for project boards:
- `OLDER_THAN`: **@every 8760h**: any action older than this expression will be deleted from database, suggest using `8760h` (1 year) because that's the max length of heatmap.
#### Cron - Check for new Gitea versions ('cron.update_checker')
-- `ENABLED`: **false**: Enable service.
+
+- `ENABLED`: **true**: Enable service.
- `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED).
- `ENABLE_SUCCESS_NOTICE`: **true**: Set to false to switch off success notices.
- `SCHEDULE`: **@every 168h**: Cron syntax for scheduling a work, e.g. `@every 168h`.
- `HTTP_ENDPOINT`: **https://dl.gitea.io/gitea/version.json**: the endpoint that Gitea will check for newer versions
#### Cron - Delete all old system notices from database ('cron.delete_old_system_notices')
+
- `ENABLED`: **false**: Enable service.
- `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED).
- `NO_SUCCESS_NOTICE`: **false**: Set to true to switch off success notices.
- `SCHEDULE`: **@every 168h**: Cron syntax to set how often to check.
- `OLDER_THAN`: **@every 8760h**: any system notice older than this expression will be deleted from database.
+#### Cron - Garbage collect LFS pointers in repositories ('cron.gc_lfs')
+
+- `ENABLED`: **false**: Enable service.
+- `RUN_AT_START`: **false**: Run tasks at start up time (if ENABLED).
+- `SCHEDULE`: **@every 24h**: Cron syntax to set how often to check.
+- `OLDER_THAN`: **168h**: Only attempt to garbage collect LFSMetaObjects older than this (default 7 days)
+- `LAST_UPDATED_MORE_THAN_AGO`: **72h**: Only attempt to garbage collect LFSMetaObjects that have not been attempted to be garbage collected for this long (default 3 days)
+- `NUMBER_TO_CHECK_PER_REPO`: **100**: Minimum number of stale LFSMetaObjects to check per repo. Set to `0` to always check all.
+- `PROPORTION_TO_CHECK_PER_REPO`: **0.6**: Check at least this proportion of LFSMetaObjects per repo. (This may cause all stale LFSMetaObjects to be checked.)
+
## Git (`git`)
- `PATH`: **""**: The path of Git executable. If empty, Gitea searches through the PATH environment.
+- `HOME_PATH`: **%(APP_DATA_PATH)s/home**: The HOME directory for Git.
+ This directory will be used to contain the `.gitconfig` and possible `.gnupg` directories that Gitea's git calls will use. If you can confirm Gitea is the only application running in this environment, you can set it to the normal home directory for Gitea user.
- `DISABLE_DIFF_HIGHLIGHT`: **false**: Disables highlight of added and removed changes.
- `MAX_GIT_DIFF_LINES`: **1000**: Max number of lines allowed of a single file in diff view.
- `MAX_GIT_DIFF_LINE_CHARACTERS`: **5000**: Max character count per line highlighted in diff view.
@@ -952,7 +1061,8 @@ Default templates for project boards:
- `COMMITS_RANGE_SIZE`: **50**: Set the default commits range size
- `BRANCHES_RANGE_SIZE`: **20**: Set the default branches range size
- `GC_ARGS`: **\**: Arguments for command `git gc`, e.g. `--aggressive --auto`. See more on http://git-scm.com/docs/git-gc/
-- `ENABLE_AUTO_GIT_WIRE_PROTOCOL`: **true**: If use Git wire protocol version 2 when Git version >= 2.18, default is true, set to false when you always want Git wire protocol version 1
+- `ENABLE_AUTO_GIT_WIRE_PROTOCOL`: **true**: If use Git wire protocol version 2 when Git version >= 2.18, default is true, set to false when you always want Git wire protocol version 1.
+ To enable this for Git over SSH when using a OpenSSH server, add `AcceptEnv GIT_PROTOCOL` to your sshd_config file.
- `PULL_REQUEST_PUSH_MESSAGE`: **true**: Respond to pushes to a non-default branch with a URL for creating a Pull Request (if the repository has them enabled)
- `VERBOSE_PUSH`: **true**: Print status information about pushes as they are being processed.
- `VERBOSE_PUSH_DELAY`: **5s**: Only print verbose information if push takes longer than this delay.
@@ -961,7 +1071,8 @@ Default templates for project boards:
- `DISABLE_PARTIAL_CLONE`: **false** Disable the usage of using partial clones for git.
## Git - Timeout settings (`git.timeout`)
-- `DEFAUlT`: **360**: Git operations default timeout seconds.
+
+- `DEFAULT`: **360**: Git operations default timeout seconds.
- `MIGRATE`: **600**: Migrate external repositories timeout seconds.
- `MIRROR`: **300**: Mirror external repositories timeout seconds.
- `CLONE`: **300**: Git clone from internal repositories timeout seconds.
@@ -977,11 +1088,11 @@ Default templates for project boards:
## API (`api`)
-- `ENABLE_SWAGGER`: **true**: Enables /api/swagger, /api/v1/swagger etc. endpoints. True or false; default is true.
+- `ENABLE_SWAGGER`: **true**: Enables the API documentation endpoints (`/api/swagger`, `/api/v1/swagger`, …). True or false.
- `MAX_RESPONSE_ITEMS`: **50**: Max number of items in a page.
- `DEFAULT_PAGING_NUM`: **30**: Default paging number of API.
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: Default and maximum number of items per page for Git trees API.
-- `DEFAULT_MAX_BLOB_SIZE`: **10485760**: Default max size of a blob that can be return by the blobs API.
+- `DEFAULT_MAX_BLOB_SIZE`: **10485760** (10MiB): Default max size of a blob that can be returned by the blobs API.
## OAuth2 (`oauth2`)
@@ -996,12 +1107,9 @@ Default templates for project boards:
## i18n (`i18n`)
-- `LANGS`: **en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pt-PT,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR,el-GR,fa-IR,hu-HU,id-ID,ml-IN**:
+- `LANGS`: **en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,uk-UA,ja-JP,es-ES,pt-BR,pt-PT,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sv-SE,ko-KR,el-GR,fa-IR,hu-HU,id-ID,ml-IN**:
List of locales shown in language selector. The first locale will be used as the default if user browser's language doesn't match any locale in the list.
-- `NAMES`: **English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,日本語,español,português do Brasil,Português de Portugal,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어,ελληνικά,فارسی,magyar nyelv,bahasa Indonesia,മലയാളം**: Visible names corresponding to the locales
-
-## U2F (`U2F`) **DEPRECATED**
-- `APP_ID`: **`ROOT_URL`**: Declares the facet of the application which is used for authentication of previously registered U2F keys. Requires HTTPS.
+- `NAMES`: **English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,Français,Nederlands,Latviešu,Русский,Українська,日本語,Español,Português do Brasil,Português de Portugal,Polski,Български,Italiano,Suomi,Türkçe,Čeština,Српски,Svenska,한국어,Ελληνικά,فارسی,Magyar nyelv,Bahasa Indonesia,മലയാളം**: Visible names corresponding to the locales
## Markup (`markup`)
@@ -1024,13 +1132,17 @@ IS_INPUT_FILE = false
command. Multiple extensions needs a comma as splitter.
- RENDER\_COMMAND: External command to render all matching extensions.
- IS\_INPUT\_FILE: **false** Input is not a standard input but a file param followed `RENDER_COMMAND`.
-- DISABLE_SANITIZER: **false** Don't filter html tags and attributes if true. Don't change this to true except you know what that means.
+- RENDER_CONTENT_MODE: **sanitized** How the content will be rendered.
+ - sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in `[markup.sanitizer.*]`.
+ - no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code.
+ - iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page.
Two special environment variables are passed to the render command:
+
- `GITEA_PREFIX_SRC`, which contains the current URL prefix in the `src` path tree. To be used as prefix for links.
- `GITEA_PREFIX_RAW`, which contains the current URL prefix in the `raw` path tree. To be used as prefix for image paths.
-If `DISABLE_SANITIZER` is false, Gitea supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc.
+If `RENDER_CONTENT_MODE` is `sanitized`, Gitea supports customizing the sanitization policy for rendered HTML. The example below will support KaTeX output from pandoc.
```ini
[markup.sanitizer.TeX]
@@ -1042,10 +1154,10 @@ REGEXP = ^\s*((math(\s+|$)|inline(\s+|$)|display(\s+|$)))+
ALLOW_DATA_URI_IMAGES = true
```
- - `ELEMENT`: The element this policy applies to. Must be non-empty.
- - `ALLOW_ATTR`: The attribute this policy allows. Must be non-empty.
- - `REGEXP`: A regex to match the contents of the attribute against. Must be present but may be empty for unconditional whitelisting of this attribute.
- - `ALLOW_DATA_URI_IMAGES`: **false** Allow data uri images (` `).
+- `ELEMENT`: The element this policy applies to. Must be non-empty.
+- `ALLOW_ATTR`: The attribute this policy allows. Must be non-empty.
+- `REGEXP`: A regex to match the contents of the attribute against. Must be present but may be empty for unconditional whitelisting of this attribute.
+- `ALLOW_DATA_URI_IMAGES`: **false** Allow data uri images (` `).
Multiple sanitisation rules can be defined by adding unique subsections, e.g. `[markup.sanitizer.TeX-2]`.
To apply a sanitisation rules only for a specify external renderer they must use the renderer name, e.g. `[markup.sanitizer.asciidoc.rule-1]`.
@@ -1062,7 +1174,7 @@ in this mapping or the filetype using heuristics.
## Time (`time`)
- `FORMAT`: Time format to display on UI. i.e. RFC1123 or 2006-01-02 15:04:05
-- `DEFAULT_UI_LOCATION`: Default location of time on the UI, so that we can display correct user's time on UI. i.e. Shanghai/Asia
+- `DEFAULT_UI_LOCATION`: Default location of time on the UI, so that we can display correct user's time on UI. i.e. Asia/Shanghai
## Task (`task`)
@@ -1076,23 +1188,46 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
- `MAX_ATTEMPTS`: **3**: Max attempts per http/https request on migrations.
- `RETRY_BACKOFF`: **3**: Backoff time per http/https request retry (seconds)
-- `ALLOWED_DOMAINS`: **\**: Domains allowlist for migrating repositories, default is blank. It means everything will be allowed. Multiple domains could be separated by commas.
-- `BLOCKED_DOMAINS`: **\**: Domains blocklist for migrating repositories, default is blank. Multiple domains could be separated by commas. When `ALLOWED_DOMAINS` is not blank, this option has a higher priority to deny domains.
-- `ALLOW_LOCALNETWORKS`: **false**: Allow private addresses defined by RFC 1918, RFC 1122, RFC 4632 and RFC 4291
+- `ALLOWED_DOMAINS`: **\**: Domains allowlist for migrating repositories, default is blank. It means everything will be allowed. Multiple domains could be separated by commas. Wildcard is supported: `github.com, *.github.com`.
+- `BLOCKED_DOMAINS`: **\**: Domains blocklist for migrating repositories, default is blank. Multiple domains could be separated by commas. When `ALLOWED_DOMAINS` is not blank, this option has a higher priority to deny domains. Wildcard is supported.
+- `ALLOW_LOCALNETWORKS`: **false**: Allow private addresses defined by RFC 1918, RFC 1122, RFC 4632 and RFC 4291. If a domain is allowed by `ALLOWED_DOMAINS`, this option will be ignored.
- `SKIP_TLS_VERIFY`: **false**: Allow skip tls verify
## Federation (`federation`)
-- `ENABLED`: **true**: Enable/Disable federation capabilities
+- `ENABLED`: **false**: Enable/Disable federation capabilities
+- `SHARE_USER_STATISTICS`: **true**: Enable/Disable user statistics for nodeinfo if federation is enabled
+- `MAX_SIZE`: **4**: Maximum federation request and response size (MB)
+
+ WARNING: Changing the settings below can break federation.
+
+- `ALGORITHMS`: **rsa-sha256, rsa-sha512, ed25519**: HTTP signature algorithms
+- `DIGEST_ALGORITHM`: **SHA-256**: HTTP signature digest algorithm
+- `GET_HEADERS`: **(request-target), Date**: GET headers for federation requests
+- `POST_HEADERS`: **(request-target), Date, Digest**: POST headers for federation requests
## Packages (`packages`)
- `ENABLED`: **true**: Enable/Disable package registry capabilities
- `CHUNKED_UPLOAD_PATH`: **tmp/package-upload**: Path for chunked uploads. Defaults to `APP_DATA_PATH` + `tmp/package-upload`
+- `LIMIT_TOTAL_OWNER_COUNT`: **-1**: Maximum count of package versions a single owner can have (`-1` means no limits)
+- `LIMIT_TOTAL_OWNER_SIZE`: **-1**: Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_COMPOSER`: **-1**: Maximum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_CONAN`: **-1**: Maximum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_CONTAINER`: **-1**: Maximum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_GENERIC`: **-1**: Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_HELM`: **-1**: Maximum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_MAVEN`: **-1**: Maximum size of a Maven upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_NPM`: **-1**: Maximum size of a npm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_NUGET`: **-1**: Maximum size of a NuGet upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_PUB`: **-1**: Maximum size of a Pub upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_PYPI`: **-1**: Maximum size of a PyPI upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_RUBYGEMS`: **-1**: Maximum size of a RubyGems upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
+- `LIMIT_SIZE_VAGRANT`: **-1**: Maximum size of a Vagrant upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
## Mirror (`mirror`)
-- `ENABLED`: **true**: Enables the mirror functionality. Set to **false** to disable all mirrors.
+- `ENABLED`: **true**: Enables the mirror functionality. Set to **false** to disable all mirrors. Pre-existing mirrors remain valid but won't be updated; may be converted to regular repo.
- `DISABLE_NEW_PULL`: **false**: Disable the creation of **new** pull mirrors. Pre-existing mirrors remain valid. Will be ignored if `mirror.ENABLED` is `false`.
- `DISABLE_NEW_PUSH`: **false**: Disable the creation of **new** push mirrors. Pre-existing mirrors remain valid. Will be ignored if `mirror.ENABLED` is `false`.
- `DEFAULT_INTERVAL`: **8h**: Default interval between each check
@@ -1172,6 +1307,7 @@ is `data/repo-archive` and the default of `MINIO_BASE_PATH` is `repo-archive/`.
- `PROXY_HOSTS`: **\**: Comma separated list of host names requiring proxy. Glob patterns (*) are accepted; use ** to match all hosts.
i.e.
+
```ini
PROXY_ENABLED = true
PROXY_URL = socks://127.0.0.1:1080
@@ -1183,3 +1319,5 @@ PROXY_HOSTS = *.github.com
- `SHOW_FOOTER_BRANDING`: **false**: Show Gitea branding in the footer.
- `SHOW_FOOTER_VERSION`: **true**: Show Gitea and Go version information in the footer.
- `SHOW_FOOTER_TEMPLATE_LOAD_TIME`: **true**: Show time of template execution in the footer.
+- `ENABLE_SITEMAP`: **true**: Generate sitemap.
+- `ENABLE_FEED`: **true**: Enable/Disable RSS/Atom feed.
diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
index 600e54a85e54e..f10b6258c87a2 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md
@@ -15,7 +15,14 @@ menu:
# 配置说明
-这是针对Gitea配置文件的说明,你可以了解Gitea的强大配置。需要说明的是,你的所有改变请修改 `custom/conf/app.ini` 文件而不是源文件。所有默认值可以通过 [app.example.ini](https://github.com/go-gitea/gitea/blob/master/custom/conf/app.example.ini) 查看到。如果你发现 `%(X)s` 这样的内容,请查看 [ini](https://github.com/go-ini/ini/#recursive-values) 这里的说明。标注了 :exclamation: 的配置项表明除非你真的理解这个配置项的意义,否则最好使用默认值。
+这是针对Gitea配置文件的说明,你可以了解Gitea的强大配置。需要说明的是,你的所有改变请修改 `custom/conf/app.ini` 文件而不是源文件。
+所有默认值可以通过 [app.example.ini](https://github.com/go-gitea/gitea/blob/master/custom/conf/app.example.ini) 查看到。
+如果你发现 `%(X)s` 这样的内容,请查看 [ini](https://github.com/go-ini/ini/#recursive-values) 这里的说明。
+标注了 :exclamation: 的配置项表明除非你真的理解这个配置项的意义,否则最好使用默认值。
+
+## ⚠️时效性警告⚠️
+
+此文档的内容可能过于陈旧或者错误,请参考英文文档。
{{< toc >}}
@@ -138,7 +145,8 @@ menu:
- `ENABLE_NOTIFY_MAIL`: 是否发送工单创建等提醒邮件,需要 `Mailer` 被激活。
- `ENABLE_REVERSE_PROXY_AUTHENTICATION`: 允许反向代理认证,更多细节见:https://github.com/gogits/gogs/issues/165
- `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: 允许通过反向认证做自动注册。
-- `ENABLE_CAPTCHA`: 注册时使用图片验证码。
+- `ENABLE_CAPTCHA`: **false**: 注册时使用图片验证码。
+- `REQUIRE_CAPTCHA_FOR_LOGIN`: **false**: 登录时需要图片验证码。需要同时开启 `ENABLE_CAPTCHA`。
### Service - Expore (`service.explore`)
@@ -173,14 +181,14 @@ menu:
- `ADAPTER`: **memory**: 缓存引擎,可以为 `memory`, `redis` 或 `memcache`。
- `INTERVAL`: **60**: 只对内存缓存有效,GC间隔,单位秒。
- `HOST`: **\**: 针对redis和memcache有效,主机地址和端口。
- - Redis: `network=tcp,addr=127.0.0.1:6379,password=macaron,db=0,pool_size=100,idle_timeout=180`
- - Memache: `127.0.0.1:9090;127.0.0.1:9091`
-- `ITEM_TTL`: **16h**: 缓存项目失效时间,设置为 0 则禁用缓存。
+ - Redis: `network=tcp,addr=127.0.0.1:6379,password=macaron,db=0,pool_size=100,idle_timeout=180`
+ - Memache: `127.0.0.1:9090;127.0.0.1:9091`
+- `ITEM_TTL`: **16h**: 缓存项目失效时间,设置为 -1 则禁用缓存。
## Cache - LastCommitCache settings (`cache.last_commit`)
- `ENABLED`: **true**: 是否启用。
-- `ITEM_TTL`: **8760h**: 缓存项目失效时间,设置为 0 则禁用缓存。
+- `ITEM_TTL`: **8760h**: 缓存项目失效时间,设置为 -1 则禁用缓存。
- `COMMITS_COUNT`: **1000**: 仅当仓库的提交数大于时才启用缓存。
## Session (`session`)
@@ -239,7 +247,6 @@ file -I test01.xls
test01.xls: application/vnd.ms-excel; charset=binary
```
-
## Log (`log`)
- `ROOT_PATH`: 日志文件根目录。
@@ -251,10 +258,9 @@ test01.xls: application/vnd.ms-excel; charset=binary
- `ENABLED`: 是否在后台运行定期任务。
- `RUN_AT_START`: 是否启动时自动运行。
- `SCHEDULE` 所接受的格式
- - 完整 crontab 控制, 例如 `* * * * * ?`
- - 描述符, 例如 `@midnight`, `@every 1h30m` ...
- - 更多细节参见 [cron api文档](https://pkg.go.dev/github.com/gogs/cron@v0.0.0-20171120032916-9f6c956d3e14)
-
+ - 完整 crontab 控制, 例如 `* * * * * ?`
+ - 描述符, 例如 `@midnight`, `@every 1h30m` ...
+ - 更多细节参见 [cron api文档](https://pkg.go.dev/github.com/gogs/cron@v0.0.0-20171120032916-9f6c956d3e14)
### Cron - Update Mirrors (`cron.update_mirrors`)
@@ -294,7 +300,7 @@ test01.xls: application/vnd.ms-excel; charset=binary
## API (`api`)
-- `ENABLE_SWAGGER`: **true**: 是否启用swagger路由 /api/swagger, /api/v1/swagger etc. endpoints. True 或 false; 默认是 true.
+- `ENABLE_SWAGGER`: **true**: 是否启用swagger路由 /api/swagger, /api/v1/swagger etc. endpoints. True 或 false.
- `MAX_RESPONSE_ITEMS`: **50**: 一个页面最大的项目数。
- `DEFAULT_PAGING_NUM`: **30**: API中默认分页条数。
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: GIT TREES API每页的默认最大项数.
@@ -318,14 +324,17 @@ IS_INPUT_FILE = false
- FILE_EXTENSIONS: 关联的文档的扩展名,多个扩展名用都好分隔。
- RENDER_COMMAND: 工具的命令行命令及参数。
- IS_INPUT_FILE: 输入方式是最后一个参数为文件路径还是从标准输入读取。
-- DISABLE_SANITIZER: **false** 如果为 true 则不过滤 HTML 标签和属性。除非你知道这意味着什么,否则不要设置为 true。
+- RENDER_CONTENT_MODE: **sanitized** 内容如何被渲染。
+ - sanitized: 对内容进行净化并渲染到当前页面中,仅有一部分 HTML 标签和属性是被允许的。
+ - no-sanitizer: 禁用净化器,把内容渲染到当前页面中。此模式是**不安全**的,如果内容中含有恶意代码,可能会导致 XSS 攻击。
+ - iframe: 把内容渲染在一个独立的页面中并使用 iframe 嵌入到当前页面中。使用的 iframe 工作在沙箱模式并禁用了同源请求,JS 代码被安全的从父页面中隔离出去。
以下两个环境变量将会被传递给渲染命令:
- `GITEA_PREFIX_SRC`:包含当前的`src`路径的URL前缀,可以被用于链接的前缀。
- `GITEA_PREFIX_RAW`:包含当前的`raw`路径的URL前缀,可以被用于图片的前缀。
-如果 `DISABLE_SANITIZER` 为 false,则 Gitea 支持自定义渲染 HTML 的净化策略。以下例子将用 pandoc 支持 KaTeX 输出。
+如果 `RENDER_CONTENT_MODE` 为 `sanitized`,则 Gitea 支持自定义渲染 HTML 的净化策略。以下例子将用 pandoc 支持 KaTeX 输出。
```ini
[markup.sanitizer.TeX]
@@ -343,7 +352,7 @@ ALLOW_DATA_URI_IMAGES = true
- `ALLOW_DATA_URI_IMAGES`: **false** 允许 data uri 图片 (` `)。
多个净化规则可以被同时定义,只要section名称最后一位不重复即可。如: `[markup.sanitizer.TeX-2]`。
-为了针对一种渲染类型进行一个特殊的净化策略,必须使用形如 `[markup.sanitizer.asciidoc.rule-1]` 的方式来命名 seciton。
+为了针对一种渲染类型进行一个特殊的净化策略,必须使用形如 `[markup.sanitizer.asciidoc.rule-1]` 的方式来命名 section。
如果此规则没有匹配到任何渲染类型,它将会被应用到所有的渲染类型。
## Time (`time`)
@@ -437,6 +446,7 @@ Repository archive 的存储配置。 如果 `STORAGE_TYPE` 为空,则此配
- `PROXY_HOSTS`: **\**: 逗号分隔的多个需要代理的网址,支持 * 号匹配符号, ** 表示匹配所有网站
i.e.
+
```ini
PROXY_ENABLED = true
PROXY_URL = socks://127.0.0.1:1080
diff --git a/docs/content/doc/advanced/customizing-gitea.en-us.md b/docs/content/doc/advanced/customizing-gitea.en-us.md
index 1a8386fc3e1d0..18fc1b3e73d16 100644
--- a/docs/content/doc/advanced/customizing-gitea.en-us.md
+++ b/docs/content/doc/advanced/customizing-gitea.en-us.md
@@ -60,14 +60,15 @@ the url `http://gitea.domain.tld/assets/image.png`.
## Changing the logo
-To build a custom logo clone the Gitea source repository, replace `assets/logo.svg` and run
-`make generate-images`. This will update below output files which you can then place in `$GITEA_CUSTOM/public/img` on your server:
+To build a custom logo and/or favicon clone the Gitea source repository, replace `assets/logo.svg` and/or `assets/favicon.svg` and run
+`make generate-images`. `assets/favicon.svg` is used for the favicon only. This will update below output files which you can then place in `$GITEA_CUSTOM/public/img` on your server:
-- `public/img/logo.svg` - Used for favicon, site icon, app icon
+- `public/img/logo.svg` - Used for site icon, app icon
- `public/img/logo.png` - Used for Open Graph
-- `public/img/favicon.png` - Used as fallback for browsers that don't support SVG favicons
- `public/img/avatar_default.png` - Used as the default avatar image
- `public/img/apple-touch-icon.png` - Used on iOS devices for bookmarks
+- `public/img/favicon.svg` - Used for favicon
+- `public/img/favicon.png` - Used as fallback for browsers that don't support SVG favicons
In case the source image is not in vector format, you can attempt to convert a raster image using tools like [this](https://www.aconvert.com/image/png-to-svg/).
@@ -120,7 +121,7 @@ Apart from `extra_links.tmpl` and `extra_tabs.tmpl`, there are other useful temp
- `body_inner_pre.tmpl`, before the top navigation bar, but already inside the main container ``.
- `body_inner_post.tmpl`, before the end of the main container.
- `body_outer_post.tmpl`, before the bottom `
` element.
-- `footer.tmpl`, right before the end of the `` tag, a good place for additional Javascript.
+- `footer.tmpl`, right before the end of the `` tag, a good place for additional JavaScript.
#### Example: PlantUML
@@ -128,30 +129,33 @@ You can add [PlantUML](https://plantuml.com/) support to Gitea's markdown by usi
The data is encoded and sent to the PlantUML server which generates the picture. There is an online
demo server at http://www.plantuml.com/plantuml, but if you (or your users) have sensitive data you
can set up your own [PlantUML server](https://plantuml.com/server) instead. To set up PlantUML rendering,
-copy javascript files from https://gitea.com/davidsvantesson/plantuml-code-highlight and put them in your
+copy JavaScript files from https://gitea.com/davidsvantesson/plantuml-code-highlight and put them in your
`$GITEA_CUSTOM/public` folder. Then add the following to `custom/footer.tmpl`:
```html
-{{if .RequireHighlightJS}}
-
-
-
-{{end}}
```
You can then add blocks like the following to your markdown:
- ```plantuml
- Alice -> Bob: Authentication Request
- Bob --> Alice: Authentication Response
+```plantuml
+Alice -> Bob: Authentication Request
+Bob --> Alice: Authentication Response
- Alice -> Bob: Another authentication Request
- Alice <-- Bob: Another authentication Response
- ```
+Alice -> Bob: Another authentication Request
+Alice <-- Bob: Another authentication Response
+```
The script will detect tags with `class="language-plantuml"`, but you can change this by providing a second argument to `parsePlantumlCodeBlocks`.
@@ -198,7 +202,7 @@ You can display STL file directly in Gitea by adding:
to the file `templates/custom/footer.tmpl`
-You also need to download the content of the library [Madeleine.js](https://jinjunho.github.io/Madeleine.js/) and place it under `$GITEA_CUSTOM/public/` folder.
+You also need to download the content of the library [Madeleine.js](https://github.com/beige90/Madeleine.js) and place it under `$GITEA_CUSTOM/public/` folder.
You should end-up with a folder structure similar to:
@@ -331,8 +335,8 @@ The list of themes a user can choose from can be configured with the `THEMES` va
To make a custom theme available to all users:
-1. Add a CSS file to `$GITEA_PUBLIC/public/css/theme-.css`.
- The value of `$GITEA_PUBLIC` of your instance can be queried by calling `gitea help` and looking up the value of "CustomPath".
+1. Add a CSS file to `$GITEA_CUSTOM/public/css/theme-.css`.
+ The value of `$GITEA_CUSTOM` of your instance can be queried by calling `gitea help` and looking up the value of "CustomPath".
2. Add `` to the comma-separated list of setting `THEMES` in `app.ini`
Community themes are listed in [gitea/awesome-gitea#themes](https://gitea.com/gitea/awesome-gitea#themes).
diff --git a/docs/content/doc/advanced/environment-variables.zh-cn.md b/docs/content/doc/advanced/environment-variables.zh-cn.md
index 63ee4c69357f0..3de8dcfbc7418 100644
--- a/docs/content/doc/advanced/environment-variables.zh-cn.md
+++ b/docs/content/doc/advanced/environment-variables.zh-cn.md
@@ -25,31 +25,31 @@ GITEA_CUSTOM=/home/gitea/custom ./gitea web
因为 Gitea 使用 Go 语言编写,因此它使用了一些相关的 Go 的配置参数:
- * `GOOS`
- * `GOARCH`
- * [`GOPATH`](https://golang.org/cmd/go/#hdr-GOPATH_environment_variable)
+* `GOOS`
+* `GOARCH`
+* [`GOPATH`](https://golang.org/cmd/go/#hdr-GOPATH_environment_variable)
您可以在[官方文档](https://golang.org/cmd/go/#hdr-Environment_variables)中查阅这些配置参数的详细信息。
## Gitea 的文件目录
- * `GITEA_WORK_DIR`:工作目录的绝对路径
- * `GITEA_CUSTOM`:默认情况下 Gitea 使用默认目录 `GITEA_WORK_DIR`/custom,您可以使用这个参数来配置 *custom* 目录
- * `GOGS_WORK_DIR`: 已废弃,请使用 `GITEA_WORK_DIR` 替代
- * `GOGS_CUSTOM`: 已废弃,请使用 `GITEA_CUSTOM` 替代
+* `GITEA_WORK_DIR`:工作目录的绝对路径
+* `GITEA_CUSTOM`:默认情况下 Gitea 使用默认目录 `GITEA_WORK_DIR`/custom,您可以使用这个参数来配置 *custom* 目录
+* `GOGS_WORK_DIR`: 已废弃,请使用 `GITEA_WORK_DIR` 替代
+* `GOGS_CUSTOM`: 已废弃,请使用 `GITEA_CUSTOM` 替代
## 操作系统配置
- * `USER`:Gitea 运行时使用的系统用户,它将作为一些 repository 的访问地址的一部分
- * `USERNAME`: 如果没有配置 `USER`, Gitea 将使用 `USERNAME`
- * `HOME`: 用户的 home 目录,在 Windows 中会使用 `USERPROFILE` 环境变量
+* `USER`:Gitea 运行时使用的系统用户,它将作为一些 repository 的访问地址的一部分
+* `USERNAME`: 如果没有配置 `USER`, Gitea 将使用 `USERNAME`
+* `HOME`: 用户的 home 目录,在 Windows 中会使用 `USERPROFILE` 环境变量
### 仅限于 Windows 的配置
- * `USERPROFILE`: 用户的主目录,如果未配置则会使用 `HOMEDRIVE` + `HOMEPATH`
- * `HOMEDRIVE`: 用于访问 home 目录的主驱动器路径(C盘)
- * `HOMEPATH`:在指定主驱动器下的 home 目录相对路径
+* `USERPROFILE`: 用户的主目录,如果未配置则会使用 `HOMEDRIVE` + `HOMEPATH`
+* `HOMEDRIVE`: 用于访问 home 目录的主驱动器路径(C盘)
+* `HOMEPATH`:在指定主驱动器下的 home 目录相对路径
## Miscellaneous
- * `SKIP_MINWINSVC`:如果设置为 1,在 Windows 上不会以 service 的形式运行。
+* `SKIP_MINWINSVC`:如果设置为 1,在 Windows 上不会以 service 的形式运行。
diff --git a/docs/content/doc/advanced/external-renderers.en-us.md b/docs/content/doc/advanced/external-renderers.en-us.md
index 34329408a1d5b..244fcd7c83983 100644
--- a/docs/content/doc/advanced/external-renderers.en-us.md
+++ b/docs/content/doc/advanced/external-renderers.en-us.md
@@ -74,12 +74,13 @@ RENDER_COMMAND = "timeout 30s pandoc +RTS -M512M -RTS -f rst"
IS_INPUT_FILE = false
```
-If your external markup relies on additional classes and attributes on the generated HTML elements, you might need to enable custom sanitizer policies. Gitea uses the [`bluemonday`](https://godoc.org/github.com/microcosm-cc/bluemonday) package as our HTML sanitizier. The example below will support [KaTeX](https://katex.org/) output from [`pandoc`](https://pandoc.org/).
+If your external markup relies on additional classes and attributes on the generated HTML elements, you might need to enable custom sanitizer policies. Gitea uses the [`bluemonday`](https://godoc.org/github.com/microcosm-cc/bluemonday) package as our HTML sanitizer. The example below could be used to support server-side [KaTeX](https://katex.org/) rendering output from [`pandoc`](https://pandoc.org/).
```ini
[markup.sanitizer.TeX]
; Pandoc renders TeX segments as s with the "math" class, optionally
; with "inline" or "display" classes depending on context.
+; - note this is different from the built-in math support in our markdown parser which uses
ELEMENT = span
ALLOW_ATTR = class
REGEXP = ^\s*((math(\s+|$)|inline(\s+|$)|display(\s+|$)))+
@@ -127,6 +128,7 @@ ALLOW_ATTR = class
### Example: Office DOCX
Display Office DOCX files with [`pandoc`](https://pandoc.org/):
+
```ini
[markup.docx]
ENABLED = true
@@ -138,6 +140,7 @@ ALLOW_DATA_URI_IMAGES = true
```
The template file has the following content:
+
```
$body$
```
@@ -145,6 +148,7 @@ $body$
### Example: Jupyter Notebook
Display Jupyter Notebook files with [`nbconvert`](https://github.com/jupyter/nbconvert):
+
```ini
[markup.jupyter]
ENABLED = true
@@ -156,9 +160,11 @@ ALLOW_DATA_URI_IMAGES = true
```
## Customizing CSS
-The external renderer is specified in the .ini in the format `[markup.XXXXX]` and the HTML supplied by your external renderer will be wrapped in a `` with classes `markup` and `XXXXX`. The `markup` class provides out of the box styling (as does `markdown` if `XXXXX` is `markdown`). Otherwise you can use these classes to specifically target the contents of your rendered HTML.
+
+The external renderer is specified in the .ini in the format `[markup.XXXXX]` and the HTML supplied by your external renderer will be wrapped in a `
` with classes `markup` and `XXXXX`. The `markup` class provides out of the box styling (as does `markdown` if `XXXXX` is `markdown`). Otherwise you can use these classes to specifically target the contents of your rendered HTML.
And so you could write some CSS:
+
```css
.markup.XXXXX html {
font-size: 100%;
@@ -184,6 +190,7 @@ And so you could write some CSS:
```
Add your stylesheet to your custom directory e.g `custom/public/css/my-style-XXXXX.css` and import it using a custom header file `custom/templates/custom/header.tmpl`:
+
```html
-
+
```
diff --git a/docs/content/doc/advanced/hacking-on-gitea.zh-cn.md b/docs/content/doc/advanced/hacking-on-gitea.zh-cn.md
deleted file mode 100644
index 7ad8019c5e6a3..0000000000000
--- a/docs/content/doc/advanced/hacking-on-gitea.zh-cn.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-date: "2016-12-01T16:00:00+02:00"
-title: "加入 Gitea 开源"
-slug: "hacking-on-gitea"
-weight: 10
-toc: false
-draft: false
-menu:
- sidebar:
- parent: "advanced"
- name: "加入 Gitea 开源"
- weight: 10
- identifier: "hacking-on-gitea"
----
-
-# Hacking on Gitea
-
-首先你需要一些运行环境,这和 [从源代码安装]({{< relref "from-source.zh-cn.md" >}}) 相同,如果你还没有设置好,可以先阅读那个章节。
-
-如果你想为 Gitea 贡献代码,你需要 Fork 这个项目并且以 `master` 为开发分支。Gitea 使用 Govendor
-来管理依赖,因此所有依赖项都被工具自动 copy 在 vendor 子目录下。用下面的命令来下载源码:
-
-```
-go get -d code.gitea.io/gitea
-```
-
-然后你可以在 Github 上 fork [Gitea 项目](https://github.com/go-gitea/gitea),之后可以通过下面的命令进入源码目录:
-
-```
-cd $GOPATH/src/code.gitea.io/gitea
-```
-
-要创建 pull requests 你还需要在源码中新增一个 remote 指向你 Fork 的地址,直接推送到 origin 的话会告诉你没有写权限:
-
-```
-git remote rename origin upstream
-git remote add origin git@github.com:
/gitea.git
-git fetch --all --prune
-```
-
-然后你就可以开始开发了。你可以看一下 `Makefile` 的内容。`make test` 可以运行测试程序, `make build` 将生成一个 `gitea` 可运行文件在根目录。如果你的提交比较复杂,尽量多写一些单元测试代码。
-
-好了,到这里你已经设置好了所有的开发 Gitea 所需的环境。欢迎成为 Gitea 的 Contributor。
diff --git a/docs/content/doc/advanced/logging-documentation.en-us.md b/docs/content/doc/advanced/logging-documentation.en-us.md
index bdde5bd8c4d74..145c8c320c78d 100644
--- a/docs/content/doc/advanced/logging-documentation.en-us.md
+++ b/docs/content/doc/advanced/logging-documentation.en-us.md
@@ -15,12 +15,182 @@ menu:
# Logging Configuration
-The logging framework has been revamped in Gitea 1.9.0.
+The logging configuration of Gitea mainly consists of 3 types of components:
+
+- The `[log]` section for general configuration
+- `[log.]` sections for the configuration of different log outputs
+- `[log..]` sections for output specific configuration of a log group
+
+As mentioned below, there is a fully functional log output by default, so it is not necessary to define one.
**Table of Contents**
{{< toc >}}
+## Collecting Logs for Help
+
+To collect logs for help and issue report, see [Support Options]({{< relref "doc/help/seek-help.en-us.md" >}}).
+
+## The `[log]` section
+
+Configuration of logging facilities in Gitea happen in the `[log]` section and it's subsections.
+
+In the top level `[log]` section the following configurations can be placed:
+
+- `ROOT_PATH`: (Default: **%(GITEA_WORK_DIR)/log**): Base path for log files
+- `MODE`: (Default: **console**) List of log outputs to use for the Default logger.
+- `ROUTER`: (Default: **console**): List of log outputs to use for the Router logger.
+- `ACCESS`: List of log outputs to use for the Access logger.
+- `XORM`: (Default: **,**) List of log outputs to use for the XORM logger.
+- `ENABLE_ACCESS_LOG`: (Default: **false**): whether the Access logger is allowed to emit logs
+- `ENABLE_XORM_LOG`: (Default: **true**): whether the XORM logger is allowed to emit logs
+
+For details on the loggers check the "Log Groups" section.
+Important: log outputs won't be used if you don't enable them for the desired loggers in the corresponding list value.
+
+Lists are specified as comma separated values. This format also works in subsection.
+
+This section may be used for defining default values for subsections.
+Examples:
+
+- `LEVEL`: (Default: **Info**) Least severe log events to persist. Case insensitive. The full list of levels as of v1.17.3 can be read [here](https://github.com/go-gitea/gitea/blob/v1.17.3/custom/conf/app.example.ini#L507).
+- `STACKTRACE_LEVEL`: (Default: **None**) For this and more severe events the stacktrace will be printed upon getting logged.
+
+Some values are not inherited by subsections. For details see the "Non-inherited default values" section.
+
+## Log outputs
+
+Log outputs are the targets to which log messages will be sent.
+The content and the format of the log messages to be saved can be configured in these.
+
+Log outputs are also called subloggers.
+
+Gitea provides 4 possible log outputs:
+
+- `console` - Log to `os.Stdout` or `os.Stderr`
+- `file` - Log to a file
+- `conn` - Log to a socket (network or unix)
+- `smtp` - Log via email
+
+By default, Gitea has a `console` output configured, which is used by the loggers as seen in the section "The log section" above.
+
+### Common configuration
+
+Certain configuration is common to all modes of log output:
+
+- `MODE` is the mode of the log output. It will default to the sublogger
+ name, thus `[log.console.router]` will default to `MODE = console`.
+ For mode specific confgurations read further.
+- `LEVEL` is the lowest level that this output will log. This value
+ is inherited from `[log]` and in the case of the non-default loggers
+ from `[log.sublogger]`.
+- `STACKTRACE_LEVEL` is the lowest level that this output will print
+ a stacktrace. This value is inherited.
+- `COLORIZE` will default to `true` for `console` as
+ described, otherwise it will default to `false`.
+
+### Non-inherited default values
+
+There are several values which are not inherited as described above but
+rather default to those specific to type of logger, these are:
+`EXPRESSION`, `FLAGS`, `PREFIX` and `FILE_NAME`.
+
+#### `EXPRESSION`
+
+`EXPRESSION` represents a regular expression that log events must match to be logged by the sublogger. Either the log message, (with colors removed), must match or the `longfilename:linenumber:functionname` must match. NB: the whole message or string doesn't need to completely match.
+
+Please note this expression will be run in the sublogger's goroutine
+not the logging event subroutine. Therefore it can be complicated.
+
+#### `FLAGS`
+
+`FLAGS` represents the preceding logging context information that is
+printed before each message. It is a comma-separated string set. The order of values does not matter.
+
+Possible values are:
+
+- `none` or `,` - No flags.
+- `date` - the date in the local time zone: `2009/01/23`.
+- `time` - the time in the local time zone: `01:23:23`.
+- `microseconds` - microsecond resolution: `01:23:23.123123`. Assumes
+ time.
+- `longfile` - full file name and line number: `/a/b/c/d.go:23`.
+- `shortfile` - final file name element and line number: `d.go:23`.
+- `funcname` - function name of the caller: `runtime.Caller()`.
+- `shortfuncname` - last part of the function name. Overrides
+ `funcname`.
+- `utc` - if date or time is set, use UTC rather than the local time
+ zone.
+- `levelinitial` - Initial character of the provided level in brackets eg. `[I]` for info.
+- `level` - Provided level in brackets `[INFO]`
+- `medfile` - Last 20 characters of the filename - equivalent to
+ `shortfile,longfile`.
+- `stdflags` - Equivalent to `date,time,medfile,shortfuncname,levelinitial`
+
+### Console mode
+
+In this mode the logger will forward log messages to the stdout and
+stderr streams attached to the Gitea process.
+
+For loggers in console mode, `COLORIZE` will default to `true` if not
+on windows, or the windows terminal can be set into ANSI mode or is a
+cygwin or Msys pipe.
+
+Settings:
+
+- `STDERR`: **false**: Whether the logger should print to `stderr` instead of `stdout`.
+
+### File mode
+
+In this mode the logger will save log messages to a file.
+
+Settings:
+
+- `FILE_NAME`: The file to write the log events to. For details see below.
+- `MAX_SIZE_SHIFT`: **28**: Maximum size shift of a single file. 28 represents 256Mb. For details see below.
+- `LOG_ROTATE` **true**: Whether to rotate the log files. TODO: if false, will it delete instead on daily rotate, or do nothing?.
+- `DAILY_ROTATE`: **true**: Whether to rotate logs daily.
+- `MAX_DAYS`: **7**: Delete rotated log files after this number of days.
+- `COMPRESS`: **true**: Whether to compress old log files by default with gzip.
+- `COMPRESSION_LEVEL`: **-1**: Compression level. For details see below.
+
+The default value of `FILE_NAME` depends on the respective logger facility.
+If unset, their own default will be used.
+If set it will be relative to the provided `ROOT_PATH` in the master `[log]` section.
+
+`MAX_SIZE_SHIFT` defines the maximum size of a file by left shifting 1 the given number of times (`1 << x`).
+The exact behavior at the time of v1.17.3 can be seen [here](https://github.com/go-gitea/gitea/blob/v1.17.3/modules/setting/log.go#L185).
+
+The useful values of `COMPRESSION_LEVEL` are from 1 to (and including) 9, where higher numbers mean better compression.
+Beware that better compression might come with higher resource usage.
+Must be preceded with a `-` sign.
+
+### Conn mode
+
+In this mode the logger will send log messages over a network socket.
+
+Settings:
+
+- `ADDR`: **:7020**: Sets the address to connect to.
+- `PROTOCOL`: **tcp**: Set the protocol, either "tcp", "unix" or "udp".
+- `RECONNECT`: **false**: Try to reconnect when connection is lost.
+- `RECONNECT_ON_MSG`: **false**: Reconnect host for every single message.
+
+### SMTP mode
+
+In this mode the logger will send log messages in email.
+
+It is not recommended to use this logger to send general logging
+messages. However, you could perhaps set this logger to work on `FATAL` messages only.
+
+Settings:
+
+- `HOST`: **127.0.0.1:25**: The SMTP host to connect to.
+- `USER`: User email address to send from.
+- `PASSWD`: Password for the smtp server.
+- `RECEIVERS`: Email addresses to send to.
+- `SUBJECT`: **Diagnostic message from Gitea**. The content of the email's subject field.
+
## Log Groups
The fundamental thing to be aware of in Gitea is that there are several
@@ -170,106 +340,6 @@ which will not be inherited from the `[log]` or relevant
- `EXPRESSION` will default to `""`
- `PREFIX` will default to `""`
-## Log outputs
-
-Gitea provides 4 possible log outputs:
-
-- `console` - Log to `os.Stdout` or `os.Stderr`
-- `file` - Log to a file
-- `conn` - Log to a keep-alive TCP connection
-- `smtp` - Log via email
-
-Certain configuration is common to all modes of log output:
-
-- `LEVEL` is the lowest level that this output will log. This value
- is inherited from `[log]` and in the case of the non-default loggers
- from `[log.sublogger]`.
-- `STACKTRACE_LEVEL` is the lowest level that this output will print
- a stacktrace. This value is inherited.
-- `MODE` is the mode of the log output. It will default to the sublogger
- name. Thus `[log.console.router]` will default to `MODE = console`.
-- `COLORIZE` will default to `true` for `console` as
- described, otherwise it will default to `false`.
-
-### Non-inherited default values
-
-There are several values which are not inherited as described above but
-rather default to those specific to type of logger, these are:
-`EXPRESSION`, `FLAGS`, `PREFIX` and `FILE_NAME`.
-
-#### `EXPRESSION`
-
-`EXPRESSION` represents a regular expression that log events must match to be logged by the sublogger. Either the log message, (with colors removed), must match or the `longfilename:linenumber:functionname` must match. NB: the whole message or string doesn't need to completely match.
-
-Please note this expression will be run in the sublogger's goroutine
-not the logging event subroutine. Therefore it can be complicated.
-
-#### `FLAGS`
-
-`FLAGS` represents the preceding logging context information that is
-printed before each message. It is a comma-separated string set. The order of values does not matter.
-
-Possible values are:
-
-- `none` or `,` - No flags.
-- `date` - the date in the local time zone: `2009/01/23`.
-- `time` - the time in the local time zone: `01:23:23`.
-- `microseconds` - microsecond resolution: `01:23:23.123123`. Assumes
- time.
-- `longfile` - full file name and line number: `/a/b/c/d.go:23`.
-- `shortfile` - final file name element and line number: `d.go:23`.
-- `funcname` - function name of the caller: `runtime.Caller()`.
-- `shortfuncname` - last part of the function name. Overrides
- `funcname`.
-- `utc` - if date or time is set, use UTC rather than the local time
- zone.
-- `levelinitial` - Initial character of the provided level in brackets eg. `[I]` for info.
-- `level` - Provided level in brackets `[INFO]`
-- `medfile` - Last 20 characters of the filename - equivalent to
- `shortfile,longfile`.
-- `stdflags` - Equivalent to `date,time,medfile,shortfuncname,levelinitial`
-
-### Console mode
-
-For loggers in console mode, `COLORIZE` will default to `true` if not
-on windows, or the windows terminal can be set into ANSI mode or is a
-cygwin or Msys pipe.
-
-If `STDERR` is set to `true` the logger will use `os.Stderr` instead of
-`os.Stdout`.
-
-### File mode
-
-The `FILE_NAME` defaults as described above. If set it will be relative
-to the provided `ROOT_PATH` in the master `[log]` section.
-
-Other values:
-
-- `LOG_ROTATE`: **true**: Rotate the log files.
-- `MAX_SIZE_SHIFT`: **28**: Maximum size shift of a single file, 28 represents 256Mb.
-- `DAILY_ROTATE`: **true**: Rotate logs daily.
-- `MAX_DAYS`: **7**: Delete the log file after n days
-- `COMPRESS`: **true**: Compress old log files by default with gzip
-- `COMPRESSION_LEVEL`: **-1**: Compression level
-
-### Conn mode
-
-- `RECONNECT_ON_MSG`: **false**: Reconnect host for every single message.
-- `RECONNECT`: **false**: Try to reconnect when connection is lost.
-- `PROTOCOL`: **tcp**: Set the protocol, either "tcp", "unix" or "udp".
-- `ADDR`: **:7020**: Sets the address to connect to.
-
-### SMTP mode
-
-It is not recommended to use this logger to send general logging
-messages. However, you could perhaps set this logger to work on `FATAL`.
-
-- `USER`: User email address to send from.
-- `PASSWD`: Password for the smtp server.
-- `HOST`: **127.0.0.1:25**: The SMTP host to connect to.
-- `RECEIVERS`: Email addresses to send to.
-- `SUBJECT`: **Diagnostic message from Gitea**
-
## Debugging problems
When submitting logs in Gitea issues it is often helpful to submit
@@ -349,7 +419,7 @@ recommended that pausing only done for a very short period of time.
It is possible to add and remove logging whilst Gitea is running using the `gitea manager logging add` and `remove` subcommands.
This functionality can only adjust running log systems and cannot be used to start the access or router loggers if they
-were not already initialised. If you wish to start these systems you are advised to adjust the app.ini and (gracefully) restart
+were not already initialized. If you wish to start these systems you are advised to adjust the app.ini and (gracefully) restart
the Gitea service.
The main intention of these commands is to easily add a temporary logger to investigate problems on running systems where a restart
diff --git a/docs/content/doc/advanced/mail-templates-us.md b/docs/content/doc/advanced/mail-templates-us.md
deleted file mode 100644
index e73bb01e299db..0000000000000
--- a/docs/content/doc/advanced/mail-templates-us.md
+++ /dev/null
@@ -1,280 +0,0 @@
----
-date: "2019-10-23T17:00:00-03:00"
-title: "Mail templates"
-slug: "mail-templates"
-weight: 45
-toc: false
-draft: false
-menu:
- sidebar:
- parent: "advanced"
- name: "Mail templates"
- weight: 45
- identifier: "mail-templates"
----
-
-# Mail templates
-
-**Table of Contents**
-
-{{< toc >}}
-
-To craft the e-mail subject and contents for certain operations, Gitea can be customized by using templates. The templates
-for these functions are located under the [`custom` directory](https://docs.gitea.io/en-us/customizing-gitea/).
-Gitea has an internal template that serves as default in case there's no custom alternative.
-
-Custom templates are loaded when Gitea starts. Changes made to them are not recognized until Gitea is restarted again.
-
-## Mail notifications supporting templates
-
-Currently, the following notification events make use of templates:
-
-| Action name | Usage |
-| ----------- | ------------------------------------------------------------------------------------------------------------ |
-| `new` | A new issue or pull request was created. |
-| `comment` | A new comment was created in an existing issue or pull request. |
-| `close` | An issue or pull request was closed. |
-| `reopen` | An issue or pull request was reopened. |
-| `review` | The head comment of a review in a pull request. |
-| `approve` | The head comment of a approving review for a pull request. |
-| `reject` | The head comment of a review requesting changes for a pull request. |
-| `code` | A single comment on the code of a pull request. |
-| `assigned` | Used was assigned to an issue or pull request. |
-| `default` | Any action not included in the above categories, or when the corresponding category template is not present. |
-
-The path for the template of a particular message type is:
-
-```sh
-custom/templates/mail/{action type}/{action name}.tmpl
-```
-
-Where `{action type}` is one of `issue` or `pull` (for pull requests), and `{action name}` is one of the names listed above.
-
-For example, the specific template for a mail regarding a comment in a pull request is:
-
-```sh
-custom/templates/mail/pull/comment.tmpl
-```
-
-However, creating templates for each and every action type/name combination is not required.
-A fallback system is used to choose the appropriate template for an event. The _first existing_
-template on this list is used:
-
-- The specific template for the desired **action type** and **action name**.
-- The template for action type `issue` and the desired **action name**.
-- The template for the desired **action type**, action name `default`.
-- The template for action type `issue`, action name `default`.
-
-The only mandatory template is action type `issue`, action name `default`, which is already embedded in Gitea
-unless it's overridden by the user in the `custom` directory.
-
-## Template syntax
-
-Mail templates are UTF-8 encoded text files that need to follow one of the following formats:
-
-```
-Text and macros for the subject line
-------------
-Text and macros for the mail body
-```
-
-or
-
-```
-Text and macros for the mail body
-```
-
-Specifying a _subject_ section is optional (and therefore also the dash line separator). When used, the separator between
-_subject_ and _mail body_ templates requires at least three dashes; no other characters are allowed in the separator line.
-
-_Subject_ and _mail body_ are parsed by [Golang's template engine](https://golang.org/pkg/text/template/) and
-are provided with a _metadata context_ assembled for each notification. The context contains the following elements:
-
-| Name | Type | Available | Usage |
-| ------------------ | ---------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `.FallbackSubject` | string | Always | A default subject line. See Below. |
-| `.Subject` | string | Only in body | The _subject_, once resolved. |
-| `.Body` | string | Always | The message of the issue, pull request or comment, parsed from Markdown into HTML and sanitized. Do not confuse with the _mail body_. |
-| `.Link` | string | Always | The address of the originating issue, pull request or comment. |
-| `.Issue` | models.Issue | Always | The issue (or pull request) originating the notification. To get data specific to a pull request (e.g. `HasMerged`), `.Issue.PullRequest` can be used, but care should be taken as this field will be `nil` if the issue is _not_ a pull request. |
-| `.Comment` | models.Comment | If applicable | If the notification is from a comment added to an issue or pull request, this will contain the information about the comment. |
-| `.IsPull` | bool | Always | `true` if the mail notification is associated with a pull request (i.e. `.Issue.PullRequest` is not `nil`). |
-| `.Repo` | string | Always | Name of the repository, including owner name (e.g. `mike/stuff`) |
-| `.User` | models.User | Always | Owner of the repository from which the event originated. To get the user name (e.g. `mike`),`.User.Name` can be used. |
-| `.Doer` | models.User | Always | User that executed the action triggering the notification event. To get the user name (e.g. `rhonda`), `.Doer.Name` can be used. |
-| `.IsMention` | bool | Always | `true` if this notification was only generated because the user was mentioned in the comment, while not being subscribed to the source. It will be `false` if the recipient was subscribed to the issue or repository. |
-| `.SubjectPrefix` | string | Always | `Re: ` if the notification is about other than issue or pull request creation; otherwise an empty string. |
-| `.ActionType` | string | Always | `"issue"` or `"pull"`. Will correspond to the actual _action type_ independently of which template was selected. |
-| `.ActionName` | string | Always | It will be one of the action types described above (`new`, `comment`, etc.), and will correspond to the actual _action name_ independently of which template was selected. |
-| `.ReviewComments` | []models.Comment | Always | List of code comments in a review. The comment text will be in `.RenderedContent` and the referenced code will be in `.Patch`. |
-
-All names are case sensitive.
-
-### The _subject_ part of the template
-
-The template engine used for the mail _subject_ is golang's [`text/template`](https://golang.org/pkg/text/template/).
-Please refer to the linked documentation for details about its syntax.
-
-The _subject_ is built using the following steps:
-
-- A template is selected according to the type of notification and to what templates are present.
-- The template is parsed and resolved (e.g. `{{.Issue.Index}}` is converted to the number of the issue
- or pull request).
-- All space-like characters (e.g. `TAB`, `LF`, etc.) are converted to normal spaces.
-- All leading, trailing and redundant spaces are removed.
-- The string is truncated to its first 256 runes (characters).
-
-If the end result is an empty string, **or** no subject template was available (i.e. the selected template
-did not include a subject part), Gitea's **internal default** will be used.
-
-The internal default (fallback) subject is the equivalent of:
-
-```sh
-{{.SubjectPrefix}}[{{.Repo}}] {{.Issue.Title}} (#{{.Issue.Index}})
-```
-
-For example: `Re: [mike/stuff] New color palette (#38)`
-
-Gitea's default subject can also be found in the template _metadata_ as `.FallbackSubject` from any of
-the two templates, even if a valid subject template is present.
-
-### The _mail body_ part of the template
-
-The template engine used for the _mail body_ is golang's [`html/template`](https://golang.org/pkg/html/template/).
-Please refer to the linked documentation for details about its syntax.
-
-The _mail body_ is parsed after the mail subject, so there is an additional _metadata_ field which is
-the actual rendered subject, after all considerations.
-
-The expected result is HTML (including structural elements like``, ``, etc.). Styling
-through `
\ No newline at end of file
diff --git a/public/img/svg/gitea-python.svg b/public/img/svg/gitea-python.svg
index 07548897e6151..62ec452aeb842 100644
--- a/public/img/svg/gitea-python.svg
+++ b/public/img/svg/gitea-python.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/img/svg/gitea-rubygems.svg b/public/img/svg/gitea-rubygems.svg
index 5f54dce48dd92..0747f8585c4fa 100644
--- a/public/img/svg/gitea-rubygems.svg
+++ b/public/img/svg/gitea-rubygems.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/img/svg/gitea-split.svg b/public/img/svg/gitea-split.svg
new file mode 100644
index 0000000000000..f819255cca68f
--- /dev/null
+++ b/public/img/svg/gitea-split.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/gitea-vagrant.svg b/public/img/svg/gitea-vagrant.svg
new file mode 100644
index 0000000000000..4c1b78cab5402
--- /dev/null
+++ b/public/img/svg/gitea-vagrant.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/gitea-whitespace.svg b/public/img/svg/gitea-whitespace.svg
new file mode 100644
index 0000000000000..6b34f33736ff4
--- /dev/null
+++ b/public/img/svg/gitea-whitespace.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-accessibility-inset.svg b/public/img/svg/octicon-accessibility-inset.svg
new file mode 100644
index 0000000000000..ec303f9cb2d3d
--- /dev/null
+++ b/public/img/svg/octicon-accessibility-inset.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-alert-fill.svg b/public/img/svg/octicon-alert-fill.svg
new file mode 100644
index 0000000000000..34795cfbe4e9e
--- /dev/null
+++ b/public/img/svg/octicon-alert-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-arrow-down-left.svg b/public/img/svg/octicon-arrow-down-left.svg
new file mode 100644
index 0000000000000..f02bd330c9948
--- /dev/null
+++ b/public/img/svg/octicon-arrow-down-left.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-arrow-down-right.svg b/public/img/svg/octicon-arrow-down-right.svg
new file mode 100644
index 0000000000000..dff9d36c1b4a3
--- /dev/null
+++ b/public/img/svg/octicon-arrow-down-right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-arrow-up-left.svg b/public/img/svg/octicon-arrow-up-left.svg
new file mode 100644
index 0000000000000..0f5a452b8a209
--- /dev/null
+++ b/public/img/svg/octicon-arrow-up-left.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-arrow-up-right.svg b/public/img/svg/octicon-arrow-up-right.svg
new file mode 100644
index 0000000000000..896c9b60b46b4
--- /dev/null
+++ b/public/img/svg/octicon-arrow-up-right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-cache.svg b/public/img/svg/octicon-cache.svg
new file mode 100644
index 0000000000000..20b14138d910d
--- /dev/null
+++ b/public/img/svg/octicon-cache.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-checkbox.svg b/public/img/svg/octicon-checkbox.svg
new file mode 100644
index 0000000000000..f0313bc747fc9
--- /dev/null
+++ b/public/img/svg/octicon-checkbox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-clock-fill.svg b/public/img/svg/octicon-clock-fill.svg
new file mode 100644
index 0000000000000..5fcfb477c4294
--- /dev/null
+++ b/public/img/svg/octicon-clock-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-cloud-offline.svg b/public/img/svg/octicon-cloud-offline.svg
new file mode 100644
index 0000000000000..7d34b8dff69fc
--- /dev/null
+++ b/public/img/svg/octicon-cloud-offline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-cloud.svg b/public/img/svg/octicon-cloud.svg
new file mode 100644
index 0000000000000..fddfd2e42483f
--- /dev/null
+++ b/public/img/svg/octicon-cloud.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-command-palette.svg b/public/img/svg/octicon-command-palette.svg
new file mode 100644
index 0000000000000..92fcd63149510
--- /dev/null
+++ b/public/img/svg/octicon-command-palette.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-copilot-error.svg b/public/img/svg/octicon-copilot-error.svg
new file mode 100644
index 0000000000000..97c88a12bf1a9
--- /dev/null
+++ b/public/img/svg/octicon-copilot-error.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-copilot-warning.svg b/public/img/svg/octicon-copilot-warning.svg
new file mode 100644
index 0000000000000..32bd7361aeddf
--- /dev/null
+++ b/public/img/svg/octicon-copilot-warning.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-copilot.svg b/public/img/svg/octicon-copilot.svg
new file mode 100644
index 0000000000000..1a99f1f5f988f
--- /dev/null
+++ b/public/img/svg/octicon-copilot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-file-added.svg b/public/img/svg/octicon-file-added.svg
new file mode 100644
index 0000000000000..c1ad23d296e1f
--- /dev/null
+++ b/public/img/svg/octicon-file-added.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-file-directory-open-fill.svg b/public/img/svg/octicon-file-directory-open-fill.svg
new file mode 100644
index 0000000000000..50809936e2749
--- /dev/null
+++ b/public/img/svg/octicon-file-directory-open-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-file-moved.svg b/public/img/svg/octicon-file-moved.svg
new file mode 100644
index 0000000000000..2e26b4cfd2c92
--- /dev/null
+++ b/public/img/svg/octicon-file-moved.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-file-removed.svg b/public/img/svg/octicon-file-removed.svg
new file mode 100644
index 0000000000000..43c07295dd084
--- /dev/null
+++ b/public/img/svg/octicon-file-removed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-file.svg b/public/img/svg/octicon-file.svg
index 4e05d6feb5c41..4037afad5ef67 100644
--- a/public/img/svg/octicon-file.svg
+++ b/public/img/svg/octicon-file.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-git-merge-queue.svg b/public/img/svg/octicon-git-merge-queue.svg
new file mode 100644
index 0000000000000..17d7767b058cf
--- /dev/null
+++ b/public/img/svg/octicon-git-merge-queue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-goal.svg b/public/img/svg/octicon-goal.svg
new file mode 100644
index 0000000000000..b715f4bbd935e
--- /dev/null
+++ b/public/img/svg/octicon-goal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-issue-tracked-by.svg b/public/img/svg/octicon-issue-tracked-by.svg
new file mode 100644
index 0000000000000..d4352a943007c
--- /dev/null
+++ b/public/img/svg/octicon-issue-tracked-by.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-issue-tracked-in.svg b/public/img/svg/octicon-issue-tracked-in.svg
new file mode 100644
index 0000000000000..8910bd7a24a51
--- /dev/null
+++ b/public/img/svg/octicon-issue-tracked-in.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-paperclip.svg b/public/img/svg/octicon-paperclip.svg
new file mode 100644
index 0000000000000..ddae143818ffe
--- /dev/null
+++ b/public/img/svg/octicon-paperclip.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-project-roadmap.svg b/public/img/svg/octicon-project-roadmap.svg
new file mode 100644
index 0000000000000..afc3b223bd5db
--- /dev/null
+++ b/public/img/svg/octicon-project-roadmap.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-project-symlink.svg b/public/img/svg/octicon-project-symlink.svg
new file mode 100644
index 0000000000000..2c889a090cf16
--- /dev/null
+++ b/public/img/svg/octicon-project-symlink.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-read.svg b/public/img/svg/octicon-read.svg
new file mode 100644
index 0000000000000..bc275f50bf53c
--- /dev/null
+++ b/public/img/svg/octicon-read.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-rel-file-path.svg b/public/img/svg/octicon-rel-file-path.svg
new file mode 100644
index 0000000000000..e8c9899799cfa
--- /dev/null
+++ b/public/img/svg/octicon-rel-file-path.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-shield-slash.svg b/public/img/svg/octicon-shield-slash.svg
new file mode 100644
index 0000000000000..ba4db6776bf15
--- /dev/null
+++ b/public/img/svg/octicon-shield-slash.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-skip-fill.svg b/public/img/svg/octicon-skip-fill.svg
new file mode 100644
index 0000000000000..6606a56df9230
--- /dev/null
+++ b/public/img/svg/octicon-skip-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-sliders.svg b/public/img/svg/octicon-sliders.svg
new file mode 100644
index 0000000000000..4ad22ab84b6c7
--- /dev/null
+++ b/public/img/svg/octicon-sliders.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-sponsor-tiers.svg b/public/img/svg/octicon-sponsor-tiers.svg
new file mode 100644
index 0000000000000..dc200285f722a
--- /dev/null
+++ b/public/img/svg/octicon-sponsor-tiers.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-table.svg b/public/img/svg/octicon-table.svg
index 905b2bb9b4c26..5b80f78357b65 100644
--- a/public/img/svg/octicon-table.svg
+++ b/public/img/svg/octicon-table.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-tasklist.svg b/public/img/svg/octicon-tasklist.svg
index 81e41a87ced59..41b7c90f6de43 100644
--- a/public/img/svg/octicon-tasklist.svg
+++ b/public/img/svg/octicon-tasklist.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-unlink.svg b/public/img/svg/octicon-unlink.svg
new file mode 100644
index 0000000000000..7d72ef3a267e3
--- /dev/null
+++ b/public/img/svg/octicon-unlink.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/svg/octicon-unread.svg b/public/img/svg/octicon-unread.svg
new file mode 100644
index 0000000000000..bb3c1fc6ed72b
--- /dev/null
+++ b/public/img/svg/octicon-unread.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index f0251b95eb1f5..78eb5e860be26 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -1,27 +1,31 @@
// 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.
+// SPDX-License-Identifier: MIT
package packages
import (
+ gocontext "context"
"net/http"
"regexp"
"strings"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/packages/composer"
"code.gitea.io/gitea/routers/api/packages/conan"
"code.gitea.io/gitea/routers/api/packages/container"
"code.gitea.io/gitea/routers/api/packages/generic"
+ "code.gitea.io/gitea/routers/api/packages/helm"
"code.gitea.io/gitea/routers/api/packages/maven"
"code.gitea.io/gitea/routers/api/packages/npm"
"code.gitea.io/gitea/routers/api/packages/nuget"
+ "code.gitea.io/gitea/routers/api/packages/pub"
"code.gitea.io/gitea/routers/api/packages/pypi"
"code.gitea.io/gitea/routers/api/packages/rubygems"
+ "code.gitea.io/gitea/routers/api/packages/vagrant"
"code.gitea.io/gitea/services/auth"
context_service "code.gitea.io/gitea/services/context"
)
@@ -36,14 +40,17 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
}
}
-func Routes() *web.Route {
+// CommonRoutes provide endpoints for most package managers (except containers - see below)
+// These are mounted on `/api/packages` (not `/api/v1/packages`)
+func CommonRoutes(ctx gocontext.Context) *web.Route {
r := web.NewRoute()
- r.Use(context.PackageContexter())
+ r.Use(context.PackageContexter(ctx))
authMethods := []auth.Method{
&auth.OAuth2{},
&auth.Basic{},
+ &nuget.Auth{},
&conan.Auth{},
}
if setting.Service.EnableReverseProxyAuth {
@@ -52,7 +59,14 @@ func Routes() *web.Route {
authGroup := auth.NewGroup(authMethods...)
r.Use(func(ctx *context.Context) {
- ctx.Doer = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
+ var err error
+ ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
+ if err != nil {
+ log.Error("Verify: %v", err)
+ ctx.Error(http.StatusUnauthorized, "authGroup.Verify")
+ return
+ }
+ ctx.IsSigned = ctx.Doer != nil
})
r.Group("/{username}", func() {
@@ -64,7 +78,7 @@ func Routes() *web.Route {
r.Get("/p2/{vendorname}/{projectname}.json", composer.PackageMetadata)
r.Get("/files/{package}/{version}/{filename}", composer.DownloadPackageFile)
r.Put("", reqPackageAccess(perm.AccessModeWrite), composer.UploadPackage)
- })
+ }, reqPackageAccess(perm.AccessModeRead))
r.Group("/conan", func() {
r.Group("/v1", func() {
r.Get("/ping", conan.Ping)
@@ -152,48 +166,83 @@ func Routes() *web.Route {
}, conan.ExtractPathParameters)
})
})
- })
+ }, reqPackageAccess(perm.AccessModeRead))
r.Group("/generic", func() {
- r.Group("/{packagename}/{packageversion}/{filename}", func() {
- r.Get("", generic.DownloadPackageFile)
- r.Group("", func() {
- r.Put("", generic.UploadPackage)
- r.Delete("", generic.DeletePackage)
- }, reqPackageAccess(perm.AccessModeWrite))
+ r.Group("/{packagename}/{packageversion}", func() {
+ r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage)
+ r.Group("/{filename}", func() {
+ r.Get("", generic.DownloadPackageFile)
+ r.Group("", func() {
+ r.Put("", generic.UploadPackage)
+ r.Delete("", generic.DeletePackageFile)
+ }, reqPackageAccess(perm.AccessModeWrite))
+ })
})
- })
+ }, reqPackageAccess(perm.AccessModeRead))
+ r.Group("/helm", func() {
+ r.Get("/index.yaml", helm.Index)
+ r.Get("/{filename}", helm.DownloadPackageFile)
+ r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage)
+ }, reqPackageAccess(perm.AccessModeRead))
r.Group("/maven", func() {
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile)
r.Get("/*", maven.DownloadPackageFile)
- })
+ r.Head("/*", maven.ProvidePackageFileHeader)
+ }, reqPackageAccess(perm.AccessModeRead))
r.Group("/nuget", func() {
- r.Get("/index.json", nuget.ServiceIndex)
- r.Get("/query", nuget.SearchService)
- r.Group("/registration/{id}", func() {
- r.Get("/index.json", nuget.RegistrationIndex)
- r.Get("/{version}", nuget.RegistrationLeaf)
- })
- r.Group("/package/{id}", func() {
- r.Get("/index.json", nuget.EnumeratePackageVersions)
- r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
+ 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.Put("/", nuget.UploadPackage)
- r.Put("/symbolpackage", nuget.UploadSymbolPackage)
- r.Delete("/{id}/{version}", nuget.DeletePackage)
- }, reqPackageAccess(perm.AccessModeWrite))
- r.Get("/symbols/{filename}/{guid:[0-9a-f]{32}}FFFFFFFF/{filename2}", nuget.DownloadSymbolFile)
+ r.Get("/query", nuget.SearchServiceV3)
+ r.Group("/registration/{id}", func() {
+ r.Get("/index.json", nuget.RegistrationIndex)
+ r.Get("/{version}", nuget.RegistrationLeafV3)
+ })
+ r.Group("/package/{id}", func() {
+ r.Get("/index.json", nuget.EnumeratePackageVersionsV3)
+ r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
+ })
+ r.Group("", func() {
+ r.Put("/", nuget.UploadPackage)
+ r.Put("/symbolpackage", nuget.UploadSymbolPackage)
+ 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() {
r.Group("/@{scope}/{id}", func() {
r.Get("", npm.PackageMetadata)
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
- r.Get("/-/{version}/{filename}", npm.DownloadPackageFile)
+ r.Group("/-/{version}/{filename}", func() {
+ r.Get("", npm.DownloadPackageFile)
+ r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
+ })
+ r.Get("/-/{filename}", npm.DownloadPackageFileByName)
+ r.Group("/-rev/{revision}", func() {
+ r.Delete("", npm.DeletePackage)
+ r.Put("", npm.DeletePreview)
+ }, reqPackageAccess(perm.AccessModeWrite))
})
r.Group("/{id}", func() {
r.Get("", npm.PackageMetadata)
r.Put("", reqPackageAccess(perm.AccessModeWrite), npm.UploadPackage)
- r.Get("/-/{version}/{filename}", npm.DownloadPackageFile)
+ r.Group("/-/{version}/{filename}", func() {
+ r.Get("", npm.DownloadPackageFile)
+ r.Delete("/-rev/{revision}", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
+ })
+ r.Get("/-/{filename}", npm.DownloadPackageFileByName)
+ r.Group("/-rev/{revision}", func() {
+ r.Delete("", npm.DeletePackage)
+ r.Put("", npm.DeletePreview)
+ }, reqPackageAccess(perm.AccessModeWrite))
})
r.Group("/-/package/@{scope}/{id}/dist-tags", func() {
r.Get("", npm.ListPackageTags)
@@ -209,12 +258,29 @@ func Routes() *web.Route {
r.Delete("", npm.DeletePackageTag)
}, reqPackageAccess(perm.AccessModeWrite))
})
- })
+ r.Group("/-/v1/search", func() {
+ r.Get("", npm.PackageSearch)
+ })
+ }, reqPackageAccess(perm.AccessModeRead))
+ r.Group("/pub", func() {
+ r.Group("/api/packages", func() {
+ r.Group("/versions/new", func() {
+ r.Get("", pub.RequestUpload)
+ r.Post("/upload", pub.UploadPackageFile)
+ r.Get("/finalize/{id}/{version}", pub.FinalizePackage)
+ }, reqPackageAccess(perm.AccessModeWrite))
+ r.Group("/{id}", func() {
+ r.Get("", pub.EnumeratePackageVersions)
+ r.Get("/files/{version}", pub.DownloadPackageFile)
+ r.Get("/{version}", pub.PackageVersionMetadata)
+ })
+ })
+ }, reqPackageAccess(perm.AccessModeRead))
r.Group("/pypi", func() {
r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile)
r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile)
r.Get("/simple/{id}", pypi.PackageMetadata)
- })
+ }, reqPackageAccess(perm.AccessModeRead))
r.Group("/rubygems", func() {
r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest)
@@ -225,16 +291,32 @@ func Routes() *web.Route {
r.Post("/", rubygems.UploadPackageFile)
r.Delete("/yank", rubygems.DeletePackage)
}, reqPackageAccess(perm.AccessModeWrite))
- })
- }, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
+ }, reqPackageAccess(perm.AccessModeRead))
+ r.Group("/vagrant", func() {
+ r.Group("/authenticate", func() {
+ r.Get("", vagrant.CheckAuthenticate)
+ })
+ r.Group("/{name}", func() {
+ r.Head("", vagrant.CheckBoxAvailable)
+ r.Get("", vagrant.EnumeratePackageVersions)
+ r.Group("/{version}/{provider}", func() {
+ r.Get("", vagrant.DownloadPackageFile)
+ r.Put("", reqPackageAccess(perm.AccessModeWrite), vagrant.UploadPackageFile)
+ })
+ })
+ }, reqPackageAccess(perm.AccessModeRead))
+ }, context_service.UserAssignmentWeb(), context.PackageAssignment())
return r
}
-func ContainerRoutes() *web.Route {
+// ContainerRoutes provides endpoints that implement the OCI API to serve containers
+// These have to be mounted on `/v2/...` to comply with the OCI spec:
+// https://github.com/opencontainers/distribution-spec/blob/main/spec.md
+func ContainerRoutes(ctx gocontext.Context) *web.Route {
r := web.NewRoute()
- r.Use(context.PackageContexter())
+ r.Use(context.PackageContexter(ctx))
authMethods := []auth.Method{
&auth.Basic{},
@@ -246,18 +328,28 @@ func ContainerRoutes() *web.Route {
authGroup := auth.NewGroup(authMethods...)
r.Use(func(ctx *context.Context) {
- ctx.Doer = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
+ var err error
+ ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
+ if err != nil {
+ log.Error("Failed to verify user: %v", err)
+ ctx.Error(http.StatusUnauthorized, "Verify")
+ return
+ }
+ ctx.IsSigned = ctx.Doer != nil
})
r.Get("", container.ReqContainerAccess, container.DetermineSupport)
r.Get("/token", container.Authenticate)
+ r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList)
r.Group("/{username}", func() {
r.Group("/{image}", func() {
r.Group("/blobs/uploads", func() {
r.Post("", container.InitiateUploadBlob)
r.Group("/{uuid}", func() {
+ r.Get("", container.GetUploadBlob)
r.Patch("", container.UploadBlob)
r.Put("", container.EndUploadBlob)
+ r.Delete("", container.CancelUploadBlob)
})
}, reqPackageAccess(perm.AccessModeWrite))
r.Group("/blobs/{digest}", func() {
@@ -317,7 +409,7 @@ func ContainerRoutes() *web.Route {
}
m := blobsUploadsPattern.FindStringSubmatch(path)
- if len(m) == 3 && (isPut || isPatch) {
+ if len(m) == 3 && (isGet || isPut || isPatch || isDelete) {
reqPackageAccess(perm.AccessModeWrite)(ctx)
if ctx.Written() {
return
@@ -331,10 +423,14 @@ func ContainerRoutes() *web.Route {
ctx.SetParams("uuid", m[2])
- if isPatch {
+ if isGet {
+ container.GetUploadBlob(ctx)
+ } else if isPatch {
container.UploadBlob(ctx)
- } else {
+ } else if isPut {
container.EndUploadBlob(ctx)
+ } else {
+ container.CancelUploadBlob(ctx)
}
return
}
diff --git a/routers/api/packages/composer/api.go b/routers/api/packages/composer/api.go
index d8f67d130cd09..a3bcf80417c77 100644
--- a/routers/api/packages/composer/api.go
+++ b/routers/api/packages/composer/api.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package composer
@@ -76,7 +75,7 @@ type PackageVersionMetadata struct {
Dist Dist `json:"dist"`
}
-// Dist contains package download informations
+// Dist contains package download information
type Dist struct {
Type string `json:"type"`
URL string `json:"url"`
@@ -88,7 +87,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
for _, pd := range pds {
packageType := ""
- for _, pvp := range pd.Properties {
+ for _, pvp := range pd.VersionProperties {
if pvp.Name == composer_module.TypeProperty {
packageType = pvp.Value
break
@@ -99,7 +98,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
Name: pd.Package.Name,
Version: pd.Version.Version,
Type: packageType,
- Created: time.Unix(int64(pd.Version.CreatedUnix), 0),
+ Created: pd.Version.CreatedUnix.AsLocalTime(),
Metadata: pd.Metadata.(*composer_module.Metadata),
Dist: Dist{
Type: "zip",
diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go
index 23de28c7f9ed2..a623952aa7387 100644
--- a/routers/api/packages/composer/composer.go
+++ b/routers/api/packages/composer/composer.go
@@ -1,10 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package composer
import (
+ "errors"
"fmt"
"io"
"net/http"
@@ -15,11 +15,12 @@ import (
"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/convert"
packages_module "code.gitea.io/gitea/modules/packages"
composer_module "code.gitea.io/gitea/modules/packages/composer"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
+ "code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/hashicorp/go-version"
@@ -62,10 +63,11 @@ func SearchPackages(ctx *context.Context) {
}
opts := &packages_model.PackageSearchOptions{
- OwnerID: ctx.Package.Owner.ID,
- Type: packages_model.TypeComposer,
- Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
- Paginator: &paginator,
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages_model.TypeComposer,
+ Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
+ IsInternal: util.OptionalBoolFalse,
+ Paginator: &paginator,
}
if ctx.FormTrim("type") != "" {
opts.Properties = map[string]string{
@@ -182,7 +184,10 @@ func DownloadPackageFile(ctx *context.Context) {
}
defer s.Close()
- ctx.ServeStream(s, pf.Name)
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
}
// UploadPackage creates a new package
@@ -196,7 +201,11 @@ func UploadPackage(ctx *context.Context) {
cp, err := composer_module.ParsePackage(buf, buf.Size())
if err != nil {
- apiError(ctx, http.StatusBadRequest, err)
+ if errors.Is(err, util.ErrInvalidArgument) {
+ apiError(ctx, http.StatusBadRequest, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
return
}
@@ -225,7 +234,7 @@ func UploadPackage(ctx *context.Context) {
SemverCompatible: true,
Creator: ctx.Doer,
Metadata: cp.Metadata,
- Properties: map[string]string{
+ VersionProperties: map[string]string{
composer_module.TypeProperty: cp.Type,
},
},
@@ -233,16 +242,20 @@ func UploadPackage(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)),
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageVersion {
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/api/packages/conan/auth.go b/routers/api/packages/conan/auth.go
index 00855a97a47a2..ca02d61e76156 100644
--- a/routers/api/packages/conan/auth.go
+++ b/routers/api/packages/conan/auth.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package conan
@@ -20,22 +19,22 @@ func (a *Auth) Name() string {
}
// Verify extracts the user from the Bearer token
-func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User {
+func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
uid, err := packages.ParseAuthorizationToken(req)
if err != nil {
log.Trace("ParseAuthorizationToken: %v", err)
- return nil
+ return nil, err
}
if uid == 0 {
- return nil
+ return nil, nil
}
- u, err := user_model.GetUserByID(uid)
+ u, err := user_model.GetUserByID(req.Context(), uid)
if err != nil {
log.Error("GetUserByID: %v", err)
- return nil
+ return nil, err
}
- return u
+ return u, nil
}
diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go
index 0a27f18fd1ee9..d538cc7d397db 100644
--- a/routers/api/packages/conan/conan.go
+++ b/routers/api/packages/conan/conan.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package conan
@@ -14,6 +13,7 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
conan_model "code.gitea.io/gitea/models/packages/conan"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
@@ -33,20 +33,18 @@ const (
packageReferenceKey = "PackageReference"
)
-type stringSet map[string]struct{}
-
var (
- recipeFileList = stringSet{
- conanfileFile: struct{}{},
- "conanmanifest.txt": struct{}{},
- "conan_sources.tgz": struct{}{},
- "conan_export.tgz": struct{}{},
- }
- packageFileList = stringSet{
- conaninfoFile: struct{}{},
- "conanmanifest.txt": struct{}{},
- "conan_package.tgz": struct{}{},
- }
+ recipeFileList = container.SetOf(
+ conanfileFile,
+ "conanmanifest.txt",
+ "conan_sources.tgz",
+ "conan_export.tgz",
+ )
+ packageFileList = container.SetOf(
+ conaninfoFile,
+ "conanmanifest.txt",
+ "conan_package.tgz",
+ )
)
func jsonResponse(ctx *context.Context, status int, obj interface{}) {
@@ -268,7 +266,7 @@ func PackageUploadURLs(ctx *context.Context) {
)
}
-func serveUploadURLs(ctx *context.Context, fileFilter stringSet, uploadURL string) {
+func serveUploadURLs(ctx *context.Context, fileFilter container.Set[string], uploadURL string) {
defer ctx.Req.Body.Close()
var files map[string]int64
@@ -279,7 +277,7 @@ func serveUploadURLs(ctx *context.Context, fileFilter stringSet, uploadURL strin
urls := make(map[string]string)
for file := range files {
- if _, ok := fileFilter[file]; ok {
+ if fileFilter.Contains(file) {
urls[file] = fmt.Sprintf("%s/%s", uploadURL, file)
}
}
@@ -301,12 +299,12 @@ func UploadPackageFile(ctx *context.Context) {
uploadFile(ctx, packageFileList, pref.AsKey())
}
-func uploadFile(ctx *context.Context, fileFilter stringSet, fileKey string) {
+func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) {
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
filename := ctx.Params("filename")
- if _, ok := fileFilter[filename]; !ok {
+ if !fileFilter.Contains(filename) {
apiError(ctx, http.StatusBadRequest, nil)
return
}
@@ -342,16 +340,16 @@ func uploadFile(ctx *context.Context, fileFilter stringSet, fileKey string) {
Name: rref.Name,
Version: rref.Version,
},
- SemverCompatible: true,
- Creator: ctx.Doer,
+ Creator: ctx.Doer,
}
pfci := &packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(filename),
CompositeKey: fileKey,
},
- Data: buf,
- IsLead: isConanfileFile,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: isConanfileFile,
Properties: map[string]string{
conan_module.PropertyRecipeUser: rref.User,
conan_module.PropertyRecipeChannel: rref.Channel,
@@ -418,36 +416,39 @@ func uploadFile(ctx *context.Context, fileFilter stringSet, fileKey string) {
pfci,
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageFile {
+ switch err {
+ case packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusCreated)
}
-// DownloadRecipeFile serves the conent of the requested recipe file
+// DownloadRecipeFile serves the content of the requested recipe file
func DownloadRecipeFile(ctx *context.Context) {
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
downloadFile(ctx, recipeFileList, rref.AsKey())
}
-// DownloadPackageFile serves the conent of the requested package file
+// DownloadPackageFile serves the content of the requested package file
func DownloadPackageFile(ctx *context.Context) {
pref := ctx.Data[packageReferenceKey].(*conan_module.PackageReference)
downloadFile(ctx, packageFileList, pref.AsKey())
}
-func downloadFile(ctx *context.Context, fileFilter stringSet, fileKey string) {
+func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKey string) {
rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference)
filename := ctx.Params("filename")
- if _, ok := fileFilter[filename]; !ok {
+ if !fileFilter.Contains(filename) {
apiError(ctx, http.StatusBadRequest, nil)
return
}
@@ -475,7 +476,10 @@ func downloadFile(ctx *context.Context, fileFilter stringSet, fileKey string) {
}
defer s.Close()
- ctx.ServeStream(s, pf.Name)
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
}
// DeleteRecipeV1 deletes the requested recipe(s)
@@ -602,7 +606,7 @@ func DeletePackageV2(ctx *context.Context) {
}
func deleteRecipeOrPackage(apictx *context.Context, rref *conan_module.RecipeReference, ignoreRecipeRevision bool, pref *conan_module.PackageReference, ignorePackageRevision bool) error {
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
@@ -674,7 +678,7 @@ func deleteRecipeOrPackage(apictx *context.Context, rref *conan_module.RecipeRef
}
if versionDeleted {
- notification.NotifyPackageDelete(apictx.Doer, pd)
+ notification.NotifyPackageDelete(apictx, apictx.Doer, pd)
}
return nil
@@ -723,7 +727,7 @@ func listRevisions(ctx *context.Context, revisions []*conan_model.PropertyValue)
revs := make([]*revisionInfo, 0, len(revisions))
for _, rev := range revisions {
- revs = append(revs, &revisionInfo{Revision: rev.Value, Time: time.Unix(int64(rev.CreatedUnix), 0)})
+ revs = append(revs, &revisionInfo{Revision: rev.Value, Time: rev.CreatedUnix.AsLocalTime()})
}
jsonResponse(ctx, http.StatusOK, &RevisionList{revs})
@@ -743,7 +747,7 @@ func LatestRecipeRevision(ctx *context.Context) {
return
}
- jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: time.Unix(int64(revision.CreatedUnix), 0)})
+ jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: revision.CreatedUnix.AsLocalTime()})
}
// LatestPackageRevision gets the latest package revision
@@ -760,7 +764,7 @@ func LatestPackageRevision(ctx *context.Context) {
return
}
- jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: time.Unix(int64(revision.CreatedUnix), 0)})
+ jsonResponse(ctx, http.StatusOK, &revisionInfo{Revision: revision.Value, Time: revision.CreatedUnix.AsLocalTime()})
}
// ListRecipeRevisionFiles gets a list of all recipe revision files
diff --git a/routers/api/packages/conan/search.go b/routers/api/packages/conan/search.go
index 39dd6362aa488..2bcf9df162b98 100644
--- a/routers/api/packages/conan/search.go
+++ b/routers/api/packages/conan/search.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package conan
diff --git a/routers/api/packages/container/auth.go b/routers/api/packages/container/auth.go
index 770068a3bfb7b..33f439ec3e588 100644
--- a/routers/api/packages/container/auth.go
+++ b/routers/api/packages/container/auth.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package container
@@ -21,25 +20,25 @@ func (a *Auth) Name() string {
// Verify extracts the user from the Bearer token
// If it's an anonymous session a ghost user is returned
-func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User {
+func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
uid, err := packages.ParseAuthorizationToken(req)
if err != nil {
log.Trace("ParseAuthorizationToken: %v", err)
- return nil
+ return nil, err
}
if uid == 0 {
- return nil
+ return nil, nil
}
if uid == -1 {
- return user_model.NewGhostUser()
+ return user_model.NewGhostUser(), nil
}
- u, err := user_model.GetUserByID(uid)
+ u, err := user_model.GetUserByID(req.Context(), uid)
if err != nil {
log.Error("GetUserByID: %v", err)
- return nil
+ return nil, err
}
- return u
+ return u, nil
}
diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go
index 8f6254f583213..2e4309a2ebce6 100644
--- a/routers/api/packages/container/blob.go
+++ b/routers/api/packages/container/blob.go
@@ -1,14 +1,16 @@
// 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.
+// SPDX-License-Identifier: MIT
package container
import (
"context"
"encoding/hex"
+ "errors"
"fmt"
+ "os"
"strings"
+ "sync"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
@@ -16,9 +18,12 @@ import (
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
container_module "code.gitea.io/gitea/modules/packages/container"
+ "code.gitea.io/gitea/modules/util"
packages_service "code.gitea.io/gitea/services/packages"
)
+var uploadVersionMutex sync.Mutex
+
// saveAsPackageBlob creates a package blob from an upload
// The uploaded blob gets stored in a special upload version to link them to the package/image
func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_service.PackageInfo) (*packages_model.PackageBlob, error) {
@@ -28,7 +33,67 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
contentStore := packages_module.NewContentStore()
- err := db.WithTx(func(ctx context.Context) error {
+ uploadVersion, err := getOrCreateUploadVersion(pi)
+ if err != nil {
+ return nil, err
+ }
+
+ err = db.WithTx(db.DefaultContext, func(ctx context.Context) error {
+ pb, exists, err = packages_model.GetOrInsertBlob(ctx, pb)
+ if err != nil {
+ log.Error("Error inserting package blob: %v", err)
+ return err
+ }
+ // FIXME: Workaround to be removed in v1.20
+ // https://github.com/go-gitea/gitea/issues/19586
+ if exists {
+ err = contentStore.Has(packages_module.BlobHash256Key(pb.HashSHA256))
+ if err != nil && (errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist)) {
+ log.Debug("Package registry inconsistent: blob %s does not exist on file system", pb.HashSHA256)
+ exists = false
+ }
+ }
+ if !exists {
+ if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), hsr, hsr.Size()); err != nil {
+ log.Error("Error saving package blob in content store: %v", err)
+ return err
+ }
+ }
+
+ return createFileForBlob(ctx, uploadVersion, pb)
+ })
+ if err != nil {
+ if !exists {
+ if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
+ log.Error("Error deleting package blob from content store: %v", err)
+ }
+ }
+ return nil, err
+ }
+
+ return pb, nil
+}
+
+// mountBlob mounts the specific blob to a different package
+func mountBlob(pi *packages_service.PackageInfo, pb *packages_model.PackageBlob) error {
+ uploadVersion, err := getOrCreateUploadVersion(pi)
+ if err != nil {
+ return err
+ }
+
+ return db.WithTx(db.DefaultContext, func(ctx context.Context) error {
+ return createFileForBlob(ctx, uploadVersion, pb)
+ })
+}
+
+func getOrCreateUploadVersion(pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) {
+ var uploadVersion *packages_model.PackageVersion
+
+ // FIXME: Replace usage of mutex with database transaction
+ // https://github.com/go-gitea/gitea/pull/21862
+ uploadVersionMutex.Lock()
+ err := db.WithTx(db.DefaultContext, func(ctx context.Context) error {
+ created := true
p := &packages_model.Package{
OwnerID: pi.Owner.ID,
Type: packages_model.TypeContainer,
@@ -37,12 +102,21 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
}
var err error
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
- if err != packages_model.ErrDuplicatePackage {
+ if err == packages_model.ErrDuplicatePackage {
+ created = false
+ } else {
log.Error("Error inserting package: %v", err)
return err
}
}
+ if created {
+ if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(pi.Owner.LowerName+"/"+pi.Name)); err != nil {
+ log.Error("Error setting package property: %v", err)
+ return err
+ }
+ }
+
pv := &packages_model.PackageVersion{
PackageID: p.ID,
CreatorID: pi.Owner.ID,
@@ -58,56 +132,44 @@ func saveAsPackageBlob(hsr packages_module.HashedSizeReader, pi *packages_servic
}
}
- pb, exists, err = packages_model.GetOrInsertBlob(ctx, pb)
- if err != nil {
- log.Error("Error inserting package blob: %v", err)
- return err
- }
- if !exists {
- if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), hsr, hsr.Size()); err != nil {
- log.Error("Error saving package blob in content store: %v", err)
- return err
- }
- }
+ uploadVersion = pv
- filename := strings.ToLower(fmt.Sprintf("sha256_%s", pb.HashSHA256))
+ return nil
+ })
+ uploadVersionMutex.Unlock()
- pf := &packages_model.PackageFile{
- VersionID: pv.ID,
- BlobID: pb.ID,
- Name: filename,
- LowerName: filename,
- CompositeKey: packages_model.EmptyFileKey,
- }
- if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
- if err == packages_model.ErrDuplicatePackageFile {
- return nil
- }
- log.Error("Error inserting package file: %v", err)
- return err
- }
+ return uploadVersion, err
+}
- if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, container_module.PropertyDigest, digestFromPackageBlob(pb)); err != nil {
- log.Error("Error setting package file property: %v", err)
- return err
- }
+func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, pb *packages_model.PackageBlob) error {
+ filename := strings.ToLower(fmt.Sprintf("sha256_%s", pb.HashSHA256))
- return nil
- })
- if err != nil {
- if !exists {
- if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
- log.Error("Error deleting package blob from content store: %v", err)
- }
+ pf := &packages_model.PackageFile{
+ VersionID: pv.ID,
+ BlobID: pb.ID,
+ Name: filename,
+ LowerName: filename,
+ CompositeKey: packages_model.EmptyFileKey,
+ }
+ var err error
+ if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
+ if err == packages_model.ErrDuplicatePackageFile {
+ return nil
}
- return nil, err
+ log.Error("Error inserting package file: %v", err)
+ return err
}
- return pb, nil
+ if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, container_module.PropertyDigest, digestFromPackageBlob(pb)); err != nil {
+ log.Error("Error setting package file property: %v", err)
+ return err
+ }
+
+ return nil
}
func deleteBlob(ownerID int64, image, digest string) error {
- return db.WithTx(func(ctx context.Context) error {
+ return db.WithTx(db.DefaultContext, func(ctx context.Context) error {
pfds, err := container_model.GetContainerBlobs(ctx, &container_model.BlobSearchOptions{
OwnerID: ownerID,
Image: image,
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
index 08b6b421b0840..8b2c4e6bb2bc9 100644
--- a/routers/api/packages/container/container.go
+++ b/routers/api/packages/container/container.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package container
@@ -10,6 +9,7 @@ import (
"io"
"net/http"
"net/url"
+ "os"
"regexp"
"strconv"
"strings"
@@ -24,6 +24,7 @@ import (
container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/packages/container/oci"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"
@@ -112,8 +113,7 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost for anonymous access)
func ReqContainerAccess(ctx *context.Context) {
if ctx.Doer == nil {
- ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token"`)
- ctx.Resp.Header().Add("WWW-Authenticate", `Basic`)
+ ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
apiErrorDefined(ctx, errUnauthorized)
}
}
@@ -152,6 +152,39 @@ func Authenticate(ctx *context.Context) {
})
}
+// https://docs.docker.com/registry/spec/api/#listing-repositories
+func GetRepositoryList(ctx *context.Context) {
+ n := ctx.FormInt("n")
+ if n <= 0 || n > 100 {
+ n = 100
+ }
+ last := ctx.FormTrim("last")
+
+ repositories, err := container_model.GetRepositories(ctx, ctx.Doer, n, last)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ type RepositoryList struct {
+ Repositories []string `json:"repositories"`
+ }
+
+ if len(repositories) == n {
+ v := url.Values{}
+ if n > 0 {
+ v.Add("n", strconv.Itoa(n))
+ }
+ v.Add("last", repositories[len(repositories)-1])
+
+ ctx.Resp.Header().Set("Link", fmt.Sprintf(`; rel="next"`, v.Encode()))
+ }
+
+ jsonResponse(ctx, http.StatusOK, RepositoryList{
+ Repositories: repositories,
+ })
+}
+
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
@@ -161,11 +194,16 @@ func InitiateUploadBlob(ctx *context.Context) {
mount := ctx.FormTrim("mount")
from := ctx.FormTrim("from")
if mount != "" {
- blob, _ := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
- Image: from,
- Digest: mount,
+ blob, _ := workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
+ Repository: from,
+ Digest: mount,
})
if blob != nil {
+ if err := mountBlob(&packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}, blob.Blob); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
setResponseHeaders(ctx.Resp, &containerHeaders{
Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount),
ContentDigest: mount,
@@ -216,6 +254,27 @@ func InitiateUploadBlob(ctx *context.Context) {
})
}
+// https://docs.docker.com/registry/spec/api/#get-blob-upload
+func GetUploadBlob(ctx *context.Context) {
+ uuid := ctx.Params("uuid")
+
+ upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
+ if err != nil {
+ if err == packages_model.ErrPackageBlobUploadNotExist {
+ apiErrorDefined(ctx, errBlobUploadUnknown)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ setResponseHeaders(ctx.Resp, &containerHeaders{
+ Range: fmt.Sprintf("0-%d", upload.BytesReceived),
+ UploadUUID: upload.ID,
+ Status: http.StatusNoContent,
+ })
+}
+
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
func UploadBlob(ctx *context.Context) {
image := ctx.Params("image")
@@ -322,6 +381,30 @@ func EndUploadBlob(ctx *context.Context) {
})
}
+// https://docs.docker.com/registry/spec/api/#delete-blob-upload
+func CancelUploadBlob(ctx *context.Context) {
+ uuid := ctx.Params("uuid")
+
+ _, err := packages_model.GetBlobUploadByID(ctx, uuid)
+ if err != nil {
+ if err == packages_model.ErrPackageBlobUploadNotExist {
+ apiErrorDefined(ctx, errBlobUploadUnknown)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ if err := container_service.RemoveBlobUploadByID(ctx, uuid); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ setResponseHeaders(ctx.Resp, &containerHeaders{
+ Status: http.StatusNoContent,
+ })
+}
+
func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
digest := ctx.Params("digest")
@@ -329,7 +412,7 @@ func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescri
return nil, container_model.ErrContainerBlobNotExist
}
- return container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
+ return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Image: ctx.Params("image"),
Digest: digest,
@@ -471,7 +554,7 @@ func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDe
return nil, container_model.ErrContainerBlobNotExist
}
- return container_model.GetContainerBlob(ctx, opts)
+ return workaroundGetContainerBlob(ctx, opts)
}
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
@@ -611,3 +694,23 @@ func GetTagList(ctx *context.Context) {
Tags: tags,
})
}
+
+// FIXME: Workaround to be removed in v1.20
+// https://github.com/go-gitea/gitea/issues/19586
+func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) {
+ blob, err := container_model.GetContainerBlob(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+
+ err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(blob.Blob.HashSHA256))
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist) {
+ log.Debug("Package registry inconsistent: blob %s does not exist on file system", blob.Blob.HashSHA256)
+ return nil, container_model.ErrContainerBlobNotExist
+ }
+ return nil, err
+ }
+
+ return blob, nil
+}
diff --git a/routers/api/packages/container/errors.go b/routers/api/packages/container/errors.go
index 0efbb081ca2ff..1a9b0f32d2fe2 100644
--- a/routers/api/packages/container/errors.go
+++ b/routers/api/packages/container/errors.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package container
diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go
index b327538e6f2c2..350933f3d2f87 100644
--- a/routers/api/packages/container/manifest.go
+++ b/routers/api/packages/container/manifest.go
@@ -1,13 +1,14 @@
// 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.
+// SPDX-License-Identifier: MIT
package container
import (
"context"
+ "errors"
"fmt"
"io"
+ "os"
"strings"
"code.gitea.io/gitea/models/db"
@@ -19,6 +20,7 @@ import (
packages_module "code.gitea.io/gitea/modules/packages"
container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/packages/container/oci"
+ "code.gitea.io/gitea/modules/util"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -77,7 +79,7 @@ func processImageManifest(mci *manifestCreationInfo, buf *packages_module.Hashed
return err
}
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
@@ -190,7 +192,7 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H
return err
}
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
@@ -267,6 +269,7 @@ func processImageManifestIndex(mci *manifestCreationInfo, buf *packages_module.H
}
func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) {
+ created := true
p := &packages_model.Package{
OwnerID: mci.Owner.ID,
Type: packages_model.TypeContainer,
@@ -275,12 +278,21 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
}
var err error
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
- if err != packages_model.ErrDuplicatePackage {
+ if err == packages_model.ErrDuplicatePackage {
+ created = false
+ } else {
log.Error("Error inserting package: %v", err)
return nil, err
}
}
+ if created {
+ if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(mci.Owner.LowerName+"/"+mci.Image)); err != nil {
+ log.Error("Error setting package property: %v", err)
+ return nil, err
+ }
+ }
+
metadata.IsTagged = mci.IsTagged
metadataJSON, err := json.Marshal(metadata)
@@ -302,6 +314,9 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
return nil, err
}
+ // keep download count on overwrite
+ _pv.DownloadCount = pv.DownloadCount
+
if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
log.Error("Error inserting package: %v", err)
return nil, err
@@ -355,6 +370,10 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package
}
var err error
if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
+ if err == packages_model.ErrDuplicatePackageFile {
+ // Skip this blob because the manifest contains the same filesystem layer multiple times.
+ return nil
+ }
log.Error("Error inserting package file: %v", err)
return err
}
@@ -386,6 +405,15 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack
log.Error("Error inserting package blob: %v", err)
return nil, false, "", err
}
+ // FIXME: Workaround to be removed in v1.20
+ // https://github.com/go-gitea/gitea/issues/19586
+ if exists {
+ err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(pb.HashSHA256))
+ if err != nil && (errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist)) {
+ log.Debug("Package registry inconsistent: blob %s does not exist on file system", pb.HashSHA256)
+ exists = false
+ }
+ }
if !exists {
contentStore := packages_module.NewContentStore()
if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), buf, buf.Size()); err != nil {
diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go
index d862f77259ace..5fba02cacd468 100644
--- a/routers/api/packages/generic/generic.go
+++ b/routers/api/packages/generic/generic.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package generic
@@ -8,6 +7,7 @@ import (
"errors"
"net/http"
"regexp"
+ "strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
@@ -15,8 +15,6 @@ import (
packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
-
- "github.com/hashicorp/go-version"
)
var (
@@ -32,22 +30,16 @@ func apiError(ctx *context.Context, status int, obj interface{}) {
// DownloadPackageFile serves the specific generic package.
func DownloadPackageFile(ctx *context.Context) {
- packageName, packageVersion, filename, err := sanitizeParameters(ctx)
- if err != nil {
- apiError(ctx, http.StatusBadRequest, err)
- return
- }
-
s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeGeneric,
- Name: packageName,
- Version: packageVersion,
+ Name: ctx.Params("packagename"),
+ Version: ctx.Params("packageversion"),
},
&packages_service.PackageFileInfo{
- Filename: filename,
+ Filename: ctx.Params("filename"),
},
)
if err != nil {
@@ -60,15 +52,26 @@ func DownloadPackageFile(ctx *context.Context) {
}
defer s.Close()
- ctx.ServeStream(s, pf.Name)
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
}
// UploadPackage uploads the specific generic package.
// Duplicated packages get rejected.
func UploadPackage(ctx *context.Context) {
- packageName, packageVersion, filename, err := sanitizeParameters(ctx)
- if err != nil {
- apiError(ctx, http.StatusBadRequest, err)
+ packageName := ctx.Params("packagename")
+ filename := ctx.Params("filename")
+
+ if !packageNameRegex.MatchString(packageName) || !filenameRegex.MatchString(filename) {
+ apiError(ctx, http.StatusBadRequest, errors.New("Invalid package name or filename"))
+ return
+ }
+
+ packageVersion := ctx.Params("packageversion")
+ if packageVersion != strings.TrimSpace(packageVersion) {
+ apiError(ctx, http.StatusBadRequest, errors.New("Invalid package version"))
return
}
@@ -89,7 +92,7 @@ func UploadPackage(ctx *context.Context) {
}
defer buf.Close()
- _, _, err = packages_service.CreatePackageAndAddFile(
+ _, _, err = packages_service.CreatePackageOrAddFileToExisting(
&packages_service.PackageCreationInfo{
PackageInfo: packages_service.PackageInfo{
Owner: ctx.Package.Owner,
@@ -97,23 +100,26 @@ func UploadPackage(ctx *context.Context) {
Name: packageName,
Version: packageVersion,
},
- SemverCompatible: true,
- Creator: ctx.Doer,
+ Creator: ctx.Doer,
},
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: filename,
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageVersion {
- apiError(ctx, http.StatusBadRequest, err)
- return
+ switch err {
+ case packages_model.ErrDuplicatePackageFile:
+ apiError(ctx, http.StatusConflict, err)
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
@@ -122,19 +128,13 @@ func UploadPackage(ctx *context.Context) {
// DeletePackage deletes the specific generic package.
func DeletePackage(ctx *context.Context) {
- packageName, packageVersion, _, err := sanitizeParameters(ctx)
- if err != nil {
- apiError(ctx, http.StatusBadRequest, err)
- return
- }
-
- err = packages_service.RemovePackageVersionByNameAndVersion(
+ err := packages_service.RemovePackageVersionByNameAndVersion(
ctx.Doer,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeGeneric,
- Name: packageName,
- Version: packageVersion,
+ Name: ctx.Params("packagename"),
+ Version: ctx.Params("packageversion"),
},
)
if err != nil {
@@ -146,21 +146,50 @@ func DeletePackage(ctx *context.Context) {
return
}
- ctx.Status(http.StatusOK)
+ ctx.Status(http.StatusNoContent)
}
-func sanitizeParameters(ctx *context.Context) (string, string, string, error) {
- packageName := ctx.Params("packagename")
- filename := ctx.Params("filename")
+// DeletePackageFile deletes the specific file of a generic package.
+func DeletePackageFile(ctx *context.Context) {
+ pv, pf, err := func() (*packages_model.PackageVersion, *packages_model.PackageFile, error) {
+ pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeGeneric, ctx.Params("packagename"), ctx.Params("packageversion"))
+ if err != nil {
+ return nil, nil, err
+ }
- if !packageNameRegex.MatchString(packageName) || !filenameRegex.MatchString(filename) {
- return "", "", "", errors.New("Invalid package name or filename")
+ pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, ctx.Params("filename"), packages_model.EmptyFileKey)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return pv, pf, nil
+ }()
+ if err != nil {
+ if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
}
- v, err := version.NewSemver(ctx.Params("packageversion"))
+ pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
if err != nil {
- return "", "", "", err
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if len(pfs) == 1 {
+ if err := packages_service.RemovePackageVersion(ctx.Doer, pv); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ } else {
+ if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
}
- return packageName, v.String(), filename, nil
+ ctx.Status(http.StatusNoContent)
}
diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go
new file mode 100644
index 0000000000000..3bcce6bdf5855
--- /dev/null
+++ b/routers/api/packages/helm/helm.go
@@ -0,0 +1,219 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package helm
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ packages_model "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ packages_module "code.gitea.io/gitea/modules/packages"
+ helm_module "code.gitea.io/gitea/modules/packages/helm"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/packages/helper"
+ packages_service "code.gitea.io/gitea/services/packages"
+
+ "gopkg.in/yaml.v3"
+)
+
+func apiError(ctx *context.Context, status int, obj interface{}) {
+ helper.LogAndProcessError(ctx, status, obj, func(message string) {
+ type Error struct {
+ Error string `json:"error"`
+ }
+ ctx.JSON(status, Error{
+ Error: message,
+ })
+ })
+}
+
+// Index generates the Helm charts index
+func Index(ctx *context.Context) {
+ pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages_model.TypeHelm,
+ IsInternal: util.OptionalBoolFalse,
+ })
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ baseURL := setting.AppURL + "api/packages/" + url.PathEscape(ctx.Package.Owner.Name) + "/helm"
+
+ type ChartVersion struct {
+ helm_module.Metadata `yaml:",inline"`
+ URLs []string `yaml:"urls"`
+ Created time.Time `yaml:"created,omitempty"`
+ Removed bool `yaml:"removed,omitempty"`
+ Digest string `yaml:"digest,omitempty"`
+ }
+
+ type ServerInfo struct {
+ ContextPath string `yaml:"contextPath,omitempty"`
+ }
+
+ type Index struct {
+ APIVersion string `yaml:"apiVersion"`
+ Entries map[string][]*ChartVersion `yaml:"entries"`
+ Generated time.Time `yaml:"generated,omitempty"`
+ ServerInfo *ServerInfo `yaml:"serverInfo,omitempty"`
+ }
+
+ entries := make(map[string][]*ChartVersion)
+ for _, pv := range pvs {
+ metadata := &helm_module.Metadata{}
+ if err := json.Unmarshal([]byte(pv.MetadataJSON), &metadata); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ entries[metadata.Name] = append(entries[metadata.Name], &ChartVersion{
+ Metadata: *metadata,
+ Created: pv.CreatedUnix.AsTime(),
+ URLs: []string{fmt.Sprintf("%s/%s", baseURL, url.PathEscape(createFilename(metadata)))},
+ })
+ }
+
+ ctx.Resp.WriteHeader(http.StatusOK)
+ if err := yaml.NewEncoder(ctx.Resp).Encode(&Index{
+ APIVersion: "v1",
+ Entries: entries,
+ Generated: time.Now(),
+ ServerInfo: &ServerInfo{
+ ContextPath: setting.AppSubURL + "/api/packages/" + url.PathEscape(ctx.Package.Owner.Name) + "/helm",
+ },
+ }); err != nil {
+ log.Error("YAML encode failed: %v", err)
+ }
+}
+
+// DownloadPackageFile serves the content of a package
+func DownloadPackageFile(ctx *context.Context) {
+ filename := ctx.Params("filename")
+
+ pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages_model.TypeHelm,
+ Name: packages_model.SearchValue{
+ ExactMatch: true,
+ Value: ctx.Params("package"),
+ },
+ HasFileWithName: filename,
+ IsInternal: util.OptionalBoolFalse,
+ })
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ if len(pvs) != 1 {
+ apiError(ctx, http.StatusNotFound, nil)
+ return
+ }
+
+ s, pf, err := packages_service.GetFileStreamByPackageVersion(
+ ctx,
+ pvs[0],
+ &packages_service.PackageFileInfo{
+ Filename: filename,
+ },
+ )
+ if err != nil {
+ if err == packages_model.ErrPackageFileNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer s.Close()
+
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
+}
+
+// UploadPackage creates a new package
+func UploadPackage(ctx *context.Context) {
+ upload, needToClose, err := ctx.UploadStream()
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ if needToClose {
+ defer upload.Close()
+ }
+
+ buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer buf.Close()
+
+ metadata, err := helm_module.ParseChartArchive(buf)
+ if err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ apiError(ctx, http.StatusBadRequest, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ if _, err := buf.Seek(0, io.SeekStart); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ _, _, err = packages_service.CreatePackageOrAddFileToExisting(
+ &packages_service.PackageCreationInfo{
+ PackageInfo: packages_service.PackageInfo{
+ Owner: ctx.Package.Owner,
+ PackageType: packages_model.TypeHelm,
+ Name: metadata.Name,
+ Version: metadata.Version,
+ },
+ SemverCompatible: true,
+ Creator: ctx.Doer,
+ Metadata: metadata,
+ },
+ &packages_service.PackageFileCreationInfo{
+ PackageFileInfo: packages_service.PackageFileInfo{
+ Filename: createFilename(metadata),
+ },
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
+ OverwriteExisting: true,
+ },
+ )
+ if err != nil {
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
+ apiError(ctx, http.StatusConflict, err)
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusCreated)
+}
+
+func createFilename(metadata *helm_module.Metadata) string {
+ return strings.ToLower(fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version))
+}
diff --git a/routers/api/packages/helper/helper.go b/routers/api/packages/helper/helper.go
index 8cde84023f099..660aaec1a3a5f 100644
--- a/routers/api/packages/helper/helper.go
+++ b/routers/api/packages/helper/helper.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package helper
diff --git a/routers/api/packages/maven/api.go b/routers/api/packages/maven/api.go
index b60a317814b4a..167fe42b56d51 100644
--- a/routers/api/packages/maven/api.go
+++ b/routers/api/packages/maven/api.go
@@ -1,12 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package maven
import (
"encoding/xml"
- "sort"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
@@ -23,12 +21,8 @@ type MetadataResponse struct {
Version []string `xml:"versioning>versions>version"`
}
+// pds is expected to be sorted ascending by CreatedUnix
func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataResponse {
- sort.Slice(pds, func(i, j int) bool {
- // Maven and Gradle order packages by their creation timestamp and not by their version string
- return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix
- })
-
var release *packages_model.PackageDescriptor
versions := make([]string, 0, len(pds))
diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index bba4babf04f6f..d0c9983cbf1f2 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package maven
@@ -9,13 +8,15 @@ import (
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
+ "encoding/hex"
"encoding/xml"
"errors"
- "fmt"
"io"
"net/http"
"path/filepath"
"regexp"
+ "sort"
+ "strconv"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
@@ -34,6 +35,10 @@ const (
extensionSHA1 = ".sha1"
extensionSHA256 = ".sha256"
extensionSHA512 = ".sha512"
+ extensionPom = ".pom"
+ extensionJar = ".jar"
+ contentTypeJar = "application/java-archive"
+ contentTypeXML = "text/xml"
)
var (
@@ -49,6 +54,15 @@ func apiError(ctx *context.Context, status int, obj interface{}) {
// DownloadPackageFile serves the content of a package
func DownloadPackageFile(ctx *context.Context) {
+ handlePackageFile(ctx, true)
+}
+
+// ProvidePackageFileHeader provides only the headers describing a package
+func ProvidePackageFileHeader(ctx *context.Context) {
+ handlePackageFile(ctx, false)
+}
+
+func handlePackageFile(ctx *context.Context, serveContent bool) {
params, err := extractPathParameters(ctx)
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
@@ -58,7 +72,7 @@ func DownloadPackageFile(ctx *context.Context) {
if params.IsMeta && params.Version == "" {
serveMavenMetadata(ctx, params)
} else {
- servePackageFile(ctx, params)
+ servePackageFile(ctx, params, serveContent)
}
}
@@ -82,6 +96,11 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
return
}
+ sort.Slice(pds, func(i, j int) bool {
+ // Maven and Gradle order packages by their creation timestamp and not by their version string
+ return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix
+ })
+
xmlMetadata, err := xml.Marshal(createMetadataResponse(pds))
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -89,6 +108,9 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
}
xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...)
+ latest := pds[len(pds)-1]
+ ctx.Resp.Header().Set("Last-Modified", latest.Version.CreatedUnix.Format(http.TimeFormat))
+
ext := strings.ToLower(filepath.Ext(params.Filename))
if isChecksumExtension(ext) {
var hash []byte
@@ -106,14 +128,19 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
tmp := sha512.Sum512(xmlMetadataWithHeader)
hash = tmp[:]
}
- ctx.PlainText(http.StatusOK, fmt.Sprintf("%x", hash))
+ ctx.PlainText(http.StatusOK, hex.EncodeToString(hash))
return
}
- ctx.PlainTextBytes(http.StatusOK, xmlMetadataWithHeader)
+ ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader)))
+ ctx.Resp.Header().Set("Content-Type", contentTypeXML)
+
+ if _, err := ctx.Resp.Write(xmlMetadataWithHeader); err != nil {
+ log.Error("write bytes failed: %v", err)
+ }
}
-func servePackageFile(ctx *context.Context, params parameters) {
+func servePackageFile(ctx *context.Context, params parameters, serveContent bool) {
packageName := params.GroupID + "-" + params.ArtifactID
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version)
@@ -165,6 +192,23 @@ func servePackageFile(ctx *context.Context, params parameters) {
return
}
+ opts := &context.ServeHeaderOptions{
+ ContentLength: &pb.Size,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ }
+ switch ext {
+ case extensionJar:
+ opts.ContentType = contentTypeJar
+ case extensionPom:
+ opts.ContentType = contentTypeXML
+ }
+
+ if !serveContent {
+ ctx.SetServeHeaders(opts)
+ ctx.Status(http.StatusOK)
+ return
+ }
+
s, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(pb.HashSHA256))
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -177,7 +221,9 @@ func servePackageFile(ctx *context.Context, params parameters) {
}
}
- ctx.ServeStream(s, pf.Name)
+ opts.Filename = pf.Name
+
+ ctx.ServeContent(s, opts)
}
// UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
@@ -266,12 +312,14 @@ func UploadPackageFile(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: params.Filename,
},
- Data: buf,
- IsLead: false,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: false,
+ OverwriteExisting: params.IsMeta,
}
// If it's the package pom file extract the metadata
- if ext == ".pom" {
+ if ext == extensionPom {
pfci.IsLead = true
var err error
@@ -311,11 +359,14 @@ func UploadPackageFile(ctx *context.Context) {
pfci,
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageFile {
+ switch err {
+ case packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go
index 56c897704398d..f8d50f03cf501 100644
--- a/routers/api/packages/npm/api.go
+++ b/routers/api/packages/npm/api.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package npm
@@ -25,7 +24,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
for _, pd := range pds {
versions[pd.SemVer.String()] = createPackageMetadataVersion(registryURL, pd)
- for _, pvp := range pd.Properties {
+ for _, pvp := range pd.VersionProperties {
if pvp.Name == npm_module.TagProperty {
distTags[pvp.Value] = pd.Version.Version
}
@@ -55,15 +54,19 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
metadata := pd.Metadata.(*npm_module.Metadata)
return &npm_module.PackageMetadataVersion{
- ID: fmt.Sprintf("%s@%s", pd.Package.Name, pd.Version.Version),
- Name: pd.Package.Name,
- Version: pd.Version.Version,
- Description: metadata.Description,
- Author: npm_module.User{Name: metadata.Author},
- Homepage: metadata.ProjectURL,
- License: metadata.License,
- Dependencies: metadata.Dependencies,
- Readme: metadata.Readme,
+ ID: fmt.Sprintf("%s@%s", pd.Package.Name, pd.Version.Version),
+ Name: pd.Package.Name,
+ Version: pd.Version.Version,
+ Description: metadata.Description,
+ Author: npm_module.User{Name: metadata.Author},
+ Homepage: metadata.ProjectURL,
+ License: metadata.License,
+ Dependencies: metadata.Dependencies,
+ DevDependencies: metadata.DevelopmentDependencies,
+ PeerDependencies: metadata.PeerDependencies,
+ OptionalDependencies: metadata.OptionalDependencies,
+ Readme: metadata.Readme,
+ Bin: metadata.Bin,
Dist: npm_module.PackageDistribution{
Shasum: pd.Files[0].Blob.HashSHA1,
Integrity: "sha512-" + base64.StdEncoding.EncodeToString(hashBytes),
@@ -71,3 +74,38 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
},
}
}
+
+func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total int64) *npm_module.PackageSearch {
+ objects := make([]*npm_module.PackageSearchObject, 0, len(pds))
+ for _, pd := range pds {
+ metadata := pd.Metadata.(*npm_module.Metadata)
+
+ scope := metadata.Scope
+ if scope == "" {
+ scope = "unscoped"
+ }
+
+ objects = append(objects, &npm_module.PackageSearchObject{
+ Package: &npm_module.PackageSearchPackage{
+ Scope: scope,
+ Name: metadata.Name,
+ Version: pd.Version.Version,
+ Date: pd.Version.CreatedUnix.AsLocalTime(),
+ Description: metadata.Description,
+ Author: npm_module.User{Name: metadata.Author},
+ Publisher: npm_module.User{Name: pd.Owner.Name},
+ Maintainers: []npm_module.User{}, // npm cli needs this field
+ Keywords: metadata.Keywords,
+ Links: &npm_module.PackageSearchPackageLinks{
+ Registry: pd.FullWebLink(),
+ Homepage: metadata.ProjectURL,
+ },
+ },
+ })
+ }
+
+ return &npm_module.PackageSearch{
+ Objects: objects,
+ Total: total,
+ }
+}
diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go
index d127134d44558..0d25f173e922b 100644
--- a/routers/api/packages/npm/npm.go
+++ b/routers/api/packages/npm/npm.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package npm
@@ -18,6 +17,7 @@ import (
packages_module "code.gitea.io/gitea/modules/packages"
npm_module "code.gitea.io/gitea/modules/packages/npm"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
@@ -102,14 +102,67 @@ func DownloadPackageFile(ctx *context.Context) {
}
defer s.Close()
- ctx.ServeStream(s, pf.Name)
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
+}
+
+// DownloadPackageFileByName finds the version and serves the contents of a package
+func DownloadPackageFileByName(ctx *context.Context) {
+ filename := ctx.Params("filename")
+
+ pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages_model.TypeNpm,
+ Name: packages_model.SearchValue{
+ ExactMatch: true,
+ Value: packageNameFromParams(ctx),
+ },
+ HasFileWithName: filename,
+ IsInternal: util.OptionalBoolFalse,
+ })
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ if len(pvs) != 1 {
+ apiError(ctx, http.StatusNotFound, nil)
+ return
+ }
+
+ s, pf, err := packages_service.GetFileStreamByPackageVersion(
+ ctx,
+ pvs[0],
+ &packages_service.PackageFileInfo{
+ Filename: filename,
+ },
+ )
+ if err != nil {
+ if err == packages_model.ErrPackageFileNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer s.Close()
+
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
}
// UploadPackage creates a new package
func UploadPackage(ctx *context.Context) {
npmPackage, err := npm_module.ParsePackage(ctx.Req.Body)
if err != nil {
- apiError(ctx, http.StatusBadRequest, err)
+ if errors.Is(err, util.ErrInvalidArgument) {
+ apiError(ctx, http.StatusBadRequest, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
return
}
@@ -136,16 +189,20 @@ func UploadPackage(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: npmPackage.Filename,
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageVersion {
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
@@ -163,6 +220,63 @@ func UploadPackage(ctx *context.Context) {
ctx.Status(http.StatusCreated)
}
+// DeletePreview does nothing
+// The client tells the server what package version it knows about after deleting a version.
+func DeletePreview(ctx *context.Context) {
+ ctx.Status(http.StatusOK)
+}
+
+// DeletePackageVersion deletes the package version
+func DeletePackageVersion(ctx *context.Context) {
+ packageName := packageNameFromParams(ctx)
+ packageVersion := ctx.Params("version")
+
+ err := packages_service.RemovePackageVersionByNameAndVersion(
+ ctx.Doer,
+ &packages_service.PackageInfo{
+ Owner: ctx.Package.Owner,
+ PackageType: packages_model.TypeNpm,
+ Name: packageName,
+ Version: packageVersion,
+ },
+ )
+ if err != nil {
+ if err == packages_model.ErrPackageNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ ctx.Status(http.StatusOK)
+}
+
+// DeletePackage deletes the package and all versions
+func DeletePackage(ctx *context.Context) {
+ packageName := packageNameFromParams(ctx)
+
+ pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if len(pvs) == 0 {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+
+ for _, pv := range pvs {
+ if err := packages_service.RemovePackageVersion(ctx.Doer, pv); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ }
+
+ ctx.Status(http.StatusOK)
+}
+
// ListPackageTags returns all tags for a package
func ListPackageTags(ctx *context.Context) {
packageName := packageNameFromParams(ctx)
@@ -250,7 +364,7 @@ func setPackageTag(tag string, pv *packages_model.PackageVersion, deleteOnly boo
return errInvalidTagName
}
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
@@ -261,6 +375,7 @@ func setPackageTag(tag string, pv *packages_model.PackageVersion, deleteOnly boo
Properties: map[string]string{
npm_module.TagProperty: tag,
},
+ IsInternal: util.OptionalBoolFalse,
})
if err != nil {
return err
@@ -291,3 +406,36 @@ func setPackageTag(tag string, pv *packages_model.PackageVersion, deleteOnly boo
return committer.Commit()
}
+
+func PackageSearch(ctx *context.Context) {
+ pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages_model.TypeNpm,
+ IsInternal: util.OptionalBoolFalse,
+ Name: packages_model.SearchValue{
+ ExactMatch: false,
+ Value: ctx.FormTrim("text"),
+ },
+ Paginator: db.NewAbsoluteListOptions(
+ ctx.FormInt("from"),
+ ctx.FormInt("size"),
+ ),
+ })
+ 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 := createPackageSearchResponse(
+ pds,
+ total,
+ )
+
+ ctx.JSON(http.StatusOK, resp)
+}
diff --git a/routers/api/packages/nuget/api.go b/routers/api/packages/nuget/api.go
deleted file mode 100644
index b449cfc5bb3ae..0000000000000
--- a/routers/api/packages/nuget/api.go
+++ /dev/null
@@ -1,287 +0,0 @@
-// 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 nuget
-
-import (
- "bytes"
- "fmt"
- "sort"
- "time"
-
- packages_model "code.gitea.io/gitea/models/packages"
- nuget_module "code.gitea.io/gitea/modules/packages/nuget"
-
- "github.com/hashicorp/go-version"
-)
-
-// ServiceIndexResponse https://docs.microsoft.com/en-us/nuget/api/service-index#resources
-type ServiceIndexResponse struct {
- Version string `json:"version"`
- Resources []ServiceResource `json:"resources"`
-}
-
-// ServiceResource 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
-type RegistrationIndexResponse struct {
- RegistrationIndexURL string `json:"@id"`
- Type []string `json:"@type"`
- Count int `json:"count"`
- Pages []*RegistrationIndexPage `json:"items"`
-}
-
-// RegistrationIndexPage 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"`
- Upper string `json:"upper"`
- Count int `json:"count"`
- Items []*RegistrationIndexPageItem `json:"items"`
-}
-
-// RegistrationIndexPageItem 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
-type CatalogEntry struct {
- CatalogLeafURL string `json:"@id"`
- PackageContentURL string `json:"packageContent"`
- ID string `json:"id"`
- Version string `json:"version"`
- Description string `json:"description"`
- ReleaseNotes string `json:"releaseNotes"`
- Authors string `json:"authors"`
- RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
- ProjectURL string `json:"projectURL"`
- DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
-}
-
-// PackageDependencyGroup 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
-type PackageDependency struct {
- ID string `json:"id"`
- Range string `json:"range"`
-}
-
-func createRegistrationIndexResponse(l *linkBuilder, pds []*packages_model.PackageDescriptor) *RegistrationIndexResponse {
- sort.Slice(pds, func(i, j int) bool {
- return pds[i].SemVer.LessThan(pds[j].SemVer)
- })
-
- items := make([]*RegistrationIndexPageItem, 0, len(pds))
- for _, p := range pds {
- items = append(items, createRegistrationIndexPageItem(l, p))
- }
-
- return &RegistrationIndexResponse{
- RegistrationIndexURL: l.GetRegistrationIndexURL(pds[0].Package.Name),
- Type: []string{"catalog:CatalogRoot", "PackageRegistration", "catalog:Permalink"},
- Count: 1,
- Pages: []*RegistrationIndexPage{
- {
- RegistrationPageURL: l.GetRegistrationIndexURL(pds[0].Package.Name),
- Count: len(pds),
- Lower: normalizeVersion(pds[0].SemVer),
- Upper: normalizeVersion(pds[len(pds)-1].SemVer),
- Items: items,
- },
- },
- }
-}
-
-func createRegistrationIndexPageItem(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationIndexPageItem {
- metadata := pd.Metadata.(*nuget_module.Metadata)
-
- return &RegistrationIndexPageItem{
- RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
- PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
- CatalogEntry: &CatalogEntry{
- CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
- PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
- ID: pd.Package.Name,
- Version: pd.Version.Version,
- Description: metadata.Description,
- ReleaseNotes: metadata.ReleaseNotes,
- Authors: metadata.Authors,
- ProjectURL: metadata.ProjectURL,
- DependencyGroups: createDependencyGroups(pd),
- },
- }
-}
-
-func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDependencyGroup {
- metadata := pd.Metadata.(*nuget_module.Metadata)
-
- dependencyGroups := make([]*PackageDependencyGroup, 0, len(metadata.Dependencies))
- for k, v := range metadata.Dependencies {
- dependencies := make([]*PackageDependency, 0, len(v))
- for _, dep := range v {
- dependencies = append(dependencies, &PackageDependency{
- ID: dep.ID,
- Range: dep.Version,
- })
- }
-
- dependencyGroups = append(dependencyGroups, &PackageDependencyGroup{
- TargetFramework: k,
- Dependencies: dependencies,
- })
- }
- return dependencyGroups
-}
-
-// RegistrationLeafResponse 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"`
- Listed bool `json:"listed"`
- PackageContentURL string `json:"packageContent"`
- Published time.Time `json:"published"`
- RegistrationIndexURL string `json:"registration"`
-}
-
-func createRegistrationLeafResponse(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationLeafResponse {
- return &RegistrationLeafResponse{
- Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"},
- Listed: true,
- Published: time.Unix(int64(pd.Version.CreatedUnix), 0),
- RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
- PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
- RegistrationIndexURL: l.GetRegistrationIndexURL(pd.Package.Name),
- }
-}
-
-// PackageVersionsResponse https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#response
-type PackageVersionsResponse struct {
- Versions []string `json:"versions"`
-}
-
-func createPackageVersionsResponse(pds []*packages_model.PackageDescriptor) *PackageVersionsResponse {
- versions := make([]string, 0, len(pds))
- for _, pd := range pds {
- versions = append(versions, normalizeVersion(pd.SemVer))
- }
-
- return &PackageVersionsResponse{
- Versions: versions,
- }
-}
-
-// SearchResultResponse 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
-type SearchResult struct {
- ID string `json:"id"`
- Version string `json:"version"`
- Versions []*SearchResultVersion `json:"versions"`
- Description string `json:"description"`
- Authors string `json:"authors"`
- ProjectURL string `json:"projectURL"`
- RegistrationIndexURL string `json:"registration"`
-}
-
-// SearchResultVersion 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"`
- Downloads int64 `json:"downloads"`
-}
-
-func createSearchResultResponse(l *linkBuilder, totalHits int64, pds []*packages_model.PackageDescriptor) *SearchResultResponse {
- 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])
- }
- data = append(data, createSearchResult(l, group))
- }
-
- return &SearchResultResponse{
- TotalHits: totalHits,
- Data: data,
- }
-}
-
-func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor) *SearchResult {
- latest := pds[0]
- versions := make([]*SearchResultVersion, 0, len(pds))
- for _, pd := range pds {
- if latest.SemVer.LessThan(pd.SemVer) {
- latest = pd
- }
-
- versions = append(versions, &SearchResultVersion{
- RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
- Version: pd.Version.Version,
- })
- }
-
- metadata := latest.Metadata.(*nuget_module.Metadata)
-
- return &SearchResult{
- ID: latest.Package.Name,
- Version: latest.Version.Version,
- Versions: versions,
- Description: metadata.Description,
- Authors: metadata.Authors,
- ProjectURL: metadata.ProjectURL,
- RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name),
- }
-}
-
-// normalizeVersion removes the metadata
-func normalizeVersion(v *version.Version) string {
- var buf bytes.Buffer
- segments := v.Segments64()
- fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2])
- pre := v.Prerelease()
- if pre != "" {
- fmt.Fprintf(&buf, "-%s", pre)
- }
- return buf.String()
-}
diff --git a/routers/api/packages/nuget/api_v2.go b/routers/api/packages/nuget/api_v2.go
new file mode 100644
index 0000000000000..7d0ac64a8adc1
--- /dev/null
+++ b/routers/api/packages/nuget/api_v2.go
@@ -0,0 +1,392 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+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: pd.Version.Version,
+ 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_v3.go b/routers/api/packages/nuget/api_v3.go
new file mode 100644
index 0000000000000..28626f9294c31
--- /dev/null
+++ b/routers/api/packages/nuget/api_v3.go
@@ -0,0 +1,246 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package nuget
+
+import (
+ "sort"
+ "time"
+
+ packages_model "code.gitea.io/gitea/models/packages"
+ nuget_module "code.gitea.io/gitea/modules/packages/nuget"
+)
+
+// https://docs.microsoft.com/en-us/nuget/api/service-index#resources
+type ServiceIndexResponseV3 struct {
+ Version string `json:"version"`
+ Resources []ServiceResource `json:"resources"`
+}
+
+// https://docs.microsoft.com/en-us/nuget/api/service-index#resource
+type ServiceResource struct {
+ ID string `json:"@id"`
+ Type string `json:"@type"`
+}
+
+// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response
+type RegistrationIndexResponse struct {
+ RegistrationIndexURL string `json:"@id"`
+ Type []string `json:"@type"`
+ Count int `json:"count"`
+ Pages []*RegistrationIndexPage `json:"items"`
+}
+
+// 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"`
+ Upper string `json:"upper"`
+ Count int `json:"count"`
+ Items []*RegistrationIndexPageItem `json:"items"`
+}
+
+// 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"`
+}
+
+// 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"`
+ ID string `json:"id"`
+ Version string `json:"version"`
+ Description string `json:"description"`
+ ReleaseNotes string `json:"releaseNotes"`
+ Authors string `json:"authors"`
+ RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
+ ProjectURL string `json:"projectURL"`
+ DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
+}
+
+// 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"`
+}
+
+// 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"`
+}
+
+func createRegistrationIndexResponse(l *linkBuilder, pds []*packages_model.PackageDescriptor) *RegistrationIndexResponse {
+ sort.Slice(pds, func(i, j int) bool {
+ return pds[i].SemVer.LessThan(pds[j].SemVer)
+ })
+
+ items := make([]*RegistrationIndexPageItem, 0, len(pds))
+ for _, p := range pds {
+ items = append(items, createRegistrationIndexPageItem(l, p))
+ }
+
+ return &RegistrationIndexResponse{
+ RegistrationIndexURL: l.GetRegistrationIndexURL(pds[0].Package.Name),
+ Type: []string{"catalog:CatalogRoot", "PackageRegistration", "catalog:Permalink"},
+ Count: 1,
+ Pages: []*RegistrationIndexPage{
+ {
+ RegistrationPageURL: l.GetRegistrationIndexURL(pds[0].Package.Name),
+ Count: len(pds),
+ Lower: pds[0].Version.Version,
+ Upper: pds[len(pds)-1].Version.Version,
+ Items: items,
+ },
+ },
+ }
+}
+
+func createRegistrationIndexPageItem(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationIndexPageItem {
+ metadata := pd.Metadata.(*nuget_module.Metadata)
+
+ return &RegistrationIndexPageItem{
+ RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
+ PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
+ CatalogEntry: &CatalogEntry{
+ CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
+ PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
+ ID: pd.Package.Name,
+ Version: pd.Version.Version,
+ Description: metadata.Description,
+ ReleaseNotes: metadata.ReleaseNotes,
+ Authors: metadata.Authors,
+ ProjectURL: metadata.ProjectURL,
+ DependencyGroups: createDependencyGroups(pd),
+ },
+ }
+}
+
+func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDependencyGroup {
+ metadata := pd.Metadata.(*nuget_module.Metadata)
+
+ dependencyGroups := make([]*PackageDependencyGroup, 0, len(metadata.Dependencies))
+ for k, v := range metadata.Dependencies {
+ dependencies := make([]*PackageDependency, 0, len(v))
+ for _, dep := range v {
+ dependencies = append(dependencies, &PackageDependency{
+ ID: dep.ID,
+ Range: dep.Version,
+ })
+ }
+
+ dependencyGroups = append(dependencyGroups, &PackageDependencyGroup{
+ TargetFramework: k,
+ Dependencies: dependencies,
+ })
+ }
+ return dependencyGroups
+}
+
+// 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"`
+ Listed bool `json:"listed"`
+ PackageContentURL string `json:"packageContent"`
+ Published time.Time `json:"published"`
+ RegistrationIndexURL string `json:"registration"`
+}
+
+func createRegistrationLeafResponse(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationLeafResponse {
+ return &RegistrationLeafResponse{
+ Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"},
+ Listed: true,
+ Published: pd.Version.CreatedUnix.AsLocalTime(),
+ RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
+ PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
+ RegistrationIndexURL: l.GetRegistrationIndexURL(pd.Package.Name),
+ }
+}
+
+// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#response
+type PackageVersionsResponse struct {
+ Versions []string `json:"versions"`
+}
+
+func createPackageVersionsResponse(pds []*packages_model.PackageDescriptor) *PackageVersionsResponse {
+ versions := make([]string, 0, len(pds))
+ for _, pd := range pds {
+ versions = append(versions, pd.Version.Version)
+ }
+
+ return &PackageVersionsResponse{
+ Versions: versions,
+ }
+}
+
+// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#response
+type SearchResultResponse struct {
+ TotalHits int64 `json:"totalHits"`
+ Data []*SearchResult `json:"data"`
+}
+
+// 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"`
+ Versions []*SearchResultVersion `json:"versions"`
+ Description string `json:"description"`
+ Authors string `json:"authors"`
+ ProjectURL string `json:"projectURL"`
+ RegistrationIndexURL string `json:"registration"`
+}
+
+// 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"`
+ Downloads int64 `json:"downloads"`
+}
+
+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))
+ for _, group := range grouped {
+ data = append(data, createSearchResult(l, group))
+ }
+
+ return &SearchResultResponse{
+ TotalHits: totalHits,
+ Data: data,
+ }
+}
+
+func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor) *SearchResult {
+ latest := pds[0]
+ versions := make([]*SearchResultVersion, 0, len(pds))
+ for _, pd := range pds {
+ if latest.SemVer.LessThan(pd.SemVer) {
+ latest = pd
+ }
+
+ versions = append(versions, &SearchResultVersion{
+ RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
+ Version: pd.Version.Version,
+ })
+ }
+
+ metadata := latest.Metadata.(*nuget_module.Metadata)
+
+ return &SearchResult{
+ ID: latest.Package.Name,
+ Version: latest.Version.Version,
+ Versions: versions,
+ Description: metadata.Description,
+ Authors: metadata.Authors,
+ ProjectURL: metadata.ProjectURL,
+ RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name),
+ }
+}
diff --git a/routers/api/packages/nuget/auth.go b/routers/api/packages/nuget/auth.go
new file mode 100644
index 0000000000000..54b33d89c0a86
--- /dev/null
+++ b/routers/api/packages/nuget/auth.go
@@ -0,0 +1,45 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package nuget
+
+import (
+ "net/http"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/services/auth"
+)
+
+type Auth struct{}
+
+func (a *Auth) Name() string {
+ return "nuget"
+}
+
+// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#request-parameters
+func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
+ token, err := auth_model.GetAccessTokenBySHA(req.Header.Get("X-NuGet-ApiKey"))
+ if err != nil {
+ if !(auth_model.IsErrAccessTokenNotExist(err) || auth_model.IsErrAccessTokenEmpty(err)) {
+ log.Error("GetAccessTokenBySHA: %v", err)
+ return nil, err
+ }
+ return nil, nil
+ }
+
+ u, err := user_model.GetUserByID(req.Context(), token.UID)
+ if err != nil {
+ log.Error("GetUserByID: %v", err)
+ return nil, err
+ }
+
+ token.UpdatedUnix = timeutil.TimeStampNow()
+ if err := auth_model.UpdateAccessToken(token); err != nil {
+ log.Error("UpdateAccessToken: %v", err)
+ }
+
+ return u, nil
+}
diff --git a/routers/api/packages/nuget/links.go b/routers/api/packages/nuget/links.go
index f782c7f2cbc1c..1b02e46184675 100644
--- a/routers/api/packages/nuget/links.go
+++ b/routers/api/packages/nuget/links.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package nuget
@@ -26,3 +25,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 013c0c1e33543..6423db7d3a214 100644
--- a/routers/api/packages/nuget/nuget.go
+++ b/routers/api/packages/nuget/nuget.go
@@ -1,22 +1,25 @@
// 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.
+// SPDX-License-Identifier: MIT
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"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -29,19 +32,126 @@ 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")
+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)
+ }
+}
- ctx.JSON(http.StatusOK, resp)
+// 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)
}
-// SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
-func SearchService(ctx *context.Context) {
+// 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,
- Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages_model.TypeNuGet,
+ Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
+ IsInternal: util.OptionalBoolFalse,
Paginator: db.NewAbsoluteListOptions(
ctx.FormInt("skip"),
ctx.FormInt("take"),
@@ -67,7 +177,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")
@@ -95,12 +205,41 @@ 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")
- pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion)
+ 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)
@@ -124,8 +263,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)
@@ -149,7 +313,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")
@@ -177,7 +341,10 @@ func DownloadPackageFile(ctx *context.Context) {
}
defer s.Close()
- ctx.ServeStream(s, pf.Name)
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
}
// UploadPackage creates a new package with the metadata contained in the uploaded nupgk file
@@ -209,16 +376,20 @@ func UploadPackage(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(fmt.Sprintf("%s.%s.nupkg", np.ID, np.Version)),
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageVersion {
- apiError(ctx, http.StatusBadRequest, err)
- return
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
+ apiError(ctx, http.StatusConflict, err)
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
@@ -240,7 +411,11 @@ func UploadSymbolPackage(ctx *context.Context) {
pdbs, err := nuget_module.ExtractPortablePdb(buf, buf.Size())
if err != nil {
- apiError(ctx, http.StatusBadRequest, err)
+ if errors.Is(err, util.ErrInvalidArgument) {
+ apiError(ctx, http.StatusBadRequest, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
return
}
defer pdbs.Close()
@@ -263,8 +438,9 @@ func UploadSymbolPackage(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(fmt.Sprintf("%s.%s.snupkg", np.ID, np.Version)),
},
- Data: buf,
- IsLead: false,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: false,
},
)
if err != nil {
@@ -272,7 +448,9 @@ func UploadSymbolPackage(ctx *context.Context) {
case packages_model.ErrPackageNotExist:
apiError(ctx, http.StatusNotFound, err)
case packages_model.ErrDuplicatePackageFile:
- apiError(ctx, http.StatusBadRequest, err)
+ apiError(ctx, http.StatusConflict, err)
+ case packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
@@ -287,8 +465,9 @@ func UploadSymbolPackage(ctx *context.Context) {
Filename: strings.ToLower(pdb.Name),
CompositeKey: strings.ToLower(pdb.ID),
},
- Data: pdb.Content,
- IsLead: false,
+ Creator: ctx.Doer,
+ Data: pdb.Content,
+ IsLead: false,
Properties: map[string]string{
nuget_module.PropertySymbolID: strings.ToLower(pdb.ID),
},
@@ -297,7 +476,9 @@ func UploadSymbolPackage(ctx *context.Context) {
if err != nil {
switch err {
case packages_model.ErrDuplicatePackageFile:
- apiError(ctx, http.StatusBadRequest, err)
+ apiError(ctx, http.StatusConflict, err)
+ case packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
@@ -330,7 +511,7 @@ func processUploadedFile(ctx *context.Context, expectedType nuget_module.Package
np, err := nuget_module.ParsePackageMetaData(buf, buf.Size())
if err != nil {
- if err == nuget_module.ErrMissingNuspecFile || err == nuget_module.ErrNuspecFileTooLarge || err == nuget_module.ErrNuspecInvalidID || err == nuget_module.ErrNuspecInvalidVersion {
+ if errors.Is(err, util.ErrInvalidArgument) {
apiError(ctx, http.StatusBadRequest, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
@@ -348,10 +529,10 @@ 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")
+ guid := ctx.Params("guid")[:32]
filename2 := ctx.Params("filename2")
if filename != filename2 {
@@ -376,7 +557,7 @@ func DownloadSymbolFile(ctx *context.Context) {
return
}
- s, _, err := packages_service.GetPackageFileStream(ctx, pfs[0])
+ s, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
if err != nil {
if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
apiError(ctx, http.StatusNotFound, err)
@@ -387,7 +568,10 @@ func DownloadSymbolFile(ctx *context.Context) {
}
defer s.Close()
- ctx.ServeStream(s, pfs[0].Name)
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
}
// DeletePackage hard deletes the package
@@ -412,4 +596,6 @@ func DeletePackage(ctx *context.Context) {
}
apiError(ctx, http.StatusInternalServerError, err)
}
+
+ ctx.Status(http.StatusNoContent)
}
diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go
new file mode 100644
index 0000000000000..1ece4e18ed6d4
--- /dev/null
+++ b/routers/api/packages/pub/pub.go
@@ -0,0 +1,287 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pub
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "sort"
+ "strings"
+ "time"
+
+ packages_model "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ packages_module "code.gitea.io/gitea/modules/packages"
+ pub_module "code.gitea.io/gitea/modules/packages/pub"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/api/packages/helper"
+ packages_service "code.gitea.io/gitea/services/packages"
+)
+
+func jsonResponse(ctx *context.Context, status int, obj interface{}) {
+ resp := ctx.Resp
+ resp.Header().Set("Content-Type", "application/vnd.pub.v2+json")
+ resp.WriteHeader(status)
+ if err := json.NewEncoder(resp).Encode(obj); err != nil {
+ log.Error("JSON encode: %v", err)
+ }
+}
+
+func apiError(ctx *context.Context, status int, obj interface{}) {
+ type Error struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+ }
+ type ErrorWrapper struct {
+ Error Error `json:"error"`
+ }
+
+ helper.LogAndProcessError(ctx, status, obj, func(message string) {
+ jsonResponse(ctx, status, ErrorWrapper{
+ Error: Error{
+ Code: http.StatusText(status),
+ Message: message,
+ },
+ })
+ })
+}
+
+type packageVersions struct {
+ Name string `json:"name"`
+ Latest *versionMetadata `json:"latest"`
+ Versions []*versionMetadata `json:"versions"`
+}
+
+type versionMetadata struct {
+ Version string `json:"version"`
+ ArchiveURL string `json:"archive_url"`
+ Published time.Time `json:"published"`
+ Pubspec interface{} `json:"pubspec,omitempty"`
+}
+
+func packageDescriptorToMetadata(baseURL string, pd *packages_model.PackageDescriptor) *versionMetadata {
+ return &versionMetadata{
+ Version: pd.Version.Version,
+ ArchiveURL: fmt.Sprintf("%s/files/%s.tar.gz", baseURL, url.PathEscape(pd.Version.Version)),
+ Published: pd.Version.CreatedUnix.AsLocalTime(),
+ Pubspec: pd.Metadata.(*pub_module.Metadata).Pubspec,
+ }
+}
+
+func baseURL(ctx *context.Context) string {
+ return setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/pub/api/packages"
+}
+
+// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#list-all-versions-of-a-package
+func EnumeratePackageVersions(ctx *context.Context) {
+ packageName := ctx.Params("id")
+
+ pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ if len(pvs) == 0 {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+
+ pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ sort.Slice(pds, func(i, j int) bool {
+ return pds[i].SemVer.LessThan(pds[j].SemVer)
+ })
+
+ baseURL := fmt.Sprintf("%s/%s", baseURL(ctx), url.PathEscape(pds[0].Package.Name))
+
+ versions := make([]*versionMetadata, 0, len(pds))
+ for _, pd := range pds {
+ versions = append(versions, packageDescriptorToMetadata(baseURL, pd))
+ }
+
+ jsonResponse(ctx, http.StatusOK, &packageVersions{
+ Name: pds[0].Package.Name,
+ Latest: packageDescriptorToMetadata(baseURL, pds[0]),
+ Versions: versions,
+ })
+}
+
+// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#deprecated-inspect-a-specific-version-of-a-package
+func PackageVersionMetadata(ctx *context.Context) {
+ packageName := ctx.Params("id")
+ packageVersion := ctx.Params("version")
+
+ pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, 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
+ }
+
+ jsonResponse(ctx, http.StatusOK, packageDescriptorToMetadata(
+ fmt.Sprintf("%s/%s", baseURL(ctx), url.PathEscape(pd.Package.Name)),
+ pd,
+ ))
+}
+
+// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#publishing-packages
+func RequestUpload(ctx *context.Context) {
+ type UploadRequest struct {
+ URL string `json:"url"`
+ Fields map[string]string `json:"fields"`
+ }
+
+ jsonResponse(ctx, http.StatusOK, UploadRequest{
+ URL: baseURL(ctx) + "/versions/new/upload",
+ Fields: make(map[string]string),
+ })
+}
+
+// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#publishing-packages
+func UploadPackageFile(ctx *context.Context) {
+ file, _, err := ctx.Req.FormFile("file")
+ if err != nil {
+ apiError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ defer file.Close()
+
+ buf, err := packages_module.CreateHashedBufferFromReader(file, 32*1024*1024)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer buf.Close()
+
+ pck, err := pub_module.ParsePackage(buf)
+ if err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ apiError(ctx, http.StatusBadRequest, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ if _, err := buf.Seek(0, io.SeekStart); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ _, _, err = packages_service.CreatePackageAndAddFile(
+ &packages_service.PackageCreationInfo{
+ PackageInfo: packages_service.PackageInfo{
+ Owner: ctx.Package.Owner,
+ PackageType: packages_model.TypePub,
+ Name: pck.Name,
+ Version: pck.Version,
+ },
+ SemverCompatible: true,
+ Creator: ctx.Doer,
+ Metadata: pck.Metadata,
+ },
+ &packages_service.PackageFileCreationInfo{
+ PackageFileInfo: packages_service.PackageFileInfo{
+ Filename: strings.ToLower(pck.Version + ".tar.gz"),
+ },
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
+ },
+ )
+ if err != nil {
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
+ apiError(ctx, http.StatusBadRequest, err)
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.Resp.Header().Set("Location", fmt.Sprintf("%s/versions/new/finalize/%s/%s", baseURL(ctx), url.PathEscape(pck.Name), url.PathEscape(pck.Version)))
+ ctx.Status(http.StatusNoContent)
+}
+
+// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#publishing-packages
+func FinalizePackage(ctx *context.Context) {
+ packageName := ctx.Params("id")
+ packageVersion := ctx.Params("version")
+
+ _, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion)
+ if err != nil {
+ if err == packages_model.ErrPackageNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ type Success struct {
+ Message string `json:"message"`
+ }
+ type SuccessWrapper struct {
+ Success Success `json:"success"`
+ }
+
+ jsonResponse(ctx, http.StatusOK, SuccessWrapper{Success{}})
+}
+
+// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md#deprecated-download-a-specific-version-of-a-package
+func DownloadPackageFile(ctx *context.Context) {
+ packageName := ctx.Params("id")
+ packageVersion := strings.TrimSuffix(ctx.Params("version"), ".tar.gz")
+
+ pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, 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
+ }
+
+ pf := pd.Files[0].File
+
+ s, _, err := packages_service.GetPackageFileStream(ctx, pf)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer s.Close()
+
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
+}
diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go
index 9209c4edd5501..609c63dc64f5a 100644
--- a/routers/api/packages/pypi/pypi.go
+++ b/routers/api/packages/pypi/pypi.go
@@ -1,11 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package pypi
import (
- "fmt"
+ "encoding/hex"
"io"
"net/http"
"regexp"
@@ -16,18 +15,26 @@ import (
packages_module "code.gitea.io/gitea/modules/packages"
pypi_module "code.gitea.io/gitea/modules/packages/pypi"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
)
-// https://www.python.org/dev/peps/pep-0503/#normalized-names
-var normalizer = strings.NewReplacer(".", "-", "_", "-")
-var nameMatcher = regexp.MustCompile(`\A[a-z0-9\.\-_]+\z`)
+// https://peps.python.org/pep-0426/#name
+var (
+ normalizer = strings.NewReplacer(".", "-", "_", "-")
+ nameMatcher = regexp.MustCompile(`\A(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\.\-_]*[a-zA-Z0-9])\z`)
+)
-// https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
-var versionMatcher = regexp.MustCompile(`^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$`)
+// https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
+var versionMatcher = regexp.MustCompile(`\Av?` +
+ `(?:[0-9]+!)?` + // epoch
+ `[0-9]+(?:\.[0-9]+)*` + // release segment
+ `(?:[-_\.]?(?:a|b|c|rc|alpha|beta|pre|preview)[-_\.]?[0-9]*)?` + // pre-release
+ `(?:-[0-9]+|[-_\.]?(?:post|rev|r)[-_\.]?[0-9]*)?` + // post release
+ `(?:[-_\.]?dev[-_\.]?[0-9]*)?` + // dev release
+ `(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)?` + // local version
+ `\z`)
func apiError(ctx *context.Context, status int, obj interface{}) {
helper.LogAndProcessError(ctx, status, obj, func(message string) {
@@ -58,7 +65,6 @@ func PackageMetadata(ctx *context.Context) {
ctx.Data["RegistryURL"] = setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/pypi"
ctx.Data["PackageDescriptor"] = pds[0]
ctx.Data["PackageDescriptors"] = pds
- ctx.Render = templates.HTMLRenderer()
ctx.HTML(http.StatusOK, "api/packages/pypi/simple")
}
@@ -90,7 +96,10 @@ func DownloadPackageFile(ctx *context.Context) {
}
defer s.Close()
- ctx.ServeStream(s, pf.Name)
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
}
// UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
@@ -111,7 +120,7 @@ func UploadPackageFile(ctx *context.Context) {
_, _, hashSHA256, _ := buf.Sums()
- if !strings.EqualFold(ctx.Req.FormValue("sha256_digest"), fmt.Sprintf("%x", hashSHA256)) {
+ if !strings.EqualFold(ctx.Req.FormValue("sha256_digest"), hex.EncodeToString(hashSHA256)) {
apiError(ctx, http.StatusBadRequest, "hash mismatch")
return
}
@@ -123,7 +132,7 @@ func UploadPackageFile(ctx *context.Context) {
packageName := normalizer.Replace(ctx.Req.FormValue("name"))
packageVersion := ctx.Req.FormValue("version")
- if !nameMatcher.MatchString(packageName) || !versionMatcher.MatchString(packageVersion) {
+ if !isValidNameAndVersion(packageName, packageVersion) {
apiError(ctx, http.StatusBadRequest, "invalid name or version")
return
}
@@ -141,7 +150,7 @@ func UploadPackageFile(ctx *context.Context) {
Name: packageName,
Version: packageVersion,
},
- SemverCompatible: true,
+ SemverCompatible: false,
Creator: ctx.Doer,
Metadata: &pypi_module.Metadata{
Author: ctx.Req.FormValue("author"),
@@ -157,18 +166,26 @@ func UploadPackageFile(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: fileHeader.Filename,
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageFile {
+ switch err {
+ case packages_model.ErrDuplicatePackageFile:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.Status(http.StatusCreated)
}
+
+func isValidNameAndVersion(packageName, packageVersion string) bool {
+ return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion)
+}
diff --git a/routers/api/packages/pypi/pypi_test.go b/routers/api/packages/pypi/pypi_test.go
new file mode 100644
index 0000000000000..3023692177fd9
--- /dev/null
+++ b/routers/api/packages/pypi/pypi_test.go
@@ -0,0 +1,38 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pypi
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIsValidNameAndVersion(t *testing.T) {
+ // The test cases below were created from the following Python PEPs:
+ // https://peps.python.org/pep-0426/#name
+ // https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
+
+ // Valid Cases
+ assert.True(t, isValidNameAndVersion("A", "1.0.1"))
+ assert.True(t, isValidNameAndVersion("Test.Name.1234", "1.0.1"))
+ assert.True(t, isValidNameAndVersion("test_name", "1.0.1"))
+ assert.True(t, isValidNameAndVersion("test-name", "1.0.1"))
+ assert.True(t, isValidNameAndVersion("test-name", "v1.0.1"))
+ assert.True(t, isValidNameAndVersion("test-name", "2012.4"))
+ assert.True(t, isValidNameAndVersion("test-name", "1.0.1-alpha"))
+ assert.True(t, isValidNameAndVersion("test-name", "1.0.1a1"))
+ assert.True(t, isValidNameAndVersion("test-name", "1.0b2.r345.dev456"))
+ assert.True(t, isValidNameAndVersion("test-name", "1!1.0.1"))
+ assert.True(t, isValidNameAndVersion("test-name", "1.0.1+local.1"))
+
+ // Invalid Cases
+ assert.False(t, isValidNameAndVersion(".test-name", "1.0.1"))
+ assert.False(t, isValidNameAndVersion("test!name", "1.0.1"))
+ assert.False(t, isValidNameAndVersion("-test-name", "1.0.1"))
+ assert.False(t, isValidNameAndVersion("test-name-", "1.0.1"))
+ assert.False(t, isValidNameAndVersion("test-name", "a1.0.1"))
+ assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa"))
+ assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta"))
+}
diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go
index 6fdd03e8ea704..af358fb82fbad 100644
--- a/routers/api/packages/rubygems/rubygems.go
+++ b/routers/api/packages/rubygems/rubygems.go
@@ -1,12 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package rubygems
import (
"compress/gzip"
"compress/zlib"
+ "errors"
"fmt"
"io"
"net/http"
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/context"
packages_module "code.gitea.io/gitea/modules/packages"
rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -37,11 +38,12 @@ func EnumeratePackages(ctx *context.Context) {
enumeratePackages(ctx, "specs.4.8", packages)
}
-// EnumeratePackagesLatest serves the list of the lastest version of every package
+// EnumeratePackagesLatest serves the list of the latest version of every package
func EnumeratePackagesLatest(ctx *context.Context) {
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
- OwnerID: ctx.Package.Owner.ID,
- Type: packages_model.TypeRubyGems,
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages_model.TypeRubyGems,
+ IsInternal: util.OptionalBoolFalse,
})
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -75,7 +77,9 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo
})
}
- ctx.SetServeHeaders(filename + ".gz")
+ ctx.SetServeHeaders(&context.ServeHeaderOptions{
+ Filename: filename + ".gz",
+ })
zw := gzip.NewWriter(ctx.Resp)
defer zw.Close()
@@ -113,7 +117,9 @@ func ServePackageSpecification(ctx *context.Context) {
return
}
- ctx.SetServeHeaders(filename)
+ ctx.SetServeHeaders(&context.ServeHeaderOptions{
+ Filename: filename,
+ })
zw := zlib.NewWriter(ctx.Resp)
defer zw.Close()
@@ -186,7 +192,10 @@ func DownloadPackageFile(ctx *context.Context) {
}
defer s.Close()
- ctx.ServeStream(s, pf.Name)
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
}
// UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
@@ -209,7 +218,11 @@ func UploadPackageFile(ctx *context.Context) {
rp, err := rubygems_module.ParsePackageMetaData(buf)
if err != nil {
- apiError(ctx, http.StatusInternalServerError, err)
+ if errors.Is(err, util.ErrInvalidArgument) {
+ apiError(ctx, http.StatusBadRequest, err)
+ } else {
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
return
}
if _, err := buf.Seek(0, io.SeekStart); err != nil {
@@ -240,16 +253,20 @@ func UploadPackageFile(ctx *context.Context) {
PackageFileInfo: packages_service.PackageFileInfo{
Filename: filename,
},
- Data: buf,
- IsLead: true,
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
},
)
if err != nil {
- if err == packages_model.ErrDuplicatePackageVersion {
+ switch err {
+ case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusBadRequest, err)
- return
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
}
- apiError(ctx, http.StatusInternalServerError, err)
return
}
@@ -289,6 +306,7 @@ func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_m
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeRubyGems,
HasFileWithName: filename,
+ IsInternal: util.OptionalBoolFalse,
})
return pvs, err
}
diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go
new file mode 100644
index 0000000000000..7b76ab79b0580
--- /dev/null
+++ b/routers/api/packages/vagrant/vagrant.go
@@ -0,0 +1,245 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package vagrant
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "sort"
+ "strings"
+
+ packages_model "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/modules/context"
+ packages_module "code.gitea.io/gitea/modules/packages"
+ vagrant_module "code.gitea.io/gitea/modules/packages/vagrant"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/routers/api/packages/helper"
+ packages_service "code.gitea.io/gitea/services/packages"
+
+ "github.com/hashicorp/go-version"
+)
+
+func apiError(ctx *context.Context, status int, obj interface{}) {
+ helper.LogAndProcessError(ctx, status, obj, func(message string) {
+ ctx.JSON(status, struct {
+ Errors []string `json:"errors"`
+ }{
+ Errors: []string{
+ message,
+ },
+ })
+ })
+}
+
+func CheckAuthenticate(ctx *context.Context) {
+ if ctx.Doer == nil {
+ apiError(ctx, http.StatusUnauthorized, "Invalid access token")
+ return
+ }
+
+ ctx.Status(http.StatusOK)
+}
+
+func CheckBoxAvailable(ctx *context.Context) {
+ pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeVagrant, ctx.Params("name"))
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ if len(pvs) == 0 {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, nil) // needs to be Content-Type: application/json
+}
+
+type packageMetadata struct {
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ ShortDescription string `json:"short_description,omitempty"`
+ Versions []*versionMetadata `json:"versions"`
+}
+
+type versionMetadata struct {
+ Version string `json:"version"`
+ Status string `json:"status"`
+ DescriptionHTML string `json:"description_html,omitempty"`
+ DescriptionMarkdown string `json:"description_markdown,omitempty"`
+ Providers []*providerData `json:"providers"`
+}
+
+type providerData struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ Checksum string `json:"checksum"`
+ ChecksumType string `json:"checksum_type"`
+}
+
+func packageDescriptorToMetadata(baseURL string, pd *packages_model.PackageDescriptor) *versionMetadata {
+ versionURL := baseURL + "/" + url.PathEscape(pd.Version.Version)
+
+ providers := make([]*providerData, 0, len(pd.Files))
+
+ for _, f := range pd.Files {
+ providers = append(providers, &providerData{
+ Name: f.Properties.GetByName(vagrant_module.PropertyProvider),
+ URL: versionURL + "/" + url.PathEscape(f.File.Name),
+ Checksum: f.Blob.HashSHA512,
+ ChecksumType: "sha512",
+ })
+ }
+
+ return &versionMetadata{
+ Status: "active",
+ Version: pd.Version.Version,
+ Providers: providers,
+ }
+}
+
+func EnumeratePackageVersions(ctx *context.Context) {
+ pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeVagrant, ctx.Params("name"))
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ if len(pvs) == 0 {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+
+ pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ sort.Slice(pds, func(i, j int) bool {
+ return pds[i].SemVer.LessThan(pds[j].SemVer)
+ })
+
+ baseURL := fmt.Sprintf("%sapi/packages/%s/vagrant/%s", setting.AppURL, url.PathEscape(ctx.Package.Owner.Name), url.PathEscape(pds[0].Package.Name))
+
+ versions := make([]*versionMetadata, 0, len(pds))
+ for _, pd := range pds {
+ versions = append(versions, packageDescriptorToMetadata(baseURL, pd))
+ }
+
+ ctx.JSON(http.StatusOK, &packageMetadata{
+ Name: pds[0].Package.Name,
+ Description: pds[len(pds)-1].Metadata.(*vagrant_module.Metadata).Description,
+ Versions: versions,
+ })
+}
+
+func UploadPackageFile(ctx *context.Context) {
+ boxName := ctx.Params("name")
+ boxVersion := ctx.Params("version")
+ _, err := version.NewSemver(boxVersion)
+ if err != nil {
+ apiError(ctx, http.StatusBadRequest, err)
+ return
+ }
+ boxProvider := ctx.Params("provider")
+ if !strings.HasSuffix(boxProvider, ".box") {
+ apiError(ctx, http.StatusBadRequest, err)
+ return
+ }
+
+ upload, needsClose, err := ctx.UploadStream()
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ if needsClose {
+ defer upload.Close()
+ }
+
+ buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer buf.Close()
+
+ metadata, err := vagrant_module.ParseMetadataFromBox(buf)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ if _, err := buf.Seek(0, io.SeekStart); err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ _, _, err = packages_service.CreatePackageOrAddFileToExisting(
+ &packages_service.PackageCreationInfo{
+ PackageInfo: packages_service.PackageInfo{
+ Owner: ctx.Package.Owner,
+ PackageType: packages_model.TypeVagrant,
+ Name: boxName,
+ Version: boxVersion,
+ },
+ SemverCompatible: true,
+ Creator: ctx.Doer,
+ Metadata: metadata,
+ },
+ &packages_service.PackageFileCreationInfo{
+ PackageFileInfo: packages_service.PackageFileInfo{
+ Filename: strings.ToLower(boxProvider),
+ },
+ Creator: ctx.Doer,
+ Data: buf,
+ IsLead: true,
+ Properties: map[string]string{
+ vagrant_module.PropertyProvider: strings.TrimSuffix(boxProvider, ".box"),
+ },
+ },
+ )
+ if err != nil {
+ switch err {
+ case packages_model.ErrDuplicatePackageFile:
+ apiError(ctx, http.StatusConflict, err)
+ case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+ apiError(ctx, http.StatusForbidden, err)
+ default:
+ apiError(ctx, http.StatusInternalServerError, err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusCreated)
+}
+
+func DownloadPackageFile(ctx *context.Context) {
+ s, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ ctx,
+ &packages_service.PackageInfo{
+ Owner: ctx.Package.Owner,
+ PackageType: packages_model.TypeVagrant,
+ Name: ctx.Params("name"),
+ Version: ctx.Params("version"),
+ },
+ &packages_service.PackageFileInfo{
+ Filename: ctx.Params("provider"),
+ },
+ )
+ if err != nil {
+ if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ defer s.Close()
+
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
+}
diff --git a/routers/api/v1/activitypub/person.go b/routers/api/v1/activitypub/person.go
new file mode 100644
index 0000000000000..80855809856c2
--- /dev/null
+++ b/routers/api/v1/activitypub/person.go
@@ -0,0 +1,104 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package activitypub
+
+import (
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/modules/activitypub"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+
+ ap "github.com/go-ap/activitypub"
+ "github.com/go-ap/jsonld"
+)
+
+// Person function returns the Person actor for a user
+func Person(ctx *context.APIContext) {
+ // swagger:operation GET /activitypub/user/{username} activitypub activitypubPerson
+ // ---
+ // summary: Returns the Person actor for a user
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: username
+ // in: path
+ // description: username of the user
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActivityPub"
+
+ link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name
+ person := ap.PersonNew(ap.IRI(link))
+
+ person.Name = ap.NaturalLanguageValuesNew()
+ err := person.Name.Set("en", ap.Content(ctx.ContextUser.FullName))
+ if err != nil {
+ ctx.ServerError("Set Name", err)
+ return
+ }
+
+ person.PreferredUsername = ap.NaturalLanguageValuesNew()
+ err = person.PreferredUsername.Set("en", ap.Content(ctx.ContextUser.Name))
+ if err != nil {
+ ctx.ServerError("Set PreferredUsername", err)
+ return
+ }
+
+ person.URL = ap.IRI(ctx.ContextUser.HTMLURL())
+
+ person.Icon = ap.Image{
+ Type: ap.ImageType,
+ MediaType: "image/png",
+ URL: ap.IRI(ctx.ContextUser.AvatarLink()),
+ }
+
+ person.Inbox = ap.IRI(link + "/inbox")
+ person.Outbox = ap.IRI(link + "/outbox")
+
+ person.PublicKey.ID = ap.IRI(link + "#main-key")
+ person.PublicKey.Owner = ap.IRI(link)
+
+ publicKeyPem, err := activitypub.GetPublicKey(ctx.ContextUser)
+ if err != nil {
+ ctx.ServerError("GetPublicKey", err)
+ return
+ }
+ person.PublicKey.PublicKeyPem = publicKeyPem
+
+ binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(person)
+ if err != nil {
+ ctx.ServerError("MarshalJSON", err)
+ return
+ }
+ ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType)
+ ctx.Resp.WriteHeader(http.StatusOK)
+ if _, err = ctx.Resp.Write(binary); err != nil {
+ log.Error("write to resp err: %v", err)
+ }
+}
+
+// PersonInbox function handles the incoming data for a user inbox
+func PersonInbox(ctx *context.APIContext) {
+ // swagger:operation POST /activitypub/user/{username}/inbox activitypub activitypubPersonInbox
+ // ---
+ // summary: Send to the inbox
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: username
+ // in: path
+ // description: username of the user
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/activitypub/reqsignature.go b/routers/api/v1/activitypub/reqsignature.go
new file mode 100644
index 0000000000000..2d945c27a51cb
--- /dev/null
+++ b/routers/api/v1/activitypub/reqsignature.go
@@ -0,0 +1,101 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package activitypub
+
+import (
+ "crypto"
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+
+ "code.gitea.io/gitea/modules/activitypub"
+ gitea_context "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/httplib"
+ "code.gitea.io/gitea/modules/setting"
+
+ ap "github.com/go-ap/activitypub"
+ "github.com/go-fed/httpsig"
+)
+
+func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err error) {
+ person := ap.PersonNew(ap.IRI(keyID.String()))
+ err = person.UnmarshalJSON(b)
+ if err != nil {
+ err = fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
+ return
+ }
+ pubKey := person.PublicKey
+ if pubKey.ID.String() != keyID.String() {
+ err = fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, string(b))
+ return
+ }
+ pubKeyPem := pubKey.PublicKeyPem
+ block, _ := pem.Decode([]byte(pubKeyPem))
+ if block == nil || block.Type != "PUBLIC KEY" {
+ err = fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type")
+ return
+ }
+ p, err = x509.ParsePKIXPublicKey(block.Bytes)
+ return p, err
+}
+
+func fetch(iri *url.URL) (b []byte, err error) {
+ req := httplib.NewRequest(iri.String(), http.MethodGet)
+ req.Header("Accept", activitypub.ActivityStreamsContentType)
+ req.Header("User-Agent", "Gitea/"+setting.AppVer)
+ resp, err := req.Response()
+ if err != nil {
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ err = fmt.Errorf("url IRI fetch [%s] failed with status (%d): %s", iri, resp.StatusCode, resp.Status)
+ return
+ }
+ b, err = io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize))
+ return b, err
+}
+
+func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) {
+ r := ctx.Req
+
+ // 1. Figure out what key we need to verify
+ v, err := httpsig.NewVerifier(r)
+ if err != nil {
+ return
+ }
+ ID := v.KeyId()
+ idIRI, err := url.Parse(ID)
+ if err != nil {
+ return
+ }
+ // 2. Fetch the public key of the other actor
+ b, err := fetch(idIRI)
+ if err != nil {
+ return
+ }
+ pubKey, err := getPublicKeyFromResponse(b, idIRI)
+ if err != nil {
+ return
+ }
+ // 3. Verify the other actor's key
+ algo := httpsig.Algorithm(setting.Federation.Algorithms[0])
+ authenticated = v.Verify(pubKey, algo) == nil
+ return authenticated, err
+}
+
+// ReqHTTPSignature function
+func ReqHTTPSignature() func(ctx *gitea_context.APIContext) {
+ return func(ctx *gitea_context.APIContext) {
+ if authenticated, err := verifyHTTPSignatures(ctx); err != nil {
+ ctx.ServerError("verifyHttpSignatures", err)
+ } else if !authenticated {
+ ctx.Error(http.StatusForbidden, "reqSignature", "request signature verification failed")
+ }
+ }
+}
diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go
index 3c39d7c2bc4e1..0e4e498e98243 100644
--- a/routers/api/v1/admin/adopt.go
+++ b/routers/api/v1/admin/adopt.go
@@ -1,16 +1,15 @@
// 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.
+// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
- "code.gitea.io/gitea/models"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
repo_service "code.gitea.io/gitea/services/repository"
@@ -85,7 +84,7 @@ func AdoptRepository(ctx *context.APIContext) {
ownerName := ctx.Params(":username")
repoName := ctx.Params(":reponame")
- ctxUser, err := user_model.GetUserByName(ownerName)
+ ctxUser, err := user_model.GetUserByName(ctx, ownerName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound()
@@ -96,7 +95,7 @@ func AdoptRepository(ctx *context.APIContext) {
}
// check not a repo
- has, err := repo_model.IsRepositoryExist(ctxUser, repoName)
+ has, err := repo_model.IsRepositoryExist(ctx, ctxUser, repoName)
if err != nil {
ctx.InternalServerError(err)
return
@@ -110,7 +109,7 @@ func AdoptRepository(ctx *context.APIContext) {
ctx.NotFound()
return
}
- if _, err := repo_service.AdoptRepository(ctx.Doer, ctxUser, models.CreateRepoOptions{
+ if _, err := repo_service.AdoptRepository(ctx.Doer, ctxUser, repo_module.CreateRepoOptions{
Name: repoName,
IsPrivate: true,
}); err != nil {
@@ -147,7 +146,7 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) {
ownerName := ctx.Params(":username")
repoName := ctx.Params(":reponame")
- ctxUser, err := user_model.GetUserByName(ownerName)
+ ctxUser, err := user_model.GetUserByName(ctx, ownerName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound()
@@ -158,7 +157,7 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) {
}
// check not a repo
- has, err := repo_model.IsRepositoryExist(ctxUser, repoName)
+ has, err := repo_model.IsRepositoryExist(ctx, ctxUser, repoName)
if err != nil {
ctx.InternalServerError(err)
return
diff --git a/routers/api/v1/admin/cron.go b/routers/api/v1/admin/cron.go
index 0c4333b892a5d..cc8c6c9e2391f 100644
--- a/routers/api/v1/admin/cron.go
+++ b/routers/api/v1/admin/cron.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package admin
diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go
index 727f3193cf39a..ff66244184421 100644
--- a/routers/api/v1/admin/org.go
+++ b/routers/api/v1/admin/org.go
@@ -1,7 +1,6 @@
// Copyright 2015 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.
+// SPDX-License-Identifier: MIT
package admin
@@ -12,10 +11,10 @@ import (
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// CreateOrg api for create organization
diff --git a/routers/api/v1/admin/repo.go b/routers/api/v1/admin/repo.go
index 712ced89c99e0..83ed06e49bce6 100644
--- a/routers/api/v1/admin/repo.go
+++ b/routers/api/v1/admin/repo.go
@@ -1,6 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package admin
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index bf176f95710be..6b48ce4a9d4d3 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -1,7 +1,6 @@
// Copyright 2015 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.
+// SPDX-License-Identifier: MIT
package admin
@@ -17,15 +16,16 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/password"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
)
@@ -82,7 +82,6 @@ func CreateUser(ctx *context.APIContext) {
Email: form.Email,
Passwd: form.Password,
MustChangePassword: true,
- IsActive: true,
LoginType: auth.Plain,
}
if form.MustChangePassword != nil {
@@ -108,11 +107,17 @@ func CreateUser(ctx *context.APIContext) {
return
}
- var overwriteDefault *user_model.CreateUserOverwriteOptions
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsActive: util.OptionalBoolTrue,
+ }
+
+ if form.Restricted != nil {
+ overwriteDefault.IsRestricted = util.OptionalBoolOf(*form.Restricted)
+ }
+
if form.Visibility != "" {
- overwriteDefault = &user_model.CreateUserOverwriteOptions{
- Visibility: api.VisibilityModes[form.Visibility],
- }
+ visibility := api.VisibilityModes[form.Visibility]
+ overwriteDefault.Visibility = &visibility
}
if err := user_model.CreateUser(u, overwriteDefault); err != nil {
@@ -263,7 +268,7 @@ func EditUser(ctx *context.APIContext) {
ctx.ContextUser.IsRestricted = *form.Restricted
}
- if err := user_model.UpdateUser(ctx.ContextUser, emailChanged); err != nil {
+ if err := user_model.UpdateUser(ctx, ctx.ContextUser, emailChanged); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) ||
user_model.IsErrEmailCharIsNotSupported(err) ||
user_model.IsErrEmailInvalid(err) {
@@ -304,7 +309,13 @@ func DeleteUser(ctx *context.APIContext) {
return
}
- if err := user_service.DeleteUser(ctx.ContextUser); err != nil {
+ // admin should not delete themself
+ if ctx.ContextUser.ID == ctx.Doer.ID {
+ ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("you cannot delete yourself"))
+ return
+ }
+
+ if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
if models.IsErrUserOwnRepos(err) ||
models.IsErrUserHasOrgs(err) ||
models.IsErrUserOwnPackages(err) {
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index a430eb453aa9b..cd08aae4145c4 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1,78 +1,78 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
// Package v1 Gitea API.
//
// This documentation describes the Gitea API.
//
-// Schemes: http, https
-// BasePath: /api/v1
-// Version: {{AppVer | JSEscape | Safe}}
-// License: MIT http://opensource.org/licenses/MIT
+// Schemes: http, https
+// BasePath: /api/v1
+// Version: {{AppVer | JSEscape | Safe}}
+// License: MIT http://opensource.org/licenses/MIT
//
-// Consumes:
-// - application/json
-// - text/plain
+// Consumes:
+// - application/json
+// - text/plain
//
-// Produces:
-// - application/json
-// - text/html
+// Produces:
+// - application/json
+// - text/html
//
-// Security:
-// - BasicAuth :
-// - Token :
-// - AccessToken :
-// - AuthorizationHeaderToken :
-// - SudoParam :
-// - SudoHeader :
-// - TOTPHeader :
+// Security:
+// - BasicAuth :
+// - Token :
+// - AccessToken :
+// - AuthorizationHeaderToken :
+// - SudoParam :
+// - SudoHeader :
+// - TOTPHeader :
//
-// SecurityDefinitions:
-// BasicAuth:
-// type: basic
-// Token:
-// type: apiKey
-// name: token
-// in: query
-// AccessToken:
-// type: apiKey
-// name: access_token
-// in: query
-// AuthorizationHeaderToken:
-// type: apiKey
-// name: Authorization
-// in: header
-// description: API tokens must be prepended with "token" followed by a space.
-// SudoParam:
-// type: apiKey
-// name: sudo
-// in: query
-// description: Sudo API request as the user provided as the key. Admin privileges are required.
-// SudoHeader:
-// type: apiKey
-// name: Sudo
-// in: header
-// description: Sudo API request as the user provided as the key. Admin privileges are required.
-// TOTPHeader:
-// type: apiKey
-// name: X-GITEA-OTP
-// in: header
-// description: Must be used in combination with BasicAuth if two-factor authentication is enabled.
+// SecurityDefinitions:
+// BasicAuth:
+// type: basic
+// Token:
+// type: apiKey
+// name: token
+// in: query
+// AccessToken:
+// type: apiKey
+// name: access_token
+// in: query
+// AuthorizationHeaderToken:
+// type: apiKey
+// name: Authorization
+// in: header
+// description: API tokens must be prepended with "token" followed by a space.
+// SudoParam:
+// type: apiKey
+// name: sudo
+// in: query
+// description: Sudo API request as the user provided as the key. Admin privileges are required.
+// SudoHeader:
+// type: apiKey
+// name: Sudo
+// in: header
+// description: Sudo API request as the user provided as the key. Admin privileges are required.
+// TOTPHeader:
+// type: apiKey
+// name: X-GITEA-OTP
+// in: header
+// description: Must be used in combination with BasicAuth if two-factor authentication is enabled.
//
// swagger:meta
package v1
import (
+ gocontext "context"
"fmt"
"net/http"
- "reflect"
"strings"
- "code.gitea.io/gitea/models"
+ auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -81,6 +81,7 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/activitypub"
"code.gitea.io/gitea/routers/api/v1/admin"
"code.gitea.io/gitea/routers/api/v1/misc"
"code.gitea.io/gitea/routers/api/v1/notify"
@@ -108,7 +109,7 @@ func sudo() func(ctx *context.APIContext) {
if len(sudo) > 0 {
if ctx.IsSigned && ctx.Doer.IsAdmin {
- user, err := user_model.GetUserByName(sudo)
+ user, err := user_model.GetUserByName(ctx, sudo)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound()
@@ -143,7 +144,7 @@ func repoAssignment() func(ctx *context.APIContext) {
if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) {
owner = ctx.Doer
} else {
- owner, err = user_model.GetUserByName(userName)
+ owner, err = user_model.GetUserByName(ctx, userName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err := user_model.LookupUserRedirect(userName); err == nil {
@@ -183,7 +184,7 @@ func repoAssignment() func(ctx *context.APIContext) {
repo.Owner = owner
ctx.Repo.Repository = repo
- ctx.Repo.Permission, err = models.GetUserRepoPermission(repo, ctx.Doer)
+ ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return
@@ -206,9 +207,36 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext)
}
// Contexter middleware already checks token for user sign in process.
-func reqToken() func(ctx *context.APIContext) {
+func reqToken(requiredScope auth_model.AccessTokenScope) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
- if true == ctx.Data["IsApiToken"] {
+ // If OAuth2 token is present
+ if _, ok := ctx.Data["ApiTokenScope"]; ctx.Data["IsApiToken"] == true && ok {
+ // no scope required
+ if requiredScope == "" {
+ return
+ }
+
+ // check scope
+ scope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
+ allow, err := scope.HasScope(requiredScope)
+ if err != nil {
+ ctx.Error(http.StatusForbidden, "reqToken", "parsing token failed: "+err.Error())
+ return
+ }
+ if allow {
+ return
+ }
+
+ // if requires 'repo' scope, but only has 'public_repo' scope, allow it only if the repo is public
+ if requiredScope == auth_model.AccessTokenScopeRepo {
+ if allowPublicRepo, err := scope.HasScope(auth_model.AccessTokenScopePublicRepo); err == nil && allowPublicRepo {
+ if ctx.Repo.Repository != nil && !ctx.Repo.Repository.IsPrivate {
+ return
+ }
+ }
+ }
+
+ ctx.Error(http.StatusForbidden, "reqToken", "token does not have required scope: "+requiredScope)
return
}
if ctx.Context.IsBasicAuth {
@@ -230,13 +258,10 @@ func reqExploreSignIn() func(ctx *context.APIContext) {
}
}
-func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) {
+func reqBasicAuth() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
- if ctx.IsSigned && setting.Service.EnableReverseProxyAuth && ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName {
- return
- }
if !ctx.Context.IsBasicAuth {
- ctx.Error(http.StatusUnauthorized, "reqBasicOrRevProxyAuth", "auth required")
+ ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
return
}
ctx.CheckForOTP()
@@ -283,6 +308,15 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) {
}
}
+// reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin
+func reqRepoBranchWriter(ctx *context.APIContext) {
+ options, ok := web.GetForm(ctx).(api.FileOptionInterface)
+ if !ok || (!ctx.Repo.CanWriteToBranch(ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) {
+ ctx.Error(http.StatusForbidden, "reqRepoBranchWriter", "user should have a permission to write to this branch")
+ return
+ }
+}
+
// reqRepoReader user should have specific read permission or be a repo admin or a site admin
func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
@@ -458,7 +492,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
}
if assignTeam {
- ctx.Org.Team, err = organization.GetTeamByID(ctx.ParamsInt64(":teamid"))
+ ctx.Org.Team, err = organization.GetTeamByID(ctx, ctx.ParamsInt64(":teamid"))
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.NotFound()
@@ -557,14 +591,17 @@ func mustNotBeArchived(ctx *context.APIContext) {
}
}
-// bind binding an obj to a func(ctx *context.APIContext)
-func bind(obj interface{}) http.HandlerFunc {
- tp := reflect.TypeOf(obj)
- for tp.Kind() == reflect.Ptr {
- tp = tp.Elem()
+func mustEnableAttachments(ctx *context.APIContext) {
+ if !setting.Attachment.Enabled {
+ ctx.NotFound()
+ return
}
+}
+
+// bind binding an obj to a func(ctx *context.APIContext)
+func bind[T any](obj T) http.HandlerFunc {
return web.Wrap(func(ctx *context.APIContext) {
- theObj := reflect.New(tp).Interface() // create a new form obj for every request but not use obj directly
+ theObj := new(T) // create a new form obj for every request but not use obj directly
errs := binding.Bind(ctx.Req, theObj)
if len(errs) > 0 {
ctx.Error(http.StatusUnprocessableEntity, "validationError", fmt.Sprintf("%s: %s", errs[0].FieldNames, errs[0].Error()))
@@ -583,18 +620,16 @@ func bind(obj interface{}) http.HandlerFunc {
func buildAuthGroup() *auth.Group {
group := auth.NewGroup(
&auth.OAuth2{},
+ &auth.HTTPSign{},
&auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API
)
- if setting.Service.EnableReverseProxyAuth {
- group.Add(&auth.ReverseProxy{})
- }
specialAdd(group)
return group
}
// Routes registers all v1 APIs routes to web application.
-func Routes() *web.Route {
+func Routes(ctx gocontext.Context) *web.Route {
m := web.NewRoute()
m.Use(securityHeaders())
@@ -605,14 +640,14 @@ func Routes() *web.Route {
// setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
AllowedMethods: setting.CORSConfig.Methods,
AllowCredentials: setting.CORSConfig.AllowCredentials,
- AllowedHeaders: []string{"Authorization", "X-Gitea-OTP"},
+ AllowedHeaders: append([]string{"Authorization", "X-Gitea-OTP"}, setting.CORSConfig.Headers...),
MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
}))
}
m.Use(context.APIContexter())
group := buildAuthGroup()
- if err := group.Init(); err != nil {
+ if err := group.Init(ctx); err != nil {
log.Error("Could not initialize '%s' auth method, error: %s", group.Name(), err)
}
@@ -624,7 +659,7 @@ func Routes() *web.Route {
}))
m.Group("", func() {
- // Miscellaneous
+ // Miscellaneous (no scope required)
if setting.API.EnableSwagger {
m.Get("/swagger", func(ctx *context.APIContext) {
ctx.Redirect(setting.AppSubURL + "/api/swagger")
@@ -633,6 +668,12 @@ func Routes() *web.Route {
m.Get("/version", misc.Version)
if setting.Federation.Enabled {
m.Get("/nodeinfo", misc.NodeInfo)
+ m.Group("/activitypub", func() {
+ m.Group("/user/{username}", func() {
+ m.Get("", activitypub.Person)
+ m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
+ }, context_service.UserAssignmentAPI())
+ })
}
m.Get("/signing-key.gpg", misc.SigningKey)
m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
@@ -644,7 +685,7 @@ func Routes() *web.Route {
m.Get("/repository", settings.GetGeneralRepoSettings)
})
- // Notifications
+ // Notifications (requires 'notification' scope)
m.Group("/notifications", func() {
m.Combo("").
Get(notify.ListNotifications).
@@ -653,9 +694,9 @@ func Routes() *web.Route {
m.Combo("/threads/{id}").
Get(notify.GetThread).
Patch(notify.ReadThread)
- }, reqToken())
+ }, reqToken(auth_model.AccessTokenScopeNotification))
- // Users
+ // Users (no scope required)
m.Group("/users", func() {
m.Get("/search", reqExploreSignIn(), user.Search)
@@ -671,10 +712,11 @@ func Routes() *web.Route {
m.Combo("").Get(user.ListAccessTokens).
Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken)
m.Combo("/{id}").Delete(user.DeleteAccessToken)
- }, reqBasicOrRevProxyAuth())
+ }, reqBasicAuth())
}, context_service.UserAssignmentAPI())
})
+ // (no scope required)
m.Group("/users", func() {
m.Group("/{username}", func() {
m.Get("/keys", user.ListPublicKeys)
@@ -690,57 +732,62 @@ func Routes() *web.Route {
m.Get("/subscriptions", user.GetWatchedRepos)
}, context_service.UserAssignmentAPI())
- }, reqToken())
+ }, reqToken(""))
m.Group("/user", func() {
m.Get("", user.GetAuthenticatedUser)
m.Group("/settings", func() {
- m.Get("", user.GetUserSettings)
- m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
- }, reqToken())
- m.Combo("/emails").Get(user.ListEmails).
- Post(bind(api.CreateEmailOption{}), user.AddEmail).
- Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
+ m.Get("", reqToken(auth_model.AccessTokenScopeReadUser), user.GetUserSettings)
+ m.Patch("", reqToken(auth_model.AccessTokenScopeUser), bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
+ })
+ m.Combo("/emails").Get(reqToken(auth_model.AccessTokenScopeReadUser), user.ListEmails).
+ Post(reqToken(auth_model.AccessTokenScopeUser), bind(api.CreateEmailOption{}), user.AddEmail).
+ Delete(reqToken(auth_model.AccessTokenScopeUser), bind(api.DeleteEmailOption{}), user.DeleteEmail)
m.Get("/followers", user.ListMyFollowers)
m.Group("/following", func() {
m.Get("", user.ListMyFollowing)
m.Group("/{username}", func() {
m.Get("", user.CheckMyFollowing)
- m.Put("", user.Follow)
- m.Delete("", user.Unfollow)
+ m.Put("", reqToken(auth_model.AccessTokenScopeUserFollow), user.Follow) // requires 'user:follow' scope
+ m.Delete("", reqToken(auth_model.AccessTokenScopeUserFollow), user.Unfollow) // requires 'user:follow' scope
}, context_service.UserAssignmentAPI())
})
+ // (admin:public_key scope)
m.Group("/keys", func() {
- m.Combo("").Get(user.ListMyPublicKeys).
- Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
- m.Combo("/{id}").Get(user.GetPublicKey).
- Delete(user.DeletePublicKey)
+ m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadPublicKey), user.ListMyPublicKeys).
+ Post(reqToken(auth_model.AccessTokenScopeWritePublicKey), bind(api.CreateKeyOption{}), user.CreatePublicKey)
+ m.Combo("/{id}").Get(reqToken(auth_model.AccessTokenScopeReadPublicKey), user.GetPublicKey).
+ Delete(reqToken(auth_model.AccessTokenScopeWritePublicKey), user.DeletePublicKey)
})
+
+ // (admin:application scope)
m.Group("/applications", func() {
m.Combo("/oauth2").
- Get(user.ListOauth2Applications).
- Post(bind(api.CreateOAuth2ApplicationOptions{}), user.CreateOauth2Application)
+ Get(reqToken(auth_model.AccessTokenScopeReadApplication), user.ListOauth2Applications).
+ Post(reqToken(auth_model.AccessTokenScopeWriteApplication), bind(api.CreateOAuth2ApplicationOptions{}), user.CreateOauth2Application)
m.Combo("/oauth2/{id}").
- Delete(user.DeleteOauth2Application).
- Patch(bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
- Get(user.GetOauth2Application)
- }, reqToken())
+ Delete(reqToken(auth_model.AccessTokenScopeWriteApplication), user.DeleteOauth2Application).
+ Patch(reqToken(auth_model.AccessTokenScopeWriteApplication), bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
+ Get(reqToken(auth_model.AccessTokenScopeReadApplication), user.GetOauth2Application)
+ })
+ // (admin:gpg_key scope)
m.Group("/gpg_keys", func() {
- m.Combo("").Get(user.ListMyGPGKeys).
- Post(bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
- m.Combo("/{id}").Get(user.GetGPGKey).
- Delete(user.DeleteGPGKey)
+ m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadGPGKey), user.ListMyGPGKeys).
+ Post(reqToken(auth_model.AccessTokenScopeWriteGPGKey), bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
+ m.Combo("/{id}").Get(reqToken(auth_model.AccessTokenScopeReadGPGKey), user.GetGPGKey).
+ Delete(reqToken(auth_model.AccessTokenScopeWriteGPGKey), user.DeleteGPGKey)
})
+ m.Get("/gpg_key_token", reqToken(auth_model.AccessTokenScopeReadGPGKey), user.GetVerificationToken)
+ m.Post("/gpg_key_verify", reqToken(auth_model.AccessTokenScopeReadGPGKey), bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
- m.Get("/gpg_key_token", user.GetVerificationToken)
- m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
-
- m.Combo("/repos").Get(user.ListMyRepos).
+ // (repo scope)
+ m.Combo("/repos", reqToken(auth_model.AccessTokenScopeRepo)).Get(user.ListMyRepos).
Post(bind(api.CreateRepoOption{}), repo.Create)
+ // (repo scope)
m.Group("/starred", func() {
m.Get("", user.GetMyStarredRepos)
m.Group("/{username}/{reponame}", func() {
@@ -748,81 +795,85 @@ func Routes() *web.Route {
m.Put("", user.Star)
m.Delete("", user.Unstar)
}, repoAssignment())
- })
- m.Get("/times", repo.ListMyTrackedTimes)
-
- m.Get("/stopwatches", repo.GetStopwatches)
-
- m.Get("/subscriptions", user.GetMyWatchedRepos)
-
- m.Get("/teams", org.ListUserTeams)
- }, reqToken())
+ }, reqToken(auth_model.AccessTokenScopeRepo))
+ m.Get("/times", reqToken(auth_model.AccessTokenScopeRepo), repo.ListMyTrackedTimes)
+ m.Get("/stopwatches", reqToken(auth_model.AccessTokenScopeRepo), repo.GetStopwatches)
+ m.Get("/subscriptions", reqToken(auth_model.AccessTokenScopeRepo), user.GetMyWatchedRepos)
+ m.Get("/teams", reqToken(auth_model.AccessTokenScopeRepo), org.ListUserTeams)
+ }, reqToken(""))
// Repositories
- m.Post("/org/{org}/repos", reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepoDeprecated)
+ m.Post("/org/{org}/repos", reqToken(auth_model.AccessTokenScopeAdminOrg), bind(api.CreateRepoOption{}), repo.CreateOrgRepoDeprecated)
- m.Combo("/repositories/{id}", reqToken()).Get(repo.GetByID)
+ m.Combo("/repositories/{id}", reqToken(auth_model.AccessTokenScopeRepo)).Get(repo.GetByID)
m.Group("/repos", func() {
m.Get("/search", repo.Search)
m.Get("/issues/search", repo.SearchIssues)
- m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate)
+ // (repo scope)
+ m.Post("/migrate", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MigrateRepoOptions{}), repo.Migrate)
m.Group("/{username}/{reponame}", func() {
m.Combo("").Get(reqAnyRepoReader(), repo.Get).
- Delete(reqToken(), reqOwner(), repo.Delete).
- Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
- m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
- m.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
- m.Post("/transfer/accept", reqToken(), repo.AcceptTransfer)
- m.Post("/transfer/reject", reqToken(), repo.RejectTransfer)
- m.Combo("/notifications").
- Get(reqToken(), notify.ListRepoNotifications).
- Put(reqToken(), notify.ReadRepoNotifications)
+ Delete(reqToken(auth_model.AccessTokenScopeDeleteRepo), reqOwner(), repo.Delete).
+ Patch(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
+ m.Post("/generate", reqToken(auth_model.AccessTokenScopeRepo), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
+ m.Group("/transfer", func() {
+ m.Post("", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
+ m.Post("/accept", repo.AcceptTransfer)
+ m.Post("/reject", repo.RejectTransfer)
+ }, reqToken(auth_model.AccessTokenScopeRepo))
+ m.Combo("/notifications", reqToken(auth_model.AccessTokenScopeNotification)).
+ Get(notify.ListRepoNotifications).
+ Put(notify.ReadRepoNotifications)
m.Group("/hooks/git", func() {
- m.Combo("").Get(repo.ListGitHooks)
+ m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.ListGitHooks)
m.Group("/{id}", func() {
- m.Combo("").Get(repo.GetGitHook).
- Patch(bind(api.EditGitHookOption{}), repo.EditGitHook).
- Delete(repo.DeleteGitHook)
+ m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.GetGitHook).
+ Patch(reqToken(auth_model.AccessTokenScopeWriteRepoHook), bind(api.EditGitHookOption{}), repo.EditGitHook).
+ Delete(reqToken(auth_model.AccessTokenScopeWriteRepoHook), repo.DeleteGitHook)
})
- }, reqToken(), reqAdmin(), reqGitHook(), context.ReferencesGitRepo(true))
+ }, reqAdmin(), reqGitHook(), context.ReferencesGitRepo(true))
m.Group("/hooks", func() {
- m.Combo("").Get(repo.ListHooks).
- Post(bind(api.CreateHookOption{}), repo.CreateHook)
+ m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.ListHooks).
+ Post(reqToken(auth_model.AccessTokenScopeWriteRepoHook), bind(api.CreateHookOption{}), repo.CreateHook)
m.Group("/{id}", func() {
- m.Combo("").Get(repo.GetHook).
- Patch(bind(api.EditHookOption{}), repo.EditHook).
- Delete(repo.DeleteHook)
- m.Post("/tests", context.RepoRefForAPI, repo.TestHook)
+ m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.GetHook).
+ Patch(reqToken(auth_model.AccessTokenScopeWriteRepoHook), bind(api.EditHookOption{}), repo.EditHook).
+ Delete(reqToken(auth_model.AccessTokenScopeWriteRepoHook), repo.DeleteHook)
+ m.Post("/tests", reqToken(auth_model.AccessTokenScopeReadRepoHook), context.ReferencesGitRepo(), context.RepoRefForAPI, repo.TestHook)
})
- }, reqToken(), reqAdmin(), reqWebhooksEnabled())
+ }, reqAdmin(), reqWebhooksEnabled())
m.Group("/collaborators", func() {
m.Get("", reqAnyRepoReader(), repo.ListCollaborators)
- m.Combo("/{collaborator}").Get(reqAnyRepoReader(), repo.IsCollaborator).
- Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator).
- Delete(reqAdmin(), repo.DeleteCollaborator)
- }, reqToken())
- m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
- m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
+ m.Group("/{collaborator}", func() {
+ m.Combo("").Get(reqAnyRepoReader(), repo.IsCollaborator).
+ Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator).
+ Delete(reqAdmin(), repo.DeleteCollaborator)
+ m.Get("/permission", repo.GetRepoPermissions)
+ })
+ }, reqToken(auth_model.AccessTokenScopeRepo))
+ m.Get("/assignees", reqToken(auth_model.AccessTokenScopeRepo), reqAnyRepoReader(), repo.GetAssignees)
+ m.Get("/reviewers", reqToken(auth_model.AccessTokenScopeRepo), reqAnyRepoReader(), repo.GetReviewers)
m.Group("/teams", func() {
m.Get("", reqAnyRepoReader(), repo.ListTeams)
m.Combo("/{team}").Get(reqAnyRepoReader(), repo.IsTeam).
Put(reqAdmin(), repo.AddTeam).
Delete(reqAdmin(), repo.DeleteTeam)
- }, reqToken())
- m.Get("/raw/*", context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
+ }, reqToken(auth_model.AccessTokenScopeRepo))
+ m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
+ m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS)
m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
m.Combo("/forks").Get(repo.ListForks).
- Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
m.Group("/branches", func() {
- m.Get("", context.ReferencesGitRepo(false), repo.ListBranches)
- m.Get("/*", context.ReferencesGitRepo(false), repo.GetBranch)
- m.Delete("/*", reqRepoWriter(unit.TypeCode), context.ReferencesGitRepo(false), repo.DeleteBranch)
- m.Post("", reqRepoWriter(unit.TypeCode), context.ReferencesGitRepo(false), bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
- }, reqRepoReader(unit.TypeCode))
+ m.Get("", repo.ListBranches)
+ m.Get("/*", repo.GetBranch)
+ m.Delete("/*", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), repo.DeleteBranch)
+ m.Post("", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
+ }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Group("/branch_protections", func() {
m.Get("", repo.ListBranchProtections)
m.Post("", bind(api.CreateBranchProtectionOption{}), repo.CreateBranchProtection)
@@ -831,65 +882,74 @@ func Routes() *web.Route {
m.Patch("", bind(api.EditBranchProtectionOption{}), repo.EditBranchProtection)
m.Delete("", repo.DeleteBranchProtection)
})
- }, reqToken(), reqAdmin())
+ }, reqToken(auth_model.AccessTokenScopeRepo), reqAdmin())
m.Group("/tags", func() {
m.Get("", repo.ListTags)
m.Get("/*", repo.GetTag)
- m.Post("", reqRepoWriter(unit.TypeCode), bind(api.CreateTagOption{}), repo.CreateTag)
- m.Delete("/*", repo.DeleteTag)
+ m.Post("", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), bind(api.CreateTagOption{}), repo.CreateTag)
+ m.Delete("/*", reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteTag)
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
m.Group("/keys", func() {
m.Combo("").Get(repo.ListDeployKeys).
Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)
m.Combo("/{id}").Get(repo.GetDeployKey).
Delete(repo.DeleteDeploykey)
- }, reqToken(), reqAdmin())
+ }, reqToken(auth_model.AccessTokenScopeRepo), reqAdmin())
m.Group("/times", func() {
m.Combo("").Get(repo.ListTrackedTimesByRepository)
m.Combo("/{timetrackingusername}").Get(repo.ListTrackedTimesByUser)
- }, mustEnableIssues, reqToken())
+ }, mustEnableIssues, reqToken(auth_model.AccessTokenScopeRepo))
m.Group("/wiki", func() {
m.Combo("/page/{pageName}").
Get(repo.GetWikiPage).
- Patch(mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage).
- Delete(mustNotBeArchived, reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
+ Patch(mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage).
+ Delete(mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
m.Get("/revisions/{pageName}", repo.ListPageRevisions)
- m.Post("/new", mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
+ m.Post("/new", mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
m.Get("/pages", repo.ListWikiPages)
}, mustEnableWiki)
m.Group("/issues", func() {
m.Combo("").Get(repo.ListIssues).
- Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
m.Group("/comments", func() {
m.Get("", repo.ListRepoIssueComments)
m.Group("/{id}", func() {
m.Combo("").
Get(repo.GetIssueComment).
- Patch(mustNotBeArchived, reqToken(), bind(api.EditIssueCommentOption{}), repo.EditIssueComment).
- Delete(reqToken(), repo.DeleteIssueComment)
+ Patch(mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditIssueCommentOption{}), repo.EditIssueComment).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteIssueComment)
m.Combo("/reactions").
Get(repo.GetIssueCommentReactions).
- Post(reqToken(), bind(api.EditReactionOption{}), repo.PostIssueCommentReaction).
- Delete(reqToken(), bind(api.EditReactionOption{}), repo.DeleteIssueCommentReaction)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.PostIssueCommentReaction).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.DeleteIssueCommentReaction)
+ m.Group("/assets", func() {
+ m.Combo("").
+ Get(repo.ListIssueCommentAttachments).
+ Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.CreateIssueCommentAttachment)
+ m.Combo("/{asset}").
+ Get(repo.GetIssueCommentAttachment).
+ Patch(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueCommentAttachment)
+ }, mustEnableAttachments)
})
})
m.Group("/{index}", func() {
m.Combo("").Get(repo.GetIssue).
- Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
- Delete(reqToken(), reqAdmin(), repo.DeleteIssue)
+ Patch(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditIssueOption{}), repo.EditIssue).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
m.Group("/comments", func() {
m.Combo("").Get(repo.ListIssueComments).
- Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
- m.Combo("/{id}", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated).
+ Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
+ m.Combo("/{id}", reqToken(auth_model.AccessTokenScopeRepo)).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated).
Delete(repo.DeleteIssueCommentDeprecated)
})
m.Get("/timeline", repo.ListIssueCommentsAndTimeline)
m.Group("/labels", func() {
m.Combo("").Get(repo.ListIssueLabels).
- Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
- Put(reqToken(), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
- Delete(reqToken(), repo.ClearIssueLabels)
- m.Delete("/{id}", reqToken(), repo.DeleteIssueLabel)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
+ Put(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.ClearIssueLabels)
+ m.Delete("/{id}", reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteIssueLabel)
})
m.Group("/times", func() {
m.Combo("").
@@ -897,194 +957,215 @@ func Routes() *web.Route {
Post(bind(api.AddTimeOption{}), repo.AddTime).
Delete(repo.ResetIssueTime)
m.Delete("/{id}", repo.DeleteTime)
- }, reqToken())
- m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline)
+ }, reqToken(auth_model.AccessTokenScopeRepo))
+ m.Combo("/deadline").Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline)
m.Group("/stopwatch", func() {
- m.Post("/start", reqToken(), repo.StartIssueStopwatch)
- m.Post("/stop", reqToken(), repo.StopIssueStopwatch)
- m.Delete("/delete", reqToken(), repo.DeleteIssueStopwatch)
+ m.Post("/start", reqToken(auth_model.AccessTokenScopeRepo), repo.StartIssueStopwatch)
+ m.Post("/stop", reqToken(auth_model.AccessTokenScopeRepo), repo.StopIssueStopwatch)
+ m.Delete("/delete", reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteIssueStopwatch)
})
m.Group("/subscriptions", func() {
m.Get("", repo.GetIssueSubscribers)
- m.Get("/check", reqToken(), repo.CheckIssueSubscription)
- m.Put("/{user}", reqToken(), repo.AddIssueSubscription)
- m.Delete("/{user}", reqToken(), repo.DelIssueSubscription)
+ m.Get("/check", reqToken(auth_model.AccessTokenScopeRepo), repo.CheckIssueSubscription)
+ m.Put("/{user}", reqToken(auth_model.AccessTokenScopeRepo), repo.AddIssueSubscription)
+ m.Delete("/{user}", reqToken(auth_model.AccessTokenScopeRepo), repo.DelIssueSubscription)
})
m.Combo("/reactions").
Get(repo.GetIssueReactions).
- Post(reqToken(), bind(api.EditReactionOption{}), repo.PostIssueReaction).
- Delete(reqToken(), bind(api.EditReactionOption{}), repo.DeleteIssueReaction)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.PostIssueReaction).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.DeleteIssueReaction)
+ m.Group("/assets", func() {
+ m.Combo("").
+ Get(repo.ListIssueAttachments).
+ Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.CreateIssueAttachment)
+ m.Combo("/{asset}").
+ Get(repo.GetIssueAttachment).
+ Patch(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueAttachment)
+ }, mustEnableAttachments)
})
}, mustEnableIssuesOrPulls)
m.Group("/labels", func() {
m.Combo("").Get(repo.ListLabels).
- Post(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateLabelOption{}), repo.CreateLabel)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateLabelOption{}), repo.CreateLabel)
m.Combo("/{id}").Get(repo.GetLabel).
- Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditLabelOption{}), repo.EditLabel).
- Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteLabel)
+ Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditLabelOption{}), repo.EditLabel).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteLabel)
})
- m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
- m.Post("/markdown/raw", misc.MarkdownRaw)
+ m.Post("/markdown", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkdownOption{}), misc.Markdown)
+ m.Post("/markdown/raw", reqToken(auth_model.AccessTokenScopeRepo), misc.MarkdownRaw)
m.Group("/milestones", func() {
m.Combo("").Get(repo.ListMilestones).
- Post(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateMilestoneOption{}), repo.CreateMilestone)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateMilestoneOption{}), repo.CreateMilestone)
m.Combo("/{id}").Get(repo.GetMilestone).
- Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
- Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
+ Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
})
m.Get("/stargazers", repo.ListStargazers)
m.Get("/subscribers", repo.ListSubscribers)
m.Group("/subscription", func() {
m.Get("", user.IsWatching)
- m.Put("", reqToken(), user.Watch)
- m.Delete("", reqToken(), user.Unwatch)
+ m.Put("", reqToken(auth_model.AccessTokenScopeRepo), user.Watch)
+ m.Delete("", reqToken(auth_model.AccessTokenScopeRepo), user.Unwatch)
})
m.Group("/releases", func() {
m.Combo("").Get(repo.ListReleases).
- Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(false), bind(api.CreateReleaseOption{}), repo.CreateRelease)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), repo.CreateRelease)
m.Group("/{id}", func() {
m.Combo("").Get(repo.GetRelease).
- Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(false), bind(api.EditReleaseOption{}), repo.EditRelease).
- Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
+ Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), repo.EditRelease).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
m.Group("/assets", func() {
m.Combo("").Get(repo.ListReleaseAttachments).
- Post(reqToken(), reqRepoWriter(unit.TypeReleases), repo.CreateReleaseAttachment)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.CreateReleaseAttachment)
m.Combo("/{asset}").Get(repo.GetReleaseAttachment).
- Patch(reqToken(), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment).
- Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment)
+ Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment)
})
})
m.Group("/tags", func() {
m.Combo("/{tag}").
Get(repo.GetReleaseByTag).
- Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
})
}, reqRepoReader(unit.TypeReleases))
- m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), repo.MirrorSync)
- m.Get("/editorconfig/{filename}", context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetEditorconfig)
+ m.Post("/mirror-sync", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), repo.MirrorSync)
+ m.Post("/push_mirrors-sync", reqAdmin(), reqToken(auth_model.AccessTokenScopeRepo), repo.PushMirrorSync)
+ m.Group("/push_mirrors", func() {
+ m.Combo("").Get(repo.ListPushMirrors).
+ Post(bind(api.CreatePushMirrorOption{}), repo.AddPushMirror)
+ m.Combo("/{name}").
+ Delete(repo.DeletePushMirrorByRemoteName).
+ Get(repo.GetPushMirrorByName)
+ }, reqAdmin(), reqToken(auth_model.AccessTokenScopeRepo))
+
+ m.Get("/editorconfig/{filename}", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetEditorconfig)
m.Group("/pulls", func() {
m.Combo("").Get(repo.ListPullRequests).
- Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
m.Group("/{index}", func() {
m.Combo("").Get(repo.GetPullRequest).
- Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
+ Patch(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
m.Get(".{diffType:diff|patch}", repo.DownloadPullDiffOrPatch)
- m.Post("/update", reqToken(), repo.UpdatePullRequest)
+ m.Post("/update", reqToken(auth_model.AccessTokenScopeRepo), repo.UpdatePullRequest)
m.Get("/commits", repo.GetPullRequestCommits)
+ m.Get("/files", repo.GetPullRequestFiles)
m.Combo("/merge").Get(repo.IsPullRequestMerged).
- Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.CancelScheduledAutoMerge)
m.Group("/reviews", func() {
m.Combo("").
Get(repo.ListPullReviews).
- Post(reqToken(), bind(api.CreatePullReviewOptions{}), repo.CreatePullReview)
+ Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.CreatePullReviewOptions{}), repo.CreatePullReview)
m.Group("/{id}", func() {
m.Combo("").
Get(repo.GetPullReview).
- Delete(reqToken(), repo.DeletePullReview).
- Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.DeletePullReview).
+ Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
m.Combo("/comments").
Get(repo.GetPullReviewComments)
- m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
- m.Post("/undismissals", reqToken(), repo.UnDismissPullReview)
+ m.Post("/dismissals", reqToken(auth_model.AccessTokenScopeRepo), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
+ m.Post("/undismissals", reqToken(auth_model.AccessTokenScopeRepo), repo.UnDismissPullReview)
})
})
- m.Combo("/requested_reviewers").
- Delete(reqToken(), bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
- Post(reqToken(), bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
+ m.Combo("/requested_reviewers", reqToken(auth_model.AccessTokenScopeRepo)).
+ Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
+ Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
})
- }, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(false))
+ }, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
m.Group("/statuses", func() {
m.Combo("/{sha}").Get(repo.GetCommitStatuses).
- Post(reqToken(), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
+ Post(reqToken(auth_model.AccessTokenScopeRepoStatus), reqRepoWriter(unit.TypeCode), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
}, reqRepoReader(unit.TypeCode))
m.Group("/commits", func() {
- m.Get("", context.ReferencesGitRepo(false), repo.GetAllCommits)
+ m.Get("", context.ReferencesGitRepo(), repo.GetAllCommits)
m.Group("/{ref}", func() {
m.Get("/status", repo.GetCombinedCommitStatusByRef)
m.Get("/statuses", repo.GetCommitStatusesByRef)
- })
+ }, context.ReferencesGitRepo())
}, reqRepoReader(unit.TypeCode))
m.Group("/git", func() {
m.Group("/commits", func() {
- m.Get("/{sha}", context.ReferencesGitRepo(false), repo.GetSingleCommit)
+ m.Get("/{sha}", repo.GetSingleCommit)
m.Get("/{sha}.{diffType:diff|patch}", repo.DownloadCommitDiffOrPatch)
})
m.Get("/refs", repo.GetGitAllRefs)
m.Get("/refs/*", repo.GetGitRefs)
- m.Get("/trees/{sha}", context.RepoRefForAPI, repo.GetTree)
- m.Get("/blobs/{sha}", context.RepoRefForAPI, repo.GetBlob)
- m.Get("/tags/{sha}", context.RepoRefForAPI, repo.GetAnnotatedTag)
+ m.Get("/trees/{sha}", repo.GetTree)
+ m.Get("/blobs/{sha}", repo.GetBlob)
+ m.Get("/tags/{sha}", repo.GetAnnotatedTag)
m.Get("/notes/{sha}", repo.GetNote)
- }, reqRepoReader(unit.TypeCode))
- m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
+ }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode))
+ m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(auth_model.AccessTokenScopeRepo), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
m.Group("/contents", func() {
m.Get("", repo.GetContentsList)
m.Get("/*", repo.GetContents)
m.Group("/*", func() {
- m.Post("", bind(api.CreateFileOptions{}), repo.CreateFile)
- m.Put("", bind(api.UpdateFileOptions{}), repo.UpdateFile)
- m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile)
- }, reqRepoWriter(unit.TypeCode), reqToken())
+ m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)
+ m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, repo.UpdateFile)
+ m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, repo.DeleteFile)
+ }, reqToken(auth_model.AccessTokenScopeRepo))
}, reqRepoReader(unit.TypeCode))
m.Get("/signing-key.gpg", misc.SigningKey)
m.Group("/topics", func() {
m.Combo("").Get(repo.ListTopics).
- Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
+ Put(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
m.Group("/{topic}", func() {
- m.Combo("").Put(reqToken(), repo.AddTopic).
- Delete(reqToken(), repo.DeleteTopic)
+ m.Combo("").Put(reqToken(auth_model.AccessTokenScopeRepo), repo.AddTopic).
+ Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteTopic)
}, reqAdmin())
}, reqAnyRepoReader())
- m.Get("/issue_templates", context.ReferencesGitRepo(false), repo.GetIssueTemplates)
+ m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
}, repoAssignment())
})
+ // NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
m.Group("/packages/{username}", func() {
m.Group("/{type}/{name}/{version}", func() {
- m.Get("", packages.GetPackage)
- m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
- m.Get("/files", packages.ListPackageFiles)
+ m.Get("", reqToken(auth_model.AccessTokenScopeReadPackage), packages.GetPackage)
+ m.Delete("", reqToken(auth_model.AccessTokenScopeDeletePackage), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
+ m.Get("/files", reqToken(auth_model.AccessTokenScopeReadPackage), packages.ListPackageFiles)
})
- m.Get("/", packages.ListPackages)
+ m.Get("/", reqToken(auth_model.AccessTokenScopeReadPackage), packages.ListPackages)
}, context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
// Organizations
- m.Get("/user/orgs", reqToken(), org.ListMyOrgs)
+ m.Get("/user/orgs", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListMyOrgs)
m.Group("/users/{username}/orgs", func() {
- m.Get("", org.ListUserOrgs)
- m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
+ m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListUserOrgs)
+ m.Get("/{org}/permissions", reqToken(auth_model.AccessTokenScopeReadOrg), org.GetUserOrgsPermissions)
}, context_service.UserAssignmentAPI())
- m.Post("/orgs", reqToken(), bind(api.CreateOrgOption{}), org.Create)
- m.Get("/orgs", org.GetAll)
+ m.Post("/orgs", reqToken(auth_model.AccessTokenScopeWriteOrg), bind(api.CreateOrgOption{}), org.Create)
+ m.Get("/orgs", reqToken(auth_model.AccessTokenScopeReadOrg), org.GetAll)
m.Group("/orgs/{org}", func() {
- m.Combo("").Get(org.Get).
- Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
- Delete(reqToken(), reqOrgOwnership(), org.Delete)
- m.Combo("/repos").Get(user.ListOrgRepos).
- Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
+ m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.Get).
+ Patch(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
+ Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.Delete)
+ m.Combo("/repos").Get(reqToken(auth_model.AccessTokenScopeReadOrg), user.ListOrgRepos).
+ Post(reqToken(auth_model.AccessTokenScopeWriteOrg), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
m.Group("/members", func() {
- m.Get("", org.ListMembers)
- m.Combo("/{username}").Get(org.IsMember).
- Delete(reqToken(), reqOrgOwnership(), org.DeleteMember)
+ m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListMembers)
+ m.Combo("/{username}").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.IsMember).
+ Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.DeleteMember)
})
m.Group("/public_members", func() {
- m.Get("", org.ListPublicMembers)
- m.Combo("/{username}").Get(org.IsPublicMember).
- Put(reqToken(), reqOrgMembership(), org.PublicizeMember).
- Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
+ m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListPublicMembers)
+ m.Combo("/{username}").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.IsPublicMember).
+ Put(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgMembership(), org.PublicizeMember).
+ Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgMembership(), org.ConcealMember)
})
m.Group("/teams", func() {
- m.Get("", org.ListTeams)
- m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
- m.Get("/search", org.SearchTeam)
- }, reqToken(), reqOrgMembership())
+ m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListTeams)
+ m.Post("", reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
+ m.Get("/search", reqToken(auth_model.AccessTokenScopeReadOrg), org.SearchTeam)
+ }, reqOrgMembership())
m.Group("/labels", func() {
- m.Get("", org.ListLabels)
- m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
- m.Combo("/{id}").Get(org.GetLabel).
- Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel).
- Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel)
+ m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListLabels)
+ m.Post("", reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
+ m.Combo("/{id}").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetLabel).
+ Patch(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel).
+ Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.DeleteLabel)
})
m.Group("/hooks", func() {
m.Combo("").Get(org.ListHooks).
@@ -1092,26 +1173,27 @@ func Routes() *web.Route {
m.Combo("/{id}").Get(org.GetHook).
Patch(bind(api.EditHookOption{}), org.EditHook).
Delete(org.DeleteHook)
- }, reqToken(), reqOrgOwnership(), reqWebhooksEnabled())
+ }, reqToken(auth_model.AccessTokenScopeAdminOrgHook), reqOrgOwnership(), reqWebhooksEnabled())
}, orgAssignment(true))
m.Group("/teams/{teamid}", func() {
- m.Combo("").Get(org.GetTeam).
- Patch(reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
- Delete(reqOrgOwnership(), org.DeleteTeam)
+ m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeam).
+ Patch(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
+ Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.DeleteTeam)
m.Group("/members", func() {
- m.Get("", org.GetTeamMembers)
+ m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamMembers)
m.Combo("/{username}").
- Get(org.GetTeamMember).
- Put(reqOrgOwnership(), org.AddTeamMember).
- Delete(reqOrgOwnership(), org.RemoveTeamMember)
+ Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamMember).
+ Put(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.AddTeamMember).
+ Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.RemoveTeamMember)
})
m.Group("/repos", func() {
- m.Get("", org.GetTeamRepos)
+ m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamRepos)
m.Combo("/{org}/{reponame}").
- Put(org.AddTeamRepository).
- Delete(org.RemoveTeamRepository)
+ Put(reqToken(auth_model.AccessTokenScopeWriteOrg), org.AddTeamRepository).
+ Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), org.RemoveTeamRepository).
+ Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamRepo)
})
- }, orgAssignment(false, true), reqToken(), reqTeamMembership())
+ }, orgAssignment(false, true), reqToken(""), reqTeamMembership())
m.Group("/admin", func() {
m.Group("/cron", func() {
@@ -1139,7 +1221,7 @@ func Routes() *web.Route {
m.Post("/{username}/{reponame}", admin.AdoptRepository)
m.Delete("/{username}/{reponame}", admin.DeleteUnadoptedRepository)
})
- }, reqToken(), reqSiteAdmin())
+ }, reqToken(auth_model.AccessTokenScopeSudo), reqSiteAdmin())
m.Group("/topics", func() {
m.Get("/search", repo.TopicSearch)
diff --git a/routers/api/v1/auth.go b/routers/api/v1/auth.go
index 359c9ec56bcc7..e44271ba148aa 100644
--- a/routers/api/v1/auth.go
+++ b/routers/api/v1/auth.go
@@ -1,9 +1,7 @@
// 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.
+// SPDX-License-Identifier: MIT
//go:build !windows
-// +build !windows
package v1
diff --git a/routers/api/v1/auth_windows.go b/routers/api/v1/auth_windows.go
index d41c4bb223ff6..3514e21baac49 100644
--- a/routers/api/v1/auth_windows.go
+++ b/routers/api/v1/auth_windows.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package v1
diff --git a/routers/api/v1/misc/markdown.go b/routers/api/v1/misc/markdown.go
index eb0b78a5bab0e..3ff42f08d6828 100644
--- a/routers/api/v1/misc/markdown.go
+++ b/routers/api/v1/misc/markdown.go
@@ -1,6 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package misc
diff --git a/routers/api/v1/misc/markdown_test.go b/routers/api/v1/misc/markdown_test.go
index 349498e2abfdc..025f2f44b0118 100644
--- a/routers/api/v1/misc/markdown_test.go
+++ b/routers/api/v1/misc/markdown_test.go
@@ -1,10 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package misc
import (
+ go_context "context"
"io"
"net/http"
"net/http/httptest"
@@ -13,6 +13,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"
@@ -29,7 +30,7 @@ const (
)
func createContext(req *http.Request) (*context.Context, *httptest.ResponseRecorder) {
- rnd := templates.HTMLRenderer()
+ _, rnd := templates.HTMLRenderer(req.Context())
resp := httptest.NewRecorder()
c := &context.Context{
Req: req,
@@ -37,6 +38,8 @@ func createContext(req *http.Request) (*context.Context, *httptest.ResponseRecor
Render: rnd,
Data: make(map[string]interface{}),
}
+ defer c.Close()
+
return c, resp
}
@@ -48,6 +51,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/misc/nodeinfo.go b/routers/api/v1/misc/nodeinfo.go
index bc36fa1be1284..319c3483e10c9 100644
--- a/routers/api/v1/misc/nodeinfo.go
+++ b/routers/api/v1/misc/nodeinfo.go
@@ -1,17 +1,21 @@
// 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.
+// SPDX-License-Identifier: MIT
package misc
import (
"net/http"
+ "time"
+ issues_model "code.gitea.io/gitea/models/issues"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
)
+const cacheKeyNodeInfoUsage = "API_NodeInfoUsage"
+
// NodeInfo returns the NodeInfo for the Gitea instance to allow for federation
func NodeInfo(ctx *context.APIContext) {
// swagger:operation GET /nodeinfo miscellaneous getNodeInfo
@@ -23,6 +27,41 @@ func NodeInfo(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/NodeInfo"
+ nodeInfoUsage := structs.NodeInfoUsage{}
+ if setting.Federation.ShareUserStatistics {
+ cached := false
+ if setting.CacheService.Enabled {
+ nodeInfoUsage, cached = ctx.Cache.Get(cacheKeyNodeInfoUsage).(structs.NodeInfoUsage)
+ }
+ if !cached {
+ usersTotal := int(user_model.CountUsers(nil))
+ now := time.Now()
+ timeOneMonthAgo := now.AddDate(0, -1, 0).Unix()
+ timeHaveYearAgo := now.AddDate(0, -6, 0).Unix()
+ usersActiveMonth := int(user_model.CountUsers(&user_model.CountUserFilter{LastLoginSince: &timeOneMonthAgo}))
+ usersActiveHalfyear := int(user_model.CountUsers(&user_model.CountUserFilter{LastLoginSince: &timeHaveYearAgo}))
+
+ allIssues, _ := issues_model.CountIssues(ctx, &issues_model.IssuesOptions{})
+ allComments, _ := issues_model.CountComments(&issues_model.FindCommentsOptions{})
+
+ nodeInfoUsage = structs.NodeInfoUsage{
+ Users: structs.NodeInfoUsageUsers{
+ Total: usersTotal,
+ ActiveMonth: usersActiveMonth,
+ ActiveHalfyear: usersActiveHalfyear,
+ },
+ LocalPosts: int(allIssues),
+ LocalComments: int(allComments),
+ }
+ if setting.CacheService.Enabled {
+ if err := ctx.Cache.Put(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+ }
+ }
+ }
+
nodeInfo := &structs.NodeInfo{
Version: "2.1",
Software: structs.NodeInfoSoftware{
@@ -34,12 +73,10 @@ func NodeInfo(ctx *context.APIContext) {
Protocols: []string{"activitypub"},
Services: structs.NodeInfoServices{
Inbound: []string{},
- Outbound: []string{},
+ Outbound: []string{"rss2.0"},
},
OpenRegistrations: setting.Service.ShowRegistrationButton,
- Usage: structs.NodeInfoUsage{
- Users: structs.NodeInfoUsageUsers{},
- },
+ Usage: nodeInfoUsage,
}
ctx.JSON(http.StatusOK, nodeInfo)
}
diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go
index b99e560ccfd21..2ca9813e15ca0 100644
--- a/routers/api/v1/misc/signing.go
+++ b/routers/api/v1/misc/signing.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package misc
@@ -59,6 +58,6 @@ func SigningKey(ctx *context.APIContext) {
}
_, err = ctx.Write([]byte(content))
if err != nil {
- ctx.Error(http.StatusInternalServerError, "gpg export", fmt.Errorf("Error writing key content %v", err))
+ ctx.Error(http.StatusInternalServerError, "gpg export", fmt.Errorf("Error writing key content %w", err))
}
}
diff --git a/routers/api/v1/misc/version.go b/routers/api/v1/misc/version.go
index b8c7d4f9ada26..83fa35219abe9 100644
--- a/routers/api/v1/misc/version.go
+++ b/routers/api/v1/misc/version.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package misc
diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go
index c707cf4524756..3b6a9bfdc297f 100644
--- a/routers/api/v1/notify/notifications.go
+++ b/routers/api/v1/notify/notifications.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package notify
@@ -8,7 +7,7 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
@@ -22,16 +21,16 @@ func NewAvailable(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/NotificationCount"
- ctx.JSON(http.StatusOK, api.NotificationCount{New: models.CountUnread(ctx.Doer)})
+ ctx.JSON(http.StatusOK, api.NotificationCount{New: activities_model.CountUnread(ctx, ctx.Doer.ID)})
}
-func getFindNotificationOptions(ctx *context.APIContext) *models.FindNotificationOptions {
+func getFindNotificationOptions(ctx *context.APIContext) *activities_model.FindNotificationOptions {
before, since, err := context.GetQueryBeforeSince(ctx.Context)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return nil
}
- opts := &models.FindNotificationOptions{
+ opts := &activities_model.FindNotificationOptions{
ListOptions: utils.GetListOptions(ctx),
UserID: ctx.Doer.ID,
UpdatedBeforeUnix: before,
@@ -50,18 +49,18 @@ func getFindNotificationOptions(ctx *context.APIContext) *models.FindNotificatio
return opts
}
-func subjectToSource(value []string) (result []models.NotificationSource) {
+func subjectToSource(value []string) (result []activities_model.NotificationSource) {
for _, v := range value {
switch strings.ToLower(v) {
case "issue":
- result = append(result, models.NotificationSourceIssue)
+ result = append(result, activities_model.NotificationSourceIssue)
case "pull":
- result = append(result, models.NotificationSourcePullRequest)
+ result = append(result, activities_model.NotificationSourcePullRequest)
case "commit":
- result = append(result, models.NotificationSourceCommit)
+ result = append(result, activities_model.NotificationSourceCommit)
case "repository":
- result = append(result, models.NotificationSourceRepository)
+ result = append(result, activities_model.NotificationSourceRepository)
}
}
- return
+ return result
}
diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go
index a36bbc6b428ca..bd3b86a6f1525 100644
--- a/routers/api/v1/notify/repo.go
+++ b/routers/api/v1/notify/repo.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package notify
@@ -9,31 +8,31 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/convert"
)
-func statusStringToNotificationStatus(status string) models.NotificationStatus {
+func statusStringToNotificationStatus(status string) activities_model.NotificationStatus {
switch strings.ToLower(strings.TrimSpace(status)) {
case "unread":
- return models.NotificationStatusUnread
+ return activities_model.NotificationStatusUnread
case "read":
- return models.NotificationStatusRead
+ return activities_model.NotificationStatusRead
case "pinned":
- return models.NotificationStatusPinned
+ return activities_model.NotificationStatusPinned
default:
return 0
}
}
-func statusStringsToNotificationStatuses(statuses, defaultStatuses []string) []models.NotificationStatus {
+func statusStringsToNotificationStatuses(statuses, defaultStatuses []string) []activities_model.NotificationStatus {
if len(statuses) == 0 {
statuses = defaultStatuses
}
- results := make([]models.NotificationStatus, 0, len(statuses))
+ results := make([]activities_model.NotificationStatus, 0, len(statuses))
for _, status := range statuses {
notificationStatus := statusStringToNotificationStatus(status)
if notificationStatus > 0 {
@@ -109,18 +108,18 @@ func ListRepoNotifications(ctx *context.APIContext) {
}
opts.RepoID = ctx.Repo.Repository.ID
- totalCount, err := models.CountNotifications(opts)
+ totalCount, err := activities_model.CountNotifications(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- nl, err := models.GetNotifications(opts)
+ nl, err := activities_model.GetNotifications(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- err = nl.LoadAttributes()
+ err = nl.LoadAttributes(ctx)
if err != nil {
ctx.InternalServerError(err)
return
@@ -192,7 +191,7 @@ func ReadRepoNotifications(ctx *context.APIContext) {
}
}
- opts := &models.FindNotificationOptions{
+ opts := &activities_model.FindNotificationOptions{
UserID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
UpdatedBeforeUnix: lastRead,
@@ -203,7 +202,7 @@ func ReadRepoNotifications(ctx *context.APIContext) {
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
log.Error("%v", opts.Status)
}
- nl, err := models.GetNotifications(opts)
+ nl, err := activities_model.GetNotifications(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
@@ -211,18 +210,18 @@ func ReadRepoNotifications(ctx *context.APIContext) {
targetStatus := statusStringToNotificationStatus(ctx.FormString("to-status"))
if targetStatus == 0 {
- targetStatus = models.NotificationStatusRead
+ targetStatus = activities_model.NotificationStatusRead
}
- changed := make([]*structs.NotificationThread, len(nl))
+ changed := make([]*structs.NotificationThread, 0, len(nl))
for _, n := range nl {
- notif, err := models.SetNotificationStatus(n.ID, ctx.Doer, targetStatus)
+ notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
if err != nil {
ctx.InternalServerError(err)
return
}
- _ = notif.LoadAttributes()
+ _ = notif.LoadAttributes(ctx)
changed = append(changed, convert.ToNotificationThread(notif))
}
ctx.JSON(http.StatusResetContent, changed)
diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go
index fe89304dc878e..6a1bce4de41ab 100644
--- a/routers/api/v1/notify/threads.go
+++ b/routers/api/v1/notify/threads.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package notify
@@ -8,9 +7,11 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
+ "code.gitea.io/gitea/services/convert"
)
// GetThread get notification by ID
@@ -40,7 +41,7 @@ func GetThread(ctx *context.APIContext) {
if n == nil {
return
}
- if err := n.LoadAttributes(); err != nil && !models.IsErrCommentNotExist(err) {
+ if err := n.LoadAttributes(ctx); err != nil && !issues_model.IsErrCommentNotExist(err) {
ctx.InternalServerError(err)
return
}
@@ -84,25 +85,25 @@ func ReadThread(ctx *context.APIContext) {
targetStatus := statusStringToNotificationStatus(ctx.FormString("to-status"))
if targetStatus == 0 {
- targetStatus = models.NotificationStatusRead
+ targetStatus = activities_model.NotificationStatusRead
}
- notif, err := models.SetNotificationStatus(n.ID, ctx.Doer, targetStatus)
+ notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
if err != nil {
ctx.InternalServerError(err)
return
}
- if err = notif.LoadAttributes(); err != nil && !models.IsErrCommentNotExist(err) {
+ if err = notif.LoadAttributes(ctx); err != nil && !issues_model.IsErrCommentNotExist(err) {
ctx.InternalServerError(err)
return
}
ctx.JSON(http.StatusResetContent, convert.ToNotificationThread(notif))
}
-func getThread(ctx *context.APIContext) *models.Notification {
- n, err := models.GetNotificationByID(ctx.ParamsInt64(":id"))
+func getThread(ctx *context.APIContext) *activities_model.Notification {
+ n, err := activities_model.GetNotificationByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrNotExist(err) {
+ if db.IsErrNotExist(err) {
ctx.Error(http.StatusNotFound, "GetNotificationByID", err)
} else {
ctx.InternalServerError(err)
diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go
index ac3d0591d093e..2261610c09238 100644
--- a/routers/api/v1/notify/user.go
+++ b/routers/api/v1/notify/user.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package notify
@@ -8,10 +7,10 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/convert"
)
// ListNotifications list users's notification threads
@@ -69,18 +68,18 @@ func ListNotifications(ctx *context.APIContext) {
return
}
- totalCount, err := models.CountNotifications(opts)
+ totalCount, err := activities_model.CountNotifications(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- nl, err := models.GetNotifications(opts)
+ nl, err := activities_model.GetNotifications(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- err = nl.LoadAttributes()
+ err = nl.LoadAttributes(ctx)
if err != nil {
ctx.InternalServerError(err)
return
@@ -140,7 +139,7 @@ func ReadNotifications(ctx *context.APIContext) {
lastRead = tmpLastRead.Unix()
}
}
- opts := &models.FindNotificationOptions{
+ opts := &activities_model.FindNotificationOptions{
UserID: ctx.Doer.ID,
UpdatedBeforeUnix: lastRead,
}
@@ -148,7 +147,7 @@ func ReadNotifications(ctx *context.APIContext) {
statuses := ctx.FormStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
}
- nl, err := models.GetNotifications(opts)
+ nl, err := activities_model.GetNotifications(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
@@ -156,18 +155,18 @@ func ReadNotifications(ctx *context.APIContext) {
targetStatus := statusStringToNotificationStatus(ctx.FormString("to-status"))
if targetStatus == 0 {
- targetStatus = models.NotificationStatusRead
+ targetStatus = activities_model.NotificationStatusRead
}
changed := make([]*structs.NotificationThread, 0, len(nl))
for _, n := range nl {
- notif, err := models.SetNotificationStatus(n.ID, ctx.Doer, targetStatus)
+ notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
if err != nil {
ctx.InternalServerError(err)
return
}
- _ = notif.LoadAttributes()
+ _ = notif.LoadAttributes(ctx)
changed = append(changed, convert.ToNotificationThread(notif))
}
diff --git a/routers/api/v1/org/hook.go b/routers/api/v1/org/hook.go
index 67957430d1e09..4e435c9599a16 100644
--- a/routers/api/v1/org/hook.go
+++ b/routers/api/v1/org/hook.go
@@ -1,18 +1,17 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package org
import (
"net/http"
- "code.gitea.io/gitea/models/webhook"
+ webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ webhook_service "code.gitea.io/gitea/services/webhook"
)
// ListHooks list an organziation's webhooks
@@ -40,18 +39,18 @@ func ListHooks(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/HookList"
- opts := &webhook.ListWebhookOptions{
+ opts := &webhook_model.ListWebhookOptions{
ListOptions: utils.GetListOptions(ctx),
OrgID: ctx.Org.Organization.ID,
}
- count, err := webhook.CountWebhooksByOpts(opts)
+ count, err := webhook_model.CountWebhooksByOpts(opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- orgHooks, err := webhook.ListWebhooksByOpts(opts)
+ orgHooks, err := webhook_model.ListWebhooksByOpts(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
@@ -59,7 +58,11 @@ func ListHooks(ctx *context.APIContext) {
hooks := make([]*api.Hook, len(orgHooks))
for i, hook := range orgHooks {
- hooks[i] = convert.ToHook(ctx.Org.Organization.AsUser().HomeLink(), hook)
+ hooks[i], err = webhook_service.ToHook(ctx.Org.Organization.AsUser().HomeLink(), hook)
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
}
ctx.SetTotalCountHeader(count)
@@ -95,12 +98,18 @@ func GetHook(ctx *context.APIContext) {
if err != nil {
return
}
- ctx.JSON(http.StatusOK, convert.ToHook(org.AsUser().HomeLink(), hook))
+
+ apiHook, err := webhook_service.ToHook(org.AsUser().HomeLink(), hook)
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+ ctx.JSON(http.StatusOK, apiHook)
}
// CreateHook create a hook for an organization
func CreateHook(ctx *context.APIContext) {
- // swagger:operation POST /orgs/{org}/hooks/ organization orgCreateHook
+ // swagger:operation POST /orgs/{org}/hooks organization orgCreateHook
// ---
// summary: Create a hook
// consumes:
@@ -191,8 +200,8 @@ func DeleteHook(ctx *context.APIContext) {
org := ctx.Org.Organization
hookID := ctx.ParamsInt64(":id")
- if err := webhook.DeleteWebhookByOrgID(org.ID, hookID); err != nil {
- if webhook.IsErrWebhookNotExist(err) {
+ if err := webhook_model.DeleteWebhookByOrgID(org.ID, hookID); err != nil {
+ if webhook_model.IsErrWebhookNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "DeleteWebhookByOrgID", err)
diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go
index d36b1d9a9820c..5d0455cdd4f41 100644
--- a/routers/api/v1/org/label.go
+++ b/routers/api/v1/org/label.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package org
@@ -10,12 +9,12 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// ListLabels list all the labels of an organization
@@ -43,13 +42,13 @@ func ListLabels(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/LabelList"
- labels, err := models.GetLabelsByOrgID(ctx.Org.Organization.ID, ctx.FormString("sort"), utils.GetListOptions(ctx))
+ labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsByOrgID", err)
return
}
- count, err := models.CountLabelsByOrgID(ctx.Org.Organization.ID)
+ count, err := issues_model.CountLabelsByOrgID(ctx.Org.Organization.ID)
if err != nil {
ctx.InternalServerError(err)
return
@@ -88,18 +87,18 @@ func CreateLabel(ctx *context.APIContext) {
if len(form.Color) == 6 {
form.Color = "#" + form.Color
}
- if !models.LabelColorPattern.MatchString(form.Color) {
+ if !issues_model.LabelColorPattern.MatchString(form.Color) {
ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color))
return
}
- label := &models.Label{
+ label := &issues_model.Label{
Name: form.Name,
Color: form.Color,
OrgID: ctx.Org.Organization.ID,
Description: form.Description,
}
- if err := models.NewLabel(ctx, label); err != nil {
+ if err := issues_model.NewLabel(ctx, label); err != nil {
ctx.Error(http.StatusInternalServerError, "NewLabel", err)
return
}
@@ -131,17 +130,17 @@ func GetLabel(ctx *context.APIContext) {
// "$ref": "#/responses/Label"
var (
- label *models.Label
+ label *issues_model.Label
err error
)
strID := ctx.Params(":id")
if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil {
- label, err = models.GetLabelInOrgByName(ctx.Org.Organization.ID, strID)
+ label, err = issues_model.GetLabelInOrgByName(ctx, ctx.Org.Organization.ID, strID)
} else {
- label, err = models.GetLabelInOrgByID(ctx.Org.Organization.ID, intID)
+ label, err = issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, intID)
}
if err != nil {
- if models.IsErrOrgLabelNotExist(err) {
+ if issues_model.IsErrOrgLabelNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelByOrgID", err)
@@ -183,9 +182,9 @@ func EditLabel(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditLabelOption)
- label, err := models.GetLabelInOrgByID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
+ label, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrOrgLabelNotExist(err) {
+ if issues_model.IsErrOrgLabelNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)
@@ -201,7 +200,7 @@ func EditLabel(ctx *context.APIContext) {
if len(label.Color) == 6 {
label.Color = "#" + label.Color
}
- if !models.LabelColorPattern.MatchString(label.Color) {
+ if !issues_model.LabelColorPattern.MatchString(label.Color) {
ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color))
return
}
@@ -209,7 +208,7 @@ func EditLabel(ctx *context.APIContext) {
if form.Description != nil {
label.Description = *form.Description
}
- if err := models.UpdateLabel(label); err != nil {
+ if err := issues_model.UpdateLabel(label); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateLabel", err)
return
}
@@ -238,7 +237,7 @@ func DeleteLabel(ctx *context.APIContext) {
// "204":
// "$ref": "#/responses/empty"
- if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")); err != nil {
+ if err := issues_model.DeleteLabel(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteLabel", err)
return
}
diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go
index 85fe2ded4d5c7..33c9944978038 100644
--- a/routers/api/v1/org/member.go
+++ b/routers/api/v1/org/member.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package org
@@ -11,11 +10,11 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// listMembers list an organization's members
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index d55a4a4514e31..a1b071d4887a2 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -1,7 +1,6 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package org
@@ -13,11 +12,11 @@ import (
"code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/org"
)
@@ -47,7 +46,7 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
- ctx.SetTotalCountHeader(int64(maxResults))
+ ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &apiOrgs)
}
diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go
index d0f1fbef749f3..8f87e82764857 100644
--- a/routers/api/v1/org/team.go
+++ b/routers/api/v1/org/team.go
@@ -1,7 +1,6 @@
// Copyright 2016 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.
+// SPDX-License-Identifier: MIT
package org
@@ -12,15 +11,17 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
+ org_service "code.gitea.io/gitea/services/org"
)
// ListTeams list all the teams of an organization
@@ -57,14 +58,10 @@ func ListTeams(ctx *context.APIContext) {
return
}
- apiTeams := make([]*api.Team, len(teams))
- for i := range teams {
- if err := teams[i].GetUnits(); err != nil {
- ctx.Error(http.StatusInternalServerError, "GetUnits", err)
- return
- }
-
- apiTeams[i] = convert.ToTeam(teams[i])
+ apiTeams, err := convert.ToTeams(teams, false)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "ConvertToTeams", err)
+ return
}
ctx.SetTotalCountHeader(count)
@@ -100,25 +97,10 @@ func ListUserTeams(ctx *context.APIContext) {
return
}
- cache := make(map[int64]*api.Organization)
- apiTeams := make([]*api.Team, len(teams))
- for i := range teams {
- apiOrg, ok := cache[teams[i].OrgID]
- if !ok {
- org, err := organization.GetOrgByID(teams[i].OrgID)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetUserByID", err)
- return
- }
- apiOrg = convert.ToOrganization(org)
- cache[teams[i].OrgID] = apiOrg
- }
- if err := teams[i].GetUnits(); err != nil {
- ctx.Error(http.StatusInternalServerError, "teams[i].GetUnits()", err)
- return
- }
- apiTeams[i] = convert.ToTeam(teams[i])
- apiTeams[i].Organization = apiOrg
+ apiTeams, err := convert.ToTeams(teams, true)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "ConvertToTeams", err)
+ return
}
ctx.SetTotalCountHeader(count)
@@ -143,12 +125,13 @@ func GetTeam(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/Team"
- if err := ctx.Org.Team.GetUnits(); err != nil {
- ctx.Error(http.StatusInternalServerError, "team.GetUnits", err)
+ apiTeam, err := convert.ToTeam(ctx.Org.Team)
+ if err != nil {
+ ctx.InternalServerError(err)
return
}
- ctx.JSON(http.StatusOK, convert.ToTeam(ctx.Org.Team))
+ ctx.JSON(http.StatusOK, apiTeam)
}
func attachTeamUnits(team *organization.Team, units []string) {
@@ -240,7 +223,12 @@ func CreateTeam(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusCreated, convert.ToTeam(team))
+ apiTeam, err := convert.ToTeam(team)
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+ ctx.JSON(http.StatusCreated, apiTeam)
}
// EditTeam api for edit a team
@@ -274,7 +262,7 @@ func EditTeam(ctx *context.APIContext) {
}
if form.CanCreateOrgRepo != nil {
- team.CanCreateOrgRepo = *form.CanCreateOrgRepo
+ team.CanCreateOrgRepo = team.IsOwnerTeam() || *form.CanCreateOrgRepo
}
if len(form.Name) > 0 {
@@ -317,7 +305,13 @@ func EditTeam(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "EditTeam", err)
return
}
- ctx.JSON(http.StatusOK, convert.ToTeam(team))
+
+ apiTeam, err := convert.ToTeam(team)
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+ ctx.JSON(http.StatusOK, apiTeam)
}
// DeleteTeam api for delete a team
@@ -545,19 +539,68 @@ func GetTeamRepos(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err)
return
}
- repos := make([]*api.Repository, len(team.Repos))
+ repos := make([]*api.Repository, len(teamRepos))
for i, repo := range teamRepos {
- access, err := models.AccessLevel(ctx.Doer, repo)
+ access, err := access_model.AccessLevel(ctx, ctx.Doer, repo)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err)
return
}
- repos[i] = convert.ToRepo(repo, access)
+ repos[i] = convert.ToRepo(ctx, repo, access)
}
ctx.SetTotalCountHeader(int64(team.NumRepos))
ctx.JSON(http.StatusOK, repos)
}
+// GetTeamRepo api for get a particular repo of team
+func GetTeamRepo(ctx *context.APIContext) {
+ // swagger:operation GET /teams/{id}/repos/{org}/{repo} organization orgListTeamRepo
+ // ---
+ // summary: List a particular repo of team
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: id
+ // in: path
+ // description: id of the team
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: org
+ // in: path
+ // description: organization that owns the repo to list
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo to list
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/Repository"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ repo := getRepositoryByParams(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ if !organization.HasTeamRepo(ctx, ctx.Org.Team.OrgID, ctx.Org.Team.ID, repo.ID) {
+ ctx.NotFound()
+ return
+ }
+
+ access, err := access_model.AccessLevel(ctx, ctx.Doer, repo)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, access))
+}
+
// getRepositoryByParams get repository by a team's organization ID and repo name
func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository {
repo, err := repo_model.GetRepositoryByName(ctx.Org.Team.OrgID, ctx.Params(":reponame"))
@@ -606,15 +649,15 @@ func AddTeamRepository(ctx *context.APIContext) {
if ctx.Written() {
return
}
- if access, err := models.AccessLevel(ctx.Doer, repo); err != nil {
+ if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil {
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
return
} else if access < perm.AccessModeAdmin {
ctx.Error(http.StatusForbidden, "", "Must have admin-level access to the repository")
return
}
- if err := models.AddRepository(ctx.Org.Team, repo); err != nil {
- ctx.Error(http.StatusInternalServerError, "AddRepository", err)
+ if err := org_service.TeamAddRepository(ctx.Org.Team, repo); err != nil {
+ ctx.Error(http.StatusInternalServerError, "TeamAddRepository", err)
return
}
ctx.Status(http.StatusNoContent)
@@ -656,7 +699,7 @@ func RemoveTeamRepository(ctx *context.APIContext) {
if ctx.Written() {
return
}
- if access, err := models.AccessLevel(ctx.Doer, repo); err != nil {
+ if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil {
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
return
} else if access < perm.AccessModeAdmin {
@@ -715,13 +758,17 @@ func SearchTeam(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
opts := &organization.SearchTeamOptions{
- UserID: ctx.Doer.ID,
Keyword: ctx.FormTrim("q"),
OrgID: ctx.Org.Organization.ID,
IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"),
ListOptions: listOptions,
}
+ // Only admin is allowd to search for all teams
+ if !ctx.Doer.IsAdmin {
+ opts.UserID = ctx.Doer.ID
+ }
+
teams, maxResults, err := organization.SearchTeam(opts)
if err != nil {
log.Error("SearchTeam failed: %v", err)
@@ -732,17 +779,10 @@ func SearchTeam(ctx *context.APIContext) {
return
}
- apiTeams := make([]*api.Team, len(teams))
- for i := range teams {
- if err := teams[i].GetUnits(); err != nil {
- log.Error("Team GetUnits failed: %v", err)
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "ok": false,
- "error": "SearchTeam failed to get units",
- })
- return
- }
- apiTeams[i] = convert.ToTeam(teams[i])
+ apiTeams, err := convert.ToTeams(teams, false)
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go
index b445e8e2f830e..6f9083ba327b5 100644
--- a/routers/api/v1/packages/package.go
+++ b/routers/api/v1/packages/package.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package packages
@@ -9,9 +8,10 @@ import (
"code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
// in: query
// description: package type filter
// type: string
- // enum: [composer, conan, generic, maven, npm, nuget, pypi, rubygems]
+ // enum: [composer, conan, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant]
// - name: q
// in: query
// description: name filter
@@ -55,10 +55,11 @@ func ListPackages(ctx *context.APIContext) {
query := ctx.FormTrim("q")
pvs, count, err := packages.SearchVersions(ctx, &packages.PackageSearchOptions{
- OwnerID: ctx.Package.Owner.ID,
- Type: packages.Type(packageType),
- Name: packages.SearchValue{Value: query},
- Paginator: &listOptions,
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages.Type(packageType),
+ Name: packages.SearchValue{Value: query},
+ IsInternal: util.OptionalBoolFalse,
+ Paginator: &listOptions,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "SearchVersions", err)
@@ -73,7 +74,12 @@ func ListPackages(ctx *context.APIContext) {
apiPackages := make([]*api.Package, 0, len(pds))
for _, pd := range pds {
- apiPackages = append(apiPackages, convert.ToPackage(pd))
+ apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "Error converting package for api", err)
+ return
+ }
+ apiPackages = append(apiPackages, apiPackage)
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
@@ -115,7 +121,13 @@ func GetPackage(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- ctx.JSON(http.StatusOK, convert.ToPackage(ctx.Package.Descriptor))
+ apiPackage, err := convert.ToPackage(ctx, ctx.Package.Descriptor, ctx.Doer)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "Error converting package for api", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, apiPackage)
}
// DeletePackage deletes a package
diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go
index 19d893a68b92e..d0004f06860df 100644
--- a/routers/api/v1/repo/blob.go
+++ b/routers/api/v1/repo/blob.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -45,7 +44,8 @@ func GetBlob(ctx *context.APIContext) {
ctx.Error(http.StatusBadRequest, "", "sha not provided")
return
}
- if blob, err := files_service.GetBlobBySHA(ctx, ctx.Repo.Repository, sha); err != nil {
+
+ if blob, err := files_service.GetBlobBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil {
ctx.Error(http.StatusBadRequest, "", err)
} else {
ctx.JSON(http.StatusOK, blob)
diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go
index f22bed813e152..eacec6a609104 100644
--- a/routers/api/v1/repo/branch.go
+++ b/routers/api/v1/repo/branch.go
@@ -1,7 +1,6 @@
// Copyright 2016 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -11,14 +10,15 @@ import (
"net/http"
"code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -70,7 +70,7 @@ func GetBranch(ctx *context.APIContext) {
return
}
- branchProtection, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branchName)
+ branchProtection, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, branchName)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
return
@@ -124,7 +124,7 @@ func DeleteBranch(ctx *context.APIContext) {
ctx.NotFound(err)
case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch"))
- case errors.Is(err, repo_service.ErrBranchIsProtected):
+ case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected"))
default:
ctx.Error(http.StatusInternalServerError, "DeleteBranch", err)
@@ -206,7 +206,7 @@ func CreateBranch(ctx *context.APIContext) {
return
}
- branchProtection, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branch.Name)
+ branchProtection, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, branch.Name)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
return
@@ -251,36 +251,53 @@ func ListBranches(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/BranchList"
+ var totalNumOfBranches int
+ var apiBranches []*api.Branch
+
listOptions := utils.GetListOptions(ctx)
- skip, _ := listOptions.GetStartEnd()
- branches, totalNumOfBranches, err := ctx.Repo.GitRepo.GetBranches(skip, listOptions.PageSize)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetBranches", err)
- return
- }
- apiBranches := make([]*api.Branch, len(branches))
- for i := range branches {
- c, err := branches[i].GetCommit()
+ if !ctx.Repo.Repository.IsEmpty && ctx.Repo.GitRepo != nil {
+ rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetCommit", err)
+ ctx.Error(http.StatusInternalServerError, "FindMatchedProtectedBranchRules", err)
return
}
- branchProtection, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branches[i].Name)
+
+ skip, _ := listOptions.GetStartEnd()
+ branches, total, err := ctx.Repo.GitRepo.GetBranches(skip, listOptions.PageSize)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
+ ctx.Error(http.StatusInternalServerError, "GetBranches", err)
return
}
- apiBranches[i], err = convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
- return
+
+ apiBranches = make([]*api.Branch, 0, len(branches))
+ for i := range branches {
+ c, err := branches[i].GetCommit()
+ if err != nil {
+ // Skip if this branch doesn't exist anymore.
+ if git.IsErrNotExist(err) {
+ total--
+ continue
+ }
+ ctx.Error(http.StatusInternalServerError, "GetCommit", err)
+ return
+ }
+
+ branchProtection := rules.GetFirstMatched(branches[i].Name)
+ apiBranch, err := convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
+ return
+ }
+ apiBranches = append(apiBranches, apiBranch)
}
+
+ totalNumOfBranches = total
}
ctx.SetLinkHeader(totalNumOfBranches, listOptions.PageSize)
ctx.SetTotalCountHeader(int64(totalNumOfBranches))
- ctx.JSON(http.StatusOK, &apiBranches)
+ ctx.JSON(http.StatusOK, apiBranches)
}
// GetBranchProtection gets a branch protection
@@ -314,7 +331,7 @@ func GetBranchProtection(ctx *context.APIContext) {
repo := ctx.Repo.Repository
bpName := ctx.Params(":name")
- bp, err := models.GetProtectedBranchBy(repo.ID, bpName)
+ bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
return
@@ -350,7 +367,7 @@ func ListBranchProtections(ctx *context.APIContext) {
// "$ref": "#/responses/BranchProtectionList"
repo := ctx.Repo.Repository
- bps, err := models.GetProtectedBranches(repo.ID)
+ bps, err := git_model.FindRepoProtectedBranchRules(ctx, repo.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranches", err)
return
@@ -400,13 +417,18 @@ func CreateBranchProtection(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateBranchProtectionOption)
repo := ctx.Repo.Repository
- // Currently protection must match an actual branch
- if !git.IsBranchExist(ctx.Req.Context(), ctx.Repo.Repository.RepoPath(), form.BranchName) {
- ctx.NotFound()
- return
+ ruleName := form.RuleName
+ if ruleName == "" {
+ ruleName = form.BranchName //nolint
+ }
+
+ isPlainRule := !git_model.IsRuleNameSpecial(ruleName)
+ var isBranchExist bool
+ if isPlainRule {
+ isBranchExist = git.IsBranchExist(ctx.Req.Context(), ctx.Repo.Repository.RepoPath(), ruleName)
}
- protectBranch, err := models.GetProtectedBranchBy(repo.ID, form.BranchName)
+ protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, ruleName)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectBranchOfRepoByName", err)
return
@@ -420,7 +442,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
requiredApprovals = form.RequiredApprovals
}
- whitelistUsers, err := user_model.GetUserIDsByNames(form.PushWhitelistUsernames, false)
+ whitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
@@ -429,7 +451,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
return
}
- mergeWhitelistUsers, err := user_model.GetUserIDsByNames(form.MergeWhitelistUsernames, false)
+ mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
@@ -438,7 +460,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
return
}
- approvalsWhitelistUsers, err := user_model.GetUserIDsByNames(form.ApprovalsWhitelistUsernames, false)
+ approvalsWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
@@ -478,9 +500,9 @@ func CreateBranchProtection(ctx *context.APIContext) {
}
}
- protectBranch = &models.ProtectedBranch{
+ protectBranch = &git_model.ProtectedBranch{
RepoID: ctx.Repo.Repository.ID,
- BranchName: form.BranchName,
+ RuleName: form.RuleName,
CanPush: form.EnablePush,
EnableWhitelist: form.EnablePush && form.EnablePushWhitelist,
EnableMergeWhitelist: form.EnableMergeWhitelist,
@@ -498,7 +520,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
}
- err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
+ err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
UserIDs: whitelistUsers,
TeamIDs: whitelistTeams,
MergeUserIDs: mergeWhitelistUsers,
@@ -511,13 +533,42 @@ func CreateBranchProtection(ctx *context.APIContext) {
return
}
- if err = pull_service.CheckPrsForBaseBranch(ctx.Repo.Repository, protectBranch.BranchName); err != nil {
- ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err)
- return
+ if isBranchExist {
+ if err = pull_service.CheckPRsForBaseBranch(ctx.Repo.Repository, form.RuleName); err != nil {
+ ctx.Error(http.StatusInternalServerError, "CheckPRsForBaseBranch", err)
+ return
+ }
+ } else {
+ if !isPlainRule {
+ if ctx.Repo.GitRepo == nil {
+ ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
+ return
+ }
+ defer func() {
+ ctx.Repo.GitRepo.Close()
+ ctx.Repo.GitRepo = nil
+ }()
+ }
+ // FIXME: since we only need to recheck files protected rules, we could improve this
+ matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, form.RuleName)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err)
+ return
+ }
+
+ for _, branchName := range matchedBranches {
+ if err = pull_service.CheckPRsForBaseBranch(ctx.Repo.Repository, branchName); err != nil {
+ ctx.Error(http.StatusInternalServerError, "CheckPRsForBaseBranch", err)
+ return
+ }
+ }
+ }
}
// Reload from db to get all whitelists
- bp, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, form.BranchName)
+ bp, err := git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, form.RuleName)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
return
@@ -569,7 +620,7 @@ func EditBranchProtection(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditBranchProtectionOption)
repo := ctx.Repo.Repository
bpName := ctx.Params(":name")
- protectBranch, err := models.GetProtectedBranchBy(repo.ID, bpName)
+ protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
return
@@ -649,7 +700,7 @@ func EditBranchProtection(ctx *context.APIContext) {
var whitelistUsers []int64
if form.PushWhitelistUsernames != nil {
- whitelistUsers, err = user_model.GetUserIDsByNames(form.PushWhitelistUsernames, false)
+ whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
@@ -663,7 +714,7 @@ func EditBranchProtection(ctx *context.APIContext) {
}
var mergeWhitelistUsers []int64
if form.MergeWhitelistUsernames != nil {
- mergeWhitelistUsers, err = user_model.GetUserIDsByNames(form.MergeWhitelistUsernames, false)
+ mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
@@ -677,7 +728,7 @@ func EditBranchProtection(ctx *context.APIContext) {
}
var approvalsWhitelistUsers []int64
if form.ApprovalsWhitelistUsernames != nil {
- approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(form.ApprovalsWhitelistUsernames, false)
+ approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
@@ -733,7 +784,7 @@ func EditBranchProtection(ctx *context.APIContext) {
}
}
- err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
+ err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
UserIDs: whitelistUsers,
TeamIDs: whitelistTeams,
MergeUserIDs: mergeWhitelistUsers,
@@ -746,13 +797,49 @@ func EditBranchProtection(ctx *context.APIContext) {
return
}
- if err = pull_service.CheckPrsForBaseBranch(ctx.Repo.Repository, protectBranch.BranchName); err != nil {
- ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err)
- return
+ isPlainRule := !git_model.IsRuleNameSpecial(bpName)
+ var isBranchExist bool
+ if isPlainRule {
+ isBranchExist = git.IsBranchExist(ctx.Req.Context(), ctx.Repo.Repository.RepoPath(), bpName)
+ }
+
+ if isBranchExist {
+ if err = pull_service.CheckPRsForBaseBranch(ctx.Repo.Repository, bpName); err != nil {
+ ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err)
+ return
+ }
+ } else {
+ if !isPlainRule {
+ if ctx.Repo.GitRepo == nil {
+ ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
+ return
+ }
+ defer func() {
+ ctx.Repo.GitRepo.Close()
+ ctx.Repo.GitRepo = nil
+ }()
+ }
+
+ // FIXME: since we only need to recheck files protected rules, we could improve this
+ matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, protectBranch.RuleName)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err)
+ return
+ }
+
+ for _, branchName := range matchedBranches {
+ if err = pull_service.CheckPRsForBaseBranch(ctx.Repo.Repository, branchName); err != nil {
+ ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err)
+ return
+ }
+ }
+ }
}
// Reload from db to ensure get all whitelists
- bp, err := models.GetProtectedBranchBy(repo.ID, bpName)
+ bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err)
return
@@ -796,7 +883,7 @@ func DeleteBranchProtection(ctx *context.APIContext) {
repo := ctx.Repo.Repository
bpName := ctx.Params(":name")
- bp, err := models.GetProtectedBranchBy(repo.ID, bpName)
+ bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
return
@@ -806,7 +893,7 @@ func DeleteBranchProtection(ctx *context.APIContext) {
return
}
- if err := models.DeleteProtectedBranch(ctx.Repo.Repository.ID, bp.ID); err != nil {
+ if err := git_model.DeleteProtectedBranch(ctx, ctx.Repo.Repository.ID, bp.ID); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteProtectedBranch", err)
return
}
diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go
index 3bb6113d772ac..293778420bc2b 100644
--- a/routers/api/v1/repo/collaborators.go
+++ b/routers/api/v1/repo/collaborators.go
@@ -1,7 +1,6 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -11,12 +10,15 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
+ repo_module "code.gitea.io/gitea/modules/repository"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// ListCollaborators list a repository's collaborators
@@ -49,13 +51,13 @@ func ListCollaborators(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/UserList"
- count, err := models.CountCollaborators(ctx.Repo.Repository.ID)
+ count, err := repo_model.CountCollaborators(ctx.Repo.Repository.ID)
if err != nil {
ctx.InternalServerError(err)
return
}
- collaborators, err := models.GetCollaborators(ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
+ collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
return
@@ -101,7 +103,7 @@ func IsCollaborator(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
- user, err := user_model.GetUserByName(ctx.Params(":collaborator"))
+ user, err := user_model.GetUserByName(ctx, ctx.Params(":collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
@@ -110,7 +112,7 @@ func IsCollaborator(ctx *context.APIContext) {
}
return
}
- isColab, err := models.IsCollaborator(ctx.Repo.Repository.ID, user.ID)
+ isColab, err := repo_model.IsCollaborator(ctx, ctx.Repo.Repository.ID, user.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "IsCollaborator", err)
return
@@ -157,7 +159,7 @@ func AddCollaborator(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.AddCollaboratorOption)
- collaborator, err := user_model.GetUserByName(ctx.Params(":collaborator"))
+ collaborator, err := user_model.GetUserByName(ctx, ctx.Params(":collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
@@ -172,13 +174,13 @@ func AddCollaborator(ctx *context.APIContext) {
return
}
- if err := models.AddCollaborator(ctx.Repo.Repository, collaborator); err != nil {
+ if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
return
}
if form.Permission != nil {
- if err := models.ChangeCollaborationAccessMode(ctx.Repo.Repository, collaborator.ID, perm.ParseAccessMode(*form.Permission)); err != nil {
+ if err := repo_model.ChangeCollaborationAccessMode(ctx, ctx.Repo.Repository, collaborator.ID, perm.ParseAccessMode(*form.Permission)); err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeCollaborationAccessMode", err)
return
}
@@ -216,7 +218,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
- collaborator, err := user_model.GetUserByName(ctx.Params(":collaborator"))
+ collaborator, err := user_model.GetUserByName(ctx, ctx.Params(":collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
@@ -233,6 +235,61 @@ func DeleteCollaborator(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
+// GetRepoPermissions gets repository permissions for a user
+func GetRepoPermissions(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/collaborators/{collaborator}/permission repository repoGetRepoPermissions
+ // ---
+ // summary: Get repository permissions for a user
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: collaborator
+ // in: path
+ // description: username of the collaborator
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/RepoCollaboratorPermission"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ if !ctx.Doer.IsAdmin && ctx.Doer.LoginName != ctx.Params(":collaborator") && !ctx.IsUserRepoAdmin() {
+ ctx.Error(http.StatusForbidden, "User", "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own")
+ return
+ }
+
+ collaborator, err := user_model.GetUserByName(ctx, ctx.Params(":collaborator"))
+ if err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ ctx.Error(http.StatusNotFound, "GetUserByName", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
+ }
+ return
+ }
+
+ permission, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, collaborator)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToUserAndPermission(collaborator, ctx.ContextUser, permission.AccessMode))
+}
+
// GetReviewers return all users that can be requested to review in this repo
func GetReviewers(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/reviewers repository repoGetReviewers
@@ -255,7 +312,7 @@ func GetReviewers(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/UserList"
- reviewers, err := models.GetReviewers(ctx.Repo.Repository, ctx.Doer.ID, 0)
+ reviewers, err := repo_model.GetReviewers(ctx, ctx.Repo.Repository, ctx.Doer.ID, 0)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
return
@@ -285,7 +342,7 @@ func GetAssignees(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/UserList"
- assignees, err := models.GetRepoAssignees(ctx.Repo.Repository)
+ assignees, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
return
diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go
index b6c47e0685186..68a92ca2a840b 100644
--- a/routers/api/v1/repo/commits.go
+++ b/routers/api/v1/repo/commits.go
@@ -1,7 +1,6 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -11,15 +10,13 @@ import (
"net/http"
"strconv"
- repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// GetSingleCommit get a commit via sha
@@ -54,7 +51,7 @@ func GetSingleCommit(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
sha := ctx.Params(":sha")
- if (validation.GitRefNamePatternInvalid.MatchString(sha) || !validation.CheckGitRefAdditionalRulesValid(sha)) && !git.SHAPattern.MatchString(sha) {
+ if !git.IsValidRefPattern(sha) {
ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha))
return
}
@@ -72,7 +69,7 @@ func getCommit(ctx *context.APIContext, identifier string) {
return
}
- json, err := convert.ToCommit(ctx.Repo.Repository, ctx.Repo.GitRepo, commit, nil)
+ json, err := convert.ToCommit(ctx.Repo.Repository, ctx.Repo.GitRepo, commit, nil, true)
if err != nil {
ctx.Error(http.StatusInternalServerError, "toCommit", err)
return
@@ -106,6 +103,10 @@ func GetAllCommits(ctx *context.APIContext) {
// in: query
// description: filepath of a file/dir
// type: string
+ // - name: stat
+ // in: query
+ // description: include diff stats for every commit (disable for speedup, default 'true')
+ // type: boolean
// - name: page
// in: query
// description: page number of results to return (1-based)
@@ -211,9 +212,12 @@ func GetAllCommits(ctx *context.APIContext) {
userCache := make(map[string]*user_model.User)
apiCommits := make([]*api.Commit, len(commits))
+
+ stat := ctx.FormString("stat") == "" || ctx.FormBool("stat")
+
for i, commit := range commits {
// Create json struct
- apiCommits[i], err = convert.ToCommit(ctx.Repo.Repository, ctx.Repo.GitRepo, commit, userCache)
+ apiCommits[i], err = convert.ToCommit(ctx.Repo.Repository, ctx.Repo.GitRepo, commit, userCache, stat)
if err != nil {
ctx.Error(http.StatusInternalServerError, "toCommit", err)
return
@@ -268,16 +272,12 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) {
// "$ref": "#/responses/string"
// "404":
// "$ref": "#/responses/notFound"
- repoPath := repo_model.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
- if err := git.GetRawDiff(
- ctx,
- repoPath,
- ctx.Params(":sha"),
- git.RawDiffType(ctx.Params(":diffType")),
- ctx.Resp,
- ); err != nil {
+ sha := ctx.Params(":sha")
+ diffType := git.RawDiffType(ctx.Params(":diffType"))
+
+ if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, diffType, ctx.Resp); err != nil {
if git.IsErrNotExist(err) {
- ctx.NotFound(ctx.Params(":sha"))
+ ctx.NotFound(sha)
return
}
ctx.Error(http.StatusInternalServerError, "DownloadCommitDiffOrPatch", err)
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index a4811b370a911..d5e8924f5dace 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -1,28 +1,39 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
import (
+ "bytes"
"encoding/base64"
+ "errors"
"fmt"
+ "io"
"net/http"
+ "path"
"time"
"code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/httpcache"
+ "code.gitea.io/gitea/modules/lfs"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
- "code.gitea.io/gitea/routers/web/repo"
+ archiver_service "code.gitea.io/gitea/services/repository/archiver"
files_service "code.gitea.io/gitea/services/repository/files"
)
+const giteaObjectTypeHeader = "X-Gitea-Object-Type"
+
// GetRawFile get a file by path on a repository
func GetRawFile(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
@@ -53,7 +64,7 @@ func GetRawFile(ctx *context.APIContext) {
// required: false
// responses:
// 200:
- // description: success
+ // description: Returns raw file content.
// "404":
// "$ref": "#/responses/notFound"
@@ -62,33 +73,185 @@ func GetRawFile(ctx *context.APIContext) {
return
}
- commit := ctx.Repo.Commit
+ blob, entry, lastModified := getBlobForEntry(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
- if ref := ctx.FormTrim("ref"); len(ref) > 0 {
- var err error
- commit, err = ctx.Repo.GitRepo.GetCommit(ref)
- if err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound()
- } else {
- ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
- }
+ if err := common.ServeBlob(ctx.Context, blob, lastModified); err != nil {
+ ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
+ }
+}
+
+// GetRawFileOrLFS get a file by repo's path, redirecting to LFS if necessary.
+func GetRawFileOrLFS(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/media/{filepath} repository repoGetRawFileOrLFS
+ // ---
+ // summary: Get a file or it's LFS object from a repository
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: filepath
+ // in: path
+ // description: filepath of the file to get
+ // type: string
+ // required: true
+ // - name: ref
+ // in: query
+ // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
+ // type: string
+ // required: false
+ // responses:
+ // 200:
+ // description: Returns raw file content.
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ if ctx.Repo.Repository.IsEmpty {
+ ctx.NotFound()
+ return
+ }
+
+ blob, entry, lastModified := getBlobForEntry(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
+
+ // LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file
+ if blob.Size() > 1024 {
+ // First handle caching for the blob
+ if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
+ return
+ }
+
+ // OK not cached - serve!
+ if err := common.ServeBlob(ctx.Context, blob, lastModified); err != nil {
+ ctx.ServerError("ServeBlob", err)
+ }
+ return
+ }
+
+ // OK, now the blob is known to have at most 1024 bytes we can simply read this in in one go (This saves reading it twice)
+ dataRc, err := blob.DataAsync()
+ if err != nil {
+ ctx.ServerError("DataAsync", err)
+ return
+ }
+
+ buf, err := io.ReadAll(dataRc)
+ if err != nil {
+ _ = dataRc.Close()
+ ctx.ServerError("DataAsync", err)
+ return
+ }
+
+ if err := dataRc.Close(); err != nil {
+ log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Context.Repo.Repository, err)
+ }
+
+ // Check if the blob represents a pointer
+ pointer, _ := lfs.ReadPointer(bytes.NewReader(buf))
+
+ // if its not a pointer just serve the data directly
+ if !pointer.IsValid() {
+ // First handle caching for the blob
+ if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return
}
+
+ // OK not cached - serve!
+ if err := common.ServeData(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)); err != nil {
+ ctx.ServerError("ServeBlob", err)
+ }
+ return
+ }
+
+ // Now check if there is a meta object for this pointer
+ meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
+
+ // If there isn't one just serve the data directly
+ if err == git_model.ErrLFSObjectNotExist {
+ // Handle caching for the blob SHA (not the LFS object OID)
+ if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
+ return
+ }
+
+ if err := common.ServeData(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)); err != nil {
+ ctx.ServerError("ServeBlob", err)
+ }
+ return
+ } else if err != nil {
+ ctx.ServerError("GetLFSMetaObjectByOid", err)
+ return
+ }
+
+ // Handle caching for the LFS object OID
+ if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
+ return
+ }
+
+ if setting.LFS.ServeDirect {
+ // If we have a signed url (S3, object storage), redirect to this directly.
+ u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name())
+ if u != nil && err == nil {
+ ctx.Redirect(u.String())
+ return
+ }
+ }
+
+ lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
+ if err != nil {
+ ctx.ServerError("ReadMetaObject", err)
+ return
+ }
+ defer lfsDataRc.Close()
+
+ if err := common.ServeData(ctx.Context, ctx.Repo.TreePath, meta.Size, lfsDataRc); err != nil {
+ ctx.ServerError("ServeData", err)
}
+}
- blob, err := commit.GetBlobByPath(ctx.Repo.TreePath)
+func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified time.Time) {
+ entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound()
} else {
- ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
+ ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath", err)
}
return
}
- if err = common.ServeBlob(ctx.Context, blob); err != nil {
- ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
+
+ if entry.IsDir() || entry.IsSubModule() {
+ ctx.NotFound("getBlobForEntry", nil)
+ return
+ }
+
+ info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:])
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err)
+ return
+ }
+
+ if len(info) == 1 {
+ // Not Modified
+ lastModified = info[0].Commit.Committer.When
}
+ blob = entry.Blob()
+
+ return blob, entry, lastModified
}
// GetArchive get archive of a repository
@@ -131,7 +294,57 @@ func GetArchive(ctx *context.APIContext) {
defer gitRepo.Close()
}
- repo.Download(ctx.Context)
+ archiveDownload(ctx)
+}
+
+func archiveDownload(ctx *context.APIContext) {
+ uri := ctx.Params("*")
+ aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
+ if err != nil {
+ if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
+ ctx.Error(http.StatusBadRequest, "unknown archive format", err)
+ } else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
+ ctx.Error(http.StatusNotFound, "unrecognized reference", err)
+ } else {
+ ctx.ServerError("archiver_service.NewRequest", err)
+ }
+ return
+ }
+
+ archiver, err := aReq.Await(ctx)
+ if err != nil {
+ ctx.ServerError("archiver.Await", err)
+ return
+ }
+
+ download(ctx, aReq.GetArchiveName(), archiver)
+}
+
+func download(ctx *context.APIContext, archiveName string, archiver *repo_model.RepoArchiver) {
+ downloadName := ctx.Repo.Repository.Name + "-" + archiveName
+
+ rPath := archiver.RelativePath()
+ if setting.RepoArchive.ServeDirect {
+ // If we have a signed url (S3, object storage), redirect to this directly.
+ u, err := storage.RepoArchives.URL(rPath, downloadName)
+ if u != nil && err == nil {
+ ctx.Redirect(u.String())
+ return
+ }
+ }
+
+ // If we have matched and access to release or issue
+ fr, err := storage.RepoArchives.Open(rPath)
+ if err != nil {
+ ctx.ServerError("Open", err)
+ return
+ }
+ defer fr.Close()
+
+ ctx.ServeContent(fr, &context.ServeHeaderOptions{
+ Filename: downloadName,
+ LastModified: archiver.CreatedUnix.AsLocalTime(),
+ })
}
// GetEditorconfig get editor config of a repository
@@ -157,13 +370,18 @@ func GetEditorconfig(ctx *context.APIContext) {
// description: filepath of file to get
// type: string
// required: true
+ // - name: ref
+ // in: query
+ // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
+ // type: string
+ // required: false
// responses:
// 200:
// description: success
// "404":
// "$ref": "#/responses/notFound"
- ec, err := ctx.Repo.GetEditorconfig()
+ ec, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(err)
@@ -183,8 +401,10 @@ func GetEditorconfig(ctx *context.APIContext) {
}
// canWriteFiles returns true if repository is editable and user has proper access level.
-func canWriteFiles(r *context.Repository) bool {
- return r.Permission.CanWrite(unit.TypeCode) && !r.Repository.IsMirror && !r.Repository.IsArchived
+func canWriteFiles(ctx *context.APIContext, branch string) bool {
+ return ctx.Repo.CanWriteToBranch(ctx.Doer, branch) &&
+ !ctx.Repo.Repository.IsMirror &&
+ !ctx.Repo.Repository.IsArchived
}
// canReadFiles returns true if repository is readable and user has proper access level.
@@ -386,8 +606,8 @@ func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
// Called from both CreateFile or UpdateFile to handle both
func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoFileOptions) (*api.FileResponse, error) {
- if !canWriteFiles(ctx.Repo) {
- return nil, models.ErrUserDoesNotHaveAccessToRepo{
+ if !canWriteFiles(ctx, opts.OldBranch) {
+ return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
UserID: ctx.Doer.ID,
RepoName: ctx.Repo.Repository.LowerName,
}
@@ -443,8 +663,8 @@ func DeleteFile(ctx *context.APIContext) {
// "$ref": "#/responses/error"
apiOpts := web.GetForm(ctx).(*api.DeleteFileOptions)
- if !canWriteFiles(ctx.Repo) {
- ctx.Error(http.StatusForbidden, "DeleteFile", models.ErrUserDoesNotHaveAccessToRepo{
+ if !canWriteFiles(ctx, apiOpts.BranchName) {
+ ctx.Error(http.StatusForbidden, "DeleteFile", repo_model.ErrUserDoesNotHaveAccessToRepo{
UserID: ctx.Doer.ID,
RepoName: ctx.Repo.Repository.LowerName,
})
@@ -542,7 +762,7 @@ func GetContents(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
if !canReadFiles(ctx.Repo) {
- ctx.Error(http.StatusInternalServerError, "GetContentsOrList", models.ErrUserDoesNotHaveAccessToRepo{
+ ctx.Error(http.StatusInternalServerError, "GetContentsOrList", repo_model.ErrUserDoesNotHaveAccessToRepo{
UserID: ctx.Doer.ID,
RepoName: ctx.Repo.Repository.LowerName,
})
diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index 10c05e55033c8..e4c7eb70414ab 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -1,7 +1,6 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,16 +8,16 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -59,12 +58,12 @@ func ListForks(ctx *context.APIContext) {
}
apiForks := make([]*api.Repository, len(forks))
for i, fork := range forks {
- access, err := models.AccessLevel(ctx.Doer, fork)
+ access, err := access_model.AccessLevel(ctx, ctx.Doer, fork)
if err != nil {
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
return
}
- apiForks[i] = convert.ToRepo(fork, access)
+ apiForks[i] = convert.ToRepo(ctx, fork, access)
}
ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumForks))
@@ -142,7 +141,7 @@ func CreateFork(ctx *context.APIContext) {
Description: repo.Description,
})
if err != nil {
- if repo_model.IsErrRepoAlreadyExist(err) {
+ if repo_model.IsErrReachLimitOfRepo(err) || repo_model.IsErrRepoAlreadyExist(err) {
ctx.Error(http.StatusConflict, "ForkRepository", err)
} else {
ctx.Error(http.StatusInternalServerError, "ForkRepository", err)
@@ -151,5 +150,5 @@ func CreateFork(ctx *context.APIContext) {
}
// TODO change back to 201
- ctx.JSON(http.StatusAccepted, convert.ToRepo(fork, perm.AccessModeOwner))
+ ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, fork, perm.AccessModeOwner))
}
diff --git a/routers/api/v1/repo/git_hook.go b/routers/api/v1/repo/git_hook.go
index 0b4e09cadd2a5..40bd35542888f 100644
--- a/routers/api/v1/repo/git_hook.go
+++ b/routers/api/v1/repo/git_hook.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -8,10 +7,10 @@ import (
"net/http"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/convert"
)
// ListGitHooks list all Git hooks of a repository
diff --git a/routers/api/v1/repo/git_ref.go b/routers/api/v1/repo/git_ref.go
index 29b126db9a49a..34d2dcfcc8143 100644
--- a/routers/api/v1/repo/git_ref.go
+++ b/routers/api/v1/repo/git_ref.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -34,7 +33,7 @@ func GetGitAllRefs(ctx *context.APIContext) {
// required: true
// responses:
// "200":
- // "$ref": "#/responses/Reference"
+ // # "$ref": "#/responses/Reference" TODO: swagger doesnt support different output formats by ref
// "$ref": "#/responses/ReferenceList"
// "404":
// "$ref": "#/responses/notFound"
@@ -67,7 +66,7 @@ func GetGitRefs(ctx *context.APIContext) {
// required: true
// responses:
// "200":
- // "$ref": "#/responses/Reference"
+ // # "$ref": "#/responses/Reference" TODO: swagger doesnt support different output formats by ref
// "$ref": "#/responses/ReferenceList"
// "404":
// "$ref": "#/responses/notFound"
diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go
index c79a1d6b13522..100a28d7f6621 100644
--- a/routers/api/v1/repo/hook.go
+++ b/routers/api/v1/repo/hook.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
@@ -11,11 +10,13 @@ import (
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
webhook_service "code.gitea.io/gitea/services/webhook"
)
@@ -60,7 +61,7 @@ func ListHooks(ctx *context.APIContext) {
return
}
- hooks, err := webhook.ListWebhooksByOpts(opts)
+ hooks, err := webhook.ListWebhooksByOpts(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
@@ -68,7 +69,11 @@ func ListHooks(ctx *context.APIContext) {
apiHooks := make([]*api.Hook, len(hooks))
for i := range hooks {
- apiHooks[i] = convert.ToHook(ctx.Repo.RepoLink, hooks[i])
+ apiHooks[i], err = webhook_service.ToHook(ctx.Repo.RepoLink, hooks[i])
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
}
ctx.SetTotalCountHeader(count)
@@ -111,7 +116,12 @@ func GetHook(ctx *context.APIContext) {
if err != nil {
return
}
- ctx.JSON(http.StatusOK, convert.ToHook(repo.RepoLink, hook))
+ apiHook, err := webhook_service.ToHook(repo.RepoLink, hook)
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+ ctx.JSON(http.StatusOK, apiHook)
}
// TestHook tests a hook
@@ -138,6 +148,11 @@ func TestHook(ctx *context.APIContext) {
// type: integer
// format: int64
// required: true
+ // - name: ref
+ // in: query
+ // description: "The name of the commit/branch/tag, indicates which commit will be loaded to the webhook payload."
+ // type: string
+ // required: false
// responses:
// "204":
// "$ref": "#/responses/empty"
@@ -148,6 +163,11 @@ func TestHook(ctx *context.APIContext) {
return
}
+ ref := git.BranchPrefix + ctx.Repo.Repository.DefaultBranch
+ if r := ctx.FormTrim("ref"); r != "" {
+ ref = r
+ }
+
hookID := ctx.ParamsInt64(":id")
hook, err := utils.GetRepoHook(ctx, ctx.Repo.Repository.ID, hookID)
if err != nil {
@@ -156,15 +176,18 @@ func TestHook(ctx *context.APIContext) {
commit := convert.ToPayloadCommit(ctx.Repo.Repository, ctx.Repo.Commit)
- if err := webhook_service.PrepareWebhook(hook, ctx.Repo.Repository, webhook.HookEventPush, &api.PushPayload{
- Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch,
- Before: ctx.Repo.Commit.ID.String(),
- After: ctx.Repo.Commit.ID.String(),
- 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),
+ commitID := ctx.Repo.Commit.ID.String()
+ if err := webhook_service.PrepareWebhook(ctx, hook, webhook_module.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, 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 07f1532f82d6b..34dc990c3da49 100644
--- a/routers/api/v1/repo/hook_test.go
+++ b/routers/api/v1/repo/hook_test.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -28,7 +27,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/repo/issue.go b/routers/api/v1/repo/issue.go
index 083fe8f0b9473..fecb601dd533d 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -1,7 +1,6 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -12,14 +11,14 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/setting"
@@ -28,6 +27,7 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
@@ -129,7 +129,7 @@ func SearchIssues(ctx *context.APIContext) {
}
// find repos user can access (for issue search)
- opts := &models.SearchRepoOptions{
+ opts := &repo_model.SearchRepoOptions{
Private: false,
AllPublic: true,
TopicOnly: false,
@@ -144,7 +144,7 @@ func SearchIssues(ctx *context.APIContext) {
opts.AllLimited = true
}
if ctx.FormString("owner") != "" {
- owner, err := user_model.GetUserByName(ctx.FormString("owner"))
+ owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusBadRequest, "Owner not found", err)
@@ -163,7 +163,7 @@ func SearchIssues(ctx *context.APIContext) {
ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
return
}
- team, err := organization.GetTeam(opts.OwnerID, ctx.FormString("team"))
+ team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.Error(http.StatusBadRequest, "Team not found", err)
@@ -175,13 +175,14 @@ func SearchIssues(ctx *context.APIContext) {
opts.TeamID = team.ID
}
- repoIDs, _, err := models.SearchRepositoryIDs(opts)
+ repoCond := repo_model.SearchRepositoryCondition(opts)
+ repoIDs, _, err := repo_model.SearchRepositoryIDs(opts)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err)
+ ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err)
return
}
- var issues []*models.Issue
+ var issues []*issues_model.Issue
var filteredCount int64
keyword := ctx.FormTrim("q")
@@ -230,12 +231,12 @@ func SearchIssues(ctx *context.APIContext) {
// Only fetch the issues if we either don't have a keyword or the search returned issues
// This would otherwise return all issues if no issues were found by the search.
if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 {
- issuesOpt := &models.IssuesOptions{
+ issuesOpt := &issues_model.IssuesOptions{
ListOptions: db.ListOptions{
Page: ctx.FormInt("page"),
PageSize: limit,
},
- RepoIDs: repoIDs,
+ RepoCond: repoCond,
IsClosed: isClosed,
IssueIDs: issueIDs,
IncludedLabelNames: includedLabelNames,
@@ -266,7 +267,7 @@ func SearchIssues(ctx *context.APIContext) {
issuesOpt.ReviewRequestedID = ctxUserID
}
- if issues, err = models.Issues(issuesOpt); err != nil {
+ if issues, err = issues_model.Issues(ctx, issuesOpt); err != nil {
ctx.Error(http.StatusInternalServerError, "Issues", err)
return
}
@@ -274,15 +275,15 @@ func SearchIssues(ctx *context.APIContext) {
issuesOpt.ListOptions = db.ListOptions{
Page: -1,
}
- if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+ if filteredCount, err = issues_model.CountIssues(ctx, issuesOpt); err != nil {
ctx.Error(http.StatusInternalServerError, "CountIssues", err)
return
}
}
- ctx.SetLinkHeader(int(filteredCount), setting.UI.IssuePagingNum)
+ ctx.SetLinkHeader(int(filteredCount), limit)
ctx.SetTotalCountHeader(filteredCount)
- ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
+ ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
}
// ListIssues list the issues of a repository
@@ -376,7 +377,7 @@ func ListIssues(ctx *context.APIContext) {
isClosed = util.OptionalBoolFalse
}
- var issues []*models.Issue
+ var issues []*issues_model.Issue
var filteredCount int64
keyword := ctx.FormTrim("q")
@@ -394,7 +395,7 @@ func ListIssues(ctx *context.APIContext) {
}
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
- labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
+ labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
return
@@ -460,9 +461,9 @@ func ListIssues(ctx *context.APIContext) {
// Only fetch the issues if we either don't have a keyword or the search returned issues
// This would otherwise return all issues if no issues were found by the search.
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
- issuesOpt := &models.IssuesOptions{
+ issuesOpt := &issues_model.IssuesOptions{
ListOptions: listOptions,
- RepoIDs: []int64{ctx.Repo.Repository.ID},
+ RepoID: ctx.Repo.Repository.ID,
IsClosed: isClosed,
IssueIDs: issueIDs,
LabelIDs: labelIDs,
@@ -475,7 +476,7 @@ func ListIssues(ctx *context.APIContext) {
MentionedID: mentionedByID,
}
- if issues, err = models.Issues(issuesOpt); err != nil {
+ if issues, err = issues_model.Issues(ctx, issuesOpt); err != nil {
ctx.Error(http.StatusInternalServerError, "Issues", err)
return
}
@@ -483,7 +484,7 @@ func ListIssues(ctx *context.APIContext) {
issuesOpt.ListOptions = db.ListOptions{
Page: -1,
}
- if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+ if filteredCount, err = issues_model.CountIssues(ctx, issuesOpt); err != nil {
ctx.Error(http.StatusInternalServerError, "CountIssues", err)
return
}
@@ -491,7 +492,7 @@ func ListIssues(ctx *context.APIContext) {
ctx.SetLinkHeader(int(filteredCount), listOptions.PageSize)
ctx.SetTotalCountHeader(filteredCount)
- ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
+ ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
}
func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
@@ -500,7 +501,7 @@ func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
return 0
}
- user, err := user_model.GetUserByName(userName)
+ user, err := user_model.GetUserByName(ctx, userName)
if user_model.IsErrUserNotExist(err) {
ctx.NotFound(err)
return 0
@@ -544,16 +545,16 @@ func GetIssue(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
}
return
}
- ctx.JSON(http.StatusOK, convert.ToAPIIssue(issue))
+ ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
}
// CreateIssue create an issue of a repository
@@ -595,7 +596,7 @@ func CreateIssue(ctx *context.APIContext) {
deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
}
- issue := &models.Issue{
+ issue := &issues_model.Issue{
RepoID: ctx.Repo.Repository.ID,
Repo: ctx.Repo.Repository,
Title: form.Title,
@@ -610,7 +611,7 @@ func CreateIssue(ctx *context.APIContext) {
var err error
if ctx.Repo.CanWrite(unit.TypeIssues) {
issue.MilestoneID = form.Milestone
- assigneeIDs, err = models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
+ assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
@@ -622,19 +623,19 @@ func CreateIssue(ctx *context.APIContext) {
// Check if the passed assignees is assignable
for _, aID := range assigneeIDs {
- assignee, err := user_model.GetUserByID(aID)
+ assignee, err := user_model.GetUserByID(ctx, aID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserByID", err)
return
}
- valid, err := models.CanBeAssigned(assignee, ctx.Repo.Repository, false)
+ valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository, false)
if err != nil {
ctx.Error(http.StatusInternalServerError, "canBeAssigned", err)
return
}
if !valid {
- ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name})
+ ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name})
return
}
}
@@ -644,7 +645,7 @@ func CreateIssue(ctx *context.APIContext) {
}
if err := issue_service.NewIssue(ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
- if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+ if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
return
}
@@ -654,7 +655,7 @@ func CreateIssue(ctx *context.APIContext) {
if form.Closed {
if err := issue_service.ChangeStatus(issue, ctx.Doer, true); err != nil {
- if models.IsErrDependenciesLeft(err) {
+ if issues_model.IsErrDependenciesLeft(err) {
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
return
}
@@ -664,12 +665,12 @@ func CreateIssue(ctx *context.APIContext) {
}
// Refetch from database to assign some automatic values
- issue, err = models.GetIssueByID(issue.ID)
+ issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
return
}
- ctx.JSON(http.StatusCreated, convert.ToAPIIssue(issue))
+ ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, issue))
}
// EditIssue modify an issue of a repository
@@ -713,9 +714,9 @@ func EditIssue(ctx *context.APIContext) {
// "$ref": "#/responses/error"
form := web.GetForm(ctx).(*api.EditIssueOption)
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -725,7 +726,7 @@ func EditIssue(ctx *context.APIContext) {
issue.Repo = ctx.Repo.Repository
canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
- err = issue.LoadAttributes()
+ err = issue.LoadAttributes(ctx)
if err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
@@ -761,7 +762,7 @@ func EditIssue(ctx *context.APIContext) {
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
}
- if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
+ if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
return
}
@@ -810,9 +811,9 @@ func EditIssue(ctx *context.APIContext) {
}
issue.IsClosed = api.StateClosed == api.StateType(*form.State)
}
- statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.Doer)
+ statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(issue, ctx.Doer)
if err != nil {
- if models.IsErrDependenciesLeft(err) {
+ if issues_model.IsErrDependenciesLeft(err) {
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
return
}
@@ -821,24 +822,24 @@ func EditIssue(ctx *context.APIContext) {
}
if titleChanged {
- notification.NotifyIssueChangeTitle(ctx.Doer, issue, oldTitle)
+ notification.NotifyIssueChangeTitle(ctx, ctx.Doer, issue, oldTitle)
}
if statusChangeComment != nil {
- notification.NotifyIssueChangeStatus(ctx.Doer, issue, statusChangeComment, issue.IsClosed)
+ notification.NotifyIssueChangeStatus(ctx, ctx.Doer, issue, statusChangeComment, issue.IsClosed)
}
// Refetch from database to assign some automatic values
- issue, err = models.GetIssueByID(issue.ID)
+ issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil {
ctx.InternalServerError(err)
return
}
- if err = issue.LoadMilestone(); err != nil {
+ if err = issue.LoadMilestone(ctx); err != nil {
ctx.InternalServerError(err)
return
}
- ctx.JSON(http.StatusCreated, convert.ToAPIIssue(issue))
+ ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, issue))
}
func DeleteIssue(ctx *context.APIContext) {
@@ -869,9 +870,9 @@ func DeleteIssue(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
@@ -925,9 +926,9 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditDeadlineOption)
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -948,7 +949,7 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
}
- if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
+ if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
return
}
diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go
new file mode 100644
index 0000000000000..8cbd2e11b699c
--- /dev/null
+++ b/routers/api/v1/repo/issue_attachment.go
@@ -0,0 +1,372 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/attachment"
+ "code.gitea.io/gitea/services/convert"
+ issue_service "code.gitea.io/gitea/services/issue"
+)
+
+// GetIssueAttachment gets a single attachment of the issue
+func GetIssueAttachment(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueGetIssueAttachment
+ // ---
+ // summary: Get an issue attachment
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the issue
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to get
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/Attachment"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ issue := getIssueFromContext(ctx)
+ if issue == nil {
+ return
+ }
+
+ attach := getIssueAttachmentSafeRead(ctx, issue)
+ if attach == nil {
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToAttachment(attach))
+}
+
+// ListIssueAttachments lists all attachments of the issue
+func ListIssueAttachments(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets issue issueListIssueAttachments
+ // ---
+ // summary: List issue's attachments
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the issue
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/AttachmentList"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ issue := getIssueFromContext(ctx)
+ if issue == nil {
+ return
+ }
+
+ if err := issue.LoadAttributes(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue).Attachments)
+}
+
+// CreateIssueAttachment creates an attachment and saves the given file
+func CreateIssueAttachment(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/issues/{index}/assets issue issueCreateIssueAttachment
+ // ---
+ // summary: Create an issue attachment
+ // produces:
+ // - application/json
+ // consumes:
+ // - multipart/form-data
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the issue
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: name
+ // in: query
+ // description: name of the attachment
+ // type: string
+ // required: false
+ // - name: attachment
+ // in: formData
+ // description: attachment to upload
+ // type: file
+ // required: true
+ // responses:
+ // "201":
+ // "$ref": "#/responses/Attachment"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ issue := getIssueFromContext(ctx)
+ if issue == nil {
+ return
+ }
+
+ if !canUserWriteIssueAttachment(ctx, issue) {
+ return
+ }
+
+ // Get uploaded file from request
+ file, header, err := ctx.Req.FormFile("attachment")
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FormFile", err)
+ return
+ }
+ defer file.Close()
+
+ filename := header.Filename
+ if query := ctx.FormString("name"); query != "" {
+ filename = query
+ }
+
+ attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, &repo_model.Attachment{
+ Name: filename,
+ UploaderID: ctx.Doer.ID,
+ RepoID: ctx.Repo.Repository.ID,
+ IssueID: issue.ID,
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
+ return
+ }
+
+ issue.Attachments = append(issue.Attachments, attachment)
+
+ if err := issue_service.ChangeContent(issue, ctx.Doer, issue.Content); err != nil {
+ ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
+ return
+ }
+
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attachment))
+}
+
+// EditIssueAttachment updates the given attachment
+func EditIssueAttachment(ctx *context.APIContext) {
+ // swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueEditIssueAttachment
+ // ---
+ // summary: Edit an issue attachment
+ // produces:
+ // - application/json
+ // consumes:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the issue
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to edit
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/EditAttachmentOptions"
+ // responses:
+ // "201":
+ // "$ref": "#/responses/Attachment"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ attachment := getIssueAttachmentSafeWrite(ctx)
+ if attachment == nil {
+ return
+ }
+
+ // do changes to attachment. only meaningful change is name.
+ form := web.GetForm(ctx).(*api.EditAttachmentOptions)
+ if form.Name != "" {
+ attachment.Name = form.Name
+ }
+
+ if err := repo_model.UpdateAttachment(ctx, attachment); err != nil {
+ ctx.Error(http.StatusInternalServerError, "UpdateAttachment", err)
+ }
+
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attachment))
+}
+
+// DeleteIssueAttachment delete a given attachment
+func DeleteIssueAttachment(ctx *context.APIContext) {
+ // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueDeleteIssueAttachment
+ // ---
+ // summary: Delete an issue attachment
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the issue
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to delete
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ attachment := getIssueAttachmentSafeWrite(ctx)
+ if attachment == nil {
+ return
+ }
+
+ if err := repo_model.DeleteAttachment(attachment, true); err != nil {
+ ctx.Error(http.StatusInternalServerError, "DeleteAttachment", err)
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue {
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64("index"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
+ return nil
+ }
+
+ issue.Repo = ctx.Repo.Repository
+
+ return issue
+}
+
+func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
+ issue := getIssueFromContext(ctx)
+ if issue == nil {
+ return nil
+ }
+
+ if !canUserWriteIssueAttachment(ctx, issue) {
+ return nil
+ }
+
+ return getIssueAttachmentSafeRead(ctx, issue)
+}
+
+func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Issue) *repo_model.Attachment {
+ attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("asset"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err)
+ return nil
+ }
+ if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) {
+ return nil
+ }
+ return attachment
+}
+
+func canUserWriteIssueAttachment(ctx *context.APIContext, issue *issues_model.Issue) bool {
+ canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
+ if !canEditIssue {
+ ctx.Error(http.StatusForbidden, "", "user should have permission to write issue")
+ return false
+ }
+
+ return true
+}
+
+func attachmentBelongsToRepoOrIssue(ctx *context.APIContext, attachment *repo_model.Attachment, issue *issues_model.Issue) bool {
+ if attachment.RepoID != ctx.Repo.Repository.ID {
+ log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository)
+ ctx.NotFound("no such attachment in repo")
+ return false
+ }
+ if attachment.IssueID == 0 {
+ log.Debug("Requested attachment[%d] is not in an issue.", attachment.ID)
+ ctx.NotFound("no such attachment in issue")
+ return false
+ } else if issue != nil && attachment.IssueID != issue.ID {
+ log.Debug("Requested attachment[%d] does not belong to issue[%d, #%d].", attachment.ID, issue.ID, issue.Index)
+ ctx.NotFound("no such attachment in issue")
+ return false
+ }
+ return true
+}
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index ef91a2481c273..40c92998d19c4 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -1,23 +1,24 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
import (
+ stdCtx "context"
"errors"
"net/http"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
- comment_service "code.gitea.io/gitea/services/comments"
+ "code.gitea.io/gitea/services/convert"
+ issue_service "code.gitea.io/gitea/services/issue"
)
// ListIssueComments list all the comments of an issue
@@ -63,37 +64,42 @@ func ListIssueComments(ctx *context.APIContext) {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
return
}
issue.Repo = ctx.Repo.Repository
- opts := &models.FindCommentsOptions{
+ opts := &issues_model.FindCommentsOptions{
IssueID: issue.ID,
Since: since,
Before: before,
- Type: models.CommentTypeComment,
+ Type: issues_model.CommentTypeComment,
}
- comments, err := models.FindComments(opts)
+ comments, err := issues_model.FindComments(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindComments", err)
return
}
- totalCount, err := models.CountComments(opts)
+ totalCount, err := issues_model.CountComments(opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- if err := models.CommentList(comments).LoadPosters(); err != nil {
+ if err := issues_model.CommentList(comments).LoadPosters(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
return
}
+ if err := issues_model.CommentList(comments).LoadAttachments(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
+ return
+ }
+
apiComments := make([]*api.Comment, len(comments))
for i, comment := range comments {
comment.Issue = issue
@@ -155,37 +161,37 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
return
}
issue.Repo = ctx.Repo.Repository
- opts := &models.FindCommentsOptions{
+ opts := &issues_model.FindCommentsOptions{
ListOptions: utils.GetListOptions(ctx),
IssueID: issue.ID,
Since: since,
Before: before,
- Type: models.CommentTypeUnknown,
+ Type: issues_model.CommentTypeUnknown,
}
- comments, err := models.FindComments(opts)
+ comments, err := issues_model.FindComments(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindComments", err)
return
}
- if err := models.CommentList(comments).LoadPosters(); err != nil {
+ if err := issues_model.CommentList(comments).LoadPosters(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
return
}
var apiComments []*api.TimelineComment
for _, comment := range comments {
- if comment.Type != models.CommentTypeCode && isXRefCommentAccessible(ctx.Doer, comment, issue.RepoID) {
+ if comment.Type != issues_model.CommentTypeCode && isXRefCommentAccessible(ctx, ctx.Doer, comment, issue.RepoID) {
comment.Issue = issue
- apiComments = append(apiComments, convert.ToTimelineComment(comment, ctx.Doer))
+ apiComments = append(apiComments, convert.ToTimelineComment(ctx, comment, ctx.Doer))
}
}
@@ -193,16 +199,16 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, &apiComments)
}
-func isXRefCommentAccessible(user *user_model.User, c *models.Comment, issueRepoID int64) bool {
+func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *issues_model.Comment, issueRepoID int64) bool {
// Remove comments that the user has no permissions to see
- if models.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 {
+ if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 {
var err error
// Set RefRepo for description in template
- c.RefRepo, err = repo_model.GetRepositoryByID(c.RefRepoID)
+ c.RefRepo, err = repo_model.GetRepositoryByID(ctx, c.RefRepoID)
if err != nil {
return false
}
- perm, err := models.GetUserRepoPermission(c.RefRepo, user)
+ perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, user)
if err != nil {
return false
}
@@ -259,41 +265,45 @@ func ListRepoIssueComments(ctx *context.APIContext) {
return
}
- opts := &models.FindCommentsOptions{
+ opts := &issues_model.FindCommentsOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
- Type: models.CommentTypeComment,
+ Type: issues_model.CommentTypeComment,
Since: since,
Before: before,
}
- comments, err := models.FindComments(opts)
+ comments, err := issues_model.FindComments(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "FindComments", err)
return
}
- totalCount, err := models.CountComments(opts)
+ totalCount, err := issues_model.CountComments(opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- if err = models.CommentList(comments).LoadPosters(); err != nil {
+ if err = issues_model.CommentList(comments).LoadPosters(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
return
}
apiComments := make([]*api.Comment, len(comments))
- if err := models.CommentList(comments).LoadIssues(); err != nil {
+ if err := issues_model.CommentList(comments).LoadIssues(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
return
}
- if err := models.CommentList(comments).LoadPosters(); err != nil {
+ if err := issues_model.CommentList(comments).LoadPosters(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
return
}
- if _, err := models.CommentList(comments).Issues().LoadRepositories(); err != nil {
+ if err := issues_model.CommentList(comments).LoadAttachments(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
+ return
+ }
+ if _, err := issues_model.CommentList(comments).Issues().LoadRepositories(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadRepositories", err)
return
}
@@ -341,7 +351,7 @@ func CreateIssueComment(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
return
@@ -352,7 +362,7 @@ func CreateIssueComment(ctx *context.APIContext) {
return
}
- comment, err := comment_service.CreateIssueComment(ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
+ comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
return
@@ -397,9 +407,9 @@ func GetIssueComment(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrCommentNotExist(err) {
+ if issues_model.IsErrCommentNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
@@ -407,7 +417,7 @@ func GetIssueComment(ctx *context.APIContext) {
return
}
- if err = comment.LoadIssue(); err != nil {
+ if err = comment.LoadIssue(ctx); err != nil {
ctx.InternalServerError(err)
return
}
@@ -416,12 +426,12 @@ func GetIssueComment(ctx *context.APIContext) {
return
}
- if comment.Type != models.CommentTypeComment {
+ if comment.Type != issues_model.CommentTypeComment {
ctx.Status(http.StatusNoContent)
return
}
- if err := comment.LoadPoster(); err != nil {
+ if err := comment.LoadPoster(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "comment.LoadPoster", err)
return
}
@@ -524,9 +534,9 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) {
}
func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrCommentNotExist(err) {
+ if issues_model.IsErrCommentNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
@@ -539,14 +549,14 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
return
}
- if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeReview && comment.Type != models.CommentTypeCode {
+ if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeReview && comment.Type != issues_model.CommentTypeCode {
ctx.Status(http.StatusNoContent)
return
}
oldContent := comment.Content
comment.Content = form.Body
- if err := comment_service.UpdateComment(comment, ctx.Doer, oldContent); err != nil {
+ if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
return
}
@@ -627,9 +637,9 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
}
func deleteIssueComment(ctx *context.APIContext) {
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrCommentNotExist(err) {
+ if issues_model.IsErrCommentNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
@@ -640,12 +650,12 @@ func deleteIssueComment(ctx *context.APIContext) {
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
ctx.Status(http.StatusForbidden)
return
- } else if comment.Type != models.CommentTypeComment {
+ } else if comment.Type != issues_model.CommentTypeComment {
ctx.Status(http.StatusNoContent)
return
}
- if err = comment_service.DeleteComment(ctx.Doer, comment); err != nil {
+ if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err)
return
}
diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go
new file mode 100644
index 0000000000000..4c8452380f033
--- /dev/null
+++ b/routers/api/v1/repo/issue_comment_attachment.go
@@ -0,0 +1,383 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/attachment"
+ "code.gitea.io/gitea/services/convert"
+ issue_service "code.gitea.io/gitea/services/issue"
+)
+
+// GetIssueCommentAttachment gets a single attachment of the comment
+func GetIssueCommentAttachment(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueGetIssueCommentAttachment
+ // ---
+ // summary: Get a comment attachment
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the comment
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to get
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/Attachment"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ comment := getIssueCommentSafe(ctx)
+ if comment == nil {
+ return
+ }
+ attachment := getIssueCommentAttachmentSafeRead(ctx, comment)
+ if attachment == nil {
+ return
+ }
+ if attachment.CommentID != comment.ID {
+ log.Debug("User requested attachment[%d] is not in comment[%d].", attachment.ID, comment.ID)
+ ctx.NotFound("attachment not in comment")
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToAttachment(attachment))
+}
+
+// ListIssueCommentAttachments lists all attachments of the comment
+func ListIssueCommentAttachments(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueListIssueCommentAttachments
+ // ---
+ // summary: List comment's attachments
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the comment
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/AttachmentList"
+ // "404":
+ // "$ref": "#/responses/error"
+ comment := getIssueCommentSafe(ctx)
+ if comment == nil {
+ return
+ }
+
+ if err := comment.LoadAttachments(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToAttachments(comment.Attachments))
+}
+
+// CreateIssueCommentAttachment creates an attachment and saves the given file
+func CreateIssueCommentAttachment(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueCreateIssueCommentAttachment
+ // ---
+ // summary: Create a comment attachment
+ // produces:
+ // - application/json
+ // consumes:
+ // - multipart/form-data
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the comment
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: name
+ // in: query
+ // description: name of the attachment
+ // type: string
+ // required: false
+ // - name: attachment
+ // in: formData
+ // description: attachment to upload
+ // type: file
+ // required: true
+ // responses:
+ // "201":
+ // "$ref": "#/responses/Attachment"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ // Check if comment exists and load comment
+ comment := getIssueCommentSafe(ctx)
+ if comment == nil {
+ return
+ }
+
+ if !canUserWriteIssueCommentAttachment(ctx, comment) {
+ return
+ }
+
+ // Get uploaded file from request
+ file, header, err := ctx.Req.FormFile("attachment")
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FormFile", err)
+ return
+ }
+ defer file.Close()
+
+ filename := header.Filename
+ if query := ctx.FormString("name"); query != "" {
+ filename = query
+ }
+
+ attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, &repo_model.Attachment{
+ Name: filename,
+ UploaderID: ctx.Doer.ID,
+ RepoID: ctx.Repo.Repository.ID,
+ IssueID: comment.IssueID,
+ CommentID: comment.ID,
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
+ return
+ }
+ if err := comment.LoadAttachments(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
+ return
+ }
+
+ if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
+ ctx.ServerError("UpdateComment", err)
+ return
+ }
+
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attachment))
+}
+
+// EditIssueCommentAttachment updates the given attachment
+func EditIssueCommentAttachment(ctx *context.APIContext) {
+ // swagger:operation PATCH /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueEditIssueCommentAttachment
+ // ---
+ // summary: Edit a comment attachment
+ // produces:
+ // - application/json
+ // consumes:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the comment
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to edit
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/EditAttachmentOptions"
+ // responses:
+ // "201":
+ // "$ref": "#/responses/Attachment"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ attach := getIssueCommentAttachmentSafeWrite(ctx)
+ if attach == nil {
+ return
+ }
+
+ form := web.GetForm(ctx).(*api.EditAttachmentOptions)
+ if form.Name != "" {
+ attach.Name = form.Name
+ }
+
+ if err := repo_model.UpdateAttachment(ctx, attach); err != nil {
+ ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach)
+ }
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attach))
+}
+
+// DeleteIssueCommentAttachment delete a given attachment
+func DeleteIssueCommentAttachment(ctx *context.APIContext) {
+ // swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueDeleteIssueCommentAttachment
+ // ---
+ // summary: Delete a comment attachment
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: id
+ // in: path
+ // description: id of the comment
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: attachment_id
+ // in: path
+ // description: id of the attachment to delete
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/error"
+
+ attach := getIssueCommentAttachmentSafeWrite(ctx)
+ if attach == nil {
+ return
+ }
+
+ if err := repo_model.DeleteAttachment(attach, true); err != nil {
+ ctx.Error(http.StatusInternalServerError, "DeleteAttachment", err)
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
+
+func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
+ comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64("id"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
+ return nil
+ }
+ if err := comment.LoadIssue(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
+ return nil
+ }
+ if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID {
+ ctx.Error(http.StatusNotFound, "", "no matching issue comment found")
+ return nil
+ }
+
+ comment.Issue.Repo = ctx.Repo.Repository
+
+ return comment
+}
+
+func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
+ comment := getIssueCommentSafe(ctx)
+ if comment == nil {
+ return nil
+ }
+ if !canUserWriteIssueCommentAttachment(ctx, comment) {
+ return nil
+ }
+ return getIssueCommentAttachmentSafeRead(ctx, comment)
+}
+
+func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool {
+ canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)
+ if !canEditComment {
+ ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment")
+ return false
+ }
+
+ return true
+}
+
+func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment {
+ attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("asset"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err)
+ return nil
+ }
+ if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) {
+ return nil
+ }
+ return attachment
+}
+
+func attachmentBelongsToRepoOrComment(ctx *context.APIContext, attachment *repo_model.Attachment, comment *issues_model.Comment) bool {
+ if attachment.RepoID != ctx.Repo.Repository.ID {
+ log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository)
+ ctx.NotFound("no such attachment in repo")
+ return false
+ }
+ if attachment.IssueID == 0 || attachment.CommentID == 0 {
+ log.Debug("Requested attachment[%d] is not in a comment.", attachment.ID)
+ ctx.NotFound("no such attachment in comment")
+ return false
+ }
+ if comment != nil && attachment.CommentID != comment.ID {
+ log.Debug("Requested attachment[%d] does not belong to comment[%d].", attachment.ID, comment.ID)
+ ctx.NotFound("no such attachment in comment")
+ return false
+ }
+ return true
+}
diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go
index e314e756dda6b..1f0d21141bcce 100644
--- a/routers/api/v1/repo/issue_label.go
+++ b/routers/api/v1/repo/issue_label.go
@@ -1,18 +1,17 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
)
@@ -46,9 +45,9 @@ func ListIssueLabels(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -56,7 +55,7 @@ func ListIssueLabels(ctx *context.APIContext) {
return
}
- if err := issue.LoadAttributes(); err != nil {
+ if err := issue.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
@@ -111,7 +110,7 @@ func AddIssueLabels(ctx *context.APIContext) {
return
}
- labels, err = models.GetLabelsByIssueID(issue.ID)
+ labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsByIssueID", err)
return
@@ -158,9 +157,9 @@ func DeleteIssueLabel(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -173,9 +172,9 @@ func DeleteIssueLabel(ctx *context.APIContext) {
return
}
- label, err := models.GetLabelByID(ctx.ParamsInt64(":id"))
+ label, err := issues_model.GetLabelByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrLabelNotExist(err) {
+ if issues_model.IsErrLabelNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelByID", err)
@@ -237,7 +236,7 @@ func ReplaceIssueLabels(ctx *context.APIContext) {
return
}
- labels, err = models.GetLabelsByIssueID(issue.ID)
+ labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsByIssueID", err)
return
@@ -276,9 +275,9 @@ func ClearIssueLabels(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -299,10 +298,10 @@ func ClearIssueLabels(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
-func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (issue *models.Issue, labels []*models.Label, err error) {
- issue, err = models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (issue *issues_model.Issue, labels []*issues_model.Label, err error) {
+ issue, err = issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -310,7 +309,7 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
return
}
- labels, err = models.GetLabelsByIDs(form.Labels)
+ labels, err = issues_model.GetLabelsByIDs(form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsByIDs", err)
return
@@ -321,5 +320,5 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
return
}
- return
+ return issue, labels, err
}
diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go
index 5aa73667968ef..1b998a5354241 100644
--- a/routers/api/v1/repo/issue_reaction.go
+++ b/routers/api/v1/repo/issue_reaction.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -8,13 +7,12 @@ import (
"errors"
"net/http"
- "code.gitea.io/gitea/models"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// GetIssueCommentReactions list reactions of a comment from an issue
@@ -49,9 +47,9 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrCommentNotExist(err) {
+ if issues_model.IsErrCommentNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
@@ -59,7 +57,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
return
}
- if err := comment.LoadIssue(); err != nil {
+ if err := comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
}
@@ -176,9 +174,9 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) {
}
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrCommentNotExist(err) {
+ if issues_model.IsErrCommentNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
@@ -186,7 +184,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
return
}
- err = comment.LoadIssue()
+ err = comment.LoadIssue(ctx)
if err != nil {
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err)
}
@@ -271,9 +269,9 @@ func GetIssueReactions(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
- issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -391,9 +389,9 @@ func DeleteIssueReaction(ctx *context.APIContext) {
}
func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
- issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go
index 382f294346bb7..9ab3c61deb4fa 100644
--- a/routers/api/v1/repo/issue_stopwatch.go
+++ b/routers/api/v1/repo/issue_stopwatch.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -8,10 +7,10 @@ import (
"errors"
"net/http"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// StartIssueStopwatch creates a stopwatch for the given issue.
@@ -55,7 +54,7 @@ func StartIssueStopwatch(ctx *context.APIContext) {
return
}
- if err := models.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
+ if err := issues_model.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
ctx.Error(http.StatusInternalServerError, "CreateOrStopIssueStopwatch", err)
return
}
@@ -104,7 +103,7 @@ func StopIssueStopwatch(ctx *context.APIContext) {
return
}
- if err := models.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
+ if err := issues_model.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
ctx.Error(http.StatusInternalServerError, "CreateOrStopIssueStopwatch", err)
return
}
@@ -153,7 +152,7 @@ func DeleteIssueStopwatch(ctx *context.APIContext) {
return
}
- if err := models.CancelStopwatch(ctx.Doer, issue); err != nil {
+ if err := issues_model.CancelStopwatch(ctx.Doer, issue); err != nil {
ctx.Error(http.StatusInternalServerError, "CancelStopwatch", err)
return
}
@@ -161,10 +160,10 @@ func DeleteIssueStopwatch(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
-func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*models.Issue, error) {
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*issues_model.Issue, error) {
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -183,7 +182,7 @@ func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*models.I
return nil, errors.New("Cannot use time tracker")
}
- if models.StopwatchExists(ctx.Doer.ID, issue.ID) != shouldExist {
+ if issues_model.StopwatchExists(ctx.Doer.ID, issue.ID) != shouldExist {
if shouldExist {
ctx.Error(http.StatusConflict, "StopwatchExists", "cannot stop/cancel a non existent stopwatch")
err = errors.New("cannot stop/cancel a non existent stopwatch")
@@ -219,13 +218,13 @@ func GetStopwatches(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/StopWatchList"
- sws, err := models.GetUserStopwatches(ctx.Doer.ID, utils.GetListOptions(ctx))
+ sws, err := issues_model.GetUserStopwatches(ctx.Doer.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserStopwatches", err)
return
}
- count, err := models.CountUserStopwatches(ctx.Doer.ID)
+ count, err := issues_model.CountUserStopwatches(ctx.Doer.ID)
if err != nil {
ctx.InternalServerError(err)
return
diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go
index f00c85b12622c..6d22c82652ef0 100644
--- a/routers/api/v1/repo/issue_subscription.go
+++ b/routers/api/v1/repo/issue_subscription.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -8,12 +7,12 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// AddIssueSubscription Subscribe user to issue
@@ -105,9 +104,9 @@ func DelIssueSubscription(ctx *context.APIContext) {
}
func setIssueSubscription(ctx *context.APIContext, watch bool) {
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -116,7 +115,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
return
}
- user, err := user_model.GetUserByName(ctx.Params(":user"))
+ user, err := user_model.GetUserByName(ctx, ctx.Params(":user"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound()
@@ -133,7 +132,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
return
}
- current, err := models.CheckIssueWatch(user, issue)
+ current, err := issues_model.CheckIssueWatch(user, issue)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CheckIssueWatch", err)
return
@@ -146,7 +145,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
}
// Update watch state
- if err := models.CreateOrUpdateIssueWatch(user.ID, issue.ID, watch); err != nil {
+ if err := issues_model.CreateOrUpdateIssueWatch(user.ID, issue.ID, watch); err != nil {
ctx.Error(http.StatusInternalServerError, "CreateOrUpdateIssueWatch", err)
return
}
@@ -186,9 +185,9 @@ func CheckIssueSubscription(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -197,7 +196,7 @@ func CheckIssueSubscription(ctx *context.APIContext) {
return
}
- watching, err := models.CheckIssueWatch(ctx.Doer, issue)
+ watching, err := issues_model.CheckIssueWatch(ctx.Doer, issue)
if err != nil {
ctx.InternalServerError(err)
return
@@ -252,9 +251,9 @@ func GetIssueSubscribers(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -263,7 +262,7 @@ func GetIssueSubscribers(ctx *context.APIContext) {
return
}
- iwl, err := models.GetIssueWatchers(issue.ID, utils.GetListOptions(ctx))
+ iwl, err := issues_model.GetIssueWatchers(ctx, issue.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetIssueWatchers", err)
return
@@ -284,7 +283,7 @@ func GetIssueSubscribers(ctx *context.APIContext) {
apiUsers = append(apiUsers, convert.ToUser(v, ctx.Doer))
}
- count, err := models.CountIssueWatchers(issue.ID)
+ count, err := issues_model.CountIssueWatchers(ctx, issue.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CountIssueWatchers", err)
return
diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go
index e42dc60a94c81..16bb8cb73d6c3 100644
--- a/routers/api/v1/repo/issue_tracked_time.go
+++ b/routers/api/v1/repo/issue_tracked_time.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,14 +8,15 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// ListTrackedTimes list all the tracked times of an issue
@@ -71,13 +71,13 @@ func ListTrackedTimes(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- if !ctx.Repo.Repository.IsTimetrackerEnabled() {
+ if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.NotFound("Timetracker is disabled")
return
}
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -85,7 +85,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
return
}
- opts := &models.FindTrackedTimesOptions{
+ opts := &issues_model.FindTrackedTimesOptions{
ListOptions: utils.GetListOptions(ctx),
RepositoryID: ctx.Repo.Repository.ID,
IssueID: issue.ID,
@@ -93,7 +93,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
qUser := ctx.FormTrim("user")
if qUser != "" {
- user, err := user_model.GetUserByName(qUser)
+ user, err := user_model.GetUserByName(ctx, qUser)
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusNotFound, "User does not exist", err)
} else if err != nil {
@@ -121,13 +121,13 @@ func ListTrackedTimes(ctx *context.APIContext) {
}
}
- count, err := models.CountTrackedTimes(opts)
+ count, err := issues_model.CountTrackedTimes(opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- trackedTimes, err := models.GetTrackedTimes(opts)
+ trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err)
return
@@ -138,7 +138,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
}
ctx.SetTotalCountHeader(count)
- ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(trackedTimes))
+ ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
}
// AddTime add time manual to the given issue
@@ -179,9 +179,9 @@ func AddTime(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
form := web.GetForm(ctx).(*api.AddTimeOption)
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -190,7 +190,7 @@ func AddTime(ctx *context.APIContext) {
}
if !ctx.Repo.CanUseTimetracker(issue, ctx.Doer) {
- if !ctx.Repo.Repository.IsTimetrackerEnabled() {
+ if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.Error(http.StatusBadRequest, "", "time tracking disabled")
return
}
@@ -202,7 +202,7 @@ func AddTime(ctx *context.APIContext) {
if form.User != "" {
if (ctx.IsUserRepoAdmin() && ctx.Doer.Name != form.User) || ctx.Doer.IsAdmin {
// allow only RepoAdmin, Admin and User to add time
- user, err = user_model.GetUserByName(form.User)
+ user, err = user_model.GetUserByName(ctx, form.User)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
}
@@ -214,7 +214,7 @@ func AddTime(ctx *context.APIContext) {
created = form.Created
}
- trackedTime, err := models.AddTime(user, issue, form.Time, created)
+ trackedTime, err := issues_model.AddTime(user, issue, form.Time, created)
if err != nil {
ctx.Error(http.StatusInternalServerError, "AddTime", err)
return
@@ -223,7 +223,7 @@ func AddTime(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
- ctx.JSON(http.StatusOK, convert.ToTrackedTime(trackedTime))
+ ctx.JSON(http.StatusOK, convert.ToTrackedTime(ctx, trackedTime))
}
// ResetIssueTime reset time manual to the given issue
@@ -260,9 +260,9 @@ func ResetIssueTime(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -271,7 +271,7 @@ func ResetIssueTime(ctx *context.APIContext) {
}
if !ctx.Repo.CanUseTimetracker(issue, ctx.Doer) {
- if !ctx.Repo.Repository.IsTimetrackerEnabled() {
+ if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
return
}
@@ -279,9 +279,9 @@ func ResetIssueTime(ctx *context.APIContext) {
return
}
- err = models.DeleteIssueUserTimes(issue, ctx.Doer)
+ err = issues_model.DeleteIssueUserTimes(issue, ctx.Doer)
if err != nil {
- if models.IsErrNotExist(err) {
+ if db.IsErrNotExist(err) {
ctx.Error(http.StatusNotFound, "DeleteIssueUserTimes", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteIssueUserTimes", err)
@@ -331,9 +331,9 @@ func DeleteTime(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
@@ -342,7 +342,7 @@ func DeleteTime(ctx *context.APIContext) {
}
if !ctx.Repo.CanUseTimetracker(issue, ctx.Doer) {
- if !ctx.Repo.Repository.IsTimetrackerEnabled() {
+ if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
return
}
@@ -350,9 +350,9 @@ func DeleteTime(ctx *context.APIContext) {
return
}
- time, err := models.GetTrackedTimeByID(ctx.ParamsInt64(":id"))
+ time, err := issues_model.GetTrackedTimeByID(ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrNotExist(err) {
+ if db.IsErrNotExist(err) {
ctx.NotFound(err)
return
}
@@ -370,7 +370,7 @@ func DeleteTime(ctx *context.APIContext) {
return
}
- err = models.DeleteTime(time)
+ err = issues_model.DeleteTime(time)
if err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteTime", err)
return
@@ -410,11 +410,11 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
- if !ctx.Repo.Repository.IsTimetrackerEnabled() {
+ if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.Error(http.StatusBadRequest, "", "time tracking disabled")
return
}
- user, err := user_model.GetUserByName(ctx.Params(":timetrackingusername"))
+ user, err := user_model.GetUserByName(ctx, ctx.Params(":timetrackingusername"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound(err)
@@ -433,12 +433,12 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
return
}
- opts := &models.FindTrackedTimesOptions{
+ opts := &issues_model.FindTrackedTimesOptions{
UserID: user.ID,
RepositoryID: ctx.Repo.Repository.ID,
}
- trackedTimes, err := models.GetTrackedTimes(opts)
+ trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err)
return
@@ -447,7 +447,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
- ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(trackedTimes))
+ ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
}
// ListTrackedTimesByRepository lists all tracked times of the repository
@@ -498,12 +498,12 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
- if !ctx.Repo.Repository.IsTimetrackerEnabled() {
+ if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.Error(http.StatusBadRequest, "", "time tracking disabled")
return
}
- opts := &models.FindTrackedTimesOptions{
+ opts := &issues_model.FindTrackedTimesOptions{
ListOptions: utils.GetListOptions(ctx),
RepositoryID: ctx.Repo.Repository.ID,
}
@@ -511,7 +511,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
// Filters
qUser := ctx.FormTrim("user")
if qUser != "" {
- user, err := user_model.GetUserByName(qUser)
+ user, err := user_model.GetUserByName(ctx, qUser)
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusNotFound, "User does not exist", err)
} else if err != nil {
@@ -540,13 +540,13 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
}
}
- count, err := models.CountTrackedTimes(opts)
+ count, err := issues_model.CountTrackedTimes(opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- trackedTimes, err := models.GetTrackedTimes(opts)
+ trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err)
return
@@ -557,7 +557,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
}
ctx.SetTotalCountHeader(count)
- ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(trackedTimes))
+ ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
}
// ListMyTrackedTimes lists all tracked times of the current user
@@ -565,6 +565,8 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
// swagger:operation GET /user/times user userCurrentTrackedTimes
// ---
// summary: List the current user's tracked times
+ // produces:
+ // - application/json
// parameters:
// - name: page
// in: query
@@ -574,9 +576,6 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
// in: query
// description: page size of results
// type: integer
- // produces:
- // - application/json
- // parameters:
// - name: since
// in: query
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
@@ -591,7 +590,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/TrackedTimeList"
- opts := &models.FindTrackedTimesOptions{
+ opts := &issues_model.FindTrackedTimesOptions{
ListOptions: utils.GetListOptions(ctx),
UserID: ctx.Doer.ID,
}
@@ -602,13 +601,13 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
return
}
- count, err := models.CountTrackedTimes(opts)
+ count, err := issues_model.CountTrackedTimes(opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- trackedTimes, err := models.GetTrackedTimes(opts)
+ trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTrackedTimesByUser", err)
return
@@ -620,5 +619,5 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
}
ctx.SetTotalCountHeader(count)
- ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(trackedTimes))
+ ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, trackedTimes))
}
diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go
index 0c780eb97d0c6..d496c4a73c11c 100644
--- a/routers/api/v1/repo/key.go
+++ b/routers/api/v1/repo/key.go
@@ -1,11 +1,11 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
import (
+ stdCtx "context"
"fmt"
"net/http"
"net/url"
@@ -15,25 +15,25 @@ import (
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/convert"
)
// appendPrivateInformation appends the owner and key type information to api.PublicKey
-func appendPrivateInformation(apiKey *api.DeployKey, key *asymkey_model.DeployKey, repository *repo_model.Repository) (*api.DeployKey, error) {
+func appendPrivateInformation(ctx stdCtx.Context, apiKey *api.DeployKey, key *asymkey_model.DeployKey, repository *repo_model.Repository) (*api.DeployKey, error) {
apiKey.ReadOnly = key.Mode == perm.AccessModeRead
if repository.ID == key.RepoID {
- apiKey.Repository = convert.ToRepo(repository, key.Mode)
+ apiKey.Repository = convert.ToRepo(ctx, repository, key.Mode)
} else {
- repo, err := repo_model.GetRepositoryByID(key.RepoID)
+ repo, err := repo_model.GetRepositoryByID(ctx, key.RepoID)
if err != nil {
return apiKey, err
}
- apiKey.Repository = convert.ToRepo(repo, key.Mode)
+ apiKey.Repository = convert.ToRepo(ctx, repo, key.Mode)
}
return apiKey, nil
}
@@ -108,7 +108,7 @@ func ListDeployKeys(ctx *context.APIContext) {
}
apiKeys[i] = convert.ToDeployKey(apiLink, keys[i])
if ctx.Doer.IsAdmin || ((ctx.Repo.Repository.ID == keys[i].RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
- apiKeys[i], _ = appendPrivateInformation(apiKeys[i], keys[i], ctx.Repo.Repository)
+ apiKeys[i], _ = appendPrivateInformation(ctx, apiKeys[i], keys[i], ctx.Repo.Repository)
}
}
@@ -162,7 +162,7 @@ func GetDeployKey(ctx *context.APIContext) {
apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
apiKey := convert.ToDeployKey(apiLink, key)
if ctx.Doer.IsAdmin || ((ctx.Repo.Repository.ID == key.RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
- apiKey, _ = appendPrivateInformation(apiKey, key, ctx.Repo.Repository)
+ apiKey, _ = appendPrivateInformation(ctx, apiKey, key, ctx.Repo.Repository)
}
ctx.JSON(http.StatusOK, apiKey)
}
@@ -174,7 +174,7 @@ func HandleCheckKeyStringError(ctx *context.APIContext, err error) {
} else if asymkey_model.IsErrKeyUnableVerify(err) {
ctx.Error(http.StatusUnprocessableEntity, "", "Unable to verify key content")
} else {
- ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid key content: %v", err))
+ ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid key content: %w", err))
}
}
diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go
index ab559a2eed3cf..411c0274e6c78 100644
--- a/routers/api/v1/repo/label.go
+++ b/routers/api/v1/repo/label.go
@@ -1,7 +1,6 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -11,12 +10,12 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// ListLabels list all the labels of a repository
@@ -49,13 +48,13 @@ func ListLabels(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/LabelList"
- labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, ctx.FormString("sort"), utils.GetListOptions(ctx))
+ labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsByRepoID", err)
return
}
- count, err := models.CountLabelsByRepoID(ctx.Repo.Repository.ID)
+ count, err := issues_model.CountLabelsByRepoID(ctx.Repo.Repository.ID)
if err != nil {
ctx.InternalServerError(err)
return
@@ -94,17 +93,17 @@ func GetLabel(ctx *context.APIContext) {
// "$ref": "#/responses/Label"
var (
- label *models.Label
+ label *issues_model.Label
err error
)
strID := ctx.Params(":id")
if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil {
- label, err = models.GetLabelInRepoByName(ctx.Repo.Repository.ID, strID)
+ label, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID)
} else {
- label, err = models.GetLabelInRepoByID(ctx.Repo.Repository.ID, intID)
+ label, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID)
}
if err != nil {
- if models.IsErrRepoLabelNotExist(err) {
+ if issues_model.IsErrRepoLabelNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)
@@ -150,18 +149,18 @@ func CreateLabel(ctx *context.APIContext) {
if len(form.Color) == 6 {
form.Color = "#" + form.Color
}
- if !models.LabelColorPattern.MatchString(form.Color) {
+ if !issues_model.LabelColorPattern.MatchString(form.Color) {
ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", form.Color))
return
}
- label := &models.Label{
+ label := &issues_model.Label{
Name: form.Name,
Color: form.Color,
RepoID: ctx.Repo.Repository.ID,
Description: form.Description,
}
- if err := models.NewLabel(ctx, label); err != nil {
+ if err := issues_model.NewLabel(ctx, label); err != nil {
ctx.Error(http.StatusInternalServerError, "NewLabel", err)
return
}
@@ -206,9 +205,9 @@ func EditLabel(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditLabelOption)
- label, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
+ label, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrRepoLabelNotExist(err) {
+ if issues_model.IsErrRepoLabelNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err)
@@ -224,7 +223,7 @@ func EditLabel(ctx *context.APIContext) {
if len(label.Color) == 6 {
label.Color = "#" + label.Color
}
- if !models.LabelColorPattern.MatchString(label.Color) {
+ if !issues_model.LabelColorPattern.MatchString(label.Color) {
ctx.Error(http.StatusUnprocessableEntity, "ColorPattern", fmt.Errorf("bad color code: %s", label.Color))
return
}
@@ -232,7 +231,7 @@ func EditLabel(ctx *context.APIContext) {
if form.Description != nil {
label.Description = *form.Description
}
- if err := models.UpdateLabel(label); err != nil {
+ if err := issues_model.UpdateLabel(label); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateLabel", err)
return
}
@@ -266,7 +265,7 @@ func DeleteLabel(ctx *context.APIContext) {
// "204":
// "$ref": "#/responses/empty"
- if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")); err != nil {
+ if err := issues_model.DeleteLabel(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteLabel", err)
return
}
diff --git a/routers/api/v1/repo/language.go b/routers/api/v1/repo/language.go
index 427a8fd6b5448..12f1761ad08c1 100644
--- a/routers/api/v1/repo/language.go
+++ b/routers/api/v1/repo/language.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -68,7 +67,7 @@ func GetLanguages(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/LanguageStatistics"
- langs, err := repo_model.GetLanguageStats(ctx.Repo.Repository)
+ langs, err := repo_model.GetLanguageStats(ctx, ctx.Repo.Repository)
if err != nil {
log.Error("GetLanguageStats failed: %v", err)
ctx.InternalServerError(err)
@@ -76,9 +75,7 @@ func GetLanguages(ctx *context.APIContext) {
}
resp := make(languageResponse, len(langs))
- for i, v := range langs {
- resp[i] = v
- }
+ copy(resp, langs)
ctx.JSON(http.StatusOK, resp)
}
diff --git a/routers/api/v1/repo/main_test.go b/routers/api/v1/repo/main_test.go
index 19e524d014f76..543fb922d6ba2 100644
--- a/routers/api/v1/repo/main_test.go
+++ b/routers/api/v1/repo/main_test.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,10 +8,15 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/setting"
+ webhook_service "code.gitea.io/gitea/services/webhook"
)
func TestMain(m *testing.M) {
+ setting.LoadForTest()
+ setting.NewQueueService()
unittest.MainTest(m, &unittest.TestOptions{
GiteaRootPath: filepath.Join("..", "..", "..", ".."),
+ SetUp: webhook_service.Init,
})
}
diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go
index f5851bfcae666..09e857b5fcfe8 100644
--- a/routers/api/v1/repo/migrate.go
+++ b/routers/api/v1/repo/migrate.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -18,7 +17,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
@@ -29,6 +27,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/migrations"
)
@@ -65,9 +64,9 @@ func Migrate(ctx *context.APIContext) {
err error
)
if len(form.RepoOwner) != 0 {
- repoOwner, err = user_model.GetUserByName(form.RepoOwner)
+ repoOwner, err = user_model.GetUserByName(ctx, form.RepoOwner)
} else if form.RepoOwnerID != 0 {
- repoOwner, err = user_model.GetUserByID(form.RepoOwnerID)
+ repoOwner, err = user_model.GetUserByID(ctx, form.RepoOwnerID)
} else {
repoOwner = ctx.Doer
}
@@ -170,7 +169,7 @@ func Migrate(ctx *context.APIContext) {
opts.Releases = false
}
- repo, err := repo_module.CreateRepository(ctx.Doer, repoOwner, models.CreateRepoOptions{
+ repo, err := repo_module.CreateRepository(ctx.Doer, repoOwner, repo_module.CreateRepoOptions{
Name: opts.RepoName,
Description: opts.Description,
OriginalURL: form.CloneAddr,
@@ -195,7 +194,7 @@ func Migrate(ctx *context.APIContext) {
}
if err == nil {
- notification.NotifyMigrateRepository(ctx.Doer, repoOwner, repo)
+ notification.NotifyMigrateRepository(ctx, ctx.Doer, repoOwner, repo)
return
}
@@ -212,7 +211,7 @@ func Migrate(ctx *context.APIContext) {
}
log.Trace("Repository migrated: %s/%s", repoOwner.Name, form.RepoName)
- ctx.JSON(http.StatusCreated, convert.ToRepo(repo, perm.AccessModeAdmin))
+ ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, perm.AccessModeAdmin))
}
func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, remoteAddr string, err error) {
diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go
index ce6aa7f46f10b..b77fe8aca8a94 100644
--- a/routers/api/v1/repo/milestone.go
+++ b/routers/api/v1/repo/milestone.go
@@ -1,7 +1,6 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
@@ -12,11 +11,11 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// ListMilestones list milestones for a repository
@@ -39,7 +38,7 @@ func ListMilestones(ctx *context.APIContext) {
// required: true
// - name: state
// in: query
- // description: Milestone state, Recognised values are open, closed and all. Defaults to "open"
+ // description: Milestone state, Recognized values are open, closed and all. Defaults to "open"
// type: string
// - name: name
// in: query
diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go
index d7facd24d96cc..06bfabe3d2d29 100644
--- a/routers/api/v1/repo/mirror.go
+++ b/routers/api/v1/repo/mirror.go
@@ -1,17 +1,28 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
import (
"errors"
+ "fmt"
"net/http"
+ "time"
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/context"
+ mirror_module "code.gitea.io/gitea/modules/mirror"
"code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/migrations"
mirror_service "code.gitea.io/gitea/services/mirror"
)
@@ -50,7 +61,7 @@ func MirrorSync(ctx *context.APIContext) {
return
}
- if _, err := repo_model.GetMirrorByRepoID(repo.ID); err != nil {
+ if _, err := repo_model.GetMirrorByRepoID(ctx, repo.ID); err != nil {
if errors.Is(err, repo_model.ErrMirrorNotExist) {
ctx.Error(http.StatusBadRequest, "MirrorSync", "Repository is not a mirror")
return
@@ -59,7 +70,322 @@ func MirrorSync(ctx *context.APIContext) {
return
}
- mirror_service.StartToMirror(repo.ID)
+ mirror_module.AddPullMirrorToQueue(repo.ID)
ctx.Status(http.StatusOK)
}
+
+// PushMirrorSync adds all push mirrored repositories to the sync queue
+func PushMirrorSync(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/push_mirrors-sync repository repoPushMirrorSync
+ // ---
+ // summary: Sync all push mirrored repository
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo to sync
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo to sync
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/empty"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ if !setting.Mirror.Enabled {
+ ctx.Error(http.StatusBadRequest, "PushMirrorSync", "Mirror feature is disabled")
+ return
+ }
+ // Get All push mirrors of a specific repo
+ pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
+ if err != nil {
+ ctx.Error(http.StatusNotFound, "PushMirrorSync", err)
+ return
+ }
+ for _, mirror := range pushMirrors {
+ ok := mirror_service.SyncPushMirror(ctx, mirror.ID)
+ if !ok {
+ ctx.Error(http.StatusInternalServerError, "PushMirrorSync", "error occurred when syncing push mirror "+mirror.RemoteName)
+ return
+ }
+ }
+
+ ctx.Status(http.StatusOK)
+}
+
+// ListPushMirrors get list of push mirrors of a repository
+func ListPushMirrors(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/push_mirrors repository repoListPushMirrors
+ // ---
+ // summary: Get all push mirrors of the repository
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/PushMirrorList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ if !setting.Mirror.Enabled {
+ ctx.Error(http.StatusBadRequest, "GetPushMirrorsByRepoID", "Mirror feature is disabled")
+ return
+ }
+
+ repo := ctx.Repo.Repository
+ // Get all push mirrors for the specified repository.
+ pushMirrors, count, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, utils.GetListOptions(ctx))
+ if err != nil {
+ ctx.Error(http.StatusNotFound, "GetPushMirrorsByRepoID", err)
+ return
+ }
+
+ responsePushMirrors := make([]*api.PushMirror, 0, len(pushMirrors))
+ for _, mirror := range pushMirrors {
+ m, err := convert.ToPushMirror(mirror)
+ if err == nil {
+ responsePushMirrors = append(responsePushMirrors, m)
+ }
+
+ }
+ ctx.SetLinkHeader(len(responsePushMirrors), utils.GetListOptions(ctx).PageSize)
+ ctx.SetTotalCountHeader(count)
+ ctx.JSON(http.StatusOK, responsePushMirrors)
+}
+
+// GetPushMirrorByName get push mirror of a repository by name
+func GetPushMirrorByName(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/push_mirrors/{name} repository repoGetPushMirrorByRemoteName
+ // ---
+ // summary: Get push mirror of the repository by remoteName
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: name
+ // in: path
+ // description: remote name of push mirror
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/PushMirror"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ if !setting.Mirror.Enabled {
+ ctx.Error(http.StatusBadRequest, "GetPushMirrorByRemoteName", "Mirror feature is disabled")
+ return
+ }
+
+ mirrorName := ctx.Params(":name")
+ // Get push mirror of a specific repo by remoteName
+ pushMirror, err := repo_model.GetPushMirror(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: mirrorName})
+ if err != nil {
+ ctx.Error(http.StatusNotFound, "GetPushMirrors", err)
+ return
+ }
+ m, err := convert.ToPushMirror(pushMirror)
+ if err != nil {
+ ctx.ServerError("GetPushMirrorByRemoteName", err)
+ return
+ }
+ ctx.JSON(http.StatusOK, m)
+}
+
+// AddPushMirror adds a push mirror to a repository
+func AddPushMirror(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/push_mirrors repository repoAddPushMirror
+ // ---
+ // summary: add a push mirror to the repository
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/CreatePushMirrorOption"
+ // responses:
+ // "201":
+ // "$ref": "#/responses/PushMirror"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "400":
+ // "$ref": "#/responses/error"
+
+ if !setting.Mirror.Enabled {
+ ctx.Error(http.StatusBadRequest, "AddPushMirror", "Mirror feature is disabled")
+ return
+ }
+
+ pushMirror := web.GetForm(ctx).(*api.CreatePushMirrorOption)
+ CreatePushMirror(ctx, pushMirror)
+}
+
+// DeletePushMirrorByRemoteName deletes a push mirror from a repository by remoteName
+func DeletePushMirrorByRemoteName(ctx *context.APIContext) {
+ // swagger:operation DELETE /repos/{owner}/{repo}/push_mirrors/{name} repository repoDeletePushMirror
+ // ---
+ // summary: deletes a push mirror from a repository by remoteName
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: name
+ // in: path
+ // description: remote name of the pushMirror
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "400":
+ // "$ref": "#/responses/error"
+
+ if !setting.Mirror.Enabled {
+ ctx.Error(http.StatusBadRequest, "DeletePushMirrorByName", "Mirror feature is disabled")
+ return
+ }
+
+ remoteName := ctx.Params(":name")
+ // Delete push mirror on repo by name.
+ err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName})
+ if err != nil {
+ ctx.Error(http.StatusNotFound, "DeletePushMirrors", err)
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
+
+func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirrorOption) {
+ repo := ctx.Repo.Repository
+
+ interval, err := time.ParseDuration(mirrorOption.Interval)
+ if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+ ctx.Error(http.StatusBadRequest, "CreatePushMirror", err)
+ return
+ }
+
+ address, err := forms.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword)
+ if err == nil {
+ err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser)
+ }
+ if err != nil {
+ HandleRemoteAddressError(ctx, err)
+ return
+ }
+
+ remoteSuffix, err := util.CryptoRandomString(10)
+ if err != nil {
+ ctx.ServerError("CryptoRandomString", err)
+ return
+ }
+
+ pushMirror := &repo_model.PushMirror{
+ RepoID: repo.ID,
+ Repo: repo,
+ RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix),
+ Interval: interval,
+ SyncOnCommit: mirrorOption.SyncOnCommit,
+ }
+
+ if err = repo_model.InsertPushMirror(ctx, pushMirror); err != nil {
+ ctx.ServerError("InsertPushMirror", err)
+ return
+ }
+
+ // if the registration of the push mirrorOption fails remove it from the database
+ if err = mirror_service.AddPushMirrorRemote(ctx, pushMirror, address); err != nil {
+ if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: pushMirror.ID, RepoID: pushMirror.RepoID}); err != nil {
+ ctx.ServerError("DeletePushMirrors", err)
+ }
+ ctx.ServerError("AddPushMirrorRemote", err)
+ return
+ }
+ m, err := convert.ToPushMirror(pushMirror)
+ if err != nil {
+ ctx.ServerError("ToPushMirror", err)
+ return
+ }
+ ctx.JSON(http.StatusOK, m)
+}
+
+func HandleRemoteAddressError(ctx *context.APIContext, err error) {
+ if models.IsErrInvalidCloneAddr(err) {
+ addrErr := err.(*models.ErrInvalidCloneAddr)
+ switch {
+ case addrErr.IsProtocolInvalid:
+ ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid mirror protocol")
+ case addrErr.IsURLError:
+ ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid Url ")
+ case addrErr.IsPermissionDenied:
+ ctx.Error(http.StatusUnauthorized, "CreatePushMirror", "Permission denied")
+ default:
+ ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Unknown error")
+ }
+ return
+ }
+}
diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go
index f85883566f012..8eaa503ff7cc5 100644
--- a/routers/api/v1/repo/notes.go
+++ b/routers/api/v1/repo/notes.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,10 +8,9 @@ import (
"net/http"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/validation"
+ "code.gitea.io/gitea/services/convert"
)
// GetNote Get a note corresponding to a single commit from a repository
@@ -47,7 +45,7 @@ func GetNote(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
sha := ctx.Params(":sha")
- if (validation.GitRefNamePatternInvalid.MatchString(sha) || !validation.CheckGitRefAdditionalRulesValid(sha)) && !git.SHAPattern.MatchString(sha) {
+ if !git.IsValidRefPattern(sha) {
ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha))
return
}
@@ -55,15 +53,13 @@ func GetNote(ctx *context.APIContext) {
}
func getNote(ctx *context.APIContext, identifier string) {
- gitRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
+ if ctx.Repo.GitRepo == nil {
+ ctx.InternalServerError(fmt.Errorf("no open git repo"))
return
}
- defer gitRepo.Close()
+
var note git.Note
- err = git.GetNote(ctx, gitRepo, identifier, ¬e)
- if err != nil {
+ if err := git.GetNote(ctx, ctx.Repo.GitRepo, identifier, ¬e); err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(identifier)
return
@@ -72,7 +68,7 @@ func getNote(ctx *context.APIContext, identifier string) {
return
}
- cmt, err := convert.ToCommit(ctx.Repo.Repository, gitRepo, note.Commit, nil)
+ cmt, err := convert.ToCommit(ctx.Repo.Repository, ctx.Repo.GitRepo, note.Commit, nil, true)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ToCommit", err)
return
diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go
index ae64c6efe3aa3..6fbb9e7b3a75f 100644
--- a/routers/api/v1/repo/patch.go
+++ b/routers/api/v1/repo/patch.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,6 +8,7 @@ import (
"time"
"code.gitea.io/gitea/models"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
@@ -77,8 +77,8 @@ func ApplyDiffPatch(ctx *context.APIContext) {
opts.Message = "apply-patch"
}
- if !canWriteFiles(ctx.Repo) {
- ctx.Error(http.StatusInternalServerError, "ApplyPatch", models.ErrUserDoesNotHaveAccessToRepo{
+ if !canWriteFiles(ctx, apiOpts.BranchName) {
+ ctx.Error(http.StatusInternalServerError, "ApplyPatch", repo_model.ErrUserDoesNotHaveAccessToRepo{
UserID: ctx.Doer.ID,
RepoName: ctx.Repo.Repository.LowerName,
})
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 94262f81d1874..8fdbec4b8903c 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -1,6 +1,5 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
@@ -14,21 +13,28 @@ import (
"time"
"code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
+ git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
+ "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/automerge"
+ "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/gitdiff"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
@@ -89,7 +95,7 @@ func ListPullRequests(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
- prs, maxResults, err := models.PullRequests(ctx.Repo.Repository.ID, &models.PullRequestsOptions{
+ prs, maxResults, err := issues_model.PullRequests(ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{
ListOptions: listOptions,
State: ctx.FormTrim("state"),
SortType: ctx.FormTrim("sort"),
@@ -103,19 +109,19 @@ func ListPullRequests(ctx *context.APIContext) {
apiPrs := make([]*api.PullRequest, len(prs))
for i := range prs {
- if err = prs[i].LoadIssue(); err != nil {
+ if err = prs[i].LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
- if err = prs[i].LoadAttributes(); err != nil {
+ if err = prs[i].LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
- if err = prs[i].LoadBaseRepo(); err != nil {
+ if err = prs[i].LoadBaseRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
return
}
- if err = prs[i].LoadHeadRepo(); err != nil {
+ if err = prs[i].LoadHeadRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
return
}
@@ -157,9 +163,9 @@ func GetPullRequest(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -167,11 +173,11 @@ func GetPullRequest(ctx *context.APIContext) {
return
}
- if err = pr.LoadBaseRepo(); err != nil {
+ if err = pr.LoadBaseRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
return
}
- if err = pr.LoadHeadRepo(); err != nil {
+ if err = pr.LoadHeadRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
return
}
@@ -217,9 +223,9 @@ func DownloadPullDiffOrPatch(ctx *context.APIContext) {
// "$ref": "#/responses/string"
// "404":
// "$ref": "#/responses/notFound"
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound()
} else {
ctx.InternalServerError(err)
@@ -294,14 +300,14 @@ func CreatePullRequest(ctx *context.APIContext) {
defer headGitRepo.Close()
// Check if another PR exists with the same targets
- existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, models.PullRequestFlowGithub)
+ existingPr, err := issues_model.GetUnmergedPullRequest(ctx, headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, issues_model.PullRequestFlowGithub)
if err != nil {
- if !models.IsErrPullRequestNotExist(err) {
+ if !issues_model.IsErrPullRequestNotExist(err) {
ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err)
return
}
} else {
- err = models.ErrPullRequestAlreadyExists{
+ err = issues_model.ErrPullRequestAlreadyExists{
ID: existingPr.ID,
IssueID: existingPr.Index,
HeadRepoID: existingPr.HeadRepoID,
@@ -314,7 +320,7 @@ func CreatePullRequest(ctx *context.APIContext) {
}
if len(form.Labels) > 0 {
- labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
+ labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
return
@@ -328,7 +334,7 @@ func CreatePullRequest(ctx *context.APIContext) {
}
if ctx.Repo.Owner.IsOrganization() {
- orgLabels, err := models.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels)
+ orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err)
return
@@ -361,7 +367,7 @@ func CreatePullRequest(ctx *context.APIContext) {
deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
}
- prIssue := &models.Issue{
+ prIssue := &issues_model.Issue{
RepoID: repo.ID,
Title: form.Title,
PosterID: ctx.Doer.ID,
@@ -371,7 +377,7 @@ func CreatePullRequest(ctx *context.APIContext) {
Content: form.Body,
DeadlineUnix: deadlineUnix,
}
- pr := &models.PullRequest{
+ pr := &issues_model.PullRequest{
HeadRepoID: headRepo.ID,
BaseRepoID: repo.ID,
HeadBranch: headBranch,
@@ -379,11 +385,11 @@ func CreatePullRequest(ctx *context.APIContext) {
HeadRepo: headRepo,
BaseRepo: repo,
MergeBase: compareInfo.MergeBase,
- Type: models.PullRequestGitea,
+ Type: issues_model.PullRequestGitea,
}
// Get all assignee IDs
- assigneeIDs, err := models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees)
+ assigneeIDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
@@ -394,25 +400,25 @@ func CreatePullRequest(ctx *context.APIContext) {
}
// Check if the passed assignees is assignable
for _, aID := range assigneeIDs {
- assignee, err := user_model.GetUserByID(aID)
+ assignee, err := user_model.GetUserByID(ctx, aID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserByID", err)
return
}
- valid, err := models.CanBeAssigned(assignee, repo, true)
+ valid, err := access_model.CanBeAssigned(ctx, assignee, repo, true)
if err != nil {
ctx.Error(http.StatusInternalServerError, "canBeAssigned", err)
return
}
if !valid {
- ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
+ ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
return
}
}
if err := pull_service.NewPullRequest(ctx, repo, prIssue, labelIDs, []string{}, pr, assigneeIDs); err != nil {
- if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+ if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
return
}
@@ -467,9 +473,9 @@ func EditPullRequest(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditPullRequestOption)
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -477,7 +483,7 @@ func EditPullRequest(ctx *context.APIContext) {
return
}
- err = pr.LoadIssue()
+ err = pr.LoadIssue(ctx)
if err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
@@ -485,6 +491,11 @@ func EditPullRequest(ctx *context.APIContext) {
issue := pr.Issue
issue.Repo = ctx.Repo.Repository
+ if err := issue.LoadAttributes(ctx); err != nil {
+ ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
+ return
+ }
+
if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) {
ctx.Status(http.StatusForbidden)
return
@@ -507,7 +518,7 @@ func EditPullRequest(ctx *context.APIContext) {
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
}
- if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
+ if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
return
}
@@ -545,14 +556,14 @@ func EditPullRequest(ctx *context.APIContext) {
}
if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil {
- labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
+ labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDsError", err)
return
}
if ctx.Repo.Owner.IsOrganization() {
- orgLabels, err := models.GetLabelsInOrgByIDs(ctx.Repo.Owner.ID, form.Labels)
+ orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err)
return
@@ -561,7 +572,7 @@ func EditPullRequest(ctx *context.APIContext) {
labels = append(labels, orgLabels...)
}
- if err = models.ReplaceIssueLabels(issue, labels, ctx.Doer); err != nil {
+ if err = issues_model.ReplaceIssueLabels(issue, labels, ctx.Doer); err != nil {
ctx.Error(http.StatusInternalServerError, "ReplaceLabelsError", err)
return
}
@@ -574,9 +585,9 @@ func EditPullRequest(ctx *context.APIContext) {
}
issue.IsClosed = api.StateClosed == api.StateType(*form.State)
}
- statusChangeComment, titleChanged, err := models.UpdateIssueByAPI(issue, ctx.Doer)
+ statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(issue, ctx.Doer)
if err != nil {
- if models.IsErrDependenciesLeft(err) {
+ if issues_model.IsErrDependenciesLeft(err) {
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
return
}
@@ -585,11 +596,11 @@ func EditPullRequest(ctx *context.APIContext) {
}
if titleChanged {
- notification.NotifyIssueChangeTitle(ctx.Doer, issue, oldTitle)
+ notification.NotifyIssueChangeTitle(ctx, ctx.Doer, issue, oldTitle)
}
if statusChangeComment != nil {
- notification.NotifyIssueChangeStatus(ctx.Doer, issue, statusChangeComment, issue.IsClosed)
+ notification.NotifyIssueChangeStatus(ctx, ctx.Doer, issue, statusChangeComment, issue.IsClosed)
}
// change pull target branch
@@ -599,10 +610,10 @@ func EditPullRequest(ctx *context.APIContext) {
return
}
if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil {
- if models.IsErrPullRequestAlreadyExists(err) {
+ if issues_model.IsErrPullRequestAlreadyExists(err) {
ctx.Error(http.StatusConflict, "IsErrPullRequestAlreadyExists", err)
return
- } else if models.IsErrIssueIsClosed(err) {
+ } else if issues_model.IsErrIssueIsClosed(err) {
ctx.Error(http.StatusUnprocessableEntity, "IsErrIssueIsClosed", err)
return
} else if models.IsErrPullRequestHasMerged(err) {
@@ -613,13 +624,25 @@ func EditPullRequest(ctx *context.APIContext) {
}
return
}
- notification.NotifyPullRequestChangeTargetBranch(ctx.Doer, pr, form.Base)
+ notification.NotifyPullRequestChangeTargetBranch(ctx, ctx.Doer, pr, form.Base)
+ }
+
+ // update allow edits
+ if form.AllowMaintainerEdit != nil {
+ if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, *form.AllowMaintainerEdit); err != nil {
+ if errors.Is(pull_service.ErrUserHasNoPermissionForAction, err) {
+ ctx.Error(http.StatusForbidden, "SetAllowEdits", fmt.Sprintf("SetAllowEdits: %s", err))
+ return
+ }
+ ctx.ServerError("SetAllowEdits", err)
+ return
+ }
}
// Refetch from database
- pr, err = models.GetPullRequestByIndex(ctx.Repo.Repository.ID, pr.Index)
+ pr, err = issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pr.Index)
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -661,9 +684,9 @@ func IsPullRequestMerged(ctx *context.APIContext) {
// "404":
// description: pull request has not been merged
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -714,9 +737,10 @@ func MergePullRequest(ctx *context.APIContext) {
// "$ref": "#/responses/error"
form := web.GetForm(ctx).(*forms.MergePullRequestForm)
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound("GetPullRequestByIndex", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -724,12 +748,12 @@ func MergePullRequest(ctx *context.APIContext) {
return
}
- if err := pr.LoadHeadRepo(); err != nil {
+ if err := pr.LoadHeadRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
return
}
- if err := pr.LoadIssue(); err != nil {
+ if err := pr.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
@@ -737,7 +761,7 @@ func MergePullRequest(ctx *context.APIContext) {
if ctx.IsSigned {
// Update issue-user.
- if err = pr.Issue.ReadBy(ctx.Doer.ID); err != nil {
+ if err = activities_model.SetIssueReadBy(ctx, pr.Issue.ID, ctx.Doer.ID); err != nil {
ctx.Error(http.StatusInternalServerError, "ReadBy", err)
return
}
@@ -746,6 +770,7 @@ func MergePullRequest(ctx *context.APIContext) {
manuallMerge := repo_model.MergeStyle(form.Do) == repo_model.MergeStyleManuallyMerged
force := form.ForceMerge != nil && *form.ForceMerge
+ // start with merging by checking
if err := pull_service.CheckPullMergable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, manuallMerge, force); err != nil {
if errors.Is(err, pull_service.ErrIsClosed) {
ctx.NotFound()
@@ -785,16 +810,43 @@ func MergePullRequest(ctx *context.APIContext) {
return
}
- // set defaults to propagate needed fields
- if err := form.SetDefaults(pr); err != nil {
- ctx.ServerError("SetDefaults", fmt.Errorf("SetDefaults: %v", err))
- return
+ if len(form.Do) == 0 {
+ form.Do = string(repo_model.MergeStyleMerge)
+ }
+
+ message := strings.TrimSpace(form.MergeTitleField)
+ if len(message) == 0 {
+ message, _, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetDefaultMergeMessage", err)
+ return
+ }
+ }
+
+ form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
+ if len(form.MergeMessageField) > 0 {
+ message += "\n\n" + form.MergeMessageField
}
- if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, form.MergeTitleField); err != nil {
+ if form.MergeWhenChecksSucceed {
+ scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message)
+ if err != nil {
+ if pull_model.IsErrAlreadyScheduledToAutoMerge(err) {
+ ctx.Error(http.StatusConflict, "ScheduleAutoMerge", err)
+ return
+ }
+ ctx.Error(http.StatusInternalServerError, "ScheduleAutoMerge", err)
+ return
+ } else if scheduled {
+ // nothing more to do ...
+ ctx.Status(http.StatusCreated)
+ return
+ }
+ }
+
+ if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if models.IsErrInvalidMergeStyle(err) {
ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
- return
} else if models.IsErrMergeConflicts(err) {
conflictError := err.(models.ErrMergeConflicts)
ctx.JSON(http.StatusConflict, conflictError)
@@ -806,28 +858,25 @@ func MergePullRequest(ctx *context.APIContext) {
ctx.JSON(http.StatusConflict, conflictError)
} else if git.IsErrPushOutOfDate(err) {
ctx.Error(http.StatusConflict, "Merge", "merge push out of date")
- return
} else if models.IsErrSHADoesNotMatch(err) {
ctx.Error(http.StatusConflict, "Merge", "head out of date")
- return
} else if git.IsErrPushRejected(err) {
errPushRej := err.(*git.ErrPushRejected)
if len(errPushRej.Message) == 0 {
ctx.Error(http.StatusConflict, "Merge", "PushRejected without remote error message")
- return
+ } else {
+ ctx.Error(http.StatusConflict, "Merge", "PushRejected with remote message: "+errPushRej.Message)
}
- ctx.Error(http.StatusConflict, "Merge", "PushRejected with remote message: "+errPushRej.Message)
- return
+ } else {
+ ctx.Error(http.StatusInternalServerError, "Merge", err)
}
- ctx.Error(http.StatusInternalServerError, "Merge", err)
return
}
-
log.Trace("Pull request merged: %d", pr.ID)
if form.DeleteBranchAfterMerge {
// Don't cleanup when there are other PR's that use this branch as head branch.
- exist, err := models.HasUnmergedPullRequestsByHeadInfo(pr.HeadRepoID, pr.HeadBranch)
+ exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
if err != nil {
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
return
@@ -854,14 +903,14 @@ func MergePullRequest(ctx *context.APIContext) {
ctx.NotFound(err)
case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch"))
- case errors.Is(err, repo_service.ErrBranchIsProtected):
+ case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected"))
default:
ctx.Error(http.StatusInternalServerError, "DeleteBranch", err)
}
return
}
- if err := models.AddDeletePRBranchComment(ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
+ if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
// Do not fail here as branch has already been deleted
log.Error("DeleteBranch: %v", err)
}
@@ -897,7 +946,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
headBranch = headInfos[0]
} else if len(headInfos) == 2 {
- headUser, err = user_model.GetUserByName(headInfos[0])
+ headUser, err = user_model.GetUserByName(ctx, headInfos[0])
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound("GetUserByName")
@@ -943,7 +992,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
}
// user should have permission to read baseRepo's codes and pulls, NOT headRepo's
- permBase, err := models.GetUserRepoPermission(baseRepo, ctx.Doer)
+ permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
if err != nil {
headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
@@ -962,7 +1011,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
}
// user should have permission to read headrepo's codes
- permHead, err := models.GetUserRepoPermission(headRepo, ctx.Doer)
+ permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer)
if err != nil {
headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
@@ -1038,9 +1087,9 @@ func UpdatePullRequest(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -1053,7 +1102,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
return
}
- if err = pr.LoadIssue(); err != nil {
+ if err = pr.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
@@ -1063,18 +1112,18 @@ func UpdatePullRequest(ctx *context.APIContext) {
return
}
- if err = pr.LoadBaseRepo(); err != nil {
+ if err = pr.LoadBaseRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
return
}
- if err = pr.LoadHeadRepo(); err != nil {
+ if err = pr.LoadHeadRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
return
}
rebase := ctx.FormString("style") == "rebase"
- allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(pr, ctx.Doer)
+ allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "IsUserAllowedToMerge", err)
return
@@ -1103,6 +1152,78 @@ func UpdatePullRequest(ctx *context.APIContext) {
ctx.Status(http.StatusOK)
}
+// MergePullRequest cancel an auto merge scheduled for a given PullRequest by index
+func CancelScheduledAutoMerge(ctx *context.APIContext) {
+ // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/merge repository repoCancelScheduledAutoMerge
+ // ---
+ // summary: Cancel the scheduled auto merge for the given pull request
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the pull request to merge
+ // type: integer
+ // format: int64
+ // required: true
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ pullIndex := ctx.ParamsInt64(":index")
+ pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex)
+ if err != nil {
+ if issues_model.IsErrPullRequestNotExist(err) {
+ ctx.NotFound()
+ return
+ }
+ ctx.InternalServerError(err)
+ return
+ }
+
+ exist, autoMerge, err := pull_model.GetScheduledMergeByPullID(ctx, pull.ID)
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+ if !exist {
+ ctx.NotFound()
+ return
+ }
+
+ if ctx.Doer.ID != autoMerge.DoerID {
+ allowed, err := access_model.IsUserRepoAdmin(ctx, ctx.Repo.Repository, ctx.Doer)
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+ if !allowed {
+ ctx.Error(http.StatusForbidden, "No permission to cancel", "user has no permission to cancel the scheduled auto merge")
+ return
+ }
+ }
+
+ if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, pull); err != nil {
+ ctx.InternalServerError(err)
+ } else {
+ ctx.Status(http.StatusNoContent)
+ }
+}
+
// GetPullRequestCommits gets all commits associated with a given PR
func GetPullRequestCommits(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/commits repository repoGetPullRequestCommits
@@ -1141,9 +1262,9 @@ func GetPullRequestCommits(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -1151,7 +1272,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
return
}
- if err := pr.LoadBaseRepo(); err != nil {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
ctx.InternalServerError(err)
return
}
@@ -1190,7 +1311,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
apiCommits := make([]*api.Commit, 0, end-start)
for i := start; i < end; i++ {
- apiCommit, err := convert.ToCommit(ctx.Repo.Repository, baseGitRepo, commits[i], userCache)
+ apiCommit, err := convert.ToCommit(ctx.Repo.Repository, baseGitRepo, commits[i], userCache, true)
if err != nil {
ctx.ServerError("toCommit", err)
return
@@ -1209,3 +1330,141 @@ func GetPullRequestCommits(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, &apiCommits)
}
+
+// GetPullRequestFiles gets all changed files associated with a given PR
+func GetPullRequestFiles(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/files repository repoGetPullRequestFiles
+ // ---
+ // summary: Get changed files for a pull request
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: index
+ // in: path
+ // description: index of the pull request to get
+ // type: integer
+ // format: int64
+ // required: true
+ // - name: skip-to
+ // in: query
+ // description: skip to given file
+ // type: string
+ // - name: whitespace
+ // in: query
+ // description: whitespace behavior
+ // type: string
+ // enum: [ignore-all, ignore-change, ignore-eol, show-all]
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ChangedFileList"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ if err != nil {
+ if issues_model.IsErrPullRequestNotExist(err) {
+ ctx.NotFound()
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
+ }
+ return
+ }
+
+ if err := pr.LoadBaseRepo(ctx); err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+
+ if err := pr.LoadHeadRepo(ctx); err != nil {
+ ctx.InternalServerError(err)
+ return
+ }
+
+ baseGitRepo := ctx.Repo.GitRepo
+
+ var prInfo *git.CompareInfo
+ if pr.HasMerged {
+ prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitRefName(), true, false)
+ } else {
+ prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName(), true, false)
+ }
+ if err != nil {
+ ctx.ServerError("GetCompareInfo", err)
+ return
+ }
+
+ headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
+ if err != nil {
+ ctx.ServerError("GetRefCommitID", err)
+ return
+ }
+
+ startCommitID := prInfo.MergeBase
+ endCommitID := headCommitID
+
+ maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
+
+ diff, err := gitdiff.GetDiff(baseGitRepo,
+ &gitdiff.DiffOptions{
+ BeforeCommitID: startCommitID,
+ AfterCommitID: endCommitID,
+ SkipTo: ctx.FormString("skip-to"),
+ MaxLines: maxLines,
+ MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
+ MaxFiles: maxFiles,
+ WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.FormString("whitespace")),
+ })
+ if err != nil {
+ ctx.ServerError("GetDiff", err)
+ return
+ }
+
+ listOptions := utils.GetListOptions(ctx)
+
+ totalNumberOfFiles := diff.NumFiles
+ totalNumberOfPages := int(math.Ceil(float64(totalNumberOfFiles) / float64(listOptions.PageSize)))
+
+ start, end := listOptions.GetStartEnd()
+
+ if end > totalNumberOfFiles {
+ end = totalNumberOfFiles
+ }
+
+ lenFiles := end - start
+ if lenFiles < 0 {
+ lenFiles = 0
+ }
+ apiFiles := make([]*api.ChangedFile, 0, lenFiles)
+ for i := start; i < end; i++ {
+ apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID))
+ }
+
+ ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize)
+ ctx.SetTotalCountHeader(int64(totalNumberOfFiles))
+
+ ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page))
+ ctx.RespHeader().Set("X-PerPage", strconv.Itoa(listOptions.PageSize))
+ ctx.RespHeader().Set("X-PageCount", strconv.Itoa(totalNumberOfPages))
+ ctx.RespHeader().Set("X-HasMore", strconv.FormatBool(listOptions.Page < totalNumberOfPages))
+ ctx.AppendAccessControlExposeHeaders("X-Page", "X-PerPage", "X-PageCount", "X-HasMore")
+
+ ctx.JSON(http.StatusOK, &apiFiles)
+}
diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go
index 3b36f28326f5b..96dc1fc2dee1d 100644
--- a/routers/api/v1/repo/pull_review.go
+++ b/routers/api/v1/repo/pull_review.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,15 +8,16 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
+ access_model "code.gitea.io/gitea/models/perm/access"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
)
@@ -60,9 +60,9 @@ func ListPullReviews(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound("GetPullRequestByIndex", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -70,7 +70,7 @@ func ListPullReviews(ctx *context.APIContext) {
return
}
- if err = pr.LoadIssue(); err != nil {
+ if err = pr.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
@@ -80,19 +80,19 @@ func ListPullReviews(ctx *context.APIContext) {
return
}
- opts := models.FindReviewOptions{
+ opts := issues_model.FindReviewOptions{
ListOptions: utils.GetListOptions(ctx),
- Type: models.ReviewTypeUnknown,
+ Type: issues_model.ReviewTypeUnknown,
IssueID: pr.IssueID,
}
- allReviews, err := models.FindReviews(opts)
+ allReviews, err := issues_model.FindReviews(ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- count, err := models.CountReviews(opts)
+ count, err := issues_model.CountReviews(opts)
if err != nil {
ctx.InternalServerError(err)
return
@@ -260,7 +260,7 @@ func DeletePullReview(ctx *context.APIContext) {
return
}
- if err := models.DeleteReview(review); err != nil {
+ if err := issues_model.DeleteReview(review); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteReview", fmt.Errorf("can not delete ReviewID: %d", review.ID))
return
}
@@ -306,9 +306,9 @@ func CreatePullReview(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.CreatePullReviewOptions)
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound("GetPullRequestByIndex", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -434,7 +434,7 @@ func SubmitPullReview(ctx *context.APIContext) {
return
}
- if review.Type != models.ReviewTypePending {
+ if review.Type != issues_model.ReviewTypePending {
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("only a pending review can be submitted"))
return
}
@@ -446,7 +446,7 @@ func SubmitPullReview(ctx *context.APIContext) {
}
// if review stay pending return
- if reviewType == models.ReviewTypePending {
+ if reviewType == issues_model.ReviewTypePending {
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review stay pending"))
return
}
@@ -474,8 +474,8 @@ func SubmitPullReview(ctx *context.APIContext) {
}
// preparePullReviewType return ReviewType and false or nil and true if an error happen
-func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, event api.ReviewStateType, body string, hasComments bool) (models.ReviewType, bool) {
- if err := pr.LoadIssue(); err != nil {
+func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest, event api.ReviewStateType, body string, hasComments bool) (issues_model.ReviewType, bool) {
+ if err := pr.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return -1, true
}
@@ -483,7 +483,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
needsBody := true
hasBody := len(strings.TrimSpace(body)) > 0
- var reviewType models.ReviewType
+ var reviewType issues_model.ReviewType
switch event {
case api.ReviewStateApproved:
// can not approve your own PR
@@ -491,7 +491,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("approve your own pull is not allowed"))
return -1, true
}
- reviewType = models.ReviewTypeApprove
+ reviewType = issues_model.ReviewTypeApprove
needsBody = false
case api.ReviewStateRequestChanges:
@@ -500,10 +500,10 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("reject your own pull is not allowed"))
return -1, true
}
- reviewType = models.ReviewTypeReject
+ reviewType = issues_model.ReviewTypeReject
case api.ReviewStateComment:
- reviewType = models.ReviewTypeComment
+ reviewType = issues_model.ReviewTypeComment
needsBody = false
// if there is no body we need to ensure that there are comments
if !hasBody && !hasComments {
@@ -511,7 +511,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
return -1, true
}
default:
- reviewType = models.ReviewTypePending
+ reviewType = issues_model.ReviewTypePending
}
// reject reviews with empty body if a body is required for this call
@@ -524,10 +524,10 @@ func preparePullReviewType(ctx *context.APIContext, pr *models.PullRequest, even
}
// prepareSingleReview return review, related pull and false or nil, nil and true if an error happen
-func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullRequest, bool) {
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) {
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound("GetPullRequestByIndex", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -535,9 +535,9 @@ func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullR
return nil, nil, true
}
- review, err := models.GetReviewByID(ctx.ParamsInt64(":id"))
+ review, err := issues_model.GetReviewByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- if models.IsErrReviewNotExist(err) {
+ if issues_model.IsErrReviewNotExist(err) {
ctx.NotFound("GetReviewByID", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetReviewByID", err)
@@ -552,7 +552,7 @@ func prepareSingleReview(ctx *context.APIContext) (*models.Review, *models.PullR
}
// make sure that the user has access to this review if it is pending
- if review.Type == models.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
+ if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
ctx.NotFound("GetReviewByID")
return nil, nil, true
}
@@ -647,9 +647,9 @@ func DeleteReviewRequests(ctx *context.APIContext) {
}
func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) {
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound("GetPullRequestByIndex", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
@@ -664,7 +664,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
reviewers := make([]*user_model.User, 0, len(opts.Reviewers))
- permDoer, err := models.GetUserRepoPermission(pr.Issue.Repo, ctx.Doer)
+ permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return
@@ -675,7 +675,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
if strings.Contains(r, "@") {
reviewer, err = user_model.GetUserByEmail(r)
} else {
- reviewer, err = user_model.GetUserByName(r)
+ reviewer, err = user_model.GetUserByName(ctx, r)
}
if err != nil {
@@ -687,9 +687,9 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
return
}
- err = issue_service.IsValidReviewRequest(reviewer, ctx.Doer, isAdd, pr.Issue, &permDoer)
+ err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, isAdd, pr.Issue, &permDoer)
if err != nil {
- if models.IsErrNotValidReviewRequest(err) {
+ if issues_model.IsErrNotValidReviewRequest(err) {
ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err)
return
}
@@ -700,9 +700,9 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
reviewers = append(reviewers, reviewer)
}
- var reviews []*models.Review
+ var reviews []*issues_model.Review
if isAdd {
- reviews = make([]*models.Review, 0, len(reviewers))
+ reviews = make([]*issues_model.Review, 0, len(reviewers))
}
for _, reviewer := range reviewers {
@@ -726,7 +726,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
teamReviewers := make([]*organization.Team, 0, len(opts.TeamReviewers))
for _, t := range opts.TeamReviewers {
var teamReviewer *organization.Team
- teamReviewer, err = organization.GetTeam(ctx.Repo.Owner.ID, t)
+ teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t))
@@ -736,9 +736,9 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
return
}
- err = issue_service.IsValidTeamReviewRequest(teamReviewer, ctx.Doer, isAdd, pr.Issue)
+ err = issue_service.IsValidTeamReviewRequest(ctx, teamReviewer, ctx.Doer, isAdd, pr.Issue)
if err != nil {
- if models.IsErrNotValidReviewRequest(err) {
+ if issues_model.IsErrNotValidReviewRequest(err) {
ctx.Error(http.StatusUnprocessableEntity, "NotValidReviewRequest", err)
return
}
@@ -822,7 +822,7 @@ func DismissPullReview(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.DismissPullReviewOptions)
- dismissReview(ctx, opts.Message, true)
+ dismissReview(ctx, opts.Message, true, opts.Priors)
}
// UnDismissPullReview cancel to dismiss a review for a pull request
@@ -862,10 +862,10 @@ func UnDismissPullReview(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
- dismissReview(ctx, "", false)
+ dismissReview(ctx, "", false, false)
}
-func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) {
+func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) {
if !ctx.Repo.IsAdmin() {
ctx.Error(http.StatusForbidden, "", "Must be repo admin")
return
@@ -875,7 +875,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) {
return
}
- if review.Type != models.ReviewTypeApprove && review.Type != models.ReviewTypeReject {
+ if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject {
ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because it's type is not Approve or change request")
return
}
@@ -885,13 +885,13 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss bool) {
return
}
- _, err := pull_service.DismissReview(ctx, review.ID, msg, ctx.Doer, isDismiss)
+ _, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors)
if err != nil {
ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err)
return
}
- if review, err = models.GetReviewByID(review.ID); err != nil {
+ if review, err = issues_model.GetReviewByID(ctx, review.ID); err != nil {
ctx.Error(http.StatusInternalServerError, "GetReviewByID", err)
return
}
diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go
index 7d23a38add22a..d0b20102f7a4f 100644
--- a/routers/api/v1/repo/release.go
+++ b/routers/api/v1/repo/release.go
@@ -1,6 +1,5 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,13 +8,14 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
- releaseservice "code.gitea.io/gitea/services/release"
+ "code.gitea.io/gitea/services/convert"
+ release_service "code.gitea.io/gitea/services/release"
)
// GetRelease get a single release of a repository
@@ -49,18 +49,18 @@ func GetRelease(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
id := ctx.ParamsInt64(":id")
- release, err := models.GetReleaseByID(id)
- if err != nil && !models.IsErrReleaseNotExist(err) {
+ release, err := repo_model.GetReleaseByID(ctx, id)
+ if err != nil && !repo_model.IsErrReleaseNotExist(err) {
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
return
}
- if err != nil && models.IsErrReleaseNotExist(err) ||
+ if err != nil && repo_model.IsErrReleaseNotExist(err) ||
release.IsTag || release.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound()
return
}
- if err := release.LoadAttributes(); err != nil {
+ if err := release.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
@@ -114,7 +114,7 @@ func ListReleases(ctx *context.APIContext) {
listOptions.PageSize = ctx.FormInt("per_page")
}
- opts := models.FindReleasesOptions{
+ opts := repo_model.FindReleasesOptions{
ListOptions: listOptions,
IncludeDrafts: ctx.Repo.AccessMode >= perm.AccessModeWrite || ctx.Repo.UnitAccessMode(unit.TypeReleases) >= perm.AccessModeWrite,
IncludeTags: false,
@@ -122,21 +122,21 @@ func ListReleases(ctx *context.APIContext) {
IsPreRelease: ctx.FormOptionalBool("pre-release"),
}
- releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, opts)
+ releases, err := repo_model.GetReleasesByRepoID(ctx, ctx.Repo.Repository.ID, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetReleasesByRepoID", err)
return
}
rels := make([]*api.Release, len(releases))
for i, release := range releases {
- if err := release.LoadAttributes(); err != nil {
+ if err := release.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
rels[i] = convert.ToRelease(release)
}
- filteredCount, err := models.CountReleasesByRepoID(ctx.Repo.Repository.ID, opts)
+ filteredCount, err := repo_model.CountReleasesByRepoID(ctx.Repo.Repository.ID, opts)
if err != nil {
ctx.InternalServerError(err)
return
@@ -179,9 +179,9 @@ func CreateRelease(ctx *context.APIContext) {
// "409":
// "$ref": "#/responses/error"
form := web.GetForm(ctx).(*api.CreateReleaseOption)
- rel, err := models.GetRelease(ctx.Repo.Repository.ID, form.TagName)
+ rel, err := repo_model.GetRelease(ctx.Repo.Repository.ID, form.TagName)
if err != nil {
- if !models.IsErrReleaseNotExist(err) {
+ if !repo_model.IsErrReleaseNotExist(err) {
ctx.Error(http.StatusInternalServerError, "GetRelease", err)
return
}
@@ -189,7 +189,7 @@ func CreateRelease(ctx *context.APIContext) {
if len(form.Target) == 0 {
form.Target = ctx.Repo.Repository.DefaultBranch
}
- rel = &models.Release{
+ rel = &repo_model.Release{
RepoID: ctx.Repo.Repository.ID,
PublisherID: ctx.Doer.ID,
Publisher: ctx.Doer,
@@ -202,8 +202,8 @@ func CreateRelease(ctx *context.APIContext) {
IsTag: false,
Repo: ctx.Repo.Repository,
}
- if err := releaseservice.CreateRelease(ctx.Repo.GitRepo, rel, nil, ""); err != nil {
- if models.IsErrReleaseAlreadyExist(err) {
+ if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, ""); err != nil {
+ if repo_model.IsErrReleaseAlreadyExist(err) {
ctx.Error(http.StatusConflict, "ReleaseAlreadyExist", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateRelease", err)
@@ -224,8 +224,9 @@ func CreateRelease(ctx *context.APIContext) {
rel.IsTag = false
rel.Repo = ctx.Repo.Repository
rel.Publisher = ctx.Doer
+ rel.Target = form.Target
- if err = releaseservice.UpdateRelease(ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
+ if err = release_service.UpdateRelease(ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateRelease", err)
return
}
@@ -271,12 +272,12 @@ func EditRelease(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditReleaseOption)
id := ctx.ParamsInt64(":id")
- rel, err := models.GetReleaseByID(id)
- if err != nil && !models.IsErrReleaseNotExist(err) {
+ rel, err := repo_model.GetReleaseByID(ctx, id)
+ if err != nil && !repo_model.IsErrReleaseNotExist(err) {
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
return
}
- if err != nil && models.IsErrReleaseNotExist(err) ||
+ if err != nil && repo_model.IsErrReleaseNotExist(err) ||
rel.IsTag || rel.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound()
return
@@ -300,17 +301,18 @@ func EditRelease(ctx *context.APIContext) {
if form.IsPrerelease != nil {
rel.IsPrerelease = *form.IsPrerelease
}
- if err := releaseservice.UpdateRelease(ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
+ if err := release_service.UpdateRelease(ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateRelease", err)
return
}
- rel, err = models.GetReleaseByID(id)
+ // reload data from database
+ rel, err = repo_model.GetReleaseByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
return
}
- if err := rel.LoadAttributes(); err != nil {
+ if err := rel.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
@@ -344,19 +346,25 @@ func DeleteRelease(ctx *context.APIContext) {
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
+ // "405":
+ // "$ref": "#/responses/empty"
id := ctx.ParamsInt64(":id")
- rel, err := models.GetReleaseByID(id)
- if err != nil && !models.IsErrReleaseNotExist(err) {
+ rel, err := repo_model.GetReleaseByID(ctx, id)
+ if err != nil && !repo_model.IsErrReleaseNotExist(err) {
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
return
}
- if err != nil && models.IsErrReleaseNotExist(err) ||
+ if err != nil && repo_model.IsErrReleaseNotExist(err) ||
rel.IsTag || rel.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound()
return
}
- if err := releaseservice.DeleteReleaseByID(ctx, id, ctx.Doer, false); err != nil {
+ if err := release_service.DeleteReleaseByID(ctx, id, ctx.Doer, false); err != nil {
+ if models.IsErrProtectedTagName(err) {
+ ctx.Error(http.StatusMethodNotAllowed, "delTag", "user not allowed to delete protected tag")
+ return
+ }
ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err)
return
}
diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go
index c172b66127244..5aaea693c03b3 100644
--- a/routers/api/v1/repo/release_attachment.go
+++ b/routers/api/v1/repo/release_attachment.go
@@ -1,22 +1,20 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
- "code.gitea.io/gitea/models"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/attachment"
+ "code.gitea.io/gitea/services/convert"
)
// GetReleaseAttachment gets a single attachment of the release
@@ -55,8 +53,12 @@ func GetReleaseAttachment(ctx *context.APIContext) {
releaseID := ctx.ParamsInt64(":id")
attachID := ctx.ParamsInt64(":asset")
- attach, err := repo_model.GetAttachmentByID(attachID)
+ attach, err := repo_model.GetAttachmentByID(ctx, attachID)
if err != nil {
+ if repo_model.IsErrAttachmentNotExist(err) {
+ ctx.NotFound()
+ return
+ }
ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err)
return
}
@@ -66,7 +68,7 @@ func GetReleaseAttachment(ctx *context.APIContext) {
return
}
// FIXME Should prove the existence of the given repo, but results in unnecessary database requests
- ctx.JSON(http.StatusOK, convert.ToReleaseAttachment(attach))
+ ctx.JSON(http.StatusOK, convert.ToAttachment(attach))
}
// ListReleaseAttachments lists all attachments of the release
@@ -98,8 +100,12 @@ func ListReleaseAttachments(ctx *context.APIContext) {
// "$ref": "#/responses/AttachmentList"
releaseID := ctx.ParamsInt64(":id")
- release, err := models.GetReleaseByID(releaseID)
+ release, err := repo_model.GetReleaseByID(ctx, releaseID)
if err != nil {
+ if repo_model.IsErrReleaseNotExist(err) {
+ ctx.NotFound()
+ return
+ }
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
return
}
@@ -107,7 +113,7 @@ func ListReleaseAttachments(ctx *context.APIContext) {
ctx.NotFound()
return
}
- if err := release.LoadAttributes(); err != nil {
+ if err := release.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
@@ -164,8 +170,12 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
// Check if release exists an load release
releaseID := ctx.ParamsInt64(":id")
- release, err := models.GetReleaseByID(releaseID)
+ release, err := repo_model.GetReleaseByID(ctx, releaseID)
if err != nil {
+ if repo_model.IsErrReleaseNotExist(err) {
+ ctx.NotFound()
+ return
+ }
ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err)
return
}
@@ -184,7 +194,12 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
}
// Create a new attachment and save the file
- attach, err := attachment.UploadAttachment(file, ctx.Doer.ID, release.RepoID, releaseID, filename, setting.Repository.Release.AllowedTypes)
+ attach, err := attachment.UploadAttachment(file, setting.Repository.Release.AllowedTypes, &repo_model.Attachment{
+ Name: filename,
+ UploaderID: ctx.Doer.ID,
+ RepoID: release.RepoID,
+ ReleaseID: releaseID,
+ })
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.Error(http.StatusBadRequest, "DetectContentType", err)
@@ -194,7 +209,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusCreated, convert.ToReleaseAttachment(attach))
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attach))
}
// EditReleaseAttachment updates the given attachment
@@ -242,8 +257,12 @@ func EditReleaseAttachment(ctx *context.APIContext) {
// Check if release exists an load release
releaseID := ctx.ParamsInt64(":id")
attachID := ctx.ParamsInt64(":asset")
- attach, err := repo_model.GetAttachmentByID(attachID)
+ attach, err := repo_model.GetAttachmentByID(ctx, attachID)
if err != nil {
+ if repo_model.IsErrAttachmentNotExist(err) {
+ ctx.NotFound()
+ return
+ }
ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err)
return
}
@@ -257,10 +276,10 @@ func EditReleaseAttachment(ctx *context.APIContext) {
attach.Name = form.Name
}
- if err := repo_model.UpdateAttachment(attach); err != nil {
+ if err := repo_model.UpdateAttachment(ctx, attach); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach)
}
- ctx.JSON(http.StatusCreated, convert.ToReleaseAttachment(attach))
+ ctx.JSON(http.StatusCreated, convert.ToAttachment(attach))
}
// DeleteReleaseAttachment delete a given attachment
@@ -300,8 +319,12 @@ func DeleteReleaseAttachment(ctx *context.APIContext) {
// Check if release exists an load release
releaseID := ctx.ParamsInt64(":id")
attachID := ctx.ParamsInt64(":asset")
- attach, err := repo_model.GetAttachmentByID(attachID)
+ attach, err := repo_model.GetAttachmentByID(ctx, attachID)
if err != nil {
+ if repo_model.IsErrAttachmentNotExist(err) {
+ ctx.NotFound()
+ return
+ }
ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err)
return
}
diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go
index a737bcf1c8f4c..7cc846fdc4437 100644
--- a/routers/api/v1/repo/release_tags.go
+++ b/routers/api/v1/repo/release_tags.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -8,8 +7,9 @@ import (
"net/http"
"code.gitea.io/gitea/models"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
+ "code.gitea.io/gitea/services/convert"
releaseservice "code.gitea.io/gitea/services/release"
)
@@ -44,9 +44,9 @@ func GetReleaseByTag(ctx *context.APIContext) {
tag := ctx.Params(":tag")
- release, err := models.GetRelease(ctx.Repo.Repository.ID, tag)
+ release, err := repo_model.GetRelease(ctx.Repo.Repository.ID, tag)
if err != nil {
- if models.IsErrReleaseNotExist(err) {
+ if repo_model.IsErrReleaseNotExist(err) {
ctx.NotFound()
return
}
@@ -59,7 +59,7 @@ func GetReleaseByTag(ctx *context.APIContext) {
return
}
- if err = release.LoadAttributes(); err != nil {
+ if err = release.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
@@ -92,12 +92,14 @@ func DeleteReleaseByTag(ctx *context.APIContext) {
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
+ // "405":
+ // "$ref": "#/responses/empty"
tag := ctx.Params(":tag")
- release, err := models.GetRelease(ctx.Repo.Repository.ID, tag)
+ release, err := repo_model.GetRelease(ctx.Repo.Repository.ID, tag)
if err != nil {
- if models.IsErrReleaseNotExist(err) {
+ if repo_model.IsErrReleaseNotExist(err) {
ctx.NotFound()
return
}
@@ -111,7 +113,12 @@ func DeleteReleaseByTag(ctx *context.APIContext) {
}
if err = releaseservice.DeleteReleaseByID(ctx, release.ID, ctx.Doer, false); err != nil {
+ if models.IsErrProtectedTagName(err) {
+ ctx.Error(http.StatusMethodNotAllowed, "delTag", "user not allowed to delete protected tag")
+ return
+ }
ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err)
+ return
}
ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index f645502590e73..e0cae5f82c7a8 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -11,23 +10,24 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -121,7 +121,7 @@ func Search(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
- opts := &models.SearchRepoOptions{
+ opts := &repo_model.SearchRepoOptions{
ListOptions: utils.GetListOptions(ctx),
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
@@ -190,7 +190,7 @@ func Search(ctx *context.APIContext) {
}
var err error
- repos, count, err := models.SearchRepository(opts)
+ repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
ctx.JSON(http.StatusInternalServerError, api.SearchError{
OK: false,
@@ -208,16 +208,15 @@ func Search(ctx *context.APIContext) {
})
return
}
- accessMode, err := models.AccessLevel(ctx.Doer, repo)
+ accessMode, err := access_model.AccessLevel(ctx, ctx.Doer, repo)
if err != nil {
ctx.JSON(http.StatusInternalServerError, api.SearchError{
OK: false,
Error: err.Error(),
})
}
- results[i] = convert.ToRepo(repo, accessMode)
+ results[i] = convert.ToRepo(ctx, repo, accessMode)
}
-
ctx.SetLinkHeader(int(count), opts.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, api.SearchResults{
@@ -231,7 +230,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
if opt.AutoInit && opt.Readme == "" {
opt.Readme = "Default"
}
- repo, err := repo_service.CreateRepository(ctx.Doer, owner, models.CreateRepoOptions{
+ repo, err := repo_service.CreateRepository(ctx.Doer, owner, repo_module.CreateRepoOptions{
Name: opt.Name,
Description: opt.Description,
IssueLabels: opt.IssueLabels,
@@ -248,7 +247,8 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
if repo_model.IsErrRepoAlreadyExist(err) {
ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.")
} else if db.IsErrNameReserved(err) ||
- db.IsErrNamePatternNotAllowed(err) {
+ db.IsErrNamePatternNotAllowed(err) ||
+ repo_module.IsErrIssueLabelTemplateLoad(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateRepository", err)
@@ -257,12 +257,12 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
}
// reload repo from db to get a real state after creation
- repo, err = repo_model.GetRepositoryByID(repo.ID)
+ repo, err = repo_model.GetRepositoryByID(ctx, repo.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err)
}
- ctx.JSON(http.StatusCreated, convert.ToRepo(repo, perm.AccessModeOwner))
+ ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, perm.AccessModeOwner))
}
// Create one repository of mine
@@ -342,7 +342,7 @@ func Generate(ctx *context.APIContext) {
return
}
- opts := models.GenerateRepoOptions{
+ opts := repo_module.GenerateRepoOptions{
Name: form.Name,
DefaultBranch: form.DefaultBranch,
Description: form.Description,
@@ -363,7 +363,7 @@ func Generate(ctx *context.APIContext) {
ctxUser := ctx.Doer
var err error
if form.Owner != ctxUser.Name {
- ctxUser, err = user_model.GetUserByName(form.Owner)
+ ctxUser, err = user_model.GetUserByName(ctx, form.Owner)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.JSON(http.StatusNotFound, map[string]interface{}{
@@ -407,7 +407,7 @@ func Generate(ctx *context.APIContext) {
}
log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)
- ctx.JSON(http.StatusCreated, convert.ToRepo(repo, perm.AccessModeOwner))
+ ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, perm.AccessModeOwner))
}
// CreateOrgRepoDeprecated create one repository of the organization
@@ -523,7 +523,7 @@ func Get(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusOK, convert.ToRepo(ctx.Repo.Repository, ctx.Repo.AccessMode))
+ ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.AccessMode))
}
// GetByID returns a single Repository
@@ -544,7 +544,7 @@ func GetByID(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/Repository"
- repo, err := repo_model.GetRepositoryByID(ctx.ParamsInt64(":id"))
+ repo, err := repo_model.GetRepositoryByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.NotFound()
@@ -554,7 +554,7 @@ func GetByID(ctx *context.APIContext) {
return
}
- perm, err := models.GetUserRepoPermission(repo, ctx.Doer)
+ perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
return
@@ -562,7 +562,7 @@ func GetByID(ctx *context.APIContext) {
ctx.NotFound()
return
}
- ctx.JSON(http.StatusOK, convert.ToRepo(repo, perm.AccessMode))
+ ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, perm.AccessMode))
}
// Edit edit repository properties
@@ -583,7 +583,6 @@ func Edit(ctx *context.APIContext) {
// description: name of the repo to edit
// type: string
// required: true
- // required: true
// - name: body
// in: body
// description: "Properties of a repo that you can edit"
@@ -614,18 +613,18 @@ func Edit(ctx *context.APIContext) {
}
if opts.MirrorInterval != nil {
- if err := updateMirrorInterval(ctx, opts); err != nil {
+ if err := updateMirror(ctx, opts); err != nil {
return
}
}
- repo, err := repo_model.GetRepositoryByID(ctx.Repo.Repository.ID)
+ repo, err := repo_model.GetRepositoryByID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.InternalServerError(err)
return
}
- ctx.JSON(http.StatusOK, convert.ToRepo(repo, ctx.Repo.AccessMode))
+ ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, ctx.Repo.AccessMode))
}
// updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility
@@ -670,7 +669,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
if opts.Private != nil {
// Visibility of forked repository is forced sync with base repository.
if repo.IsFork {
- if err := repo.GetBaseRepo(); err != nil {
+ if err := repo.GetBaseRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "Unable to load base repository", err)
return err
}
@@ -715,7 +714,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
repo.DefaultBranch = *opts.DefaultBranch
}
- if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
+ if err := repo_service.UpdateRepository(repo, visibilityChanged); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateRepository", err)
return err
}
@@ -732,8 +731,13 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
var units []repo_model.RepoUnit
var deleteUnitTypes []unit_model.Type
+ currHasIssues := repo.UnitEnabled(ctx, unit_model.TypeIssues)
+ newHasIssues := currHasIssues
if opts.HasIssues != nil {
- if *opts.HasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
+ newHasIssues = *opts.HasIssues
+ }
+ if currHasIssues || newHasIssues {
+ if newHasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
// Check that values are valid
if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) {
err := fmt.Errorf("External tracker URL not valid")
@@ -750,13 +754,14 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
RepoID: repo.ID,
Type: unit_model.TypeExternalTracker,
Config: &repo_model.ExternalTrackerConfig{
- ExternalTrackerURL: opts.ExternalTracker.ExternalTrackerURL,
- ExternalTrackerFormat: opts.ExternalTracker.ExternalTrackerFormat,
- ExternalTrackerStyle: opts.ExternalTracker.ExternalTrackerStyle,
+ ExternalTrackerURL: opts.ExternalTracker.ExternalTrackerURL,
+ ExternalTrackerFormat: opts.ExternalTracker.ExternalTrackerFormat,
+ ExternalTrackerStyle: opts.ExternalTracker.ExternalTrackerStyle,
+ ExternalTrackerRegexpPattern: opts.ExternalTracker.ExternalTrackerRegexpPattern,
},
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
- } else if *opts.HasIssues && opts.ExternalTracker == nil && !unit_model.TypeIssues.UnitGlobalDisabled() {
+ } else if newHasIssues && opts.ExternalTracker == nil && !unit_model.TypeIssues.UnitGlobalDisabled() {
// Default to built-in tracker
var config *repo_model.IssuesConfig
@@ -766,7 +771,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime,
EnableDependencies: opts.InternalTracker.EnableIssueDependencies,
}
- } else if unit, err := repo.GetUnit(unit_model.TypeIssues); err != nil {
+ } else if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err != nil {
// Unit type doesn't exist so we make a new config file with default values
config = &repo_model.IssuesConfig{
EnableTimetracker: true,
@@ -783,7 +788,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
Config: config,
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
- } else if !*opts.HasIssues {
+ } else if !newHasIssues {
if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
}
@@ -793,8 +798,13 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
}
}
+ currHasWiki := repo.UnitEnabled(ctx, unit_model.TypeWiki)
+ newHasWiki := currHasWiki
if opts.HasWiki != nil {
- if *opts.HasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
+ newHasWiki = *opts.HasWiki
+ }
+ if currHasWiki || newHasWiki {
+ if newHasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
// Check that values are valid
if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) {
err := fmt.Errorf("External wiki URL not valid")
@@ -810,7 +820,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
},
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
- } else if *opts.HasWiki && opts.ExternalWiki == nil && !unit_model.TypeWiki.UnitGlobalDisabled() {
+ } else if newHasWiki && opts.ExternalWiki == nil && !unit_model.TypeWiki.UnitGlobalDisabled() {
config := &repo_model.UnitConfig{}
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
@@ -818,7 +828,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
Config: config,
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
- } else if !*opts.HasWiki {
+ } else if !newHasWiki {
if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
}
@@ -828,12 +838,17 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
}
}
+ currHasPullRequests := repo.UnitEnabled(ctx, unit_model.TypePullRequests)
+ newHasPullRequests := currHasPullRequests
if opts.HasPullRequests != nil {
- if *opts.HasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() {
+ newHasPullRequests = *opts.HasPullRequests
+ }
+ if currHasPullRequests || newHasPullRequests {
+ if newHasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() {
// We do allow setting individual PR settings through the API, so
// we get the config settings and then set them
// if those settings were provided in the opts.
- unit, err := repo.GetUnit(unit_model.TypePullRequests)
+ unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests)
var config *repo_model.PullRequestsConfig
if err != nil {
// Unit type doesn't exist so we make a new config file with default values
@@ -889,7 +904,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
Type: unit_model.TypePullRequests,
Config: config,
})
- } else if !*opts.HasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() {
+ } else if !newHasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
}
}
@@ -943,37 +958,67 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
return nil
}
-// updateMirrorInterval updates the repo's mirror Interval
-func updateMirrorInterval(ctx *context.APIContext, opts api.EditRepoOption) error {
+// updateMirror updates a repo's mirror Interval and EnablePrune
+func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error {
repo := ctx.Repo.Repository
+ // only update mirror if interval or enable prune are provided
+ if opts.MirrorInterval == nil && opts.EnablePrune == nil {
+ return nil
+ }
+
+ // these values only make sense if the repo is a mirror
+ if !repo.IsMirror {
+ err := fmt.Errorf("repo is not a mirror, can not change mirror interval")
+ ctx.Error(http.StatusUnprocessableEntity, err.Error(), err)
+ return err
+ }
+
+ // get the mirror from the repo
+ mirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
+ if err != nil {
+ log.Error("Failed to get mirror: %s", err)
+ ctx.Error(http.StatusInternalServerError, "MirrorInterval", err)
+ return err
+ }
+
+ // update MirrorInterval
if opts.MirrorInterval != nil {
- if !repo.IsMirror {
- err := fmt.Errorf("repo is not a mirror, can not change mirror interval")
- ctx.Error(http.StatusUnprocessableEntity, err.Error(), err)
- return err
- }
- mirror, err := repo_model.GetMirrorByRepoID(repo.ID)
+
+ // MirrorInterval should be a duration
+ interval, err := time.ParseDuration(*opts.MirrorInterval)
if err != nil {
- log.Error("Failed to get mirror: %s", err)
- ctx.Error(http.StatusInternalServerError, "MirrorInterval", err)
+ log.Error("Wrong format for MirrorInternal Sent: %s", err)
+ ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
return err
}
- if interval, err := time.ParseDuration(*opts.MirrorInterval); err == nil {
- mirror.Interval = interval
- mirror.Repo = repo
- if err := repo_model.UpdateMirror(mirror); err != nil {
- log.Error("Failed to Set Mirror Interval: %s", err)
- ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
- return err
- }
- log.Trace("Repository %s/%s Mirror Interval was Updated to %s", ctx.Repo.Owner.Name, repo.Name, interval)
- } else {
- log.Error("Wrong format for MirrorInternal Sent: %s", err)
+
+ // Ensure the provided duration is not too short
+ if interval != 0 && interval < setting.Mirror.MinInterval {
+ err := fmt.Errorf("invalid mirror interval: %s is below minimum interval: %s", interval, setting.Mirror.MinInterval)
ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
return err
}
+
+ mirror.Interval = interval
+ mirror.Repo = repo
+ mirror.ScheduleNextUpdate()
+ log.Trace("Repository %s Mirror[%d] Set Interval: %s NextUpdateUnix: %s", repo.FullName(), mirror.ID, interval, mirror.NextUpdateUnix)
+ }
+
+ // update EnablePrune
+ if opts.EnablePrune != nil {
+ mirror.EnablePrune = *opts.EnablePrune
+ log.Trace("Repository %s Mirror[%d] Set EnablePrune: %t", repo.FullName(), mirror.ID, mirror.EnablePrune)
}
+
+ // finally update the mirror in the DB
+ if err := repo_model.UpdateMirror(ctx, mirror); err != nil {
+ log.Error("Failed to Set Mirror Interval: %s", err)
+ ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err)
+ return err
+ }
+
return nil
}
@@ -1004,7 +1049,7 @@ func Delete(ctx *context.APIContext) {
owner := ctx.Repo.Owner
repo := ctx.Repo.Repository
- canDelete, err := models.CanUserDelete(repo, ctx.Doer)
+ canDelete, err := repo_module.CanUserDelete(repo, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CanUserDelete", err)
return
diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go
index 9acc0ee7d21b0..59c3bde81930e 100644
--- a/routers/api/v1/repo/repo_test.go
+++ b/routers/api/v1/repo/repo_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
diff --git a/routers/api/v1/repo/star.go b/routers/api/v1/repo/star.go
index c78c3cc512f29..c7b2eb01ff65f 100644
--- a/routers/api/v1/repo/star.go
+++ b/routers/api/v1/repo/star.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,9 +8,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// ListStargazers list a repository's stargazers
diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go
index f4c0ebd38cc57..5158f38e144e5 100644
--- a/routers/api/v1/repo/status.go
+++ b/routers/api/v1/repo/status.go
@@ -1,6 +1,5 @@
// Copyright 2017 Gitea. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
@@ -8,12 +7,12 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
files_service "code.gitea.io/gitea/services/repository/files"
)
@@ -56,8 +55,8 @@ func NewCommitStatus(ctx *context.APIContext) {
ctx.Error(http.StatusBadRequest, "sha not given", nil)
return
}
- status := &models.CommitStatus{
- State: api.CommitStatusState(form.State),
+ status := &git_model.CommitStatus{
+ State: form.State,
TargetURL: form.TargetURL,
Description: form.Description,
Context: form.Context,
@@ -67,7 +66,7 @@ func NewCommitStatus(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusCreated, convert.ToCommitStatus(status))
+ ctx.JSON(http.StatusCreated, convert.ToCommitStatus(ctx, status))
}
// GetCommitStatuses returns all statuses for any given commit hash
@@ -184,23 +183,24 @@ func getCommitStatuses(ctx *context.APIContext, sha string) {
ctx.Error(http.StatusBadRequest, "ref/sha not given", nil)
return
}
+ sha = utils.MustConvertToSHA1(ctx.Context, sha)
repo := ctx.Repo.Repository
listOptions := utils.GetListOptions(ctx)
- statuses, maxResults, err := models.GetCommitStatuses(repo, sha, &models.CommitStatusOptions{
+ statuses, maxResults, err := git_model.GetCommitStatuses(ctx, repo, sha, &git_model.CommitStatusOptions{
ListOptions: listOptions,
SortType: ctx.FormTrim("sort"),
State: ctx.FormTrim("state"),
})
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetCommitStatuses", fmt.Errorf("GetCommitStatuses[%s, %s, %d]: %v", repo.FullName(), sha, ctx.FormInt("page"), err))
+ ctx.Error(http.StatusInternalServerError, "GetCommitStatuses", fmt.Errorf("GetCommitStatuses[%s, %s, %d]: %w", repo.FullName(), sha, ctx.FormInt("page"), err))
return
}
apiStatuses := make([]*api.CommitStatus, 0, len(statuses))
for _, status := range statuses {
- apiStatuses = append(apiStatuses, convert.ToCommitStatus(status))
+ apiStatuses = append(apiStatuses, convert.ToCommitStatus(ctx, status))
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
@@ -253,9 +253,9 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) {
repo := ctx.Repo.Repository
- statuses, count, err := models.GetLatestCommitStatus(repo.ID, sha, utils.GetListOptions(ctx))
+ statuses, count, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, utils.GetListOptions(ctx))
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetLatestCommitStatus", fmt.Errorf("GetLatestCommitStatus[%s, %s]: %v", repo.FullName(), sha, err))
+ ctx.Error(http.StatusInternalServerError, "GetLatestCommitStatus", fmt.Errorf("GetLatestCommitStatus[%s, %s]: %w", repo.FullName(), sha, err))
return
}
@@ -264,7 +264,7 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) {
return
}
- combiStatus := convert.ToCombinedStatus(statuses, convert.ToRepo(repo, ctx.Repo.AccessMode))
+ combiStatus := convert.ToCombinedStatus(ctx, statuses, convert.ToRepo(ctx, repo, ctx.Repo.AccessMode))
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, combiStatus)
diff --git a/routers/api/v1/repo/subscriber.go b/routers/api/v1/repo/subscriber.go
index c1aaa241937d6..6cd369898eba0 100644
--- a/routers/api/v1/repo/subscriber.go
+++ b/routers/api/v1/repo/subscriber.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,9 +8,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// ListSubscribers list a repo's subscribers (i.e. watchers)
diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go
index 894291275400b..cb65e2b651b0e 100644
--- a/routers/api/v1/repo/tag.go
+++ b/routers/api/v1/repo/tag.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -10,11 +9,12 @@ import (
"net/http"
"code.gitea.io/gitea/models"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
releaseservice "code.gitea.io/gitea/services/release"
)
@@ -176,6 +176,8 @@ func CreateTag(ctx *context.APIContext) {
// "$ref": "#/responses/Tag"
// "404":
// "$ref": "#/responses/notFound"
+ // "405":
+ // "$ref": "#/responses/empty"
// "409":
// "$ref": "#/responses/conflict"
form := web.GetForm(ctx).(*api.CreateTagOption)
@@ -187,7 +189,7 @@ func CreateTag(ctx *context.APIContext) {
commit, err := ctx.Repo.GitRepo.GetCommit(form.Target)
if err != nil {
- ctx.Error(http.StatusNotFound, "target not found", fmt.Errorf("target not found: %v", err))
+ ctx.Error(http.StatusNotFound, "target not found", fmt.Errorf("target not found: %w", err))
return
}
@@ -196,6 +198,11 @@ func CreateTag(ctx *context.APIContext) {
ctx.Error(http.StatusConflict, "tag exist", err)
return
}
+ if models.IsErrProtectedTagName(err) {
+ ctx.Error(http.StatusMethodNotAllowed, "CreateNewTag", "user not allowed to create protected tag")
+ return
+ }
+
ctx.InternalServerError(err)
return
}
@@ -236,13 +243,15 @@ func DeleteTag(ctx *context.APIContext) {
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
+ // "405":
+ // "$ref": "#/responses/empty"
// "409":
// "$ref": "#/responses/conflict"
tagName := ctx.Params("*")
- tag, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
+ tag, err := repo_model.GetRelease(ctx.Repo.Repository.ID, tagName)
if err != nil {
- if models.IsErrReleaseNotExist(err) {
+ if repo_model.IsErrReleaseNotExist(err) {
ctx.NotFound()
return
}
@@ -256,7 +265,12 @@ func DeleteTag(ctx *context.APIContext) {
}
if err = releaseservice.DeleteReleaseByID(ctx, tag.ID, ctx.Doer, true); err != nil {
+ if models.IsErrProtectedTagName(err) {
+ ctx.Error(http.StatusMethodNotAllowed, "delTag", "user not allowed to delete protected tag")
+ return
+ }
ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err)
+ return
}
ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go
index 1e3ea326d3c7b..eafe4236ec3ac 100644
--- a/routers/api/v1/repo/teams.go
+++ b/routers/api/v1/repo/teams.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -11,8 +10,8 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
- api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/convert"
+ org_service "code.gitea.io/gitea/services/org"
)
// ListTeams list a repository's teams
@@ -42,20 +41,16 @@ func ListTeams(ctx *context.APIContext) {
return
}
- teams, err := organization.GetRepoTeams(ctx.Repo.Repository)
+ teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository)
if err != nil {
ctx.InternalServerError(err)
return
}
- apiTeams := make([]*api.Team, len(teams))
- for i := range teams {
- if err := teams[i].GetUnits(); err != nil {
- ctx.Error(http.StatusInternalServerError, "GetUnits", err)
- return
- }
-
- apiTeams[i] = convert.ToTeam(teams[i])
+ apiTeams, err := convert.ToTeams(teams, false)
+ if err != nil {
+ ctx.InternalServerError(err)
+ return
}
ctx.JSON(http.StatusOK, apiTeams)
@@ -103,11 +98,11 @@ func IsTeam(ctx *context.APIContext) {
}
if models.HasRepository(team, ctx.Repo.Repository.ID) {
- if err := team.GetUnits(); err != nil {
- ctx.Error(http.StatusInternalServerError, "GetUnits", err)
+ apiTeam, err := convert.ToTeam(team)
+ if err != nil {
+ ctx.InternalServerError(err)
return
}
- apiTeam := convert.ToTeam(team)
ctx.JSON(http.StatusOK, apiTeam)
return
}
@@ -204,7 +199,7 @@ func changeRepoTeam(ctx *context.APIContext, add bool) {
ctx.Error(http.StatusUnprocessableEntity, "alreadyAdded", fmt.Errorf("team '%s' is already added to repo", team.Name))
return
}
- err = models.AddRepository(team, ctx.Repo.Repository)
+ err = org_service.TeamAddRepository(team, ctx.Repo.Repository)
} else {
if !repoHasTeam {
ctx.Error(http.StatusUnprocessableEntity, "notAdded", fmt.Errorf("team '%s' was not added to repo", team.Name))
@@ -221,7 +216,7 @@ func changeRepoTeam(ctx *context.APIContext, add bool) {
}
func getTeamByParam(ctx *context.APIContext) *organization.Team {
- team, err := organization.GetTeam(ctx.Repo.Owner.ID, ctx.Params(":team"))
+ team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.Params(":team"))
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.Error(http.StatusNotFound, "TeamNotExit", err)
diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go
index 1cc2c50dc20d8..7d27fe7d25a0d 100644
--- a/routers/api/v1/repo/topic.go
+++ b/routers/api/v1/repo/topic.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -10,11 +9,11 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// ListTopics returns list of current topics for repo
@@ -240,6 +239,7 @@ func DeleteTopic(ctx *context.APIContext) {
if topic == nil {
ctx.NotFound()
+ return
}
ctx.Status(http.StatusNoContent)
diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go
index 7578fbd1873ce..aec398da7a416 100644
--- a/routers/api/v1/repo/transfer.go
+++ b/routers/api/v1/repo/transfer.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -14,10 +13,10 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
)
@@ -57,7 +56,7 @@ func Transfer(ctx *context.APIContext) {
opts := web.GetForm(ctx).(*api.TransferRepoOption)
- newOwner, err := user_model.GetUserByName(opts.NewOwner)
+ newOwner, err := user_model.GetUserByName(ctx, opts.NewOwner)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusNotFound, "", "The new owner does not exist or cannot be found")
@@ -84,7 +83,7 @@ func Transfer(ctx *context.APIContext) {
org := convert.ToOrganization(organization.OrgFromUser(newOwner))
for _, tID := range *opts.TeamIDs {
- team, err := organization.GetTeamByID(tID)
+ team, err := organization.GetTeamByID(ctx, tID)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "team", fmt.Errorf("team %d not found", tID))
return
@@ -104,14 +103,16 @@ func Transfer(ctx *context.APIContext) {
ctx.Repo.GitRepo = nil
}
- if err := repo_service.StartRepositoryTransfer(ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil {
+ oldFullname := ctx.Repo.Repository.FullName()
+
+ if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil {
if models.IsErrRepoTransferInProgress(err) {
- ctx.Error(http.StatusConflict, "CreatePendingRepositoryTransfer", err)
+ ctx.Error(http.StatusConflict, "StartRepositoryTransfer", err)
return
}
if repo_model.IsErrRepoAlreadyExist(err) {
- ctx.Error(http.StatusUnprocessableEntity, "CreatePendingRepositoryTransfer", err)
+ ctx.Error(http.StatusUnprocessableEntity, "StartRepositoryTransfer", err)
return
}
@@ -120,13 +121,13 @@ func Transfer(ctx *context.APIContext) {
}
if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
- log.Trace("Repository transfer initiated: %s -> %s", ctx.Repo.Repository.FullName(), newOwner.Name)
- ctx.JSON(http.StatusCreated, convert.ToRepo(ctx.Repo.Repository, perm.AccessModeAdmin))
+ log.Trace("Repository transfer initiated: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
+ ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, ctx.Repo.Repository, perm.AccessModeAdmin))
return
}
- log.Trace("Repository transferred: %s -> %s", ctx.Repo.Repository.FullName(), newOwner.Name)
- ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx.Repo.Repository, perm.AccessModeAdmin))
+ log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
+ ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, perm.AccessModeAdmin))
}
// AcceptTransfer accept a repo transfer
@@ -164,7 +165,7 @@ func AcceptTransfer(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx.Repo.Repository, ctx.Repo.AccessMode))
+ ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.AccessMode))
}
// RejectTransfer reject a repo transfer
@@ -202,11 +203,11 @@ func RejectTransfer(ctx *context.APIContext) {
return
}
- ctx.JSON(http.StatusOK, convert.ToRepo(ctx.Repo.Repository, ctx.Repo.AccessMode))
+ ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.AccessMode))
}
func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
- repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
+ repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
if err != nil {
if models.IsErrNoPendingTransfer(err) {
ctx.NotFound()
@@ -215,7 +216,7 @@ func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
return err
}
- if err := repoTransfer.LoadAttributes(); err != nil {
+ if err := repoTransfer.LoadAttributes(ctx); err != nil {
return err
}
@@ -225,7 +226,7 @@ func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
}
if accept {
- return repo_service.TransferOwnership(repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams)
+ return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams)
}
return models.CancelRepositoryTransfer(ctx.Repo.Repository)
diff --git a/routers/api/v1/repo/tree.go b/routers/api/v1/repo/tree.go
index b8294ebe312b0..9df96204cbef0 100644
--- a/routers/api/v1/repo/tree.go
+++ b/routers/api/v1/repo/tree.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go
index a3a5904925000..764530a671e6d 100644
--- a/routers/api/v1/repo/wiki.go
+++ b/routers/api/v1/repo/wiki.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -10,14 +9,15 @@ import (
"net/http"
"net/url"
- "code.gitea.io/gitea/models"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/convert"
wiki_service "code.gitea.io/gitea/services/wiki"
)
@@ -72,9 +72,9 @@ func NewWikiPage(ctx *context.APIContext) {
form.ContentBase64 = string(content)
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.ContentBase64, form.Message); err != nil {
- if models.IsErrWikiReservedName(err) {
+ if repo_model.IsErrWikiReservedName(err) {
ctx.Error(http.StatusBadRequest, "IsErrWikiReservedName", err)
- } else if models.IsErrWikiAlreadyExist(err) {
+ } else if repo_model.IsErrWikiAlreadyExist(err) {
ctx.Error(http.StatusBadRequest, "IsErrWikiAlreadyExists", err)
} else {
ctx.Error(http.StatusInternalServerError, "AddWikiPage", err)
@@ -85,6 +85,7 @@ func NewWikiPage(ctx *context.APIContext) {
wikiPage := getWikiPage(ctx, wikiName)
if !ctx.Written() {
+ notification.NotifyNewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Message)
ctx.JSON(http.StatusCreated, wikiPage)
}
}
@@ -152,6 +153,7 @@ func EditWikiPage(ctx *context.APIContext) {
wikiPage := getWikiPage(ctx, newWikiName)
if !ctx.Written() {
+ notification.NotifyEditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, newWikiName, form.Message)
ctx.JSON(http.StatusOK, wikiPage)
}
}
@@ -242,6 +244,8 @@ func DeleteWikiPage(ctx *context.APIContext) {
return
}
+ notification.NotifyDeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName)
+
ctx.Status(http.StatusNoContent)
}
@@ -314,7 +318,7 @@ func ListWikiPages(ctx *context.APIContext) {
}
wikiName, err := wiki_service.FilenameToName(entry.Name())
if err != nil {
- if models.IsErrWikiInvalidFileName(err) {
+ if repo_model.IsErrWikiInvalidFileName(err) {
continue
}
ctx.Error(http.StatusInternalServerError, "WikiFilenameToName", err)
@@ -427,9 +431,9 @@ func ListPageRevisions(ctx *context.APIContext) {
}
// get Commit Count
- commitsHistory, err := wikiRepo.CommitsByFileAndRangeNoFollow("master", pageFilename, page)
+ commitsHistory, err := wikiRepo.CommitsByFileAndRange("master", pageFilename, page)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "CommitsByFileAndRangeNoFollow", err)
+ ctx.Error(http.StatusInternalServerError, "CommitsByFileAndRange", err)
return
}
diff --git a/routers/api/v1/settings/settings.go b/routers/api/v1/settings/settings.go
index 36f93893a8800..02bda1309d729 100644
--- a/routers/api/v1/settings/settings.go
+++ b/routers/api/v1/settings/settings.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package settings
diff --git a/routers/api/v1/swagger/activitypub.go b/routers/api/v1/swagger/activitypub.go
new file mode 100644
index 0000000000000..91341669da902
--- /dev/null
+++ b/routers/api/v1/swagger/activitypub.go
@@ -0,0 +1,15 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package swagger
+
+import (
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ActivityPub
+// swagger:response ActivityPub
+type swaggerResponseActivityPub struct {
+ // in:body
+ Body api.ActivityPub `json:"body"`
+}
diff --git a/routers/api/v1/swagger/app.go b/routers/api/v1/swagger/app.go
index 9783abe1a082f..6a08b118745d7 100644
--- a/routers/api/v1/swagger/app.go
+++ b/routers/api/v1/swagger/app.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
diff --git a/routers/api/v1/swagger/cron.go b/routers/api/v1/swagger/cron.go
index 85f2ed0e3520a..00cfbe0adba22 100644
--- a/routers/api/v1/swagger/cron.go
+++ b/routers/api/v1/swagger/cron.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
diff --git a/routers/api/v1/swagger/issue.go b/routers/api/v1/swagger/issue.go
index 09e7077b200c1..62458a3424315 100644
--- a/routers/api/v1/swagger/issue.go
+++ b/routers/api/v1/swagger/issue.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
diff --git a/routers/api/v1/swagger/key.go b/routers/api/v1/swagger/key.go
index c3da37af636a5..8390833589660 100644
--- a/routers/api/v1/swagger/key.go
+++ b/routers/api/v1/swagger/key.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
diff --git a/routers/api/v1/swagger/misc.go b/routers/api/v1/swagger/misc.go
index 1d3c257efacb2..a4052a6a763ed 100644
--- a/routers/api/v1/swagger/misc.go
+++ b/routers/api/v1/swagger/misc.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
diff --git a/routers/api/v1/swagger/nodeinfo.go b/routers/api/v1/swagger/nodeinfo.go
index c1ecf3a3f36da..8650dfa092338 100644
--- a/routers/api/v1/swagger/nodeinfo.go
+++ b/routers/api/v1/swagger/nodeinfo.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
diff --git a/routers/api/v1/swagger/notify.go b/routers/api/v1/swagger/notify.go
index cd30d496e0dc1..743d807a0a919 100644
--- a/routers/api/v1/swagger/notify.go
+++ b/routers/api/v1/swagger/notify.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 2bd43c6180870..979b18407590c 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
@@ -107,9 +106,6 @@ type swaggerParameterBodies struct {
// in:body
EditUserOption api.EditUserOption
- // in:body
- MigrateRepoForm forms.MigrateRepoForm
-
// in:body
EditAttachmentOptions api.EditAttachmentOptions
@@ -172,4 +168,7 @@ type swaggerParameterBodies struct {
// in:body
CreateWikiPageOptions api.CreateWikiPageOptions
+
+ // in:body
+ CreatePushMirrorOption api.CreatePushMirrorOption
}
diff --git a/routers/api/v1/swagger/org.go b/routers/api/v1/swagger/org.go
index d98e821ba744e..0105446b00a33 100644
--- a/routers/api/v1/swagger/org.go
+++ b/routers/api/v1/swagger/org.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
diff --git a/routers/api/v1/swagger/package.go b/routers/api/v1/swagger/package.go
index 2a1f057314775..eada12d1eae6f 100644
--- a/routers/api/v1/swagger/package.go
+++ b/routers/api/v1/swagger/package.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go
index 40aeca677de83..bd867213a62da 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
@@ -254,6 +253,28 @@ type swaggerCommitList struct {
Body []api.Commit `json:"body"`
}
+// ChangedFileList
+// swagger:response ChangedFileList
+type swaggerChangedFileList struct {
+ // The current page
+ Page int `json:"X-Page"`
+
+ // Commits per page
+ PerPage int `json:"X-PerPage"`
+
+ // Total commit count
+ Total int `json:"X-Total"`
+
+ // Total number of pages
+ PageCount int `json:"X-PageCount"`
+
+ // True if there is another page
+ HasMore bool `json:"X-HasMore"`
+
+ // in: body
+ Body []api.ChangedFile `json:"body"`
+}
+
// Note
// swagger:response Note
type swaggerNote struct {
@@ -344,3 +365,24 @@ type swaggerWikiCommitList struct {
// in:body
Body api.WikiCommitList `json:"body"`
}
+
+// PushMirror
+// swagger:response PushMirror
+type swaggerPushMirror struct {
+ // in:body
+ Body api.PushMirror `json:"body"`
+}
+
+// PushMirrorList
+// swagger:response PushMirrorList
+type swaggerPushMirrorList struct {
+ // in:body
+ Body []api.PushMirror `json:"body"`
+}
+
+// RepoCollaboratorPermission
+// swagger:response RepoCollaboratorPermission
+type swaggerRepoCollaboratorPermission struct {
+ // in:body
+ Body api.RepoCollaboratorPermission `json:"body"`
+}
diff --git a/routers/api/v1/swagger/settings.go b/routers/api/v1/swagger/settings.go
index 4bf153cb9c523..a9466699df3f4 100644
--- a/routers/api/v1/swagger/settings.go
+++ b/routers/api/v1/swagger/settings.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
diff --git a/routers/api/v1/swagger/user.go b/routers/api/v1/swagger/user.go
index a4d52012367a5..fb6d185ee7de3 100644
--- a/routers/api/v1/swagger/user.go
+++ b/routers/api/v1/swagger/user.go
@@ -1,11 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package swagger
import (
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
api "code.gitea.io/gitea/modules/structs"
)
@@ -40,7 +39,7 @@ type swaggerModelEditUserOption struct {
// swagger:response UserHeatmapData
type swaggerResponseUserHeatmapData struct {
// in:body
- Body []models.UserHeatmapData `json:"body"`
+ Body []activities_model.UserHeatmapData `json:"body"`
}
// UserSettings
diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go
index 165b8f005ecca..7b2f0d8c30d2e 100644
--- a/routers/api/v1/user/app.go
+++ b/routers/api/v1/user/app.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package user
@@ -11,13 +10,12 @@ import (
"net/http"
"strconv"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/auth"
+ auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// ListAccessTokens list all the access tokens
@@ -45,14 +43,14 @@ func ListAccessTokens(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/AccessTokenList"
- opts := models.ListAccessTokensOptions{UserID: ctx.Doer.ID, ListOptions: utils.GetListOptions(ctx)}
+ opts := auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID, ListOptions: utils.GetListOptions(ctx)}
- count, err := models.CountAccessTokens(opts)
+ count, err := auth_model.CountAccessTokens(opts)
if err != nil {
ctx.InternalServerError(err)
return
}
- tokens, err := models.ListAccessTokens(opts)
+ tokens, err := auth_model.ListAccessTokens(opts)
if err != nil {
ctx.InternalServerError(err)
return
@@ -98,12 +96,12 @@ func CreateAccessToken(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateAccessTokenOption)
- t := &models.AccessToken{
+ t := &auth_model.AccessToken{
UID: ctx.Doer.ID,
Name: form.Name,
}
- exist, err := models.AccessTokenByNameExists(t)
+ exist, err := auth_model.AccessTokenByNameExists(t)
if err != nil {
ctx.InternalServerError(err)
return
@@ -113,7 +111,7 @@ func CreateAccessToken(ctx *context.APIContext) {
return
}
- if err := models.NewAccessToken(t); err != nil {
+ if err := auth_model.NewAccessToken(t); err != nil {
ctx.Error(http.StatusInternalServerError, "NewAccessToken", err)
return
}
@@ -155,7 +153,7 @@ func DeleteAccessToken(ctx *context.APIContext) {
tokenID, _ := strconv.ParseInt(token, 0, 64)
if tokenID == 0 {
- tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{
+ tokens, err := auth_model.ListAccessTokens(auth_model.ListAccessTokensOptions{
Name: token,
UserID: ctx.Doer.ID,
})
@@ -180,8 +178,8 @@ func DeleteAccessToken(ctx *context.APIContext) {
return
}
- if err := models.DeleteAccessTokenByID(tokenID, ctx.Doer.ID); err != nil {
- if models.IsErrAccessTokenNotExist(err) {
+ if err := auth_model.DeleteAccessTokenByID(tokenID, ctx.Doer.ID); err != nil {
+ if auth_model.IsErrAccessTokenNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "DeleteAccessTokenByID", err)
@@ -213,10 +211,11 @@ func CreateOauth2Application(ctx *context.APIContext) {
data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions)
- app, err := auth.CreateOAuth2Application(auth.CreateOAuth2ApplicationOptions{
- Name: data.Name,
- UserID: ctx.Doer.ID,
- RedirectURIs: data.RedirectURIs,
+ app, err := auth_model.CreateOAuth2Application(ctx, auth_model.CreateOAuth2ApplicationOptions{
+ Name: data.Name,
+ UserID: ctx.Doer.ID,
+ RedirectURIs: data.RedirectURIs,
+ ConfidentialClient: data.ConfidentialClient,
})
if err != nil {
ctx.Error(http.StatusBadRequest, "", "error creating oauth2 application")
@@ -252,7 +251,7 @@ func ListOauth2Applications(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/OAuth2ApplicationList"
- apps, total, err := auth.ListOAuth2Applications(ctx.Doer.ID, utils.GetListOptions(ctx))
+ apps, total, err := auth_model.ListOAuth2Applications(ctx.Doer.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "ListOAuth2Applications", err)
return
@@ -288,8 +287,8 @@ func DeleteOauth2Application(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
appID := ctx.ParamsInt64(":id")
- if err := auth.DeleteOAuth2Application(appID, ctx.Doer.ID); err != nil {
- if auth.IsErrOAuthApplicationNotFound(err) {
+ if err := auth_model.DeleteOAuth2Application(appID, ctx.Doer.ID); err != nil {
+ if auth_model.IsErrOAuthApplicationNotFound(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "DeleteOauth2ApplicationByID", err)
@@ -320,9 +319,9 @@ func GetOauth2Application(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
appID := ctx.ParamsInt64(":id")
- app, err := auth.GetOAuth2ApplicationByID(appID)
+ app, err := auth_model.GetOAuth2ApplicationByID(ctx, appID)
if err != nil {
- if auth.IsErrOauthClientIDInvalid(err) || auth.IsErrOAuthApplicationNotFound(err) {
+ if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetOauth2ApplicationByID", err)
@@ -363,14 +362,15 @@ func UpdateOauth2Application(ctx *context.APIContext) {
data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions)
- app, err := auth.UpdateOAuth2Application(auth.UpdateOAuth2ApplicationOptions{
- Name: data.Name,
- UserID: ctx.Doer.ID,
- ID: appID,
- RedirectURIs: data.RedirectURIs,
+ app, err := auth_model.UpdateOAuth2Application(auth_model.UpdateOAuth2ApplicationOptions{
+ Name: data.Name,
+ UserID: ctx.Doer.ID,
+ ID: appID,
+ RedirectURIs: data.RedirectURIs,
+ ConfidentialClient: data.ConfidentialClient,
})
if err != nil {
- if auth.IsErrOauthClientIDInvalid(err) || auth.IsErrOAuthApplicationNotFound(err) {
+ if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "UpdateOauth2ApplicationByID", err)
diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go
index 9060741c5932c..fc74c8d148460 100644
--- a/routers/api/v1/user/email.go
+++ b/routers/api/v1/user/email.go
@@ -1,6 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package user
@@ -10,10 +9,10 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/convert"
)
// ListEmails list all of the authenticated user's email addresses
@@ -48,11 +47,6 @@ func AddEmail(ctx *context.APIContext) {
// produces:
// - application/json
// parameters:
- // - name: options
- // in: body
- // schema:
- // "$ref": "#/definitions/CreateEmailOption"
- // parameters:
// - name: body
// in: body
// schema:
@@ -80,9 +74,16 @@ func AddEmail(ctx *context.APIContext) {
if err := user_model.AddEmailAddresses(emails); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email)
- } else if user_model.IsErrEmailCharIsNotSupported(err) ||
- user_model.IsErrEmailInvalid(err) {
- errMsg := fmt.Sprintf("Email address %s invalid", err.(user_model.ErrEmailInvalid).Email)
+ } else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) {
+ email := ""
+ if typedError, ok := err.(user_model.ErrEmailInvalid); ok {
+ email = typedError.Email
+ }
+ if typedError, ok := err.(user_model.ErrEmailCharIsNotSupported); ok {
+ email = typedError.Email
+ }
+
+ errMsg := fmt.Sprintf("Email address %q invalid", email)
ctx.Error(http.StatusUnprocessableEntity, "", errMsg)
} else {
ctx.Error(http.StatusInternalServerError, "AddEmailAddresses", err)
diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go
index 3c81b27f8dad4..e9d4ae478b3d2 100644
--- a/routers/api/v1/user/follower.go
+++ b/routers/api/v1/user/follower.go
@@ -1,7 +1,6 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package user
@@ -10,9 +9,9 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
func responseAPIUsers(ctx *context.APIContext, users []*user_model.User) {
@@ -24,13 +23,13 @@ func responseAPIUsers(ctx *context.APIContext, users []*user_model.User) {
}
func listUserFollowers(ctx *context.APIContext, u *user_model.User) {
- users, err := user_model.GetUserFollowers(u, utils.GetListOptions(ctx))
+ users, count, err := user_model.GetUserFollowers(ctx, u, ctx.Doer, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserFollowers", err)
return
}
- ctx.SetTotalCountHeader(int64(u.NumFollowers))
+ ctx.SetTotalCountHeader(count)
responseAPIUsers(ctx, users)
}
@@ -86,13 +85,13 @@ func ListFollowers(ctx *context.APIContext) {
}
func listUserFollowing(ctx *context.APIContext, u *user_model.User) {
- users, err := user_model.GetUserFollowing(u, utils.GetListOptions(ctx))
+ users, count, err := user_model.GetUserFollowing(ctx, u, ctx.Doer, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserFollowing", err)
return
}
- ctx.SetTotalCountHeader(int64(u.NumFollowing))
+ ctx.SetTotalCountHeader(count)
responseAPIUsers(ctx, users)
}
diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go
index b211a24a0e0d4..84327cc92a437 100644
--- a/routers/api/v1/user/gpg_key.go
+++ b/routers/api/v1/user/gpg_key.go
@@ -1,20 +1,20 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
import (
"fmt"
"net/http"
+ "strings"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
func listGPGKeys(ctx *context.APIContext, uid int64, listOptions db.ListOptions) {
@@ -177,6 +177,12 @@ func VerifyUserGPGKey(ctx *context.APIContext) {
token := asymkey_model.VerificationToken(ctx.Doer, 1)
lastToken := asymkey_model.VerificationToken(ctx.Doer, 0)
+ form.KeyID = strings.TrimLeft(form.KeyID, "0")
+ if form.KeyID == "" {
+ ctx.NotFound()
+ return
+ }
+
_, err := asymkey_model.VerifyGPGKey(ctx.Doer.ID, form.KeyID, token, form.Signature)
if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
_, err = asymkey_model.VerifyGPGKey(ctx.Doer.ID, form.KeyID, lastToken, form.Signature)
diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go
index fab3ce2ae5394..28f600ad92a23 100644
--- a/routers/api/v1/user/helper.go
+++ b/routers/api/v1/user/helper.go
@@ -1,6 +1,5 @@
// Copyright 2021 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package user
@@ -14,7 +13,7 @@ import (
// GetUserByParamsName get user by name
func GetUserByParamsName(ctx *context.APIContext, name string) *user_model.User {
username := ctx.Params(name)
- user, err := user_model.GetUserByName(username)
+ user, err := user_model.GetUserByName(ctx, username)
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err2 := user_model.LookupUserRedirect(username); err2 == nil {
diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go
index cc7ee739ce3a8..8aad69884fd8b 100644
--- a/routers/api/v1/user/key.go
+++ b/routers/api/v1/user/key.go
@@ -1,6 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package user
@@ -8,16 +7,17 @@ import (
"net/http"
asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/repo"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/convert"
)
// appendPrivateInformation appends the owner and key type information to api.PublicKey
@@ -30,7 +30,7 @@ func appendPrivateInformation(apiKey *api.PublicKey, key *asymkey_model.PublicKe
if defaultUser.ID == key.OwnerID {
apiKey.Owner = convert.ToUser(defaultUser, defaultUser)
} else {
- user, err := user_model.GetUserByID(key.OwnerID)
+ user, err := user_model.GetUserByID(db.DefaultContext, key.OwnerID)
if err != nil {
return apiKey, err
}
@@ -262,16 +262,21 @@ func DeletePublicKey(ctx *context.APIContext) {
id := ctx.ParamsInt64(":id")
externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(id)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "PublicKeyIsExternallyManaged", err)
+ if asymkey_model.IsErrKeyNotExist(err) {
+ ctx.NotFound()
+ } else {
+ ctx.Error(http.StatusInternalServerError, "PublicKeyIsExternallyManaged", err)
+ }
+ return
}
+
if externallyManaged {
ctx.Error(http.StatusForbidden, "", "SSH Key is externally managed for this user")
+ return
}
if err := asymkey_service.DeletePublicKey(ctx.Doer, id); err != nil {
- if asymkey_model.IsErrKeyNotExist(err) {
- ctx.NotFound()
- } else if asymkey_model.IsErrKeyAccessDenied(err) {
+ if asymkey_model.IsErrKeyAccessDenied(err) {
ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
} else {
ctx.Error(http.StatusInternalServerError, "DeletePublicKey", err)
diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go
index 0231c8ccbcb9f..d566c072fba77 100644
--- a/routers/api/v1/user/repo.go
+++ b/routers/api/v1/user/repo.go
@@ -1,26 +1,26 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
import (
"net/http"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// listUserRepos - List the repositories owned by the given user.
func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
opts := utils.GetListOptions(ctx)
- repos, count, err := models.GetUserRepositories(&models.SearchRepoOptions{
+ repos, count, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{
Actor: u,
Private: private,
ListOptions: opts,
@@ -38,13 +38,13 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
apiRepos := make([]*api.Repository, 0, len(repos))
for i := range repos {
- access, err := models.AccessLevel(ctx.Doer, repos[i])
+ access, err := access_model.AccessLevel(ctx, ctx.Doer, repos[i])
if err != nil {
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
return
}
if ctx.IsSigned && ctx.Doer.IsAdmin || access >= perm.AccessModeRead {
- apiRepos = append(apiRepos, convert.ToRepo(repos[i], access))
+ apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], access))
}
}
@@ -102,7 +102,7 @@ func ListMyRepos(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/RepositoryList"
- opts := &models.SearchRepoOptions{
+ opts := &repo_model.SearchRepoOptions{
ListOptions: utils.GetListOptions(ctx),
Actor: ctx.Doer,
OwnerID: ctx.Doer.ID,
@@ -111,7 +111,7 @@ func ListMyRepos(ctx *context.APIContext) {
}
var err error
- repos, count, err := models.SearchRepository(opts)
+ repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError, "SearchRepository", err)
return
@@ -123,11 +123,11 @@ func ListMyRepos(ctx *context.APIContext) {
ctx.Error(http.StatusInternalServerError, "GetOwner", err)
return
}
- accessMode, err := models.AccessLevel(ctx.Doer, repo)
+ accessMode, err := access_model.AccessLevel(ctx, ctx.Doer, repo)
if err != nil {
ctx.Error(http.StatusInternalServerError, "AccessLevel", err)
}
- results[i] = convert.ToRepo(repo, accessMode)
+ results[i] = convert.ToRepo(ctx, repo, accessMode)
}
ctx.SetLinkHeader(int(count), opts.ListOptions.PageSize)
diff --git a/routers/api/v1/user/settings.go b/routers/api/v1/user/settings.go
index dc7e7f11607eb..53794c82f8385 100644
--- a/routers/api/v1/user/settings.go
+++ b/routers/api/v1/user/settings.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
@@ -9,9 +8,9 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/convert"
)
// GetUserSettings returns user settings
@@ -74,7 +73,7 @@ func UpdateUserSettings(ctx *context.APIContext) {
ctx.Doer.KeepActivityPrivate = *form.HideActivity
}
- if err := user_model.UpdateUser(ctx.Doer, false); err != nil {
+ if err := user_model.UpdateUser(ctx, ctx.Doer, false); err != nil {
ctx.InternalServerError(err)
return
}
diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go
index cdbc35471b696..ad5a8bee33be8 100644
--- a/routers/api/v1/user/star.go
+++ b/routers/api/v1/user/star.go
@@ -1,38 +1,38 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package user
import (
+ std_context "context"
"net/http"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// getStarredRepos returns the repos that the user with the specified userID has
// starred
-func getStarredRepos(user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) {
- starredRepos, err := repo_model.GetStarredRepos(user.ID, private, listOptions)
+func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) {
+ starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions)
if err != nil {
return nil, err
}
repos := make([]*api.Repository, len(starredRepos))
for i, starred := range starredRepos {
- access, err := models.AccessLevel(user, starred)
+ access, err := access_model.AccessLevel(ctx, user, starred)
if err != nil {
return nil, err
}
- repos[i] = convert.ToRepo(starred, access)
+ repos[i] = convert.ToRepo(ctx, starred, access)
}
return repos, nil
}
@@ -63,7 +63,7 @@ func GetStarredRepos(ctx *context.APIContext) {
// "$ref": "#/responses/RepositoryList"
private := ctx.ContextUser.ID == ctx.Doer.ID
- repos, err := getStarredRepos(ctx.ContextUser, private, utils.GetListOptions(ctx))
+ repos, err := getStarredRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
return
@@ -93,7 +93,7 @@ func GetMyStarredRepos(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/RepositoryList"
- repos, err := getStarredRepos(ctx.Doer, true, utils.GetListOptions(ctx))
+ repos, err := getStarredRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "getStarredRepos", err)
}
@@ -124,7 +124,7 @@ func IsStarring(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- if repo_model.IsStaring(ctx.Doer.ID, ctx.Repo.Repository.ID) {
+ if repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) {
ctx.Status(http.StatusNoContent)
} else {
ctx.NotFound()
diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go
index 018f75762fbbd..55f3df40b9cca 100644
--- a/routers/api/v1/user/user.go
+++ b/routers/api/v1/user/user.go
@@ -1,18 +1,17 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package user
import (
"net/http"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// Search search users
@@ -98,7 +97,7 @@ func GetInfo(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- if !user_model.IsUserVisibleToViewer(ctx.ContextUser, ctx.Doer) {
+ if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
// fake ErrUserNotExist error message to not leak information about existence
ctx.NotFound("GetUserByName", user_model.ErrUserNotExist{Name: ctx.Params(":username")})
return
@@ -139,7 +138,7 @@ func GetUserHeatmapData(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- heatmap, err := models.GetUserHeatmapDataByUser(ctx.ContextUser, ctx.Doer)
+ heatmap, err := activities_model.GetUserHeatmapDataByUser(ctx.ContextUser, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserHeatmapDataByUser", err)
return
diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go
index e7c6837cb83e3..211f36459a83c 100644
--- a/routers/api/v1/user/watch.go
+++ b/routers/api/v1/user/watch.go
@@ -1,36 +1,36 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package user
import (
+ std_context "context"
"net/http"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
+ "code.gitea.io/gitea/services/convert"
)
// getWatchedRepos returns the repos that the user with the specified userID is watching
-func getWatchedRepos(user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) {
- watchedRepos, total, err := repo_model.GetWatchedRepos(user.ID, private, listOptions)
+func getWatchedRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, int64, error) {
+ watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, user.ID, private, listOptions)
if err != nil {
return nil, 0, err
}
repos := make([]*api.Repository, len(watchedRepos))
for i, watched := range watchedRepos {
- access, err := models.AccessLevel(user, watched)
+ access, err := access_model.AccessLevel(ctx, user, watched)
if err != nil {
return nil, 0, err
}
- repos[i] = convert.ToRepo(watched, access)
+ repos[i] = convert.ToRepo(ctx, watched, access)
}
return repos, total, nil
}
@@ -61,7 +61,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
// "$ref": "#/responses/RepositoryList"
private := ctx.ContextUser.ID == ctx.Doer.ID
- repos, total, err := getWatchedRepos(ctx.ContextUser, private, utils.GetListOptions(ctx))
+ repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
}
@@ -90,7 +90,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/RepositoryList"
- repos, total, err := getWatchedRepos(ctx.Doer, true, utils.GetListOptions(ctx))
+ repos, total, err := getWatchedRepos(ctx, ctx.Doer, true, utils.GetListOptions(ctx))
if err != nil {
ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err)
}
@@ -156,7 +156,7 @@ func Watch(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/WatchInfo"
- err := repo_model.WatchRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+ err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
if err != nil {
ctx.Error(http.StatusInternalServerError, "WatchRepo", err)
return
@@ -191,7 +191,7 @@ func Unwatch(ctx *context.APIContext) {
// "204":
// "$ref": "#/responses/empty"
- err := repo_model.WatchRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+ err := repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
if err != nil {
ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err)
return
diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go
index 9f02bc8083f9f..eaf0f5fd37fce 100644
--- a/routers/api/v1/utils/git.go
+++ b/routers/api/v1/utils/git.go
@@ -1,14 +1,15 @@
// 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.
+// SPDX-License-Identifier: MIT
package utils
import (
+ "fmt"
"net/http"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
)
// ResolveRefOrSha resolve ref to sha if exist
@@ -18,6 +19,7 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string {
return ""
}
+ sha := ref
// Search branches and tags
for _, refType := range []string{"heads", "tags"} {
refSHA, lastMethodName, err := searchRefCommitByType(ctx, refType, ref)
@@ -26,21 +28,27 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string {
return ""
}
if refSHA != "" {
- return refSHA
+ sha = refSHA
+ break
}
}
- return ref
+
+ sha = MustConvertToSHA1(ctx.Context, sha)
+
+ if ctx.Repo.GitRepo != nil {
+ err := ctx.Repo.GitRepo.AddLastCommitCache(ctx.Repo.Repository.GetCommitsCountCacheKey(ref, ref != sha), ctx.Repo.Repository.FullName(), sha)
+ if err != nil {
+ log.Error("Unable to get commits count for %s in %s. Error: %v", sha, ctx.Repo.Repository.FullName(), err)
+ }
+ }
+
+ return sha
}
// GetGitRefs return git references based on filter
func GetGitRefs(ctx *context.APIContext, filter string) ([]*git.Reference, string, error) {
if ctx.Repo.GitRepo == nil {
- var err error
- ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
- if err != nil {
- return nil, "OpenRepository", err
- }
- defer ctx.Repo.GitRepo.Close()
+ return nil, "", fmt.Errorf("no open git repo found in context")
}
if len(filter) > 0 {
filter = "refs/" + filter
@@ -59,3 +67,30 @@ func searchRefCommitByType(ctx *context.APIContext, refType, filter string) (str
}
return "", "", nil
}
+
+// ConvertToSHA1 returns a full-length SHA1 from a potential ID string
+func ConvertToSHA1(ctx *context.Context, commitID string) (git.SHA1, error) {
+ if len(commitID) == git.SHAFullLength && git.IsValidSHAPattern(commitID) {
+ sha1, err := git.NewIDFromString(commitID)
+ if err == nil {
+ return sha1, nil
+ }
+ }
+
+ gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, ctx.Repo.Repository.RepoPath())
+ if err != nil {
+ return git.SHA1{}, fmt.Errorf("RepositoryFromContextOrOpen: %w", err)
+ }
+ defer closer.Close()
+
+ return gitRepo.ConvertToSHA1(commitID)
+}
+
+// MustConvertToSHA1 returns a full-length SHA1 string from a potential ID string, or returns origin input if it can't convert to SHA1
+func MustConvertToSHA1(ctx *context.Context, commitID string) string {
+ sha, err := ConvertToSHA1(ctx, commitID)
+ if err != nil {
+ return commitID
+ }
+ return sha.String()
+}
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index 4c3753231d589..44fba22b5afed 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -1,6 +1,5 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package utils
@@ -11,11 +10,10 @@ import (
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/routers/utils"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
webhook_service "code.gitea.io/gitea/services/webhook"
)
@@ -73,26 +71,47 @@ func CheckCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption)
func AddOrgHook(ctx *context.APIContext, form *api.CreateHookOption) {
org := ctx.Org.Organization
hook, ok := addHook(ctx, form, org.ID, 0)
- if ok {
- ctx.JSON(http.StatusCreated, convert.ToHook(org.AsUser().HomeLink(), hook))
+ if !ok {
+ return
+ }
+ apiHook, ok := toAPIHook(ctx, org.AsUser().HomeLink(), hook)
+ if !ok {
+ return
}
+ ctx.JSON(http.StatusCreated, apiHook)
}
// AddRepoHook add a hook to a repo. Writes to `ctx` accordingly
func AddRepoHook(ctx *context.APIContext, form *api.CreateHookOption) {
repo := ctx.Repo
hook, ok := addHook(ctx, form, 0, repo.Repository.ID)
- if ok {
- ctx.JSON(http.StatusCreated, convert.ToHook(repo.RepoLink, hook))
+ if !ok {
+ return
+ }
+ apiHook, ok := toAPIHook(ctx, repo.RepoLink, hook)
+ if !ok {
+ return
+ }
+ ctx.JSON(http.StatusCreated, apiHook)
+}
+
+// toAPIHook converts the hook to its API representation.
+// If there is an error, write to `ctx` accordingly. Return (hook, ok)
+func toAPIHook(ctx *context.APIContext, repoLink string, hook *webhook.Webhook) (*api.Hook, bool) {
+ apiHook, err := webhook_service.ToHook(repoLink, hook)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "ToHook", err)
+ return nil, false
}
+ return apiHook, true
}
func issuesHook(events []string, event string) bool {
- return util.IsStringInSlice(event, events, true) || util.IsStringInSlice(string(webhook.HookEventIssues), events, true)
+ return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventIssues), true)
}
func pullHook(events []string, event string) bool {
- return util.IsStringInSlice(event, events, true) || util.IsStringInSlice(string(webhook.HookEventPullRequest), events, true)
+ return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true)
}
// addHook add the hook specified by `form`, `orgID` and `repoID`. If there is
@@ -108,47 +127,54 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID
ContentType: webhook.ToHookContentType(form.Config["content_type"]),
Secret: form.Config["secret"],
HTTPMethod: "POST",
- HookEvent: &webhook.HookEvent{
+ HookEvent: &webhook_module.HookEvent{
ChooseEvents: true,
- HookEvents: webhook.HookEvents{
- Create: util.IsStringInSlice(string(webhook.HookEventCreate), form.Events, true),
- Delete: util.IsStringInSlice(string(webhook.HookEventDelete), form.Events, true),
- Fork: util.IsStringInSlice(string(webhook.HookEventFork), form.Events, true),
+ HookEvents: webhook_module.HookEvents{
+ Create: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true),
+ Delete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true),
+ Fork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true),
Issues: issuesHook(form.Events, "issues_only"),
- IssueAssign: issuesHook(form.Events, string(webhook.HookEventIssueAssign)),
- IssueLabel: issuesHook(form.Events, string(webhook.HookEventIssueLabel)),
- IssueMilestone: issuesHook(form.Events, string(webhook.HookEventIssueMilestone)),
- IssueComment: issuesHook(form.Events, string(webhook.HookEventIssueComment)),
- Push: util.IsStringInSlice(string(webhook.HookEventPush), form.Events, true),
+ IssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)),
+ IssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)),
+ IssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)),
+ IssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)),
+ Push: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true),
PullRequest: pullHook(form.Events, "pull_request_only"),
- PullRequestAssign: pullHook(form.Events, string(webhook.HookEventPullRequestAssign)),
- PullRequestLabel: pullHook(form.Events, string(webhook.HookEventPullRequestLabel)),
- PullRequestMilestone: pullHook(form.Events, string(webhook.HookEventPullRequestMilestone)),
- PullRequestComment: pullHook(form.Events, string(webhook.HookEventPullRequestComment)),
+ PullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)),
+ PullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)),
+ PullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)),
+ PullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)),
PullRequestReview: pullHook(form.Events, "pull_request_review"),
- PullRequestSync: pullHook(form.Events, string(webhook.HookEventPullRequestSync)),
- Repository: util.IsStringInSlice(string(webhook.HookEventRepository), form.Events, true),
- Release: util.IsStringInSlice(string(webhook.HookEventRelease), form.Events, true),
+ PullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)),
+ Wiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
+ Repository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
+ Release: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
},
BranchFilter: form.BranchFilter,
},
IsActive: form.Active,
- Type: webhook.HookType(form.Type),
+ Type: form.Type,
}
- if w.Type == webhook.SLACK {
+ err := w.SetHeaderAuthorization(form.AuthorizationHeader)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "SetHeaderAuthorization", err)
+ return nil, false
+ }
+ if w.Type == webhook_module.SLACK {
channel, ok := form.Config["channel"]
if !ok {
ctx.Error(http.StatusUnprocessableEntity, "", "Missing config option: channel")
return nil, false
}
+ channel = strings.TrimSpace(channel)
- if !utils.IsValidSlackChannel(channel) {
+ if !webhook_service.IsValidSlackChannel(channel) {
ctx.Error(http.StatusBadRequest, "", "Invalid slack channel name")
return nil, false
}
meta, err := json.Marshal(&webhook_service.SlackMeta{
- Channel: strings.TrimSpace(channel),
+ Channel: channel,
Username: form.Config["username"],
IconURL: form.Config["icon_url"],
Color: form.Config["color"],
@@ -184,7 +210,11 @@ func EditOrgHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64
if err != nil {
return
}
- ctx.JSON(http.StatusOK, convert.ToHook(org.AsUser().HomeLink(), updated))
+ apiHook, ok := toAPIHook(ctx, org.AsUser().HomeLink(), updated)
+ if !ok {
+ return
+ }
+ ctx.JSON(http.StatusOK, apiHook)
}
// EditRepoHook edit webhook `w` according to `form`. Writes to `ctx` accordingly
@@ -201,7 +231,11 @@ func EditRepoHook(ctx *context.APIContext, form *api.EditHookOption, hookID int6
if err != nil {
return
}
- ctx.JSON(http.StatusOK, convert.ToHook(repo.RepoLink, updated))
+ apiHook, ok := toAPIHook(ctx, repo.RepoLink, updated)
+ if !ok {
+ return
+ }
+ ctx.JSON(http.StatusOK, apiHook)
}
// editHook edit the webhook `w` according to `form`. If an error occurs, write
@@ -219,7 +253,7 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
w.ContentType = webhook.ToHookContentType(ct)
}
- if w.Type == webhook.SLACK {
+ if w.Type == webhook_module.SLACK {
if channel, ok := form.Config["channel"]; ok {
meta, err := json.Marshal(&webhook_service.SlackMeta{
Channel: channel,
@@ -243,30 +277,37 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
w.PushOnly = false
w.SendEverything = false
w.ChooseEvents = true
- w.Create = util.IsStringInSlice(string(webhook.HookEventCreate), form.Events, true)
- w.Push = util.IsStringInSlice(string(webhook.HookEventPush), form.Events, true)
- w.Create = util.IsStringInSlice(string(webhook.HookEventCreate), form.Events, true)
- w.Delete = util.IsStringInSlice(string(webhook.HookEventDelete), form.Events, true)
- w.Fork = util.IsStringInSlice(string(webhook.HookEventFork), form.Events, true)
- w.Repository = util.IsStringInSlice(string(webhook.HookEventRepository), form.Events, true)
- w.Release = util.IsStringInSlice(string(webhook.HookEventRelease), form.Events, true)
+ w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
+ w.Push = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true)
+ w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
+ w.Delete = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true)
+ w.Fork = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true)
+ w.Repository = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true)
+ w.Wiki = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true)
+ w.Release = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true)
w.BranchFilter = form.BranchFilter
+ err := w.SetHeaderAuthorization(form.AuthorizationHeader)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "SetHeaderAuthorization", err)
+ return false
+ }
+
// Issues
w.Issues = issuesHook(form.Events, "issues_only")
- w.IssueAssign = issuesHook(form.Events, string(webhook.HookEventIssueAssign))
- w.IssueLabel = issuesHook(form.Events, string(webhook.HookEventIssueLabel))
- w.IssueMilestone = issuesHook(form.Events, string(webhook.HookEventIssueMilestone))
- w.IssueComment = issuesHook(form.Events, string(webhook.HookEventIssueComment))
+ w.IssueAssign = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign))
+ w.IssueLabel = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel))
+ w.IssueMilestone = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone))
+ w.IssueComment = issuesHook(form.Events, string(webhook_module.HookEventIssueComment))
// Pull requests
w.PullRequest = pullHook(form.Events, "pull_request_only")
- w.PullRequestAssign = pullHook(form.Events, string(webhook.HookEventPullRequestAssign))
- w.PullRequestLabel = pullHook(form.Events, string(webhook.HookEventPullRequestLabel))
- w.PullRequestMilestone = pullHook(form.Events, string(webhook.HookEventPullRequestMilestone))
- w.PullRequestComment = pullHook(form.Events, string(webhook.HookEventPullRequestComment))
+ w.PullRequestAssign = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign))
+ w.PullRequestLabel = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel))
+ w.PullRequestMilestone = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone))
+ w.PullRequestComment = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment))
w.PullRequestReview = pullHook(form.Events, "pull_request_review")
- w.PullRequestSync = pullHook(form.Events, string(webhook.HookEventPullRequestSync))
+ w.PullRequestSync = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync))
if err := w.UpdateEvent(); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateEvent", err)
diff --git a/routers/api/v1/utils/page.go b/routers/api/v1/utils/page.go
index 608bec739576a..6910b8293196e 100644
--- a/routers/api/v1/utils/page.go
+++ b/routers/api/v1/utils/page.go
@@ -1,13 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package utils
import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
+ "code.gitea.io/gitea/services/convert"
)
// GetListOptions returns list options using the page and limit parameters
diff --git a/routers/common/db.go b/routers/common/db.go
index 99d66f93e89c0..2e86fbd0fd42c 100644
--- a/routers/common/db.go
+++ b/routers/common/db.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package common
@@ -13,6 +12,8 @@ import (
"code.gitea.io/gitea/models/migrations"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+
+ "xorm.io/xorm"
)
// InitDBEngine In case of problems connecting to DB, retry connection. Eg, PGSQL in Docker Container on Synology
@@ -25,7 +26,7 @@ func InitDBEngine(ctx context.Context) (err error) {
default:
}
log.Info("ORM engine initialization attempt #%d/%d...", i+1, setting.Database.DBConnectRetries)
- if err = db.InitEngineWithMigration(ctx, migrations.Migrate); err == nil {
+ if err = db.InitEngineWithMigration(ctx, migrateWithSetting); err == nil {
break
} else if i == setting.Database.DBConnectRetries-1 {
return err
@@ -37,3 +38,20 @@ func InitDBEngine(ctx context.Context) (err error) {
db.HasEngine = true
return nil
}
+
+func migrateWithSetting(x *xorm.Engine) error {
+ if setting.Database.AutoMigration {
+ return migrations.Migrate(x)
+ }
+
+ if current, err := migrations.GetCurrentDBVersion(x); err != nil {
+ return err
+ } else if current < 0 {
+ // execute migrations when the database isn't initialized even if AutoMigration is false
+ return migrations.Migrate(x)
+ } else if expected := migrations.ExpectedVersion(); current != expected {
+ log.Fatal(`"database.AUTO_MIGRATION" is disabled, but current database version %d is not equal to the expected version %d.`+
+ `You can set "database.AUTO_MIGRATION" to true or migrate manually by running "gitea [--config /path/to/app.ini] migrate"`, current, expected)
+ }
+ return nil
+}
diff --git a/routers/common/middleware.go b/routers/common/middleware.go
index 6ea1e1dfbe5ea..0c7838b0ef5be 100644
--- a/routers/common/middleware.go
+++ b/routers/common/middleware.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package common
diff --git a/routers/common/repo.go b/routers/common/repo.go
index b0e14b63f542c..4b04ddae347d4 100644
--- a/routers/common/repo.go
+++ b/routers/common/repo.go
@@ -1,17 +1,16 @@
// 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.
+// SPDX-License-Identifier: MIT
package common
import (
- "fmt"
"io"
"path"
"path/filepath"
"strings"
+ "time"
- "code.gitea.io/gitea/modules/charset"
+ charsetModule "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
@@ -22,8 +21,8 @@ import (
)
// ServeBlob download a git.Blob
-func ServeBlob(ctx *context.Context, blob *git.Blob) error {
- if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
+func ServeBlob(ctx *context.Context, blob *git.Blob, lastModified time.Time) error {
+ if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return nil
}
@@ -41,7 +40,7 @@ func ServeBlob(ctx *context.Context, blob *git.Blob) error {
}
// ServeData download file from io.Reader
-func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error {
+func ServeData(ctx *context.Context, filePath string, size int64, reader io.Reader) error {
buf := make([]byte, 1024)
n, err := util.ReadAtMost(reader, buf)
if err != nil {
@@ -51,52 +50,63 @@ func ServeData(ctx *context.Context, name string, size int64, reader io.Reader)
buf = buf[:n]
}
- ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
+ opts := &context.ServeHeaderOptions{
+ Filename: path.Base(filePath),
+ }
if size >= 0 {
- ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
+ opts.ContentLength = &size
} else {
- log.Error("ServeData called to serve data: %s with size < 0: %d", name, size)
+ log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size)
}
- name = path.Base(name)
-
- // Google Chrome dislike commas in filenames, so let's change it to a space
- name = strings.ReplaceAll(name, ",", " ")
- st := typesniffer.DetectContentType(buf)
+ sniffedType := typesniffer.DetectContentType(buf)
+ isPlain := sniffedType.IsText() || ctx.FormBool("render")
- mappedMimeType := ""
if setting.MimeTypeMap.Enabled {
- fileExtension := strings.ToLower(filepath.Ext(name))
- mappedMimeType = setting.MimeTypeMap.Map[fileExtension]
+ fileExtension := strings.ToLower(filepath.Ext(filePath))
+ opts.ContentType = setting.MimeTypeMap.Map[fileExtension]
}
- if st.IsText() || ctx.FormBool("render") {
- cs, err := charset.DetectEncoding(buf)
- if err != nil {
- log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err)
- cs = "utf-8"
- }
- if mappedMimeType == "" {
- mappedMimeType = "text/plain"
- }
- ctx.Resp.Header().Set("Content-Type", mappedMimeType+"; charset="+strings.ToLower(cs))
- } else {
- ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
- if mappedMimeType != "" {
- ctx.Resp.Header().Set("Content-Type", mappedMimeType)
- }
- if (st.IsImage() || st.IsPDF()) && (setting.UI.SVG.Enabled || !st.IsSvgImage()) {
- ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
- if st.IsSvgImage() {
- ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
- ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
- ctx.Resp.Header().Set("Content-Type", typesniffer.SvgMimeType)
- }
+
+ if opts.ContentType == "" {
+ if sniffedType.IsBrowsableBinaryType() {
+ opts.ContentType = sniffedType.GetMimeType()
+ } else if isPlain {
+ opts.ContentType = "text/plain"
} else {
- ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
+ opts.ContentType = typesniffer.ApplicationOctetStream
+ }
+ }
+
+ if isPlain {
+ var charset string
+ charset, err = charsetModule.DetectEncoding(buf)
+ if err != nil {
+ log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
+ charset = "utf-8"
}
+ opts.ContentTypeCharset = strings.ToLower(charset)
}
+ isSVG := sniffedType.IsSvgImage()
+
+ // serve types that can present a security risk with CSP
+ if isSVG {
+ ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
+ } else if sniffedType.IsPDF() {
+ // no sandbox attribute for pdf as it breaks rendering in at least safari. this
+ // should generally be safe as scripts inside PDF can not escape the PDF document
+ // see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
+ ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
+ }
+
+ opts.Disposition = "inline"
+ if isSVG && !setting.UI.SVG.Enabled {
+ opts.Disposition = "attachment"
+ }
+
+ ctx.SetServeHeaders(opts)
+
_, err = ctx.Resp.Write(buf)
if err != nil {
return err
diff --git a/routers/init.go b/routers/init.go
index 88c393736ef48..25f557d69569c 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -1,20 +1,15 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package routers
import (
"context"
- "net"
"reflect"
"runtime"
- "strconv"
- "strings"
"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"
@@ -30,7 +25,10 @@ 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"
"code.gitea.io/gitea/modules/web"
packages_router "code.gitea.io/gitea/routers/api/packages"
apiv1 "code.gitea.io/gitea/routers/api/v1"
@@ -39,8 +37,11 @@ import (
web_routers "code.gitea.io/gitea/routers/web"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
+ "code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/cron"
"code.gitea.io/gitea/services/mailer"
+ mailer_incoming "code.gitea.io/gitea/services/mailer/incoming"
+ 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"
@@ -72,25 +73,35 @@ func mustInitCtx(ctx context.Context, fn func(ctx context.Context) error) {
func InitGitServices() {
setting.NewServices()
mustInit(storage.Init)
- mustInit(repo_service.NewContext)
+ mustInit(repo_service.Init)
}
-func syncAppPathForGit(ctx context.Context) error {
- runtimeState := new(appstate.RuntimeState)
- if err := appstate.AppState.Get(runtimeState); err != nil {
+func syncAppConfForGit(ctx context.Context) error {
+ runtimeState := new(system.RuntimeState)
+ if err := system.AppState.Get(runtimeState); err != nil {
return err
}
+
+ updated := false
if runtimeState.LastAppPath != setting.AppPath {
log.Info("AppPath changed from '%s' to '%s'", runtimeState.LastAppPath, setting.AppPath)
+ runtimeState.LastAppPath = setting.AppPath
+ updated = true
+ }
+ if runtimeState.LastCustomConf != setting.CustomConf {
+ log.Info("CustomConf changed from '%s' to '%s'", runtimeState.LastCustomConf, setting.CustomConf)
+ runtimeState.LastCustomConf = setting.CustomConf
+ updated = true
+ }
+ if updated {
log.Info("re-sync repository hooks ...")
mustInitCtx(ctx, repo_service.SyncRepositoryHooks)
log.Info("re-write ssh public keys ...")
mustInit(asymkey_model.RewriteAllPublicKeys)
- runtimeState.LastAppPath = setting.AppPath
- return appstate.AppState.Set(runtimeState)
+ return system.AppState.Set(runtimeState)
}
return nil
}
@@ -101,29 +112,30 @@ func GlobalInitInstalled(ctx context.Context) {
log.Fatal("Gitea is not installed")
}
- mustInitCtx(ctx, git.Init)
- log.Info(git.VersionInfo())
-
- git.CheckLFSVersion()
+ mustInitCtx(ctx, git.InitFull)
+ log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir())
log.Info("AppPath: %s", setting.AppPath)
log.Info("AppWorkPath: %s", setting.AppWorkPath)
log.Info("Custom path: %s", setting.CustomPath)
log.Info("Log path: %s", setting.LogRootPath)
log.Info("Configuration file: %s", setting.CustomConf)
- log.Info("Run Mode: %s", strings.Title(setting.RunMode))
+ log.Info("Run Mode: %s", util.ToTitleCase(setting.RunMode))
+ log.Info("Gitea v%s%s", setting.AppVer, setting.AppBuiltWith)
// Setup i18n
- translation.InitLocales()
+ translation.InitLocales(ctx)
+
+ setting.NewServices()
+ mustInit(storage.Init)
- InitGitServices()
- mailer.NewContext()
+ mailer.NewContext(ctx)
mustInit(cache.NewContext)
notification.NewContext()
mustInit(archiver.Init)
highlight.NewContext()
external.RegisterRenderers()
- markup.Init()
+ markup.Init(markup_service.ProcessorHelper())
if setting.EnableSQLite3 {
log.Info("SQLite3 support is enabled")
@@ -133,51 +145,57 @@ 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.
- cron.NewContext(ctx)
issue_indexer.InitIssueIndexer(false)
code_indexer.Init()
mustInit(stats_indexer.Init)
mirror_service.InitSyncMirrors()
- webhook.InitDeliverHooks()
+ mustInit(webhook.Init)
mustInit(pull_service.Init)
+ mustInit(automerge.Init)
mustInit(task.Init)
mustInit(repo_migrations.Init)
eventsource.GetManager().Init()
+ mustInitCtx(ctx, mailer_incoming.Init)
- mustInitCtx(ctx, syncAppPathForGit)
+ mustInitCtx(ctx, syncAppConfForGit)
+
+ mustInit(ssh.Init)
- if setting.SSH.StartBuiltinServer {
- ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
- log.Info("SSH server started on %s. Cipher list (%v), key exchange algorithms (%v), MACs (%v)",
- net.JoinHostPort(setting.SSH.ListenHost, strconv.Itoa(setting.SSH.ListenPort)),
- setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
- } else {
- ssh.Unused()
- }
auth.Init()
svg.Init()
+
+ // Finally start up the cron
+ cron.NewContext(ctx)
}
// NormalRoutes represents non install routes
-func NormalRoutes() *web.Route {
+func NormalRoutes(ctx context.Context) *web.Route {
+ ctx, _ = templates.HTMLRenderer(ctx)
r := web.NewRoute()
for _, middle := range common.Middlewares() {
r.Use(middle)
}
- r.Mount("/", web_routers.Routes())
- r.Mount("/api/v1", apiv1.Routes())
+ r.Mount("/", web_routers.Routes(ctx))
+ r.Mount("/api/v1", apiv1.Routes(ctx))
r.Mount("/api/internal", private.Routes())
+
if setting.Packages.Enabled {
- r.Mount("/api/packages", packages_router.Routes())
- r.Mount("/v2", packages_router.ContainerRoutes())
+ // Add endpoints to match common package manager APIs
+
+ // This implements package support for most package managers
+ r.Mount("/api/packages", packages_router.CommonRoutes(ctx))
+
+ // This implements the OCI API (Note this is not preceded by /api but is instead /v2)
+ r.Mount("/v2", packages_router.ContainerRoutes(ctx))
}
return r
}
diff --git a/routers/install/install.go b/routers/install/install.go
index ec1719439f53a..e9fa844a09568 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -1,22 +1,24 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2021 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package install
import (
+ goctx "context"
"fmt"
"net/http"
"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"
@@ -51,37 +53,41 @@ func getSupportedDbTypeNames() (dbTypeNames []map[string]string) {
}
// Init prepare for rendering installation page
-func Init(next http.Handler) http.Handler {
- rnd := templates.HTMLRenderer()
+func Init(ctx goctx.Context) func(next http.Handler) http.Handler {
+ _, rnd := templates.HTMLRenderer(ctx)
dbTypeNames := getSupportedDbTypeNames()
- return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
- if setting.InstallLock {
- resp.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
- _ = rnd.HTML(resp, http.StatusOK, string(tplPostInstall), nil)
- return
- }
- locale := middleware.Locale(resp, req)
- startTime := time.Now()
- ctx := context.Context{
- Resp: context.NewResponse(resp),
- Flash: &middleware.Flash{},
- Locale: locale,
- Render: rnd,
- Session: session.GetSession(req),
- Data: map[string]interface{}{
- "i18n": locale,
- "Title": locale.Tr("install.install"),
- "PageIsInstall": true,
- "DbTypeNames": dbTypeNames,
- "AllLangs": translation.AllLangs(),
- "PageStartTime": startTime,
-
- "PasswordHashAlgorithms": user_model.AvailableHashAlgorithms,
- },
- }
- ctx.Req = context.WithContext(req, &ctx)
- next.ServeHTTP(resp, ctx.Req)
- })
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+ if setting.InstallLock {
+ resp.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
+ _ = rnd.HTML(resp, http.StatusOK, string(tplPostInstall), nil)
+ return
+ }
+ locale := middleware.Locale(resp, req)
+ startTime := time.Now()
+ ctx := context.Context{
+ Resp: context.NewResponse(resp),
+ Flash: &middleware.Flash{},
+ Locale: locale,
+ Render: rnd,
+ Session: session.GetSession(req),
+ Data: map[string]interface{}{
+ "locale": locale,
+ "Title": locale.Tr("install.install"),
+ "PageIsInstall": true,
+ "DbTypeNames": dbTypeNames,
+ "AllLangs": translation.AllLangs(),
+ "PageStartTime": startTime,
+
+ "PasswordHashAlgorithms": user_model.AvailableHashAlgorithms,
+ },
+ }
+ defer ctx.Close()
+
+ ctx.Req = context.WithContext(req, &ctx)
+ next.ServeHTTP(resp, ctx.Req)
+ })
+ }
}
// Install render installation page
@@ -131,17 +137,20 @@ func Install(ctx *context.Context) {
// E-mail service settings
if setting.MailService != nil {
- form.SMTPHost = setting.MailService.Host
+ form.SMTPAddr = setting.MailService.SMTPAddr
+ form.SMTPPort = setting.MailService.SMTPPort
form.SMTPFrom = setting.MailService.From
form.SMTPUser = setting.MailService.User
+ form.SMTPPasswd = setting.MailService.Passwd
}
form.RegisterConfirm = setting.Service.RegisterEmailConfirm
form.MailNotify = setting.Service.EnableNotifyMail
// Server and other services settings
form.OfflineMode = setting.OfflineMode
- form.DisableGravatar = setting.DisableGravatar
- form.EnableFederatedAvatar = setting.EnableFederatedAvatar
+ form.DisableGravatar = setting.DisableGravatar // when installing, there is no database connection so that given a default value
+ form.EnableFederatedAvatar = setting.EnableFederatedAvatar // 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
@@ -365,7 +374,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()
@@ -418,9 +426,10 @@ func SubmitInstall(ctx *context.Context) {
cfg.Section("server").Key("LFS_START_SERVER").SetValue("false")
}
- if len(strings.TrimSpace(form.SMTPHost)) > 0 {
+ if len(strings.TrimSpace(form.SMTPAddr)) > 0 {
cfg.Section("mailer").Key("ENABLED").SetValue("true")
- cfg.Section("mailer").Key("HOST").SetValue(form.SMTPHost)
+ cfg.Section("mailer").Key("SMTP_ADDR").SetValue(form.SMTPAddr)
+ cfg.Section("mailer").Key("SMTP_PORT").SetValue(form.SMTPPort)
cfg.Section("mailer").Key("FROM").SetValue(form.SMTPFrom)
cfg.Section("mailer").Key("USER").SetValue(form.SMTPUser)
cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd)
@@ -431,8 +440,15 @@ 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))
- cfg.Section("picture").Key("ENABLE_FEDERATED_AVATAR").SetValue(fmt.Sprint(form.EnableFederatedAvatar))
+ // 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.save_config_failed", err), tplInstall, &form)
+ return
+ }
+ if err := system_model.SetSettingNoVersion(system_model.KeyPictureEnableFederatedAvatar, strconv.FormatBool(form.EnableFederatedAvatar)); err != nil {
+ ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
+ return
+ }
cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(fmt.Sprint(form.EnableOpenIDSignIn))
cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(fmt.Sprint(form.EnableOpenIDSignUp))
cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(fmt.Sprint(form.DisableRegistration))
@@ -443,6 +459,7 @@ func SubmitInstall(ctx *context.Context) {
cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(fmt.Sprint(form.DefaultAllowCreateOrganization))
cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(fmt.Sprint(form.DefaultEnableTimetracking))
cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(fmt.Sprint(form.NoReplyAddress))
+ cfg.Section("cron.update_checker").Key("ENABLED").SetValue(fmt.Sprint(form.EnableUpdateChecker))
cfg.Section("").Key("RUN_MODE").SetValue("prod")
@@ -453,16 +470,22 @@ func SubmitInstall(ctx *context.Context) {
cfg.Section("log").Key("ROOT_PATH").SetValue(form.LogRootPath)
cfg.Section("log").Key("ROUTER").SetValue("console")
+ cfg.Section("repository.pull-request").Key("DEFAULT_MERGE_STYLE").SetValue("merge")
+
cfg.Section("repository.signing").Key("DEFAULT_TRUST_MODEL").SetValue("committer")
cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
- var internalToken string
- if internalToken, err = generate.NewInternalToken(); err != nil {
- ctx.RenderWithErr(ctx.Tr("install.internal_token_failed", err), tplInstall, &form)
- return
+ // the internal token could be read from INTERNAL_TOKEN or INTERNAL_TOKEN_URI (the file is guaranteed to be non-empty)
+ // if there is no InternalToken, generate one and save to security.INTERNAL_TOKEN
+ if setting.InternalToken == "" {
+ var internalToken string
+ if internalToken, err = generate.NewInternalToken(); err != nil {
+ ctx.RenderWithErr(ctx.Tr("install.internal_token_failed", err), tplInstall, &form)
+ return
+ }
+ cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
}
- cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
// if there is already a SECRET_KEY, we should not overwrite it, otherwise the encrypted data will not be able to be decrypted
if setting.SecretKey == "" {
@@ -491,6 +514,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)
@@ -499,13 +525,17 @@ func SubmitInstall(ctx *context.Context) {
// Create admin account
if len(form.AdminName) > 0 {
u := &user_model.User{
- Name: form.AdminName,
- Email: form.AdminEmail,
- Passwd: form.AdminPasswd,
- IsAdmin: true,
- IsActive: true,
+ Name: form.AdminName,
+ Email: form.AdminEmail,
+ Passwd: form.AdminPasswd,
+ IsAdmin: true,
+ }
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsRestricted: util.OptionalBoolFalse,
+ IsActive: util.OptionalBoolTrue,
}
- if err = user_model.CreateUser(u); err != nil {
+
+ if err = user_model.CreateUser(u, overwriteDefault); err != nil {
if !user_model.IsErrUserAlreadyExist(err) {
setting.InstallLock = false
ctx.Data["Err_AdminName"] = true
@@ -514,7 +544,7 @@ func SubmitInstall(ctx *context.Context) {
return
}
log.Info("Admin account already exist")
- u, _ = user_model.GetUserByName(u.Name)
+ u, _ = user_model.GetUserByName(ctx, u.Name)
}
days := 86400 * setting.LogInRememberDays
diff --git a/routers/install/routes.go b/routers/install/routes.go
index ef96e99628ef8..9aa5a88d24b18 100644
--- a/routers/install/routes.go
+++ b/routers/install/routes.go
@@ -1,14 +1,15 @@
// 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.
+// SPDX-License-Identifier: MIT
package install
import (
+ goctx "context"
"fmt"
"net/http"
"path"
+ "code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
@@ -16,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/common"
+ "code.gitea.io/gitea/routers/web/healthcheck"
"code.gitea.io/gitea/services/forms"
"gitea.com/go-chi/session"
@@ -27,8 +29,8 @@ func (d *dataStore) GetData() map[string]interface{} {
return *d
}
-func installRecovery() func(next http.Handler) http.Handler {
- rnd := templates.HTMLRenderer()
+func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
+ _, rnd := templates.HTMLRenderer(ctx)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
@@ -56,11 +58,12 @@ func installRecovery() func(next http.Handler) http.Handler {
store := dataStore{
"Language": lc.Language(),
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
- "i18n": lc,
+ "locale": lc,
"SignedUserID": int64(0),
"SignedUserName": "",
}
+ httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform")
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
if !setting.IsProd {
@@ -79,7 +82,7 @@ func installRecovery() func(next http.Handler) http.Handler {
}
// Routes registers the install routes
-func Routes() *web.Route {
+func Routes(ctx goctx.Context) *web.Route {
r := web.NewRoute()
for _, middle := range common.Middlewares() {
r.Use(middle)
@@ -102,10 +105,11 @@ func Routes() *web.Route {
Domain: setting.SessionConfig.Domain,
}))
- r.Use(installRecovery())
- r.Use(Init)
+ r.Use(installRecovery(ctx))
+ r.Use(Init(ctx))
r.Get("/", Install)
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
+ r.Get("/api/healthz", healthcheck.Check)
r.NotFound(web.Wrap(installNotFound))
return r
diff --git a/routers/install/routes_test.go b/routers/install/routes_test.go
index 35a66c1c47426..35b5e5e9160b9 100644
--- a/routers/install/routes_test.go
+++ b/routers/install/routes_test.go
@@ -1,19 +1,20 @@
// 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.
+// SPDX-License-Identifier: MIT
package install
import (
+ "context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRoutes(t *testing.T) {
- routes := Routes()
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ routes := Routes(ctx)
assert.NotNil(t, routes)
- assert.Len(t, routes.R.Routes(), 1)
assert.EqualValues(t, "/", routes.R.Routes()[0].Pattern)
assert.Nil(t, routes.R.Routes()[0].SubRoutes)
assert.Len(t, routes.R.Routes()[0].Handlers, 2)
diff --git a/routers/install/setting.go b/routers/install/setting.go
index cf0a01ce31f57..b76219f45d36e 100644
--- a/routers/install/setting.go
+++ b/routers/install/setting.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package install
@@ -24,7 +23,7 @@ func PreloadSettings(ctx context.Context) bool {
log.Info("Log path: %s", setting.LogRootPath)
log.Info("Configuration file: %s", setting.CustomConf)
log.Info("Prepare to run install page")
- translation.InitLocales()
+ translation.InitLocales(ctx)
if setting.EnableSQLite3 {
log.Info("SQLite3 is supported")
}
diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go
index 55ffef43f3f0c..268ebbe4431ba 100644
--- a/routers/private/default_branch.go
+++ b/routers/private/default_branch.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
package private
diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index 5e315ede478de..c62038899d71a 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
package private
@@ -11,7 +10,7 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
@@ -106,7 +105,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
repo.IsPrivate = opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate, repo.IsPrivate)
repo.IsTemplate = opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate, repo.IsTemplate)
- if err := repo_model.UpdateRepositoryCols(repo, "is_private", "is_template"); err != nil {
+ if err := repo_model.UpdateRepositoryCols(ctx, repo, "is_private", "is_template"); err != nil {
log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
@@ -141,8 +140,8 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
continue
}
- pr, err := models.GetPullRequestByIndex(repo.ID, pullIndex)
- if err != nil && !models.IsErrPullRequestNotExist(err) {
+ pr, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, pullIndex)
+ if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
log.Error("Failed to get PR by index %v Error: %v", pullIndex, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to get PR by index %v Error: %v", pullIndex, err),
@@ -184,7 +183,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
baseRepo = repo
if repo.IsFork {
- if err := repo.GetBaseRepo(); err != nil {
+ if err := repo.GetBaseRepo(ctx); err != nil {
log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err),
@@ -202,8 +201,8 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
continue
}
- pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, models.PullRequestFlowGithub)
- if err != nil && !models.IsErrPullRequestNotExist(err) {
+ pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub)
+ if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err)
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
Err: fmt.Sprintf(
diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go
index 763fe1cf1c559..8468227077fdf 100644
--- a/routers/private/hook_pre_receive.go
+++ b/routers/private/hook_pre_receive.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
package private
@@ -13,7 +12,10 @@ import (
"code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
perm_model "code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
gitea_context "code.gitea.io/gitea/modules/context"
@@ -30,7 +32,7 @@ type preReceiveContext struct {
// loadedPusher indicates that where the following information are loaded
loadedPusher bool
user *user_model.User // it's the org user if a DeployKey is used
- userPerm models.Permission
+ userPerm access_model.Permission
deployKeyAccessMode perm_model.AccessMode
canCreatePullRequest bool
@@ -39,12 +41,14 @@ type preReceiveContext struct {
canWriteCode bool
checkedCanWriteCode bool
- protectedTags []*models.ProtectedTag
+ protectedTags []*git_model.ProtectedTag
gotProtectedTags bool
env []string
opts *private.HookOptions
+
+ branchName string
}
// CanWriteCode returns true if pusher can write code
@@ -53,7 +57,7 @@ func (ctx *preReceiveContext) CanWriteCode() bool {
if !ctx.loadPusherAndPermission() {
return false
}
- ctx.canWriteCode = ctx.userPerm.CanWrite(unit.TypeCode) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
+ ctx.canWriteCode = issues_model.CanMaintainerWriteToBranch(ctx.userPerm, ctx.branchName, ctx.user) || ctx.deployKeyAccessMode >= perm_model.AccessModeWrite
ctx.checkedCanWriteCode = true
}
return ctx.canWriteCode
@@ -134,13 +138,15 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
}
func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullName string) {
+ branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
+ ctx.branchName = branchName
+
if !ctx.AssertCanWriteCode() {
return
}
repo := ctx.Repo.Repository
gitRepo := ctx.Repo.GitRepo
- branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA {
log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo)
@@ -150,7 +156,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
return
}
- protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName)
+ protectBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
if err != nil {
log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
@@ -160,9 +166,10 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
}
// Allow pushes to non-protected branches
- if protectBranch == nil || !protectBranch.IsProtected() {
+ if protectBranch == nil {
return
}
+ protectBranch.Repo = repo
// This ref is a protected branch.
//
@@ -179,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{
@@ -232,7 +239,6 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
Err: fmt.Sprintf("Unable to check file protection for commits from %s to %s: %v", oldCommitID, newCommitID, err),
})
return
-
}
changedProtectedfiles = true
@@ -241,11 +247,19 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
}
// 5. Check if the doer is allowed to push
- canPush := false
+ var canPush bool
if ctx.opts.DeployKeyID != 0 {
canPush = !changedProtectedfiles && protectBranch.CanPush && (!protectBranch.EnableWhitelist || protectBranch.WhitelistDeployKeys)
} else {
- canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx.opts.UserID)
+ user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
+ if err != nil {
+ log.Error("Unable to GetUserByID for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
+ ctx.JSON(http.StatusInternalServerError, private.Response{
+ Err: fmt.Sprintf("Unable to GetUserByID for commits from %s to %s: %v", oldCommitID, newCommitID, err),
+ })
+ return
+ }
+ canPush = !changedProtectedfiles && protectBranch.CanUserPush(ctx, user)
}
// 6. If we're not allowed to push directly
@@ -290,7 +304,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
// 6b. Merge (from UI or API)
// Get the PR, user and permissions for the user in the repository
- pr, err := models.GetPullRequestByID(ctx.opts.PullRequestID)
+ pr, err := issues_model.GetPullRequestByID(ctx, ctx.opts.PullRequestID)
if err != nil {
log.Error("Unable to get PullRequest %d Error: %v", ctx.opts.PullRequestID, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
@@ -307,7 +321,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
// Now check if the user is allowed to merge PRs for this repository
// Note: we can use ctx.perm and ctx.user directly as they will have been loaded above
- allowedMerge, err := pull_service.IsUserAllowedToMerge(pr, ctx.userPerm, ctx.user)
+ allowedMerge, err := pull_service.IsUserAllowedToMerge(ctx, pr, ctx.userPerm, ctx.user)
if err != nil {
log.Error("Error calculating if allowed to merge: %v", err)
ctx.JSON(http.StatusInternalServerError, private.Response{
@@ -339,7 +353,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID, refFullN
}
// Check all status checks and reviews are ok
- if err := pull_service.CheckPRReadyToMerge(ctx, pr, true); err != nil {
+ if err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil {
if models.IsErrDisallowedToMerge(err) {
log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error())
ctx.JSON(http.StatusForbidden, private.Response{
@@ -365,7 +379,7 @@ func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID, refFullName
if !ctx.gotProtectedTags {
var err error
- ctx.protectedTags, err = models.GetProtectedTags(ctx.Repo.Repository.ID)
+ ctx.protectedTags, err = git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
if err != nil {
log.Error("Unable to get protected tags for %-v Error: %v", ctx.Repo.Repository, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
@@ -376,7 +390,7 @@ func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID, refFullName
ctx.gotProtectedTags = true
}
- isAllowed, err := models.IsUserAllowedToControlTag(ctx.protectedTags, tagName, ctx.opts.UserID)
+ isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
@@ -458,7 +472,7 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool {
return true
}
- user, err := user_model.GetUserByID(ctx.opts.UserID)
+ user, err := user_model.GetUserByID(ctx, ctx.opts.UserID)
if err != nil {
log.Error("Unable to get User id %d Error: %v", ctx.opts.UserID, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
@@ -468,9 +482,9 @@ func (ctx *preReceiveContext) loadPusherAndPermission() bool {
}
ctx.user = user
- userPerm, err := models.GetUserRepoPermission(ctx.Repo.Repository, user)
+ userPerm, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, user)
if err != nil {
- log.Error("Unable to get Repo permission of repo %s/%s of User %s", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err)
+ log.Error("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get Repo permission of repo %s/%s of User %s: %v", ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, user.Name, err),
})
diff --git a/routers/private/hook_proc_receive.go b/routers/private/hook_proc_receive.go
index e427a55c56161..05921e6f5841b 100644
--- a/routers/private/hook_proc_receive.go
+++ b/routers/private/hook_proc_receive.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
package private
@@ -8,8 +7,10 @@ package private
import (
"net/http"
+ repo_model "code.gitea.io/gitea/models/repo"
gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/agit"
@@ -23,8 +24,17 @@ func HookProcReceive(ctx *gitea_context.PrivateContext) {
return
}
- results := agit.ProcRecive(ctx, opts)
- if ctx.Written() {
+ results, err := agit.ProcReceive(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, opts)
+ if err != nil {
+ if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
+ ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
+ } else {
+ log.Error(err.Error())
+ ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+ "Err": err.Error(),
+ })
+ }
+
return
}
diff --git a/routers/private/hook_verification.go b/routers/private/hook_verification.go
index dfa6195b19717..7b9550dfddddf 100644
--- a/routers/private/hook_verification.go
+++ b/routers/private/hook_verification.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
package private
@@ -44,7 +43,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 +90,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/private/internal.go b/routers/private/internal.go
index 6ba87d67bf542..306e4ffb0040f 100644
--- a/routers/private/internal.go
+++ b/routers/private/internal.go
@@ -1,13 +1,11 @@
// 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.
+// SPDX-License-Identifier: MIT
// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
package private
import (
"net/http"
- "reflect"
"strings"
"code.gitea.io/gitea/modules/context"
@@ -17,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/web"
"gitea.com/go-chi/binding"
+ chi_middleware "github.com/go-chi/chi/v5/middleware"
)
// CheckInternalToken check internal token is set
@@ -24,6 +23,11 @@ func CheckInternalToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
tokens := req.Header.Get("Authorization")
fields := strings.SplitN(tokens, " ", 2)
+ if setting.InternalToken == "" {
+ log.Warn(`The INTERNAL_TOKEN setting is missing from the configuration file: %q, internal API can't work.`, setting.CustomConf)
+ http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+ return
+ }
if len(fields) != 2 || fields[0] != "Bearer" || fields[1] != setting.InternalToken {
log.Debug("Forbidden attempt to access internal url: Authorization header: %s", tokens)
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
@@ -34,13 +38,9 @@ func CheckInternalToken(next http.Handler) http.Handler {
}
// bind binding an obj to a handler
-func bind(obj interface{}) http.HandlerFunc {
- tp := reflect.TypeOf(obj)
- for tp.Kind() == reflect.Ptr {
- tp = tp.Elem()
- }
+func bind[T any](obj T) http.HandlerFunc {
return web.Wrap(func(ctx *context.PrivateContext) {
- theObj := reflect.New(tp).Interface() // create a new form obj for every request but not use obj directly
+ theObj := new(T) // create a new form obj for every request but not use obj directly
binding.Bind(ctx.Req, theObj)
web.SetForm(ctx, theObj)
})
@@ -52,6 +52,9 @@ func Routes() *web.Route {
r := web.NewRoute()
r.Use(context.PrivateContexter())
r.Use(CheckInternalToken)
+ // Log the real ip address of the request from SSH is really helpful for diagnosing sometimes.
+ // Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers.
+ r.Use(chi_middleware.RealIP)
r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent)
r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo)
@@ -68,6 +71,7 @@ func Routes() *web.Route {
r.Post("/manager/pause-logging", PauseLogging)
r.Post("/manager/resume-logging", ResumeLogging)
r.Post("/manager/release-and-reopen-logging", ReleaseReopenLogging)
+ r.Post("/manager/set-log-sql", SetLogSQL)
r.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger)
r.Post("/manager/remove-logger/{group}/{name}", RemoveLogger)
r.Get("/manager/processes", Processes)
diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go
index c50d2a01a72a0..bd8db0a1854dc 100644
--- a/routers/private/internal_repo.go
+++ b/routers/private/internal_repo.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
package private
@@ -69,7 +68,7 @@ func RepoAssignment(ctx *gitea_context.PrivateContext) context.CancelFunc {
}
func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string) *repo_model.Repository {
- repo, err := repo_model.GetRepositoryByOwnerAndName(ownerName, repoName)
+ repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
if err != nil {
log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
diff --git a/routers/private/key.go b/routers/private/key.go
index 3366b764e6db6..b536019dd7a01 100644
--- a/routers/private/key.go
+++ b/routers/private/key.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
package private
@@ -25,7 +24,7 @@ func UpdatePublicKeyInRepo(ctx *context.PrivateContext) {
return
}
- deployKey, err := asymkey_model.GetDeployKeyByRepo(keyID, repoID)
+ deployKey, err := asymkey_model.GetDeployKeyByRepo(ctx, keyID, repoID)
if err != nil {
if asymkey_model.IsErrDeployKeyNotExist(err) {
ctx.PlainText(http.StatusOK, "success")
@@ -52,7 +51,7 @@ func UpdatePublicKeyInRepo(ctx *context.PrivateContext) {
func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) {
content := ctx.FormString("content")
- publicKey, err := asymkey_model.SearchPublicKeyByContent(content)
+ publicKey, err := asymkey_model.SearchPublicKeyByContent(ctx, content)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: err.Error(),
diff --git a/routers/private/mail.go b/routers/private/mail.go
index 853b58b09d4dc..622a01dd8fee1 100644
--- a/routers/private/mail.go
+++ b/routers/private/mail.go
@@ -1,14 +1,15 @@
// 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.
+// SPDX-License-Identifier: MIT
package private
import (
+ stdCtx "context"
"fmt"
"net/http"
"strconv"
+ "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
@@ -44,7 +45,7 @@ func SendEmail(ctx *context.PrivateContext) {
var emails []string
if len(mail.To) > 0 {
for _, uname := range mail.To {
- user, err := user_model.GetUserByName(uname)
+ user, err := user_model.GetUserByName(ctx, uname)
if err != nil {
err := fmt.Sprintf("Failed to get user information: %v", err)
log.Error(err)
@@ -59,7 +60,7 @@ func SendEmail(ctx *context.PrivateContext) {
}
}
} else {
- err := user_model.IterateUser(func(user *user_model.User) error {
+ err := db.Iterate(ctx, nil, func(ctx stdCtx.Context, user *user_model.User) error {
if len(user.Email) > 0 && user.IsActive {
emails = append(emails, user.Email)
}
diff --git a/routers/private/manager.go b/routers/private/manager.go
index a3b9a16f79a3f..f15da298d6a04 100644
--- a/routers/private/manager.go
+++ b/routers/private/manager.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package private
@@ -8,6 +7,7 @@ import (
"fmt"
"net/http"
+ "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/json"
@@ -67,6 +67,12 @@ func ReleaseReopenLogging(ctx *context.PrivateContext) {
ctx.PlainText(http.StatusOK, "success")
}
+// SetLogSQL re-sets database SQL logging
+func SetLogSQL(ctx *context.PrivateContext) {
+ db.SetLogSQL(ctx, ctx.FormBool("on"))
+ ctx.PlainText(http.StatusOK, "success")
+}
+
// RemoveLogger removes a logger
func RemoveLogger(ctx *context.PrivateContext) {
group := ctx.Params("group")
diff --git a/routers/private/manager_process.go b/routers/private/manager_process.go
index f8932d61fae73..a5993bf3718d3 100644
--- a/routers/private/manager_process.go
+++ b/routers/private/manager_process.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package private
@@ -34,7 +33,7 @@ func Processes(ctx *context.PrivateContext) {
var processes []*process_module.Process
goroutineCount := int64(0)
- processCount := 0
+ var processCount int
var err error
if stacktraces {
processes, processCount, goroutineCount, err = process_module.GetManager().ProcessStacktraces(flat, noSystem)
diff --git a/routers/private/manager_unix.go b/routers/private/manager_unix.go
index 402bade5d4125..09ced33b8d760 100644
--- a/routers/private/manager_unix.go
+++ b/routers/private/manager_unix.go
@@ -1,9 +1,7 @@
// 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.
+// SPDX-License-Identifier: MIT
//go:build !windows
-// +build !windows
package private
diff --git a/routers/private/manager_windows.go b/routers/private/manager_windows.go
index 014018a5395af..b5382c7d91611 100644
--- a/routers/private/manager_windows.go
+++ b/routers/private/manager_windows.go
@@ -1,9 +1,7 @@
// 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.
+// SPDX-License-Identifier: MIT
//go:build windows
-// +build windows
package private
diff --git a/routers/private/restore_repo.go b/routers/private/restore_repo.go
index 34e06e51c29bf..97ac9a3c5ad95 100644
--- a/routers/private/restore_repo.go
+++ b/routers/private/restore_repo.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package private
diff --git a/routers/private/serv.go b/routers/private/serv.go
index b0451df5d85eb..17f966e3e40a7 100644
--- a/routers/private/serv.go
+++ b/routers/private/serv.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead.
package private
@@ -10,9 +9,9 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -52,7 +51,7 @@ func ServNoCommand(ctx *context.PrivateContext) {
results.Key = key
if key.Type == asymkey_model.KeyTypeUser || key.Type == asymkey_model.KeyTypePrincipal {
- user, err := user_model.GetUserByID(key.OwnerID)
+ user, err := user_model.GetUserByID(ctx, key.OwnerID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.JSON(http.StatusUnauthorized, private.Response{
@@ -109,7 +108,7 @@ func ServCommand(ctx *context.PrivateContext) {
results.RepoName = repoName[:len(repoName)-5]
}
- owner, err := user_model.GetUserByName(results.OwnerName)
+ owner, err := user_model.GetUserByName(ctx, results.OwnerName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
// User is fetching/cloning a non-existent repository
@@ -142,7 +141,7 @@ func ServCommand(ctx *context.PrivateContext) {
if repo_model.IsErrRepoNotExist(err) {
repoExist = false
for _, verb := range ctx.FormStrings("verb") {
- if "git-upload-pack" == verb {
+ if verb == "git-upload-pack" {
// User is fetching/cloning a non-existent repository
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
ctx.JSON(http.StatusNotFound, private.ErrServCommand{
@@ -230,7 +229,7 @@ func ServCommand(ctx *context.PrivateContext) {
var user *user_model.User
if key.Type == asymkey_model.KeyTypeDeploy {
var err error
- deployKey, err = asymkey_model.GetDeployKeyByRepo(key.ID, repo.ID)
+ deployKey, err = asymkey_model.GetDeployKeyByRepo(ctx, key.ID, repo.ID)
if err != nil {
if asymkey_model.IsErrDeployKeyNotExist(err) {
ctx.JSON(http.StatusNotFound, private.ErrServCommand{
@@ -260,7 +259,7 @@ func ServCommand(ctx *context.PrivateContext) {
} else {
// Get the user represented by the Key
var err error
- user, err = user_model.GetUserByID(key.OwnerID)
+ user, err = user_model.GetUserByID(ctx, key.OwnerID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{
@@ -320,7 +319,7 @@ func ServCommand(ctx *context.PrivateContext) {
mode = perm.AccessModeRead
}
- perm, err := models.GetUserRepoPermission(repo, user)
+ perm, err := access_model.GetUserRepoPermission(ctx, repo, user)
if err != nil {
log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{
@@ -345,7 +344,7 @@ func ServCommand(ctx *context.PrivateContext) {
// We already know we aren't using a deploy key
if !repoExist {
- owner, err := user_model.GetUserByName(ownerName)
+ owner, err := user_model.GetUserByName(ctx, ownerName)
if err != nil {
ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{
Results: results,
@@ -383,7 +382,7 @@ func ServCommand(ctx *context.PrivateContext) {
if results.IsWiki {
// Ensure the wiki is enabled before we allow access to it
- if _, err := repo.GetUnit(unit.TypeWiki); err != nil {
+ if _, err := repo.GetUnit(ctx, unit.TypeWiki); err != nil {
if repo_model.IsErrUnitTypeNotExist(err) {
ctx.JSON(http.StatusForbidden, private.ErrServCommand{
Results: results,
diff --git a/routers/private/ssh_log.go b/routers/private/ssh_log.go
index 2f1793a0e0f4f..54604ad55466b 100644
--- a/routers/private/ssh_log.go
+++ b/routers/private/ssh_log.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package private
diff --git a/routers/utils/utils.go b/routers/utils/utils.go
index f15bc1e62e4be..d6856fceacd20 100644
--- a/routers/utils/utils.go
+++ b/routers/utils/utils.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package utils
@@ -20,25 +19,6 @@ func RemoveUsernameParameterSuffix(name string) string {
return name
}
-// IsValidSlackChannel validates a channel name conforms to what slack expects.
-// It makes sure a channel name cannot be empty and invalid ( only an # )
-func IsValidSlackChannel(channelName string) bool {
- switch len(strings.TrimSpace(channelName)) {
- case 0:
- return false
- case 1:
- // Keep default behaviour where a channel name is still
- // valid without an #
- // But if it contains only an #, it should be regarded as
- // invalid
- if channelName[0] == '#' {
- return false
- }
- }
-
- return true
-}
-
// SanitizeFlashErrorString will sanitize a flash error string
func SanitizeFlashErrorString(x string) string {
return strings.ReplaceAll(html.EscapeString(x), "\n", " ")
diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go
index f49ed77b6fc08..6d19214c88e57 100644
--- a/routers/utils/utils_test.go
+++ b/routers/utils/utils_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package utils
@@ -18,23 +17,6 @@ func TestRemoveUsernameParameterSuffix(t *testing.T) {
assert.Equal(t, "", RemoveUsernameParameterSuffix(""))
}
-func TestIsValidSlackChannel(t *testing.T) {
- tt := []struct {
- channelName string
- expected bool
- }{
- {"gitea", true},
- {" ", false},
- {"#", false},
- {"gitea ", true},
- {" gitea", true},
- }
-
- for _, v := range tt {
- assert.Equal(t, v.expected, IsValidSlackChannel(v.channelName))
- }
-}
-
func TestIsExternalURL(t *testing.T) {
setting.AppURL = "https://try.gitea.io/"
type test struct {
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index d4093f2049ac4..0a51000c70c90 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -1,42 +1,33 @@
// 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.
+// SPDX-License-Identifier: MIT
package admin
import (
"fmt"
"net/http"
- "net/url"
- "os"
"runtime"
"strconv"
- "strings"
"time"
- "code.gitea.io/gitea/models"
+ 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"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/updatechecker"
"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"
@@ -84,7 +75,7 @@ var sysStatus struct {
}
func updateSystemStatus() {
- sysStatus.Uptime = timeutil.TimeSincePro(setting.AppStartTime, "en")
+ sysStatus.Uptime = timeutil.TimeSincePro(setting.AppStartTime, translation.NewLocale("en-US"))
m := new(runtime.MemStats)
runtime.ReadMemStats(m)
@@ -126,7 +117,7 @@ func Dashboard(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminDashboard"] = true
- ctx.Data["Stats"] = models.GetStatistic()
+ ctx.Data["Stats"] = activities_model.GetStatistic()
ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate()
ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion()
// FIXME: update periodically
@@ -142,7 +133,7 @@ func DashboardPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminDashboard"] = true
- ctx.Data["Stats"] = models.GetStatistic()
+ ctx.Data["Stats"] = activities_model.GetStatistic()
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
@@ -163,165 +154,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"] = strings.Title(setting.RunMode)
- if version, err := git.LocalVersion(); err == nil {
- ctx.Data["GitVersion"] = version.Original()
- }
- ctx.Data["RepoRootPath"] = setting.RepoRootPath
- ctx.Data["CustomRootPath"] = setting.CustomPath
- ctx.Data["StaticRootPath"] = setting.StaticRootPath
- 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 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/admin_test.go b/routers/web/admin/admin_test.go
index 1bc43998b25af..2b65ab3ea37ec 100644
--- a/routers/web/admin/admin_test.go
+++ b/routers/web/admin/admin_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package admin
diff --git a/routers/web/admin/applications.go b/routers/web/admin/applications.go
new file mode 100644
index 0000000000000..745d17ff2a442
--- /dev/null
+++ b/routers/web/admin/applications.go
@@ -0,0 +1,92 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "fmt"
+ "net/http"
+
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
+ user_setting "code.gitea.io/gitea/routers/web/user/setting"
+)
+
+var (
+ tplSettingsApplications base.TplName = "admin/applications/list"
+ tplSettingsOauth2ApplicationEdit base.TplName = "admin/applications/oauth2_edit"
+)
+
+func newOAuth2CommonHandlers() *user_setting.OAuth2CommonHandlers {
+ return &user_setting.OAuth2CommonHandlers{
+ OwnerID: 0,
+ BasePathList: fmt.Sprintf("%s/admin/applications", setting.AppSubURL),
+ BasePathEditPrefix: fmt.Sprintf("%s/admin/applications/oauth2", setting.AppSubURL),
+ TplAppEdit: tplSettingsOauth2ApplicationEdit,
+ }
+}
+
+// Applications render org applications page (for org, at the moment, there are only OAuth2 applications)
+func Applications(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings.applications")
+ ctx.Data["PageIsAdmin"] = true
+ ctx.Data["PageIsAdminApplications"] = true
+
+ apps, err := auth.GetOAuth2ApplicationsByUserID(ctx, 0)
+ if err != nil {
+ ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
+ return
+ }
+ ctx.Data["Applications"] = apps
+
+ ctx.HTML(http.StatusOK, tplSettingsApplications)
+}
+
+// ApplicationsPost response for adding an oauth2 application
+func ApplicationsPost(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings.applications")
+ ctx.Data["PageIsAdmin"] = true
+ ctx.Data["PageIsAdminApplications"] = true
+
+ oa := newOAuth2CommonHandlers()
+ oa.AddApp(ctx)
+}
+
+// EditApplication displays the given application
+func EditApplication(ctx *context.Context) {
+ ctx.Data["PageIsAdmin"] = true
+ ctx.Data["PageIsAdminApplications"] = true
+
+ oa := newOAuth2CommonHandlers()
+ oa.EditShow(ctx)
+}
+
+// EditApplicationPost response for editing oauth2 application
+func EditApplicationPost(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings.applications")
+ ctx.Data["PageIsAdmin"] = true
+ ctx.Data["PageIsAdminApplications"] = true
+
+ oa := newOAuth2CommonHandlers()
+ oa.EditSave(ctx)
+}
+
+// ApplicationsRegenerateSecret handles the post request for regenerating the secret
+func ApplicationsRegenerateSecret(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsAdmin"] = true
+ ctx.Data["PageIsAdminApplications"] = true
+
+ oa := newOAuth2CommonHandlers()
+ oa.RegenerateSecret(ctx)
+}
+
+// DeleteApplication deletes the given oauth2 application
+func DeleteApplication(ctx *context.Context) {
+ oa := newOAuth2CommonHandlers()
+ oa.DeleteApp(ctx)
+}
+
+// TODO: revokes the grant with the given id
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 1d72a88aa1bd9..1bc166902c7bd 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -1,6 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package admin
@@ -112,7 +111,7 @@ func NewAuthSource(ctx *context.Context) {
ctx.Data["SSPIDefaultLanguage"] = ""
// only the first as default
- ctx.Data["oauth2_provider"] = oauth2providers[0].Name
+ ctx.Data["oauth2_provider"] = oauth2providers[0].Name()
ctx.HTML(http.StatusOK, tplAuthNew)
}
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
new file mode 100644
index 0000000000000..1f71e81785a99
--- /dev/null
+++ b/routers/web/admin/config.go
@@ -0,0 +1,246 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "strconv"
+ "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"
+ "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 check, ok := changeConfigChecks[key]; ok {
+ if err := check(ctx, value); err != nil {
+ log.Warn("refused to set setting: %v", err)
+ ctx.JSON(http.StatusOK, map[string]string{
+ "err": ctx.Tr("admin.config.set_setting_failed", key),
+ })
+ return
+ }
+ }
+
+ if err := system_model.SetSetting(&system_model.Setting{
+ SettingKey: key,
+ SettingValue: value,
+ Version: 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,
+ })
+}
+
+var changeConfigChecks = map[string]func(ctx *context.Context, newValue string) error{
+ system_model.KeyPictureDisableGravatar: func(_ *context.Context, newValue string) error {
+ if v, _ := strconv.ParseBool(newValue); setting.OfflineMode && !v {
+ return fmt.Errorf("%q should be true when OFFLINE_MODE is true", system_model.KeyPictureDisableGravatar)
+ }
+ return nil
+ },
+ system_model.KeyPictureEnableFederatedAvatar: func(_ *context.Context, newValue string) error {
+ if v, _ := strconv.ParseBool(newValue); setting.OfflineMode && v {
+ return fmt.Errorf("%q cannot be false when OFFLINE_MODE is true", system_model.KeyPictureEnableFederatedAvatar)
+ }
+ return nil
+ },
+}
diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go
index 9482ae01235ea..c16158c6ae47f 100644
--- a/routers/web/admin/emails.go
+++ b/routers/web/admin/emails.go
@@ -1,6 +1,5 @@
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package admin
diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go
index 1483d0959dbe3..e8db9a3ded70a 100644
--- a/routers/web/admin/hooks.go
+++ b/routers/web/admin/hooks.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package admin
@@ -35,7 +34,7 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
sys["Title"] = ctx.Tr("admin.systemhooks")
sys["Description"] = ctx.Tr("admin.systemhooks.desc")
- sys["Webhooks"], err = webhook.GetSystemWebhooks(util.OptionalBoolNone)
+ sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
sys["BaseLink"] = setting.AppSubURL + "/admin/hooks"
sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks"
if err != nil {
@@ -45,7 +44,7 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
def["Title"] = ctx.Tr("admin.defaulthooks")
def["Description"] = ctx.Tr("admin.defaulthooks.desc")
- def["Webhooks"], err = webhook.GetDefaultWebhooks()
+ def["Webhooks"], err = webhook.GetDefaultWebhooks(ctx)
def["BaseLink"] = setting.AppSubURL + "/admin/hooks"
def["BaseLinkNew"] = setting.AppSubURL + "/admin/default-hooks"
if err != nil {
diff --git a/routers/web/admin/main_test.go b/routers/web/admin/main_test.go
index 4e6ad4d743f60..ea79830fa1358 100644
--- a/routers/web/admin/main_test.go
+++ b/routers/web/admin/main_test.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package admin
diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go
index b50549b80454f..f60cf90b3c74a 100644
--- a/routers/web/admin/notice.go
+++ b/routers/web/admin/notice.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package admin
@@ -9,7 +8,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 +25,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 +56,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 +67,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/admin/orgs.go b/routers/web/admin/orgs.go
index 6081ab9b1c6bd..a4eb0af58d0c8 100644
--- a/routers/web/admin/orgs.go
+++ b/routers/web/admin/orgs.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package admin
diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go
index 79bf025dd28c3..7c6d1ed840f51 100644
--- a/routers/web/admin/packages.go
+++ b/routers/web/admin/packages.go
@@ -1,6 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package admin
@@ -13,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
packages_service "code.gitea.io/gitea/services/packages"
)
@@ -31,9 +31,10 @@ func Packages(ctx *context.Context) {
sort := ctx.FormTrim("sort")
pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
- Type: packages_model.Type(packageType),
- Name: packages_model.SearchValue{Value: query},
- Sort: sort,
+ Type: packages_model.Type(packageType),
+ Name: packages_model.SearchValue{Value: query},
+ Sort: sort,
+ IsInternal: util.OptionalBoolFalse,
Paginator: &db.ListOptions{
PageSize: setting.UI.PackagesPagingNum,
Page: page,
@@ -61,6 +62,7 @@ func Packages(ctx *context.Context) {
ctx.Data["PageIsAdminPackages"] = true
ctx.Data["Query"] = query
ctx.Data["PackageType"] = packageType
+ ctx.Data["AvailableTypes"] = packages_model.TypeList
ctx.Data["SortType"] = sort
ctx.Data["PackageDescriptors"] = pds
ctx.Data["Total"] = total
diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go
index fb7be12c35888..dc5432c54998c 100644
--- a/routers/web/admin/repos.go
+++ b/routers/web/admin/repos.go
@@ -1,6 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package admin
@@ -9,13 +8,13 @@ import (
"net/url"
"strings"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/web/explore"
@@ -42,7 +41,7 @@ func Repos(ctx *context.Context) {
// DeleteRepo delete one repository
func DeleteRepo(ctx *context.Context) {
- repo, err := repo_model.GetRepositoryByID(ctx.FormInt64("id"))
+ repo, err := repo_model.GetRepositoryByID(ctx, ctx.FormInt64("id"))
if err != nil {
ctx.ServerError("GetRepositoryByID", err)
return
@@ -101,7 +100,7 @@ func UnadoptedRepos(ctx *context.Context) {
ctx.ServerError("ListUnadoptedRepositories", err)
}
ctx.Data["Dirs"] = repoNames
- pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
+ pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "search", "search")
ctx.Data["Page"] = pager
@@ -121,7 +120,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) {
return
}
- ctxUser, err := user_model.GetUserByName(dirSplit[0])
+ ctxUser, err := user_model.GetUserByName(ctx, dirSplit[0])
if err != nil {
if user_model.IsErrUserNotExist(err) {
log.Debug("User does not exist: %s", dirSplit[0])
@@ -135,7 +134,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) {
repoName := dirSplit[1]
// check not a repo
- has, err := repo_model.IsRepositoryExist(ctxUser, repoName)
+ has, err := repo_model.IsRepositoryExist(ctx, ctxUser, repoName)
if err != nil {
ctx.ServerError("IsRepositoryExist", err)
return
@@ -148,7 +147,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) {
if has || !isDir {
// Fallthrough to failure mode
} else if action == "adopt" {
- if _, err := repo_service.AdoptRepository(ctx.Doer, ctxUser, models.CreateRepoOptions{
+ if _, err := repo_service.AdoptRepository(ctx.Doer, ctxUser, repo_module.CreateRepoOptions{
Name: dirSplit[1],
IsPrivate: true,
}); err != nil {
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index fcfea53801289..38969dadaadc8 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package admin
@@ -125,10 +124,14 @@ func NewUserPost(ctx *context.Context) {
Name: form.UserName,
Email: form.Email,
Passwd: form.Password,
- IsActive: true,
LoginType: auth.Plain,
}
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsActive: util.OptionalBoolTrue,
+ Visibility: &form.Visibility,
+ }
+
if len(form.LoginType) > 0 {
fields := strings.Split(form.LoginType, "-")
if len(fields) == 2 {
@@ -163,7 +166,7 @@ func NewUserPost(ctx *context.Context) {
u.MustChangePassword = form.MustChangePassword
}
- if err := user_model.CreateUser(u, &user_model.CreateUserOverwriteOptions{Visibility: form.Visibility}); err != nil {
+ if err := user_model.CreateUser(u, overwriteDefault); err != nil {
switch {
case user_model.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
@@ -203,9 +206,13 @@ func NewUserPost(ctx *context.Context) {
}
func prepareUserInfo(ctx *context.Context) *user_model.User {
- u, err := user_model.GetUserByID(ctx.ParamsInt64(":userid"))
+ u, err := user_model.GetUserByID(ctx, ctx.ParamsInt64(":userid"))
if err != nil {
- ctx.ServerError("GetUserByID", err)
+ if user_model.IsErrUserNotExist(err) {
+ ctx.Redirect(setting.AppSubURL + "/admin/users")
+ } else {
+ ctx.ServerError("GetUserByID", err)
+ }
return nil
}
ctx.Data["User"] = u
@@ -385,7 +392,7 @@ func EditUserPost(ctx *context.Context) {
u.ProhibitLogin = form.ProhibitLogin
}
- if err := user_model.UpdateUser(u, emailChanged); err != nil {
+ if err := user_model.UpdateUser(ctx, u, emailChanged); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) {
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
@@ -406,29 +413,30 @@ func EditUserPost(ctx *context.Context) {
// DeleteUser response for deleting a user
func DeleteUser(ctx *context.Context) {
- u, err := user_model.GetUserByID(ctx.ParamsInt64(":userid"))
+ u, err := user_model.GetUserByID(ctx, ctx.ParamsInt64(":userid"))
if err != nil {
ctx.ServerError("GetUserByID", err)
return
}
- if err = user_service.DeleteUser(u); err != nil {
+ // admin should not delete themself
+ if u.ID == ctx.Doer.ID {
+ ctx.Flash.Error(ctx.Tr("admin.users.cannot_delete_self"))
+ ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
+ return
+ }
+
+ if err = user_service.DeleteUser(ctx, u, ctx.FormBool("purge")); err != nil {
switch {
case models.IsErrUserOwnRepos(err):
ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")),
- })
+ ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
case models.IsErrUserHasOrgs(err):
ctx.Flash.Error(ctx.Tr("admin.users.still_has_org"))
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")),
- })
+ ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.Params(":userid")))
case models.IsErrUserOwnPackages(err):
ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages"))
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"),
- })
+ ctx.Redirect(setting.AppSubURL + "/admin/users/" + ctx.Params(":userid"))
default:
ctx.ServerError("DeleteUser", err)
}
@@ -437,9 +445,7 @@ func DeleteUser(ctx *context.Context) {
log.Trace("Account deleted by admin (%s): %s", ctx.Doer.Name, u.Name)
ctx.Flash.Success(ctx.Tr("admin.users.deletion_success"))
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": setting.AppSubURL + "/admin/users",
- })
+ ctx.Redirect(setting.AppSubURL + "/admin/users")
}
// AvatarPost response for change user's avatar request
diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go
index 9de548685cef6..ed58a54eef939 100644
--- a/routers/web/admin/users_test.go
+++ b/routers/web/admin/users_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package admin
@@ -25,7 +24,7 @@ func TestNewUserPost_MustChangePassword(t *testing.T) {
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{
IsAdmin: true,
ID: 2,
- }).(*user_model.User)
+ })
ctx.Doer = u
@@ -47,7 +46,7 @@ func TestNewUserPost_MustChangePassword(t *testing.T) {
assert.NotEmpty(t, ctx.Flash.SuccessMsg)
- u, err := user_model.GetUserByName(username)
+ u, err := user_model.GetUserByName(ctx, username)
assert.NoError(t, err)
assert.Equal(t, username, u.Name)
@@ -62,7 +61,7 @@ func TestNewUserPost_MustChangePasswordFalse(t *testing.T) {
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{
IsAdmin: true,
ID: 2,
- }).(*user_model.User)
+ })
ctx.Doer = u
@@ -84,7 +83,7 @@ func TestNewUserPost_MustChangePasswordFalse(t *testing.T) {
assert.NotEmpty(t, ctx.Flash.SuccessMsg)
- u, err := user_model.GetUserByName(username)
+ u, err := user_model.GetUserByName(ctx, username)
assert.NoError(t, err)
assert.Equal(t, username, u.Name)
@@ -99,7 +98,7 @@ func TestNewUserPost_InvalidEmail(t *testing.T) {
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{
IsAdmin: true,
ID: 2,
- }).(*user_model.User)
+ })
ctx.Doer = u
@@ -129,7 +128,7 @@ func TestNewUserPost_VisibilityDefaultPublic(t *testing.T) {
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{
IsAdmin: true,
ID: 2,
- }).(*user_model.User)
+ })
ctx.Doer = u
@@ -151,7 +150,7 @@ func TestNewUserPost_VisibilityDefaultPublic(t *testing.T) {
assert.NotEmpty(t, ctx.Flash.SuccessMsg)
- u, err := user_model.GetUserByName(username)
+ u, err := user_model.GetUserByName(ctx, username)
assert.NoError(t, err)
assert.Equal(t, username, u.Name)
@@ -167,7 +166,7 @@ func TestNewUserPost_VisibilityPrivate(t *testing.T) {
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{
IsAdmin: true,
ID: 2,
- }).(*user_model.User)
+ })
ctx.Doer = u
@@ -190,7 +189,7 @@ func TestNewUserPost_VisibilityPrivate(t *testing.T) {
assert.NotEmpty(t, ctx.Flash.SuccessMsg)
- u, err := user_model.GetUserByName(username)
+ u, err := user_model.GetUserByName(ctx, username)
assert.NoError(t, err)
assert.Equal(t, username, u.Name)
diff --git a/routers/web/auth.go b/routers/web/auth.go
index 4a7fb856be2e7..1ca860ecc8dea 100644
--- a/routers/web/auth.go
+++ b/routers/web/auth.go
@@ -1,9 +1,7 @@
// 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.
+// SPDX-License-Identifier: MIT
//go:build !windows
-// +build !windows
package web
diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go
index c61922cd9da09..4791b043131db 100644
--- a/routers/web/auth/2fa.go
+++ b/routers/web/auth/2fa.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
@@ -69,7 +68,7 @@ func TwoFactorPost(ctx *context.Context) {
if ok && twofa.LastUsedPasscode != form.Passcode {
remember := ctx.Session.Get("twofaRemember").(bool)
- u, err := user_model.GetUserByID(id)
+ u, err := user_model.GetUserByID(ctx, id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
@@ -147,7 +146,7 @@ func TwoFactorScratchPost(ctx *context.Context) {
}
remember := ctx.Session.Get("twofaRemember").(bool)
- u, err := user_model.GetUserByID(id)
+ u, err := user_model.GetUserByID(ctx, id)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index c82fde49eb2f4..71a62bce65420 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -1,11 +1,11 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package auth
import (
+ "errors"
"fmt"
"net/http"
"strings"
@@ -16,13 +16,12 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource"
- "code.gitea.io/gitea/modules/hcaptcha"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/password"
- "code.gitea.io/gitea/modules/recaptcha"
"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"
@@ -64,10 +63,10 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
}
}()
- u, err := user_model.GetUserByName(uname)
+ u, err := user_model.GetUserByName(ctx, uname)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
- return false, fmt.Errorf("GetUserByName: %v", err)
+ return false, fmt.Errorf("GetUserByName: %w", err)
}
return false, nil
}
@@ -79,19 +78,12 @@ func AutoSignIn(ctx *context.Context) (bool, error) {
isSucceed = true
- if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
- return false, fmt.Errorf("unable to RegenerateSession: Error: %w", err)
- }
-
- // Set session IDs
- if err := ctx.Session.Set("uid", u.ID); err != nil {
- return false, err
- }
- if err := ctx.Session.Set("uname", u.Name); err != nil {
- return false, err
- }
- if err := ctx.Session.Release(); err != nil {
- return false, err
+ if err := updateSession(ctx, nil, map[string]interface{}{
+ // Set session IDs
+ "uid": u.ID,
+ "uname": u.Name,
+ }); err != nil {
+ return false, fmt.Errorf("unable to updateSession: %w", err)
}
if err := resetLocale(ctx, u); err != nil {
@@ -167,6 +159,10 @@ func SignIn(ctx *context.Context) {
ctx.Data["PageIsLogin"] = true
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled()
+ if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
+ context.SetCaptchaData(ctx)
+ }
+
ctx.HTML(http.StatusOK, tplSignIn)
}
@@ -193,6 +189,16 @@ func SignInPost(ctx *context.Context) {
}
form := web.GetForm(ctx).(*forms.SignInForm)
+
+ if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
+ context.SetCaptchaData(ctx)
+
+ context.VerifyCaptcha(ctx, tplSignIn, form)
+ if ctx.Written() {
+ return
+ }
+ }
+
u, source, err := auth_service.UserSignIn(form.UserName, form.Password)
if err != nil {
if user_model.IsErrUserNotExist(err) || user_model.IsErrEmailAddressNotExist(err) {
@@ -249,36 +255,21 @@ func SignInPost(ctx *context.Context) {
return
}
- if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
- ctx.ServerError("UserSignIn: Unable to set regenerate session", err)
- return
- }
-
- // User will need to use 2FA TOTP or WebAuthn, save data
- if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
- ctx.ServerError("UserSignIn: Unable to set twofaUid in session", err)
- return
+ updates := map[string]interface{}{
+ // User will need to use 2FA TOTP or WebAuthn, save data
+ "twofaUid": u.ID,
+ "twofaRemember": form.Remember,
}
-
- if err := ctx.Session.Set("twofaRemember", form.Remember); err != nil {
- ctx.ServerError("UserSignIn: Unable to set twofaRemember in session", err)
- return
- }
-
if hasTOTPtwofa {
- // User will need to use U2F, save data
- if err := ctx.Session.Set("totpEnrolled", u.ID); err != nil {
- ctx.ServerError("UserSignIn: Unable to set WebAuthn Enrolled in session", err)
- return
- }
+ // User will need to use WebAuthn, save data
+ updates["totpEnrolled"] = u.ID
}
-
- if err := ctx.Session.Release(); err != nil {
- ctx.ServerError("UserSignIn: Unable to save session", err)
+ if err := updateSession(ctx, nil, updates); err != nil {
+ ctx.ServerError("UserSignIn: Unable to update session", err)
return
}
- // If we have U2F redirect there first
+ // If we have WebAuthn redirect there first
if hasWebAuthnTwofa {
ctx.Redirect(setting.AppSubURL + "/user/webauthn")
return
@@ -305,30 +296,23 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
setting.CookieRememberName, u.Name, days)
}
- if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
+ if err := updateSession(ctx, []string{
+ // Delete the openid, 2fa and linkaccount data
+ "openid_verified_uri",
+ "openid_signin_remember",
+ "openid_determined_email",
+ "openid_determined_username",
+ "twofaUid",
+ "twofaRemember",
+ "linkAccount",
+ }, map[string]interface{}{
+ "uid": u.ID,
+ "uname": u.Name,
+ }); err != nil {
ctx.ServerError("RegenerateSession", err)
return setting.AppSubURL + "/"
}
- // Delete the openid, 2fa and linkaccount data
- _ = ctx.Session.Delete("openid_verified_uri")
- _ = ctx.Session.Delete("openid_signin_remember")
- _ = ctx.Session.Delete("openid_determined_email")
- _ = ctx.Session.Delete("openid_determined_username")
- _ = ctx.Session.Delete("twofaUid")
- _ = ctx.Session.Delete("twofaRemember")
- _ = ctx.Session.Delete("u2fChallenge")
- _ = ctx.Session.Delete("linkAccount")
- if err := ctx.Session.Set("uid", u.ID); err != nil {
- log.Error("Error setting uid %d in session: %v", u.ID, err)
- }
- if err := ctx.Session.Set("uname", u.Name); err != nil {
- log.Error("Error setting uname %s session: %v", u.Name, err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Unable to store session: %v", err)
- }
-
// Language setting of the user overwrites the one previously set
// If the user does not have a locale set, we save the current one.
if len(u.Language) == 0 {
@@ -409,12 +393,7 @@ func SignUp(ctx *context.Context) {
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
- ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
- ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
- ctx.Data["Captcha"] = context.GetImageCaptcha()
- ctx.Data["CaptchaType"] = setting.Service.CaptchaType
- ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
- ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+ context.SetCaptchaData(ctx)
ctx.Data["PageIsSignUp"] = true
// Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true
@@ -430,12 +409,7 @@ func SignUpPost(ctx *context.Context) {
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
- ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
- ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
- ctx.Data["Captcha"] = context.GetImageCaptcha()
- ctx.Data["CaptchaType"] = setting.Service.CaptchaType
- ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
- ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+ context.SetCaptchaData(ctx)
ctx.Data["PageIsSignUp"] = true
// Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
@@ -449,29 +423,9 @@ func SignUpPost(ctx *context.Context) {
return
}
- if setting.Service.EnableCaptcha {
- var valid bool
- var err error
- switch setting.Service.CaptchaType {
- case setting.ImageCaptcha:
- valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
- case setting.ReCaptcha:
- valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
- case setting.HCaptcha:
- valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
- default:
- ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
- return
- }
- if err != nil {
- log.Debug("%s", err.Error())
- }
-
- if !valid {
- ctx.Data["Err_Captcha"] = true
- ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUp, &form)
- return
- }
+ context.VerifyCaptcha(ctx, tplSignUp, form)
+ if ctx.Written() {
+ return
}
if !form.IsEmailDomainAllowed() {
@@ -507,14 +461,12 @@ func SignUpPost(ctx *context.Context) {
}
u := &user_model.User{
- Name: form.UserName,
- Email: form.Email,
- Passwd: form.Password,
- IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
- IsRestricted: setting.Service.DefaultUserIsRestricted,
+ Name: form.UserName,
+ Email: form.Email,
+ Passwd: form.Password,
}
- if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, false) {
+ if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil, false) {
// error already handled
return
}
@@ -525,8 +477,8 @@ func SignUpPost(ctx *context.Context) {
// createAndHandleCreatedUser calls createUserInContext and
// then handleUserCreated.
-func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form interface{}, u *user_model.User, gothUser *goth.User, allowLink bool) bool {
- if !createUserInContext(ctx, tpl, form, u, gothUser, allowLink) {
+func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form interface{}, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) bool {
+ if !createUserInContext(ctx, tpl, form, u, overwrites, gothUser, allowLink) {
return false
}
return handleUserCreated(ctx, u, gothUser)
@@ -534,8 +486,8 @@ func createAndHandleCreatedUser(ctx *context.Context, tpl base.TplName, form int
// createUserInContext creates a user and handles errors within a given context.
// Optionally a template can be specified.
-func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{}, u *user_model.User, gothUser *goth.User, allowLink bool) (ok bool) {
- if err := user_model.CreateUser(u); err != nil {
+func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{}, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) {
+ if err := user_model.CreateUser(u, overwrites); err != nil {
if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto {
var user *user_model.User
@@ -602,7 +554,7 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{
// sends a confirmation email if required.
func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
// Auto-set admin for the only user.
- if user_model.CountUsers() == 1 {
+ if user_model.CountUsers(nil) == 1 {
u.IsAdmin = true
u.IsActive = true
u.SetLastLogin()
@@ -615,7 +567,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)
+ }
}
}
@@ -631,11 +585,13 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
ctx.Data["IsSendRegisterMail"] = true
ctx.Data["Email"] = u.Email
- ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
+ ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
ctx.HTML(http.StatusOK, TplActivate)
- if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
+ if setting.CacheService.Enabled {
+ if err := ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ }
}
return
}
@@ -655,14 +611,16 @@ func Activate(ctx *context.Context) {
}
// Resend confirmation email.
if setting.Service.RegisterEmailConfirm {
- if ctx.Cache.IsExist("MailResendLimit_" + ctx.Doer.LowerName) {
+ if setting.CacheService.Enabled && ctx.Cache.IsExist("MailResendLimit_"+ctx.Doer.LowerName) {
ctx.Data["ResendLimited"] = true
} else {
- ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
+ ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
- if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
+ if setting.CacheService.Enabled {
+ if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ }
}
}
} else {
@@ -750,27 +708,27 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
log.Trace("User activated: %s", user.Name)
- if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
+ if err := updateSession(ctx, nil, map[string]interface{}{
+ "uid": user.ID,
+ "uname": user.Name,
+ }); err != nil {
log.Error("Unable to regenerate session for user: %-v with email: %s: %v", user, user.Email, err)
ctx.ServerError("ActivateUserEmail", err)
return
}
- if err := ctx.Session.Set("uid", user.ID); err != nil {
- log.Error("Error setting uid in session[%s]: %v", ctx.Session.ID(), err)
- }
- if err := ctx.Session.Set("uname", user.Name); err != nil {
- log.Error("Error setting uname in session[%s]: %v", ctx.Session.ID(), err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Error storing session[%s]: %v", ctx.Session.ID(), err)
- }
-
if err := resetLocale(ctx, user); err != nil {
ctx.ServerError("resetLocale", err)
return
}
+ // Register last login
+ user.SetLastLogin()
+ if err := user_model.UpdateUserCols(ctx, user, "last_login_unix"); err != nil {
+ ctx.ServerError("UpdateUserCols", err)
+ return
+ }
+
ctx.Flash.Success(ctx.Tr("auth.account_activated"))
ctx.Redirect(setting.AppSubURL + "/")
}
@@ -789,9 +747,9 @@ func ActivateEmail(ctx *context.Context) {
log.Trace("Email activated: %s", email.Email)
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
- if u, err := user_model.GetUserByID(email.UID); err != nil {
+ if u, err := user_model.GetUserByID(ctx, email.UID); err != nil {
log.Warn("GetUserByID: %d", email.UID)
- } else {
+ } else if setting.CacheService.Enabled {
// Allow user to validate more emails
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
}
@@ -802,3 +760,25 @@ func ActivateEmail(ctx *context.Context) {
// Should users be logged in automatically here? (consider 2FA requirements, etc.)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
+
+func updateSession(ctx *context.Context, deletes []string, updates map[string]interface{}) error {
+ if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
+ return fmt.Errorf("regenerate session: %w", err)
+ }
+ sess := ctx.Session
+ sessID := sess.ID()
+ for _, k := range deletes {
+ if err := sess.Delete(k); err != nil {
+ return fmt.Errorf("delete %v in session[%s]: %w", k, sessID, err)
+ }
+ }
+ for k, v := range updates {
+ if err := sess.Set(k, v); err != nil {
+ return fmt.Errorf("set %v in session[%s]: %w", k, sessID, err)
+ }
+ }
+ if err := sess.Release(); err != nil {
+ return fmt.Errorf("store session[%s]: %w", sessID, err)
+ }
+ return nil
+}
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index bf5fb83265ba4..6c409c6b9df8b 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
@@ -14,10 +13,6 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/hcaptcha"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/recaptcha"
- "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
auth_service "code.gitea.io/gitea/services/auth"
@@ -40,6 +35,8 @@ func LinkAccount(ctx *context.Context) {
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+ ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
+ ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["ShowRegistrationButton"] = false
@@ -70,7 +67,7 @@ func LinkAccount(ctx *context.Context) {
ctx.Data["user_exists"] = true
}
} else if len(uname) != 0 {
- u, err := user_model.GetUserByName(uname)
+ u, err := user_model.GetUserByName(ctx, uname)
if err != nil && !user_model.IsErrUserNotExist(err) {
ctx.ServerError("UserSignIn", err)
return
@@ -96,6 +93,8 @@ func LinkAccountPostSignIn(ctx *context.Context) {
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+ ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
+ ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["ShowRegistrationButton"] = false
@@ -151,25 +150,16 @@ func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, r
return
}
- if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
+ if err := updateSession(ctx, nil, map[string]interface{}{
+ // User needs to use 2FA, save data and redirect to 2FA page.
+ "twofaUid": u.ID,
+ "twofaRemember": remember,
+ "linkAccount": true,
+ }); err != nil {
ctx.ServerError("RegenerateSession", err)
return
}
- // User needs to use 2FA, save data and redirect to 2FA page.
- if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
- log.Error("Error setting twofaUid in session: %v", err)
- }
- if err := ctx.Session.Set("twofaRemember", remember); err != nil {
- log.Error("Error setting twofaRemember in session: %v", err)
- }
- if err := ctx.Session.Set("linkAccount", true); err != nil {
- log.Error("Error setting linkAccount in session: %v", err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Error storing session: %v", err)
- }
-
// If WebAuthn is enrolled -> Redirect to WebAuthn instead
regs, err := auth.GetWebAuthnCredentialsByUID(u.ID)
if err == nil && len(regs) > 0 {
@@ -195,6 +185,8 @@ func LinkAccountPostRegister(ctx *context.Context) {
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+ ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
+ ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["ShowRegistrationButton"] = false
@@ -224,26 +216,8 @@ func LinkAccountPostRegister(ctx *context.Context) {
}
if setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha {
- var valid bool
- var err error
- switch setting.Service.CaptchaType {
- case setting.ImageCaptcha:
- valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
- case setting.ReCaptcha:
- valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
- case setting.HCaptcha:
- valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
- default:
- ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
- return
- }
- if err != nil {
- log.Debug("%s", err.Error())
- }
-
- if !valid {
- ctx.Data["Err_Captcha"] = true
- ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplLinkAccount, &form)
+ context.VerifyCaptcha(ctx, tplLinkAccount, form)
+ if ctx.Written() {
return
}
}
@@ -283,13 +257,12 @@ func LinkAccountPostRegister(ctx *context.Context) {
Name: form.UserName,
Email: form.Email,
Passwd: form.Password,
- IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
LoginType: auth.OAuth2,
LoginSource: authSource.ID,
LoginName: gothUser.UserID,
}
- if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, &gothUser, false) {
+ if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &gothUser, false) {
// error already handled
return
}
diff --git a/routers/web/auth/main_test.go b/routers/web/auth/main_test.go
index 71f522fb07540..8295515ba9506 100644
--- a/routers/web/auth/main_test.go
+++ b/routers/web/auth/main_test.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package auth
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 12de208ad7bc1..3d70ca9a50677 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -1,10 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
import (
+ stdContext "context"
"encoding/base64"
"errors"
"fmt"
@@ -14,16 +14,16 @@ import (
"net/url"
"strings"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/auth"
+ 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/context"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
- "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"
auth_service "code.gitea.io/gitea/services/auth"
@@ -35,6 +35,7 @@ import (
"gitea.com/go-chi/binding"
"github.com/golang-jwt/jwt/v4"
"github.com/markbates/goth"
+ "github.com/markbates/goth/gothic"
)
const (
@@ -45,6 +46,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 (
@@ -65,6 +67,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
@@ -77,6 +80,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 (
@@ -95,6 +99,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"`
@@ -126,6 +131,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"`
@@ -134,9 +140,9 @@ type AccessTokenResponse struct {
IDToken string `json:"id_token,omitempty"`
}
-func newAccessTokenResponse(grant *auth.OAuth2Grant, serverKey, clientKey oauth2.JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
+func newAccessTokenResponse(ctx stdContext.Context, grant *auth.OAuth2Grant, serverKey, clientKey oauth2.JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
if setting.OAuth2.InvalidateRefreshTokens {
- if err := grant.IncreaseCounter(); err != nil {
+ if err := grant.IncreaseCounter(ctx); err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "cannot increase the grant counter",
@@ -166,7 +172,7 @@ func newAccessTokenResponse(grant *auth.OAuth2Grant, serverKey, clientKey oauth2
GrantID: grant.ID,
Counter: grant.Counter,
Type: oauth2.TypeRefreshToken,
- RegisteredClaims: jwt.RegisteredClaims{ // nolint
+ RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(refreshExpirationDate),
},
}
@@ -181,14 +187,14 @@ func newAccessTokenResponse(grant *auth.OAuth2Grant, serverKey, clientKey oauth2
// generate OpenID Connect id_token
signedIDToken := ""
if grant.ScopeContains("openid") {
- app, err := auth.GetOAuth2ApplicationByID(grant.ApplicationID)
+ app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
if err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot find application",
}
}
- user, err := user_model.GetUserByID(grant.UserID)
+ user, err := user_model.GetUserByID(ctx, grant.UserID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
return nil, &AccessTokenError{
@@ -213,7 +219,7 @@ func newAccessTokenResponse(grant *auth.OAuth2Grant, serverKey, clientKey oauth2
Nonce: grant.Nonce,
}
if grant.ScopeContains("profile") {
- idToken.Name = user.FullName
+ idToken.Name = user.GetDisplayName()
idToken.PreferredUsername = user.Name
idToken.Profile = user.HTMLURL()
idToken.Picture = user.AvatarLink()
@@ -293,9 +299,9 @@ func InfoOAuth(ctx *context.Context) {
// returns a list of "org" and "org:team" strings,
// that the given user is a part of.
func getOAuthGroupsForUser(user *user_model.User) ([]string, error) {
- orgs, err := models.GetUserOrgsList(user)
+ orgs, err := org_model.GetUserOrgsList(user)
if err != nil {
- return nil, fmt.Errorf("GetUserOrgList: %v", err)
+ return nil, fmt.Errorf("GetUserOrgList: %w", err)
}
var groups []string
@@ -303,7 +309,7 @@ func getOAuthGroupsForUser(user *user_model.User) ([]string, error) {
groups = append(groups, org.Name)
teams, err := org.LoadTeams()
if err != nil {
- return nil, fmt.Errorf("LoadTeams: %v", err)
+ return nil, fmt.Errorf("LoadTeams: %w", err)
}
for _, team := range teams {
if team.IsMember(user.ID) {
@@ -332,9 +338,9 @@ func IntrospectOAuth(ctx *context.Context) {
token, err := oauth2.ParseToken(form.Token, oauth2.DefaultSigningKey)
if err == nil {
if token.Valid() == nil {
- grant, err := auth.GetOAuth2GrantByID(token.GrantID)
+ grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID)
if err == nil && grant != nil {
- app, err := auth.GetOAuth2ApplicationByID(grant.ApplicationID)
+ app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
if err == nil && app != nil {
response.Active = true
response.Scope = grant.Scope
@@ -363,7 +369,7 @@ func AuthorizeOAuth(ctx *context.Context) {
return
}
- app, err := auth.GetOAuth2ApplicationByClientID(form.ClientID)
+ app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
if err != nil {
if auth.IsErrOauthClientIDInvalid(err) {
handleAuthorizeError(ctx, AuthorizeError{
@@ -377,10 +383,13 @@ func AuthorizeOAuth(ctx *context.Context) {
return
}
- user, err := user_model.GetUserByID(app.UID)
- if err != nil {
- ctx.ServerError("GetUserByID", err)
- return
+ var user *user_model.User
+ if app.UID != 0 {
+ user, err = user_model.GetUserByID(ctx, app.UID)
+ if err != nil {
+ ctx.ServerError("GetUserByID", err)
+ return
+ }
}
if !app.ContainsRedirectURI(form.RedirectURI) {
@@ -427,8 +436,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",
@@ -437,7 +459,7 @@ func AuthorizeOAuth(ctx *context.Context) {
return
}
- grant, err := app.GetGrantByUserID(ctx.Doer.ID)
+ grant, err := app.GetGrantByUserID(ctx, ctx.Doer.ID)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
@@ -445,7 +467,7 @@ func AuthorizeOAuth(ctx *context.Context) {
// Redirect if user already granted access
if grant != nil {
- code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
+ code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
@@ -457,7 +479,7 @@ func AuthorizeOAuth(ctx *context.Context) {
}
// Update nonce to reflect the new session
if len(form.Nonce) > 0 {
- err := grant.SetNonce(form.Nonce)
+ err := grant.SetNonce(ctx, form.Nonce)
if err != nil {
log.Error("Unable to update nonce: %v", err)
}
@@ -472,7 +494,11 @@ func AuthorizeOAuth(ctx *context.Context) {
ctx.Data["State"] = form.State
ctx.Data["Scope"] = form.Scope
ctx.Data["Nonce"] = form.Nonce
- ctx.Data["ApplicationUserLinkHTML"] = "@" + html.EscapeString(user.Name) + " "
+ if user != nil {
+ ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`@%s `, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name))
+ } else {
+ ctx.Data["ApplicationCreatorLinkHTML"] = fmt.Sprintf(`%s `, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName))
+ }
ctx.Data["ApplicationRedirectDomainHTML"] = "" + html.EscapeString(form.RedirectURI) + " "
// TODO document SESSION <=> FORM
err = ctx.Session.Set("client_id", app.ClientID)
@@ -509,12 +535,12 @@ func GrantApplicationOAuth(ctx *context.Context) {
ctx.Error(http.StatusBadRequest)
return
}
- app, err := auth.GetOAuth2ApplicationByClientID(form.ClientID)
+ app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
if err != nil {
ctx.ServerError("GetOAuth2ApplicationByClientID", err)
return
}
- grant, err := app.CreateGrant(ctx.Doer.ID, form.Scope)
+ grant, err := app.CreateGrant(ctx, ctx.Doer.ID, form.Scope)
if err != nil {
handleAuthorizeError(ctx, AuthorizeError{
State: form.State,
@@ -524,7 +550,7 @@ func GrantApplicationOAuth(ctx *context.Context) {
return
}
if len(form.Nonce) > 0 {
- err := grant.SetNonce(form.Nonce)
+ err := grant.SetNonce(ctx, form.Nonce)
if err != nil {
log.Error("Unable to update nonce: %v", err)
}
@@ -534,7 +560,7 @@ func GrantApplicationOAuth(ctx *context.Context) {
codeChallenge, _ = ctx.Session.Get("CodeChallenge").(string)
codeChallengeMethod, _ = ctx.Session.Get("CodeChallengeMethod").(string)
- code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, codeChallenge, codeChallengeMethod)
+ code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, codeChallenge, codeChallengeMethod)
if err != nil {
handleServerError(ctx, form.State, form.RedirectURI)
return
@@ -585,7 +611,8 @@ func OIDCKeys(ctx *context.Context) {
// AccessTokenOAuth manages all access token requests by the client
func AccessTokenOAuth(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AccessTokenForm)
- if form.ClientID == "" {
+ // if there is no ClientID or ClientSecret in the request body, fill these fields by the Authorization header and ensure the provided field matches the Authorization header
+ if form.ClientID == "" || form.ClientSecret == "" {
authHeader := ctx.Req.Header.Get("Authorization")
authContent := strings.SplitN(authHeader, " ", 2)
if len(authContent) == 2 && authContent[0] == "Basic" {
@@ -605,7 +632,21 @@ func AccessTokenOAuth(ctx *context.Context) {
})
return
}
+ if form.ClientID != "" && form.ClientID != pair[0] {
+ handleAccessTokenError(ctx, AccessTokenError{
+ ErrorCode: AccessTokenErrorCodeInvalidRequest,
+ ErrorDescription: "client_id in request body inconsistent with Authorization header",
+ })
+ return
+ }
form.ClientID = pair[0]
+ if form.ClientSecret != "" && form.ClientSecret != pair[1] {
+ handleAccessTokenError(ctx, AccessTokenError{
+ ErrorCode: AccessTokenErrorCodeInvalidRequest,
+ ErrorDescription: "client_secret in request body inconsistent with Authorization header",
+ })
+ return
+ }
form.ClientSecret = pair[1]
}
}
@@ -638,16 +679,40 @@ 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{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: "client is not authorized",
+ ErrorDescription: "unable to parse refresh token",
})
return
}
// get grant before increasing counter
- grant, err := auth.GetOAuth2GrantByID(token.GrantID)
+ grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID)
if err != nil || grant == nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidGrant,
@@ -665,7 +730,7 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
log.Warn("A client tried to use a refresh token for grant_id = %d was used twice!", grant.ID)
return
}
- accessToken, tokenErr := newAccessTokenResponse(grant, serverKey, clientKey)
+ accessToken, tokenErr := newAccessTokenResponse(ctx, grant, serverKey, clientKey)
if tokenErr != nil {
handleAccessTokenError(ctx, *tokenErr)
return
@@ -674,7 +739,7 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
}
func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2.JWTSigningKey) {
- app, err := auth.GetOAuth2ApplicationByClientID(form.ClientID)
+ app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidClient,
@@ -683,20 +748,24 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
return
}
if !app.ValidateClientSecret([]byte(form.ClientSecret)) {
+ errorDescription := "invalid client secret"
+ if form.ClientSecret == "" {
+ errorDescription = "invalid empty client secret"
+ }
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: "client is not authorized",
+ ErrorDescription: errorDescription,
})
return
}
if form.RedirectURI != "" && !app.ContainsRedirectURI(form.RedirectURI) {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: "client is not authorized",
+ ErrorDescription: "unexpected redirect URI",
})
return
}
- authorizationCode, err := auth.GetOAuth2AuthorizationByCode(form.Code)
+ authorizationCode, err := auth.GetOAuth2AuthorizationByCode(ctx, form.Code)
if err != nil || authorizationCode == nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
@@ -708,7 +777,7 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: "client is not authorized",
+ ErrorDescription: "failed PKCE code challenge",
})
return
}
@@ -721,13 +790,13 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
return
}
// remove token from database to deny duplicate usage
- if err := authorizationCode.Invalidate(); err != nil {
+ if err := authorizationCode.Invalidate(ctx); err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot proceed your request",
})
}
- resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant, serverKey, clientKey)
+ resp, tokenErr := newAccessTokenResponse(ctx, authorizationCode.Grant, serverKey, clientKey)
if tokenErr != nil {
handleAccessTokenError(ctx, *tokenErr)
return
@@ -845,7 +914,17 @@ func SignInOAuthCallback(ctx *context.Context) {
}
if u == nil {
- if !setting.Service.AllowOnlyInternalRegistration && setting.OAuth2Client.EnableAutoRegistration {
+ if ctx.Doer != nil {
+ // attach user to already logged in user
+ err = externalaccount.LinkAccountToUser(ctx.Doer, gothUser)
+ if err != nil {
+ ctx.ServerError("UserLinkAccount", err)
+ return
+ }
+
+ ctx.Redirect(setting.AppSubURL + "/user/settings/security")
+ return
+ } else if !setting.Service.AllowOnlyInternalRegistration && setting.OAuth2Client.EnableAutoRegistration {
// create new user with details from oauth2 provider
var missingFields []string
if gothUser.UserID == "" {
@@ -867,19 +946,21 @@ func SignInOAuthCallback(ctx *context.Context) {
return
}
u = &user_model.User{
- Name: getUserName(&gothUser),
- FullName: gothUser.Name,
- Email: gothUser.Email,
- IsActive: !setting.OAuth2Client.RegisterEmailConfirm,
- LoginType: auth.OAuth2,
- LoginSource: authSource.ID,
- LoginName: gothUser.UserID,
- IsRestricted: setting.Service.DefaultUserIsRestricted,
+ Name: getUserName(&gothUser),
+ FullName: gothUser.Name,
+ Email: gothUser.Email,
+ LoginType: auth.OAuth2,
+ LoginSource: authSource.ID,
+ LoginName: gothUser.UserID,
+ }
+
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm),
}
setUserGroupClaims(authSource, u, &gothUser)
- if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
+ if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
// error already handled
return
}
@@ -944,17 +1025,12 @@ func setUserGroupClaims(loginSource *auth.Source, u *user_model.User, gothUser *
}
func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
- if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
- ctx.ServerError("RegenerateSession", err)
+ if err := updateSession(ctx, nil, map[string]interface{}{
+ "linkAccountGothUser": gothUser,
+ }); err != nil {
+ ctx.ServerError("updateSession", err)
return
}
-
- if err := ctx.Session.Set("linkAccountGothUser", gothUser); err != nil {
- log.Error("Error setting linkAccountGothUser in session: %v", err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Error storing session: %v", err)
- }
ctx.Redirect(setting.AppSubURL + "/user/link_account")
}
@@ -992,21 +1068,14 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
// If this user is enrolled in 2FA and this source doesn't override it,
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
if !needs2FA {
- if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
- ctx.ServerError("RegenerateSession", err)
+ if err := updateSession(ctx, nil, map[string]interface{}{
+ "uid": u.ID,
+ "uname": u.Name,
+ }); err != nil {
+ ctx.ServerError("updateSession", err)
return
}
- if err := ctx.Session.Set("uid", u.ID); err != nil {
- log.Error("Error setting uid in session: %v", err)
- }
- if err := ctx.Session.Set("uname", u.Name); err != nil {
- log.Error("Error setting uname in session: %v", err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Error storing session: %v", err)
- }
-
// Clear whatever CSRF cookie has right now, force to generate a new one
middleware.DeleteCSRFCookie(ctx.Resp)
@@ -1027,7 +1096,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 {
@@ -1053,22 +1124,15 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
}
}
- if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
- ctx.ServerError("RegenerateSession", err)
+ if err := updateSession(ctx, nil, map[string]interface{}{
+ // User needs to use 2FA, save data and redirect to 2FA page.
+ "twofaUid": u.ID,
+ "twofaRemember": false,
+ }); err != nil {
+ ctx.ServerError("updateSession", err)
return
}
- // User needs to use 2FA, save data and redirect to 2FA page.
- if err := ctx.Session.Set("twofaUid", u.ID); err != nil {
- log.Error("Error setting twofaUid in session: %v", err)
- }
- if err := ctx.Session.Set("twofaRemember", false); err != nil {
- log.Error("Error setting twofaRemember in session: %v", err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("Error storing session: %v", err)
- }
-
// If WebAuthn is enrolled -> Redirect to WebAuthn instead
regs, err := auth.GetWebAuthnCredentialsByUID(u.ID)
if err == nil && len(regs) > 0 {
@@ -1084,24 +1148,31 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
func oAuth2UserLoginCallback(authSource *auth.Source, request *http.Request, response http.ResponseWriter) (*user_model.User, goth.User, error) {
oauth2Source := authSource.Cfg.(*oauth2.Source)
+ // Make sure that the response is not an error response.
+ errorName := request.FormValue("error")
+
+ if len(errorName) > 0 {
+ errorDescription := request.FormValue("error_description")
+
+ // Delete the goth session
+ err := gothic.Logout(response, request)
+ if err != nil {
+ return nil, goth.User{}, err
+ }
+
+ return nil, goth.User{}, errCallback{
+ Code: errorName,
+ Description: errorDescription,
+ }
+ }
+
+ // Proceed to authenticate through goth.
gothUser, err := oauth2Source.Callback(request, response)
if err != nil {
if err.Error() == "securecookie: the value is too long" || strings.Contains(err.Error(), "Data too long") {
log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", authSource.Name, setting.OAuth2.MaxTokenLength)
err = fmt.Errorf("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", authSource.Name, setting.OAuth2.MaxTokenLength)
}
- // goth does not provide the original error message
- // https://github.com/markbates/goth/issues/348
- if strings.Contains(err.Error(), "server response missing access_token") || strings.Contains(err.Error(), "could not find a matching session for this request") {
- errorCode := request.FormValue("error")
- errorDescription := request.FormValue("error_description")
- if errorCode != "" || errorDescription != "" {
- return nil, goth.User{}, errCallback{
- Code: errorCode,
- Description: errorDescription,
- }
- }
- }
return nil, goth.User{}, err
}
@@ -1151,7 +1222,7 @@ func oAuth2UserLoginCallback(authSource *auth.Source, request *http.Request, res
return nil, goth.User{}, err
}
if hasUser {
- user, err = user_model.GetUserByID(externalLoginUser.UserID)
+ user, err = user_model.GetUserByID(request.Context(), externalLoginUser.UserID)
return user, gothUser, err
}
diff --git a/routers/web/auth/oauth_test.go b/routers/web/auth/oauth_test.go
index 669d7431fc483..5116b4fc713d2 100644
--- a/routers/web/auth/oauth_test.go
+++ b/routers/web/auth/oauth_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
@@ -8,8 +7,10 @@ import (
"testing"
"code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/auth/source/oauth2"
"github.com/golang-jwt/jwt/v4"
@@ -21,7 +22,7 @@ func createAndParseToken(t *testing.T, grant *auth.OAuth2Grant) *oauth2.OIDCToke
assert.NoError(t, err)
assert.NotNil(t, signingKey)
- response, terr := newAccessTokenResponse(grant, signingKey, signingKey)
+ response, terr := newAccessTokenResponse(db.DefaultContext, grant, signingKey, signingKey)
assert.Nil(t, terr)
assert.NotNil(t, response)
@@ -43,7 +44,7 @@ func createAndParseToken(t *testing.T, grant *auth.OAuth2Grant) *oauth2.OIDCToke
func TestNewAccessTokenResponse_OIDCToken(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- grants, err := auth.GetOAuth2GrantsByUserID(3)
+ grants, err := auth.GetOAuth2GrantsByUserID(db.DefaultContext, 3)
assert.NoError(t, err)
assert.Len(t, grants, 1)
@@ -58,11 +59,29 @@ func TestNewAccessTokenResponse_OIDCToken(t *testing.T) {
assert.Empty(t, oidcToken.Email)
assert.False(t, oidcToken.EmailVerified)
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}).(*user_model.User)
- grants, err = auth.GetOAuth2GrantsByUserID(user.ID)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+ grants, err = auth.GetOAuth2GrantsByUserID(db.DefaultContext, user.ID)
assert.NoError(t, err)
assert.Len(t, grants, 1)
+ // Scopes: openid profile email
+ oidcToken = createAndParseToken(t, grants[0])
+ assert.Equal(t, user.Name, oidcToken.Name)
+ assert.Equal(t, user.Name, oidcToken.PreferredUsername)
+ assert.Equal(t, user.HTMLURL(), oidcToken.Profile)
+ assert.Equal(t, user.AvatarLink(), oidcToken.Picture)
+ assert.Equal(t, user.Website, oidcToken.Website)
+ assert.Equal(t, user.UpdatedUnix, oidcToken.UpdatedAt)
+ assert.Equal(t, user.Email, oidcToken.Email)
+ assert.Equal(t, user.IsActive, oidcToken.EmailVerified)
+
+ // set DefaultShowFullName to true
+ oldDefaultShowFullName := setting.UI.DefaultShowFullName
+ setting.UI.DefaultShowFullName = true
+ defer func() {
+ setting.UI.DefaultShowFullName = oldDefaultShowFullName
+ }()
+
// Scopes: openid profile email
oidcToken = createAndParseToken(t, grants[0])
assert.Equal(t, user.FullName, oidcToken.Name)
diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go
index f3189887a5308..f544b6535687d 100644
--- a/routers/web/auth/openid.go
+++ b/routers/web/auth/openid.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
@@ -13,10 +12,7 @@ import (
"code.gitea.io/gitea/modules/auth/openid"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/hcaptcha"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/recaptcha"
- "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -217,7 +213,7 @@ func signInOpenIDVerify(ctx *context.Context) {
}
if u == nil && nickname != "" {
- u, _ = user_model.GetUserByName(nickname)
+ u, _ = user_model.GetUserByName(ctx, nickname)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
@@ -231,27 +227,16 @@ func signInOpenIDVerify(ctx *context.Context) {
}
}
- if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
- ctx.ServerError("RegenerateSession", err)
- return
- }
-
- if err := ctx.Session.Set("openid_verified_uri", id); err != nil {
- log.Error("signInOpenIDVerify: Could not set openid_verified_uri in session: %v", err)
- }
- if err := ctx.Session.Set("openid_determined_email", email); err != nil {
- log.Error("signInOpenIDVerify: Could not set openid_determined_email in session: %v", err)
- }
-
if u != nil {
nickname = u.LowerName
}
-
- if err := ctx.Session.Set("openid_determined_username", nickname); err != nil {
- log.Error("signInOpenIDVerify: Could not set openid_determined_username in session: %v", err)
- }
- if err := ctx.Session.Release(); err != nil {
- log.Error("signInOpenIDVerify: Unable to save changes to the session: %v", err)
+ if err := updateSession(ctx, nil, map[string]interface{}{
+ "openid_verified_uri": id,
+ "openid_determined_email": email,
+ "openid_determined_username": nickname,
+ }); err != nil {
+ ctx.ServerError("updateSession", err)
+ return
}
if u != nil || !setting.Service.EnableOpenIDSignUp || setting.Service.AllowOnlyInternalRegistration {
@@ -307,7 +292,7 @@ func ConnectOpenIDPost(ctx *context.Context) {
// add OpenID for the user
userOID := &user_model.UserOpenID{UID: u.ID, URI: oid}
- if err = user_model.AddUserOpenID(userOID); err != nil {
+ if err = user_model.AddUserOpenID(ctx, userOID); err != nil {
if user_model.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form)
return
@@ -341,6 +326,8 @@ func RegisterOpenID(ctx *context.Context) {
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
+ ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
+ ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["OpenID"] = oid
userName, _ := ctx.Session.Get("openid_determined_username").(string)
if userName != "" {
@@ -366,12 +353,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDRegister"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
- ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
- ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
- ctx.Data["Captcha"] = context.GetImageCaptcha()
- ctx.Data["CaptchaType"] = setting.Service.CaptchaType
- ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
- ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
+ context.SetCaptchaData(ctx)
ctx.Data["OpenID"] = oid
if setting.Service.AllowOnlyInternalRegistration {
@@ -380,36 +362,11 @@ func RegisterOpenIDPost(ctx *context.Context) {
}
if setting.Service.EnableCaptcha {
- var valid bool
- var err error
- switch setting.Service.CaptchaType {
- case setting.ImageCaptcha:
- valid = context.GetImageCaptcha().VerifyReq(ctx.Req)
- case setting.ReCaptcha:
- if err := ctx.Req.ParseForm(); err != nil {
- ctx.ServerError("", err)
- return
- }
- valid, err = recaptcha.Verify(ctx, form.GRecaptchaResponse)
- case setting.HCaptcha:
- if err := ctx.Req.ParseForm(); err != nil {
- ctx.ServerError("", err)
- return
- }
- valid, err = hcaptcha.Verify(ctx, form.HcaptchaResponse)
- default:
- ctx.ServerError("Unknown Captcha Type", fmt.Errorf("Unknown Captcha Type: %s", setting.Service.CaptchaType))
- return
- }
- if err != nil {
- log.Debug("%s", err.Error())
- }
-
- if !valid {
- ctx.Data["Err_Captcha"] = true
- ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tplSignUpOID, &form)
+ if err := ctx.Req.ParseForm(); err != nil {
+ ctx.ServerError("", err)
return
}
+ context.VerifyCaptcha(ctx, tplSignUpOID, form)
}
length := setting.MinPasswordLength
@@ -423,19 +380,18 @@ func RegisterOpenIDPost(ctx *context.Context) {
}
u := &user_model.User{
- Name: form.UserName,
- Email: form.Email,
- Passwd: password,
- IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm),
+ Name: form.UserName,
+ Email: form.Email,
+ Passwd: password,
}
- if !createUserInContext(ctx, tplSignUpOID, form, u, nil, false) {
+ if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil, false) {
// error already handled
return
}
// add OpenID for the user
userOID := &user_model.UserOpenID{UID: u.ID, URI: oid}
- if err = user_model.AddUserOpenID(userOID); err != nil {
+ if err = user_model.AddUserOpenID(ctx, userOID); err != nil {
if user_model.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form)
return
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index d7bf67cffb6f0..e546c77bc91e9 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
@@ -63,7 +62,7 @@ func ForgotPasswdPost(ctx *context.Context) {
u, err := user_model.GetUserByEmail(email)
if err != nil {
if user_model.IsErrUserNotExist(err) {
- ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
+ ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale)
ctx.Data["IsResetSent"] = true
ctx.HTML(http.StatusOK, tplForgotPassword)
return
@@ -79,7 +78,7 @@ func ForgotPasswdPost(ctx *context.Context) {
return
}
- if ctx.Cache.IsExist("MailResendLimit_" + u.LowerName) {
+ if setting.CacheService.Enabled && ctx.Cache.IsExist("MailResendLimit_"+u.LowerName) {
ctx.Data["ResendLimited"] = true
ctx.HTML(http.StatusOK, tplForgotPassword)
return
@@ -87,11 +86,13 @@ func ForgotPasswdPost(ctx *context.Context) {
mailer.SendResetPasswordMail(u)
- if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
+ if setting.CacheService.Enabled {
+ if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ }
}
- ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
+ ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale)
ctx.Data["IsResetSent"] = true
ctx.HTML(http.StatusOK, tplForgotPassword)
}
@@ -272,7 +273,7 @@ func MustChangePassword(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplMustChangePassword)
}
-// MustChangePasswordPost response for updating a user's password after his/her
+// MustChangePasswordPost response for updating a user's password after their
// account was created by an admin
func MustChangePasswordPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.MustChangePasswordForm)
diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go
index c0cf58f3d35e8..e369f860811fa 100644
--- a/routers/web/auth/webauthn.go
+++ b/routers/web/auth/webauthn.go
@@ -1,11 +1,9 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package auth
import (
- "encoding/base32"
"errors"
"net/http"
@@ -18,8 +16,8 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/externalaccount"
- "github.com/duo-labs/webauthn/protocol"
- "github.com/duo-labs/webauthn/webauthn"
+ "github.com/go-webauthn/webauthn/protocol"
+ "github.com/go-webauthn/webauthn/webauthn"
)
var tplWebAuthn base.TplName = "user/auth/webauthn"
@@ -51,7 +49,7 @@ func WebAuthnLoginAssertion(ctx *context.Context) {
return
}
- user, err := user_model.GetUserByID(idSess)
+ user, err := user_model.GetUserByID(ctx, idSess)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
@@ -67,10 +65,7 @@ func WebAuthnLoginAssertion(ctx *context.Context) {
return
}
- // FIXME: DEPRECATED appid is deprecated and is planned to be removed in v1.18.0
- assertion, sessionData, err := wa.WebAuthn.BeginLogin((*wa.User)(user), webauthn.WithAssertionExtensions(protocol.AuthenticationExtensions{
- "appid": setting.U2F.AppID,
- }))
+ assertion, sessionData, err := wa.WebAuthn.BeginLogin((*wa.User)(user))
if err != nil {
ctx.ServerError("webauthn.BeginLogin", err)
return
@@ -96,7 +91,7 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
}()
// Load the user from the db
- user, err := user_model.GetUserByID(idSess)
+ user, err := user_model.GetUserByID(ctx, idSess)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
@@ -132,7 +127,7 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
}
// Success! Get the credential and update the sign count with the new value we received.
- dbCred, err := auth.GetWebAuthnCredentialByCredID(user.ID, base32.HexEncoding.EncodeToString(cred.ID))
+ dbCred, err := auth.GetWebAuthnCredentialByCredID(user.ID, cred.ID)
if err != nil {
ctx.ServerError("GetWebAuthnCredentialByCredID", err)
return
@@ -159,12 +154,5 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
}
_ = ctx.Session.Delete("twofaUid")
- // Finally check if the appid extension was used:
- if value, ok := parsedResponse.ClientExtensionResults["appid"]; ok {
- if appid, ok := value.(bool); ok && appid {
- ctx.Flash.Error(ctx.Tr("webauthn_u2f_deprecated", dbCred.Name))
- }
- }
-
ctx.JSON(http.StatusOK, map[string]string{"redirect": redirect})
}
diff --git a/routers/web/auth_windows.go b/routers/web/auth_windows.go
index f404fd377160e..3125d7ce9a74b 100644
--- a/routers/web/auth_windows.go
+++ b/routers/web/auth_windows.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package web
diff --git a/routers/web/base.go b/routers/web/base.go
index 938abaef81631..b0d8a7c3f1e6d 100644
--- a/routers/web/base.go
+++ b/routers/web/base.go
@@ -1,10 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package web
import (
+ goctx "context"
"errors"
"fmt"
"io"
@@ -62,7 +62,7 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
w,
req,
u.String(),
- http.StatusPermanentRedirect,
+ http.StatusTemporaryRedirect,
)
})
}
@@ -123,8 +123,8 @@ func (d *dataStore) GetData() map[string]interface{} {
// Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so.
// This error will be created with the gitea 500 page.
-func Recovery() func(next http.Handler) http.Handler {
- rnd := templates.HTMLRenderer()
+func Recovery(ctx goctx.Context) func(next http.Handler) http.Handler {
+ _, rnd := templates.HTMLRenderer(ctx)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
@@ -139,7 +139,7 @@ func Recovery() func(next http.Handler) http.Handler {
store := dataStore{
"Language": lc.Language(),
"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
- "i18n": lc,
+ "locale": lc,
}
user := context.GetContextUser(req)
@@ -158,6 +158,7 @@ func Recovery() func(next http.Handler) http.Handler {
store["SignedUserName"] = ""
}
+ httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform")
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
if !setting.IsProd {
diff --git a/routers/web/dev/template.go b/routers/web/dev/template.go
deleted file mode 100644
index 29d6033a7a109..0000000000000
--- a/routers/web/dev/template.go
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright 2014 The Gogs 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 dev
-
-import (
- "net/http"
-
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
-)
-
-// TemplatePreview render for previewing the indicated template
-func TemplatePreview(ctx *context.Context) {
- ctx.Data["User"] = user_model.User{Name: "Unknown"}
- ctx.Data["AppName"] = setting.AppName
- ctx.Data["AppVer"] = setting.AppVer
- ctx.Data["AppUrl"] = setting.AppURL
- ctx.Data["Code"] = "2014031910370000009fff6782aadb2162b4a997acb69d4400888e0b9274657374"
- ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())
- ctx.Data["ResetPwdCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, ctx.Locale.Language())
- ctx.Data["CurDbValue"] = ""
-
- ctx.HTML(http.StatusOK, base.TplName(ctx.Params("*")))
-}
diff --git a/routers/web/events/events.go b/routers/web/events/events.go
index 02d20550afcd2..1a5a162c1a2e7 100644
--- a/routers/web/events/events.go
+++ b/routers/web/events/events.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package events
@@ -8,15 +7,10 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/web/auth"
)
@@ -71,8 +65,6 @@ func Events(ctx *context.Context) {
timer := time.NewTicker(30 * time.Second)
- stopwatchTimer := time.NewTicker(setting.UI.Notification.MinTimeout)
-
loop:
for {
select {
@@ -93,32 +85,6 @@ loop:
case <-shutdownCtx.Done():
go unregister()
break loop
- case <-stopwatchTimer.C:
- sws, err := models.GetUserStopwatches(ctx.Doer.ID, db.ListOptions{})
- if err != nil {
- log.Error("Unable to GetUserStopwatches: %v", err)
- continue
- }
- apiSWs, err := convert.ToStopWatches(sws)
- if err != nil {
- log.Error("Unable to APIFormat stopwatches: %v", err)
- continue
- }
- dataBs, err := json.Marshal(apiSWs)
- if err != nil {
- log.Error("Unable to marshal stopwatches: %v", err)
- continue
- }
- _, err = (&eventsource.Event{
- Name: "stopwatches",
- Data: string(dataBs),
- }).WriteTo(ctx.Resp)
- if err != nil {
- log.Error("Unable to write to EventStream for user %s: %v", ctx.Doer.Name, err)
- go unregister()
- break loop
- }
- ctx.Resp.Flush()
case event, ok := <-messageChan:
if !ok {
break loop
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index 28bdc7c9ca596..942b1f83789b8 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -1,15 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package explore
import (
"net/http"
- "code.gitea.io/gitea/models"
repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
@@ -36,14 +33,25 @@ func Code(ctx *context.Context) {
language := ctx.FormTrim("l")
keyword := ctx.FormTrim("q")
+
+ queryType := ctx.FormTrim("t")
+ isMatch := queryType == "match"
+
+ ctx.Data["Keyword"] = keyword
+ ctx.Data["Language"] = language
+ ctx.Data["queryType"] = queryType
+ ctx.Data["PageIsViewCode"] = true
+
+ if keyword == "" {
+ ctx.HTML(http.StatusOK, tplExploreCode)
+ return
+ }
+
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}
- queryType := ctx.FormTrim("t")
- isMatch := queryType == "match"
-
var (
repoIDs []int64
err error
@@ -55,9 +63,9 @@ func Code(ctx *context.Context) {
// guest user or non-admin user
if ctx.Doer == nil || !isAdmin {
- repoIDs, err = models.FindUserAccessibleRepoIDs(ctx.Doer)
+ repoIDs, err = repo_model.FindUserCodeAccessibleRepoIDs(ctx, ctx.Doer)
if err != nil {
- ctx.ServerError("SearchResults", err)
+ ctx.ServerError("FindUserCodeAccessibleRepoIDs", err)
return
}
}
@@ -68,37 +76,7 @@ func Code(ctx *context.Context) {
searchResultLanguages []*code_indexer.SearchResultLanguages
)
- // if non-admin login user, we need check UnitTypeCode at first
- if ctx.Doer != nil && len(repoIDs) > 0 {
- repoMaps, err := repo_model.GetRepositoriesMapByIDs(repoIDs)
- if err != nil {
- ctx.ServerError("SearchResults", err)
- return
- }
-
- rightRepoMap := make(map[int64]*repo_model.Repository, len(repoMaps))
- repoIDs = make([]int64, 0, len(repoMaps))
- for id, repo := range repoMaps {
- if models.CheckRepoUnitUser(repo, ctx.Doer, unit.TypeCode) {
- rightRepoMap[id] = repo
- repoIDs = append(repoIDs, id)
- }
- }
-
- ctx.Data["RepoMaps"] = rightRepoMap
-
- total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
- if err != nil {
- if code_indexer.IsAvailable() {
- ctx.ServerError("SearchResults", err)
- return
- }
- ctx.Data["CodeIndexerUnavailable"] = true
- } else {
- ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable()
- }
- // if non-login user or isAdmin, no need to check UnitTypeCode
- } else if (ctx.Doer == nil && len(repoIDs) > 0) || isAdmin {
+ if (len(repoIDs) > 0) || isAdmin {
total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
if err != nil {
if code_indexer.IsAvailable() {
@@ -126,20 +104,27 @@ func Code(ctx *context.Context) {
repoMaps, err := repo_model.GetRepositoriesMapByIDs(loadRepoIDs)
if err != nil {
- ctx.ServerError("SearchResults", err)
+ ctx.ServerError("GetRepositoriesMapByIDs", err)
return
}
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["Keyword"] = keyword
- ctx.Data["Language"] = language
- ctx.Data["queryType"] = queryType
ctx.Data["SearchResults"] = searchResults
ctx.Data["SearchResultLanguages"] = searchResultLanguages
- ctx.Data["RequireHighlightJS"] = true
- ctx.Data["PageIsViewCode"] = true
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go
index eb6972fad3343..c9fb05ff3a883 100644
--- a/routers/web/explore/org.go
+++ b/routers/web/explore/org.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package explore
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index 3e8aa2bb0fda7..5271e39bbc91b 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -1,18 +1,18 @@
// 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.
+// SPDX-License-Identifier: MIT
package explore
import (
"net/http"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/sitemap"
)
const (
@@ -31,16 +31,27 @@ type RepoSearchOptions struct {
// RenderRepoSearch render repositories search page
func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
- page := ctx.FormInt("page")
+ // Sitemap index for sitemap paths
+ page := int(ctx.ParamsInt64("idx"))
+ isSitemap := ctx.Params("idx") != ""
+ if page <= 1 {
+ page = ctx.FormInt("page")
+ }
+
if page <= 0 {
page = 1
}
+ if isSitemap {
+ opts.PageSize = setting.UI.SitemapPagingNum
+ }
+
var (
- repos []*repo_model.Repository
- count int64
- err error
- orderBy db.SearchOrderBy
+ repos []*repo_model.Repository
+ count int64
+ err error
+ orderBy db.SearchOrderBy
+ onlyShowRelevant bool
)
ctx.Data["SortType"] = ctx.FormString("sort")
@@ -49,8 +60,6 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
orderBy = db.SearchOrderByNewest
case "oldest":
orderBy = db.SearchOrderByOldest
- case "recentupdate":
- orderBy = db.SearchOrderByRecentUpdated
case "leastupdate":
orderBy = db.SearchOrderByLeastUpdated
case "reversealphabetically":
@@ -72,16 +81,23 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
default:
ctx.Data["SortType"] = "recentupdate"
orderBy = db.SearchOrderByRecentUpdated
+ onlyShowRelevant = setting.UI.OnlyShowRelevantRepos && !ctx.FormBool("no_filter")
}
keyword := ctx.FormTrim("q")
+ if keyword != "" {
+ onlyShowRelevant = false
+ }
+
+ ctx.Data["OnlyShowRelevant"] = onlyShowRelevant
+
topicOnly := ctx.FormBool("topic")
ctx.Data["TopicOnly"] = topicOnly
language := ctx.FormTrim("language")
ctx.Data["Language"] = language
- repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+ repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: opts.PageSize,
@@ -96,11 +112,24 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
TopicOnly: topicOnly,
Language: language,
IncludeDescription: setting.UI.SearchRepoDescription,
+ OnlyShowRelevant: onlyShowRelevant,
})
if err != nil {
ctx.ServerError("SearchRepository", err)
return
}
+ if isSitemap {
+ m := sitemap.NewSitemap()
+ for _, item := range repos {
+ m.Add(sitemap.URL{URL: item.HTMLURL(), LastMod: item.UpdatedUnix.AsTimePtr()})
+ }
+ ctx.Resp.Header().Set("Content-Type", "text/xml")
+ if _, err := m.WriteTo(ctx.Resp); err != nil {
+ log.Error("Failed writing sitemap: %v", err)
+ }
+ return
+ }
+
ctx.Data["Keyword"] = keyword
ctx.Data["Total"] = count
ctx.Data["Repos"] = repos
@@ -110,6 +139,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "topic", "TopicOnly")
pager.AddParam(ctx, "language", "Language")
+ pager.AddParamString("no_filter", ctx.FormString("no_filter"))
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, opts.TplName)
diff --git a/routers/web/explore/topic.go b/routers/web/explore/topic.go
index 39b87f2498091..e172d9e04d018 100644
--- a/routers/web/explore/topic.go
+++ b/routers/web/explore/topic.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package explore
@@ -10,8 +9,8 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/convert"
)
// TopicSearch search for creating topic
diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go
index ea0d7d5f9d8c3..e00493c87b766 100644
--- a/routers/web/explore/user.go
+++ b/routers/web/explore/user.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package explore
@@ -12,7 +11,9 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/sitemap"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
)
@@ -33,11 +34,20 @@ func isKeywordValid(keyword string) bool {
// RenderUserSearch render user search page
func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, tplName base.TplName) {
- opts.Page = ctx.FormInt("page")
+ // Sitemap index for sitemap paths
+ opts.Page = int(ctx.ParamsInt64("idx"))
+ isSitemap := ctx.Params("idx") != ""
+ if opts.Page <= 1 {
+ opts.Page = ctx.FormInt("page")
+ }
if opts.Page <= 1 {
opts.Page = 1
}
+ if isSitemap {
+ opts.PageSize = setting.UI.SitemapPagingNum
+ }
+
var (
users []*user_model.User
count int64
@@ -58,6 +68,10 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions,
orderBy = "`user`.updated_unix ASC"
case "reversealphabetically":
orderBy = "`user`.name DESC"
+ case "lastlogin":
+ orderBy = "`user`.last_login_unix ASC"
+ case "reverselastlogin":
+ orderBy = "`user`.last_login_unix DESC"
case UserSearchDefaultSortType: // "alphabetically"
default:
orderBy = "`user`.name ASC"
@@ -73,6 +87,18 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions,
return
}
}
+ if isSitemap {
+ m := sitemap.NewSitemap()
+ for _, item := range users {
+ m.Add(sitemap.URL{URL: item.HTMLURL(), LastMod: item.UpdatedUnix.AsTimePtr()})
+ }
+ ctx.Resp.Header().Set("Content-Type", "text/xml")
+ if _, err := m.WriteTo(ctx.Resp); err != nil {
+ log.Error("Failed writing sitemap: %v", err)
+ }
+ return
+ }
+
ctx.Data["Keyword"] = opts.Keyword
ctx.Data["Total"] = count
ctx.Data["Users"] = users
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index 64801a6078bca..7c375a085f033 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package feed
@@ -12,7 +11,8 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
@@ -23,33 +23,33 @@ import (
"github.com/gorilla/feeds"
)
-func toBranchLink(act *models.Action) string {
- return act.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(act.GetBranch())
+func toBranchLink(act *activities_model.Action) string {
+ return act.GetRepoAbsoluteLink() + "/src/branch/" + util.PathEscapeSegments(act.GetBranch())
}
-func toTagLink(act *models.Action) string {
- return act.GetRepoLink() + "/src/tag/" + util.PathEscapeSegments(act.GetTag())
+func toTagLink(act *activities_model.Action) string {
+ return act.GetRepoAbsoluteLink() + "/src/tag/" + util.PathEscapeSegments(act.GetTag())
}
-func toIssueLink(act *models.Action) string {
- return act.GetRepoLink() + "/issues/" + url.PathEscape(act.GetIssueInfos()[0])
+func toIssueLink(act *activities_model.Action) string {
+ return act.GetRepoAbsoluteLink() + "/issues/" + url.PathEscape(act.GetIssueInfos()[0])
}
-func toPullLink(act *models.Action) string {
- return act.GetRepoLink() + "/pulls/" + url.PathEscape(act.GetIssueInfos()[0])
+func toPullLink(act *activities_model.Action) string {
+ return act.GetRepoAbsoluteLink() + "/pulls/" + url.PathEscape(act.GetIssueInfos()[0])
}
-func toSrcLink(act *models.Action) string {
- return act.GetRepoLink() + "/src/" + util.PathEscapeSegments(act.GetBranch())
+func toSrcLink(act *activities_model.Action) string {
+ return act.GetRepoAbsoluteLink() + "/src/" + util.PathEscapeSegments(act.GetBranch())
}
-func toReleaseLink(act *models.Action) string {
- return act.GetRepoLink() + "/releases/tag/" + util.PathEscapeSegments(act.GetBranch())
+func toReleaseLink(act *activities_model.Action) string {
+ return act.GetRepoAbsoluteLink() + "/releases/tag/" + util.PathEscapeSegments(act.GetBranch())
}
// renderMarkdown creates a minimal markdown render context from an action.
// If rendering fails, the original markdown text is returned
-func renderMarkdown(ctx *context.Context, act *models.Action, content string) string {
+func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) string {
markdownCtx := &markup.RenderContext{
Ctx: ctx,
URLPrefix: act.GetRepoLink(),
@@ -67,123 +67,129 @@ func renderMarkdown(ctx *context.Context, act *models.Action, content string) st
}
// feedActionsToFeedItems convert gitea's Action feed to feeds Item
-func feedActionsToFeedItems(ctx *context.Context, actions models.ActionList) (items []*feeds.Item, err error) {
+func feedActionsToFeedItems(ctx *context.Context, actions activities_model.ActionList) (items []*feeds.Item, err error) {
for _, act := range actions {
- act.LoadActUser()
+ act.LoadActUser(ctx)
- content, desc, title := "", "", ""
+ var content, desc, title string
link := &feeds.Link{Href: act.GetCommentLink()}
// title
title = act.ActUser.DisplayName() + " "
switch act.OpType {
- case models.ActionCreateRepo:
- title += ctx.TrHTMLEscapeArgs("action.create_repo", act.GetRepoLink(), act.ShortRepoPath())
- link.Href = act.GetRepoLink()
- case models.ActionRenameRepo:
- title += ctx.TrHTMLEscapeArgs("action.rename_repo", act.GetContent(), act.GetRepoLink(), act.ShortRepoPath())
- link.Href = act.GetRepoLink()
- case models.ActionCommitRepo:
+ case activities_model.ActionCreateRepo:
+ title += ctx.TrHTMLEscapeArgs("action.create_repo", act.GetRepoAbsoluteLink(), act.ShortRepoPath())
+ link.Href = act.GetRepoAbsoluteLink()
+ case activities_model.ActionRenameRepo:
+ title += ctx.TrHTMLEscapeArgs("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(), act.ShortRepoPath())
+ link.Href = act.GetRepoAbsoluteLink()
+ case activities_model.ActionCommitRepo:
link.Href = toBranchLink(act)
if len(act.Content) != 0 {
- title += ctx.TrHTMLEscapeArgs("action.commit_repo", act.GetRepoLink(), link.Href, act.GetBranch(), act.ShortRepoPath())
+ title += ctx.TrHTMLEscapeArgs("action.commit_repo", act.GetRepoAbsoluteLink(), link.Href, act.GetBranch(), act.ShortRepoPath())
} else {
- title += ctx.TrHTMLEscapeArgs("action.create_branch", act.GetRepoLink(), link.Href, act.GetBranch(), act.ShortRepoPath())
+ title += ctx.TrHTMLEscapeArgs("action.create_branch", act.GetRepoAbsoluteLink(), link.Href, act.GetBranch(), act.ShortRepoPath())
}
- case models.ActionCreateIssue:
+ case activities_model.ActionCreateIssue:
link.Href = toIssueLink(act)
title += ctx.TrHTMLEscapeArgs("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath())
- case models.ActionCreatePullRequest:
+ case activities_model.ActionCreatePullRequest:
link.Href = toPullLink(act)
title += ctx.TrHTMLEscapeArgs("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath())
- case models.ActionTransferRepo:
- link.Href = act.GetRepoLink()
- title += ctx.TrHTMLEscapeArgs("action.transfer_repo", act.GetContent(), act.GetRepoLink(), act.ShortRepoPath())
- case models.ActionPushTag:
+ case activities_model.ActionTransferRepo:
+ link.Href = act.GetRepoAbsoluteLink()
+ title += ctx.TrHTMLEscapeArgs("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(), act.ShortRepoPath())
+ case activities_model.ActionPushTag:
link.Href = toTagLink(act)
- title += ctx.TrHTMLEscapeArgs("action.push_tag", act.GetRepoLink(), link.Href, act.GetTag(), act.ShortRepoPath())
- case models.ActionCommentIssue:
+ title += ctx.TrHTMLEscapeArgs("action.push_tag", act.GetRepoAbsoluteLink(), link.Href, act.GetTag(), act.ShortRepoPath())
+ case activities_model.ActionCommentIssue:
issueLink := toIssueLink(act)
if link.Href == "#" {
link.Href = issueLink
}
title += ctx.TrHTMLEscapeArgs("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath())
- case models.ActionMergePullRequest:
+ case activities_model.ActionMergePullRequest:
pullLink := toPullLink(act)
if link.Href == "#" {
link.Href = pullLink
}
title += ctx.TrHTMLEscapeArgs("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath())
- case models.ActionCloseIssue:
+ case activities_model.ActionAutoMergePullRequest:
+ pullLink := toPullLink(act)
+ if link.Href == "#" {
+ link.Href = pullLink
+ }
+ title += ctx.TrHTMLEscapeArgs("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath())
+ case activities_model.ActionCloseIssue:
issueLink := toIssueLink(act)
if link.Href == "#" {
link.Href = issueLink
}
title += ctx.TrHTMLEscapeArgs("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath())
- case models.ActionReopenIssue:
+ case activities_model.ActionReopenIssue:
issueLink := toIssueLink(act)
if link.Href == "#" {
link.Href = issueLink
}
title += ctx.TrHTMLEscapeArgs("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath())
- case models.ActionClosePullRequest:
+ case activities_model.ActionClosePullRequest:
pullLink := toPullLink(act)
if link.Href == "#" {
link.Href = pullLink
}
title += ctx.TrHTMLEscapeArgs("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath())
- case models.ActionReopenPullRequest:
+ case activities_model.ActionReopenPullRequest:
pullLink := toPullLink(act)
if link.Href == "#" {
link.Href = pullLink
}
title += ctx.TrHTMLEscapeArgs("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath())
- case models.ActionDeleteTag:
- link.Href = act.GetRepoLink()
- title += ctx.TrHTMLEscapeArgs("action.delete_tag", act.GetRepoLink(), act.GetTag(), act.ShortRepoPath())
- case models.ActionDeleteBranch:
- link.Href = act.GetRepoLink()
- title += ctx.TrHTMLEscapeArgs("action.delete_branch", act.GetRepoLink(), html.EscapeString(act.GetBranch()), act.ShortRepoPath())
- case models.ActionMirrorSyncPush:
+ case activities_model.ActionDeleteTag:
+ link.Href = act.GetRepoAbsoluteLink()
+ title += ctx.TrHTMLEscapeArgs("action.delete_tag", act.GetRepoAbsoluteLink(), act.GetTag(), act.ShortRepoPath())
+ case activities_model.ActionDeleteBranch:
+ link.Href = act.GetRepoAbsoluteLink()
+ title += ctx.TrHTMLEscapeArgs("action.delete_branch", act.GetRepoAbsoluteLink(), html.EscapeString(act.GetBranch()), act.ShortRepoPath())
+ case activities_model.ActionMirrorSyncPush:
srcLink := toSrcLink(act)
if link.Href == "#" {
link.Href = srcLink
}
- title += ctx.TrHTMLEscapeArgs("action.mirror_sync_push", act.GetRepoLink(), srcLink, act.GetBranch(), act.ShortRepoPath())
- case models.ActionMirrorSyncCreate:
+ title += ctx.TrHTMLEscapeArgs("action.mirror_sync_push", act.GetRepoAbsoluteLink(), srcLink, act.GetBranch(), act.ShortRepoPath())
+ case activities_model.ActionMirrorSyncCreate:
srcLink := toSrcLink(act)
if link.Href == "#" {
link.Href = srcLink
}
- title += ctx.TrHTMLEscapeArgs("action.mirror_sync_create", act.GetRepoLink(), srcLink, act.GetBranch(), act.ShortRepoPath())
- case models.ActionMirrorSyncDelete:
- link.Href = act.GetRepoLink()
- title += ctx.TrHTMLEscapeArgs("action.mirror_sync_delete", act.GetRepoLink(), act.GetBranch(), act.ShortRepoPath())
- case models.ActionApprovePullRequest:
+ title += ctx.TrHTMLEscapeArgs("action.mirror_sync_create", act.GetRepoAbsoluteLink(), srcLink, act.GetBranch(), act.ShortRepoPath())
+ case activities_model.ActionMirrorSyncDelete:
+ link.Href = act.GetRepoAbsoluteLink()
+ title += ctx.TrHTMLEscapeArgs("action.mirror_sync_delete", act.GetRepoAbsoluteLink(), act.GetBranch(), act.ShortRepoPath())
+ case activities_model.ActionApprovePullRequest:
pullLink := toPullLink(act)
title += ctx.TrHTMLEscapeArgs("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath())
- case models.ActionRejectPullRequest:
+ case activities_model.ActionRejectPullRequest:
pullLink := toPullLink(act)
title += ctx.TrHTMLEscapeArgs("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath())
- case models.ActionCommentPull:
+ case activities_model.ActionCommentPull:
pullLink := toPullLink(act)
title += ctx.TrHTMLEscapeArgs("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath())
- case models.ActionPublishRelease:
+ case activities_model.ActionPublishRelease:
releaseLink := toReleaseLink(act)
if link.Href == "#" {
link.Href = releaseLink
}
- title += ctx.TrHTMLEscapeArgs("action.publish_release", act.GetRepoLink(), releaseLink, act.ShortRepoPath(), act.Content)
- case models.ActionPullReviewDismissed:
+ title += ctx.TrHTMLEscapeArgs("action.publish_release", act.GetRepoAbsoluteLink(), releaseLink, act.ShortRepoPath(), act.Content)
+ case activities_model.ActionPullReviewDismissed:
pullLink := toPullLink(act)
title += ctx.TrHTMLEscapeArgs("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(), act.GetIssueInfos()[1])
- case models.ActionStarRepo:
- link.Href = act.GetRepoLink()
- title += ctx.TrHTMLEscapeArgs("action.starred_repo", act.GetRepoLink(), act.GetRepoPath())
- case models.ActionWatchRepo:
- link.Href = act.GetRepoLink()
- title += ctx.TrHTMLEscapeArgs("action.watched_repo", act.GetRepoLink(), act.GetRepoPath())
+ case activities_model.ActionStarRepo:
+ link.Href = act.GetRepoAbsoluteLink()
+ title += ctx.TrHTMLEscapeArgs("action.starred_repo", act.GetRepoAbsoluteLink(), act.GetRepoPath())
+ case activities_model.ActionWatchRepo:
+ link.Href = act.GetRepoAbsoluteLink()
+ title += ctx.TrHTMLEscapeArgs("action.watched_repo", act.GetRepoAbsoluteLink(), act.GetRepoPath())
default:
return nil, fmt.Errorf("unknown action type: %v", act.OpType)
}
@@ -191,16 +197,16 @@ func feedActionsToFeedItems(ctx *context.Context, actions models.ActionList) (it
// description & content
{
switch act.OpType {
- case models.ActionCommitRepo, models.ActionMirrorSyncPush:
+ case activities_model.ActionCommitRepo, activities_model.ActionMirrorSyncPush:
push := templates.ActionContent2Commits(act)
- repoLink := act.GetRepoLink()
+ repoLink := act.GetRepoAbsoluteLink()
for _, commit := range push.Commits {
if len(desc) != 0 {
desc += "\n\n"
}
desc += fmt.Sprintf("%s \n%s",
- html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoLink(), commit.Sha1)),
+ html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(), commit.Sha1)),
commit.Sha1,
templates.RenderCommitMessage(ctx, commit.Message, repoLink, nil),
)
@@ -209,23 +215,23 @@ func feedActionsToFeedItems(ctx *context.Context, actions models.ActionList) (it
if push.Len > 1 {
link = &feeds.Link{Href: fmt.Sprintf("%s/%s", setting.AppSubURL, push.CompareURL)}
} else if push.Len == 1 {
- link = &feeds.Link{Href: fmt.Sprintf("%s/commit/%s", act.GetRepoLink(), push.Commits[0].Sha1)}
+ link = &feeds.Link{Href: fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(), push.Commits[0].Sha1)}
}
- case models.ActionCreateIssue, models.ActionCreatePullRequest:
+ case activities_model.ActionCreateIssue, activities_model.ActionCreatePullRequest:
desc = strings.Join(act.GetIssueInfos(), "#")
content = renderMarkdown(ctx, act, act.GetIssueContent())
- case models.ActionCommentIssue, models.ActionApprovePullRequest, models.ActionRejectPullRequest, models.ActionCommentPull:
+ case activities_model.ActionCommentIssue, activities_model.ActionApprovePullRequest, activities_model.ActionRejectPullRequest, activities_model.ActionCommentPull:
desc = act.GetIssueTitle()
comment := act.GetIssueInfos()[1]
if len(comment) != 0 {
desc += "\n\n" + renderMarkdown(ctx, act, comment)
}
- case models.ActionMergePullRequest:
+ case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
desc = act.GetIssueInfos()[1]
- case models.ActionCloseIssue, models.ActionReopenIssue, models.ActionClosePullRequest, models.ActionReopenPullRequest:
+ case activities_model.ActionCloseIssue, activities_model.ActionReopenIssue, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest:
desc = act.GetIssueTitle()
- case models.ActionPullReviewDismissed:
+ case activities_model.ActionPullReviewDismissed:
desc = ctx.Tr("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
}
}
@@ -241,12 +247,12 @@ func feedActionsToFeedItems(ctx *context.Context, actions models.ActionList) (it
Name: act.ActUser.DisplayName(),
Email: act.ActUser.GetEmail(),
},
- Id: strconv.FormatInt(act.ID, 10),
+ Id: fmt.Sprintf("%v: %v", strconv.FormatInt(act.ID, 10), link.Href),
Created: act.CreatedUnix.AsTime(),
Content: content,
})
}
- return
+ return items, err
}
// GetFeedType return if it is a feed request and altered name and feed type.
@@ -263,3 +269,46 @@ func GetFeedType(name string, req *http.Request) (bool, string, string) {
return false, name, ""
}
+
+// feedActionsToFeedItems convert gitea's Repo's Releases to feeds Item
+func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, isReleasesOnly bool) (items []*feeds.Item, err error) {
+ for _, rel := range releases {
+ err := rel.LoadAttributes(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ var title, content string
+
+ if rel.IsTag {
+ title = rel.TagName
+ } else {
+ title = rel.Title
+ }
+
+ link := &feeds.Link{Href: rel.HTMLURL()}
+ content, err = markdown.RenderString(&markup.RenderContext{
+ Ctx: ctx,
+ URLPrefix: rel.Repo.Link(),
+ Metas: rel.Repo.ComposeMetas(),
+ }, rel.Note)
+
+ if err != nil {
+ return nil, err
+ }
+
+ items = append(items, &feeds.Item{
+ Title: title,
+ Link: link,
+ Created: rel.CreatedUnix.AsTime(),
+ Author: &feeds.Author{
+ Name: rel.Publisher.DisplayName(),
+ Email: rel.Publisher.GetEmail(),
+ },
+ Id: fmt.Sprintf("%v: %v", strconv.FormatInt(rel.ID, 10), link.Href),
+ Content: content,
+ })
+ }
+
+ return items, err
+}
diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go
index 61a39755f5020..7641769192c5e 100644
--- a/routers/web/feed/profile.go
+++ b/routers/web/feed/profile.go
@@ -1,14 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package feed
import (
- "net/http"
"time"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/context"
"github.com/gorilla/feeds"
@@ -26,10 +24,12 @@ func ShowUserFeedAtom(ctx *context.Context) {
// showUserFeed show user activity as RSS / Atom feed
func showUserFeed(ctx *context.Context, formatType string) {
- actions, err := models.GetFeeds(ctx, models.GetFeedsOptions{
+ includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
+
+ actions, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctx.ContextUser,
Actor: ctx.Doer,
- IncludePrivate: false,
+ IncludePrivate: includePrivate,
OnlyPerformedBy: !ctx.ContextUser.IsOrganization(),
IncludeDeleted: false,
Date: ctx.FormString("date"),
@@ -57,7 +57,6 @@ func showUserFeed(ctx *context.Context, formatType string) {
// writeFeed write a feeds.Feed as atom or rss to ctx.Resp
func writeFeed(ctx *context.Context, feed *feeds.Feed, formatType string) {
- ctx.Resp.WriteHeader(http.StatusOK)
if formatType == "atom" {
ctx.Resp.Header().Set("Content-Type", "application/atom+xml;charset=utf-8")
if err := feed.WriteAtom(ctx.Resp); err != nil {
diff --git a/routers/web/feed/release.go b/routers/web/feed/release.go
new file mode 100644
index 0000000000000..fbfa11c63ecff
--- /dev/null
+++ b/routers/web/feed/release.go
@@ -0,0 +1,50 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package feed
+
+import (
+ "time"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/context"
+
+ "github.com/gorilla/feeds"
+)
+
+// shows tags and/or releases on the repo as RSS / Atom feed
+func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleasesOnly bool, formatType string) {
+ releases, err := repo_model.GetReleasesByRepoID(ctx, ctx.Repo.Repository.ID, repo_model.FindReleasesOptions{
+ IncludeTags: !isReleasesOnly,
+ })
+ if err != nil {
+ ctx.ServerError("GetReleasesByRepoID", err)
+ return
+ }
+
+ var title string
+ var link *feeds.Link
+
+ if isReleasesOnly {
+ title = ctx.Tr("repo.release.releases_for", repo.FullName())
+ link = &feeds.Link{Href: repo.HTMLURL() + "/release"}
+ } else {
+ title = ctx.Tr("repo.release.tags_for", repo.FullName())
+ link = &feeds.Link{Href: repo.HTMLURL() + "/tags"}
+ }
+
+ feed := &feeds.Feed{
+ Title: title,
+ Link: link,
+ Description: repo.Description,
+ Created: time.Now(),
+ }
+
+ feed.Items, err = releasesToFeedItems(ctx, releases, isReleasesOnly)
+ if err != nil {
+ ctx.ServerError("releasesToFeedItems", err)
+ return
+ }
+
+ writeFeed(ctx, feed, formatType)
+}
diff --git a/routers/web/feed/repo.go b/routers/web/feed/repo.go
index ac856195b9f7f..1d24b5880052e 100644
--- a/routers/web/feed/repo.go
+++ b/routers/web/feed/repo.go
@@ -1,13 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package feed
import (
"time"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
@@ -16,7 +15,7 @@ import (
// ShowRepoFeed shows user activity on the repo as RSS / Atom feed
func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType string) {
- actions, err := models.GetFeeds(ctx, models.GetFeedsOptions{
+ actions, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
RequestedRepo: repo,
Actor: ctx.Doer,
IncludePrivate: true,
diff --git a/routers/web/goget.go b/routers/web/goget.go
index a58739fe42a3c..fb8afae9991a6 100644
--- a/routers/web/goget.go
+++ b/routers/web/goget.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package web
@@ -53,7 +52,7 @@ func goGet(ctx *context.Context) {
}
branchName := setting.Repository.DefaultBranch
- repo, err := repo_model.GetRepositoryByOwnerAndName(ownerName, repoName)
+ repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
if err == nil && len(repo.DefaultBranch) > 0 {
branchName = repo.DefaultBranch
}
diff --git a/routers/web/healthcheck/check.go b/routers/web/healthcheck/check.go
new file mode 100644
index 0000000000000..1142a0aec5d55
--- /dev/null
+++ b/routers/web/healthcheck/check.go
@@ -0,0 +1,142 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package healthcheck
+
+import (
+ "net/http"
+ "os"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/cache"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+type status string
+
+const (
+ // pass healthy (acceptable aliases: "ok" to support Node's Terminus and "up" for Java's SpringBoot)
+ // fail unhealthy (acceptable aliases: "error" to support Node's Terminus and "down" for Java's SpringBoot), and
+ // warn healthy, with some concerns.
+ //
+ // ref https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check#section-3.1
+ // status: (required) indicates whether the service status is acceptable
+ // or not. API publishers SHOULD use following values for the field:
+ // The value of the status field is case-insensitive and is tightly
+ // related with the HTTP response code returned by the health endpoint.
+ // For "pass" status, HTTP response code in the 2xx-3xx range MUST be
+ // used. For "fail" status, HTTP response code in the 4xx-5xx range
+ // MUST be used. In case of the "warn" status, endpoints MUST return
+ // HTTP status in the 2xx-3xx range, and additional information SHOULD
+ // be provided, utilizing optional fields of the response.
+ pass status = "pass"
+ fail status = "fail"
+ warn status = "warn"
+)
+
+func (s status) ToHTTPStatus() int {
+ if s == pass || s == warn {
+ return http.StatusOK
+ }
+ return http.StatusFailedDependency
+}
+
+type checks map[string][]componentStatus
+
+// response is the data returned by the health endpoint, which will be marshaled to JSON format
+type response struct {
+ Status status `json:"status"`
+ Description string `json:"description"` // a human-friendly description of the service
+ Checks checks `json:"checks,omitempty"` // The Checks Object, should be omitted on installation route
+}
+
+// componentStatus presents one status of a single check object
+// an object that provides detailed health statuses of additional downstream systems and endpoints
+// which can affect the overall health of the main API.
+type componentStatus struct {
+ Status status `json:"status"`
+ Time string `json:"time"` // the date-time, in ISO8601 format
+ Output string `json:"output,omitempty"` // this field SHOULD be omitted for "pass" state.
+}
+
+// Check is the health check API handler
+func Check(w http.ResponseWriter, r *http.Request) {
+ rsp := response{
+ Status: pass,
+ Description: setting.AppName,
+ Checks: make(checks),
+ }
+
+ statuses := make([]status, 0)
+ if setting.InstallLock {
+ statuses = append(statuses, checkDatabase(rsp.Checks))
+ statuses = append(statuses, checkCache(rsp.Checks))
+ }
+ for _, s := range statuses {
+ if s != pass {
+ rsp.Status = fail
+ break
+ }
+ }
+
+ data, _ := json.MarshalIndent(rsp, "", " ")
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(rsp.Status.ToHTTPStatus())
+ _, _ = w.Write(data)
+}
+
+// database checks gitea database status
+func checkDatabase(checks checks) status {
+ st := componentStatus{}
+ if err := db.GetEngine(db.DefaultContext).Ping(); err != nil {
+ st.Status = fail
+ st.Time = getCheckTime()
+ log.Error("database ping failed with error: %v", err)
+ } else {
+ st.Status = pass
+ st.Time = getCheckTime()
+ }
+
+ if setting.Database.UseSQLite3 && st.Status == pass {
+ if !setting.EnableSQLite3 {
+ st.Status = fail
+ st.Time = getCheckTime()
+ log.Error("SQLite3 health check failed with error: %v", "this Gitea binary is built without SQLite3 enabled")
+ } else {
+ if _, err := os.Stat(setting.Database.Path); err != nil {
+ st.Status = fail
+ st.Time = getCheckTime()
+ log.Error("SQLite3 file exists check failed with error: %v", err)
+ }
+ }
+ }
+
+ checks["database:ping"] = []componentStatus{st}
+ return st.Status
+}
+
+// cache checks gitea cache status
+func checkCache(checks checks) status {
+ if !setting.CacheService.Enabled {
+ return pass
+ }
+
+ st := componentStatus{}
+ if err := cache.GetCache().Ping(); err != nil {
+ st.Status = fail
+ st.Time = getCheckTime()
+ log.Error("cache ping failed with error: %v", err)
+ } else {
+ st.Status = pass
+ st.Time = getCheckTime()
+ }
+ checks["cache:ping"] = []componentStatus{st}
+ return st.Status
+}
+
+func getCheckTime() string {
+ return time.Now().UTC().Format(time.RFC3339)
+}
diff --git a/routers/web/home.go b/routers/web/home.go
index 9036814ddfa9d..ecfecf6e67e4a 100644
--- a/routers/web/home.go
+++ b/routers/web/home.go
@@ -1,17 +1,23 @@
// 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.
+// SPDX-License-Identifier: MIT
package web
import (
"net/http"
+ "strconv"
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/sitemap"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/web/auth"
"code.gitea.io/gitea/routers/web/user"
@@ -59,6 +65,52 @@ func Home(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplHome)
}
+// HomeSitemap renders the main sitemap
+func HomeSitemap(ctx *context.Context) {
+ m := sitemap.NewSitemapIndex()
+ if !setting.Service.Explore.DisableUsersPage {
+ _, cnt, err := user_model.SearchUsers(&user_model.SearchUserOptions{
+ Type: user_model.UserTypeIndividual,
+ ListOptions: db.ListOptions{PageSize: 1},
+ IsActive: util.OptionalBoolTrue,
+ Visible: []structs.VisibleType{structs.VisibleTypePublic},
+ })
+ if err != nil {
+ ctx.ServerError("SearchUsers", err)
+ return
+ }
+ count := int(cnt)
+ idx := 1
+ for i := 0; i < count; i += setting.UI.SitemapPagingNum {
+ m.Add(sitemap.URL{URL: setting.AppURL + "explore/users/sitemap-" + strconv.Itoa(idx) + ".xml"})
+ idx++
+ }
+ }
+
+ _, cnt, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ ListOptions: db.ListOptions{
+ PageSize: 1,
+ },
+ Actor: ctx.Doer,
+ AllPublic: true,
+ })
+ if err != nil {
+ ctx.ServerError("SearchRepository", err)
+ return
+ }
+ count := int(cnt)
+ idx := 1
+ for i := 0; i < count; i += setting.UI.SitemapPagingNum {
+ m.Add(sitemap.URL{URL: setting.AppURL + "explore/repos/sitemap-" + strconv.Itoa(idx) + ".xml"})
+ idx++
+ }
+
+ ctx.Resp.Header().Set("Content-Type", "text/xml")
+ if _, err := m.WriteTo(ctx.Resp); err != nil {
+ log.Error("Failed writing sitemap: %v", err)
+ }
+}
+
// NotFound render 404 page
func NotFound(ctx *context.Context) {
ctx.Data["Title"] = "Page Not Found"
diff --git a/routers/web/metrics.go b/routers/web/metrics.go
index c7e01b8faafa3..46c13f0a24402 100644
--- a/routers/web/metrics.go
+++ b/routers/web/metrics.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package web
diff --git a/routers/web/misc/markdown.go b/routers/web/misc/markdown.go
index b37aaf10ffecb..aaa3ed0781f7c 100644
--- a/routers/web/misc/markdown.go
+++ b/routers/web/misc/markdown.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// 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.
+// SPDX-License-Identifier: MIT
package misc
@@ -16,6 +15,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+
"mvdan.cc/xurls/v2"
)
diff --git a/routers/web/misc/swagger.go b/routers/web/misc/swagger.go
index e46d4194b46e8..72c09a3780174 100644
--- a/routers/web/misc/swagger.go
+++ b/routers/web/misc/swagger.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package misc
diff --git a/routers/web/nodeinfo.go b/routers/web/nodeinfo.go
index 2ee94e35e86b9..01b71e7086bdc 100644
--- a/routers/web/nodeinfo.go
+++ b/routers/web/nodeinfo.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package web
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 24a0f13b542e1..4cc364acd3a01 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package org
@@ -8,7 +7,6 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
@@ -40,11 +38,6 @@ func Home(ctx *context.Context) {
org := ctx.Org.Organization
- if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) {
- ctx.NotFound("HasOrgOrUserVisible", nil)
- return
- }
-
ctx.Data["PageIsUserProfile"] = true
ctx.Data["Title"] = org.DisplayName()
if len(org.Description) != 0 {
@@ -105,7 +98,7 @@ func Home(ctx *context.Context) {
count int64
err error
)
- repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+ repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
diff --git a/routers/web/org/members.go b/routers/web/org/members.go
index add8e724bda79..8361da663211c 100644
--- a/routers/web/org/members.go
+++ b/routers/web/org/members.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package org
@@ -45,6 +44,7 @@ func Members(ctx *context.Context) {
}
opts.PublicOnly = !isMember && !ctx.Doer.IsAdmin
}
+ ctx.Data["PublicOnly"] = opts.PublicOnly
total, err := organization.CountOrgMembers(opts)
if err != nil {
@@ -63,7 +63,7 @@ func Members(ctx *context.Context) {
ctx.Data["Page"] = pager
ctx.Data["Members"] = members
ctx.Data["MembersIsPublicMember"] = membersIsPublic
- ctx.Data["MembersIsUserOrgOwner"] = models.IsUserOrgOwner(members, org.ID)
+ ctx.Data["MembersIsUserOrgOwner"] = organization.IsUserOrgOwner(members, org.ID)
ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus()
ctx.HTML(http.StatusOK, tplMembers)
@@ -107,13 +107,20 @@ func MembersAction(ctx *context.Context) {
}
case "leave":
err = models.RemoveOrgUser(org.ID, ctx.Doer.ID)
- if organization.IsErrLastOrgOwner(err) {
+ if err == nil {
+ ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": "", // keep the user stay on current page, in case they want to do other operations.
+ })
+ } else if organization.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Org.OrgLink + "/members",
})
- return
+ } else {
+ log.Error("RemoveOrgUser(%d,%d): %v", org.ID, ctx.Doer.ID, err)
}
+ return
}
if err != nil {
diff --git a/routers/web/org/org.go b/routers/web/org/org.go
index 32d8787995d06..f67e7edb4c7cc 100644
--- a/routers/web/org/org.go
+++ b/routers/web/org/org.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package org
diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go
index d79ffc597c315..1c910a93a50bd 100644
--- a/routers/web/org/org_labels.go
+++ b/routers/web/org/org_labels.go
@@ -1,14 +1,13 @@
// 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.
+// SPDX-License-Identifier: MIT
package org
import (
"net/http"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/web"
@@ -17,7 +16,7 @@ import (
// RetrieveLabels find all the labels of an organization
func RetrieveLabels(ctx *context.Context) {
- labels, err := models.GetLabelsByOrgID(ctx.Org.Organization.ID, ctx.FormString("sort"), db.ListOptions{})
+ labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
ctx.ServerError("RetrieveLabels.GetLabels", err)
return
@@ -43,13 +42,13 @@ func NewLabel(ctx *context.Context) {
return
}
- l := &models.Label{
+ l := &issues_model.Label{
OrgID: ctx.Org.Organization.ID,
Name: form.Title,
Description: form.Description,
Color: form.Color,
}
- if err := models.NewLabel(ctx, l); err != nil {
+ if err := issues_model.NewLabel(ctx, l); err != nil {
ctx.ServerError("NewLabel", err)
return
}
@@ -59,10 +58,10 @@ func NewLabel(ctx *context.Context) {
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateLabelForm)
- l, err := models.GetLabelInOrgByID(ctx.Org.Organization.ID, form.ID)
+ l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, form.ID)
if err != nil {
switch {
- case models.IsErrOrgLabelNotExist(err):
+ case issues_model.IsErrOrgLabelNotExist(err):
ctx.Error(http.StatusNotFound)
default:
ctx.ServerError("UpdateLabel", err)
@@ -73,7 +72,7 @@ func UpdateLabel(ctx *context.Context) {
l.Name = form.Title
l.Description = form.Description
l.Color = form.Color
- if err := models.UpdateLabel(l); err != nil {
+ if err := issues_model.UpdateLabel(l); err != nil {
ctx.ServerError("UpdateLabel", err)
return
}
@@ -82,7 +81,7 @@ func UpdateLabel(ctx *context.Context) {
// DeleteLabel delete a label
func DeleteLabel(ctx *context.Context) {
- if err := models.DeleteLabel(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil {
+ if err := issues_model.DeleteLabel(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil {
ctx.Flash.Error("DeleteLabel: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index 5cd245ef09a3b..e625962f7597a 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package org
@@ -13,6 +12,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
+ secret_model "code.gitea.io/gitea/models/secret"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
@@ -24,6 +24,8 @@ import (
user_setting "code.gitea.io/gitea/routers/web/user/setting"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/org"
+ container_service "code.gitea.io/gitea/services/packages/container"
+ repo_service "code.gitea.io/gitea/services/repository"
user_service "code.gitea.io/gitea/services/user"
)
@@ -36,6 +38,8 @@ const (
tplSettingsHooks base.TplName = "org/settings/hooks"
// tplSettingsLabels template path for render labels settings
tplSettingsLabels base.TplName = "org/settings/labels"
+ // tplSettingsSecrets template path for render secrets settings
+ tplSettingsSecrets base.TplName = "org/settings/secrets"
)
// Settings render the main settings page
@@ -66,7 +70,7 @@ func SettingsPost(ctx *context.Context) {
// Check if organization name has been changed.
if org.LowerName != strings.ToLower(form.Name) {
- isExist, err := user_model.IsUserExist(org.ID, form.Name)
+ isExist, err := user_model.IsUserExist(ctx, org.ID, form.Name)
if err != nil {
ctx.ServerError("IsUserExist", err)
return
@@ -87,6 +91,12 @@ func SettingsPost(ctx *context.Context) {
}
return
}
+
+ if err := container_service.UpdateRepositoryNames(ctx, org.AsUser(), form.Name); err != nil {
+ ctx.ServerError("UpdateRepositoryNames", err)
+ return
+ }
+
// reset ctx.org.OrgLink with new name
ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(form.Name)
log.Trace("Organization name changed: %s -> %s", org.Name, form.Name)
@@ -110,14 +120,14 @@ func SettingsPost(ctx *context.Context) {
visibilityChanged := form.Visibility != org.Visibility
org.Visibility = form.Visibility
- if err := user_model.UpdateUser(org.AsUser(), false); err != nil {
+ if err := user_model.UpdateUser(ctx, org.AsUser(), false); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
// update forks visibility
if visibilityChanged {
- repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{
+ repos, _, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{
Actor: org.AsUser(), Private: true, ListOptions: db.ListOptions{Page: 1, PageSize: org.NumRepos},
})
if err != nil {
@@ -126,7 +136,7 @@ func SettingsPost(ctx *context.Context) {
}
for _, repo := range repos {
repo.OwnerName = org.Name
- if err := models.UpdateRepository(repo, true); err != nil {
+ if err := repo_service.UpdateRepository(repo, true); err != nil {
ctx.ServerError("UpdateRepository", err)
return
}
@@ -207,7 +217,7 @@ func Webhooks(ctx *context.Context) {
ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks"
ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc")
- ws, err := webhook.ListWebhooksByOpts(&webhook.ListWebhookOptions{OrgID: ctx.Org.Organization.ID})
+ ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OrgID: ctx.Org.Organization.ID})
if err != nil {
ctx.ServerError("GetWebhooksByOrgId", err)
return
@@ -239,3 +249,51 @@ func Labels(ctx *context.Context) {
ctx.Data["LabelTemplates"] = repo_module.LabelTemplates
ctx.HTML(http.StatusOK, tplSettingsLabels)
}
+
+// Secrets render organization secrets page
+func Secrets(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.secrets")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsOrgSettingsSecrets"] = true
+
+ secrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{OwnerID: ctx.Org.Organization.ID})
+ if err != nil {
+ ctx.ServerError("FindSecrets", err)
+ return
+ }
+ ctx.Data["Secrets"] = secrets
+
+ ctx.HTML(http.StatusOK, tplSettingsSecrets)
+}
+
+// SecretsPost add secrets
+func SecretsPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.AddSecretForm)
+
+ _, err := secret_model.InsertEncryptedSecret(ctx, ctx.Org.Organization.ID, 0, form.Title, form.Content)
+ if err != nil {
+ ctx.Flash.Error(ctx.Tr("secrets.creation.failed"))
+ log.Error("validate secret: %v", err)
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/secrets")
+ return
+ }
+
+ log.Trace("Org %d: secret added", ctx.Org.Organization.ID)
+ ctx.Flash.Success(ctx.Tr("secrets.creation.success", form.Title))
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/secrets")
+}
+
+// SecretsDelete delete secrets
+func SecretsDelete(ctx *context.Context) {
+ id := ctx.FormInt64("id")
+ if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id}); err != nil {
+ ctx.Flash.Error(ctx.Tr("secrets.deletion.failed"))
+ log.Error("delete secret %d: %v", id, err)
+ } else {
+ ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": ctx.Org.OrgLink + "/settings/secrets",
+ })
+}
diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go
new file mode 100644
index 0000000000000..9bf4280b07e53
--- /dev/null
+++ b/routers/web/org/setting_oauth2.go
@@ -0,0 +1,92 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "fmt"
+ "net/http"
+
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
+ user_setting "code.gitea.io/gitea/routers/web/user/setting"
+)
+
+const (
+ tplSettingsApplications base.TplName = "org/settings/applications"
+ tplSettingsOAuthApplicationEdit base.TplName = "org/settings/applications_oauth2_edit"
+)
+
+func newOAuth2CommonHandlers(org *context.Organization) *user_setting.OAuth2CommonHandlers {
+ return &user_setting.OAuth2CommonHandlers{
+ OwnerID: org.Organization.ID,
+ BasePathList: fmt.Sprintf("%s/org/%s/settings/applications", setting.AppSubURL, org.Organization.Name),
+ BasePathEditPrefix: fmt.Sprintf("%s/org/%s/settings/applications/oauth2", setting.AppSubURL, org.Organization.Name),
+ TplAppEdit: tplSettingsOAuthApplicationEdit,
+ }
+}
+
+// Applications render org applications page (for org, at the moment, there are only OAuth2 applications)
+func Applications(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings.applications")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsApplications"] = true
+
+ apps, err := auth.GetOAuth2ApplicationsByUserID(ctx, ctx.Org.Organization.ID)
+ if err != nil {
+ ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
+ return
+ }
+ ctx.Data["Applications"] = apps
+
+ ctx.HTML(http.StatusOK, tplSettingsApplications)
+}
+
+// OAuthApplicationsPost response for adding an oauth2 application
+func OAuthApplicationsPost(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings.applications")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsApplications"] = true
+
+ oa := newOAuth2CommonHandlers(ctx.Org)
+ oa.AddApp(ctx)
+}
+
+// OAuth2ApplicationShow displays the given application
+func OAuth2ApplicationShow(ctx *context.Context) {
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsApplications"] = true
+
+ oa := newOAuth2CommonHandlers(ctx.Org)
+ oa.EditShow(ctx)
+}
+
+// OAuth2ApplicationEdit response for editing oauth2 application
+func OAuth2ApplicationEdit(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings.applications")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsApplications"] = true
+
+ oa := newOAuth2CommonHandlers(ctx.Org)
+ oa.EditSave(ctx)
+}
+
+// OAuthApplicationsRegenerateSecret handles the post request for regenerating the secret
+func OAuthApplicationsRegenerateSecret(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsApplications"] = true
+
+ oa := newOAuth2CommonHandlers(ctx.Org)
+ oa.RegenerateSecret(ctx)
+}
+
+// DeleteOAuth2Application deletes the given oauth2 application
+func DeleteOAuth2Application(ctx *context.Context) {
+ oa := newOAuth2CommonHandlers(ctx.Org)
+ oa.DeleteApp(ctx)
+}
+
+// TODO: revokes the grant with the given id
diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go
new file mode 100644
index 0000000000000..80135ca2d0a01
--- /dev/null
+++ b/routers/web/org/setting_packages.go
@@ -0,0 +1,86 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "fmt"
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
+ shared "code.gitea.io/gitea/routers/web/shared/packages"
+)
+
+const (
+ tplSettingsPackages base.TplName = "org/settings/packages"
+ tplSettingsPackagesRuleEdit base.TplName = "org/settings/packages_cleanup_rules_edit"
+ tplSettingsPackagesRulePreview base.TplName = "org/settings/packages_cleanup_rules_preview"
+)
+
+func Packages(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("packages.title")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.SetPackagesContext(ctx, ctx.ContextUser)
+
+ ctx.HTML(http.StatusOK, tplSettingsPackages)
+}
+
+func PackagesRuleAdd(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("packages.title")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.SetRuleAddContext(ctx)
+
+ ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
+}
+
+func PackagesRuleEdit(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("packages.title")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.SetRuleEditContext(ctx, ctx.ContextUser)
+
+ ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
+}
+
+func PackagesRuleAddPost(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("packages.title")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.PerformRuleAddPost(
+ ctx,
+ ctx.ContextUser,
+ fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name),
+ tplSettingsPackagesRuleEdit,
+ )
+}
+
+func PackagesRuleEditPost(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("packages.title")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.PerformRuleEditPost(
+ ctx,
+ ctx.ContextUser,
+ fmt.Sprintf("%s/org/%s/settings/packages", setting.AppSubURL, ctx.ContextUser.Name),
+ tplSettingsPackagesRuleEdit,
+ )
+}
+
+func PackagesRulePreview(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("packages.title")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.SetRulePreviewContext(ctx, ctx.ContextUser)
+
+ ctx.HTML(http.StatusOK, tplSettingsPackagesRulePreview)
+}
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
index 31bfaea92f310..d9754633bfd97 100644
--- a/routers/web/org/teams.go
+++ b/routers/web/org/teams.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package org
@@ -14,19 +13,20 @@ 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"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
+ "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
+ org_service "code.gitea.io/gitea/services/org"
)
const (
@@ -38,6 +38,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
@@ -47,7 +49,7 @@ func Teams(ctx *context.Context) {
ctx.Data["PageIsOrgTeams"] = true
for _, t := range ctx.Org.Teams {
- if err := t.GetMembersCtx(ctx); err != nil {
+ if err := t.LoadMembers(ctx); err != nil {
ctx.ServerError("GetMembers", err)
return
}
@@ -59,12 +61,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 +73,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 +94,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)
@@ -123,13 +126,26 @@ func TeamsAction(ctx *context.Context) {
}
uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("uname")))
var u *user_model.User
- u, err = user_model.GetUserByName(uname)
+ 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 +162,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)
@@ -195,7 +230,7 @@ func TeamsRepoAction(ctx *context.Context) {
ctx.ServerError("GetRepositoryByName", err)
return
}
- err = models.AddRepository(ctx.Org.Team, repo)
+ err = org_service.TeamAddRepository(ctx.Org.Team, repo)
case "remove":
err = models.RemoveRepository(ctx.Org.Team, ctx.FormInt64("repoid"))
case "addall":
@@ -224,7 +259,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 +290,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 +300,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 +330,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)
@@ -311,11 +346,20 @@ func TeamMembers(ctx *context.Context) {
ctx.Data["Title"] = ctx.Org.Team.Name
ctx.Data["PageIsOrgTeams"] = true
ctx.Data["PageIsOrgTeamMembers"] = true
- if err := ctx.Org.Team.GetMembersCtx(ctx); err != nil {
+ if err := ctx.Org.Team.LoadMembers(ctx); err != nil {
ctx.ServerError("GetMembers", err)
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)
}
@@ -324,7 +368,7 @@ func TeamRepositories(ctx *context.Context) {
ctx.Data["Title"] = ctx.Org.Team.Name
ctx.Data["PageIsOrgTeams"] = true
ctx.Data["PageIsOrgTeamRepos"] = true
- if err := ctx.Org.Team.GetRepositoriesCtx(ctx); err != nil {
+ if err := ctx.Org.Team.LoadRepositories(ctx); err != nil {
ctx.ServerError("GetRepositories", err)
return
}
@@ -339,15 +383,15 @@ func SearchTeam(ctx *context.Context) {
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
}
- opts := &organization.SearchTeamOptions{
- UserID: ctx.Doer.ID,
+ 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,
IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"),
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{}{
@@ -357,17 +401,14 @@ func SearchTeam(ctx *context.Context) {
return
}
- apiTeams := make([]*api.Team, len(teams))
- for i := range teams {
- if err := teams[i].GetUnits(); err != nil {
- log.Error("Team GetUnits failed: %v", err)
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "ok": false,
- "error": "SearchTeam failed to get units",
- })
- return
- }
- apiTeams[i] = convert.ToTeam(teams[i])
+ apiTeams, err := convert.ToTeams(teams, false)
+ if err != nil {
+ log.Error("convert ToTeams failed: %v", err)
+ ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
+ "ok": false,
+ "error": "SearchTeam failed to get units",
+ })
+ return
}
ctx.SetTotalCountHeader(maxResults)
@@ -420,24 +461,27 @@ func EditTeamPost(ctx *context.Context) {
isIncludeAllChanged = true
t.IncludesAllRepositories = includesAllRepositories
}
+ t.CanCreateOrgRepo = form.CanCreateOrgRepo
+ } else {
+ t.CanCreateOrgRepo = true
}
+
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
}
}
- t.CanCreateOrgRepo = form.CanCreateOrgRepo
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplTeamNew)
@@ -452,7 +496,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 +518,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.GetUserByID(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.GetUserByID(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/activity.go b/routers/web/repo/activity.go
index b2f25ebe724e4..3d030edaca065 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -8,7 +7,7 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
@@ -47,12 +46,12 @@ func Activity(ctx *context.Context) {
ctx.Data["Period"] = "weekly"
timeFrom = timeUntil.Add(-time.Hour * 168)
}
- ctx.Data["DateFrom"] = timeFrom.Format("January 2, 2006")
- ctx.Data["DateUntil"] = timeUntil.Format("January 2, 2006")
+ ctx.Data["DateFrom"] = timeFrom.UTC().Format(time.RFC3339)
+ ctx.Data["DateUntil"] = timeUntil.UTC().Format(time.RFC3339)
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
var err error
- if ctx.Data["Activity"], err = models.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
+ if ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
ctx.Repo.CanRead(unit.TypeReleases),
ctx.Repo.CanRead(unit.TypeIssues),
ctx.Repo.CanRead(unit.TypePullRequests),
@@ -61,7 +60,7 @@ func Activity(ctx *context.Context) {
return
}
- if ctx.PageData["repoActivityTopAuthors"], err = models.GetActivityStatsTopAuthors(ctx, ctx.Repo.Repository, timeFrom, 10); err != nil {
+ if ctx.PageData["repoActivityTopAuthors"], err = activities_model.GetActivityStatsTopAuthors(ctx, ctx.Repo.Repository, timeFrom, 10); err != nil {
ctx.ServerError("GetActivityStatsTopAuthors", err)
return
}
@@ -94,7 +93,7 @@ func ActivityAuthors(ctx *context.Context) {
}
var err error
- authors, err := models.GetActivityStatsTopAuthors(ctx, ctx.Repo.Repository, timeFrom, 10)
+ authors, err := activities_model.GetActivityStatsTopAuthors(ctx, ctx.Repo.Repository, timeFrom, 10)
if err != nil {
ctx.ServerError("GetActivityStatsTopAuthors", err)
return
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index be5b5812d3805..589632ad6e10d 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -8,7 +7,7 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/models"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/httpcache"
@@ -18,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/attachment"
+ repo_service "code.gitea.io/gitea/services/repository"
)
// UploadIssueAttachment response for Issue/PR attachments
@@ -44,7 +44,11 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
}
defer file.Close()
- attach, err := attachment.UploadAttachment(file, ctx.Doer.ID, repoID, 0, header.Filename, allowedTypes)
+ attach, err := attachment.UploadAttachment(file, allowedTypes, &repo_model.Attachment{
+ Name: header.Filename,
+ UploaderID: ctx.Doer.ID,
+ RepoID: repoID,
+ })
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.Error(http.StatusBadRequest, err.Error())
@@ -63,7 +67,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
// DeleteAttachment response for deleting issue's attachment
func DeleteAttachment(ctx *context.Context) {
file := ctx.FormString("file")
- attach, err := repo_model.GetAttachmentByUUID(file)
+ attach, err := repo_model.GetAttachmentByUUID(ctx, file)
if err != nil {
ctx.Error(http.StatusBadRequest, err.Error())
return
@@ -82,9 +86,9 @@ func DeleteAttachment(ctx *context.Context) {
})
}
-// GetAttachment serve attachements
+// GetAttachment serve attachments
func GetAttachment(ctx *context.Context) {
- attach, err := repo_model.GetAttachmentByUUID(ctx.Params(":uuid"))
+ attach, err := repo_model.GetAttachmentByUUID(ctx, ctx.Params(":uuid"))
if err != nil {
if repo_model.IsErrAttachmentNotExist(err) {
ctx.Error(http.StatusNotFound)
@@ -94,7 +98,7 @@ func GetAttachment(ctx *context.Context) {
return
}
- repository, unitType, err := models.LinkedRepository(attach)
+ repository, unitType, err := repo_service.LinkedRepository(ctx, attach)
if err != nil {
ctx.ServerError("LinkedRepository", err)
return
@@ -106,7 +110,7 @@ func GetAttachment(ctx *context.Context) {
return
}
} else { // If we have the repository we check access
- perm, err := models.GetUserRepoPermission(repository, ctx.Doer)
+ perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
return
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index e96e2142d295a..50bfa9d3bd289 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -40,7 +39,7 @@ type blameRow struct {
CommitMessage string
CommitSince gotemplate.HTML
Code gotemplate.HTML
- EscapeStatus charset.EscapeStatus
+ EscapeStatus *charset.EscapeStatus
}
// RefBlame render blame page
@@ -100,6 +99,8 @@ func RefBlame(ctx *context.Context) {
ctx.Data["FileName"] = blob.Name()
ctx.Data["NumLines"], err = blob.GetBlobLineCount()
+ ctx.Data["NumLinesSet"] = true
+
if err != nil {
ctx.NotFound("GetBlobLineCount", err)
return
@@ -216,7 +217,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,
@@ -235,7 +236,9 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
}
lines := make([]string, 0)
rows := make([]*blameRow, 0)
- escapeStatus := charset.EscapeStatus{}
+ escapeStatus := &charset.EscapeStatus{}
+
+ var lexerName string
i := 0
commitCnt := 0
@@ -255,7 +258,7 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
commitCnt++
// User avatar image
- commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Locale.Language())
+ commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Locale)
var avatar string
if commit.User != nil {
@@ -278,9 +281,15 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
line += "\n"
}
fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
- line = highlight.Code(fileName, language, line)
+ line, lexerNameForLine := highlight.Code(fileName, language, line)
+
+ // set lexer name to the first detected lexer. this is certainly suboptimal and
+ // we should instead highlight the whole file at once
+ if lexerName == "" {
+ lexerName = lexerNameForLine
+ }
- br.EscapeStatus, line = charset.EscapeControlString(line)
+ br.EscapeStatus, line = charset.EscapeControlHTML(line, ctx.Locale)
br.Code = gotemplate.HTML(line)
rows = append(rows, br)
escapeStatus = escapeStatus.Or(br.EscapeStatus)
@@ -290,4 +299,5 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
ctx.Data["EscapeStatus"] = escapeStatus
ctx.Data["BlameRows"] = rows
ctx.Data["CommitCnt"] = commitCnt
+ ctx.Data["LexerName"] = lexerName
}
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index 0d139ec79c48e..b34ccf8538553 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -12,6 +11,8 @@ import (
"strings"
"code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
@@ -40,10 +41,10 @@ type Branch struct {
IsProtected bool
IsDeleted bool
IsIncluded bool
- DeletedBranch *models.DeletedBranch
+ DeletedBranch *git_model.DeletedBranch
CommitsAhead int
CommitsBehind int
- LatestPullRequest *models.PullRequest
+ LatestPullRequest *issues_model.PullRequest
MergeMovedOn bool
}
@@ -78,7 +79,7 @@ func Branches(ctx *context.Context) {
}
ctx.Data["Branches"] = branches
ctx.Data["DefaultBranchBranch"] = defaultBranchBranch
- pager := context.NewPagination(int(branchesCount), setting.Git.BranchesRangeSize, page, 5)
+ pager := context.NewPagination(branchesCount, setting.Git.BranchesRangeSize, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
@@ -98,7 +99,7 @@ func DeleteBranchPost(ctx *context.Context) {
case errors.Is(err, repo_service.ErrBranchIsDefault):
log.Debug("DeleteBranch: Can't delete default branch '%s'", branchName)
ctx.Flash.Error(ctx.Tr("repo.branch.default_deletion_failed", branchName))
- case errors.Is(err, repo_service.ErrBranchIsProtected):
+ case errors.Is(err, git_model.ErrBranchIsProtected):
log.Debug("DeleteBranch: Can't delete protected branch '%s'", branchName)
ctx.Flash.Error(ctx.Tr("repo.branch.protected_deletion_failed", branchName))
default:
@@ -119,17 +120,21 @@ func RestoreBranchPost(ctx *context.Context) {
branchID := ctx.FormInt64("branch_id")
branchName := ctx.FormString("name")
- deletedBranch, err := models.GetDeletedBranchByID(ctx.Repo.Repository.ID, branchID)
+ deletedBranch, err := git_model.GetDeletedBranchByID(ctx, ctx.Repo.Repository.ID, branchID)
if err != nil {
log.Error("GetDeletedBranchByID: %v", err)
ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", branchName))
return
+ } else if deletedBranch == nil {
+ log.Debug("RestoreBranch: Can't restore branch[%d] '%s', as it does not exist", branchID, branchName)
+ ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", branchName))
+ return
}
if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{
Remote: ctx.Repo.Repository.RepoPath(),
Branch: fmt.Sprintf("%s:%s%s", deletedBranch.Commit, git.BranchPrefix, deletedBranch.Name),
- Env: models.PushingEnvironment(ctx.Doer, ctx.Repo.Repository),
+ Env: repo_module.PushingEnvironment(ctx.Doer, ctx.Repo.Repository),
}); err != nil {
if strings.Contains(err.Error(), "already exists") {
log.Debug("RestoreBranch: Can't restore branch '%s', since one with same name already exist", deletedBranch.Name)
@@ -184,9 +189,9 @@ func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, in
return nil, nil, 0
}
- protectedBranches, err := models.GetProtectedBranches(ctx.Repo.Repository.ID)
+ rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID)
if err != nil {
- ctx.ServerError("GetProtectedBranches", err)
+ ctx.ServerError("FindRepoProtectedBranchRules", err)
return nil, nil, 0
}
@@ -203,7 +208,7 @@ func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, in
continue
}
- branch := loadOneBranch(ctx, rawBranches[i], defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo)
+ branch := loadOneBranch(ctx, rawBranches[i], defaultBranch, &rules, repoIDToRepo, repoIDToGitRepo)
if branch == nil {
return nil, nil, 0
}
@@ -215,7 +220,7 @@ func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, in
if defaultBranch != nil {
// Always add the default branch
log.Debug("loadOneBranch: load default: '%s'", defaultBranch.Name)
- defaultBranchBranch = loadOneBranch(ctx, defaultBranch, defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo)
+ defaultBranchBranch = loadOneBranch(ctx, defaultBranch, defaultBranch, &rules, repoIDToRepo, repoIDToGitRepo)
branches = append(branches, defaultBranchBranch)
}
@@ -231,7 +236,7 @@ func loadBranches(ctx *context.Context, skip, limit int) (*Branch, []*Branch, in
return defaultBranchBranch, branches, totalNumOfBranches
}
-func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, protectedBranches []*models.ProtectedBranch,
+func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, protectedBranches *git_model.ProtectedBranchRules,
repoIDToRepo map[int64]*repo_model.Repository,
repoIDToGitRepo map[int64]*git.Repository,
) *Branch {
@@ -244,13 +249,8 @@ func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, p
}
branchName := rawBranch.Name
- var isProtected bool
- for _, b := range protectedBranches {
- if b.BranchName == branchName {
- isProtected = true
- break
- }
- }
+ p := protectedBranches.GetFirstMatched(branchName)
+ isProtected := p != nil
divergence := &git.DivergeObject{
Ahead: -1,
@@ -263,7 +263,7 @@ func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, p
}
}
- pr, err := models.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName)
+ pr, err := issues_model.GetLatestPullRequestByHeadInfo(ctx.Repo.Repository.ID, branchName)
if err != nil {
ctx.ServerError("GetLatestPullRequestByHeadInfo", err)
return nil
@@ -273,14 +273,14 @@ func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, p
mergeMovedOn := false
if pr != nil {
pr.HeadRepo = ctx.Repo.Repository
- if err := pr.LoadIssue(); err != nil {
- ctx.ServerError("pr.LoadIssue", err)
+ if err := pr.LoadIssue(ctx); err != nil {
+ ctx.ServerError("LoadIssue", err)
return nil
}
if repo, ok := repoIDToRepo[pr.BaseRepoID]; ok {
pr.BaseRepo = repo
- } else if err := pr.LoadBaseRepo(); err != nil {
- ctx.ServerError("pr.LoadBaseRepo", err)
+ } else if err := pr.LoadBaseRepo(ctx); err != nil {
+ ctx.ServerError("LoadBaseRepo", err)
return nil
} else {
repoIDToRepo[pr.BaseRepoID] = pr.BaseRepo
@@ -326,13 +326,13 @@ func loadOneBranch(ctx *context.Context, rawBranch, defaultBranch *git.Branch, p
func getDeletedBranches(ctx *context.Context) ([]*Branch, error) {
branches := []*Branch{}
- deletedBranches, err := models.GetDeletedBranches(ctx.Repo.Repository.ID)
+ deletedBranches, err := git_model.GetDeletedBranches(ctx, ctx.Repo.Repository.ID)
if err != nil {
return branches, err
}
for i := range deletedBranches {
- deletedBranches[i].LoadUser()
+ deletedBranches[i].LoadUser(ctx)
branches = append(branches, &Branch{
Name: deletedBranches[i].Name,
IsDeleted: true,
@@ -371,6 +371,12 @@ func CreateBranch(ctx *context.Context) {
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName)
}
if err != nil {
+ if models.IsErrProtectedTagName(err) {
+ ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
+ return
+ }
+
if models.IsErrTagAlreadyExists(err) {
e := err.(models.ErrTagAlreadyExists)
ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName))
@@ -419,5 +425,5 @@ func CreateBranch(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName))
- ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(form.NewBranchName))
+ ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(form.NewBranchName) + "/" + util.PathEscapeSegments(form.CurrentPath))
}
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
index 926361ccd7686..48bc6959e075f 100644
--- a/routers/web/repo/cherry_pick.go
+++ b/routers/web/repo/cherry_pick.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -47,8 +46,6 @@ func CherryPick(ctx *context.Context) {
ctx.Data["commit_message"] = splits[1]
}
- ctx.Data["RequireHighlightJS"] = true
-
canCommit := renderCommitRights(ctx)
ctx.Data["TreePath"] = ""
@@ -77,7 +74,6 @@ func CherryPickPost(ctx *context.Context) {
ctx.Data["CherryPickType"] = "cherry-pick"
}
- ctx.Data["RequireHighlightJS"] = true
canCommit := renderCommitRights(ctx)
branchName := ctx.Repo.BranchName
if form.CommitChoice == frmCommitChoiceNewBranch {
@@ -151,7 +147,7 @@ func CherryPickPost(ctx *context.Context) {
return
}
} else {
- if err := git.GetRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, git.RawDiffType("patch"), buf); err != nil {
+ if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.Params(":sha")+" does not exist."))
return
@@ -181,7 +177,7 @@ func CherryPickPost(ctx *context.Context) {
}
}
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(unit.TypePullRequests) {
+ if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
} else {
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName))
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 2b1b5440d04aa..9f94159d0d464 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -1,18 +1,18 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
import (
"errors"
+ "fmt"
"net/http"
"strings"
- "code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
@@ -75,14 +75,14 @@ func Commits(ctx *context.Context) {
ctx.ServerError("CommitsByRange", err)
return
}
- ctx.Data["Commits"] = models.ConvertFromGitCommit(commits, ctx.Repo.Repository)
+ ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository)
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["CommitCount"] = commitsCount
ctx.Data["RefName"] = ctx.Repo.RefName
- pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
+ pager := context.NewPagination(int(commitsCount), pageSize, page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
@@ -194,7 +194,7 @@ func SearchCommits(ctx *context.Context) {
return
}
ctx.Data["CommitCount"] = len(commits)
- ctx.Data["Commits"] = models.ConvertFromGitCommit(commits, ctx.Repo.Repository)
+ ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository)
ctx.Data["Keyword"] = query
if all {
@@ -235,7 +235,7 @@ func FileHistory(ctx *context.Context) {
ctx.ServerError("CommitsByFileAndRange", err)
return
}
- ctx.Data["Commits"] = models.ConvertFromGitCommit(commits, ctx.Repo.Repository)
+ ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository)
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
@@ -253,7 +253,6 @@ func FileHistory(ctx *context.Context) {
// Diff show different from current commit to previous commit
func Diff(ctx *context.Context) {
ctx.Data["PageIsDiff"] = true
- ctx.Data["RequireHighlightJS"] = true
ctx.Data["RequireTribute"] = true
userName := ctx.Repo.Owner.Name
@@ -284,7 +283,7 @@ func Diff(ctx *context.Context) {
}
return
}
- if len(commitID) != 40 {
+ if len(commitID) != git.SHAFullLength {
commitID = commit.ID.String()
}
@@ -336,12 +335,12 @@ func Diff(ctx *context.Context) {
ctx.Data["Commit"] = commit
ctx.Data["Diff"] = diff
- statuses, _, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, commitID, db.ListOptions{})
+ statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptions{})
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
- ctx.Data["CommitStatus"] = models.CalcCommitStatus(statuses)
+ ctx.Data["CommitStatus"] = git_model.CalcCommitStatus(statuses)
ctx.Data["CommitStatuses"] = statuses
verification := asymkey_model.ParseCommitWithSignature(commit)
@@ -351,7 +350,7 @@ func Diff(ctx *context.Context) {
ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
- return models.IsOwnerMemberCollaborator(ctx.Repo.Repository, user.ID)
+ return repo_model.IsOwnerMemberCollaborator(ctx.Repo.Repository, user.ID)
}, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return
@@ -381,15 +380,24 @@ func Diff(ctx *context.Context) {
// RawDiff dumps diff results of repository in given commit ID to io.Writer
func RawDiff(ctx *context.Context) {
- var repoPath string
+ var gitRepo *git.Repository
if ctx.Data["PageIsWiki"] != nil {
- repoPath = ctx.Repo.Repository.WikiPath()
+ wikiRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.WikiPath())
+ if err != nil {
+ ctx.ServerError("OpenRepository", err)
+ return
+ }
+ defer wikiRepo.Close()
+ gitRepo = wikiRepo
} else {
- repoPath = repo_model.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
+ gitRepo = ctx.Repo.GitRepo
+ if gitRepo == nil {
+ ctx.ServerError("GitRepo not open", fmt.Errorf("no open git repo for '%s'", ctx.Repo.Repository.FullName()))
+ return
+ }
}
if err := git.GetRawDiff(
- ctx,
- repoPath,
+ gitRepo,
ctx.Params(":sha"),
git.RawDiffType(ctx.Params(":ext")),
ctx.Resp,
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 60c6ae0298c22..8d45f8d674444 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -17,7 +16,9 @@ import (
"path/filepath"
"strings"
- "code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -110,17 +111,17 @@ func setCsvCompareContext(ctx *context.Context) {
Error string
}
- ctx.Data["CreateCsvDiff"] = func(diffFile *gitdiff.DiffFile, baseCommit, headCommit *git.Commit) CsvDiffResult {
- if diffFile == nil || baseCommit == nil || headCommit == nil {
+ ctx.Data["CreateCsvDiff"] = func(diffFile *gitdiff.DiffFile, baseBlob, headBlob *git.Blob) CsvDiffResult {
+ if diffFile == nil {
return CsvDiffResult{nil, ""}
}
errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large"))
- csvReaderFromCommit := func(ctx *markup.RenderContext, c *git.Commit) (*csv.Reader, io.Closer, error) {
- blob, err := c.GetBlobByPath(diffFile.Name)
- if err != nil {
- return nil, nil, err
+ csvReaderFromCommit := func(ctx *markup.RenderContext, blob *git.Blob) (*csv.Reader, io.Closer, error) {
+ if blob == nil {
+ // It's ok for blob to be nil (file added or deleted)
+ return nil, nil, nil
}
if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < blob.Size() {
@@ -136,27 +137,36 @@ func setCsvCompareContext(ctx *context.Context) {
return csvReader, reader, err
}
- baseReader, baseBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, Filename: diffFile.OldName}, baseCommit)
+ baseReader, baseBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.OldName}, baseBlob)
if baseBlobCloser != nil {
defer baseBlobCloser.Close()
}
- if err == errTooLarge {
- return CsvDiffResult{nil, err.Error()}
+ if err != nil {
+ if err == errTooLarge {
+ return CsvDiffResult{nil, err.Error()}
+ }
+ log.Error("error whilst creating csv.Reader from file %s in base commit %s in %s: %v", diffFile.Name, baseBlob.ID.String(), ctx.Repo.Repository.Name, err)
+ return CsvDiffResult{nil, "unable to load file"}
}
- headReader, headBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, Filename: diffFile.Name}, headCommit)
+
+ headReader, headBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.Name}, headBlob)
if headBlobCloser != nil {
defer headBlobCloser.Close()
}
- if err == errTooLarge {
- return CsvDiffResult{nil, err.Error()}
+ if err != nil {
+ if err == errTooLarge {
+ return CsvDiffResult{nil, err.Error()}
+ }
+ log.Error("error whilst creating csv.Reader from file %s in head commit %s in %s: %v", diffFile.Name, headBlob.ID.String(), ctx.Repo.Repository.Name, err)
+ return CsvDiffResult{nil, "unable to load file"}
}
sections, err := gitdiff.CreateCsvDiff(diffFile, baseReader, headReader)
if err != nil {
errMessage, err := csv_module.FormatError(err, ctx.Locale)
if err != nil {
- log.Error("RenderCsvDiff failed: %v", err)
- return CsvDiffResult{nil, ""}
+ log.Error("CreateCsvDiff FormatError failed: %v", err)
+ return CsvDiffResult{nil, "unknown csv diff error"}
}
return CsvDiffResult{nil, errMessage}
}
@@ -244,7 +254,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
} else if len(headInfos) == 2 {
headInfosSplit := strings.Split(headInfos[0], "/")
if len(headInfosSplit) == 1 {
- ci.HeadUser, err = user_model.GetUserByName(headInfos[0])
+ ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound("GetUserByName", nil)
@@ -259,7 +269,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
ci.HeadRepo = baseRepo
}
} else {
- ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(headInfosSplit[0], headInfosSplit[1])
+ ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1])
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.NotFound("GetRepositoryByOwnerAndName", nil)
@@ -329,7 +339,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
// forked from
var rootRepo *repo_model.Repository
if baseRepo.IsFork {
- err = baseRepo.GetBaseRepo()
+ err = baseRepo.GetBaseRepo(ctx)
if err != nil {
if !repo_model.IsErrRepoNotExist(err) {
ctx.ServerError("Unable to find root repo", err)
@@ -398,11 +408,12 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
}
ctx.Data["HeadRepo"] = ci.HeadRepo
+ ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository
// Now we need to assert that the ctx.Doer has permission to read
// the baseRepo's code and pulls
// (NOT headRepo's)
- permBase, err := models.GetUserRepoPermission(baseRepo, ctx.Doer)
+ permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return nil
@@ -421,7 +432,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
// If we're not merging from the same repo:
if !isSameRepo {
// Assert ctx.Doer has permission to read headRepo's codes
- permHead, err := models.GetUserRepoPermission(ci.HeadRepo, ctx.Doer)
+ permHead, err := access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return nil
@@ -436,6 +447,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
ctx.NotFound("ParseCompareInfo", nil)
return nil
}
+ ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode)
}
// If we have a rootRepo and it's different from:
@@ -445,7 +457,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
if rootRepo != nil &&
rootRepo.ID != ci.HeadRepo.ID &&
rootRepo.ID != baseRepo.ID {
- canRead := models.CheckRepoUnitUser(rootRepo, ctx.Doer, unit.TypeCode)
+ canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode)
if canRead {
ctx.Data["RootRepo"] = rootRepo
if !fileOnly {
@@ -470,7 +482,7 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo {
ownForkRepo.ID != ci.HeadRepo.ID &&
ownForkRepo.ID != baseRepo.ID &&
(rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
- canRead := models.CheckRepoUnitUser(ownForkRepo, ctx.Doer, unit.TypeCode)
+ canRead := access_model.CheckRepoUnitUser(ctx, ownForkRepo, ctx.Doer, unit.TypeCode)
if canRead {
ctx.Data["OwnForkRepo"] = ownForkRepo
if !fileOnly {
@@ -547,7 +559,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
@@ -565,7 +577,7 @@ func PrepareCompareDiff(
if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) ||
headCommitID == ci.CompareInfo.BaseCommitID {
ctx.Data["IsNothingToCompare"] = true
- if unit, err := repo.GetUnit(unit.TypePullRequests); err == nil {
+ if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil {
config := unit.PullRequestsConfig()
if !config.AutodetectManualMerge {
@@ -624,7 +636,7 @@ func PrepareCompareDiff(
return false
}
- commits := models.ConvertFromGitCommit(ci.CompareInfo.Commits, ci.HeadRepo)
+ commits := git_model.ConvertFromGitCommit(ctx, ci.CompareInfo.Commits, ci.HeadRepo)
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits)
@@ -734,15 +746,15 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["HeadTags"] = headTags
if ctx.Data["PageIsComparePull"] == true {
- pr, err := models.GetUnmergedPullRequest(ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, models.PullRequestFlowGithub)
+ pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
if err != nil {
- if !models.IsErrPullRequestNotExist(err) {
+ if !issues_model.IsErrPullRequestNotExist(err) {
ctx.ServerError("GetUnmergedPullRequest", err)
return
}
} else {
ctx.Data["HasPullRequest"] = true
- if err := pr.LoadIssue(); err != nil {
+ if err := pr.LoadIssue(ctx); err != nil {
ctx.ServerError("LoadIssue", err)
return
}
@@ -771,7 +783,24 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["IsRepoToolbarCommits"] = true
ctx.Data["IsDiffCompare"] = true
ctx.Data["RequireTribute"] = true
- setTemplateIfExists(ctx, pullRequestTemplateKey, nil, pullRequestTemplateCandidates)
+ templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
+
+ if len(templateErrs) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
+ }
+
+ // If a template content is set, prepend the "content". In this case that's only
+ // applicable if you have one commit to compare and that commit has a message.
+ // In that case the commit message will be prepend to the template body.
+ if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" {
+ if content, ok := ctx.Data["content"].(string); ok && content != "" {
+ // Re-use the same key as that's priortized over the "content" key.
+ // Add two new lines between the content to ensure there's always at least
+ // one empty line between them.
+ ctx.Data[pullRequestTemplateKey] = content + "\n\n" + templateContent
+ }
+ }
+
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
@@ -863,7 +892,7 @@ func ExcerptBlob(ctx *context.Context) {
}
}
ctx.Data["section"] = section
- ctx.Data["fileName"] = filePath
+ ctx.Data["FileNameHash"] = base.EncodeSha1(filePath)
ctx.Data["AfterCommitID"] = commitID
ctx.Data["Anchor"] = anchor
ctx.HTML(http.StatusOK, tplBlobExcerpt)
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index 72d34cb937939..9e95f9dd0c238 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -1,12 +1,14 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
import (
- "code.gitea.io/gitea/models"
+ "path"
+ "time"
+
+ git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
@@ -18,8 +20,8 @@ import (
)
// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
-func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
- if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
+func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified time.Time) error {
+ if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return nil
}
@@ -39,13 +41,13 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
pointer, _ := lfs.ReadPointer(dataRc)
if pointer.IsValid() {
- meta, _ := models.GetLFSMetaObjectByOid(ctx.Repo.Repository.ID, pointer.Oid)
+ meta, _ := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
if meta == nil {
if err = dataRc.Close(); err != nil {
log.Error("ServeBlobOrLFS: Close: %v", err)
}
closed = true
- return common.ServeBlob(ctx, blob)
+ return common.ServeBlob(ctx, blob, lastModified)
}
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
return nil
@@ -76,37 +78,60 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
}
closed = true
- return common.ServeBlob(ctx, blob)
+ return common.ServeBlob(ctx, blob, lastModified)
}
-// SingleDownload download a file by repos path
-func SingleDownload(ctx *context.Context) {
- blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
+func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified time.Time) {
+ entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
if git.IsErrNotExist(err) {
- ctx.NotFound("GetBlobByPath", nil)
+ ctx.NotFound("GetTreeEntryByPath", err)
} else {
- ctx.ServerError("GetBlobByPath", err)
+ ctx.ServerError("GetTreeEntryByPath", err)
}
return
}
- if err = common.ServeBlob(ctx, blob); err != nil {
+
+ if entry.IsDir() || entry.IsSubModule() {
+ ctx.NotFound("getBlobForEntry", nil)
+ return
+ }
+
+ info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:])
+ if err != nil {
+ ctx.ServerError("GetCommitsInfo", err)
+ return
+ }
+
+ if len(info) == 1 {
+ // Not Modified
+ lastModified = info[0].Commit.Committer.When
+ }
+ blob = entry.Blob()
+
+ return blob, lastModified
+}
+
+// SingleDownload download a file by repos path
+func SingleDownload(ctx *context.Context) {
+ blob, lastModified := getBlobForEntry(ctx)
+ if blob == nil {
+ return
+ }
+
+ if err := common.ServeBlob(ctx, blob, lastModified); err != nil {
ctx.ServerError("ServeBlob", err)
}
}
// SingleDownloadOrLFS download a file by repos path redirecting to LFS if necessary
func SingleDownloadOrLFS(ctx *context.Context) {
- blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
- if err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound("GetBlobByPath", nil)
- } else {
- ctx.ServerError("GetBlobByPath", err)
- }
+ blob, lastModified := getBlobForEntry(ctx)
+ if blob == nil {
return
}
- if err = ServeBlobOrLFS(ctx, blob); err != nil {
+
+ if err := ServeBlobOrLFS(ctx, blob, lastModified); err != nil {
ctx.ServerError("ServeBlobOrLFS", err)
}
}
@@ -122,7 +147,7 @@ func DownloadByID(ctx *context.Context) {
}
return
}
- if err = common.ServeBlob(ctx, blob); err != nil {
+ if err = common.ServeBlob(ctx, blob, time.Time{}); err != nil {
ctx.ServerError("ServeBlob", err)
}
}
@@ -138,7 +163,7 @@ func DownloadByIDOrLFS(ctx *context.Context) {
}
return
}
- if err = ServeBlobOrLFS(ctx, blob); err != nil {
+ if err = ServeBlobOrLFS(ctx, blob, time.Time{}); err != nil {
ctx.ServerError("ServeBlob", err)
}
}
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index c10162c7595d8..e5ba4ad2c1398 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -1,6 +1,5 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
@@ -12,6 +11,8 @@ import (
"strings"
"code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
@@ -67,7 +68,6 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
func editFile(ctx *context.Context, isNewFile bool) {
ctx.Data["PageIsEdit"] = true
ctx.Data["IsNewFile"] = isNewFile
- ctx.Data["RequireHighlightJS"] = true
canCommit := renderCommitRights(ctx)
treePath := cleanUploadFileName(ctx.Repo.TreePath)
@@ -197,7 +197,6 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
ctx.Data["PageIsEdit"] = true
ctx.Data["PageHasPosted"] = true
ctx.Data["IsNewFile"] = isNewFile
- ctx.Data["RequireHighlightJS"] = true
ctx.Data["TreePath"] = form.TreePath
ctx.Data["TreeNames"] = treeNames
ctx.Data["TreePaths"] = treePaths
@@ -255,9 +254,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
// This is where we handle all the errors thrown by files_service.CreateOrUpdateRepoFile
if git.IsErrNotExist(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
- } else if models.IsErrLFSFileLocked(err) {
+ } else if git_model.IsErrLFSFileLocked(err) {
ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(models.ErrLFSFileLocked).Path, err.(models.ErrLFSFileLocked).UserName), tplEditFile, &form)
+ ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form)
} else if models.IsErrFilenameInvalid(err) {
ctx.Data["Err_TreePath"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
@@ -329,7 +328,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
}
}
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(unit.TypePullRequests) {
+ if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
} else {
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
@@ -515,7 +514,7 @@ func DeleteFilePost(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(unit.TypePullRequests) {
+ if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
} else {
treePath := path.Dir(ctx.Repo.TreePath)
@@ -662,9 +661,9 @@ func UploadFilePost(ctx *context.Context) {
Files: form.Files,
Signoff: form.Signoff,
}); err != nil {
- if models.IsErrLFSFileLocked(err) {
+ if git_model.IsErrLFSFileLocked(err) {
ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(models.ErrLFSFileLocked).Path, err.(models.ErrLFSFileLocked).UserName), tplUploadFile, &form)
+ ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form)
} else if models.IsErrFilenameInvalid(err) {
ctx.Data["Err_TreePath"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form)
@@ -718,7 +717,7 @@ func UploadFilePost(ctx *context.Context) {
return
}
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(unit.TypePullRequests) {
+ if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
} else {
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
@@ -764,7 +763,7 @@ func UploadFileToServer(ctx *context.Context) {
return
}
- upload, err := models.NewUpload(name, buf, file)
+ upload, err := repo_model.NewUpload(name, buf, file)
if err != nil {
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err))
return
@@ -784,7 +783,7 @@ func RemoveUploadFileFromServer(ctx *context.Context) {
return
}
- if err := models.DeleteUploadByUUID(form.File); err != nil {
+ if err := repo_model.DeleteUploadByUUID(form.File); err != nil {
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err))
return
}
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
index 2bebb6fd5227b..1e53aac9b0938 100644
--- a/routers/web/repo/editor_test.go
+++ b/routers/web/repo/editor_test.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -70,9 +69,7 @@ func TestGetClosestParentWithFiles(t *testing.T) {
gitRepo, _ := git.OpenRepository(git.DefaultContext, repo.RepoPath())
defer gitRepo.Close()
commit, _ := gitRepo.GetBranchCommit(branch)
- expectedTreePath := ""
-
- expectedTreePath = "" // Should return the root dir, empty string, since there are no subdirs in this repo
+ var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo
for _, deletedFile := range []string{
"dir1/dir2/dir3/file.txt",
"file.txt",
diff --git a/routers/web/repo/find.go b/routers/web/repo/find.go
new file mode 100644
index 0000000000000..daefe59c8f274
--- /dev/null
+++ b/routers/web/repo/find.go
@@ -0,0 +1,23 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+)
+
+const (
+ tplFindFiles base.TplName = "repo/find/files"
+)
+
+// FindFiles render the page to find repository files
+func FindFiles(ctx *context.Context) {
+ path := ctx.Params("*")
+ ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + path
+ ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + path
+ ctx.HTML(http.StatusOK, tplFindFiles)
+}
diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go
index 1306a54369455..b2a49e3e3a103 100644
--- a/routers/web/repo/http.go
+++ b/routers/web/repo/http.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -19,14 +18,15 @@ import (
"sync"
"time"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
@@ -181,7 +181,7 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
}
if repoExist {
- p, err := models.GetUserRepoPermission(repo, ctx.Doer)
+ p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
@@ -204,21 +204,21 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
}
environ = []string{
- models.EnvRepoUsername + "=" + username,
- models.EnvRepoName + "=" + reponame,
- models.EnvPusherName + "=" + ctx.Doer.Name,
- models.EnvPusherID + fmt.Sprintf("=%d", ctx.Doer.ID),
- models.EnvAppURL + "=" + setting.AppURL,
+ repo_module.EnvRepoUsername + "=" + username,
+ repo_module.EnvRepoName + "=" + reponame,
+ repo_module.EnvPusherName + "=" + ctx.Doer.Name,
+ repo_module.EnvPusherID + fmt.Sprintf("=%d", ctx.Doer.ID),
+ repo_module.EnvAppURL + "=" + setting.AppURL,
}
if !ctx.Doer.KeepEmailPrivate {
- environ = append(environ, models.EnvPusherEmail+"="+ctx.Doer.Email)
+ environ = append(environ, repo_module.EnvPusherEmail+"="+ctx.Doer.Email)
}
if isWiki {
- environ = append(environ, models.EnvRepoIsWiki+"=true")
+ environ = append(environ, repo_module.EnvRepoIsWiki+"=true")
} else {
- environ = append(environ, models.EnvRepoIsWiki+"=false")
+ environ = append(environ, repo_module.EnvRepoIsWiki+"=false")
}
}
@@ -258,7 +258,7 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
if isWiki {
// Ensure the wiki is enabled before we allow access to it
- if _, err := repo.GetUnit(unit.TypeWiki); err != nil {
+ if _, err := repo.GetUnit(ctx, unit.TypeWiki); err != nil {
if repo_model.IsErrUnitTypeNotExist(err) {
ctx.PlainText(http.StatusForbidden, "repository wiki is disabled")
return
@@ -269,7 +269,7 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
}
}
- environ = append(environ, models.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
+ environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
w := ctx.Resp
r := ctx.Req
@@ -397,7 +397,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)
}
@@ -470,14 +470,15 @@ 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,
- Env: append(os.Environ(), h.environ...),
- Stdout: h.w,
- Stdin: reqBody,
- Stderr: &stderr,
+ Dir: h.dir,
+ Env: append(os.Environ(), h.environ...),
+ Stdout: h.w,
+ Stdin: reqBody,
+ Stderr: &stderr,
+ UseContextTimeout: true,
}); err != nil {
if err.Error() != "signal: killed" {
log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.dir, err, stderr.String())
@@ -541,7 +542,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/http_test.go b/routers/web/repo/http_test.go
index 58ac1c07a129f..5ba8de3d63c36 100644
--- a/routers/web/repo/http_test.go
+++ b/routers/web/repo/http_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 3ca193a15e739..b081092c57fc2 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1,36 +1,39 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
import (
"bytes"
+ stdCtx "context"
"errors"
"fmt"
- "io"
"math/big"
"net/http"
"net/url"
- "path"
+ "sort"
"strconv"
"strings"
"time"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
+ access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project"
+ pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
+ issue_template "code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
@@ -41,11 +44,13 @@ import (
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
- comment_service "code.gitea.io/gitea/services/comments"
+ "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
+ repo_service "code.gitea.io/gitea/services/repository"
)
const (
@@ -65,11 +70,23 @@ const (
// IssueTemplateCandidates issue templates
var IssueTemplateCandidates = []string{
"ISSUE_TEMPLATE.md",
+ "ISSUE_TEMPLATE.yaml",
+ "ISSUE_TEMPLATE.yml",
"issue_template.md",
+ "issue_template.yaml",
+ "issue_template.yml",
".gitea/ISSUE_TEMPLATE.md",
+ ".gitea/ISSUE_TEMPLATE.yaml",
+ ".gitea/ISSUE_TEMPLATE.yml",
".gitea/issue_template.md",
+ ".gitea/issue_template.yaml",
+ ".gitea/issue_template.yml",
".github/ISSUE_TEMPLATE.md",
+ ".github/ISSUE_TEMPLATE.yaml",
+ ".github/ISSUE_TEMPLATE.yml",
".github/issue_template.md",
+ ".github/issue_template.yaml",
+ ".github/issue_template.yml",
}
// MustAllowUserComment checks to make sure if an issue is locked.
@@ -96,7 +113,7 @@ func MustEnableIssues(ctx *context.Context) {
return
}
- unit, err := ctx.Repo.Repository.GetUnit(unit.TypeExternalTracker)
+ unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
if err == nil {
ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
return
@@ -122,13 +139,13 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
viewType := ctx.FormString("type")
sortType := ctx.FormString("sort")
types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested"}
- if !util.IsStringInSlice(viewType, types, true) {
+ if !util.SliceContainsString(types, viewType, true) {
viewType = "all"
}
var (
assigneeID = ctx.FormInt64("assignee")
- posterID int64
+ posterID = ctx.FormInt64("poster")
mentionedID int64
reviewRequestedID int64
forceEmpty bool
@@ -178,11 +195,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
}
}
- var issueStats *models.IssueStats
+ var issueStats *issues_model.IssueStats
if forceEmpty {
- issueStats = &models.IssueStats{}
+ issueStats = &issues_model.IssueStats{}
} else {
- issueStats, err = models.GetIssueStats(&models.IssueStatsOptions{
+ issueStats, err = issues_model.GetIssueStats(&issues_model.IssueStatsOptions{
RepoID: repo.ID,
Labels: selectLabels,
MilestoneID: milestoneID,
@@ -223,16 +240,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
mileIDs = []int64{milestoneID}
}
- var issues []*models.Issue
+ var issues []*issues_model.Issue
if forceEmpty {
- issues = []*models.Issue{}
+ issues = []*issues_model.Issue{}
} else {
- issues, err = models.Issues(&models.IssuesOptions{
+ issues, err = issues_model.Issues(ctx, &issues_model.IssuesOptions{
ListOptions: db.ListOptions{
Page: pager.Paginater.Current(),
PageSize: setting.UI.IssuePagingNum,
},
- RepoIDs: []int64{repo.ID},
+ RepoID: repo.ID,
AssigneeID: assigneeID,
PosterID: posterID,
MentionedID: mentionedID,
@@ -251,8 +268,8 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
}
}
- issueList := models.IssueList(issues)
- approvalCounts, err := issueList.GetApprovalCounts()
+ issueList := issues_model.IssueList(issues)
+ approvalCounts, err := issueList.GetApprovalCounts(ctx)
if err != nil {
ctx.ServerError("ApprovalCounts", err)
return
@@ -269,35 +286,42 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
}
}
- commitStatus, err := pull_service.GetIssuesLastCommitStatus(ctx, issues)
+ commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
if err != nil {
- ctx.ServerError("GetIssuesLastCommitStatus", err)
+ ctx.ServerError("GetIssuesAllCommitStatus", err)
return
}
ctx.Data["Issues"] = issues
- ctx.Data["CommitStatus"] = commitStatus
+ ctx.Data["CommitLastStatus"] = lastStatus
+ ctx.Data["CommitStatuses"] = commitStatuses
// Get assignees.
- ctx.Data["Assignees"], err = models.GetRepoAssignees(repo)
+ ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, repo)
if err != nil {
ctx.ServerError("GetAssignees", err)
return
}
+ ctx.Data["Posters"], err = repo_model.GetIssuePosters(ctx, repo, isPullOption.IsTrue())
+ if err != nil {
+ ctx.ServerError("GetIssuePosters", err)
+ return
+ }
+
handleTeamMentions(ctx)
if ctx.Written() {
return
}
- labels, err := models.GetLabelsByRepoID(repo.ID, "", db.ListOptions{})
+ labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByRepoID", err)
return
}
if repo.Owner.IsOrganization() {
- orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
+ orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByOrgID", err)
return
@@ -324,11 +348,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
if !ok || len(counts) == 0 {
return 0
}
- reviewTyp := models.ReviewTypeApprove
+ reviewTyp := issues_model.ReviewTypeApprove
if typ == "reject" {
- reviewTyp = models.ReviewTypeReject
+ reviewTyp = issues_model.ReviewTypeReject
} else if typ == "waiting" {
- reviewTyp = models.ReviewTypeRequest
+ reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
if count.Type == reviewTyp {
@@ -339,7 +363,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
}
if ctx.Repo.CanWriteIssuesOrPulls(ctx.Params(":type") == "pulls") {
- projects, _, err := project_model.GetProjects(project_model.SearchOptions{
+ projects, _, err := project_model.GetProjects(ctx, project_model.SearchOptions{
RepoID: repo.ID,
Type: project_model.TypeRepository,
IsClosed: util.OptionalBoolOf(isShowClosed),
@@ -358,6 +382,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
ctx.Data["SortType"] = sortType
ctx.Data["MilestoneID"] = milestoneID
ctx.Data["AssigneeID"] = assigneeID
+ ctx.Data["PosterID"] = posterID
ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["Keyword"] = keyword
if isShowClosed {
@@ -373,6 +398,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
pager.AddParam(ctx, "labels", "SelectLabels")
pager.AddParam(ctx, "milestone", "MilestoneID")
pager.AddParam(ctx, "assignee", "AssigneeID")
+ pager.AddParam(ctx, "poster", "PosterID")
ctx.Data["Page"] = pager
}
@@ -437,7 +463,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
return
}
- ctx.Data["Assignees"], err = models.GetRepoAssignees(repo)
+ ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, repo)
if err != nil {
ctx.ServerError("GetAssignees", err)
return
@@ -449,7 +475,7 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *repo_model.R
func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
var err error
- ctx.Data["OpenProjects"], _, err = project_model.GetProjects(project_model.SearchOptions{
+ ctx.Data["OpenProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{
RepoID: repo.ID,
Page: -1,
IsClosed: util.OptionalBoolFalse,
@@ -460,7 +486,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
return
}
- ctx.Data["ClosedProjects"], _, err = project_model.GetProjects(project_model.SearchOptions{
+ ctx.Data["ClosedProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{
RepoID: repo.ID,
Page: -1,
IsClosed: util.OptionalBoolTrue,
@@ -477,24 +503,24 @@ type repoReviewerSelection struct {
IsTeam bool
Team *organization.Team
User *user_model.User
- Review *models.Review
+ Review *issues_model.Review
CanChange bool
Checked bool
ItemID int64
}
// RetrieveRepoReviewers find all reviewers of a repository
-func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *models.Issue, canChooseReviewer bool) {
+func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, canChooseReviewer bool) {
ctx.Data["CanChooseReviewer"] = canChooseReviewer
- originalAuthorReviews, err := models.GetReviewersFromOriginalAuthorsByIssueID(issue.ID)
+ originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(issue.ID)
if err != nil {
ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
return
}
ctx.Data["OriginalReviews"] = originalAuthorReviews
- reviews, err := models.GetReviewersByIssueID(issue.ID)
+ reviews, err := issues_model.GetReviewersByIssueID(issue.ID)
if err != nil {
ctx.ServerError("GetReviewersByIssueID", err)
return
@@ -518,13 +544,13 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
posterID = 0
}
- reviewers, err = models.GetReviewers(repo, ctx.Doer.ID, posterID)
+ reviewers, err = repo_model.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
if err != nil {
ctx.ServerError("GetReviewers", err)
return
}
- teamReviewers, err = models.GetReviewerTeams(repo)
+ teamReviewers, err = repo_service.GetReviewerTeams(repo)
if err != nil {
ctx.ServerError("GetReviewerTeams", err)
return
@@ -543,7 +569,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
for _, review := range reviews {
tmp := &repoReviewerSelection{
- Checked: review.Type == models.ReviewTypeRequest,
+ Checked: review.Type == issues_model.ReviewTypeRequest,
Review: review,
ItemID: review.ReviewerID,
}
@@ -555,10 +581,10 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
if ctx.Repo.IsAdmin() {
// Admin can dismiss or re-request any review requests
tmp.CanChange = true
- } else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == models.ReviewTypeRequest {
+ } else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
// A user can refuse review requests
tmp.CanChange = true
- } else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != models.ReviewTypeRequest &&
+ } else if (canChooseReviewer || (ctx.Doer != nil && ctx.Doer.ID == issue.PosterID)) && review.Type != issues_model.ReviewTypeRequest &&
ctx.Doer.ID != review.ReviewerID {
// The poster of the PR, a manager, or official reviewers can re-request review from other reviewers
tmp.CanChange = true
@@ -580,7 +606,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
for _, item := range pullReviews {
if item.Review.ReviewerID > 0 {
- if err = item.Review.LoadReviewer(); err != nil {
+ if err = item.Review.LoadReviewer(ctx); err != nil {
if user_model.IsErrUserNotExist(err) {
continue
}
@@ -589,7 +615,7 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
}
item.User = item.Review.Reviewer
} else if item.Review.ReviewerTeamID > 0 {
- if err = item.Review.LoadReviewerTeam(); err != nil {
+ if err = item.Review.LoadReviewerTeam(ctx); err != nil {
if organization.IsErrTeamNotExist(err) {
continue
}
@@ -664,19 +690,19 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
}
// RetrieveRepoMetas find all the meta information of a repository
-func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*models.Label {
+func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label {
if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
return nil
}
- labels, err := models.GetLabelsByRepoID(repo.ID, "", db.ListOptions{})
+ labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByRepoID", err)
return nil
}
ctx.Data["Labels"] = labels
if repo.Owner.IsOrganization() {
- orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
+ orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
return nil
}
@@ -708,83 +734,66 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull
return labels
}
-func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) {
- var bytes []byte
-
- if ctx.Repo.Commit == nil {
- var err error
- ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
- if err != nil {
- return "", false
- }
- }
-
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(filename)
- if err != nil {
- return "", false
- }
- if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
- return "", false
- }
- r, err := entry.Blob().DataAsync()
+func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) map[string]error {
+ commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
- return "", false
- }
- defer r.Close()
- bytes, err = io.ReadAll(r)
- if err != nil {
- return "", false
+ return nil
}
- return string(bytes), true
-}
-func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleDirs, possibleFiles []string) {
- templateCandidates := make([]string, 0, len(possibleFiles))
- if ctx.FormString("template") != "" {
- for _, dirName := range possibleDirs {
- templateCandidates = append(templateCandidates, path.Join(dirName, ctx.FormString("template")))
- }
+ templateCandidates := make([]string, 0, 1+len(possibleFiles))
+ if t := ctx.FormString("template"); t != "" {
+ templateCandidates = append(templateCandidates, t)
}
templateCandidates = append(templateCandidates, possibleFiles...) // Append files to the end because they should be fallback
+
+ templateErrs := map[string]error{}
for _, filename := range templateCandidates {
- templateContent, found := getFileContentFromDefaultBranch(ctx, filename)
- if found {
- var meta api.IssueTemplate
- templateBody, err := markdown.ExtractMetadata(templateContent, &meta)
- if err != nil {
- log.Debug("could not extract metadata from %s [%s]: %v", filename, ctx.Repo.Repository.FullName(), err)
- ctx.Data[ctxDataKey] = templateContent
- return
- }
- ctx.Data[issueTemplateTitleKey] = meta.Title
- ctx.Data[ctxDataKey] = templateBody
- labelIDs := make([]string, 0, len(meta.Labels))
- if repoLabels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
- ctx.Data["Labels"] = repoLabels
- if ctx.Repo.Owner.IsOrganization() {
- if orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
- ctx.Data["OrgLabels"] = orgLabels
- repoLabels = append(repoLabels, orgLabels...)
- }
+ if ok, _ := commit.HasFile(filename); !ok {
+ continue
+ }
+ template, err := issue_template.UnmarshalFromCommit(commit, filename)
+ if err != nil {
+ templateErrs[filename] = err
+ continue
+ }
+ ctx.Data[issueTemplateTitleKey] = template.Title
+ ctx.Data[ctxDataKey] = template.Content
+
+ if template.Type() == api.IssueTemplateTypeYaml {
+ ctx.Data["Fields"] = template.Fields
+ ctx.Data["TemplateFile"] = template.FileName
+ }
+ labelIDs := make([]string, 0, len(template.Labels))
+ if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
+ ctx.Data["Labels"] = repoLabels
+ if ctx.Repo.Owner.IsOrganization() {
+ if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
+ ctx.Data["OrgLabels"] = orgLabels
+ repoLabels = append(repoLabels, orgLabels...)
}
+ }
- for _, metaLabel := range meta.Labels {
- for _, repoLabel := range repoLabels {
- if strings.EqualFold(repoLabel.Name, metaLabel) {
- repoLabel.IsChecked = true
- labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
- break
- }
+ for _, metaLabel := range template.Labels {
+ for _, repoLabel := range repoLabels {
+ if strings.EqualFold(repoLabel.Name, metaLabel) {
+ repoLabel.IsChecked = true
+ labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
+ break
}
}
}
- ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
- ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
- ctx.Data["Reference"] = meta.Ref
- ctx.Data["RefEndName"] = git.RefEndName(meta.Ref)
- return
+
}
+ if !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/[
+ template.Ref = git.BranchPrefix + template.Ref
+ }
+ ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
+ ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
+ ctx.Data["Reference"] = template.Ref
+ ctx.Data["RefEndName"] = git.RefEndName(template.Ref)
+ return templateErrs
}
+ return templateErrs
}
// NewIssue render creating issue page
@@ -792,7 +801,6 @@ func NewIssue(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
- ctx.Data["RequireHighlightJS"] = true
ctx.Data["RequireTribute"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
title := ctx.FormString("title")
@@ -800,7 +808,8 @@ func NewIssue(ctx *context.Context) {
body := ctx.FormString("body")
ctx.Data["BodyQuery"] = body
- ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
+ isProjectsEnabled := ctx.Repo.CanRead(unit.TypeProjects)
+ ctx.Data["IsProjectsEnabled"] = isProjectsEnabled
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
@@ -816,8 +825,8 @@ func NewIssue(ctx *context.Context) {
}
projectID := ctx.FormInt64("project")
- if projectID > 0 {
- project, err := project_model.GetProjectByID(projectID)
+ if projectID > 0 && isProjectsEnabled {
+ project, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
log.Error("GetProjectByID: %d: %v", projectID, err)
} else if project.RepoID != ctx.Repo.Repository.ID {
@@ -833,24 +842,62 @@ func NewIssue(ctx *context.Context) {
}
RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
- setTemplateIfExists(ctx, issueTemplateKey, context.IssueTemplateDirCandidates, IssueTemplateCandidates)
+
+ _, templateErrs := ctx.IssueTemplatesErrorsFromDefaultBranch()
+ if errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates); len(errs) > 0 {
+ for k, v := range errs {
+ templateErrs[k] = v
+ }
+ }
if ctx.Written() {
return
}
+ if len(templateErrs) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
+ }
+
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypeIssues)
ctx.HTML(http.StatusOK, tplIssueNew)
}
+func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string {
+ var files []string
+ for k := range errs {
+ files = append(files, k)
+ }
+ sort.Strings(files) // keep the output stable
+
+ var lines []string
+ for _, file := range files {
+ lines = append(lines, fmt.Sprintf("%s: %v", file, errs[file]))
+ }
+
+ flashError, err := ctx.RenderToString(tplAlertDetails, map[string]interface{}{
+ "Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"),
+ "Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)),
+ "Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")),
+ })
+ if err != nil {
+ log.Debug("render flash error: %v", err)
+ flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates")
+ }
+ return flashError
+}
+
// NewIssueChooseTemplate render creating issue from template page
func NewIssueChooseTemplate(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true
- issueTemplates := ctx.IssueTemplatesFromDefaultBranch()
+ issueTemplates, errs := ctx.IssueTemplatesErrorsFromDefaultBranch()
ctx.Data["IssueTemplates"] = issueTemplates
+ if len(errs) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, errs), true)
+ }
+
if len(issueTemplates) == 0 {
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters.
ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.HTMLURL(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
@@ -875,6 +922,11 @@ func DeleteIssue(ctx *context.Context) {
return
}
+ if issue.IsPull {
+ ctx.Redirect(fmt.Sprintf("%s/pulls", ctx.Repo.Repository.HTMLURL()), http.StatusSeeOther)
+ return
+ }
+
ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.HTMLURL()), http.StatusSeeOther)
}
@@ -898,10 +950,11 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
if err != nil {
return nil, nil, 0, 0
}
- labelIDMark := base.Int64sToMap(labelIDs)
+ labelIDMark := make(container.Set[int64])
+ labelIDMark.AddMultiple(labelIDs...)
for i := range labels {
- if labelIDMark[labels[i].ID] {
+ if labelIDMark.Contains(labels[i].ID) {
labels[i].IsChecked = true
hasSelected = true
}
@@ -929,7 +982,7 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
}
if form.ProjectID > 0 {
- p, err := project_model.GetProjectByID(form.ProjectID)
+ p, err := project_model.GetProjectByID(ctx, form.ProjectID)
if err != nil {
ctx.ServerError("GetProjectByID", err)
return nil, nil, 0, 0
@@ -953,20 +1006,20 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
// Check if the passed assignees actually exists and is assignable
for _, aID := range assigneeIDs {
- assignee, err := user_model.GetUserByID(aID)
+ assignee, err := user_model.GetUserByID(ctx, aID)
if err != nil {
ctx.ServerError("GetUserByID", err)
return nil, nil, 0, 0
}
- valid, err := models.CanBeAssigned(assignee, repo, isPull)
+ valid, err := access_model.CanBeAssigned(ctx, assignee, repo, isPull)
if err != nil {
ctx.ServerError("CanBeAssigned", err)
return nil, nil, 0, 0
}
if !valid {
- ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
+ ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
return nil, nil, 0, 0
}
}
@@ -986,7 +1039,6 @@ func NewIssuePost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true
ctx.Data["NewIssueChooseTemplate"] = len(ctx.IssueTemplatesFromDefaultBranch()) > 0
- ctx.Data["RequireHighlightJS"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
@@ -1015,19 +1067,26 @@ func NewIssuePost(ctx *context.Context) {
return
}
- issue := &models.Issue{
+ content := form.Content
+ if filename := ctx.Req.Form.Get("template-file"); filename != "" {
+ if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {
+ content = issue_template.RenderToMarkdown(template, ctx.Req.Form)
+ }
+ }
+
+ issue := &issues_model.Issue{
RepoID: repo.ID,
Repo: repo,
Title: form.Title,
PosterID: ctx.Doer.ID,
Poster: ctx.Doer,
MilestoneID: milestoneID,
- Content: form.Content,
+ Content: content,
Ref: form.Ref,
}
if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
- if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+ if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
return
}
@@ -1036,7 +1095,12 @@ func NewIssuePost(ctx *context.Context) {
}
if projectID > 0 {
- if err := models.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
+ if !ctx.Repo.CanRead(unit.TypeProjects) {
+ // User must also be able to see the project.
+ ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
+ return
+ }
+ if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
ctx.ServerError("ChangeProjectAssign", err)
return
}
@@ -1051,29 +1115,29 @@ func NewIssuePost(ctx *context.Context) {
}
// roleDescriptor returns the Role Descriptor for a comment in/with the given repo, poster and issue
-func roleDescriptor(repo *repo_model.Repository, poster *user_model.User, issue *models.Issue) (models.RoleDescriptor, error) {
- perm, err := models.GetUserRepoPermission(repo, poster)
+func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue) (issues_model.RoleDescriptor, error) {
+ perm, err := access_model.GetUserRepoPermission(ctx, repo, poster)
if err != nil {
- return models.RoleDescriptorNone, err
+ return issues_model.RoleDescriptorNone, err
}
// By default the poster has no roles on the comment.
- roleDescriptor := models.RoleDescriptorNone
+ roleDescriptor := issues_model.RoleDescriptorNone
// Check if the poster is owner of the repo.
if perm.IsOwner() {
// If the poster isn't a admin, enable the owner role.
if !poster.IsAdmin {
- roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorOwner)
+ roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner)
} else {
// Otherwise check if poster is the real repo admin.
- ok, err := models.IsUserRealRepoAdmin(repo, poster)
+ ok, err := access_model.IsUserRealRepoAdmin(repo, poster)
if err != nil {
- return models.RoleDescriptorNone, err
+ return issues_model.RoleDescriptorNone, err
}
if ok {
- roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorOwner)
+ roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner)
}
}
}
@@ -1081,18 +1145,18 @@ func roleDescriptor(repo *repo_model.Repository, poster *user_model.User, issue
// Is the poster can write issues or pulls to the repo, enable the Writer role.
// Only enable this if the poster doesn't have the owner role already.
if !roleDescriptor.HasRole("Owner") && perm.CanWriteIssuesOrPulls(issue.IsPull) {
- roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorWriter)
+ roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorWriter)
}
// If the poster is the actual poster of the issue, enable Poster role.
if issue.IsPoster(poster.ID) {
- roleDescriptor = roleDescriptor.WithRole(models.RoleDescriptorPoster)
+ roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorPoster)
}
return roleDescriptor, nil
}
-func getBranchData(ctx *context.Context, issue *models.Issue) {
+func getBranchData(ctx *context.Context, issue *issues_model.Issue) {
ctx.Data["BaseBranch"] = nil
ctx.Data["HeadBranch"] = nil
ctx.Data["HeadUserName"] = nil
@@ -1101,7 +1165,7 @@ func getBranchData(ctx *context.Context, issue *models.Issue) {
pull := issue.PullRequest
ctx.Data["BaseBranch"] = pull.BaseBranch
ctx.Data["HeadBranch"] = pull.HeadBranch
- ctx.Data["HeadUserName"] = pull.MustHeadUserName()
+ ctx.Data["HeadUserName"] = pull.MustHeadUserName(ctx)
}
}
@@ -1109,7 +1173,7 @@ func getBranchData(ctx *context.Context, issue *models.Issue) {
func ViewIssue(ctx *context.Context) {
if ctx.Params(":type") == "issues" {
// If issue was requested we check if repo has external tracker and redirect
- extIssueUnit, err := ctx.Repo.Repository.GetUnit(unit.TypeExternalTracker)
+ extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
if err == nil && extIssueUnit != nil {
if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
metas := ctx.Repo.Repository.ComposeMetas()
@@ -1129,9 +1193,9 @@ func ViewIssue(ctx *context.Context) {
}
}
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound("GetIssueByIndex", err)
} else {
ctx.ServerError("GetIssueByIndex", err)
@@ -1175,13 +1239,12 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["IssueType"] = "all"
}
- ctx.Data["RequireHighlightJS"] = true
ctx.Data["RequireTribute"] = true
ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
- if err = issue.LoadAttributes(); err != nil {
+ if err = issue.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
@@ -1193,11 +1256,11 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
- iw := new(models.IssueWatch)
+ iw := new(issues_model.IssueWatch)
if ctx.Doer != nil {
iw.UserID = ctx.Doer.ID
iw.IssueID = issue.ID
- iw.IsWatching, err = models.CheckIssueWatch(ctx.Doer, issue)
+ iw.IsWatching, err = issues_model.CheckIssueWatch(ctx.Doer, issue)
if err != nil {
ctx.ServerError("CheckIssueWatch", err)
return
@@ -1234,11 +1297,11 @@ func ViewIssue(ctx *context.Context) {
// Metas.
// Check labels.
- labelIDMark := make(map[int64]bool)
- for i := range issue.Labels {
- labelIDMark[issue.Labels[i].ID] = true
+ labelIDMark := make(container.Set[int64])
+ for _, label := range issue.Labels {
+ labelIDMark.Add(label.ID)
}
- labels, err := models.GetLabelsByRepoID(repo.ID, "", db.ListOptions{})
+ labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByRepoID", err)
return
@@ -1246,7 +1309,7 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["Labels"] = labels
if repo.Owner.IsOrganization() {
- orgLabels, err := models.GetLabelsByOrgID(repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
+ orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByOrgID", err)
return
@@ -1258,7 +1321,7 @@ func ViewIssue(ctx *context.Context) {
hasSelected := false
for i := range labels {
- if labelIDMark[labels[i].ID] {
+ if labelIDMark.Contains(labels[i].ID) {
labels[i].IsChecked = true
hasSelected = true
}
@@ -1277,11 +1340,16 @@ func ViewIssue(ctx *context.Context) {
if issue.IsPull {
canChooseReviewer := ctx.Repo.CanWrite(unit.TypePullRequests)
- if !canChooseReviewer && ctx.Doer != nil && ctx.IsSigned {
- canChooseReviewer, err = models.IsOfficialReviewer(issue, ctx.Doer)
- if err != nil {
- ctx.ServerError("IsOfficialReviewer", err)
- return
+ if ctx.Doer != nil && ctx.IsSigned {
+ if !canChooseReviewer {
+ canChooseReviewer = ctx.Doer.ID == issue.PosterID
+ }
+ if !canChooseReviewer {
+ canChooseReviewer, err = issues_model.IsOfficialReviewer(ctx, issue, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("IsOfficialReviewer", err)
+ return
+ }
}
}
@@ -1293,35 +1361,35 @@ func ViewIssue(ctx *context.Context) {
if ctx.IsSigned {
// Update issue-user.
- if err = issue.ReadBy(ctx.Doer.ID); err != nil {
+ if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
ctx.ServerError("ReadBy", err)
return
}
}
var (
- role models.RoleDescriptor
+ role issues_model.RoleDescriptor
ok bool
- marked = make(map[int64]models.RoleDescriptor)
- comment *models.Comment
+ marked = make(map[int64]issues_model.RoleDescriptor)
+ comment *issues_model.Comment
participants = make([]*user_model.User, 1, 10)
)
- if ctx.Repo.Repository.IsTimetrackerEnabled() {
+ if ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
if ctx.IsSigned {
// Deal with the stopwatch
- ctx.Data["IsStopwatchRunning"] = models.StopwatchExists(ctx.Doer.ID, issue.ID)
+ ctx.Data["IsStopwatchRunning"] = issues_model.StopwatchExists(ctx.Doer.ID, issue.ID)
if !ctx.Data["IsStopwatchRunning"].(bool) {
var exists bool
- var sw *models.Stopwatch
- if exists, sw, err = models.HasUserStopwatch(ctx.Doer.ID); err != nil {
+ var sw *issues_model.Stopwatch
+ if exists, sw, err = issues_model.HasUserStopwatch(ctx, ctx.Doer.ID); err != nil {
ctx.ServerError("HasUserStopwatch", err)
return
}
ctx.Data["HasUserStopwatch"] = exists
if exists {
// Add warning if the user has already a stopwatch
- var otherIssue *models.Issue
- if otherIssue, err = models.GetIssueByID(sw.IssueID); err != nil {
+ var otherIssue *issues_model.Issue
+ if otherIssue, err = issues_model.GetIssueByID(ctx, sw.IssueID); err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
@@ -1337,7 +1405,7 @@ func ViewIssue(ctx *context.Context) {
} else {
ctx.Data["CanUseTimetracker"] = false
}
- if ctx.Data["WorkingUsers"], err = models.TotalTimes(&models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
+ if ctx.Data["WorkingUsers"], err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
ctx.ServerError("TotalTimes", err)
return
}
@@ -1349,7 +1417,7 @@ func ViewIssue(ctx *context.Context) {
// check if dependencies can be created across repositories
ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
- if issue.ShowRole, err = roleDescriptor(repo, issue.Poster, issue); err != nil {
+ if issue.ShowRole, err = roleDescriptor(ctx, repo, issue.Poster, issue); err != nil {
ctx.ServerError("roleDescriptor", err)
return
}
@@ -1360,13 +1428,13 @@ func ViewIssue(ctx *context.Context) {
for _, comment = range issue.Comments {
comment.Issue = issue
- if err := comment.LoadPoster(); err != nil {
+ if err := comment.LoadPoster(ctx); err != nil {
ctx.ServerError("LoadPoster", err)
return
}
- if comment.Type == models.CommentTypeComment || comment.Type == models.CommentTypeReview {
- if err := comment.LoadAttachments(); err != nil {
+ if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
+ if err := comment.LoadAttachments(ctx); err != nil {
ctx.ServerError("LoadAttachments", err)
return
}
@@ -1388,20 +1456,20 @@ func ViewIssue(ctx *context.Context) {
continue
}
- comment.ShowRole, err = roleDescriptor(repo, comment.Poster, issue)
+ comment.ShowRole, err = roleDescriptor(ctx, repo, comment.Poster, issue)
if err != nil {
ctx.ServerError("roleDescriptor", err)
return
}
marked[comment.PosterID] = comment.ShowRole
participants = addParticipant(comment.Poster, participants)
- } else if comment.Type == models.CommentTypeLabel {
+ } else if comment.Type == issues_model.CommentTypeLabel {
if err = comment.LoadLabel(); err != nil {
ctx.ServerError("LoadLabel", err)
return
}
- } else if comment.Type == models.CommentTypeMilestone {
- if err = comment.LoadMilestone(); err != nil {
+ } else if comment.Type == issues_model.CommentTypeMilestone {
+ if err = comment.LoadMilestone(ctx); err != nil {
ctx.ServerError("LoadMilestone", err)
return
}
@@ -1415,7 +1483,7 @@ func ViewIssue(ctx *context.Context) {
if comment.MilestoneID > 0 && comment.Milestone == nil {
comment.Milestone = ghostMilestone
}
- } else if comment.Type == models.CommentTypeProject {
+ } else if comment.Type == issues_model.CommentTypeProject {
if err = comment.LoadProject(); err != nil {
ctx.ServerError("LoadProject", err)
@@ -1435,19 +1503,19 @@ func ViewIssue(ctx *context.Context) {
comment.Project = ghostProject
}
- } else if comment.Type == models.CommentTypeAssignees || comment.Type == models.CommentTypeReviewRequest {
+ } else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest {
if err = comment.LoadAssigneeUserAndTeam(); err != nil {
ctx.ServerError("LoadAssigneeUserAndTeam", err)
return
}
- } else if comment.Type == models.CommentTypeRemoveDependency || comment.Type == models.CommentTypeAddDependency {
+ } else if comment.Type == issues_model.CommentTypeRemoveDependency || comment.Type == issues_model.CommentTypeAddDependency {
if err = comment.LoadDepIssueDetails(); err != nil {
- if !models.IsErrIssueNotExist(err) {
+ if !issues_model.IsErrIssueNotExist(err) {
ctx.ServerError("LoadDepIssueDetails", err)
return
}
}
- } else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview {
+ } else if comment.Type == issues_model.CommentTypeCode || comment.Type == issues_model.CommentTypeReview || comment.Type == issues_model.CommentTypeDismissReview {
comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: ctx.Repo.Repository.ComposeMetas(),
@@ -1458,7 +1526,7 @@ func ViewIssue(ctx *context.Context) {
ctx.ServerError("RenderString", err)
return
}
- if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) {
+ if err = comment.LoadReview(); err != nil && !issues_model.IsErrReviewNotExist(err) {
ctx.ServerError("LoadReview", err)
return
}
@@ -1487,7 +1555,7 @@ func ViewIssue(ctx *context.Context) {
continue
}
- c.ShowRole, err = roleDescriptor(repo, c.Poster, issue)
+ c.ShowRole, err = roleDescriptor(ctx, repo, c.Poster, issue)
if err != nil {
ctx.ServerError("roleDescriptor", err)
return
@@ -1501,14 +1569,14 @@ func ViewIssue(ctx *context.Context) {
ctx.ServerError("LoadResolveDoer", err)
return
}
- } else if comment.Type == models.CommentTypePullRequestPush {
+ } else if comment.Type == issues_model.CommentTypePullRequestPush {
participants = addParticipant(comment.Poster, participants)
if err = comment.LoadPushCommits(ctx); err != nil {
ctx.ServerError("LoadPushCommits", err)
return
}
- } else if comment.Type == models.CommentTypeAddTimeManual ||
- comment.Type == models.CommentTypeStopTracking {
+ } else if comment.Type == issues_model.CommentTypeAddTimeManual ||
+ comment.Type == issues_model.CommentTypeStopTracking {
// drop error since times could be pruned from DB..
_ = comment.LoadTime()
}
@@ -1525,89 +1593,112 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["AllowMerge"] = false
if ctx.IsSigned {
- if err := pull.LoadHeadRepo(); err != nil {
+ if err := pull.LoadHeadRepo(ctx); err != nil {
log.Error("LoadHeadRepo: %v", err)
- } else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch {
- perm, err := models.GetUserRepoPermission(pull.HeadRepo, ctx.Doer)
+ } else if pull.HeadRepo != nil {
+ perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
if perm.CanWrite(unit.TypeCode) {
// Check if branch is not protected
- if protected, err := models.IsProtectedBranch(pull.HeadRepo.ID, pull.HeadBranch); err != nil {
- log.Error("IsProtectedBranch: %v", err)
- } else if !protected {
- canDelete = true
- ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup"
+ if pull.HeadBranch != pull.HeadRepo.DefaultBranch {
+ if protected, err := git_model.IsBranchProtected(ctx, pull.HeadRepo.ID, pull.HeadBranch); err != nil {
+ log.Error("IsProtectedBranch: %v", err)
+ } else if !protected {
+ canDelete = true
+ ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup"
+ }
}
+ ctx.Data["CanWriteToHeadRepo"] = true
}
}
- if err := pull.LoadBaseRepo(); err != nil {
+ if err := pull.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
}
- perm, err := models.GetUserRepoPermission(pull.BaseRepo, ctx.Doer)
+ perm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
- ctx.Data["AllowMerge"], err = pull_service.IsUserAllowedToMerge(pull, perm, ctx.Doer)
+ ctx.Data["AllowMerge"], err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer)
if err != nil {
ctx.ServerError("IsUserAllowedToMerge", err)
return
}
- if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.Doer); err != nil {
+ if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(issue, ctx.Doer); err != nil {
ctx.ServerError("CanMarkConversation", err)
return
}
}
- prUnit, err := repo.GetUnit(unit.TypePullRequests)
+ prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
ctx.ServerError("GetUnit", err)
return
}
prConfig := prUnit.PullRequestsConfig()
+ var mergeStyle repo_model.MergeStyle
// Check correct values and select default
if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok ||
!prConfig.IsMergeStyleAllowed(ms) {
defaultMergeStyle := prConfig.GetDefaultMergeStyle()
if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
- ctx.Data["MergeStyle"] = defaultMergeStyle
+ mergeStyle = defaultMergeStyle
} else if prConfig.AllowMerge {
- ctx.Data["MergeStyle"] = repo_model.MergeStyleMerge
+ mergeStyle = repo_model.MergeStyleMerge
} else if prConfig.AllowRebase {
- ctx.Data["MergeStyle"] = repo_model.MergeStyleRebase
+ mergeStyle = repo_model.MergeStyleRebase
} else if prConfig.AllowRebaseMerge {
- ctx.Data["MergeStyle"] = repo_model.MergeStyleRebaseMerge
+ mergeStyle = repo_model.MergeStyleRebaseMerge
} else if prConfig.AllowSquash {
- ctx.Data["MergeStyle"] = repo_model.MergeStyleSquash
+ mergeStyle = repo_model.MergeStyleSquash
} else if prConfig.AllowManualMerge {
- ctx.Data["MergeStyle"] = repo_model.MergeStyleManuallyMerged
- } else {
- ctx.Data["MergeStyle"] = ""
+ mergeStyle = repo_model.MergeStyleManuallyMerged
}
}
- if err = pull.LoadProtectedBranch(); err != nil {
+
+ ctx.Data["MergeStyle"] = mergeStyle
+
+ defaultMergeMessage, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle)
+ if err != nil {
+ ctx.ServerError("GetDefaultMergeMessage", err)
+ return
+ }
+ ctx.Data["DefaultMergeMessage"] = defaultMergeMessage
+ ctx.Data["DefaultMergeBody"] = defaultMergeBody
+
+ defaultSquashMergeMessage, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash)
+ if err != nil {
+ ctx.ServerError("GetDefaultSquashMergeMessage", err)
+ return
+ }
+ ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage
+ ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody
+
+ pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
+ if err != nil {
ctx.ServerError("LoadProtectedBranch", err)
return
}
ctx.Data["ShowMergeInstructions"] = true
- if pull.ProtectedBranch != nil {
+ if pb != nil {
+ pb.Repo = pull.BaseRepo
var showMergeInstructions bool
if ctx.Doer != nil {
- showMergeInstructions = pull.ProtectedBranch.CanUserPush(ctx.Doer.ID)
- }
- cnt := pull.ProtectedBranch.GetGrantedApprovalsCount(pull)
- ctx.Data["IsBlockedByApprovals"] = !pull.ProtectedBranch.HasEnoughApprovals(pull)
- ctx.Data["IsBlockedByRejection"] = pull.ProtectedBranch.MergeBlockedByRejectedReview(pull)
- ctx.Data["IsBlockedByOfficialReviewRequests"] = pull.ProtectedBranch.MergeBlockedByOfficialReviewRequests(pull)
- ctx.Data["IsBlockedByOutdatedBranch"] = pull.ProtectedBranch.MergeBlockedByOutdatedBranch(pull)
- ctx.Data["GrantedApprovals"] = cnt
- ctx.Data["RequireSigned"] = pull.ProtectedBranch.RequireSignedCommits
+ showMergeInstructions = pb.CanUserPush(ctx, ctx.Doer)
+ }
+ ctx.Data["ProtectedBranch"] = pb
+ ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull)
+ ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull)
+ ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull)
+ ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pb, pull)
+ ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull)
+ ctx.Data["RequireSigned"] = pb.RequireSignedCommits
ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
@@ -1636,7 +1727,7 @@ func ViewIssue(ctx *context.Context) {
(!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
if isPullBranchDeletable && pull.HasMerged {
- exist, err := models.HasUnmergedPullRequestsByHeadInfo(pull.HeadRepoID, pull.HeadBranch)
+ exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch)
if err != nil {
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
return
@@ -1661,15 +1752,22 @@ func ViewIssue(ctx *context.Context) {
}
ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
+
+ // Check if there is a pending pr merge
+ ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID)
+ if err != nil {
+ ctx.ServerError("GetScheduledMergeByPullID", err)
+ return
+ }
}
// Get Dependencies
- ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies()
+ ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies(ctx)
if err != nil {
ctx.ServerError("BlockedByDependencies", err)
return
}
- ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies()
+ ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies(ctx)
if err != nil {
ctx.ServerError("BlockingDependencies", err)
return
@@ -1696,7 +1794,7 @@ func ViewIssue(ctx *context.Context) {
}
hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here
}
- ctx.Data["ShouldShowCommentType"] = func(commentType models.CommentType) bool {
+ ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool {
return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0
}
@@ -1704,10 +1802,10 @@ func ViewIssue(ctx *context.Context) {
}
// GetActionIssue will return the issue which is used in the context.
-func GetActionIssue(ctx *context.Context) *models.Issue {
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+func GetActionIssue(ctx *context.Context) *issues_model.Issue {
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err)
+ ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
return nil
}
issue.Repo = ctx.Repo.Repository
@@ -1715,21 +1813,21 @@ func GetActionIssue(ctx *context.Context) *models.Issue {
if ctx.Written() {
return nil
}
- if err = issue.LoadAttributes(); err != nil {
+ if err = issue.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", nil)
return nil
}
return issue
}
-func checkIssueRights(ctx *context.Context, issue *models.Issue) {
+func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) {
if issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) ||
!issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
}
}
-func getActionIssues(ctx *context.Context) []*models.Issue {
+func getActionIssues(ctx *context.Context) []*issues_model.Issue {
commaSeparatedIssueIDs := ctx.FormString("issue_ids")
if len(commaSeparatedIssueIDs) == 0 {
return nil
@@ -1743,7 +1841,7 @@ func getActionIssues(ctx *context.Context) []*models.Issue {
}
issueIDs = append(issueIDs, issueID)
}
- issues, err := models.GetIssuesByIDs(issueIDs)
+ issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
if err != nil {
ctx.ServerError("GetIssuesByIDs", err)
return nil
@@ -1752,11 +1850,15 @@ func getActionIssues(ctx *context.Context) []*models.Issue {
issueUnitEnabled := ctx.Repo.CanRead(unit.TypeIssues)
prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests)
for _, issue := range issues {
+ if issue.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect"))
+ return nil
+ }
if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
return nil
}
- if err = issue.LoadAttributes(); err != nil {
+ if err = issue.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return nil
}
@@ -1766,16 +1868,31 @@ func getActionIssues(ctx *context.Context) []*models.Issue {
// GetIssueInfo get an issue of a repository
func GetIssueInfo(ctx *context.Context) {
- issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.Error(http.StatusNotFound)
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
}
return
}
- ctx.JSON(http.StatusOK, convert.ToAPIIssue(issue))
+
+ if issue.IsPull {
+ // Need to check if Pulls are enabled and we can read Pulls
+ if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ } else {
+ // Need to check if Issues are enabled and we can read Issues
+ if !ctx.Repo.CanRead(unit.TypeIssues) {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ }
+
+ ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
}
// UpdateIssueTitle change issue's title
@@ -1849,7 +1966,7 @@ func UpdateIssueContent(ctx *context.Context) {
// when update the request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
if !ctx.FormBool("ignore_attachments") {
- if err := updateAttachments(issue, ctx.FormStrings("files[]")); err != nil {
+ if err := updateAttachments(ctx, issue, ctx.FormStrings("files[]")); err != nil {
ctx.ServerError("UpdateAttachments", err)
return
}
@@ -1875,9 +1992,9 @@ func UpdateIssueContent(ctx *context.Context) {
// UpdateIssueDeadline updates an issue deadline
func UpdateIssueDeadline(ctx *context.Context) {
form := web.GetForm(ctx).(*api.EditDeadlineOption)
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound("GetIssueByIndex", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
@@ -1898,7 +2015,7 @@ func UpdateIssueDeadline(ctx *context.Context) {
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
}
- if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
+ if err := issues_model.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error())
return
}
@@ -1949,19 +2066,19 @@ func UpdateIssueAssignee(ctx *context.Context) {
return
}
default:
- assignee, err := user_model.GetUserByID(assigneeID)
+ assignee, err := user_model.GetUserByID(ctx, assigneeID)
if err != nil {
ctx.ServerError("GetUserByID", err)
return
}
- valid, err := models.CanBeAssigned(assignee, issue.Repo, issue.IsPull)
+ valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull)
if err != nil {
ctx.ServerError("canBeAssigned", err)
return
}
if !valid {
- ctx.ServerError("canBeAssigned", models.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
+ ctx.ServerError("canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name})
return
}
@@ -2023,7 +2140,7 @@ func UpdatePullReviewRequest(ctx *context.Context) {
return
}
- team, err := organization.GetTeamByID(-reviewID)
+ team, err := organization.GetTeamByID(ctx, -reviewID)
if err != nil {
ctx.ServerError("GetTeamByID", err)
return
@@ -2037,9 +2154,9 @@ func UpdatePullReviewRequest(ctx *context.Context) {
return
}
- err = issue_service.IsValidTeamReviewRequest(team, ctx.Doer, action == "attach", issue)
+ err = issue_service.IsValidTeamReviewRequest(ctx, team, ctx.Doer, action == "attach", issue)
if err != nil {
- if models.IsErrNotValidReviewRequest(err) {
+ if issues_model.IsErrNotValidReviewRequest(err) {
log.Warn(
"UpdatePullReviewRequest: refusing to add invalid team review request for UID[%d] team %s to %s#%d owned by UID[%d]: Error: %v",
team.OrgID, team.Name, issue.Repo.FullName(), issue.Index, issue.Repo.ID,
@@ -2060,7 +2177,7 @@ func UpdatePullReviewRequest(ctx *context.Context) {
continue
}
- reviewer, err := user_model.GetUserByID(reviewID)
+ reviewer, err := user_model.GetUserByID(ctx, reviewID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
log.Warn(
@@ -2075,9 +2192,9 @@ func UpdatePullReviewRequest(ctx *context.Context) {
return
}
- err = issue_service.IsValidReviewRequest(reviewer, ctx.Doer, action == "attach", issue, nil)
+ err = issue_service.IsValidReviewRequest(ctx, reviewer, ctx.Doer, action == "attach", issue, nil)
if err != nil {
- if models.IsErrNotValidReviewRequest(err) {
+ if issues_model.IsErrNotValidReviewRequest(err) {
log.Warn(
"UpdatePullReviewRequest: refusing to add invalid review request for %-v to %-v#%d: Error: %v",
reviewer, issue.Repo, issue.Index,
@@ -2121,7 +2238,7 @@ func SearchIssues(ctx *context.Context) {
}
// find repos user can access (for issue search)
- opts := &models.SearchRepoOptions{
+ opts := &repo_model.SearchRepoOptions{
Private: false,
AllPublic: true,
TopicOnly: false,
@@ -2136,7 +2253,7 @@ func SearchIssues(ctx *context.Context) {
opts.AllLimited = true
}
if ctx.FormString("owner") != "" {
- owner, err := user_model.GetUserByName(ctx.FormString("owner"))
+ owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusBadRequest, "Owner not found", err.Error())
@@ -2155,7 +2272,7 @@ func SearchIssues(ctx *context.Context) {
ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
return
}
- team, err := organization.GetTeam(opts.OwnerID, ctx.FormString("team"))
+ team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.Error(http.StatusBadRequest, "Team not found", err.Error())
@@ -2167,13 +2284,14 @@ func SearchIssues(ctx *context.Context) {
opts.TeamID = team.ID
}
- repoIDs, _, err := models.SearchRepositoryIDs(opts)
+ repoCond := repo_model.SearchRepositoryCondition(opts)
+ repoIDs, _, err := repo_model.SearchRepositoryIDs(opts)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err.Error())
+ ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
return
}
- var issues []*models.Issue
+ var issues []*issues_model.Issue
var filteredCount int64
keyword := ctx.FormTrim("q")
@@ -2222,12 +2340,12 @@ func SearchIssues(ctx *context.Context) {
// Only fetch the issues if we either don't have a keyword or the search returned issues
// This would otherwise return all issues if no issues were found by the search.
if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 {
- issuesOpt := &models.IssuesOptions{
+ issuesOpt := &issues_model.IssuesOptions{
ListOptions: db.ListOptions{
Page: ctx.FormInt("page"),
PageSize: limit,
},
- RepoIDs: repoIDs,
+ RepoCond: repoCond,
IsClosed: isClosed,
IssueIDs: issueIDs,
IncludedLabelNames: includedLabelNames,
@@ -2258,7 +2376,7 @@ func SearchIssues(ctx *context.Context) {
issuesOpt.ReviewRequestedID = ctxUserID
}
- if issues, err = models.Issues(issuesOpt); err != nil {
+ if issues, err = issues_model.Issues(ctx, issuesOpt); err != nil {
ctx.Error(http.StatusInternalServerError, "Issues", err.Error())
return
}
@@ -2266,14 +2384,14 @@ func SearchIssues(ctx *context.Context) {
issuesOpt.ListOptions = db.ListOptions{
Page: -1,
}
- if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+ if filteredCount, err = issues_model.CountIssues(ctx, issuesOpt); err != nil {
ctx.Error(http.StatusInternalServerError, "CountIssues", err.Error())
return
}
}
ctx.SetTotalCountHeader(filteredCount)
- ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
+ ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
}
func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
@@ -2282,7 +2400,7 @@ func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
return 0
}
- user, err := user_model.GetUserByName(userName)
+ user, err := user_model.GetUserByName(ctx, userName)
if user_model.IsErrUserNotExist(err) {
ctx.NotFound("", err)
return 0
@@ -2314,7 +2432,7 @@ func ListIssues(ctx *context.Context) {
isClosed = util.OptionalBoolFalse
}
- var issues []*models.Issue
+ var issues []*issues_model.Issue
var filteredCount int64
keyword := ctx.FormTrim("q")
@@ -2332,7 +2450,7 @@ func ListIssues(ctx *context.Context) {
}
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
- labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
+ labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
@@ -2401,9 +2519,9 @@ func ListIssues(ctx *context.Context) {
// Only fetch the issues if we either don't have a keyword or the search returned issues
// This would otherwise return all issues if no issues were found by the search.
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
- issuesOpt := &models.IssuesOptions{
+ issuesOpt := &issues_model.IssuesOptions{
ListOptions: listOptions,
- RepoIDs: []int64{ctx.Repo.Repository.ID},
+ RepoID: ctx.Repo.Repository.ID,
IsClosed: isClosed,
IssueIDs: issueIDs,
LabelIDs: labelIDs,
@@ -2416,7 +2534,7 @@ func ListIssues(ctx *context.Context) {
MentionedID: mentionedByID,
}
- if issues, err = models.Issues(issuesOpt); err != nil {
+ if issues, err = issues_model.Issues(ctx, issuesOpt); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
@@ -2424,14 +2542,14 @@ func ListIssues(ctx *context.Context) {
issuesOpt.ListOptions = db.ListOptions{
Page: -1,
}
- if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+ if filteredCount, err = issues_model.CountIssues(ctx, issuesOpt); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
}
ctx.SetTotalCountHeader(filteredCount)
- ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
+ ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
}
// UpdateIssueStatus change issue's status
@@ -2451,14 +2569,14 @@ func UpdateIssueStatus(ctx *context.Context) {
log.Warn("Unrecognized action: %s", action)
}
- if _, err := models.IssueList(issues).LoadRepositories(); err != nil {
+ if _, err := issues_model.IssueList(issues).LoadRepositories(ctx); err != nil {
ctx.ServerError("LoadRepositories", err)
return
}
for _, issue := range issues {
if issue.IsClosed != isClosed {
if err := issue_service.ChangeStatus(issue, ctx.Doer, isClosed); err != nil {
- if models.IsErrDependenciesLeft(err) {
+ if issues_model.IsErrDependenciesLeft(err) {
ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{
"error": "cannot close this issue because it still has open dependencies",
})
@@ -2522,7 +2640,7 @@ func NewComment(ctx *context.Context) {
return
}
- var comment *models.Comment
+ var comment *issues_model.Comment
defer func() {
// Check if issue admin/poster changes the status of issue.
if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) &&
@@ -2530,14 +2648,14 @@ func NewComment(ctx *context.Context) {
!(issue.IsPull && issue.PullRequest.HasMerged) {
// Duplication and conflict check should apply to reopen pull request.
- var pr *models.PullRequest
+ var pr *issues_model.PullRequest
if form.Status == "reopen" && issue.IsPull {
pull := issue.PullRequest
var err error
- pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
+ pr, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
if err != nil {
- if !models.IsErrPullRequestNotExist(err) {
+ if !issues_model.IsErrPullRequestNotExist(err) {
ctx.ServerError("GetUnmergedPullRequest", err)
return
}
@@ -2557,7 +2675,7 @@ func NewComment(ctx *context.Context) {
if err := issue_service.ChangeStatus(issue, ctx.Doer, isClosed); err != nil {
log.Error("ChangeStatus: %v", err)
- if models.IsErrDependenciesLeft(err) {
+ if issues_model.IsErrDependenciesLeft(err) {
if issue.IsPull {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index))
@@ -2595,7 +2713,7 @@ func NewComment(ctx *context.Context) {
return
}
- comment, err := comment_service.CreateIssueComment(ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
+ comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
if err != nil {
ctx.ServerError("CreateIssueComment", err)
return
@@ -2606,14 +2724,14 @@ func NewComment(ctx *context.Context) {
// UpdateCommentContent change comment of issue's content
func UpdateCommentContent(ctx *context.Context) {
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+ ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
return
}
- if err := comment.LoadIssue(); err != nil {
- ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+ if err := comment.LoadIssue(ctx); err != nil {
+ ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
return
}
@@ -2622,7 +2740,7 @@ func UpdateCommentContent(ctx *context.Context) {
return
}
- if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeReview && comment.Type != models.CommentTypeCode {
+ if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeReview && comment.Type != issues_model.CommentTypeCode {
ctx.Error(http.StatusNoContent)
return
}
@@ -2635,19 +2753,19 @@ func UpdateCommentContent(ctx *context.Context) {
})
return
}
- if err = comment_service.UpdateComment(comment, ctx.Doer, oldContent); err != nil {
+ if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
ctx.ServerError("UpdateComment", err)
return
}
- if err := comment.LoadAttachments(); err != nil {
+ if err := comment.LoadAttachments(ctx); err != nil {
ctx.ServerError("LoadAttachments", err)
return
}
// when the update request doesn't intend to update attachments (eg: change checkbox state), ignore attachment updates
if !ctx.FormBool("ignore_attachments") {
- if err := updateAttachments(comment, ctx.FormStrings("files[]")); err != nil {
+ if err := updateAttachments(ctx, comment, ctx.FormStrings("files[]")); err != nil {
ctx.ServerError("UpdateAttachments", err)
return
}
@@ -2672,27 +2790,27 @@ func UpdateCommentContent(ctx *context.Context) {
// DeleteComment delete comment of issue
func DeleteComment(ctx *context.Context) {
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+ ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
return
}
- if err := comment.LoadIssue(); err != nil {
- ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+ if err := comment.LoadIssue(ctx); err != nil {
+ ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Error(http.StatusForbidden)
return
- } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
+ } else if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeCode {
ctx.Error(http.StatusNoContent)
return
}
- if err = comment_service.DeleteComment(ctx.Doer, comment); err != nil {
- ctx.ServerError("DeleteCommentByID", err)
+ if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
+ ctx.ServerError("DeleteComment", err)
return
}
@@ -2748,7 +2866,7 @@ func ChangeIssueReaction(ctx *context.Context) {
}
// Reload new reactions
issue.Reactions = nil
- if err = issue.LoadAttributes(); err != nil {
+ if err = issue.LoadAttributes(ctx); err != nil {
log.Info("issue.LoadAttributes: %s", err)
break
}
@@ -2762,7 +2880,7 @@ func ChangeIssueReaction(ctx *context.Context) {
// Reload new reactions
issue.Reactions = nil
- if err := issue.LoadAttributes(); err != nil {
+ if err := issue.LoadAttributes(ctx); err != nil {
log.Info("issue.LoadAttributes: %s", err)
break
}
@@ -2798,14 +2916,14 @@ func ChangeIssueReaction(ctx *context.Context) {
// ChangeCommentReaction create a reaction for comment
func ChangeCommentReaction(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.ReactionForm)
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+ ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
return
}
- if err := comment.LoadIssue(); err != nil {
- ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
+ if err := comment.LoadIssue(ctx); err != nil {
+ ctx.NotFoundOrServerError("LoadIssue", issues_model.IsErrIssueNotExist, err)
return
}
@@ -2832,7 +2950,7 @@ func ChangeCommentReaction(ctx *context.Context) {
return
}
- if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode && comment.Type != models.CommentTypeReview {
+ if comment.Type != issues_model.CommentTypeComment && comment.Type != issues_model.CommentTypeCode && comment.Type != issues_model.CommentTypeReview {
ctx.Error(http.StatusNoContent)
return
}
@@ -2906,18 +3024,18 @@ func addParticipant(poster *user_model.User, participants []*user_model.User) []
return append(participants, poster)
}
-func filterXRefComments(ctx *context.Context, issue *models.Issue) error {
+func filterXRefComments(ctx *context.Context, issue *issues_model.Issue) error {
// Remove comments that the user has no permissions to see
for i := 0; i < len(issue.Comments); {
c := issue.Comments[i]
- if models.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 {
+ if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 {
var err error
// Set RefRepo for description in template
- c.RefRepo, err = repo_model.GetRepositoryByID(c.RefRepoID)
+ c.RefRepo, err = repo_model.GetRepositoryByID(ctx, c.RefRepoID)
if err != nil {
return err
}
- perm, err := models.GetUserRepoPermission(c.RefRepo, ctx.Doer)
+ perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, ctx.Doer)
if err != nil {
return err
}
@@ -2936,43 +3054,43 @@ func GetIssueAttachments(ctx *context.Context) {
issue := GetActionIssue(ctx)
attachments := make([]*api.Attachment, len(issue.Attachments))
for i := 0; i < len(issue.Attachments); i++ {
- attachments[i] = convert.ToReleaseAttachment(issue.Attachments[i])
+ attachments[i] = convert.ToAttachment(issue.Attachments[i])
}
ctx.JSON(http.StatusOK, attachments)
}
// GetCommentAttachments returns attachments for the comment
func GetCommentAttachments(ctx *context.Context) {
- comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
+ comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
- ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
+ ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
return
}
attachments := make([]*api.Attachment, 0)
- if comment.Type == models.CommentTypeComment {
- if err := comment.LoadAttachments(); err != nil {
+ if comment.Type == issues_model.CommentTypeComment {
+ if err := comment.LoadAttachments(ctx); err != nil {
ctx.ServerError("LoadAttachments", err)
return
}
for i := 0; i < len(comment.Attachments); i++ {
- attachments = append(attachments, convert.ToReleaseAttachment(comment.Attachments[i]))
+ attachments = append(attachments, convert.ToAttachment(comment.Attachments[i]))
}
}
ctx.JSON(http.StatusOK, attachments)
}
-func updateAttachments(item interface{}, files []string) error {
+func updateAttachments(ctx *context.Context, item interface{}, files []string) error {
var attachments []*repo_model.Attachment
switch content := item.(type) {
- case *models.Issue:
+ case *issues_model.Issue:
attachments = content.Attachments
- case *models.Comment:
+ case *issues_model.Comment:
attachments = content.Attachments
default:
return fmt.Errorf("unknown Type: %T", content)
}
for i := 0; i < len(attachments); i++ {
- if util.IsStringInSlice(attachments[i].UUID, files) {
+ if util.SliceContainsString(files, attachments[i].UUID) {
continue
}
if err := repo_model.DeleteAttachment(attachments[i], true); err != nil {
@@ -2982,9 +3100,9 @@ func updateAttachments(item interface{}, files []string) error {
var err error
if len(files) > 0 {
switch content := item.(type) {
- case *models.Issue:
- err = models.UpdateIssueAttachments(content.ID, files)
- case *models.Comment:
+ case *issues_model.Issue:
+ err = issues_model.UpdateIssueAttachments(content.ID, files)
+ case *issues_model.Comment:
err = content.UpdateAttachments(files)
default:
return fmt.Errorf("unknown Type: %T", content)
@@ -2994,10 +3112,10 @@ func updateAttachments(item interface{}, files []string) error {
}
}
switch content := item.(type) {
- case *models.Issue:
- content.Attachments, err = repo_model.GetAttachmentsByIssueID(content.ID)
- case *models.Comment:
- content.Attachments, err = repo_model.GetAttachmentsByCommentID(content.ID)
+ case *issues_model.Issue:
+ content.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, content.ID)
+ case *issues_model.Comment:
+ content.Attachments, err = repo_model.GetAttachmentsByCommentID(ctx, content.ID)
default:
return fmt.Errorf("unknown Type: %T", content)
}
@@ -3018,17 +3136,17 @@ func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment,
}
// combineLabelComments combine the nearby label comments as one.
-func combineLabelComments(issue *models.Issue) {
- var prev, cur *models.Comment
+func combineLabelComments(issue *issues_model.Issue) {
+ var prev, cur *issues_model.Comment
for i := 0; i < len(issue.Comments); i++ {
cur = issue.Comments[i]
if i > 0 {
prev = issue.Comments[i-1]
}
- if i == 0 || cur.Type != models.CommentTypeLabel ||
+ if i == 0 || cur.Type != issues_model.CommentTypeLabel ||
(prev != nil && prev.PosterID != cur.PosterID) ||
(prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
- if cur.Type == models.CommentTypeLabel && cur.Label != nil {
+ if cur.Type == issues_model.CommentTypeLabel && cur.Label != nil {
if cur.Content != "1" {
cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
} else {
@@ -3039,7 +3157,7 @@ func combineLabelComments(issue *models.Issue) {
}
if cur.Label != nil { // now cur MUST be label comment
- if prev.Type == models.CommentTypeLabel { // we can combine them only prev is a label comment
+ if prev.Type == issues_model.CommentTypeLabel { // we can combine them only prev is a label comment
if cur.Content != "1" {
// remove labels from the AddedLabels list if the label that was removed is already
// in this list, and if it's not in this list, add the label to RemovedLabels
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index 11cc8a2a6f559..3e6b31f8f71ba 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -1,24 +1,22 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
import (
"bytes"
- "fmt"
"html"
"net/http"
"strings"
- "code.gitea.io/gitea/models"
- issuesModel "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/avatars"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/translation/i18n"
"github.com/sergi/go-diff/diffmatchpatch"
)
@@ -30,14 +28,13 @@ func GetContentHistoryOverview(ctx *context.Context) {
return
}
- lang := ctx.Locale.Language()
- editedHistoryCountMap, _ := issuesModel.QueryIssueContentHistoryEditedCountMap(ctx, issue.ID)
+ editedHistoryCountMap, _ := issues_model.QueryIssueContentHistoryEditedCountMap(ctx, issue.ID)
ctx.JSON(http.StatusOK, map[string]interface{}{
"i18n": map[string]interface{}{
- "textEdited": i18n.Tr(lang, "repo.issues.content_history.edited"),
- "textDeleteFromHistory": i18n.Tr(lang, "repo.issues.content_history.delete_from_history"),
- "textDeleteFromHistoryConfirm": i18n.Tr(lang, "repo.issues.content_history.delete_from_history_confirm"),
- "textOptions": i18n.Tr(lang, "repo.issues.content_history.options"),
+ "textEdited": ctx.Tr("repo.issues.content_history.edited"),
+ "textDeleteFromHistory": ctx.Tr("repo.issues.content_history.delete_from_history"),
+ "textDeleteFromHistoryConfirm": ctx.Tr("repo.issues.content_history.delete_from_history_confirm"),
+ "textOptions": ctx.Tr("repo.issues.content_history.options"),
},
"editedHistoryCountMap": editedHistoryCountMap,
})
@@ -51,12 +48,11 @@ func GetContentHistoryList(ctx *context.Context) {
return
}
- items, _ := issuesModel.FetchIssueContentHistoryList(ctx, issue.ID, commentID)
+ items, _ := issues_model.FetchIssueContentHistoryList(ctx, issue.ID, commentID)
// render history list to HTML for frontend dropdown items: (name, value)
// name is HTML of "avatar + userName + userAction + timeSince"
// value is historyId
- lang := ctx.Locale.Language()
var results []map[string]interface{}
for _, item := range items {
var actionText string
@@ -68,16 +64,20 @@ func GetContentHistoryList(ctx *context.Context) {
} else {
actionText = ctx.Locale.Tr("repo.issues.content_history.edited")
}
- timeSinceText := timeutil.TimeSinceUnix(item.EditedUnix, lang)
username := item.UserName
if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" {
username = strings.TrimSpace(item.UserFullName)
}
+ src := html.EscapeString(item.UserAvatarLink)
+ class := avatars.DefaultAvatarClass + " mr-3"
+ name := html.EscapeString(username)
+ avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
+ timeSinceText := string(timeutil.TimeSinceUnix(item.EditedUnix, ctx.Locale))
+
results = append(results, map[string]interface{}{
- "name": fmt.Sprintf("]%s %s %s",
- html.EscapeString(item.UserAvatarLink), html.EscapeString(username), actionText, timeSinceText),
+ "name": avatarHTML + "" + name + " " + actionText + " " + timeSinceText,
"value": item.HistoryID,
})
}
@@ -89,8 +89,8 @@ func GetContentHistoryList(ctx *context.Context) {
// canSoftDeleteContentHistory checks whether current user can soft-delete a history revision
// Admins or owners can always delete history revisions. Normal users can only delete own history revisions.
-func canSoftDeleteContentHistory(ctx *context.Context, issue *models.Issue, comment *models.Comment,
- history *issuesModel.ContentHistory,
+func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue, comment *issues_model.Comment,
+ history *issues_model.ContentHistory,
) bool {
canSoftDelete := false
if ctx.Repo.IsOwner() {
@@ -118,7 +118,7 @@ func GetContentHistoryDetail(ctx *context.Context) {
}
historyID := ctx.FormInt64("history_id")
- history, prevHistory, err := issuesModel.GetIssueContentHistoryAndPrev(ctx, historyID)
+ history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, historyID)
if err != nil {
ctx.JSON(http.StatusNotFound, map[string]interface{}{
"message": "Can not find the content history",
@@ -127,10 +127,10 @@ func GetContentHistoryDetail(ctx *context.Context) {
}
// get the related comment if this history revision is for a comment, otherwise the history revision is for an issue.
- var comment *models.Comment
+ var comment *issues_model.Comment
if history.CommentID != 0 {
var err error
- if comment, err = models.GetCommentByID(history.CommentID); err != nil {
+ if comment, err = issues_model.GetCommentByID(ctx, history.CommentID); err != nil {
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
return
}
@@ -186,16 +186,16 @@ func SoftDeleteContentHistory(ctx *context.Context) {
commentID := ctx.FormInt64("comment_id")
historyID := ctx.FormInt64("history_id")
- var comment *models.Comment
- var history *issuesModel.ContentHistory
+ var comment *issues_model.Comment
+ var history *issues_model.ContentHistory
var err error
if commentID != 0 {
- if comment, err = models.GetCommentByID(commentID); err != nil {
+ if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
return
}
}
- if history, err = issuesModel.GetIssueContentHistoryByID(ctx, historyID); err != nil {
+ if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
log.Error("can not get issue content history %v. err=%v", historyID, err)
return
}
@@ -208,7 +208,7 @@ func SoftDeleteContentHistory(ctx *context.Context) {
return
}
- err = issuesModel.SoftDeleteIssueContentHistory(ctx, historyID)
+ err = issues_model.SoftDeleteIssueContentHistory(ctx, historyID)
log.Debug("soft delete issue content history. issue=%d, comment=%d, history=%d", issue.ID, commentID, historyID)
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": err == nil,
diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
index ec713238c67b4..41c127be91f30 100644
--- a/routers/web/repo/issue_dependency.go
+++ b/routers/web/repo/issue_dependency.go
@@ -1,13 +1,12 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
@@ -15,7 +14,7 @@ import (
// AddDependency adds new dependencies
func AddDependency(ctx *context.Context) {
issueIndex := ctx.ParamsInt64("index")
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
if err != nil {
ctx.ServerError("GetIssueByIndex", err)
return
@@ -38,7 +37,7 @@ func AddDependency(ctx *context.Context) {
defer ctx.Redirect(issue.HTMLURL())
// Dependency
- dep, err := models.GetIssueByID(depID)
+ dep, err := issues_model.GetIssueByID(ctx, depID)
if err != nil {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_issue_not_exist"))
return
@@ -56,12 +55,12 @@ func AddDependency(ctx *context.Context) {
return
}
- err = models.CreateIssueDependency(ctx.Doer, issue, dep)
+ err = issues_model.CreateIssueDependency(ctx.Doer, issue, dep)
if err != nil {
- if models.IsErrDependencyExists(err) {
+ if issues_model.IsErrDependencyExists(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_exists"))
return
- } else if models.IsErrCircularDependency(err) {
+ } else if issues_model.IsErrCircularDependency(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_cannot_create_circular"))
return
} else {
@@ -74,7 +73,7 @@ func AddDependency(ctx *context.Context) {
// RemoveDependency removes the dependency
func RemoveDependency(ctx *context.Context) {
issueIndex := ctx.ParamsInt64("index")
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, issueIndex)
if err != nil {
ctx.ServerError("GetIssueByIndex", err)
return
@@ -96,27 +95,27 @@ func RemoveDependency(ctx *context.Context) {
// Dependency Type
depTypeStr := ctx.Req.PostForm.Get("dependencyType")
- var depType models.DependencyType
+ var depType issues_model.DependencyType
switch depTypeStr {
case "blockedBy":
- depType = models.DependencyTypeBlockedBy
+ depType = issues_model.DependencyTypeBlockedBy
case "blocking":
- depType = models.DependencyTypeBlocking
+ depType = issues_model.DependencyTypeBlocking
default:
ctx.Error(http.StatusBadRequest, "GetDependecyType")
return
}
// Dependency
- dep, err := models.GetIssueByID(depID)
+ dep, err := issues_model.GetIssueByID(ctx, depID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
- if err = models.RemoveIssueDependency(ctx.Doer, issue, dep, depType); err != nil {
- if models.IsErrDependencyNotExists(err) {
+ if err = issues_model.RemoveIssueDependency(ctx.Doer, issue, dep, depType); err != nil {
+ if issues_model.IsErrDependencyNotExists(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_exist"))
return
}
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index 887bbc115f06a..01421dc9276a2 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -1,14 +1,13 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
@@ -56,7 +55,7 @@ func InitializeLabels(ctx *context.Context) {
// RetrieveLabels find all the labels of a repository and organization
func RetrieveLabels(ctx *context.Context) {
- labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{})
+ labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
ctx.ServerError("RetrieveLabels.GetLabels", err)
return
@@ -69,13 +68,13 @@ func RetrieveLabels(ctx *context.Context) {
ctx.Data["Labels"] = labels
if ctx.Repo.Owner.IsOrganization() {
- orgLabels, err := models.GetLabelsByOrgID(ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
+ orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByOrgID", err)
return
}
for _, l := range orgLabels {
- l.CalOpenOrgIssues(ctx.Repo.Repository.ID, l.ID)
+ l.CalOpenOrgIssues(ctx, ctx.Repo.Repository.ID, l.ID)
}
ctx.Data["OrgLabels"] = orgLabels
@@ -111,13 +110,13 @@ func NewLabel(ctx *context.Context) {
return
}
- l := &models.Label{
+ l := &issues_model.Label{
RepoID: ctx.Repo.Repository.ID,
Name: form.Title,
Description: form.Description,
Color: form.Color,
}
- if err := models.NewLabel(ctx, l); err != nil {
+ if err := issues_model.NewLabel(ctx, l); err != nil {
ctx.ServerError("NewLabel", err)
return
}
@@ -127,10 +126,10 @@ func NewLabel(ctx *context.Context) {
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateLabelForm)
- l, err := models.GetLabelInRepoByID(ctx.Repo.Repository.ID, form.ID)
+ l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID)
if err != nil {
switch {
- case models.IsErrRepoLabelNotExist(err):
+ case issues_model.IsErrRepoLabelNotExist(err):
ctx.Error(http.StatusNotFound)
default:
ctx.ServerError("UpdateLabel", err)
@@ -141,7 +140,7 @@ func UpdateLabel(ctx *context.Context) {
l.Name = form.Title
l.Description = form.Description
l.Color = form.Color
- if err := models.UpdateLabel(l); err != nil {
+ if err := issues_model.UpdateLabel(l); err != nil {
ctx.ServerError("UpdateLabel", err)
return
}
@@ -150,7 +149,7 @@ func UpdateLabel(ctx *context.Context) {
// DeleteLabel delete a label
func DeleteLabel(ctx *context.Context) {
- if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil {
+ if err := issues_model.DeleteLabel(ctx.Repo.Repository.ID, ctx.FormInt64("id")); err != nil {
ctx.Flash.Error("DeleteLabel: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
@@ -177,9 +176,9 @@ func UpdateIssueLabel(ctx *context.Context) {
}
}
case "attach", "detach", "toggle":
- label, err := models.GetLabelByID(ctx.FormInt64("id"))
+ label, err := issues_model.GetLabelByID(ctx, ctx.FormInt64("id"))
if err != nil {
- if models.IsErrRepoLabelNotExist(err) {
+ if issues_model.IsErrRepoLabelNotExist(err) {
ctx.Error(http.StatusNotFound, "GetLabelByID")
} else {
ctx.ServerError("GetLabelByID", err)
@@ -191,7 +190,7 @@ func UpdateIssueLabel(ctx *context.Context) {
// detach if any issues already have label, otherwise attach
action = "attach"
for _, issue := range issues {
- if models.HasIssueLabel(issue.ID, label.ID) {
+ if issues_model.HasIssueLabel(ctx, issue.ID, label.ID) {
action = "detach"
break
}
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index 5d7a29ee936d5..a62d2afaa830c 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,7 +8,7 @@ import (
"strconv"
"testing"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
@@ -37,7 +36,7 @@ func TestInitializeLabels(t *testing.T) {
web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
InitializeLabels(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
- unittest.AssertExistsAndLoadBean(t, &models.Label{
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
RepoID: 2,
Name: "enhancement",
Color: "#84b6eb",
@@ -62,7 +61,7 @@ func TestRetrieveLabels(t *testing.T) {
ctx.Req.Form.Set("sort", testCase.Sort)
RetrieveLabels(ctx)
assert.False(t, ctx.Written())
- labels, ok := ctx.Data["Labels"].([]*models.Label)
+ labels, ok := ctx.Data["Labels"].([]*issues_model.Label)
assert.True(t, ok)
if assert.Len(t, labels, len(testCase.ExpectedLabelIDs)) {
for i, label := range labels {
@@ -83,7 +82,7 @@ func TestNewLabel(t *testing.T) {
})
NewLabel(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
- unittest.AssertExistsAndLoadBean(t, &models.Label{
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
Name: "newlabel",
Color: "#abcdef",
})
@@ -102,7 +101,7 @@ func TestUpdateLabel(t *testing.T) {
})
UpdateLabel(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
- unittest.AssertExistsAndLoadBean(t, &models.Label{
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
ID: 2,
Name: "newnameforlabel",
Color: "#abcdef",
@@ -118,8 +117,8 @@ func TestDeleteLabel(t *testing.T) {
ctx.Req.Form.Set("id", "2")
DeleteLabel(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
- unittest.AssertNotExistsBean(t, &models.Label{ID: 2})
- unittest.AssertNotExistsBean(t, &models.IssueLabel{LabelID: 2})
+ unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
+ unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
}
@@ -132,9 +131,9 @@ func TestUpdateIssueLabel_Clear(t *testing.T) {
ctx.Req.Form.Set("action", "clear")
UpdateIssueLabel(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
- unittest.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 1})
- unittest.AssertNotExistsBean(t, &models.IssueLabel{IssueID: 3})
- unittest.CheckConsistencyFor(t, &models.Label{})
+ unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 1})
+ unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3})
+ unittest.CheckConsistencyFor(t, &issues_model.Label{})
}
func TestUpdateIssueLabel_Toggle(t *testing.T) {
@@ -159,11 +158,11 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) {
UpdateIssueLabel(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
for _, issueID := range testCase.IssueIDs {
- unittest.AssertExistsIf(t, testCase.ExpectedAdd, &models.IssueLabel{
+ unittest.AssertExistsIf(t, testCase.ExpectedAdd, &issues_model.IssueLabel{
IssueID: issueID,
LabelID: testCase.LabelID,
})
}
- unittest.CheckConsistencyFor(t, &models.Label{})
+ unittest.CheckConsistencyFor(t, &issues_model.Label{})
}
}
diff --git a/routers/web/repo/issue_lock.go b/routers/web/repo/issue_lock.go
index 5ac5cac52e152..10db968a211e2 100644
--- a/routers/web/repo/issue_lock.go
+++ b/routers/web/repo/issue_lock.go
@@ -1,11 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
import (
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
@@ -32,7 +31,7 @@ func LockIssue(ctx *context.Context) {
return
}
- if err := models.LockIssue(&models.IssueLockOptions{
+ if err := issues_model.LockIssue(&issues_model.IssueLockOptions{
Doer: ctx.Doer,
Issue: issue,
Reason: form.Reason,
@@ -57,7 +56,7 @@ func UnlockIssue(ctx *context.Context) {
return
}
- if err := models.UnlockIssue(&models.IssueLockOptions{
+ if err := issues_model.UnlockIssue(&issues_model.IssueLockOptions{
Doer: ctx.Doer,
Issue: issue,
}); err != nil {
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
index 7ef80e77254c8..d2a7a12a1f898 100644
--- a/routers/web/repo/issue_stopwatch.go
+++ b/routers/web/repo/issue_stopwatch.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -8,8 +7,10 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/eventsource"
)
// IssueStopwatch creates or stops a stopwatch for the given issue.
@@ -21,7 +22,7 @@ func IssueStopwatch(c *context.Context) {
var showSuccessMessage bool
- if !models.StopwatchExists(c.Doer.ID, issue.ID) {
+ if !issues_model.StopwatchExists(c.Doer.ID, issue.ID) {
showSuccessMessage = true
}
@@ -30,7 +31,7 @@ func IssueStopwatch(c *context.Context) {
return
}
- if err := models.CreateOrStopIssueStopwatch(c.Doer, issue); err != nil {
+ if err := issues_model.CreateOrStopIssueStopwatch(c.Doer, issue); err != nil {
c.ServerError("CreateOrStopIssueStopwatch", err)
return
}
@@ -54,11 +55,23 @@ func CancelStopwatch(c *context.Context) {
return
}
- if err := models.CancelStopwatch(c.Doer, issue); err != nil {
+ if err := issues_model.CancelStopwatch(c.Doer, issue); err != nil {
c.ServerError("CancelStopwatch", err)
return
}
+ stopwatches, err := issues_model.GetUserStopwatches(c.Doer.ID, db.ListOptions{})
+ if err != nil {
+ c.ServerError("GetUserStopwatches", err)
+ return
+ }
+ if len(stopwatches) == 0 {
+ eventsource.GetManager().SendMessage(c.Doer.ID, &eventsource.Event{
+ Name: "stopwatches",
+ Data: "{}",
+ })
+ }
+
url := issue.HTMLURL()
c.Redirect(url, http.StatusSeeOther)
}
@@ -73,7 +86,7 @@ func GetActiveStopwatch(ctx *context.Context) {
return
}
- _, sw, err := models.HasUserStopwatch(ctx.Doer.ID)
+ _, sw, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("HasUserStopwatch", err)
return
@@ -83,9 +96,11 @@ func GetActiveStopwatch(ctx *context.Context) {
return
}
- issue, err := models.GetIssueByID(sw.IssueID)
+ issue, err := issues_model.GetIssueByID(ctx, sw.IssueID)
if err != nil || issue == nil {
- ctx.ServerError("GetIssueByID", err)
+ if !issues_model.IsErrIssueNotExist(err) {
+ ctx.ServerError("GetIssueByID", err)
+ }
return
}
if err = issue.LoadRepo(ctx); err != nil {
diff --git a/routers/web/repo/issue_test.go b/routers/web/repo/issue_test.go
index debd2a8a3cec1..f1d0aac72ff5f 100644
--- a/routers/web/repo/issue_test.go
+++ b/routers/web/repo/issue_test.go
@@ -1,13 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
import (
"testing"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"github.com/stretchr/testify/assert"
)
@@ -15,50 +14,50 @@ import (
func TestCombineLabelComments(t *testing.T) {
kases := []struct {
name string
- beforeCombined []*models.Comment
- afterCombined []*models.Comment
+ beforeCombined []*issues_model.Comment
+ afterCombined []*issues_model.Comment
}{
{
name: "kase 1",
- beforeCombined: []*models.Comment{
+ beforeCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
- Type: models.CommentTypeComment,
+ Type: issues_model.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
},
- afterCombined: []*models.Comment{
+ afterCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
CreatedUnix: 0,
- AddedLabels: []*models.Label{},
- Label: &models.Label{
+ AddedLabels: []*issues_model.Label{},
+ Label: &issues_model.Label{
Name: "kind/bug",
},
},
{
- Type: models.CommentTypeComment,
+ Type: issues_model.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
@@ -67,63 +66,63 @@ func TestCombineLabelComments(t *testing.T) {
},
{
name: "kase 2",
- beforeCombined: []*models.Comment{
+ beforeCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 70,
},
{
- Type: models.CommentTypeComment,
+ Type: issues_model.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
},
- afterCombined: []*models.Comment{
+ afterCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
CreatedUnix: 0,
- AddedLabels: []*models.Label{
+ AddedLabels: []*issues_model.Label{
{
Name: "kind/bug",
},
},
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
},
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "",
CreatedUnix: 70,
- RemovedLabels: []*models.Label{
+ RemovedLabels: []*issues_model.Label{
{
Name: "kind/bug",
},
},
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
},
{
- Type: models.CommentTypeComment,
+ Type: issues_model.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
@@ -132,63 +131,63 @@ func TestCombineLabelComments(t *testing.T) {
},
{
name: "kase 3",
- beforeCombined: []*models.Comment{
+ beforeCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 2,
Content: "",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
- Type: models.CommentTypeComment,
+ Type: issues_model.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
},
},
- afterCombined: []*models.Comment{
+ afterCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
CreatedUnix: 0,
- AddedLabels: []*models.Label{
+ AddedLabels: []*issues_model.Label{
{
Name: "kind/bug",
},
},
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
},
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 2,
Content: "",
CreatedUnix: 0,
- RemovedLabels: []*models.Label{
+ RemovedLabels: []*issues_model.Label{
{
Name: "kind/bug",
},
},
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
},
{
- Type: models.CommentTypeComment,
+ Type: issues_model.CommentTypeComment,
PosterID: 1,
Content: "test",
CreatedUnix: 0,
@@ -197,33 +196,33 @@ func TestCombineLabelComments(t *testing.T) {
},
{
name: "kase 4",
- beforeCombined: []*models.Comment{
+ beforeCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/backport",
},
CreatedUnix: 10,
},
},
- afterCombined: []*models.Comment{
+ afterCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
CreatedUnix: 10,
- AddedLabels: []*models.Label{
+ AddedLabels: []*issues_model.Label{
{
Name: "kind/bug",
},
@@ -231,7 +230,7 @@ func TestCombineLabelComments(t *testing.T) {
Name: "kind/backport",
},
},
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
},
@@ -239,41 +238,41 @@ func TestCombineLabelComments(t *testing.T) {
},
{
name: "kase 5",
- beforeCombined: []*models.Comment{
+ beforeCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
- Type: models.CommentTypeComment,
+ Type: issues_model.CommentTypeComment,
PosterID: 2,
Content: "testtest",
CreatedUnix: 0,
},
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
},
- afterCombined: []*models.Comment{
+ afterCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
- AddedLabels: []*models.Label{
+ AddedLabels: []*issues_model.Label{
{
Name: "kind/bug",
},
@@ -281,21 +280,21 @@ func TestCombineLabelComments(t *testing.T) {
CreatedUnix: 0,
},
{
- Type: models.CommentTypeComment,
+ Type: issues_model.CommentTypeComment,
PosterID: 2,
Content: "testtest",
CreatedUnix: 0,
},
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "",
- RemovedLabels: []*models.Label{
+ RemovedLabels: []*issues_model.Label{
{
Name: "kind/bug",
},
},
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
@@ -304,53 +303,53 @@ func TestCombineLabelComments(t *testing.T) {
},
{
name: "kase 6",
- beforeCombined: []*models.Comment{
+ beforeCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "reviewed/confirmed",
},
CreatedUnix: 0,
},
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
CreatedUnix: 0,
},
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/feature",
},
CreatedUnix: 0,
},
},
- afterCombined: []*models.Comment{
+ afterCombined: []*issues_model.Comment{
{
- Type: models.CommentTypeLabel,
+ Type: issues_model.CommentTypeLabel,
PosterID: 1,
Content: "1",
- Label: &models.Label{
+ Label: &issues_model.Label{
Name: "kind/bug",
},
- AddedLabels: []*models.Label{
+ AddedLabels: []*issues_model.Label{
{
Name: "reviewed/confirmed",
},
@@ -366,7 +365,7 @@ func TestCombineLabelComments(t *testing.T) {
for _, kase := range kases {
t.Run(kase.name, func(t *testing.T) {
- issue := models.Issue{
+ issue := issues_model.Issue{
Comments: kase.beforeCombined,
}
combineLabelComments(&issue)
diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
index 0809acc2e417f..6e9d3673cf9d4 100644
--- a/routers/web/repo/issue_timetrack.go
+++ b/routers/web/repo/issue_timetrack.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -8,7 +7,8 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -42,7 +42,7 @@ func AddTimeManually(c *context.Context) {
return
}
- if _, err := models.AddTime(c.Doer, issue, int64(total.Seconds()), time.Now()); err != nil {
+ if _, err := issues_model.AddTime(c.Doer, issue, int64(total.Seconds()), time.Now()); err != nil {
c.ServerError("AddTime", err)
return
}
@@ -61,9 +61,9 @@ func DeleteTime(c *context.Context) {
return
}
- t, err := models.GetTrackedTimeByID(c.ParamsInt64(":timeid"))
+ t, err := issues_model.GetTrackedTimeByID(c.ParamsInt64(":timeid"))
if err != nil {
- if models.IsErrNotExist(err) {
+ if db.IsErrNotExist(err) {
c.NotFound("time not found", err)
return
}
@@ -77,7 +77,7 @@ func DeleteTime(c *context.Context) {
return
}
- if err = models.DeleteTime(t); err != nil {
+ if err = issues_model.DeleteTime(t); err != nil {
c.ServerError("DeleteTime", err)
return
}
diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go
index 53fec11cdcf1d..c23dbf062291d 100644
--- a/routers/web/repo/issue_watch.go
+++ b/routers/web/repo/issue_watch.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -8,7 +7,7 @@ import (
"net/http"
"strconv"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
)
@@ -48,7 +47,7 @@ func IssueWatch(ctx *context.Context) {
return
}
- if err := models.CreateOrUpdateIssueWatch(ctx.Doer.ID, issue.ID, watch); err != nil {
+ if err := issues_model.CreateOrUpdateIssueWatch(ctx.Doer.ID, issue.ID, watch); err != nil {
ctx.ServerError("CreateOrUpdateIssueWatch", err)
return
}
diff --git a/routers/web/repo/lfs.go b/routers/web/repo/lfs.go
index e0ef864edf29b..41d5edcdd8ca0 100644
--- a/routers/web/repo/lfs.go
+++ b/routers/web/repo/lfs.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -15,14 +14,16 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/pipeline"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/typesniffer"
@@ -47,7 +48,7 @@ func LFSFiles(ctx *context.Context) {
if page <= 1 {
page = 1
}
- total, err := models.CountLFSMetaObjects(ctx.Repo.Repository.ID)
+ total, err := git_model.CountLFSMetaObjects(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("LFSFiles", err)
return
@@ -57,7 +58,7 @@ func LFSFiles(ctx *context.Context) {
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
ctx.Data["PageIsSettingsLFS"] = true
- lfsMetaObjects, err := models.GetLFSMetaObjects(ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
+ lfsMetaObjects, err := git_model.GetLFSMetaObjects(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
if err != nil {
ctx.ServerError("LFSFiles", err)
return
@@ -79,7 +80,7 @@ func LFSLocks(ctx *context.Context) {
if page <= 1 {
page = 1
}
- total, err := models.CountLFSLockByRepoID(ctx.Repo.Repository.ID)
+ total, err := git_model.CountLFSLockByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("LFSLocks", err)
return
@@ -89,7 +90,7 @@ func LFSLocks(ctx *context.Context) {
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks")
ctx.Data["PageIsSettingsLFS"] = true
- lfsLocks, err := models.GetLFSLockByRepoID(ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
+ lfsLocks, err := git_model.GetLFSLockByRepoID(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
if err != nil {
ctx.ServerError("LFSLocks", err)
return
@@ -103,14 +104,14 @@ func LFSLocks(ctx *context.Context) {
}
// Clone base repo.
- tmpBasePath, err := models.CreateTemporaryPath("locks")
+ tmpBasePath, err := repo_module.CreateTemporaryPath("locks")
if err != nil {
log.Error("Failed to create temporary path: %v", err)
ctx.ServerError("LFSLocks", err)
return
}
defer func() {
- if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("LFSLocks: RemoveTemporaryPath: %v", err)
}
}()
@@ -120,14 +121,14 @@ func LFSLocks(ctx *context.Context) {
Shared: true,
}); err != nil {
log.Error("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err)
- ctx.ServerError("LFSLocks", fmt.Errorf("failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err))
+ ctx.ServerError("LFSLocks", fmt.Errorf("failed to clone repository: %s (%w)", ctx.Repo.Repository.FullName(), err))
return
}
gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
if err != nil {
log.Error("Unable to open temporary repository: %s (%v)", tmpBasePath, err)
- ctx.ServerError("LFSLocks", fmt.Errorf("failed to open new temporary repository in: %s %v", tmpBasePath, err))
+ ctx.ServerError("LFSLocks", fmt.Errorf("failed to open new temporary repository in: %s %w", tmpBasePath, err))
return
}
defer gitRepo.Close()
@@ -140,12 +141,12 @@ func LFSLocks(ctx *context.Context) {
if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil {
log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err)
- ctx.ServerError("LFSLocks", fmt.Errorf("unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err))
+ ctx.ServerError("LFSLocks", fmt.Errorf("unable to read the default branch to the index: %s (%w)", ctx.Repo.Repository.DefaultBranch, err))
return
}
name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
- Attributes: []string{"lockable"},
+ Attributes: []git.CmdArg{"lockable"},
Filenames: filenames,
CachedOnly: true,
})
@@ -175,14 +176,12 @@ func LFSLocks(ctx *context.Context) {
return
}
- filemap := make(map[string]bool, len(filelist))
- for _, name := range filelist {
- filemap[name] = true
- }
+ fileset := make(container.Set[string], len(filelist))
+ fileset.AddMultiple(filelist...)
linkable := make([]bool, len(lfsLocks))
for i, lock := range lfsLocks {
- linkable[i] = filemap[lock.Path]
+ linkable[i] = fileset.Contains(lock.Path)
}
ctx.Data["Linkable"] = linkable
@@ -215,12 +214,12 @@ func LFSLockFile(ctx *context.Context) {
return
}
- _, err := models.CreateLFSLock(ctx.Repo.Repository, &models.LFSLock{
+ _, err := git_model.CreateLFSLock(ctx, ctx.Repo.Repository, &git_model.LFSLock{
Path: lockPath,
OwnerID: ctx.Doer.ID,
})
if err != nil {
- if models.IsErrLFSLockAlreadyExist(err) {
+ if git_model.IsErrLFSLockAlreadyExist(err) {
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_lock_already_exists", originalPath))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
return
@@ -237,7 +236,7 @@ func LFSUnlock(ctx *context.Context) {
ctx.NotFound("LFSUnlock", nil)
return
}
- _, err := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), ctx.Repo.Repository, ctx.Doer, true)
+ _, err := git_model.DeleteLFSLockByID(ctx, ctx.ParamsInt64("lid"), ctx.Repo.Repository, ctx.Doer, true)
if err != nil {
ctx.ServerError("LFSUnlock", err)
return
@@ -262,9 +261,9 @@ func LFSFileGet(ctx *context.Context) {
ctx.Data["Title"] = oid
ctx.Data["PageIsSettingsLFS"] = true
- meta, err := models.GetLFSMetaObjectByOid(ctx.Repo.Repository.ID, oid)
+ meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, oid)
if err != nil {
- if err == models.ErrLFSObjectNotExist {
+ if err == git_model.ErrLFSObjectNotExist {
ctx.NotFound("LFSFileGet", nil)
return
}
@@ -308,7 +307,7 @@ func LFSFileGet(ctx *context.Context) {
// Building code view blocks with line number on server side.
escapedContent := &bytes.Buffer{}
- ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, escapedContent)
+ ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, escapedContent, ctx.Locale)
var output bytes.Buffer
lines := strings.Split(escapedContent.String(), "\n")
@@ -356,7 +355,7 @@ func LFSDelete(ctx *context.Context) {
return
}
- count, err := models.RemoveLFSMetaObjectByOid(ctx.Repo.Repository.ID, oid)
+ count, err := git_model.RemoveLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, oid)
if err != nil {
ctx.ServerError("LFSDelete", err)
return
@@ -420,12 +419,9 @@ func LFSPointerFiles(ctx *context.Context) {
return
}
ctx.Data["PageIsSettingsLFS"] = true
- err := git.LoadGitVersion()
- if err != nil {
- log.Fatal("Error retrieving git version: %v", err)
- }
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
+ var err error
err = func() error {
pointerChan := make(chan lfs.PointerBlob)
errChan := make(chan error, 1)
@@ -458,8 +454,8 @@ func LFSPointerFiles(ctx *context.Context) {
Size: pointerBlob.Size,
}
- if _, err := models.GetLFSMetaObjectByOid(repo.ID, pointerBlob.Oid); err != nil {
- if err != models.ErrLFSObjectNotExist {
+ if _, err := git_model.GetLFSMetaObjectByOid(ctx, repo.ID, pointerBlob.Oid); err != nil {
+ if err != git_model.ErrLFSObjectNotExist {
return err
}
} else {
@@ -476,12 +472,12 @@ func LFSPointerFiles(ctx *context.Context) {
// Can we fix?
// OK well that's "simple"
// - we need to check whether current user has access to a repo that has access to the file
- result.Associatable, err = models.LFSObjectAccessible(ctx.Doer, pointerBlob.Oid)
+ result.Associatable, err = git_model.LFSObjectAccessible(ctx, ctx.Doer, pointerBlob.Oid)
if err != nil {
return err
}
if !result.Associatable {
- associated, err := models.LFSObjectIsAssociated(pointerBlob.Oid)
+ associated, err := git_model.ExistsLFSObject(ctx, pointerBlob.Oid)
if err != nil {
return err
}
@@ -534,7 +530,7 @@ func LFSAutoAssociate(ctx *context.Context) {
return
}
oids := ctx.FormStrings("oid")
- metas := make([]*models.LFSMetaObject, len(oids))
+ metas := make([]*git_model.LFSMetaObject, len(oids))
for i, oid := range oids {
idx := strings.IndexRune(oid, ' ')
if idx < 0 || idx+1 > len(oid) {
@@ -542,16 +538,16 @@ func LFSAutoAssociate(ctx *context.Context) {
return
}
var err error
- metas[i] = &models.LFSMetaObject{}
+ metas[i] = &git_model.LFSMetaObject{}
metas[i].Size, err = strconv.ParseInt(oid[idx+1:], 10, 64)
if err != nil {
- ctx.ServerError("LFSAutoAssociate", fmt.Errorf("illegal oid input: %s %v", oid, err))
+ ctx.ServerError("LFSAutoAssociate", fmt.Errorf("illegal oid input: %s %w", oid, err))
return
}
metas[i].Oid = oid[:idx]
// metas[i].RepositoryID = ctx.Repo.Repository.ID
}
- if err := models.LFSAutoAssociate(metas, ctx.Doer, ctx.Repo.Repository.ID); err != nil {
+ if err := git_model.LFSAutoAssociate(ctx, metas, ctx.Doer, ctx.Repo.Repository.ID); err != nil {
ctx.ServerError("LFSAutoAssociate", err)
return
}
diff --git a/routers/web/repo/main_test.go b/routers/web/repo/main_test.go
index a1ca3c3bc716a..e3a09a95bd36d 100644
--- a/routers/web/repo/main_test.go
+++ b/routers/web/repo/main_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go
index ae4177cf1e639..9a4aa3382cbba 100644
--- a/routers/web/repo/middlewares.go
+++ b/routers/web/repo/middlewares.go
@@ -1,13 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
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 +23,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/migrate.go b/routers/web/repo/migrate.go
index 38cdbd49735d0..bbc349a0a5441 100644
--- a/routers/web/repo/migrate.go
+++ b/routers/web/repo/migrate.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
@@ -128,7 +127,7 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplN
case addrErr.IsProtocolInvalid:
ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form)
case addrErr.IsURLError:
- ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
+ ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tpl, form)
case addrErr.IsPermissionDenied:
if addrErr.LocalPath {
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tpl, form)
@@ -139,11 +138,11 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplN
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
default:
log.Error("Error whilst updating url: %v", err)
- ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
+ ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form)
}
} else {
log.Error("Error whilst updating url: %v", err)
- ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form)
+ ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form)
}
}
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 1e75bd79fb27c..d712df1001bc3 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -74,7 +73,7 @@ func Milestones(ctx *context.Context) {
ctx.ServerError("GetMilestones", err)
return
}
- if ctx.Repo.Repository.IsTimetrackerEnabled() {
+ if ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
if err := miles.LoadTotalTrackedTimes(); err != nil {
ctx.ServerError("LoadTotalTrackedTimes", err)
return
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index b4db2d5787bf9..6ad2f71b5c2de 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,9 +8,11 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
)
const (
@@ -32,10 +33,11 @@ func Packages(ctx *context.Context) {
PageSize: setting.UI.PackagesPagingNum,
Page: page,
},
- OwnerID: ctx.ContextUser.ID,
- RepoID: ctx.Repo.Repository.ID,
- Type: packages.Type(packageType),
- Name: packages.SearchValue{Value: query},
+ OwnerID: ctx.ContextUser.ID,
+ RepoID: ctx.Repo.Repository.ID,
+ Type: packages.Type(packageType),
+ Name: packages.SearchValue{Value: query},
+ IsInternal: util.OptionalBoolFalse,
})
if err != nil {
ctx.ServerError("SearchLatestVersions", err)
@@ -59,9 +61,14 @@ func Packages(ctx *context.Context) {
ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["Query"] = query
ctx.Data["PackageType"] = packageType
+ ctx.Data["AvailableTypes"] = packages.TypeList
ctx.Data["HasPackages"] = hasPackages
+ if ctx.Repo != nil {
+ ctx.Data["CanWritePackages"] = ctx.IsUserRepoWriter([]unit.Type{unit.TypePackages}) || ctx.IsUserSiteAdmin()
+ }
ctx.Data["PackageDescriptors"] = pds
ctx.Data["Total"] = total
+ ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
pager.AddParam(ctx, "q", "Query")
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
index 2bb9dc1199915..12b26f38e9013 100644
--- a/routers/web/repo/patch.go
+++ b/routers/web/repo/patch.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -24,8 +23,6 @@ const (
// NewDiffPatch render create patch page
func NewDiffPatch(ctx *context.Context) {
- ctx.Data["RequireHighlightJS"] = true
-
canCommit := renderCommitRights(ctx)
ctx.Data["TreePath"] = ""
@@ -54,7 +51,6 @@ func NewDiffPatchPost(ctx *context.Context) {
if form.CommitChoice == frmCommitChoiceNewBranch {
branchName = form.NewBranchName
}
- ctx.Data["RequireHighlightJS"] = true
ctx.Data["TreePath"] = ""
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
ctx.Data["FileContent"] = form.Content
@@ -112,7 +108,7 @@ func NewDiffPatchPost(ctx *context.Context) {
}
}
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(unit.TypePullRequests) {
+ if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
} else {
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index a6f843d848fab..75cd290b8f0cb 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -1,16 +1,16 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
import (
+ "errors"
"fmt"
"net/http"
"net/url"
"strings"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/models/unit"
@@ -70,7 +70,7 @@ func Projects(ctx *context.Context) {
total = repo.NumClosedProjects
}
- projects, count, err := project_model.GetProjects(project_model.SearchOptions{
+ projects, count, err := project_model.GetProjects(ctx, project_model.SearchOptions{
RepoID: repo.ID,
Page: page,
IsClosed: util.OptionalBoolOf(isShowClosed),
@@ -105,7 +105,7 @@ func Projects(ctx *context.Context) {
numPages := 0
if count > 0 {
- numPages = int((int(count) - 1) / setting.UI.IssuePagingNum)
+ numPages = (int(count) - 1/setting.UI.IssuePagingNum)
}
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
@@ -182,7 +182,7 @@ func ChangeProjectStatus(ctx *context.Context) {
// DeleteProject delete a project
func DeleteProject(ctx *context.Context) {
- p, err := project_model.GetProjectByID(ctx.ParamsInt64(":id"))
+ p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
@@ -196,7 +196,7 @@ func DeleteProject(ctx *context.Context) {
return
}
- if err := project_model.DeleteProjectByID(p.ID); err != nil {
+ if err := project_model.DeleteProjectByID(ctx, p.ID); err != nil {
ctx.Flash.Error("DeleteProjectByID: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.projects.deletion_success"))
@@ -213,7 +213,7 @@ func EditProject(ctx *context.Context) {
ctx.Data["PageIsEditProjects"] = true
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
- p, err := project_model.GetProjectByID(ctx.ParamsInt64(":id"))
+ p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
@@ -245,7 +245,7 @@ func EditProjectPost(ctx *context.Context) {
return
}
- p, err := project_model.GetProjectByID(ctx.ParamsInt64(":id"))
+ p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
@@ -261,7 +261,7 @@ func EditProjectPost(ctx *context.Context) {
p.Title = form.Title
p.Description = form.Content
- if err = project_model.UpdateProject(p); err != nil {
+ if err = project_model.UpdateProject(ctx, p); err != nil {
ctx.ServerError("UpdateProjects", err)
return
}
@@ -272,7 +272,7 @@ func EditProjectPost(ctx *context.Context) {
// ViewProject renders the project board for a project
func ViewProject(ctx *context.Context) {
- project, err := project_model.GetProjectByID(ctx.ParamsInt64(":id"))
+ project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
@@ -286,7 +286,7 @@ func ViewProject(ctx *context.Context) {
return
}
- boards, err := project_model.GetBoards(project.ID)
+ boards, err := project_model.GetBoards(ctx, project.ID)
if err != nil {
ctx.ServerError("GetProjectBoards", err)
return
@@ -296,13 +296,13 @@ func ViewProject(ctx *context.Context) {
boards[0].Title = ctx.Tr("repo.projects.type.uncategorized")
}
- issuesMap, err := models.LoadIssuesFromBoardList(boards)
+ issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
if err != nil {
ctx.ServerError("LoadIssuesOfBoards", err)
return
}
- linkedPrsMap := make(map[int64][]*models.Issue)
+ linkedPrsMap := make(map[int64][]*issues_model.Issue)
for _, issuesList := range issuesMap {
for _, issue := range issuesList {
var referencedIds []int64
@@ -313,7 +313,7 @@ func ViewProject(ctx *context.Context) {
}
if len(referencedIds) > 0 {
- if linkedPrs, err := models.Issues(&models.IssuesOptions{
+ if linkedPrs, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
IssueIDs: referencedIds,
IsPull: util.OptionalBoolTrue,
}); err == nil {
@@ -358,7 +358,7 @@ func UpdateIssueProject(ctx *context.Context) {
continue
}
- if err := models.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
+ if err := issues_model.ChangeProjectAssign(issue, ctx.Doer, projectID); err != nil {
ctx.ServerError("ChangeProjectAssign", err)
return
}
@@ -385,7 +385,7 @@ func DeleteProjectBoard(ctx *context.Context) {
return
}
- project, err := project_model.GetProjectByID(ctx.ParamsInt64(":id"))
+ project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
@@ -395,7 +395,7 @@ func DeleteProjectBoard(ctx *context.Context) {
return
}
- pb, err := project_model.GetBoard(ctx.ParamsInt64(":boardID"))
+ pb, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
if err != nil {
ctx.ServerError("GetProjectBoard", err)
return
@@ -434,7 +434,7 @@ func AddBoardToProjectPost(ctx *context.Context) {
return
}
- project, err := project_model.GetProjectByID(ctx.ParamsInt64(":id"))
+ project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
@@ -474,7 +474,7 @@ func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
return nil, nil
}
- project, err := project_model.GetProjectByID(ctx.ParamsInt64(":id"))
+ project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("", nil)
@@ -484,7 +484,7 @@ func checkProjectBoardChangePermissions(ctx *context.Context) (*project_model.Pr
return nil, nil
}
- board, err := project_model.GetBoard(ctx.ParamsInt64(":boardID"))
+ board, err := project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
if err != nil {
ctx.ServerError("GetProjectBoard", err)
return nil, nil
@@ -523,7 +523,7 @@ func EditProjectBoard(ctx *context.Context) {
board.Sorting = form.Sorting
}
- if err := project_model.UpdateBoard(board); err != nil {
+ if err := project_model.UpdateBoard(ctx, board); err != nil {
ctx.ServerError("UpdateProjectBoard", err)
return
}
@@ -566,7 +566,7 @@ func MoveIssues(ctx *context.Context) {
return
}
- project, err := project_model.GetProjectByID(ctx.ParamsInt64(":id"))
+ project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
ctx.NotFound("ProjectNotExist", nil)
@@ -589,7 +589,7 @@ func MoveIssues(ctx *context.Context) {
Title: ctx.Tr("repo.projects.type.uncategorized"),
}
} else {
- board, err = project_model.GetBoard(ctx.ParamsInt64(":boardID"))
+ board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))
if err != nil {
if project_model.IsErrProjectBoardNotExist(err) {
ctx.NotFound("ProjectBoardNotExist", nil)
@@ -622,9 +622,9 @@ func MoveIssues(ctx *context.Context) {
issueIDs = append(issueIDs, issue.IssueID)
sortedIssueIDs[issue.Sorting] = issue.IssueID
}
- movedIssues, err := models.GetIssuesByIDs(issueIDs)
+ movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound("IssueNotExisting", nil)
} else {
ctx.ServerError("GetIssueByID", err)
@@ -633,10 +633,17 @@ func MoveIssues(ctx *context.Context) {
}
if len(movedIssues) != len(form.Issues) {
- ctx.ServerError("IssuesNotFound", err)
+ ctx.ServerError("some issues do not exist", errors.New("some issues do not exist"))
return
}
+ for _, issue := range movedIssues {
+ if issue.RepoID != project.RepoID {
+ ctx.ServerError("Some issue's repoID is not equal to project's repoID", errors.New("Some issue's repoID is not equal to project's repoID"))
+ return
+ }
+ }
+
if err = project_model.MoveIssuesOnProjectBoard(board, sortedIssueIDs); err != nil {
ctx.ServerError("MoveIssuesOnProjectBoard", err)
return
diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go
index 62fb0457400bd..c712902ea9f1a 100644
--- a/routers/web/repo/projects_test.go
+++ b/routers/web/repo/projects_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 113e2d8421125..c2208120fcf24 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -1,8 +1,7 @@
// Copyright 2018 The Gitea Authors.
// Copyright 2014 The Gogs Authors.
// All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
@@ -17,14 +16,20 @@ import (
"time"
"code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ issue_template "code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/setting"
@@ -35,6 +40,7 @@ import (
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/gitdiff"
pull_service "code.gitea.io/gitea/services/pull"
@@ -52,15 +58,27 @@ const (
var pullRequestTemplateCandidates = []string{
"PULL_REQUEST_TEMPLATE.md",
+ "PULL_REQUEST_TEMPLATE.yaml",
+ "PULL_REQUEST_TEMPLATE.yml",
"pull_request_template.md",
+ "pull_request_template.yaml",
+ "pull_request_template.yml",
".gitea/PULL_REQUEST_TEMPLATE.md",
+ ".gitea/PULL_REQUEST_TEMPLATE.yaml",
+ ".gitea/PULL_REQUEST_TEMPLATE.yml",
".gitea/pull_request_template.md",
+ ".gitea/pull_request_template.yaml",
+ ".gitea/pull_request_template.yml",
".github/PULL_REQUEST_TEMPLATE.md",
+ ".github/PULL_REQUEST_TEMPLATE.yaml",
+ ".github/PULL_REQUEST_TEMPLATE.yml",
".github/pull_request_template.md",
+ ".github/pull_request_template.yaml",
+ ".github/pull_request_template.yml",
}
func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
- repo, err := repo_model.GetRepositoryByID(repoID)
+ repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.NotFound("GetRepositoryByID", nil)
@@ -70,7 +88,7 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
return nil
}
- perm, err := models.GetUserRepoPermission(repo, ctx.Doer)
+ perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return nil
@@ -141,7 +159,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
if !traverseParentRepo.IsFork {
break
}
- traverseParentRepo, err = repo_model.GetRepositoryByID(traverseParentRepo.ForkID)
+ traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
if err != nil {
ctx.ServerError("GetRepositoryByID", err)
return nil
@@ -164,6 +182,15 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
func Fork(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_fork")
+ if ctx.Doer.CanForkRepo() {
+ ctx.Data["CanForkRepo"] = true
+ } else {
+ maxCreationLimit := ctx.Doer.MaxCreationLimit()
+ msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+ ctx.Data["Flash"] = ctx.Flash
+ ctx.Flash.Error(msg)
+ }
+
getForkRepository(ctx)
if ctx.Written() {
return
@@ -209,7 +236,7 @@ func ForkPost(ctx *context.Context) {
if !traverseParentRepo.IsFork {
break
}
- traverseParentRepo, err = repo_model.GetRepositoryByID(traverseParentRepo.ForkID)
+ traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
if err != nil {
ctx.ServerError("GetRepositoryByID", err)
return
@@ -236,6 +263,10 @@ func ForkPost(ctx *context.Context) {
if err != nil {
ctx.Data["Err_RepoName"] = true
switch {
+ case repo_model.IsErrReachLimitOfRepo(err):
+ maxCreationLimit := ctxUser.MaxCreationLimit()
+ msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+ ctx.RenderWithErr(msg, tplFork, &form)
case repo_model.IsErrRepoAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
case db.IsErrNameReserved(err):
@@ -252,17 +283,17 @@ func ForkPost(ctx *context.Context) {
ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
}
-func checkPullInfo(ctx *context.Context) *models.Issue {
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+func checkPullInfo(ctx *context.Context) *issues_model.Issue {
+ issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrIssueNotExist(err) {
+ if issues_model.IsErrIssueNotExist(err) {
ctx.NotFound("GetIssueByIndex", err)
} else {
ctx.ServerError("GetIssueByIndex", err)
}
return nil
}
- if err = issue.LoadPoster(); err != nil {
+ if err = issue.LoadPoster(ctx); err != nil {
ctx.ServerError("LoadPoster", err)
return nil
}
@@ -278,19 +309,19 @@ func checkPullInfo(ctx *context.Context) *models.Issue {
return nil
}
- if err = issue.LoadPullRequest(); err != nil {
+ if err = issue.LoadPullRequest(ctx); err != nil {
ctx.ServerError("LoadPullRequest", err)
return nil
}
- if err = issue.PullRequest.LoadHeadRepo(); err != nil {
+ if err = issue.PullRequest.LoadHeadRepo(ctx); err != nil {
ctx.ServerError("LoadHeadRepo", err)
return nil
}
if ctx.IsSigned {
// Update issue-user.
- if err = issue.ReadBy(ctx.Doer.ID); err != nil {
+ if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
ctx.ServerError("ReadBy", err)
return nil
}
@@ -299,13 +330,13 @@ func checkPullInfo(ctx *context.Context) *models.Issue {
return issue
}
-func setMergeTarget(ctx *context.Context, pull *models.PullRequest) {
- if ctx.Repo.Owner.Name == pull.MustHeadUserName() {
+func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
+ if ctx.Repo.Owner.Name == pull.MustHeadUserName(ctx) {
ctx.Data["HeadTarget"] = pull.HeadBranch
} else if pull.HeadRepo == nil {
- ctx.Data["HeadTarget"] = pull.MustHeadUserName() + ":" + pull.HeadBranch
+ ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + ":" + pull.HeadBranch
} else {
- ctx.Data["HeadTarget"] = pull.MustHeadUserName() + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch
+ ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch
}
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["HeadBranchHTMLURL"] = pull.GetHeadBranchHTMLURL()
@@ -313,7 +344,7 @@ func setMergeTarget(ctx *context.Context, pull *models.PullRequest) {
}
// PrepareMergedViewPullInfo show meta information for a merged pull request view page
-func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
+func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo {
pull := issue.PullRequest
setMergeTarget(ctx, pull)
@@ -340,7 +371,7 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.C
}
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)
}
@@ -376,14 +407,14 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.C
if len(compareInfo.Commits) != 0 {
sha := compareInfo.Commits[0].ID.String()
- commitStatuses, _, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, sha, db.ListOptions{})
+ commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptions{})
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
}
if len(commitStatuses) != 0 {
ctx.Data["LatestCommitStatuses"] = commitStatuses
- ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
+ ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
}
}
@@ -391,29 +422,30 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.C
}
// PrepareViewPullInfo show meta information for a pull request preview page
-func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
+func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo {
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
repo := ctx.Repo.Repository
pull := issue.PullRequest
- if err := pull.LoadHeadRepo(); err != nil {
+ if err := pull.LoadHeadRepo(ctx); err != nil {
ctx.ServerError("LoadHeadRepo", err)
return nil
}
- if err := pull.LoadBaseRepo(); err != nil {
+ if err := pull.LoadBaseRepo(ctx); err != nil {
ctx.ServerError("LoadBaseRepo", err)
return nil
}
setMergeTarget(ctx, pull)
- if err := pull.LoadProtectedBranch(); err != nil {
+ pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pull.BaseBranch)
+ if err != nil {
ctx.ServerError("LoadProtectedBranch", err)
return nil
}
- ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck
+ ctx.Data["EnableStatusCheck"] = pb != nil && pb.EnableStatusCheck
var baseGitRepo *git.Repository
if pull.BaseRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil {
@@ -437,14 +469,14 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
return nil
}
- commitStatuses, _, err := models.GetLatestCommitStatus(repo.ID, sha, db.ListOptions{})
+ commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{})
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
}
if len(commitStatuses) > 0 {
ctx.Data["LatestCommitStatuses"] = commitStatuses
- ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
+ ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
}
compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
@@ -478,14 +510,14 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
}
defer headGitRepo.Close()
- if pull.Flow == models.PullRequestFlowGithub {
+ if pull.Flow == issues_model.PullRequestFlowGithub {
headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
} else {
headBranchExist = git.IsReferenceExist(ctx, baseGitRepo.Path, pull.GetGitRefName())
}
if headBranchExist {
- if pull.Flow != models.PullRequestFlowGithub {
+ if pull.Flow != issues_model.PullRequestFlowGithub {
headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitRefName())
} else {
headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
@@ -499,12 +531,14 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
if headBranchExist {
var err error
- ctx.Data["UpdateAllowed"], ctx.Data["UpdateByRebaseAllowed"], err = pull_service.IsUserAllowedToUpdate(pull, ctx.Doer)
+ ctx.Data["UpdateAllowed"], ctx.Data["UpdateByRebaseAllowed"], err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer)
if err != nil {
ctx.ServerError("IsUserAllowedToUpdate", err)
return nil
}
ctx.Data["GetCommitMessages"] = pull_service.GetSquashMergeCommitMessages(ctx, pull)
+ } else {
+ ctx.Data["GetCommitMessages"] = ""
}
sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
@@ -527,26 +561,26 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
return nil
}
- commitStatuses, _, err := models.GetLatestCommitStatus(repo.ID, sha, db.ListOptions{})
+ commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{})
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
}
if len(commitStatuses) > 0 {
ctx.Data["LatestCommitStatuses"] = commitStatuses
- ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
+ ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
}
- if pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck {
+ if pb != nil && pb.EnableStatusCheck {
ctx.Data["is_context_required"] = func(context string) bool {
- for _, c := range pull.ProtectedBranch.StatusCheckContexts {
+ for _, c := range pb.StatusCheckContexts {
if c == context {
return true
}
}
return false
}
- ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pull.ProtectedBranch.StatusCheckContexts)
+ ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pb.StatusCheckContexts)
}
ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha
@@ -585,7 +619,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
if pull.IsWorkInProgress() {
ctx.Data["IsPullWorkInProgress"] = true
- ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix()
+ ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix(ctx)
}
if pull.IsFilesConflicted() {
@@ -626,7 +660,7 @@ func ViewPullCommits(ctx *context.Context) {
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- commits := models.ConvertFromGitCommit(prInfo.Commits, ctx.Repo.Repository)
+ commits := git_model.ConvertFromGitCommit(ctx, prInfo.Commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits)
@@ -685,37 +719,51 @@ func ViewPullFiles(ctx *context.Context) {
if fileOnly && (len(files) == 2 || len(files) == 1) {
maxLines, maxFiles = -1, -1
}
-
- diff, err := gitdiff.GetDiff(gitRepo,
- &gitdiff.DiffOptions{
- BeforeCommitID: startCommitID,
- AfterCommitID: endCommitID,
- SkipTo: ctx.FormString("skip-to"),
- MaxLines: maxLines,
- MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
- MaxFiles: maxFiles,
- WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
- }, ctx.FormStrings("files")...)
+ diffOptions := &gitdiff.DiffOptions{
+ BeforeCommitID: startCommitID,
+ AfterCommitID: endCommitID,
+ SkipTo: ctx.FormString("skip-to"),
+ MaxLines: maxLines,
+ MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
+ MaxFiles: maxFiles,
+ WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
+ }
+
+ var methodWithError string
+ var diff *gitdiff.Diff
+ if !ctx.IsSigned {
+ diff, err = gitdiff.GetDiff(gitRepo, diffOptions, files...)
+ methodWithError = "GetDiff"
+ } else {
+ diff, err = gitdiff.SyncAndGetUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diffOptions, files...)
+ methodWithError = "SyncAndGetUserSpecificDiff"
+ }
if err != nil {
- ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
+ ctx.ServerError(methodWithError, err)
return
}
+ ctx.PageData["prReview"] = map[string]interface{}{
+ "numberOfFiles": diff.NumFiles,
+ "numberOfViewedFiles": diff.NumViewedFiles,
+ }
+
if err = diff.LoadComments(ctx, issue, ctx.Doer); err != nil {
ctx.ServerError("LoadComments", err)
return
}
- if err = pull.LoadProtectedBranch(); err != nil {
+ pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
+ if err != nil {
ctx.ServerError("LoadProtectedBranch", err)
return
}
- if pull.ProtectedBranch != nil {
- glob := pull.ProtectedBranch.GetProtectedFilePatterns()
+ if pb != nil {
+ glob := pb.GetProtectedFilePatterns()
if len(glob) != 0 {
for _, file := range diff.Files {
- file.IsProtected = pull.ProtectedBranch.IsProtectedFile(glob, file.Name)
+ file.IsProtected = pb.IsProtectedFile(glob, file.Name)
}
}
}
@@ -735,7 +783,7 @@ func ViewPullFiles(ctx *context.Context) {
}
if ctx.IsSigned && ctx.Doer != nil {
- if ctx.Data["CanMarkConversation"], err = models.CanMarkConversation(issue, ctx.Doer); err != nil {
+ if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(issue, ctx.Doer); err != nil {
ctx.ServerError("CanMarkConversation", err)
return
}
@@ -743,9 +791,8 @@ func ViewPullFiles(ctx *context.Context) {
setCompareContext(ctx, baseCommit, commit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
- ctx.Data["RequireHighlightJS"] = true
ctx.Data["RequireTribute"] = true
- if ctx.Data["Assignees"], err = models.GetRepoAssignees(ctx.Repo.Repository); err != nil {
+ if ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository); err != nil {
ctx.ServerError("GetAssignees", err)
return
}
@@ -753,11 +800,27 @@ func ViewPullFiles(ctx *context.Context) {
if ctx.Written() {
return
}
- ctx.Data["CurrentReview"], err = models.GetCurrentReview(ctx.Doer, issue)
- if err != nil && !models.IsErrReviewNotExist(err) {
+
+ currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
+ if err != nil && !issues_model.IsErrReviewNotExist(err) {
ctx.ServerError("GetCurrentReview", err)
return
}
+ numPendingCodeComments := int64(0)
+ if currentReview != nil {
+ numPendingCodeComments, err = issues_model.CountComments(&issues_model.FindCommentsOptions{
+ Type: issues_model.CommentTypeCode,
+ ReviewID: currentReview.ID,
+ IssueID: issue.ID,
+ })
+ if err != nil {
+ ctx.ServerError("CountComments", err)
+ return
+ }
+ }
+ ctx.Data["CurrentReview"] = currentReview
+ ctx.Data["PendingCodeCommentNumber"] = numPendingCodeComments
+
getBranchData(ctx, issue)
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
@@ -785,16 +848,16 @@ func UpdatePullRequest(ctx *context.Context) {
rebase := ctx.FormString("style") == "rebase"
- if err := issue.PullRequest.LoadBaseRepo(); err != nil {
+ if err := issue.PullRequest.LoadBaseRepo(ctx); err != nil {
ctx.ServerError("LoadBaseRepo", err)
return
}
- if err := issue.PullRequest.LoadHeadRepo(); err != nil {
+ if err := issue.PullRequest.LoadHeadRepo(ctx); err != nil {
ctx.ServerError("LoadHeadRepo", err)
return
}
- allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(issue.PullRequest, ctx.Doer)
+ allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, issue.PullRequest, ctx.Doer)
if err != nil {
ctx.ServerError("IsUserAllowedToMerge", err)
return
@@ -866,6 +929,7 @@ func MergePullRequest(ctx *context.Context) {
manuallMerge := repo_model.MergeStyle(form.Do) == repo_model.MergeStyleManuallyMerged
forceMerge := form.ForceMerge != nil && *form.ForceMerge
+ // start with merging by checking
if err := pull_service.CheckPullMergable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, manuallMerge, forceMerge); err != nil {
if errors.Is(err, pull_service.ErrIsClosed) {
if issue.IsPull {
@@ -899,7 +963,6 @@ func MergePullRequest(ctx *context.Context) {
} else {
ctx.ServerError("WebCheck", err)
}
-
return
}
@@ -909,14 +972,12 @@ func MergePullRequest(ctx *context.Context) {
if models.IsErrInvalidMergeStyle(err) {
ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
ctx.Redirect(issue.Link())
- return
} else if strings.Contains(err.Error(), "Wrong commit ID") {
ctx.Flash.Error(ctx.Tr("repo.pulls.wrong_commit_id"))
ctx.Redirect(issue.Link())
- return
+ } else {
+ ctx.ServerError("MergedManually", err)
}
-
- ctx.ServerError("MergedManually", err)
return
}
@@ -924,17 +985,41 @@ func MergePullRequest(ctx *context.Context) {
return
}
- // set defaults to propagate needed fields
- if err := form.SetDefaults(pr); err != nil {
- ctx.ServerError("SetDefaults", fmt.Errorf("SetDefaults: %v", err))
- return
+ message := strings.TrimSpace(form.MergeTitleField)
+ if len(message) == 0 {
+ var err error
+ message, _, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
+ if err != nil {
+ ctx.ServerError("GetDefaultMergeMessage", err)
+ return
+ }
+ }
+
+ form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
+ if len(form.MergeMessageField) > 0 {
+ message += "\n\n" + form.MergeMessageField
}
- if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, form.MergeTitleField); err != nil {
+ if form.MergeWhenChecksSucceed {
+ // delete all scheduled auto merges
+ _ = pull_model.DeleteScheduledAutoMerge(ctx, pr.ID)
+ // schedule auto merge
+ scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message)
+ if err != nil {
+ ctx.ServerError("ScheduleAutoMerge", err)
+ return
+ } else if scheduled {
+ // nothing more to do ...
+ ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_newly_scheduled"))
+ ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, pr.Index))
+ return
+ }
+ }
+
+ if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if models.IsErrInvalidMergeStyle(err) {
ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
ctx.Redirect(issue.Link())
- return
} else if models.IsErrMergeConflicts(err) {
conflictError := err.(models.ErrMergeConflicts)
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]interface{}{
@@ -948,7 +1033,6 @@ func MergePullRequest(ctx *context.Context) {
}
ctx.Flash.Error(flashError)
ctx.Redirect(issue.Link())
- return
} else if models.IsErrRebaseConflicts(err) {
conflictError := err.(models.ErrRebaseConflicts)
flashError, err := ctx.RenderToString(tplAlertDetails, map[string]interface{}{
@@ -962,22 +1046,18 @@ func MergePullRequest(ctx *context.Context) {
}
ctx.Flash.Error(flashError)
ctx.Redirect(issue.Link())
- return
} else if models.IsErrMergeUnrelatedHistories(err) {
log.Debug("MergeUnrelatedHistories error: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories"))
ctx.Redirect(issue.Link())
- return
} else if git.IsErrPushOutOfDate(err) {
log.Debug("MergePushOutOfDate error: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
ctx.Redirect(issue.Link())
- return
} else if models.IsErrSHADoesNotMatch(err) {
log.Debug("MergeHeadOutOfDate error: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pulls.head_out_of_date"))
ctx.Redirect(issue.Link())
- return
} else if git.IsErrPushRejected(err) {
log.Debug("MergePushRejected error: %v", err)
pushrejErr := err.(*git.ErrPushRejected)
@@ -997,11 +1077,12 @@ func MergePullRequest(ctx *context.Context) {
ctx.Flash.Error(flashError)
}
ctx.Redirect(issue.Link())
- return
+ } else {
+ ctx.ServerError("Merge", err)
}
- ctx.ServerError("Merge", err)
return
}
+ log.Trace("Pull request merged: %d", pr.ID)
if err := stopTimerIfAvailable(ctx.Doer, issue); err != nil {
ctx.ServerError("CreateOrStopIssueStopwatch", err)
@@ -1012,7 +1093,7 @@ func MergePullRequest(ctx *context.Context) {
if form.DeleteBranchAfterMerge {
// Don't cleanup when other pr use this branch as head branch
- exist, err := models.HasUnmergedPullRequestsByHeadInfo(pr.HeadRepoID, pr.HeadBranch)
+ exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
if err != nil {
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
return
@@ -1039,9 +1120,29 @@ func MergePullRequest(ctx *context.Context) {
ctx.Redirect(issue.Link())
}
-func stopTimerIfAvailable(user *user_model.User, issue *models.Issue) error {
- if models.StopwatchExists(user.ID, issue.ID) {
- if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil {
+// CancelAutoMergePullRequest cancels a scheduled pr
+func CancelAutoMergePullRequest(ctx *context.Context) {
+ issue := checkPullInfo(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, issue.PullRequest); err != nil {
+ if db.IsErrNotExist(err) {
+ ctx.Flash.Error(ctx.Tr("repo.pulls.auto_merge_not_scheduled"))
+ ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index))
+ return
+ }
+ ctx.ServerError("RemoveScheduledAutoMerge", err)
+ return
+ }
+ ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_canceled_schedule"))
+ ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index))
+}
+
+func stopTimerIfAvailable(user *user_model.User, issue *issues_model.Issue) error {
+ if issues_model.StopwatchExists(user.ID, issue.ID) {
+ if err := issues_model.CreateOrStopIssueStopwatch(user, issue); err != nil {
return err
}
}
@@ -1057,7 +1158,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
ctx.Data["IsDiffCompare"] = true
ctx.Data["IsRepoToolbarCommits"] = true
ctx.Data["RequireTribute"] = true
- ctx.Data["RequireHighlightJS"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
@@ -1121,7 +1221,14 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return
}
- pullIssue := &models.Issue{
+ content := form.Content
+ if filename := ctx.Req.Form.Get("template-file"); filename != "" {
+ if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {
+ content = issue_template.RenderToMarkdown(template, ctx.Req.Form)
+ }
+ }
+
+ pullIssue := &issues_model.Issue{
RepoID: repo.ID,
Repo: repo,
Title: form.Title,
@@ -1129,23 +1236,24 @@ func CompareAndPullRequestPost(ctx *context.Context) {
Poster: ctx.Doer,
MilestoneID: milestoneID,
IsPull: true,
- Content: form.Content,
- }
- pullRequest := &models.PullRequest{
- HeadRepoID: ci.HeadRepo.ID,
- BaseRepoID: repo.ID,
- HeadBranch: ci.HeadBranch,
- BaseBranch: ci.BaseBranch,
- HeadRepo: ci.HeadRepo,
- BaseRepo: repo,
- MergeBase: ci.CompareInfo.MergeBase,
- Type: models.PullRequestGitea,
+ Content: content,
+ }
+ pullRequest := &issues_model.PullRequest{
+ HeadRepoID: ci.HeadRepo.ID,
+ BaseRepoID: repo.ID,
+ HeadBranch: ci.HeadBranch,
+ BaseBranch: ci.BaseBranch,
+ HeadRepo: ci.HeadRepo,
+ BaseRepo: repo,
+ MergeBase: ci.CompareInfo.MergeBase,
+ Type: issues_model.PullRequestGitea,
+ AllowMaintainerEdit: form.AllowMaintainerEdit,
}
// FIXME: check error in the case two people send pull request at almost same time, give nice error prompt
// instead of 500.
if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
- if models.IsErrUserDoesNotHaveAccessToRepo(err) {
+ if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
return
} else if git.IsErrPushRejected(err) {
@@ -1192,7 +1300,7 @@ func CleanUpPullRequest(ctx *context.Context) {
}
// Don't cleanup when there are other PR's that use this branch as head branch.
- exist, err := models.HasUnmergedPullRequestsByHeadInfo(pr.HeadRepoID, pr.HeadBranch)
+ exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
if err != nil {
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
return
@@ -1202,14 +1310,14 @@ func CleanUpPullRequest(ctx *context.Context) {
return
}
- if err := pr.LoadHeadRepo(); err != nil {
+ if err := pr.LoadHeadRepo(ctx); err != nil {
ctx.ServerError("LoadHeadRepo", err)
return
} else if pr.HeadRepo == nil {
// Forked repository has already been deleted
ctx.NotFound("CleanUpPullRequest", nil)
return
- } else if err = pr.LoadBaseRepo(); err != nil {
+ } else if err = pr.LoadBaseRepo(ctx); err != nil {
ctx.ServerError("LoadBaseRepo", err)
return
} else if err = pr.HeadRepo.GetOwner(ctx); err != nil {
@@ -1217,7 +1325,7 @@ func CleanUpPullRequest(ctx *context.Context) {
return
}
- perm, err := models.GetUserRepoPermission(pr.HeadRepo, ctx.Doer)
+ perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
@@ -1286,7 +1394,7 @@ func CleanUpPullRequest(ctx *context.Context) {
deleteBranch(ctx, pr, gitRepo)
}
-func deleteBranch(ctx *context.Context, pr *models.PullRequest, gitRepo *git.Repository) {
+func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) {
fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch
if err := repo_service.DeleteBranch(ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil {
switch {
@@ -1294,7 +1402,7 @@ func deleteBranch(ctx *context.Context, pr *models.PullRequest, gitRepo *git.Rep
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
- case errors.Is(err, repo_service.ErrBranchIsProtected):
+ case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
default:
log.Error("DeleteBranch: %v", err)
@@ -1303,7 +1411,7 @@ func deleteBranch(ctx *context.Context, pr *models.PullRequest, gitRepo *git.Rep
return
}
- if err := models.AddDeletePRBranchComment(ctx.Doer, pr.BaseRepo, pr.IssueID, pr.HeadBranch); err != nil {
+ if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.IssueID, pr.HeadBranch); err != nil {
// Do not fail here as branch has already been deleted
log.Error("DeleteBranch: %v", err)
}
@@ -1323,9 +1431,9 @@ func DownloadPullPatch(ctx *context.Context) {
// DownloadPullDiffOrPatch render a pull's raw diff or patch
func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) {
- pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
- if models.IsErrPullRequestNotExist(err) {
+ if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound("GetPullRequestByIndex", err)
} else {
ctx.ServerError("GetPullRequestByIndex", err)
@@ -1365,18 +1473,18 @@ func UpdatePullRequestTarget(ctx *context.Context) {
}
if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, targetBranch); err != nil {
- if models.IsErrPullRequestAlreadyExists(err) {
- err := err.(models.ErrPullRequestAlreadyExists)
+ if issues_model.IsErrPullRequestAlreadyExists(err) {
+ err := err.(issues_model.ErrPullRequestAlreadyExists)
RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name
- errorMessage := ctx.Tr("repo.pulls.has_pull_request", html.EscapeString(ctx.Repo.RepoLink+"/pulls/"+strconv.FormatInt(err.IssueID, 10)), html.EscapeString(RepoRelPath), err.IssueID) // FIXME: Creates url insidde locale string
+ errorMessage := ctx.Tr("repo.pulls.has_pull_request", html.EscapeString(ctx.Repo.RepoLink+"/pulls/"+strconv.FormatInt(err.IssueID, 10)), html.EscapeString(RepoRelPath), err.IssueID) // FIXME: Creates url inside locale string
ctx.Flash.Error(errorMessage)
ctx.JSON(http.StatusConflict, map[string]interface{}{
"error": err.Error(),
"user_error": errorMessage,
})
- } else if models.IsErrIssueIsClosed(err) {
+ } else if issues_model.IsErrIssueIsClosed(err) {
errorMessage := ctx.Tr("repo.pulls.is_closed")
ctx.Flash.Error(errorMessage)
@@ -1405,9 +1513,37 @@ func UpdatePullRequestTarget(ctx *context.Context) {
}
return
}
- notification.NotifyPullRequestChangeTargetBranch(ctx.Doer, pr, targetBranch)
+ notification.NotifyPullRequestChangeTargetBranch(ctx, ctx.Doer, pr, targetBranch)
ctx.JSON(http.StatusOK, map[string]interface{}{
"base_branch": pr.BaseBranch,
})
}
+
+// SetAllowEdits allow edits from maintainers to PRs
+func SetAllowEdits(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.UpdateAllowEditsForm)
+
+ pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ if err != nil {
+ if issues_model.IsErrPullRequestNotExist(err) {
+ ctx.NotFound("GetPullRequestByIndex", err)
+ } else {
+ ctx.ServerError("GetPullRequestByIndex", err)
+ }
+ return
+ }
+
+ if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, form.AllowMaintainerEdit); err != nil {
+ if errors.Is(pull_service.ErrUserHasNoPermissionForAction, err) {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ ctx.ServerError("SetAllowEdits", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "allow_maintainer_edit": pr.AllowMaintainerEdit,
+ })
+}
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index 939b0037a09e0..9f4cdde864b64 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -1,16 +1,18 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
import (
+ "errors"
"fmt"
"net/http"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
+ pull_model "code.gitea.io/gitea/models/pull"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
@@ -29,8 +31,8 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
if !issue.IsPull {
return
}
- currentReview, err := models.GetCurrentReview(ctx.Doer, issue)
- if err != nil && !models.IsErrReviewNotExist(err) {
+ currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
+ if err != nil && !issues_model.IsErrReviewNotExist(err) {
ctx.ServerError("GetCurrentReview", err)
return
}
@@ -105,19 +107,24 @@ func UpdateResolveConversation(ctx *context.Context) {
action := ctx.FormString("action")
commentID := ctx.FormInt64("comment_id")
- comment, err := models.GetCommentByID(commentID)
+ comment, err := issues_model.GetCommentByID(ctx, commentID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
- if err = comment.LoadIssue(); err != nil {
+ if err = comment.LoadIssue(ctx); err != nil {
ctx.ServerError("comment.LoadIssue", err)
return
}
+ if comment.Issue.RepoID != ctx.Repo.Repository.ID {
+ ctx.NotFound("comment's repoID is incorrect", errors.New("comment's repoID is incorrect"))
+ return
+ }
+
var permResult bool
- if permResult, err = models.CanMarkConversation(comment.Issue, ctx.Doer); err != nil {
+ if permResult, err = issues_model.CanMarkConversation(comment.Issue, ctx.Doer); err != nil {
ctx.ServerError("CanMarkConversation", err)
return
}
@@ -132,7 +139,7 @@ func UpdateResolveConversation(ctx *context.Context) {
}
if action == "Resolve" || action == "UnResolve" {
- err = models.MarkConversation(comment, ctx.Doer, action == "Resolve")
+ err = issues_model.MarkConversation(comment, ctx.Doer, action == "Resolve")
if err != nil {
ctx.ServerError("MarkConversation", err)
return
@@ -151,8 +158,8 @@ func UpdateResolveConversation(ctx *context.Context) {
})
}
-func renderConversation(ctx *context.Context, comment *models.Comment) {
- comments, err := models.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line)
+func renderConversation(ctx *context.Context, comment *issues_model.Comment) {
+ comments, err := issues_model.FetchCodeCommentsByLine(ctx, comment.Issue, ctx.Doer, comment.TreePath, comment.Line)
if err != nil {
ctx.ServerError("FetchCodeCommentsByLine", err)
return
@@ -161,7 +168,7 @@ func renderConversation(ctx *context.Context, comment *models.Comment) {
ctx.Data["comments"] = comments
ctx.Data["CanMarkConversation"] = true
ctx.Data["Issue"] = comment.Issue
- if err = comment.Issue.LoadPullRequest(); err != nil {
+ if err = comment.Issue.LoadPullRequest(ctx); err != nil {
ctx.ServerError("comment.Issue.LoadPullRequest", err)
return
}
@@ -192,15 +199,15 @@ func SubmitReview(ctx *context.Context) {
reviewType := form.ReviewType()
switch reviewType {
- case models.ReviewTypeUnknown:
+ case issues_model.ReviewTypeUnknown:
ctx.ServerError("ReviewType", fmt.Errorf("unknown ReviewType: %s", form.Type))
return
// can not approve/reject your own PR
- case models.ReviewTypeApprove, models.ReviewTypeReject:
+ case issues_model.ReviewTypeApprove, issues_model.ReviewTypeReject:
if issue.IsPoster(ctx.Doer.ID) {
var translated string
- if reviewType == models.ReviewTypeApprove {
+ if reviewType == issues_model.ReviewTypeApprove {
translated = ctx.Tr("repo.issues.review.self.approval")
} else {
translated = ctx.Tr("repo.issues.review.self.rejection")
@@ -219,7 +226,7 @@ func SubmitReview(ctx *context.Context) {
_, comm, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, issue, reviewType, form.Content, form.CommitID, attachments)
if err != nil {
- if models.IsContentEmptyErr(err) {
+ if issues_model.IsContentEmptyErr(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))
ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
} else {
@@ -234,7 +241,7 @@ func SubmitReview(ctx *context.Context) {
// DismissReview dismissing stale review by repo admin
func DismissReview(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.DismissReviewForm)
- comm, err := pull_service.DismissReview(ctx, form.ReviewID, form.Message, ctx.Doer, true)
+ comm, err := pull_service.DismissReview(ctx, form.ReviewID, ctx.Repo.Repository.ID, form.Message, ctx.Doer, true, true)
if err != nil {
ctx.ServerError("pull_service.DismissReview", err)
return
@@ -242,3 +249,47 @@ func DismissReview(ctx *context.Context) {
ctx.Redirect(fmt.Sprintf("%s/pulls/%d#%s", ctx.Repo.RepoLink, comm.Issue.Index, comm.HashTag()))
}
+
+// viewedFilesUpdate Struct to parse the body of a request to update the reviewed files of a PR
+// If you want to implement an API to update the review, simply move this struct into modules.
+type viewedFilesUpdate struct {
+ Files map[string]bool `json:"files"`
+ HeadCommitSHA string `json:"headCommitSHA"`
+}
+
+func UpdateViewedFiles(ctx *context.Context) {
+ // Find corresponding PR
+ issue := checkPullInfo(ctx)
+ if ctx.Written() {
+ return
+ }
+ pull := issue.PullRequest
+
+ var data *viewedFilesUpdate
+ err := json.NewDecoder(ctx.Req.Body).Decode(&data)
+ if err != nil {
+ log.Warn("Attempted to update a review but could not parse request body: %v", err)
+ ctx.Resp.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ // Expect the review to have been now if no head commit was supplied
+ if data.HeadCommitSHA == "" {
+ data.HeadCommitSHA = pull.HeadCommitID
+ }
+
+ updatedFiles := make(map[string]pull_model.ViewedState, len(data.Files))
+ for file, viewed := range data.Files {
+
+ // Only unviewed and viewed are possible, has-changed can not be set from the outside
+ state := pull_model.Unviewed
+ if viewed {
+ state = pull_model.Viewed
+ }
+ updatedFiles[file] = state
+ }
+
+ if err := pull_model.UpdateReviewState(ctx, ctx.Doer.ID, pull.ID, data.HeadCommitSHA, updatedFiles); err != nil {
+ ctx.ServerError("UpdateReview", err)
+ }
+}
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index 5f894ae50128a..54f503642bc90 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -12,6 +11,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
@@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/web/feed"
"code.gitea.io/gitea/services/forms"
releaseservice "code.gitea.io/gitea/services/release"
)
@@ -33,7 +34,7 @@ const (
)
// calReleaseNumCommitsBehind calculates given release has how many commits behind release target.
-func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *models.Release, countCache map[string]int64) error {
+func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model.Release, countCache map[string]int64) error {
// Fast return if release target is same as default branch.
if repoCtx.BranchName == release.Target {
release.NumCommitsBehind = repoCtx.CommitsCount - release.NumCommits
@@ -45,11 +46,11 @@ func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *models.Rel
if repoCtx.GitRepo.IsBranchExist(release.Target) {
commit, err := repoCtx.GitRepo.GetBranchCommit(release.Target)
if err != nil {
- return fmt.Errorf("GetBranchCommit: %v", err)
+ return fmt.Errorf("GetBranchCommit: %w", err)
}
countCache[release.Target], err = commit.CommitsCount()
if err != nil {
- return fmt.Errorf("CommitsCount: %v", err)
+ return fmt.Errorf("CommitsCount: %w", err)
}
} else {
// Use NumCommits of the newest release on that target
@@ -98,7 +99,14 @@ func releasesOrTags(ctx *context.Context, isTagList bool) {
listOptions.PageSize = setting.API.MaxResponseItems
}
- tags, err := ctx.Repo.GitRepo.GetTags(listOptions.GetStartEnd())
+ // TODO(20073) tags are used for compare feature which needs all tags
+ // filtering is done on the client-side atm
+ tagListStart, tagListEnd := 0, 0
+ if isTagList {
+ tagListStart, tagListEnd = listOptions.GetStartEnd()
+ }
+
+ tags, err := ctx.Repo.GitRepo.GetTags(tagListStart, tagListEnd)
if err != nil {
ctx.ServerError("GetTags", err)
return
@@ -108,25 +116,33 @@ func releasesOrTags(ctx *context.Context, isTagList bool) {
writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
- opts := models.FindReleasesOptions{
- ListOptions: listOptions,
- IncludeDrafts: writeAccess && !isTagList,
- IncludeTags: isTagList,
+ opts := repo_model.FindReleasesOptions{
+ ListOptions: listOptions,
+ }
+ if isTagList {
+ // for the tags list page, show all releases with real tags (having real commit-id),
+ // the drafts should also be included because a real tag might be used as a draft.
+ opts.IncludeDrafts = true
+ opts.IncludeTags = true
+ opts.HasSha1 = util.OptionalBoolTrue
+ } else {
+ // only show draft releases for users who can write, read-only users shouldn't see draft releases.
+ opts.IncludeDrafts = writeAccess
}
- releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, opts)
+ releases, err := repo_model.GetReleasesByRepoID(ctx, ctx.Repo.Repository.ID, opts)
if err != nil {
ctx.ServerError("GetReleasesByRepoID", err)
return
}
- count, err := models.GetReleaseCountByRepoID(ctx.Repo.Repository.ID, opts)
+ count, err := repo_model.GetReleaseCountByRepoID(ctx, ctx.Repo.Repository.ID, opts)
if err != nil {
ctx.ServerError("GetReleaseCountByRepoID", err)
return
}
- if err = models.GetReleaseAttachments(releases...); err != nil {
+ if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
ctx.ServerError("GetReleaseAttachments", err)
return
}
@@ -141,7 +157,7 @@ func releasesOrTags(ctx *context.Context, isTagList bool) {
for _, r := range releases {
if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
- r.Publisher, err = user_model.GetUserByID(r.PublisherID)
+ r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
r.Publisher = user_model.NewGhostUser()
@@ -184,6 +200,30 @@ func releasesOrTags(ctx *context.Context, isTagList bool) {
ctx.HTML(http.StatusOK, tplReleases)
}
+// ReleasesFeedRSS get feeds for releases in RSS format
+func ReleasesFeedRSS(ctx *context.Context) {
+ releasesOrTagsFeed(ctx, true, "rss")
+}
+
+// TagsListFeedRSS get feeds for tags in RSS format
+func TagsListFeedRSS(ctx *context.Context) {
+ releasesOrTagsFeed(ctx, false, "rss")
+}
+
+// ReleasesFeedAtom get feeds for releases in Atom format
+func ReleasesFeedAtom(ctx *context.Context) {
+ releasesOrTagsFeed(ctx, true, "atom")
+}
+
+// TagsListFeedAtom get feeds for tags in RSS format
+func TagsListFeedAtom(ctx *context.Context) {
+ releasesOrTagsFeed(ctx, false, "atom")
+}
+
+func releasesOrTagsFeed(ctx *context.Context, isReleasesOnly bool, formatType string) {
+ feed.ShowReleaseFeed(ctx, ctx.Repo.Repository, isReleasesOnly, formatType)
+}
+
// SingleRelease renders a single release's page
func SingleRelease(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.release.releases")
@@ -192,9 +232,9 @@ func SingleRelease(ctx *context.Context) {
writeAccess := ctx.Repo.CanWrite(unit.TypeReleases)
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
- release, err := models.GetRelease(ctx.Repo.Repository.ID, ctx.Params("*"))
+ release, err := repo_model.GetRelease(ctx.Repo.Repository.ID, ctx.Params("*"))
if err != nil {
- if models.IsErrReleaseNotExist(err) {
+ if repo_model.IsErrReleaseNotExist(err) {
ctx.NotFound("GetRelease", err)
return
}
@@ -202,13 +242,13 @@ func SingleRelease(ctx *context.Context) {
return
}
- err = models.GetReleaseAttachments(release)
+ err = repo_model.GetReleaseAttachments(ctx, release)
if err != nil {
ctx.ServerError("GetReleaseAttachments", err)
return
}
- release.Publisher, err = user_model.GetUserByID(release.PublisherID)
+ release.Publisher, err = user_model.GetUserByID(ctx, release.PublisherID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
release.Publisher = user_model.NewGhostUser()
@@ -234,15 +274,15 @@ func SingleRelease(ctx *context.Context) {
return
}
- ctx.Data["Releases"] = []*models.Release{release}
+ ctx.Data["Releases"] = []*repo_model.Release{release}
ctx.HTML(http.StatusOK, tplReleases)
}
// LatestRelease redirects to the latest release
func LatestRelease(ctx *context.Context) {
- release, err := models.GetLatestReleaseByRepoID(ctx.Repo.Repository.ID)
+ release, err := repo_model.GetLatestReleaseByRepoID(ctx.Repo.Repository.ID)
if err != nil {
- if models.IsErrReleaseNotExist(err) {
+ if repo_model.IsErrReleaseNotExist(err) {
ctx.NotFound("LatestRelease", err)
return
}
@@ -250,7 +290,7 @@ func LatestRelease(ctx *context.Context) {
return
}
- if err := release.LoadAttributes(); err != nil {
+ if err := release.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
@@ -265,21 +305,23 @@ func NewRelease(ctx *context.Context) {
ctx.Data["RequireTribute"] = true
ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch
if tagName := ctx.FormString("tag"); len(tagName) > 0 {
- rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
- if err != nil && !models.IsErrReleaseNotExist(err) {
+ rel, err := repo_model.GetRelease(ctx.Repo.Repository.ID, tagName)
+ if err != nil && !repo_model.IsErrReleaseNotExist(err) {
ctx.ServerError("GetRelease", err)
return
}
if rel != nil {
rel.Repo = ctx.Repo.Repository
- if err := rel.LoadAttributes(); err != nil {
+ if err := rel.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
ctx.Data["tag_name"] = rel.TagName
- ctx.Data["tag_target"] = rel.Target
+ if rel.Target != "" {
+ ctx.Data["tag_target"] = rel.Target
+ }
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["attachments"] = rel.Attachments
@@ -312,9 +354,9 @@ func NewReleasePost(ctx *context.Context) {
attachmentUUIDs = form.Files
}
- rel, err := models.GetRelease(ctx.Repo.Repository.ID, form.TagName)
+ rel, err := repo_model.GetRelease(ctx.Repo.Repository.ID, form.TagName)
if err != nil {
- if !models.IsErrReleaseNotExist(err) {
+ if !repo_model.IsErrReleaseNotExist(err) {
ctx.ServerError("GetRelease", err)
return
}
@@ -354,7 +396,7 @@ func NewReleasePost(ctx *context.Context) {
return
}
- rel = &models.Release{
+ rel = &repo_model.Release{
RepoID: ctx.Repo.Repository.ID,
Repo: ctx.Repo.Repository,
PublisherID: ctx.Doer.ID,
@@ -371,7 +413,7 @@ func NewReleasePost(ctx *context.Context) {
if err = releaseservice.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, msg); err != nil {
ctx.Data["Err_TagName"] = true
switch {
- case models.IsErrReleaseAlreadyExist(err):
+ case repo_model.IsErrReleaseAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
case models.IsErrInvalidTagName(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
@@ -418,9 +460,9 @@ func EditRelease(ctx *context.Context) {
upload.AddUploadContext(ctx, "release")
tagName := ctx.Params("*")
- rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
+ rel, err := repo_model.GetRelease(ctx.Repo.Repository.ID, tagName)
if err != nil {
- if models.IsErrReleaseNotExist(err) {
+ if repo_model.IsErrReleaseNotExist(err) {
ctx.NotFound("GetRelease", err)
} else {
ctx.ServerError("GetRelease", err)
@@ -436,7 +478,7 @@ func EditRelease(ctx *context.Context) {
ctx.Data["IsDraft"] = rel.IsDraft
rel.Repo = ctx.Repo.Repository
- if err := rel.LoadAttributes(); err != nil {
+ if err := rel.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
@@ -454,9 +496,9 @@ func EditReleasePost(ctx *context.Context) {
ctx.Data["RequireTribute"] = true
tagName := ctx.Params("*")
- rel, err := models.GetRelease(ctx.Repo.Repository.ID, tagName)
+ rel, err := repo_model.GetRelease(ctx.Repo.Repository.ID, tagName)
if err != nil {
- if models.IsErrReleaseNotExist(err) {
+ if repo_model.IsErrReleaseNotExist(err) {
ctx.NotFound("GetRelease", err)
} else {
ctx.ServerError("GetRelease", err)
@@ -505,19 +547,23 @@ func EditReleasePost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
}
-// DeleteRelease delete a release
+// DeleteRelease deletes a release
func DeleteRelease(ctx *context.Context) {
deleteReleaseOrTag(ctx, false)
}
-// DeleteTag delete a tag
+// DeleteTag deletes a tag
func DeleteTag(ctx *context.Context) {
deleteReleaseOrTag(ctx, true)
}
func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
if err := releaseservice.DeleteReleaseByID(ctx, ctx.FormInt64("id"), ctx.Doer, isDelTag); err != nil {
- ctx.Flash.Error("DeleteReleaseByID: " + err.Error())
+ if models.IsErrProtectedTagName(err) {
+ ctx.Flash.Error(ctx.Tr("repo.release.tag_name_protected"))
+ } else {
+ ctx.Flash.Error("DeleteReleaseByID: " + err.Error())
+ }
} else {
if isDelTag {
ctx.Flash.Success(ctx.Tr("repo.release.deletion_tag_success"))
diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go
index 33cf54cdc96e5..81ae58178faea 100644
--- a/routers/web/repo/release_test.go
+++ b/routers/web/repo/release_test.go
@@ -1,13 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
import (
"testing"
- "code.gitea.io/gitea/models"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
@@ -52,7 +51,7 @@ func TestNewReleasePost(t *testing.T) {
test.LoadGitRepo(t, ctx)
web.SetForm(ctx, &testCase.Form)
NewReleasePost(ctx)
- unittest.AssertExistsAndLoadBean(t, &models.Release{
+ unittest.AssertExistsAndLoadBean(t, &repo_model.Release{
RepoID: 1,
PublisherID: 2,
TagName: testCase.Form.TagName,
diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go
new file mode 100644
index 0000000000000..f07b4e8c113c4
--- /dev/null
+++ b/routers/web/repo/render.go
@@ -0,0 +1,78 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "bytes"
+ "io"
+ "net/http"
+ "path"
+
+ "code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/typesniffer"
+ "code.gitea.io/gitea/modules/util"
+)
+
+// RenderFile renders a file by repos path
+func RenderFile(ctx *context.Context) {
+ blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath)
+ if err != nil {
+ if git.IsErrNotExist(err) {
+ ctx.NotFound("GetBlobByPath", err)
+ } else {
+ ctx.ServerError("GetBlobByPath", err)
+ }
+ return
+ }
+
+ dataRc, err := blob.DataAsync()
+ if err != nil {
+ ctx.ServerError("DataAsync", err)
+ return
+ }
+ defer dataRc.Close()
+
+ buf := make([]byte, 1024)
+ n, _ := util.ReadAtMost(dataRc, buf)
+ buf = buf[:n]
+
+ st := typesniffer.DetectContentType(buf)
+ isTextFile := st.IsText()
+
+ rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
+
+ if markupType := markup.Type(blob.Name()); markupType == "" {
+ if isTextFile {
+ _, err = io.Copy(ctx.Resp, rd)
+ if err != nil {
+ ctx.ServerError("Copy", err)
+ }
+ return
+ }
+ ctx.Error(http.StatusInternalServerError, "Unsupported file type render")
+ return
+ }
+
+ treeLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
+ if ctx.Repo.TreePath != "" {
+ treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
+ }
+
+ ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
+ err = markup.Render(&markup.RenderContext{
+ Ctx: ctx,
+ RelativePath: ctx.Repo.TreePath,
+ URLPrefix: path.Dir(treeLink),
+ Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
+ GitRepo: ctx.Repo.GitRepo,
+ InStandalonePage: true,
+ }, rd, ctx.Resp)
+ if err != nil {
+ ctx.ServerError("Render", err)
+ return
+ }
+}
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 60298121dfe4b..f9c67f170bec2 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
@@ -10,18 +9,16 @@ import (
"fmt"
"net/http"
"strings"
- "time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
- "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
@@ -29,6 +26,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
repo_service "code.gitea.io/gitea/services/repository"
archiver_service "code.gitea.io/gitea/services/repository/archiver"
@@ -85,13 +83,13 @@ func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
return ctx.Doer
}
- org, err := user_model.GetUserByID(uid)
+ org, err := user_model.GetUserByID(ctx, uid)
if user_model.IsErrUserNotExist(err) {
return ctx.Doer
}
if err != nil {
- ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %v", uid, err))
+ ctx.ServerError("GetUserByID", fmt.Errorf("[%d]: %w", uid, err))
return nil
}
@@ -151,8 +149,8 @@ func Create(ctx *context.Context) {
ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select")
templateID := ctx.FormInt64("template_id")
if templateID > 0 {
- templateRepo, err := repo_model.GetRepositoryByID(templateID)
- if err == nil && models.CheckRepoUnitUser(templateRepo, ctxUser, unit.TypeCode) {
+ templateRepo, err := repo_model.GetRepositoryByID(ctx, templateID)
+ if err == nil && access_model.CheckRepoUnitUser(ctx, templateRepo, ctxUser, unit.TypeCode) {
ctx.Data["repo_template"] = templateID
ctx.Data["repo_template_name"] = templateRepo.Name
}
@@ -223,7 +221,7 @@ func CreatePost(ctx *context.Context) {
var repo *repo_model.Repository
var err error
if form.RepoTemplate > 0 {
- opts := models.GenerateRepoOptions{
+ opts := repo_module.GenerateRepoOptions{
Name: form.RepoName,
Description: form.Description,
Private: form.Private,
@@ -257,7 +255,7 @@ func CreatePost(ctx *context.Context) {
return
}
} else {
- repo, err = repo_service.CreateRepository(ctx.Doer, ctxUser, models.CreateRepoOptions{
+ repo, err = repo_service.CreateRepository(ctx.Doer, ctxUser, repo_module.CreateRepoOptions{
Name: form.RepoName,
Description: form.Description,
Gitignores: form.Gitignores,
@@ -285,9 +283,9 @@ func Action(ctx *context.Context) {
var err error
switch ctx.Params(":action") {
case "watch":
- err = repo_model.WatchRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, true)
+ err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, true)
case "unwatch":
- err = repo_model.WatchRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, false)
+ err = repo_model.WatchRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID, false)
case "star":
err = repo_model.StarRepo(ctx.Doer.ID, ctx.Repo.Repository.ID, true)
case "unstar":
@@ -304,7 +302,7 @@ func Action(ctx *context.Context) {
ctx.Repo.Repository.Description = ctx.FormString("desc")
ctx.Repo.Repository.Website = ctx.FormString("site")
- err = models.UpdateRepository(ctx.Repo.Repository, false)
+ err = repo_service.UpdateRepository(ctx.Repo.Repository, false)
}
if err != nil {
@@ -316,12 +314,12 @@ func Action(ctx *context.Context) {
}
func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
- repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
+ repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
if err != nil {
return err
}
- if err := repoTransfer.LoadAttributes(); err != nil {
+ if err := repoTransfer.LoadAttributes(ctx); err != nil {
return err
}
@@ -335,7 +333,7 @@ func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
ctx.Repo.GitRepo = nil
}
- if err := repo_service.TransferOwnership(repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
+ if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
return err
}
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
@@ -358,7 +356,7 @@ func RedirectDownload(ctx *context.Context) {
)
tagNames := []string{vTag}
curRepo := ctx.Repo.Repository
- releases, err := models.GetReleasesByRepoIDAndNames(ctx, curRepo.ID, tagNames)
+ releases, err := repo_model.GetReleasesByRepoIDAndNames(ctx, curRepo.ID, tagNames)
if err != nil {
if repo_model.IsErrAttachmentNotExist(err) {
ctx.Error(http.StatusNotFound)
@@ -369,7 +367,7 @@ func RedirectDownload(ctx *context.Context) {
}
if len(releases) == 1 {
release := releases[0]
- att, err := repo_model.GetAttachmentByReleaseIDFileName(release.ID, fileName)
+ att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName)
if err != nil {
ctx.Error(http.StatusNotFound)
return
@@ -389,68 +387,27 @@ func Download(ctx *context.Context) {
if err != nil {
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
ctx.Error(http.StatusBadRequest, err.Error())
+ } else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
+ ctx.Error(http.StatusNotFound, err.Error())
} else {
ctx.ServerError("archiver_service.NewRequest", err)
}
return
}
- if aReq == nil {
- ctx.Error(http.StatusNotFound)
- return
- }
- archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
+ archiver, err := aReq.Await(ctx)
if err != nil {
- ctx.ServerError("models.GetRepoArchiver", err)
- return
- }
- if archiver != nil && archiver.Status == repo_model.ArchiverReady {
- download(ctx, aReq.GetArchiveName(), archiver)
+ ctx.ServerError("archiver.Await", err)
return
}
- if err := archiver_service.StartArchive(aReq); err != nil {
- ctx.ServerError("archiver_service.StartArchive", err)
- return
- }
-
- var times int
- t := time.NewTicker(time.Second * 1)
- defer t.Stop()
-
- for {
- select {
- case <-graceful.GetManager().HammerContext().Done():
- log.Warn("exit archive download because system stop")
- return
- case <-t.C:
- if times > 20 {
- ctx.ServerError("wait download timeout", nil)
- return
- }
- times++
- archiver, err = repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
- if err != nil {
- ctx.ServerError("archiver_service.StartArchive", err)
- return
- }
- if archiver != nil && archiver.Status == repo_model.ArchiverReady {
- download(ctx, aReq.GetArchiveName(), archiver)
- return
- }
- }
- }
+ download(ctx, aReq.GetArchiveName(), archiver)
}
func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) {
downloadName := ctx.Repo.Repository.Name + "-" + archiveName
- rPath, err := archiver.RelativePath()
- if err != nil {
- ctx.ServerError("archiver.RelativePath", err)
- return
- }
-
+ rPath := archiver.RelativePath()
if setting.RepoArchive.ServeDirect {
// If we have a signed url (S3, object storage), redirect to this directly.
u, err := storage.RepoArchives.URL(rPath, downloadName)
@@ -467,7 +424,11 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep
return
}
defer fr.Close()
- ctx.ServeStream(fr, downloadName)
+
+ ctx.ServeContent(fr, &context.ServeHeaderOptions{
+ Filename: downloadName,
+ LastModified: archiver.CreatedUnix.AsLocalTime(),
+ })
}
// InitiateDownload will enqueue an archival request, as needed. It may submit
@@ -509,7 +470,7 @@ func InitiateDownload(ctx *context.Context) {
// SearchRepo repositories via options
func SearchRepo(ctx *context.Context) {
- opts := &models.SearchRepoOptions{
+ opts := &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: ctx.FormInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
@@ -581,7 +542,7 @@ func SearchRepo(ctx *context.Context) {
}
var err error
- repos, count, err := models.SearchRepository(opts)
+ repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
ctx.JSON(http.StatusInternalServerError, api.SearchError{
OK: false,
@@ -590,26 +551,28 @@ func SearchRepo(ctx *context.Context) {
return
}
+ ctx.SetTotalCountHeader(count)
+
+ // To improve performance when only the count is requested
+ if ctx.FormBool("count_only") {
+ return
+ }
+
results := make([]*api.Repository, len(repos))
for i, repo := range repos {
- if err = repo.GetOwner(ctx); err != nil {
- ctx.JSON(http.StatusInternalServerError, api.SearchError{
- OK: false,
- Error: err.Error(),
- })
- return
- }
- accessMode, err := models.AccessLevel(ctx.Doer, repo)
- if err != nil {
- ctx.JSON(http.StatusInternalServerError, api.SearchError{
- OK: false,
- Error: err.Error(),
- })
+ results[i] = &api.Repository{
+ ID: repo.ID,
+ FullName: repo.FullName(),
+ Fork: repo.IsFork,
+ Private: repo.IsPrivate,
+ Template: repo.IsTemplate,
+ Mirror: repo.IsMirror,
+ Stars: repo.NumStars,
+ HTMLURL: repo.HTMLURL(),
+ Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
}
- results[i] = convert.ToRepo(repo, accessMode)
}
- ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, api.SearchResults{
OK: true,
Data: results,
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index c230e88d2d90f..137f38d40913a 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -21,14 +20,27 @@ func Search(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink)
return
}
+
language := ctx.FormTrim("l")
keyword := ctx.FormTrim("q")
+
+ queryType := ctx.FormTrim("t")
+ isMatch := queryType == "match"
+
+ ctx.Data["Keyword"] = keyword
+ ctx.Data["Language"] = language
+ ctx.Data["queryType"] = queryType
+ ctx.Data["PageIsViewCode"] = true
+
+ if keyword == "" {
+ ctx.HTML(http.StatusOK, tplSearch)
+ return
+ }
+
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}
- queryType := ctx.FormTrim("t")
- isMatch := queryType == "match"
total, searchResults, searchResultLanguages, err := code_indexer.PerformSearch(ctx, []int64{ctx.Repo.Repository.ID},
language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
@@ -41,14 +53,10 @@ func Search(ctx *context.Context) {
} else {
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable()
}
- ctx.Data["Keyword"] = keyword
- ctx.Data["Language"] = language
- ctx.Data["queryType"] = queryType
+
ctx.Data["SourcePath"] = ctx.Repo.Repository.HTMLURL()
ctx.Data["SearchResults"] = searchResults
ctx.Data["SearchResultLanguages"] = searchResultLanguages
- ctx.Data["RequireHighlightJS"] = true
- ctx.Data["PageIsViewCode"] = true
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go
index ef99eee15a258..43a615abfe991 100644
--- a/routers/web/repo/setting.go
+++ b/routers/web/repo/setting.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -20,6 +19,7 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
+ secret_model "code.gitea.io/gitea/models/secret"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
@@ -29,10 +29,10 @@ import (
"code.gitea.io/gitea/modules/indexer/stats"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/repository"
+ mirror_module "code.gitea.io/gitea/modules/mirror"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
@@ -43,6 +43,7 @@ import (
"code.gitea.io/gitea/services/mailer"
"code.gitea.io/gitea/services/migrations"
mirror_service "code.gitea.io/gitea/services/mirror"
+ org_service "code.gitea.io/gitea/services/org"
repo_service "code.gitea.io/gitea/services/repository"
wiki_service "code.gitea.io/gitea/services/wiki"
)
@@ -55,53 +56,67 @@ const (
tplGithooks base.TplName = "repo/settings/githooks"
tplGithookEdit base.TplName = "repo/settings/githook_edit"
tplDeployKeys base.TplName = "repo/settings/deploy_keys"
- tplProtectedBranch base.TplName = "repo/settings/protected_branch"
)
-// Settings show a repository's settings page
-func Settings(ctx *context.Context) {
+// SettingsCtxData is a middleware that sets all the general context data for the
+// settings template.
+func SettingsCtxData(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsOptions"] = true
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
+ ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
ctx.Data["SigningKeyAvailable"] = len(signing) > 0
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["CodeIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+
if ctx.Doer.IsAdmin {
if setting.Indexer.RepoIndexerEnabled {
- status, err := repo_model.GetIndexerStatus(ctx.Repo.Repository, repo_model.RepoIndexerTypeCode)
+ status, err := repo_model.GetIndexerStatus(ctx, ctx.Repo.Repository, repo_model.RepoIndexerTypeCode)
if err != nil {
ctx.ServerError("repo.indexer_status", err)
return
}
ctx.Data["CodeIndexerStatus"] = status
}
- status, err := repo_model.GetIndexerStatus(ctx.Repo.Repository, repo_model.RepoIndexerTypeStats)
+ status, err := repo_model.GetIndexerStatus(ctx, ctx.Repo.Repository, repo_model.RepoIndexerTypeStats)
if err != nil {
ctx.ServerError("repo.indexer_status", err)
return
}
ctx.Data["StatsIndexerStatus"] = status
}
- pushMirrors, err := repo_model.GetPushMirrorsByRepoID(ctx.Repo.Repository.ID)
+ pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
if err != nil {
ctx.ServerError("GetPushMirrorsByRepoID", err)
return
}
ctx.Data["PushMirrors"] = pushMirrors
+}
+// Settings show a repository's settings page
+func Settings(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplSettingsOptions)
}
// SettingsPost response for changes of a repository
func SettingsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsOptions"] = true
+
+ ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
+ ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
+ ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
+ ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
+ ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
+
+ signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
+ ctx.Data["SigningKeyAvailable"] = len(signing) > 0
+ ctx.Data["SigningSettings"] = setting.Repository.Signing
+ ctx.Data["CodeIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
repo := ctx.Repo.Repository
@@ -169,7 +184,7 @@ func SettingsPost(ctx *context.Context) {
}
repo.IsPrivate = form.Private
- if err := models.UpdateRepository(repo, visibilityChanged); err != nil {
+ if err := repo_service.UpdateRepository(repo, visibilityChanged); err != nil {
ctx.ServerError("UpdateRepository", err)
return
}
@@ -192,22 +207,23 @@ func SettingsPost(ctx *context.Context) {
if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
ctx.Data["Err_Interval"] = true
ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
- } else {
- ctx.Repo.Mirror.EnablePrune = form.EnablePrune
- ctx.Repo.Mirror.Interval = interval
- if interval != 0 {
- ctx.Repo.Mirror.NextUpdateUnix = timeutil.TimeStampNow().AddDuration(interval)
- } else {
- ctx.Repo.Mirror.NextUpdateUnix = 0
- }
- if err := repo_model.UpdateMirror(ctx.Repo.Mirror); err != nil {
- ctx.Data["Err_Interval"] = true
- ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
- return
- }
+ return
+ }
+
+ ctx.Repo.Mirror.EnablePrune = form.EnablePrune
+ ctx.Repo.Mirror.Interval = interval
+ ctx.Repo.Mirror.ScheduleNextUpdate()
+ if err := repo_model.UpdateMirror(ctx, ctx.Repo.Mirror); err != nil {
+ ctx.ServerError("UpdateMirror", err)
+ return
}
- u, _ := git.GetRemoteAddress(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Mirror.GetRemoteName())
+ u, err := git.GetRemoteURL(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Mirror.GetRemoteName())
+ if err != nil {
+ ctx.Data["Err_MirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() {
form.MirrorPassword, _ = u.User.Password()
}
@@ -246,7 +262,7 @@ func SettingsPost(ctx *context.Context) {
ctx.Repo.Mirror.LFS = form.LFS
ctx.Repo.Mirror.LFSEndpoint = form.LFSEndpoint
- if err := repo_model.UpdateMirror(ctx.Repo.Mirror); err != nil {
+ if err := repo_model.UpdateMirror(ctx, ctx.Repo.Mirror); err != nil {
ctx.ServerError("UpdateMirror", err)
return
}
@@ -260,7 +276,7 @@ func SettingsPost(ctx *context.Context) {
return
}
- mirror_service.StartToMirror(repo.ID)
+ mirror_module.AddPullMirrorToQueue(repo.ID)
ctx.Flash.Info(ctx.Tr("repo.settings.mirror_sync_in_progress"))
ctx.Redirect(repo.Link() + "/settings")
@@ -271,13 +287,13 @@ func SettingsPost(ctx *context.Context) {
return
}
- m, err := selectPushMirrorByForm(form, repo)
+ m, err := selectPushMirrorByForm(ctx, form, repo)
if err != nil {
ctx.NotFound("", nil)
return
}
- mirror_service.AddPushMirrorToQueue(m.ID)
+ mirror_module.AddPushMirrorToQueue(m.ID)
ctx.Flash.Info(ctx.Tr("repo.settings.mirror_sync_in_progress"))
ctx.Redirect(repo.Link() + "/settings")
@@ -292,7 +308,7 @@ func SettingsPost(ctx *context.Context) {
// as an error on the UI for this action
ctx.Data["Err_RepoName"] = nil
- m, err := selectPushMirrorByForm(form, repo)
+ m, err := selectPushMirrorByForm(ctx, form, repo)
if err != nil {
ctx.NotFound("", nil)
return
@@ -303,7 +319,7 @@ func SettingsPost(ctx *context.Context) {
return
}
- if err = repo_model.DeletePushMirrorByID(m.ID); err != nil {
+ if err = repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
ctx.ServerError("DeletePushMirrorByID", err)
return
}
@@ -345,19 +361,20 @@ func SettingsPost(ctx *context.Context) {
}
m := &repo_model.PushMirror{
- RepoID: repo.ID,
- Repo: repo,
- RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix),
- Interval: interval,
+ RepoID: repo.ID,
+ Repo: repo,
+ RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix),
+ SyncOnCommit: form.PushMirrorSyncOnCommit,
+ Interval: interval,
}
- if err := repo_model.InsertPushMirror(m); err != nil {
+ if err := repo_model.InsertPushMirror(ctx, m); err != nil {
ctx.ServerError("InsertPushMirror", err)
return
}
if err := mirror_service.AddPushMirrorRemote(ctx, m, address); err != nil {
- if err := repo_model.DeletePushMirrorByID(m.ID); err != nil {
- log.Error("DeletePushMirrorByID %v", err)
+ if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
+ log.Error("DeletePushMirrors %v", err)
}
ctx.ServerError("AddPushMirrorRemote", err)
return
@@ -380,6 +397,15 @@ func SettingsPost(ctx *context.Context) {
repoChanged = true
}
+ if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
+ units = append(units, repo_model.RepoUnit{
+ RepoID: repo.ID,
+ Type: unit_model.TypeCode,
+ })
+ } else if !unit_model.TypeCode.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
+ }
+
if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
if !validation.IsValidExternalURL(form.ExternalWikiURL) {
ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
@@ -426,9 +452,10 @@ func SettingsPost(ctx *context.Context) {
RepoID: repo.ID,
Type: unit_model.TypeExternalTracker,
Config: &repo_model.ExternalTrackerConfig{
- ExternalTrackerURL: form.ExternalTrackerURL,
- ExternalTrackerFormat: form.TrackerURLFormat,
- ExternalTrackerStyle: form.TrackerIssueStyle,
+ ExternalTrackerURL: form.ExternalTrackerURL,
+ ExternalTrackerFormat: form.TrackerURLFormat,
+ ExternalTrackerStyle: form.TrackerIssueStyle,
+ ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
},
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
@@ -461,6 +488,15 @@ func SettingsPost(ctx *context.Context) {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
}
+ if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() {
+ units = append(units, repo_model.RepoUnit{
+ RepoID: repo.ID,
+ Type: unit_model.TypePackages,
+ })
+ } else if !unit_model.TypePackages.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
+ }
+
if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
@@ -487,7 +523,7 @@ func SettingsPost(ctx *context.Context) {
return
}
if repoChanged {
- if err := models.UpdateRepository(repo, false); err != nil {
+ if err := repo_service.UpdateRepository(repo, false); err != nil {
ctx.ServerError("UpdateRepository", err)
return
}
@@ -506,7 +542,7 @@ func SettingsPost(ctx *context.Context) {
}
if changed {
- if err := models.UpdateRepository(repo, false); err != nil {
+ if err := repo_service.UpdateRepository(repo, false); err != nil {
ctx.ServerError("UpdateRepository", err)
return
}
@@ -526,7 +562,7 @@ func SettingsPost(ctx *context.Context) {
repo.IsFsckEnabled = form.EnableHealthCheck
}
- if err := models.UpdateRepository(repo, false); err != nil {
+ if err := repo_service.UpdateRepository(repo, false); err != nil {
ctx.ServerError("UpdateRepository", err)
return
}
@@ -580,7 +616,7 @@ func SettingsPost(ctx *context.Context) {
}
repo.IsMirror = false
- if _, err := repository.CleanUpMigrateInfo(ctx, repo); err != nil {
+ if _, err := repo_module.CleanUpMigrateInfo(ctx, repo); err != nil {
ctx.ServerError("CleanUpMigrateInfo", err)
return
} else if err = repo_model.DeleteMirrorByRepoID(ctx.Repo.Repository.ID); err != nil {
@@ -638,7 +674,7 @@ func SettingsPost(ctx *context.Context) {
return
}
- newOwner, err := user_model.GetUserByName(ctx.FormString("new_owner_name"))
+ newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
@@ -662,7 +698,7 @@ func SettingsPost(ctx *context.Context) {
ctx.Repo.GitRepo = nil
}
- if err := repo_service.StartRepositoryTransfer(ctx.Doer, newOwner, repo, nil); err != nil {
+ if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
if repo_model.IsErrRepoAlreadyExist(err) {
ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
} else if models.IsErrRepoTransferInProgress(err) {
@@ -684,7 +720,7 @@ func SettingsPost(ctx *context.Context) {
return
}
- repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
+ repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
if err != nil {
if models.IsErrNoPendingTransfer(err) {
ctx.Flash.Error("repo.settings.transfer_abort_invalid")
@@ -696,7 +732,7 @@ func SettingsPost(ctx *context.Context) {
return
}
- if err := repoTransfer.LoadAttributes(); err != nil {
+ if err := repoTransfer.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadRecipient", err)
return
}
@@ -807,7 +843,7 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R
case addrErr.IsProtocolInvalid:
ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
case addrErr.IsURLError:
- ctx.RenderWithErr(ctx.Tr("form.url_error"), tplSettingsOptions, form)
+ ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tplSettingsOptions, form)
case addrErr.IsPermissionDenied:
if addrErr.LocalPath {
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
@@ -829,14 +865,14 @@ func Collaboration(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsCollaboration"] = true
- users, err := models.GetCollaborators(ctx.Repo.Repository.ID, db.ListOptions{})
+ users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
if err != nil {
ctx.ServerError("GetCollaborators", err)
return
}
ctx.Data["Collaborators"] = users
- teams, err := organization.GetRepoTeams(ctx.Repo.Repository)
+ teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository)
if err != nil {
ctx.ServerError("GetRepoTeams", err)
return
@@ -859,7 +895,7 @@ func CollaborationPost(ctx *context.Context) {
return
}
- u, err := user_model.GetUserByName(name)
+ u, err := user_model.GetUserByName(ctx, name)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
@@ -883,13 +919,26 @@ func CollaborationPost(ctx *context.Context) {
return
}
- if got, err := models.IsCollaborator(ctx.Repo.Repository.ID, u.ID); err == nil && got {
+ if got, err := repo_model.IsCollaborator(ctx, ctx.Repo.Repository.ID, u.ID); err == nil && got {
ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_duplicate"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
return
}
- if err = models.AddCollaborator(ctx.Repo.Repository, u); err != nil {
+ // find the owner team of the organization the repo belongs too and
+ // check if the user we're trying to add is an owner.
+ if ctx.Repo.Repository.Owner.IsOrganization() {
+ if isOwner, err := organization.IsOrganizationOwner(ctx, ctx.Repo.Repository.Owner.ID, u.ID); err != nil {
+ ctx.ServerError("IsOrganizationOwner", err)
+ return
+ } else if isOwner {
+ ctx.Flash.Error(ctx.Tr("repo.settings.add_collaborator_owner"))
+ ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
+ return
+ }
+ }
+
+ if err = repo_module.AddCollaborator(ctx, ctx.Repo.Repository, u); err != nil {
ctx.ServerError("AddCollaborator", err)
return
}
@@ -904,7 +953,8 @@ func CollaborationPost(ctx *context.Context) {
// ChangeCollaborationAccessMode response for changing access of a collaboration
func ChangeCollaborationAccessMode(ctx *context.Context) {
- if err := models.ChangeCollaborationAccessMode(
+ if err := repo_model.ChangeCollaborationAccessMode(
+ ctx,
ctx.Repo.Repository,
ctx.FormInt64("uid"),
perm.AccessMode(ctx.FormInt("mode"))); err != nil {
@@ -962,8 +1012,8 @@ func AddTeamPost(ctx *context.Context) {
return
}
- if err = models.AddRepository(team, ctx.Repo.Repository); err != nil {
- ctx.ServerError("team.AddRepository", err)
+ if err = org_service.TeamAddRepository(team, ctx.Repo.Repository); err != nil {
+ ctx.ServerError("TeamAddRepository", err)
return
}
@@ -979,7 +1029,7 @@ func DeleteTeam(ctx *context.Context) {
return
}
- team, err := organization.GetTeamByID(ctx.FormInt64("id"))
+ team, err := organization.GetTeamByID(ctx, ctx.FormInt64("id"))
if err != nil {
ctx.ServerError("GetTeamByID", err)
return
@@ -1063,12 +1113,37 @@ func DeployKeys(ctx *context.Context) {
}
ctx.Data["Deploykeys"] = keys
+ secrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{RepoID: ctx.Repo.Repository.ID})
+ if err != nil {
+ ctx.ServerError("FindSecrets", err)
+ return
+ }
+ ctx.Data["Secrets"] = secrets
+
ctx.HTML(http.StatusOK, tplDeployKeys)
}
+// SecretsPost response for creating a new secret
+func SecretsPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.AddSecretForm)
+
+ _, err := secret_model.InsertEncryptedSecret(ctx, 0, ctx.Repo.Repository.ID, form.Title, form.Content)
+ if err != nil {
+ ctx.Flash.Error(ctx.Tr("secrets.creation.failed"))
+ log.Error("validate secret: %v", err)
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
+ return
+ }
+
+ log.Trace("Secret added: %d", ctx.Repo.Repository.ID)
+ ctx.Flash.Success(ctx.Tr("secrets.creation.success", form.Title))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
+}
+
// DeployKeysPost response for adding a deploy key of a repository
func DeployKeysPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AddKeyForm)
+
ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
@@ -1127,6 +1202,20 @@ func DeployKeysPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
}
+func DeleteSecret(ctx *context.Context) {
+ id := ctx.FormInt64("id")
+ if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id}); err != nil {
+ ctx.Flash.Error(ctx.Tr("secrets.deletion.failed"))
+ log.Error("delete secret %d: %v", id, err)
+ } else {
+ ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": ctx.Repo.RepoLink + "/settings/keys",
+ })
+}
+
// DeleteDeployKey response for deleting a deploy key
func DeleteDeployKey(ctx *context.Context) {
if err := asymkey_service.DeleteDeployKey(ctx.Doer, ctx.FormInt64("id")); err != nil {
@@ -1156,7 +1245,7 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
r, err := form.Avatar.Open()
if err != nil {
- return fmt.Errorf("Avatar.Open: %v", err)
+ return fmt.Errorf("Avatar.Open: %w", err)
}
defer r.Close()
@@ -1166,14 +1255,14 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
data, err := io.ReadAll(r)
if err != nil {
- return fmt.Errorf("io.ReadAll: %v", err)
+ return fmt.Errorf("io.ReadAll: %w", err)
}
st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) {
return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
}
if err = repo_service.UploadAvatar(ctxRepo, data); err != nil {
- return fmt.Errorf("UploadAvatar: %v", err)
+ return fmt.Errorf("UploadAvatar: %w", err)
}
return nil
}
@@ -1198,19 +1287,20 @@ func SettingsDeleteAvatar(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
-func selectPushMirrorByForm(form *forms.RepoSettingForm, repo *repo_model.Repository) (*repo_model.PushMirror, error) {
+func selectPushMirrorByForm(ctx *context.Context, form *forms.RepoSettingForm, repo *repo_model.Repository) (*repo_model.PushMirror, error) {
id, err := strconv.ParseInt(form.PushMirrorID, 10, 64)
if err != nil {
return nil, err
}
- pushMirrors, err := repo_model.GetPushMirrorsByRepoID(repo.ID)
+ pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, db.ListOptions{})
if err != nil {
return nil, err
}
for _, m := range pushMirrors {
if m.ID == id {
+ m.Repo = repo
return m, nil
}
}
diff --git a/routers/web/repo/setting_protected_branch.go b/routers/web/repo/setting_protected_branch.go
index a26a0a620a926..31abde1ef6801 100644
--- a/routers/web/repo/setting_protected_branch.go
+++ b/routers/web/repo/setting_protected_branch.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -10,56 +9,43 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
pull_service "code.gitea.io/gitea/services/pull"
"code.gitea.io/gitea/services/repository"
)
-// ProtectedBranch render the page to protect the repository
-func ProtectedBranch(ctx *context.Context) {
+const (
+ tplProtectedBranch base.TplName = "repo/settings/protected_branch"
+)
+
+// ProtectedBranchRules render the page to protect the repository
+func ProtectedBranchRules(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsBranches"] = true
- protectedBranches, err := models.GetProtectedBranches(ctx.Repo.Repository.ID)
+ rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetProtectedBranches", err)
return
}
- ctx.Data["ProtectedBranches"] = protectedBranches
-
- branches := ctx.Data["Branches"].([]string)
- leftBranches := make([]string, 0, len(branches)-len(protectedBranches))
- for _, b := range branches {
- var protected bool
- for _, pb := range protectedBranches {
- if b == pb.BranchName {
- protected = true
- break
- }
- }
- if !protected {
- leftBranches = append(leftBranches, b)
- }
- }
-
- ctx.Data["LeftBranches"] = leftBranches
+ ctx.Data["ProtectedBranches"] = rules
ctx.HTML(http.StatusOK, tplBranches)
}
-// ProtectedBranchPost response for protect for a branch of a repository
-func ProtectedBranchPost(ctx *context.Context) {
+// SetDefaultBranchPost set default branch
+func SetDefaultBranchPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsBranches"] = true
@@ -101,41 +87,36 @@ func ProtectedBranchPost(ctx *context.Context) {
// SettingsProtectedBranch renders the protected branch setting page
func SettingsProtectedBranch(c *context.Context) {
- branch := c.Params("*")
- if !c.Repo.GitRepo.IsBranchExist(branch) {
- c.NotFound("IsBranchExist", nil)
- return
- }
-
- c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + branch
- c.Data["PageIsSettingsBranches"] = true
-
- protectBranch, err := models.GetProtectedBranchBy(c.Repo.Repository.ID, branch)
- if err != nil {
- if !git.IsErrBranchNotExist(err) {
+ ruleName := c.FormString("rule_name")
+ var rule *git_model.ProtectedBranch
+ if ruleName != "" {
+ var err error
+ rule, err = git_model.GetProtectedBranchRuleByName(c, c.Repo.Repository.ID, ruleName)
+ if err != nil {
c.ServerError("GetProtectBranchOfRepoByName", err)
return
}
}
- if protectBranch == nil {
+ if rule == nil {
// No options found, create defaults.
- protectBranch = &models.ProtectedBranch{
- BranchName: branch,
- }
+ rule = &git_model.ProtectedBranch{}
}
- users, err := models.GetRepoReaders(c.Repo.Repository)
+ c.Data["PageIsSettingsBranches"] = true
+ c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + rule.RuleName
+
+ users, err := access_model.GetRepoReaders(c.Repo.Repository)
if err != nil {
c.ServerError("Repo.Repository.GetReaders", err)
return
}
c.Data["Users"] = users
- c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistUserIDs), ",")
- c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistUserIDs), ",")
- c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistUserIDs), ",")
- contexts, _ := models.FindRepoRecentCommitStatusContexts(c.Repo.Repository.ID, 7*24*time.Hour) // Find last week status check contexts
- for _, ctx := range protectBranch.StatusCheckContexts {
+ c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(rule.WhitelistUserIDs), ",")
+ c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistUserIDs), ",")
+ c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistUserIDs), ",")
+ contexts, _ := git_model.FindRepoRecentCommitStatusContexts(c, c.Repo.Repository.ID, 7*24*time.Hour) // Find last week status check contexts
+ for _, ctx := range rule.StatusCheckContexts {
var found bool
for i := range contexts {
if contexts[i] == ctx {
@@ -150,7 +131,7 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["branch_status_check_contexts"] = contexts
c.Data["is_context_required"] = func(context string) bool {
- for _, c := range protectBranch.StatusCheckContexts {
+ for _, c := range rule.StatusCheckContexts {
if c == context {
return true
}
@@ -165,130 +146,173 @@ func SettingsProtectedBranch(c *context.Context) {
return
}
c.Data["Teams"] = teams
- c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.WhitelistTeamIDs), ",")
- c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.MergeWhitelistTeamIDs), ",")
- c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(protectBranch.ApprovalsWhitelistTeamIDs), ",")
+ c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.WhitelistTeamIDs), ",")
+ c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistTeamIDs), ",")
+ c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistTeamIDs), ",")
}
- c.Data["Branch"] = protectBranch
+ c.Data["Rule"] = rule
c.HTML(http.StatusOK, tplProtectedBranch)
}
// SettingsProtectedBranchPost updates the protected branch settings
func SettingsProtectedBranchPost(ctx *context.Context) {
f := web.GetForm(ctx).(*forms.ProtectBranchForm)
- branch := ctx.Params("*")
- if !ctx.Repo.GitRepo.IsBranchExist(branch) {
- ctx.NotFound("IsBranchExist", nil)
+ var protectBranch *git_model.ProtectedBranch
+ if f.RuleName == "" {
+ ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_rule_name"))
+ ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit", ctx.Repo.RepoLink))
return
}
- protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branch)
+ var err error
+ protectBranch, err = git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, f.RuleName)
if err != nil {
- if !git.IsErrBranchNotExist(err) {
- ctx.ServerError("GetProtectBranchOfRepoByName", err)
- return
+ ctx.ServerError("GetProtectBranchOfRepoByName", err)
+ return
+ }
+ if protectBranch == nil {
+ // No options found, create defaults.
+ protectBranch = &git_model.ProtectedBranch{
+ RepoID: ctx.Repo.Repository.ID,
+ RuleName: f.RuleName,
}
}
- if f.Protected {
- if protectBranch == nil {
- // No options found, create defaults.
- protectBranch = &models.ProtectedBranch{
- RepoID: ctx.Repo.Repository.ID,
- BranchName: branch,
- }
+ var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
+ protectBranch.RuleName = f.RuleName
+ if f.RequiredApprovals < 0 {
+ ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min"))
+ ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit?rule_name=%s", ctx.Repo.RepoLink, f.RuleName))
+ return
+ }
+
+ switch f.EnablePush {
+ case "all":
+ protectBranch.CanPush = true
+ protectBranch.EnableWhitelist = false
+ protectBranch.WhitelistDeployKeys = false
+ case "whitelist":
+ protectBranch.CanPush = true
+ protectBranch.EnableWhitelist = true
+ protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
+ if strings.TrimSpace(f.WhitelistUsers) != "" {
+ whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
}
- if f.RequiredApprovals < 0 {
- ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min"))
- ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, util.PathEscapeSegments(branch)))
+ if strings.TrimSpace(f.WhitelistTeams) != "" {
+ whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
}
+ default:
+ protectBranch.CanPush = false
+ protectBranch.EnableWhitelist = false
+ protectBranch.WhitelistDeployKeys = false
+ }
- var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
- switch f.EnablePush {
- case "all":
- protectBranch.CanPush = true
- protectBranch.EnableWhitelist = false
- protectBranch.WhitelistDeployKeys = false
- case "whitelist":
- protectBranch.CanPush = true
- protectBranch.EnableWhitelist = true
- protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
- if strings.TrimSpace(f.WhitelistUsers) != "" {
- whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
- }
- if strings.TrimSpace(f.WhitelistTeams) != "" {
- whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
- }
- default:
- protectBranch.CanPush = false
- protectBranch.EnableWhitelist = false
- protectBranch.WhitelistDeployKeys = false
+ protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
+ if f.EnableMergeWhitelist {
+ if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
+ mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
}
-
- protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
- if f.EnableMergeWhitelist {
- if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
- mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
- }
- if strings.TrimSpace(f.MergeWhitelistTeams) != "" {
- mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
- }
+ if strings.TrimSpace(f.MergeWhitelistTeams) != "" {
+ mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
}
+ }
- protectBranch.EnableStatusCheck = f.EnableStatusCheck
- if f.EnableStatusCheck {
- protectBranch.StatusCheckContexts = f.StatusCheckContexts
- } else {
- protectBranch.StatusCheckContexts = nil
- }
+ protectBranch.EnableStatusCheck = f.EnableStatusCheck
+ if f.EnableStatusCheck {
+ protectBranch.StatusCheckContexts = f.StatusCheckContexts
+ } else {
+ protectBranch.StatusCheckContexts = nil
+ }
- protectBranch.RequiredApprovals = f.RequiredApprovals
- protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist
- if f.EnableApprovalsWhitelist {
- if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
- approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ","))
- }
- if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" {
- approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
- }
+ protectBranch.RequiredApprovals = f.RequiredApprovals
+ protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist
+ if f.EnableApprovalsWhitelist {
+ if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
+ approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ","))
}
- protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
- protectBranch.BlockOnOfficialReviewRequests = f.BlockOnOfficialReviewRequests
- protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
- protectBranch.RequireSignedCommits = f.RequireSignedCommits
- protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
- protectBranch.UnprotectedFilePatterns = f.UnprotectedFilePatterns
- protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
-
- err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
- UserIDs: whitelistUsers,
- TeamIDs: whitelistTeams,
- MergeUserIDs: mergeWhitelistUsers,
- MergeTeamIDs: mergeWhitelistTeams,
- ApprovalsUserIDs: approvalsWhitelistUsers,
- ApprovalsTeamIDs: approvalsWhitelistTeams,
- })
- if err != nil {
- ctx.ServerError("UpdateProtectBranch", err)
- return
+ if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" {
+ approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
}
- if err = pull_service.CheckPrsForBaseBranch(ctx.Repo.Repository, protectBranch.BranchName); err != nil {
- ctx.ServerError("CheckPrsForBaseBranch", err)
+ }
+ protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
+ protectBranch.BlockOnOfficialReviewRequests = f.BlockOnOfficialReviewRequests
+ protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
+ protectBranch.RequireSignedCommits = f.RequireSignedCommits
+ protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
+ protectBranch.UnprotectedFilePatterns = f.UnprotectedFilePatterns
+ protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
+
+ err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
+ UserIDs: whitelistUsers,
+ TeamIDs: whitelistTeams,
+ MergeUserIDs: mergeWhitelistUsers,
+ MergeTeamIDs: mergeWhitelistTeams,
+ ApprovalsUserIDs: approvalsWhitelistUsers,
+ ApprovalsTeamIDs: approvalsWhitelistTeams,
+ })
+ if err != nil {
+ ctx.ServerError("UpdateProtectBranch", err)
+ return
+ }
+
+ // FIXME: since we only need to recheck files protected rules, we could improve this
+ matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.GitRepo, protectBranch.RuleName)
+ if err != nil {
+ ctx.ServerError("FindAllMatchedBranches", err)
+ return
+ }
+ for _, branchName := range matchedBranches {
+ if err = pull_service.CheckPRsForBaseBranch(ctx.Repo.Repository, branchName); err != nil {
+ ctx.ServerError("CheckPRsForBaseBranch", err)
return
}
- ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", branch))
- ctx.Redirect(fmt.Sprintf("%s/settings/branches/%s", ctx.Repo.RepoLink, util.PathEscapeSegments(branch)))
- } else {
- if protectBranch != nil {
- if err := models.DeleteProtectedBranch(ctx.Repo.Repository.ID, protectBranch.ID); err != nil {
- ctx.ServerError("DeleteProtectedBranch", err)
- return
- }
- }
- ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", branch))
- ctx.Redirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
}
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", protectBranch.RuleName))
+ ctx.Redirect(fmt.Sprintf("%s/settings/branches?rule_name=%s", ctx.Repo.RepoLink, protectBranch.RuleName))
+}
+
+// DeleteProtectedBranchRulePost delete protected branch rule by id
+func DeleteProtectedBranchRulePost(ctx *context.Context) {
+ ruleID := ctx.ParamsInt64("id")
+ if ruleID <= 0 {
+ ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink),
+ })
+ return
+ }
+
+ rule, err := git_model.GetProtectedBranchRuleByID(ctx, ctx.Repo.Repository.ID, ruleID)
+ if err != nil {
+ ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink),
+ })
+ return
+ }
+
+ if rule == nil {
+ ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink),
+ })
+ return
+ }
+
+ if err := git_model.DeleteProtectedBranch(ctx, ctx.Repo.Repository.ID, ruleID); err != nil {
+ ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", rule.RuleName))
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink),
+ })
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", rule.RuleName))
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink),
+ })
}
// RenameBranchPost responses for rename a branch
diff --git a/routers/web/repo/settings_test.go b/routers/web/repo/settings_test.go
index 36d02de2735ec..3bb202505c47d 100644
--- a/routers/web/repo/settings_test.go
+++ b/routers/web/repo/settings_test.go
@@ -1,12 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
- "os"
"testing"
"code.gitea.io/gitea/models"
@@ -19,7 +17,6 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
@@ -27,18 +24,13 @@ import (
)
func createSSHAuthorizedKeysTmpPath(t *testing.T) func() {
- tmpDir, err := os.MkdirTemp("", "tmp-ssh")
- if err != nil {
- assert.Fail(t, "Unable to create temporary directory: %v", err)
- return nil
- }
+ tmpDir := t.TempDir()
oldPath := setting.SSH.RootPath
setting.SSH.RootPath = tmpDir
return func() {
setting.SSH.RootPath = oldPath
- util.RemoveAll(tmpDir)
}
}
@@ -130,7 +122,7 @@ func TestCollaborationPost(t *testing.T) {
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
- exists, err := models.IsCollaborator(re.ID, 4)
+ exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
assert.NoError(t, err)
assert.True(t, exists)
}
@@ -188,7 +180,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
- exists, err := models.IsCollaborator(re.ID, 4)
+ exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
assert.NoError(t, err)
assert.True(t, exists)
diff --git a/routers/web/repo/tag.go b/routers/web/repo/tag.go
index 7da1e36c817a3..aa9edc73755d9 100644
--- a/routers/web/repo/tag.go
+++ b/routers/web/repo/tag.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -9,9 +8,10 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
@@ -42,7 +42,7 @@ func NewProtectedTagPost(ctx *context.Context) {
repo := ctx.Repo.Repository
form := web.GetForm(ctx).(*forms.ProtectTagForm)
- pt := &models.ProtectedTag{
+ pt := &git_model.ProtectedTag{
RepoID: repo.ID,
NamePattern: strings.TrimSpace(form.NamePattern),
}
@@ -54,7 +54,7 @@ func NewProtectedTagPost(ctx *context.Context) {
pt.AllowlistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistTeams, ","))
}
- if err := models.InsertProtectedTag(pt); err != nil {
+ if err := git_model.InsertProtectedTag(ctx, pt); err != nil {
ctx.ServerError("InsertProtectedTag", err)
return
}
@@ -107,7 +107,7 @@ func EditProtectedTagPost(ctx *context.Context) {
pt.AllowlistUserIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistUsers, ","))
pt.AllowlistTeamIDs, _ = base.StringsToInt64s(strings.Split(form.AllowlistTeams, ","))
- if err := models.UpdateProtectedTag(pt); err != nil {
+ if err := git_model.UpdateProtectedTag(ctx, pt); err != nil {
ctx.ServerError("UpdateProtectedTag", err)
return
}
@@ -123,7 +123,7 @@ func DeleteProtectedTagPost(ctx *context.Context) {
return
}
- if err := models.DeleteProtectedTag(pt); err != nil {
+ if err := git_model.DeleteProtectedTag(ctx, pt); err != nil {
ctx.ServerError("DeleteProtectedTag", err)
return
}
@@ -136,14 +136,14 @@ func setTagsContext(ctx *context.Context) error {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsTags"] = true
- protectedTags, err := models.GetProtectedTags(ctx.Repo.Repository.ID)
+ protectedTags, err := git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetProtectedTags", err)
return err
}
ctx.Data["ProtectedTags"] = protectedTags
- users, err := models.GetRepoReaders(ctx.Repo.Repository)
+ users, err := access_model.GetRepoReaders(ctx.Repo.Repository)
if err != nil {
ctx.ServerError("Repo.Repository.GetReaders", err)
return err
@@ -162,13 +162,13 @@ func setTagsContext(ctx *context.Context) error {
return nil
}
-func selectProtectedTagByContext(ctx *context.Context) *models.ProtectedTag {
+func selectProtectedTagByContext(ctx *context.Context) *git_model.ProtectedTag {
id := ctx.FormInt64("id")
if id == 0 {
id = ctx.ParamsInt64(":id")
}
- tag, err := models.GetProtectedTagByID(id)
+ tag, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.ServerError("GetProtectedTagByID", err)
return nil
diff --git a/routers/web/repo/topic.go b/routers/web/repo/topic.go
index efbfc62d56bb9..4c0b38bd91027 100644
--- a/routers/web/repo/topic.go
+++ b/routers/web/repo/topic.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go
new file mode 100644
index 0000000000000..c364e7090f789
--- /dev/null
+++ b/routers/web/repo/treelist.go
@@ -0,0 +1,54 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/git"
+
+ "github.com/go-enry/go-enry/v2"
+)
+
+// TreeList get all files' entries of a repository
+func TreeList(ctx *context.Context) {
+ tree, err := ctx.Repo.Commit.SubTree("/")
+ if err != nil {
+ ctx.ServerError("Repo.Commit.SubTree", err)
+ return
+ }
+
+ entries, err := tree.ListEntriesRecursiveFast()
+ if err != nil {
+ ctx.ServerError("ListEntriesRecursiveFast", err)
+ return
+ }
+ entries.CustomSort(base.NaturalSortLess)
+
+ files := make([]string, 0, len(entries))
+ for _, entry := range entries {
+ if !isExcludedEntry(entry) {
+ files = append(files, entry.Name())
+ }
+ }
+ ctx.JSON(http.StatusOK, files)
+}
+
+func isExcludedEntry(entry *git.TreeEntry) bool {
+ if entry.IsDir() {
+ return true
+ }
+
+ if entry.IsSubModule() {
+ return true
+ }
+
+ if enry.IsVendor(entry.Name()) {
+ return true
+ }
+
+ return false
+}
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 0faa01d573ce0..769e4e895cd99 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -1,7 +1,6 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package repo
@@ -10,30 +9,31 @@ import (
gocontext "context"
"encoding/base64"
"fmt"
- gotemplate "html/template"
"io"
"net/http"
"net/url"
"path"
- "strconv"
"strings"
"time"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
+ admin_model "code.gitea.io/gitea/models/admin"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/typesniffer"
@@ -56,17 +56,8 @@ type namedBlob struct {
blob *git.Blob
}
-func linesBytesCount(s []byte) int {
- nl := []byte{'\n'}
- n := bytes.Count(s, nl)
- if len(s) > 0 && !bytes.HasSuffix(s, nl) {
- n++
- }
- return n
-}
-
// FIXME: There has to be a more efficient way of doing this
-func getReadmeFileFromPath(commit *git.Commit, treePath string) (*namedBlob, error) {
+func getReadmeFileFromPath(ctx *context.Context, commit *git.Commit, treePath string) (*namedBlob, error) {
tree, err := commit.SubTree(treePath)
if err != nil {
return nil, err
@@ -77,50 +68,33 @@ func getReadmeFileFromPath(commit *git.Commit, treePath string) (*namedBlob, err
return nil, err
}
- var readmeFiles [4]*namedBlob
- exts := []string{".md", ".txt", ""} // sorted by priority
+ // Create a list of extensions in priority order
+ // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
+ // 2. Txt files - e.g. README.txt
+ // 3. No extension - e.g. README
+ exts := append(localizedExtensions(".md", ctx.Language()), ".txt", "") // sorted by priority
+ extCount := len(exts)
+ readmeFiles := make([]*namedBlob, extCount+1)
for _, entry := range entries {
if entry.IsDir() {
continue
}
- for i, ext := range exts {
- if markup.IsReadmeFile(entry.Name(), ext) {
- if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].name, entry.Blob().Name()) {
- name := entry.Name()
- isSymlink := entry.IsLink()
- target := entry
- if isSymlink {
- target, err = entry.FollowLinks()
- if err != nil && !git.IsErrBadLink(err) {
- return nil, err
- }
- }
- if target != nil && (target.IsExecutable() || target.IsRegular()) {
- readmeFiles[i] = &namedBlob{
- name,
- isSymlink,
- target.Blob(),
- }
- }
- }
- }
- }
-
- if markup.IsReadmeFile(entry.Name()) {
- if readmeFiles[3] == nil || base.NaturalSortLess(readmeFiles[3].name, entry.Blob().Name()) {
+ if i, ok := markup.IsReadmeFileExtension(entry.Name(), exts...); ok {
+ if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].name, entry.Blob().Name()) {
name := entry.Name()
isSymlink := entry.IsLink()
+ target := entry
if isSymlink {
- entry, err = entry.FollowLinks()
+ target, err = entry.FollowLinks()
if err != nil && !git.IsErrBadLink(err) {
return nil, err
}
}
- if entry != nil && (entry.IsExecutable() || entry.IsRegular()) {
- readmeFiles[3] = &namedBlob{
+ if target != nil && (target.IsExecutable() || target.IsRegular()) {
+ readmeFiles[i] = &namedBlob{
name,
isSymlink,
- entry.Blob(),
+ target.Blob(),
}
}
}
@@ -143,15 +117,56 @@ 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)
}
- // 3 for the extensions in exts[] in order
- // the last one is for a readme that doesn't
- // strictly match an extension
- var readmeFiles [4]*namedBlob
- var docsEntries [3]*git.TreeEntry
- exts := []string{".md", ".txt", ""} // sorted by priority
+ // Check permission to add or upload new file.
+ if ctx.Repo.CanWrite(unit_model.TypeCode) && ctx.Repo.IsViewBranch {
+ ctx.Data["CanAddFile"] = !ctx.Repo.Repository.IsArchived
+ ctx.Data["CanUploadFile"] = setting.Repository.Upload.Enabled && !ctx.Repo.Repository.IsArchived
+ }
+
+ readmeFile, readmeTreelink := findReadmeFile(ctx, entries, treeLink)
+ if ctx.Written() || readmeFile == nil {
+ return
+ }
+
+ renderReadmeFile(ctx, readmeFile, readmeTreelink)
+}
+
+// localizedExtensions prepends the provided language code with and without a
+// regional identifier to the provided extension.
+// Note: the language code will always be lower-cased, if a region is present it must be separated with a `-`
+// Note: ext should be prefixed with a `.`
+func localizedExtensions(ext, languageCode string) (localizedExts []string) {
+ if len(languageCode) < 1 {
+ return []string{ext}
+ }
+
+ lowerLangCode := "." + strings.ToLower(languageCode)
+
+ if strings.Contains(lowerLangCode, "-") {
+ underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_")
+ indexOfDash := strings.Index(lowerLangCode, "-")
+ // 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]
+ return []string{lowerLangCode + ext, ext}
+}
+
+func findReadmeFile(ctx *context.Context, entries git.Entries, treeLink string) (*namedBlob, string) {
+ // Create a list of extensions in priority order
+ // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
+ // 2. Txt files - e.g. README.txt
+ // 3. No extension - e.g. README
+ exts := append(localizedExtensions(".md", ctx.Language()), ".txt", "") // sorted by priority
+ extCount := len(exts)
+ readmeFiles := make([]*namedBlob, extCount+1)
+
+ docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
for _, entry := range entries {
if entry.IsDir() {
lowerName := strings.ToLower(entry.Name())
@@ -172,47 +187,24 @@ func renderDirectory(ctx *context.Context, treeLink string) {
continue
}
- for i, ext := range exts {
- if markup.IsReadmeFile(entry.Name(), ext) {
- log.Debug("%s", entry.Name())
- name := entry.Name()
- isSymlink := entry.IsLink()
- target := entry
- if isSymlink {
- var err error
- target, err = entry.FollowLinks()
- if err != nil && !git.IsErrBadLink(err) {
- ctx.ServerError("FollowLinks", err)
- return
- }
- }
- log.Debug("%t", target == nil)
- if target != nil && (target.IsExecutable() || target.IsRegular()) {
- readmeFiles[i] = &namedBlob{
- name,
- isSymlink,
- target.Blob(),
- }
- }
- }
- }
-
- if markup.IsReadmeFile(entry.Name()) {
+ if i, ok := markup.IsReadmeFileExtension(entry.Name(), exts...); ok {
+ log.Debug("Potential readme file: %s", entry.Name())
name := entry.Name()
isSymlink := entry.IsLink()
+ target := entry
if isSymlink {
var err error
- entry, err = entry.FollowLinks()
+ target, err = entry.FollowLinks()
if err != nil && !git.IsErrBadLink(err) {
ctx.ServerError("FollowLinks", err)
- return
+ return nil, ""
}
}
- if entry != nil && (entry.IsExecutable() || entry.IsRegular()) {
- readmeFiles[3] = &namedBlob{
+ if target != nil && (target.IsExecutable() || target.IsRegular()) {
+ readmeFiles[i] = &namedBlob{
name,
isSymlink,
- entry.Blob(),
+ target.Blob(),
}
}
}
@@ -233,10 +225,10 @@ func renderDirectory(ctx *context.Context, treeLink string) {
continue
}
var err error
- readmeFile, err = getReadmeFileFromPath(ctx.Repo.Commit, entry.GetSubJumpablePathName())
+ readmeFile, err = getReadmeFileFromPath(ctx, ctx.Repo.Commit, entry.GetSubJumpablePathName())
if err != nil {
ctx.ServerError("getReadmeFileFromPath", err)
- return
+ return nil, ""
}
if readmeFile != nil {
readmeFile.name = entry.Name() + "/" + readmeFile.name
@@ -245,232 +237,200 @@ func renderDirectory(ctx *context.Context, treeLink string) {
}
}
}
+ return readmeFile, readmeTreelink
+}
- if readmeFile != nil {
- ctx.Data["RawFileLink"] = ""
- ctx.Data["ReadmeInList"] = true
- ctx.Data["ReadmeExist"] = true
- ctx.Data["FileIsSymlink"] = readmeFile.isSymlink
+type fileInfo struct {
+ isTextFile bool
+ isLFSFile bool
+ fileSize int64
+ lfsMeta *lfs.Pointer
+ st typesniffer.SniffedType
+}
- dataRc, err := readmeFile.blob.DataAsync()
- if err != nil {
- ctx.ServerError("Data", err)
- return
- }
- defer dataRc.Close()
-
- buf := make([]byte, 1024)
- n, _ := util.ReadAtMost(dataRc, buf)
- buf = buf[:n]
-
- st := typesniffer.DetectContentType(buf)
- isTextFile := st.IsText()
-
- ctx.Data["FileIsText"] = isTextFile
- ctx.Data["FileName"] = readmeFile.name
- fileSize := int64(0)
- isLFSFile := false
- ctx.Data["IsLFSFile"] = false
-
- // FIXME: what happens when README file is an image?
- if isTextFile && setting.LFS.StartServer {
- pointer, _ := lfs.ReadPointerFromBuffer(buf)
- if pointer.IsValid() {
- meta, err := models.GetLFSMetaObjectByOid(ctx.Repo.Repository.ID, pointer.Oid)
- if err != nil && err != models.ErrLFSObjectNotExist {
- ctx.ServerError("GetLFSMetaObject", err)
- return
- }
- if meta != nil {
- ctx.Data["IsLFSFile"] = true
- isLFSFile = true
-
- // OK read the lfs object
- var err error
- dataRc, err = lfs.ReadMetaObject(pointer)
- if err != nil {
- ctx.ServerError("ReadMetaObject", err)
- return
- }
- defer dataRc.Close()
+func getFileReader(repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) {
+ dataRc, err := blob.DataAsync()
+ if err != nil {
+ return nil, nil, nil, err
+ }
- buf = make([]byte, 1024)
- n, err = util.ReadAtMost(dataRc, buf)
- if err != nil {
- ctx.ServerError("Data", err)
- return
- }
- buf = buf[:n]
+ buf := make([]byte, 1024)
+ n, _ := util.ReadAtMost(dataRc, buf)
+ buf = buf[:n]
- st = typesniffer.DetectContentType(buf)
- isTextFile = st.IsText()
- ctx.Data["IsTextFile"] = isTextFile
+ st := typesniffer.DetectContentType(buf)
+ isTextFile := st.IsText()
- fileSize = meta.Size
- ctx.Data["FileSize"] = meta.Size
- filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.name))
- ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.HTMLURL(), url.PathEscape(meta.Oid), url.PathEscape(filenameBase64))
- }
- }
- }
+ // FIXME: what happens when README file is an image?
+ if !isTextFile || !setting.LFS.StartServer {
+ return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ }
- if !isLFSFile {
- fileSize = readmeFile.blob.Size()
- }
+ pointer, _ := lfs.ReadPointerFromBuffer(buf)
+ if !pointer.IsValid() { // fallback to plain file
+ return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ }
- if isTextFile {
- if fileSize >= setting.UI.MaxDisplayFileSize {
- // Pretend that this is a normal text file to display 'This file is too large to be shown'
- ctx.Data["IsFileTooLarge"] = true
- ctx.Data["IsTextFile"] = true
- ctx.Data["FileSize"] = fileSize
- } else {
- rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
-
- if markupType := markup.Type(readmeFile.name); markupType != "" {
- ctx.Data["IsMarkup"] = true
- ctx.Data["MarkupType"] = string(markupType)
- var result strings.Builder
- err := markup.Render(&markup.RenderContext{
- Ctx: ctx,
- Filename: readmeFile.name,
- URLPrefix: readmeTreelink,
- Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
- GitRepo: ctx.Repo.GitRepo,
- }, rd, &result)
- if err != nil {
- log.Error("Render failed: %v then fallback", err)
- buf := &bytes.Buffer{}
- ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, buf)
- ctx.Data["FileContent"] = strings.ReplaceAll(
- gotemplate.HTMLEscapeString(buf.String()), "\n", ` `,
- )
- } else {
- ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlString(result.String())
- }
- } else {
- ctx.Data["IsRenderedHTML"] = true
- buf := &bytes.Buffer{}
- ctx.Data["EscapeStatus"], err = charset.EscapeControlReader(rd, buf)
- if err != nil {
- log.Error("Read failed: %v", err)
- }
+ meta, err := git_model.GetLFSMetaObjectByOid(db.DefaultContext, repoID, pointer.Oid)
+ if err != nil && err != git_model.ErrLFSObjectNotExist { // fallback to plain file
+ return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ }
- ctx.Data["FileContent"] = strings.ReplaceAll(
- gotemplate.HTMLEscapeString(buf.String()), "\n", ` `,
- )
- }
- }
- }
+ dataRc.Close()
+ if err != nil {
+ return nil, nil, nil, err
}
- // Check permission to add or upload new file.
- if ctx.Repo.CanWrite(unit_model.TypeCode) && ctx.Repo.IsViewBranch {
- ctx.Data["CanAddFile"] = !ctx.Repo.Repository.IsArchived
- ctx.Data["CanUploadFile"] = setting.Repository.Upload.Enabled && !ctx.Repo.Repository.IsArchived
+ dataRc, err = lfs.ReadMetaObject(pointer)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ buf = make([]byte, 1024)
+ n, err = util.ReadAtMost(dataRc, buf)
+ if err != nil {
+ dataRc.Close()
+ return nil, nil, nil, err
+ }
+ buf = buf[:n]
+
+ st = typesniffer.DetectContentType(buf)
+
+ return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
+}
+
+func renderReadmeFile(ctx *context.Context, readmeFile *namedBlob, readmeTreelink string) {
+ ctx.Data["RawFileLink"] = ""
+ ctx.Data["ReadmeInList"] = true
+ ctx.Data["ReadmeExist"] = true
+ ctx.Data["FileIsSymlink"] = readmeFile.isSymlink
+
+ buf, dataRc, fInfo, err := getFileReader(ctx.Repo.Repository.ID, readmeFile.blob)
+ if err != nil {
+ ctx.ServerError("getFileReader", err)
+ return
+ }
+ defer dataRc.Close()
+
+ ctx.Data["FileIsText"] = fInfo.isTextFile
+ ctx.Data["FileName"] = readmeFile.name
+ ctx.Data["IsLFSFile"] = fInfo.isLFSFile
+
+ if fInfo.isLFSFile {
+ filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.name))
+ ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.HTMLURL(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
+ }
+
+ if !fInfo.isTextFile {
+ return
+ }
+
+ if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
+ // Pretend that this is a normal text file to display 'This file is too large to be shown'
+ ctx.Data["IsFileTooLarge"] = true
+ ctx.Data["IsTextFile"] = true
+ ctx.Data["FileSize"] = fInfo.fileSize
+ return
+ }
+
+ rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
+
+ if markupType := markup.Type(readmeFile.name); markupType != "" {
+ ctx.Data["IsMarkup"] = true
+ ctx.Data["MarkupType"] = markupType
+
+ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
+ Ctx: ctx,
+ RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.name), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
+ URLPrefix: readmeTreelink,
+ Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
+ GitRepo: ctx.Repo.GitRepo,
+ }, rd)
+ if err != nil {
+ log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.name, ctx.Repo.Repository, err)
+ buf := &bytes.Buffer{}
+ ctx.Data["EscapeStatus"], _ = charset.EscapeControlStringReader(rd, buf, ctx.Locale)
+ ctx.Data["FileContent"] = buf.String()
+ }
+ } else {
+ ctx.Data["IsPlainText"] = true
+ buf := &bytes.Buffer{}
+ ctx.Data["EscapeStatus"], err = charset.EscapeControlStringReader(rd, buf, ctx.Locale)
+ if err != nil {
+ log.Error("Read failed: %v", err)
+ }
+
+ ctx.Data["FileContent"] = buf.String()
}
}
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()
+ buf, dataRc, fInfo, err := getFileReader(ctx.Repo.Repository.ID, blob)
if err != nil {
- ctx.ServerError("DataAsync", err)
+ ctx.ServerError("getFileReader", err)
return
}
defer dataRc.Close()
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
-
- fileSize := blob.Size()
ctx.Data["FileIsSymlink"] = entry.IsLink()
ctx.Data["FileName"] = blob.Name()
ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
- buf := make([]byte, 1024)
- n, _ := util.ReadAtMost(dataRc, buf)
- buf = buf[:n]
-
- st := typesniffer.DetectContentType(buf)
- isTextFile := st.IsText()
+ if ctx.Repo.TreePath == ".editorconfig" {
+ _, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
+ ctx.Data["FileError"] = editorconfigErr
+ }
- isLFSFile := false
isDisplayingSource := ctx.FormString("display") == "source"
isDisplayingRendered := !isDisplayingSource
- // Check for LFS meta file
- if isTextFile && setting.LFS.StartServer {
- pointer, _ := lfs.ReadPointerFromBuffer(buf)
- if pointer.IsValid() {
- meta, err := models.GetLFSMetaObjectByOid(ctx.Repo.Repository.ID, pointer.Oid)
- if err != nil && err != models.ErrLFSObjectNotExist {
- ctx.ServerError("GetLFSMetaObject", err)
- return
- }
- if meta != nil {
- isLFSFile = true
-
- // OK read the lfs object
- var err error
- dataRc, err = lfs.ReadMetaObject(pointer)
- if err != nil {
- ctx.ServerError("ReadMetaObject", err)
- return
- }
- defer dataRc.Close()
-
- buf = make([]byte, 1024)
- n, err = util.ReadAtMost(dataRc, buf)
- if err != nil {
- ctx.ServerError("Data", err)
- return
- }
- buf = buf[:n]
-
- st = typesniffer.DetectContentType(buf)
- isTextFile = st.IsText()
-
- fileSize = meta.Size
- ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
- }
- }
+ if fInfo.isLFSFile {
+ ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
- isRepresentableAsText := st.IsRepresentableAsText()
+ isRepresentableAsText := fInfo.st.IsRepresentableAsText()
if !isRepresentableAsText {
// If we can't show plain text, always try to render.
isDisplayingSource = false
isDisplayingRendered = true
}
- ctx.Data["IsLFSFile"] = isLFSFile
- ctx.Data["FileSize"] = fileSize
- ctx.Data["IsTextFile"] = isTextFile
+ ctx.Data["IsLFSFile"] = fInfo.isLFSFile
+ ctx.Data["FileSize"] = fInfo.fileSize
+ ctx.Data["IsTextFile"] = fInfo.isTextFile
ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
ctx.Data["IsDisplayingSource"] = isDisplayingSource
ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
- ctx.Data["IsTextSource"] = isTextFile || isDisplayingSource
+
+ isTextSource := fInfo.isTextFile || isDisplayingSource
+ ctx.Data["IsTextSource"] = isTextSource
+ if isTextSource {
+ ctx.Data["CanCopyContent"] = true
+ }
// Check LFS Lock
- lfsLock, err := models.GetTreePathLock(ctx.Repo.Repository.ID, ctx.Repo.TreePath)
+ lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
ctx.Data["LFSLock"] = lfsLock
if err != nil {
ctx.ServerError("GetTreePathLock", err)
return
}
if lfsLock != nil {
- u, err := user_model.GetUserByID(lfsLock.OwnerID)
+ u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
if err != nil {
ctx.ServerError("GetTreePathLock", err)
return
}
- ctx.Data["LFSLockOwner"] = u.DisplayName()
+ ctx.Data["LFSLockOwner"] = u.Name
ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
}
// Assume file is not editable first.
- if isLFSFile {
+ if fInfo.isLFSFile {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
} else if !isRepresentableAsText {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
@@ -478,12 +438,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
switch {
case isRepresentableAsText:
- if st.IsSvgImage() {
+ if fInfo.st.IsSvgImage() {
ctx.Data["IsImageFile"] = true
+ ctx.Data["CanCopyContent"] = true
ctx.Data["HasSourceRenderedToggle"] = true
}
- if fileSize >= setting.UI.MaxDisplayFileSize {
+ if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
ctx.Data["IsFileTooLarge"] = true
break
}
@@ -495,6 +456,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
ctx.Data["ReadmeExist"] = readmeExist
markupType := markup.Type(blob.Name())
+ // If the markup is detected by custom markup renderer it should not be reset later on
+ // to not pass it down to the render context.
+ detected := false
+ if markupType == "" {
+ detected = true
+ markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf))
+ }
if markupType != "" {
ctx.Data["HasSourceRenderedToggle"] = true
}
@@ -502,32 +470,35 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
if markupType != "" && !shouldRenderSource {
ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = markupType
- var result strings.Builder
- err := markup.Render(&markup.RenderContext{
- Ctx: ctx,
- Filename: blob.Name(),
- URLPrefix: path.Dir(treeLink),
- Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
- GitRepo: ctx.Repo.GitRepo,
- }, rd, &result)
+ if !detected {
+ markupType = ""
+ }
+ metas := ctx.Repo.Repository.ComposeDocumentMetas()
+ metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
+ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
+ Ctx: ctx,
+ Type: markupType,
+ RelativePath: ctx.Repo.TreePath,
+ URLPrefix: path.Dir(treeLink),
+ Metas: metas,
+ GitRepo: ctx.Repo.GitRepo,
+ }, rd)
if err != nil {
ctx.ServerError("Render", err)
return
}
- ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlString(result.String())
- } else if readmeExist && !shouldRenderSource {
- buf := &bytes.Buffer{}
- ctx.Data["IsRenderedHTML"] = true
-
- ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, buf)
-
- ctx.Data["FileContent"] = strings.ReplaceAll(
- gotemplate.HTMLEscapeString(buf.String()), "\n", ` `,
- )
+ // to prevent iframe load third-party url
+ ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
} else {
buf, _ := io.ReadAll(rd)
- lineNums := linesBytesCount(buf)
- ctx.Data["NumLines"] = strconv.Itoa(lineNums)
+
+ // empty: 0 lines; "a": one line; "a\n": two lines; "a\nb": two lines;
+ // the NumLines is only used for the display on the UI: "xxx lines"
+ if len(buf) == 0 {
+ ctx.Data["NumLines"] = 0
+ } else {
+ ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
+ }
ctx.Data["NumLinesSet"] = true
language := ""
@@ -538,7 +509,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,
@@ -555,18 +526,24 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
language = ""
}
}
- fileContent := highlight.File(lineNums, blob.Name(), language, buf)
- status, _ := charset.EscapeControlReader(bytes.NewReader(buf), io.Discard)
- ctx.Data["EscapeStatus"] = status
- statuses := make([]charset.EscapeStatus, len(fileContent))
+ fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
+ ctx.Data["LexerName"] = lexerName
+ if err != nil {
+ log.Error("highlight.File failed, fallback to plain text: %v", err)
+ fileContent = highlight.PlainText(buf)
+ }
+ status := &charset.EscapeStatus{}
+ statuses := make([]*charset.EscapeStatus, len(fileContent))
for i, line := range fileContent {
- statuses[i], fileContent[i] = charset.EscapeControlString(line)
+ statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
+ status = status.Or(statuses[i])
}
+ ctx.Data["EscapeStatus"] = status
ctx.Data["FileContent"] = fileContent
ctx.Data["LineEscapeStatus"] = statuses
}
- if !isLFSFile {
- if ctx.Repo.CanEnableEditor() {
+ if !fInfo.isLFSFile {
+ if ctx.Repo.CanEnableEditor(ctx.Doer) {
if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
ctx.Data["CanEditFile"] = false
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
@@ -576,21 +553,22 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
}
} else if !ctx.Repo.IsViewBranch {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
- } else if !ctx.Repo.CanWrite(unit_model.TypeCode) {
+ } else if !ctx.Repo.CanWriteToBranch(ctx.Doer, ctx.Repo.BranchName) {
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
}
}
- case st.IsPDF():
+ case fInfo.st.IsPDF():
ctx.Data["IsPDFFile"] = true
- case st.IsVideo():
+ case fInfo.st.IsVideo():
ctx.Data["IsVideoFile"] = true
- case st.IsAudio():
+ case fInfo.st.IsAudio():
ctx.Data["IsAudioFile"] = true
- case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
+ case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
ctx.Data["IsImageFile"] = true
+ ctx.Data["CanCopyContent"] = true
default:
- if fileSize >= setting.UI.MaxDisplayFileSize {
+ if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
ctx.Data["IsFileTooLarge"] = true
break
}
@@ -599,24 +577,21 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
rd := io.MultiReader(bytes.NewReader(buf), dataRc)
ctx.Data["IsMarkup"] = true
ctx.Data["MarkupType"] = markupType
- var result strings.Builder
- err := markup.Render(&markup.RenderContext{
- Ctx: ctx,
- Filename: blob.Name(),
- URLPrefix: path.Dir(treeLink),
- Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
- GitRepo: ctx.Repo.GitRepo,
- }, rd, &result)
+ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
+ Ctx: ctx,
+ RelativePath: ctx.Repo.TreePath,
+ URLPrefix: path.Dir(treeLink),
+ Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
+ GitRepo: ctx.Repo.GitRepo,
+ }, rd)
if err != nil {
ctx.ServerError("Render", err)
return
}
-
- ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlString(result.String())
}
}
- if ctx.Repo.CanEnableEditor() {
+ if ctx.Repo.CanEnableEditor(ctx.Doer) {
if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
ctx.Data["CanDeleteFile"] = false
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
@@ -626,11 +601,28 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
}
} else if !ctx.Repo.IsViewBranch {
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
- } else if !ctx.Repo.CanWrite(unit_model.TypeCode) {
+ } else if !ctx.Repo.CanWriteToBranch(ctx.Doer, ctx.Repo.BranchName) {
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
}
}
+func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output string, err error) {
+ markupRd, markupWr := io.Pipe()
+ defer markupWr.Close()
+ done := make(chan struct{})
+ go func() {
+ sb := &strings.Builder{}
+ // We allow NBSP here this is rendered
+ escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.RuneNBSP)
+ output = sb.String()
+ close(done)
+ }()
+ err = markup.Render(renderCtx, input, markupWr)
+ _ = markupWr.CloseWithError(err)
+ <-done
+ return escaped, output, err
+}
+
func safeURL(address string) string {
u, err := url.Parse(address)
if err != nil {
@@ -643,9 +635,9 @@ func safeURL(address string) string {
func checkHomeCodeViewable(ctx *context.Context) {
if len(ctx.Repo.Units) > 0 {
if ctx.Repo.Repository.IsBeingCreated() {
- task, err := models.GetMigratingTask(ctx.Repo.Repository.ID)
+ task, err := admin_model.GetMigratingTask(ctx.Repo.Repository.ID)
if err != nil {
- if models.IsErrTaskDoesNotExist(err) {
+ if admin_model.IsErrTaskDoesNotExist(err) {
ctx.Data["Repo"] = ctx.Repo
ctx.Data["CloneAddr"] = ""
ctx.Data["Failed"] = true
@@ -671,7 +663,7 @@ func checkHomeCodeViewable(ctx *context.Context) {
if ctx.IsSigned {
// Set repo notification-status read if unread
- if err := models.SetRepoReadBy(ctx.Repo.Repository.ID, ctx.Doer.ID); err != nil {
+ if err := activities_model.SetRepoReadBy(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID); err != nil {
ctx.ServerError("ReadBy", err)
return
}
@@ -698,15 +690,56 @@ func checkHomeCodeViewable(ctx *context.Context) {
ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
}
-// Home render repository home page
-func Home(ctx *context.Context) {
- isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req)
- if isFeed {
- feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
+func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {
+ if entry.Name() != "" {
+ return
+ }
+ tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
+ if err != nil {
+ ctx.NotFoundOrServerError("Repo.Commit.SubTree", git.IsErrNotExist, err)
return
}
+ allEntries, err := tree.ListEntries()
+ if err != nil {
+ ctx.ServerError("ListEntries", err)
+ return
+ }
+ for _, entry := range allEntries {
+ if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" {
+ ctx.Data["CitiationExist"] = true
+ // Read Citation file contents
+ blob := entry.Blob()
+ dataRc, err := blob.DataAsync()
+ if err != nil {
+ ctx.ServerError("DataAsync", err)
+ return
+ }
+ defer dataRc.Close()
+ buf := make([]byte, 1024)
+ n, err := util.ReadAtMost(dataRc, buf)
+ if err != nil {
+ ctx.ServerError("ReadAtMost", err)
+ return
+ }
+ buf = buf[:n]
+ ctx.PageData["citationFileContent"] = string(buf)
+ break
+ }
+ }
+}
- ctx.Data["FeedURL"] = ctx.Repo.Repository.HTMLURL()
+// Home render repository home page
+func Home(ctx *context.Context) {
+ if setting.EnableFeed {
+ isFeed, _, showFeedType := feed.GetFeedType(ctx.Params(":reponame"), ctx.Req)
+ if isFeed {
+ feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
+ return
+ }
+
+ ctx.Data["EnableFeed"] = true
+ ctx.Data["FeedURL"] = ctx.Repo.Repository.HTMLURL()
+ }
checkHomeCodeViewable(ctx)
if ctx.Written() {
@@ -782,28 +815,21 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
defer cancel()
}
- var c *git.LastCommitCache
- if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount {
- c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
- }
-
- selected := map[string]bool{}
- for _, pth := range ctx.FormStrings("f[]") {
- selected[pth] = true
- }
+ selected := make(container.Set[string])
+ selected.AddMultiple(ctx.FormStrings("f[]")...)
entries := allEntries
if len(selected) > 0 {
entries = make(git.Entries, 0, len(selected))
for _, entry := range allEntries {
- if selected[entry.Name()] {
+ if selected.Contains(entry.Name()) {
entries = append(entries, entry)
}
}
}
var latestCommit *git.Commit
- ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath, c)
+ ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("GetCommitsInfo", err)
return nil
@@ -817,22 +843,22 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
verification := asymkey_model.ParseCommitWithSignature(latestCommit)
if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
- return models.IsOwnerMemberCollaborator(ctx.Repo.Repository, user.ID)
+ return repo_model.IsOwnerMemberCollaborator(ctx.Repo.Repository, user.ID)
}, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return nil
}
ctx.Data["LatestCommitVerification"] = verification
ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(latestCommit)
- }
- statuses, _, err := models.GetLatestCommitStatus(ctx.Repo.Repository.ID, ctx.Repo.Commit.ID.String(), db.ListOptions{})
- if err != nil {
- log.Error("GetLatestCommitStatus: %v", err)
- }
+ statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptions{})
+ if err != nil {
+ log.Error("GetLatestCommitStatus: %v", err)
+ }
- ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(statuses)
- ctx.Data["LatestCommitStatuses"] = statuses
+ ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(statuses)
+ ctx.Data["LatestCommitStatuses"] = statuses
+ }
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
treeLink := branchLink
@@ -872,10 +898,14 @@ func renderCode(ctx *context.Context) {
ctx.Data["PageIsViewCode"] = true
if ctx.Repo.Repository.IsEmpty {
- reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
- if err != nil {
- ctx.ServerError("GitRepo.IsEmpty", err)
- return
+ reallyEmpty := true
+ var err error
+ if ctx.Repo.GitRepo != nil {
+ reallyEmpty, err = ctx.Repo.GitRepo.IsEmpty()
+ if err != nil {
+ ctx.ServerError("GitRepo.IsEmpty", err)
+ return
+ }
}
if reallyEmpty {
ctx.HTML(http.StatusOK, tplRepoEMPTY)
@@ -888,11 +918,11 @@ func renderCode(ctx *context.Context) {
// it's possible for a repository to be non-empty by that flag but still 500
// because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed.
ctx.Repo.Repository.IsEmpty = false
- if err = repo_model.UpdateRepositoryCols(ctx.Repo.Repository, "is_empty"); err != nil {
+ if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil {
ctx.ServerError("UpdateRepositoryCols", err)
return
}
- if err = models.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil {
+ if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil {
ctx.ServerError("UpdateRepoSize", err)
return
}
@@ -925,6 +955,13 @@ func renderCode(ctx *context.Context) {
return
}
+ if !ctx.Repo.Repository.IsEmpty {
+ checkCitationFile(ctx, entry)
+ if ctx.Written() {
+ return
+ }
+ }
+
renderLanguageStats(ctx)
if ctx.Written() {
return
@@ -966,12 +1003,12 @@ func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOp
if page <= 0 {
page = 1
}
- pager := context.NewPagination(total, models.ItemsPerPage, page, 5)
+ pager := context.NewPagination(total, setting.ItemsPerPage, page, 5)
ctx.Data["Page"] = pager
items, err := getter(db.ListOptions{
Page: pager.Paginater.Current(),
- PageSize: models.ItemsPerPage,
+ PageSize: setting.ItemsPerPage,
})
if err != nil {
ctx.ServerError("getter", err)
@@ -1012,12 +1049,12 @@ func Forks(ctx *context.Context) {
page = 1
}
- pager := context.NewPagination(ctx.Repo.Repository.NumForks, models.ItemsPerPage, page, 5)
+ pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.ItemsPerPage, page, 5)
ctx.Data["Page"] = pager
forks, err := repo_model.GetForks(ctx.Repo.Repository, db.ListOptions{
Page: pager.Paginater.Current(),
- PageSize: models.ItemsPerPage,
+ PageSize: setting.ItemsPerPage,
})
if err != nil {
ctx.ServerError("GetForks", err)
diff --git a/routers/web/repo/view_test.go b/routers/web/repo/view_test.go
new file mode 100644
index 0000000000000..73ba118823bf6
--- /dev/null
+++ b/routers/web/repo/view_test.go
@@ -0,0 +1,62 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "reflect"
+ "testing"
+)
+
+func Test_localizedExtensions(t *testing.T) {
+ tests := []struct {
+ name string
+ ext string
+ languageCode string
+ wantLocalizedExts []string
+ }{
+ {
+ name: "empty language",
+ ext: ".md",
+ wantLocalizedExts: []string{".md"},
+ },
+ {
+ name: "No region - lowercase",
+ languageCode: "en",
+ ext: ".csv",
+ wantLocalizedExts: []string{".en.csv", ".csv"},
+ },
+ {
+ name: "No region - uppercase",
+ languageCode: "FR",
+ ext: ".txt",
+ wantLocalizedExts: []string{".fr.txt", ".txt"},
+ },
+ {
+ name: "With region - lowercase",
+ languageCode: "en-us",
+ ext: ".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", "_en.MD", ".MD"},
+ },
+ {
+ name: "With region - all uppercase",
+ languageCode: "ZH-TW",
+ ext: ".md",
+ wantLocalizedExts: []string{".zh-tw.md", ".zh_tw.md", ".zh.md", "_zh.md", ".md"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if gotLocalizedExts := localizedExtensions(tt.ext, tt.languageCode); !reflect.DeepEqual(gotLocalizedExts, tt.wantLocalizedExts) {
+ t.Errorf("localizedExtensions() = %v, want %v", gotLocalizedExts, tt.wantLocalizedExts)
+ }
+ })
+ }
+}
diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go
index 2dafd4b5f4263..96261af674461 100644
--- a/routers/web/repo/webhook.go
+++ b/routers/web/repo/webhook.go
@@ -1,7 +1,6 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -18,13 +17,14 @@ import (
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
+ "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
webhook_service "code.gitea.io/gitea/services/webhook"
)
@@ -44,7 +44,7 @@ func Webhooks(ctx *context.Context) {
ctx.Data["BaseLinkNew"] = ctx.Repo.RepoLink + "/settings/hooks"
ctx.Data["Description"] = ctx.Tr("repo.settings.hooks_desc", "https://docs.gitea.io/en-us/webhooks/")
- ws, err := webhook.ListWebhooksByOpts(&webhook.ListWebhookOptions{RepoID: ctx.Repo.Repository.ID})
+ ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{RepoID: ctx.Repo.Repository.ID})
if err != nil {
ctx.ServerError("GetWebhooksByRepoID", err)
return
@@ -110,7 +110,7 @@ func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) {
func checkHookType(ctx *context.Context) string {
hookType := strings.ToLower(ctx.Params(":type"))
- if !util.IsStringInSlice(hookType, setting.Webhook.Types, true) {
+ if !util.SliceContainsString(setting.Webhook.Types, hookType, true) {
ctx.NotFound("checkHookType", nil)
return ""
}
@@ -120,7 +120,7 @@ func checkHookType(ctx *context.Context) string {
// WebhooksNew render creating webhook page
func WebhooksNew(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
+ ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook_module.HookEvent{}}
orCtx, err := getOrgRepoCtx(ctx)
if err != nil {
@@ -147,7 +147,6 @@ func WebhooksNew(ctx *context.Context) {
if hookType == "discord" {
ctx.Data["DiscordHook"] = map[string]interface{}{
"Username": "Gitea",
- "IconURL": setting.AppURL + "img/favicon.png",
}
}
ctx.Data["BaseLink"] = orCtx.LinkNew
@@ -156,12 +155,12 @@ func WebhooksNew(ctx *context.Context) {
}
// ParseHookEvent convert web form content to webhook.HookEvent
-func ParseHookEvent(form forms.WebhookForm) *webhook.HookEvent {
- return &webhook.HookEvent{
+func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
+ return &webhook_module.HookEvent{
PushOnly: form.PushOnly(),
SendEverything: form.SendEverything(),
ChooseEvents: form.ChooseEvents(),
- HookEvents: webhook.HookEvents{
+ HookEvents: webhook_module.HookEvents{
Create: form.Create,
Delete: form.Delete,
Fork: form.Fork,
@@ -179,6 +178,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook.HookEvent {
PullRequestComment: form.PullRequestComment,
PullRequestReview: form.PullRequestReview,
PullRequestSync: form.PullRequestSync,
+ Wiki: form.Wiki,
Repository: form.Repository,
Package: form.Package,
},
@@ -186,69 +186,24 @@ func ParseHookEvent(form forms.WebhookForm) *webhook.HookEvent {
}
}
-// GiteaHooksNewPost response for creating Gitea webhook
-func GiteaHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewWebhookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
- ctx.Data["HookType"] = webhook.GITEA
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
- ctx.Data["BaseLink"] = orCtx.LinkNew
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- contentType := webhook.ContentTypeJSON
- if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm {
- contentType = webhook.ContentTypeForm
- }
-
- w := &webhook.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- HTTPMethod: form.HTTPMethod,
- ContentType: contentType,
- Secret: form.Secret,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: webhook.GITEA,
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.CreateWebhook(ctx, w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
+type webhookParams struct {
+ // Type should be imported from webhook package (webhook.XXX)
+ Type string
-// GogsHooksNewPost response for creating webhook
-func GogsHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewGogshookForm)
- newGogsWebhookPost(ctx, *form, webhook.GOGS)
+ URL string
+ ContentType webhook.HookContentType
+ Secret string
+ HTTPMethod string
+ WebhookForm forms.WebhookForm
+ Meta interface{}
}
-// newGogsWebhookPost response for creating gogs hook
-func newGogsWebhookPost(ctx *context.Context, form forms.NewGogshookForm, kind webhook.HookType) {
+func createWebhook(ctx *context.Context, params webhookParams) {
ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
ctx.Data["PageIsSettingsHooks"] = true
ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
- ctx.Data["HookType"] = webhook.GOGS
+ ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook_module.HookEvent{}}
+ ctx.Data["HookType"] = params.Type
orCtx, err := getOrgRepoCtx(ctx)
if err != nil {
@@ -262,796 +217,47 @@ func newGogsWebhookPost(ctx *context.Context, form forms.NewGogshookForm, kind w
return
}
- contentType := webhook.ContentTypeJSON
- if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm {
- contentType = webhook.ContentTypeForm
- }
-
- w := &webhook.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: contentType,
- Secret: form.Secret,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: kind,
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.CreateWebhook(ctx, w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// DiscordHooksNewPost response for creating discord hook
-func DiscordHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
- ctx.Data["HookType"] = webhook.DISCORD
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- meta, err := json.Marshal(&webhook_service.DiscordMeta{
- Username: form.Username,
- IconURL: form.IconURL,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w := &webhook.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: webhook.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: webhook.DISCORD,
- Meta: string(meta),
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.CreateWebhook(ctx, w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// DingtalkHooksNewPost response for creating dingtalk hook
-func DingtalkHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
- ctx.Data["HookType"] = webhook.DINGTALK
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- w := &webhook.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: webhook.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: webhook.DINGTALK,
- Meta: "",
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.CreateWebhook(ctx, w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// TelegramHooksNewPost response for creating telegram hook
-func TelegramHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
- ctx.Data["HookType"] = webhook.TELEGRAM
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- meta, err := json.Marshal(&webhook_service.TelegramMeta{
- BotToken: form.BotToken,
- ChatID: form.ChatID,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w := &webhook.Webhook{
- RepoID: orCtx.RepoID,
- URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID)),
- ContentType: webhook.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: webhook.TELEGRAM,
- Meta: string(meta),
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.CreateWebhook(ctx, w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// MatrixHooksNewPost response for creating a Matrix hook
-func MatrixHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
- ctx.Data["HookType"] = webhook.MATRIX
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- meta, err := json.Marshal(&webhook_service.MatrixMeta{
- HomeserverURL: form.HomeserverURL,
- Room: form.RoomID,
- AccessToken: form.AccessToken,
- MessageType: form.MessageType,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w := &webhook.Webhook{
- RepoID: orCtx.RepoID,
- URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)),
- ContentType: webhook.ContentTypeJSON,
- HTTPMethod: "PUT",
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: webhook.MATRIX,
- Meta: string(meta),
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.CreateWebhook(ctx, w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// MSTeamsHooksNewPost response for creating MS Teams hook
-func MSTeamsHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
- ctx.Data["HookType"] = webhook.MSTEAMS
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- w := &webhook.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: webhook.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: webhook.MSTEAMS,
- Meta: "",
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.CreateWebhook(ctx, w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// SlackHooksNewPost response for creating slack hook
-func SlackHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewSlackHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
- ctx.Data["HookType"] = webhook.SLACK
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- if form.HasInvalidChannel() {
- ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
- ctx.Redirect(orCtx.LinkNew + "/slack/new")
- return
- }
-
- meta, err := json.Marshal(&webhook_service.SlackMeta{
- Channel: strings.TrimSpace(form.Channel),
- Username: form.Username,
- IconURL: form.IconURL,
- Color: form.Color,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w := &webhook.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: webhook.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: webhook.SLACK,
- Meta: string(meta),
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.CreateWebhook(ctx, w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// FeishuHooksNewPost response for creating feishu hook
-func FeishuHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
- ctx.Data["HookType"] = webhook.FEISHU
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- w := &webhook.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: webhook.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: webhook.FEISHU,
- Meta: "",
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.CreateWebhook(ctx, w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// WechatworkHooksNewPost response for creating wechatwork hook
-func WechatworkHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewWechatWorkHookForm)
-
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
- ctx.Data["HookType"] = webhook.WECHATWORK
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- w := &webhook.Webhook{
- RepoID: orCtx.RepoID,
- URL: form.PayloadURL,
- ContentType: webhook.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: webhook.WECHATWORK,
- Meta: "",
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.CreateWebhook(ctx, w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-// PackagistHooksNewPost response for creating packagist hook
-func PackagistHooksNewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewPackagistHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksNew"] = true
- ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}}
- ctx.Data["HookType"] = webhook.PACKAGIST
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return
- }
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- meta, err := json.Marshal(&webhook_service.PackagistMeta{
- Username: form.Username,
- APIToken: form.APIToken,
- PackageURL: form.PackageURL,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w := &webhook.Webhook{
- RepoID: orCtx.RepoID,
- URL: fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)),
- ContentType: webhook.ContentTypeJSON,
- HookEvent: ParseHookEvent(form.WebhookForm),
- IsActive: form.Active,
- Type: webhook.PACKAGIST,
- Meta: string(meta),
- OrgID: orCtx.OrgID,
- IsSystemWebhook: orCtx.IsSystemWebhook,
- }
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.CreateWebhook(ctx, w); err != nil {
- ctx.ServerError("CreateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
- ctx.Redirect(orCtx.Link)
-}
-
-func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) {
- ctx.Data["RequireHighlightJS"] = true
-
- orCtx, err := getOrgRepoCtx(ctx)
- if err != nil {
- ctx.ServerError("getOrgRepoCtx", err)
- return nil, nil
- }
- ctx.Data["BaseLink"] = orCtx.Link
-
- var w *webhook.Webhook
- if orCtx.RepoID > 0 {
- w, err = webhook.GetWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
- } else if orCtx.OrgID > 0 {
- w, err = webhook.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
- } else if orCtx.IsAdmin {
- w, err = webhook.GetSystemOrDefaultWebhook(ctx.ParamsInt64(":id"))
- }
- if err != nil || w == nil {
- if webhook.IsErrWebhookNotExist(err) {
- ctx.NotFound("GetWebhookByID", nil)
- } else {
- ctx.ServerError("GetWebhookByID", err)
- }
- return nil, nil
- }
-
- ctx.Data["HookType"] = w.Type
- switch w.Type {
- case webhook.SLACK:
- ctx.Data["SlackHook"] = webhook_service.GetSlackHook(w)
- case webhook.DISCORD:
- ctx.Data["DiscordHook"] = webhook_service.GetDiscordHook(w)
- case webhook.TELEGRAM:
- ctx.Data["TelegramHook"] = webhook_service.GetTelegramHook(w)
- case webhook.MATRIX:
- ctx.Data["MatrixHook"] = webhook_service.GetMatrixHook(w)
- case webhook.PACKAGIST:
- ctx.Data["PackagistHook"] = webhook_service.GetPackagistHook(w)
- }
-
- ctx.Data["History"], err = w.History(1)
- if err != nil {
- ctx.ServerError("History", err)
- }
- return orCtx, w
-}
-
-// WebHooksEdit render editing web hook page
-func WebHooksEdit(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
-}
-
-// WebHooksEditPost response for editing web hook
-func WebHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewWebhookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- contentType := webhook.ContentTypeJSON
- if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm {
- contentType = webhook.ContentTypeForm
- }
-
- w.URL = form.PayloadURL
- w.ContentType = contentType
- w.Secret = form.Secret
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- w.HTTPMethod = form.HTTPMethod
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.UpdateWebhook(w); err != nil {
- ctx.ServerError("WebHooksEditPost", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// GogsHooksEditPost response for editing gogs hook
-func GogsHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewGogshookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- contentType := webhook.ContentTypeJSON
- if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm {
- contentType = webhook.ContentTypeForm
- }
-
- w.URL = form.PayloadURL
- w.ContentType = contentType
- w.Secret = form.Secret
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.UpdateWebhook(w); err != nil {
- ctx.ServerError("GogsHooksEditPost", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// SlackHooksEditPost response for editing slack hook
-func SlackHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewSlackHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- if form.HasInvalidChannel() {
- ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
- return
- }
-
- meta, err := json.Marshal(&webhook_service.SlackMeta{
- Channel: strings.TrimSpace(form.Channel),
- Username: form.Username,
- IconURL: form.IconURL,
- Color: form.Color,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w.URL = form.PayloadURL
- w.Meta = string(meta)
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// DiscordHooksEditPost response for editing discord hook
-func DiscordHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- meta, err := json.Marshal(&webhook_service.DiscordMeta{
- Username: form.Username,
- IconURL: form.IconURL,
- })
- if err != nil {
- ctx.ServerError("Marshal", err)
- return
- }
-
- w.URL = form.PayloadURL
- w.Meta = string(meta)
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// DingtalkHooksEditPost response for editing discord hook
-func DingtalkHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- w.URL = form.PayloadURL
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
-}
-
-// TelegramHooksEditPost response for editing discord hook
-func TelegramHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
-
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
- }
- ctx.Data["Webhook"] = w
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
- }
-
- meta, err := json.Marshal(&webhook_service.TelegramMeta{
- BotToken: form.BotToken,
- ChatID: form.ChatID,
- })
+ var meta []byte
+ if params.Meta != nil {
+ meta, err = json.Marshal(params.Meta)
+ if err != nil {
+ ctx.ServerError("Marshal", err)
+ return
+ }
+ }
+
+ w := &webhook.Webhook{
+ RepoID: orCtx.RepoID,
+ URL: params.URL,
+ HTTPMethod: params.HTTPMethod,
+ ContentType: params.ContentType,
+ Secret: params.Secret,
+ HookEvent: ParseHookEvent(params.WebhookForm),
+ IsActive: params.WebhookForm.Active,
+ Type: params.Type,
+ Meta: string(meta),
+ OrgID: orCtx.OrgID,
+ IsSystemWebhook: orCtx.IsSystemWebhook,
+ }
+ err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader)
if err != nil {
- ctx.ServerError("Marshal", err)
+ ctx.ServerError("SetHeaderAuthorization", err)
return
}
- w.Meta = string(meta)
- w.URL = fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID))
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err)
return
- } else if err := webhook.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
+ } else if err := webhook.CreateWebhook(ctx, w); err != nil {
+ ctx.ServerError("CreateWebhook", err)
return
}
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+ ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success"))
+ ctx.Redirect(orCtx.Link)
}
-// MatrixHooksEditPost response for editing a Matrix hook
-func MatrixHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
+func editWebhook(ctx *context.Context, params webhookParams) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
ctx.Data["PageIsSettingsHooks"] = true
ctx.Data["PageIsSettingsHooksEdit"] = true
@@ -1066,21 +272,30 @@ func MatrixHooksEditPost(ctx *context.Context) {
return
}
- meta, err := json.Marshal(&webhook_service.MatrixMeta{
- HomeserverURL: form.HomeserverURL,
- Room: form.RoomID,
- AccessToken: form.AccessToken,
- MessageType: form.MessageType,
- })
+ var meta []byte
+ var err error
+ if params.Meta != nil {
+ meta, err = json.Marshal(params.Meta)
+ if err != nil {
+ ctx.ServerError("Marshal", err)
+ return
+ }
+ }
+
+ w.URL = params.URL
+ w.ContentType = params.ContentType
+ w.Secret = params.Secret
+ w.HookEvent = ParseHookEvent(params.WebhookForm)
+ w.IsActive = params.WebhookForm.Active
+ w.HTTPMethod = params.HTTPMethod
+ w.Meta = string(meta)
+
+ err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader)
if err != nil {
- ctx.ServerError("Marshal", err)
+ ctx.ServerError("SetHeaderAuthorization", err)
return
}
- w.Meta = string(meta)
- w.URL = fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID))
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
if err := w.UpdateEvent(); err != nil {
ctx.ServerError("UpdateEvent", err)
return
@@ -1093,147 +308,334 @@ func MatrixHooksEditPost(ctx *context.Context) {
ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
}
-// MSTeamsHooksEditPost response for editing MS Teams hook
-func MSTeamsHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
+// GiteaHooksNewPost response for creating Gitea webhook
+func GiteaHooksNewPost(ctx *context.Context) {
+ createWebhook(ctx, giteaHookParams(ctx))
+}
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
+// GiteaHooksEditPost response for editing Gitea webhook
+func GiteaHooksEditPost(ctx *context.Context) {
+ editWebhook(ctx, giteaHookParams(ctx))
+}
+
+func giteaHookParams(ctx *context.Context) webhookParams {
+ form := web.GetForm(ctx).(*forms.NewWebhookForm)
+
+ contentType := webhook.ContentTypeJSON
+ if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm {
+ contentType = webhook.ContentTypeForm
}
- ctx.Data["Webhook"] = w
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
+ return webhookParams{
+ Type: webhook_module.GITEA,
+ URL: form.PayloadURL,
+ ContentType: contentType,
+ Secret: form.Secret,
+ HTTPMethod: form.HTTPMethod,
+ WebhookForm: form.WebhookForm,
}
+}
- w.URL = form.PayloadURL
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
+// GogsHooksNewPost response for creating Gogs webhook
+func GogsHooksNewPost(ctx *context.Context) {
+ createWebhook(ctx, gogsHookParams(ctx))
+}
+
+// GogsHooksEditPost response for editing Gogs webhook
+func GogsHooksEditPost(ctx *context.Context) {
+ editWebhook(ctx, gogsHookParams(ctx))
+}
+
+func gogsHookParams(ctx *context.Context) webhookParams {
+ form := web.GetForm(ctx).(*forms.NewGogshookForm)
+
+ contentType := webhook.ContentTypeJSON
+ if webhook.HookContentType(form.ContentType) == webhook.ContentTypeForm {
+ contentType = webhook.ContentTypeForm
}
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+ return webhookParams{
+ Type: webhook_module.GOGS,
+ URL: form.PayloadURL,
+ ContentType: contentType,
+ Secret: form.Secret,
+ WebhookForm: form.WebhookForm,
+ }
}
-// FeishuHooksEditPost response for editing feishu hook
-func FeishuHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
+// DiscordHooksNewPost response for creating Discord webhook
+func DiscordHooksNewPost(ctx *context.Context) {
+ createWebhook(ctx, discordHookParams(ctx))
+}
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
+// DiscordHooksEditPost response for editing Discord webhook
+func DiscordHooksEditPost(ctx *context.Context) {
+ editWebhook(ctx, discordHookParams(ctx))
+}
+
+func discordHookParams(ctx *context.Context) webhookParams {
+ form := web.GetForm(ctx).(*forms.NewDiscordHookForm)
+
+ return webhookParams{
+ Type: webhook_module.DISCORD,
+ URL: form.PayloadURL,
+ ContentType: webhook.ContentTypeJSON,
+ WebhookForm: form.WebhookForm,
+ Meta: &webhook_service.DiscordMeta{
+ Username: form.Username,
+ IconURL: form.IconURL,
+ },
}
- ctx.Data["Webhook"] = w
+}
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
+// DingtalkHooksNewPost response for creating Dingtalk webhook
+func DingtalkHooksNewPost(ctx *context.Context) {
+ createWebhook(ctx, dingtalkHookParams(ctx))
+}
+
+// DingtalkHooksEditPost response for editing Dingtalk webhook
+func DingtalkHooksEditPost(ctx *context.Context) {
+ editWebhook(ctx, dingtalkHookParams(ctx))
+}
+
+func dingtalkHookParams(ctx *context.Context) webhookParams {
+ form := web.GetForm(ctx).(*forms.NewDingtalkHookForm)
+
+ return webhookParams{
+ Type: webhook_module.DINGTALK,
+ URL: form.PayloadURL,
+ ContentType: webhook.ContentTypeJSON,
+ WebhookForm: form.WebhookForm,
}
+}
- w.URL = form.PayloadURL
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
+// TelegramHooksNewPost response for creating Telegram webhook
+func TelegramHooksNewPost(ctx *context.Context) {
+ createWebhook(ctx, telegramHookParams(ctx))
+}
+
+// TelegramHooksEditPost response for editing Telegram webhook
+func TelegramHooksEditPost(ctx *context.Context) {
+ editWebhook(ctx, telegramHookParams(ctx))
+}
+
+func telegramHookParams(ctx *context.Context) webhookParams {
+ form := web.GetForm(ctx).(*forms.NewTelegramHookForm)
+
+ return webhookParams{
+ Type: webhook_module.TELEGRAM,
+ URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID)),
+ ContentType: webhook.ContentTypeJSON,
+ WebhookForm: form.WebhookForm,
+ Meta: &webhook_service.TelegramMeta{
+ BotToken: form.BotToken,
+ ChatID: form.ChatID,
+ },
}
+}
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+// MatrixHooksNewPost response for creating Matrix webhook
+func MatrixHooksNewPost(ctx *context.Context) {
+ createWebhook(ctx, matrixHookParams(ctx))
}
-// WechatworkHooksEditPost response for editing wechatwork hook
-func WechatworkHooksEditPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.NewWechatWorkHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
+// MatrixHooksEditPost response for editing Matrix webhook
+func MatrixHooksEditPost(ctx *context.Context) {
+ editWebhook(ctx, matrixHookParams(ctx))
+}
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
+func matrixHookParams(ctx *context.Context) webhookParams {
+ form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
+
+ return webhookParams{
+ Type: webhook_module.MATRIX,
+ URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)),
+ ContentType: webhook.ContentTypeJSON,
+ HTTPMethod: http.MethodPut,
+ WebhookForm: form.WebhookForm,
+ Meta: &webhook_service.MatrixMeta{
+ HomeserverURL: form.HomeserverURL,
+ Room: form.RoomID,
+ MessageType: form.MessageType,
+ },
}
- ctx.Data["Webhook"] = w
+}
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
+// MSTeamsHooksNewPost response for creating MSTeams webhook
+func MSTeamsHooksNewPost(ctx *context.Context) {
+ createWebhook(ctx, mSTeamsHookParams(ctx))
+}
+
+// MSTeamsHooksEditPost response for editing MSTeams webhook
+func MSTeamsHooksEditPost(ctx *context.Context) {
+ editWebhook(ctx, mSTeamsHookParams(ctx))
+}
+
+func mSTeamsHookParams(ctx *context.Context) webhookParams {
+ form := web.GetForm(ctx).(*forms.NewMSTeamsHookForm)
+
+ return webhookParams{
+ Type: webhook_module.MSTEAMS,
+ URL: form.PayloadURL,
+ ContentType: webhook.ContentTypeJSON,
+ WebhookForm: form.WebhookForm,
}
+}
- w.URL = form.PayloadURL
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
- return
+// SlackHooksNewPost response for creating Slack webhook
+func SlackHooksNewPost(ctx *context.Context) {
+ createWebhook(ctx, slackHookParams(ctx))
+}
+
+// SlackHooksEditPost response for editing Slack webhook
+func SlackHooksEditPost(ctx *context.Context) {
+ editWebhook(ctx, slackHookParams(ctx))
+}
+
+func slackHookParams(ctx *context.Context) webhookParams {
+ form := web.GetForm(ctx).(*forms.NewSlackHookForm)
+
+ return webhookParams{
+ Type: webhook_module.SLACK,
+ URL: form.PayloadURL,
+ ContentType: webhook.ContentTypeJSON,
+ WebhookForm: form.WebhookForm,
+ Meta: &webhook_service.SlackMeta{
+ Channel: strings.TrimSpace(form.Channel),
+ Username: form.Username,
+ IconURL: form.IconURL,
+ Color: form.Color,
+ },
}
+}
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+// FeishuHooksNewPost response for creating Feishu webhook
+func FeishuHooksNewPost(ctx *context.Context) {
+ createWebhook(ctx, feishuHookParams(ctx))
+}
+
+// FeishuHooksEditPost response for editing Feishu webhook
+func FeishuHooksEditPost(ctx *context.Context) {
+ editWebhook(ctx, feishuHookParams(ctx))
+}
+
+func feishuHookParams(ctx *context.Context) webhookParams {
+ form := web.GetForm(ctx).(*forms.NewFeishuHookForm)
+
+ return webhookParams{
+ Type: webhook_module.FEISHU,
+ URL: form.PayloadURL,
+ ContentType: webhook.ContentTypeJSON,
+ WebhookForm: form.WebhookForm,
+ }
+}
+
+// WechatworkHooksNewPost response for creating Wechatwork webhook
+func WechatworkHooksNewPost(ctx *context.Context) {
+ createWebhook(ctx, wechatworkHookParams(ctx))
+}
+
+// WechatworkHooksEditPost response for editing Wechatwork webhook
+func WechatworkHooksEditPost(ctx *context.Context) {
+ editWebhook(ctx, wechatworkHookParams(ctx))
+}
+
+func wechatworkHookParams(ctx *context.Context) webhookParams {
+ form := web.GetForm(ctx).(*forms.NewWechatWorkHookForm)
+
+ return webhookParams{
+ Type: webhook_module.WECHATWORK,
+ URL: form.PayloadURL,
+ ContentType: webhook.ContentTypeJSON,
+ WebhookForm: form.WebhookForm,
+ }
+}
+
+// PackagistHooksNewPost response for creating Packagist webhook
+func PackagistHooksNewPost(ctx *context.Context) {
+ createWebhook(ctx, packagistHookParams(ctx))
}
-// PackagistHooksEditPost response for editing packagist hook
+// PackagistHooksEditPost response for editing Packagist webhook
func PackagistHooksEditPost(ctx *context.Context) {
+ editWebhook(ctx, packagistHookParams(ctx))
+}
+
+func packagistHookParams(ctx *context.Context) webhookParams {
form := web.GetForm(ctx).(*forms.NewPackagistHookForm)
- ctx.Data["Title"] = ctx.Tr("repo.settings")
- ctx.Data["PageIsSettingsHooks"] = true
- ctx.Data["PageIsSettingsHooksEdit"] = true
- orCtx, w := checkWebhook(ctx)
- if ctx.Written() {
- return
+ return webhookParams{
+ Type: webhook_module.PACKAGIST,
+ URL: fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)),
+ ContentType: webhook.ContentTypeJSON,
+ WebhookForm: form.WebhookForm,
+ Meta: &webhook_service.PackagistMeta{
+ Username: form.Username,
+ APIToken: form.APIToken,
+ PackageURL: form.PackageURL,
+ },
}
- ctx.Data["Webhook"] = w
+}
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, orCtx.NewTemplate)
- return
+func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) {
+ orCtx, err := getOrgRepoCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getOrgRepoCtx", err)
+ return nil, nil
}
+ ctx.Data["BaseLink"] = orCtx.Link
- meta, err := json.Marshal(&webhook_service.PackagistMeta{
- Username: form.Username,
- APIToken: form.APIToken,
- PackageURL: form.PackageURL,
- })
+ var w *webhook.Webhook
+ if orCtx.RepoID > 0 {
+ w, err = webhook.GetWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id"))
+ } else if orCtx.OrgID > 0 {
+ w, err = webhook.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id"))
+ } else if orCtx.IsAdmin {
+ w, err = webhook.GetSystemOrDefaultWebhook(ctx.ParamsInt64(":id"))
+ }
+ if err != nil || w == nil {
+ if webhook.IsErrWebhookNotExist(err) {
+ ctx.NotFound("GetWebhookByID", nil)
+ } else {
+ ctx.ServerError("GetWebhookByID", err)
+ }
+ return nil, nil
+ }
+
+ ctx.Data["HookType"] = w.Type
+ switch w.Type {
+ case webhook_module.SLACK:
+ ctx.Data["SlackHook"] = webhook_service.GetSlackHook(w)
+ case webhook_module.DISCORD:
+ ctx.Data["DiscordHook"] = webhook_service.GetDiscordHook(w)
+ case webhook_module.TELEGRAM:
+ ctx.Data["TelegramHook"] = webhook_service.GetTelegramHook(w)
+ case webhook_module.MATRIX:
+ ctx.Data["MatrixHook"] = webhook_service.GetMatrixHook(w)
+ case webhook_module.PACKAGIST:
+ ctx.Data["PackagistHook"] = webhook_service.GetPackagistHook(w)
+ }
+
+ ctx.Data["History"], err = w.History(1)
if err != nil {
- ctx.ServerError("Marshal", err)
- return
+ ctx.ServerError("History", err)
}
+ return orCtx, w
+}
- w.Meta = string(meta)
- w.URL = fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken))
- w.HookEvent = ParseHookEvent(form.WebhookForm)
- w.IsActive = form.Active
- if err := w.UpdateEvent(); err != nil {
- ctx.ServerError("UpdateEvent", err)
- return
- } else if err := webhook.UpdateWebhook(w); err != nil {
- ctx.ServerError("UpdateWebhook", err)
+// WebHooksEdit render editing web hook page
+func WebHooksEdit(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.settings.update_webhook")
+ ctx.Data["PageIsSettingsHooks"] = true
+ ctx.Data["PageIsSettingsHooksEdit"] = true
+
+ orCtx, w := checkWebhook(ctx)
+ if ctx.Written() {
return
}
+ ctx.Data["Webhook"] = w
- ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success"))
- ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
+ ctx.HTML(http.StatusOK, orCtx.NewTemplate)
}
// TestWebhook test if web hook is work fine
@@ -1241,7 +643,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
}
@@ -1274,17 +676,20 @@ func TestWebhook(ctx *context.Context) {
},
}
+ commitID := commit.ID.String()
p := &api.PushPayload{
- Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch,
- Before: commit.ID.String(),
- After: commit.ID.String(),
- Commits: []*api.PayloadCommit{apiCommit},
- 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 {
+ 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, ctx.Repo.Repository, perm.AccessModeNone),
+ Pusher: apiUser,
+ Sender: apiUser,
+ }
+ if err := webhook_service.PrepareWebhook(ctx, w, webhook_module.HookEventPush, p); err != nil {
ctx.Flash.Error("PrepareWebhook: " + err.Error())
ctx.Status(http.StatusInternalServerError)
} else {
@@ -1302,7 +707,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 32f596ff370fb..fe2becb7bb473 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -1,7 +1,6 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package repo
@@ -13,8 +12,10 @@ import (
"net/url"
"path/filepath"
"strings"
+ "time"
- "code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
@@ -23,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
@@ -57,7 +59,7 @@ func MustEnableWiki(ctx *context.Context) {
return
}
- unit, err := ctx.Repo.Repository.GetUnit(unit.TypeExternalWiki)
+ unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki)
if err == nil {
ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL)
return
@@ -96,7 +98,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
}
@@ -162,7 +164,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
}
wikiName, err := wiki_service.FilenameToName(entry.Name())
if err != nil {
- if models.IsErrWikiInvalidFileName(err) {
+ if repo_model.IsErrWikiInvalidFileName(err) {
continue
}
if wikiRepo != nil {
@@ -189,7 +191,9 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
ctx.Data["old_title"] = pageName
ctx.Data["Title"] = pageName
ctx.Data["title"] = pageName
- ctx.Data["RequireHighlightJS"] = true
+
+ isSideBar := pageName == "_Sidebar"
+ isFooter := pageName == "_Footer"
// lookup filename in wiki - get filecontent, gitTree entry , real filename
data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
@@ -203,20 +207,30 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
return nil, nil
}
- sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar")
- if ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
+ var sidebarContent []byte
+ if !isSideBar {
+ sidebarContent, _, _, _ = wikiContentsByName(ctx, commit, "_Sidebar")
+ if ctx.Written() {
+ if wikiRepo != nil {
+ wikiRepo.Close()
+ }
+ return nil, nil
}
- return nil, nil
+ } else {
+ sidebarContent = data
}
- footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer")
- if ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
+ var footerContent []byte
+ if !isFooter {
+ footerContent, _, _, _ = wikiContentsByName(ctx, commit, "_Footer")
+ if ctx.Written() {
+ if wikiRepo != nil {
+ wikiRepo.Close()
+ }
+ return nil, nil
}
- return nil, nil
+ } else {
+ footerContent = data
}
rctx := &markup.RenderContext{
@@ -225,9 +239,28 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
IsWiki: true,
}
+ buf := &strings.Builder{}
- var buf strings.Builder
- if err := markdown.Render(rctx, bytes.NewReader(data), &buf); err != nil {
+ renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) {
+ markupRd, markupWr := io.Pipe()
+ defer markupWr.Close()
+ done := make(chan struct{})
+ go func() {
+ // We allow NBSP here this is rendered
+ escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP)
+ output = buf.String()
+ buf.Reset()
+ close(done)
+ }()
+
+ err = markdown.Render(rctx, bytes.NewReader(data), markupWr)
+ _ = markupWr.CloseWithError(err)
+ <-done
+ return escaped, output, err
+ }
+
+ ctx.Data["EscapeStatus"], ctx.Data["content"], err = renderFn(data)
+ if err != nil {
if wikiRepo != nil {
wikiRepo.Close()
}
@@ -235,32 +268,40 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
return nil, nil
}
- ctx.Data["EscapeStatus"], ctx.Data["content"] = charset.EscapeControlString(buf.String())
-
- buf.Reset()
- if err := markdown.Render(rctx, bytes.NewReader(sidebarContent), &buf); err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
+ if !isSideBar {
+ buf.Reset()
+ ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"], err = renderFn(sidebarContent)
+ if err != nil {
+ if wikiRepo != nil {
+ wikiRepo.Close()
+ }
+ ctx.ServerError("Render", err)
+ return nil, nil
}
- ctx.ServerError("Render", err)
- return nil, nil
+ ctx.Data["sidebarPresent"] = sidebarContent != nil
+ } else {
+ ctx.Data["sidebarPresent"] = false
}
- ctx.Data["sidebarPresent"] = sidebarContent != nil
- ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"] = charset.EscapeControlString(buf.String())
- buf.Reset()
- if err := markdown.Render(rctx, bytes.NewReader(footerContent), &buf); err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
+ if !isFooter {
+ buf.Reset()
+ ctx.Data["footerEscapeStatus"], ctx.Data["footerContent"], err = renderFn(footerContent)
+ if err != nil {
+ if wikiRepo != nil {
+ wikiRepo.Close()
+ }
+ ctx.ServerError("Render", err)
+ return nil, nil
}
- ctx.ServerError("Render", err)
- return nil, nil
+ ctx.Data["footerPresent"] = footerContent != nil
+ } else {
+ ctx.Data["footerPresent"] = false
}
- ctx.Data["footerPresent"] = footerContent != nil
- ctx.Data["footerEscapeStatus"], ctx.Data["footerContent"] = charset.EscapeControlString(buf.String())
+
+ 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
@@ -287,7 +328,6 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
ctx.Data["old_title"] = pageName
ctx.Data["Title"] = pageName
ctx.Data["title"] = pageName
- ctx.Data["RequireHighlightJS"] = true
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
@@ -310,7 +350,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
@@ -320,15 +360,15 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
}
// get Commit Count
- commitsHistory, err := wikiRepo.CommitsByFileAndRangeNoFollow("master", pageFilename, page)
+ commitsHistory, err := wikiRepo.CommitsByFileAndRange(wiki_service.DefaultBranch, pageFilename, page)
if err != nil {
if wikiRepo != nil {
wikiRepo.Close()
}
- ctx.ServerError("CommitsByFileAndRangeNoFollow", err)
+ ctx.ServerError("CommitsByFileAndRange", err)
return nil, nil
}
- ctx.Data["Commits"] = models.ConvertFromGitCommit(commitsHistory, ctx.Repo.Repository)
+ ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository)
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
pager.SetDefaultParams(ctx)
@@ -363,7 +403,6 @@ func renderEditPage(ctx *context.Context) {
ctx.Data["old_title"] = pageName
ctx.Data["Title"] = pageName
ctx.Data["title"] = pageName
- ctx.Data["RequireHighlightJS"] = true
// lookup filename in wiki - get filecontent, gitTree entry , real filename
data, entry, _, noEntry := wikiContentsByName(ctx, commit, pageName)
@@ -549,7 +588,7 @@ func WikiPages(ctx *context.Context) {
}
wikiName, err := wiki_service.FilenameToName(entry.Name())
if err != nil {
- if models.IsErrWikiInvalidFileName(err) {
+ if repo_model.IsErrWikiInvalidFileName(err) {
continue
}
ctx.ServerError("WikiFilenameToName", err)
@@ -609,7 +648,7 @@ func WikiRaw(ctx *context.Context) {
}
if entry != nil {
- if err = common.ServeBlob(ctx, entry.Blob()); err != nil {
+ if err = common.ServeBlob(ctx, entry.Blob(), time.Time{}); err != nil {
ctx.ServerError("ServeBlob", err)
}
return
@@ -654,10 +693,10 @@ func NewWikiPost(ctx *context.Context) {
}
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil {
- if models.IsErrWikiReservedName(err) {
+ if repo_model.IsErrWikiReservedName(err) {
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form)
- } else if models.IsErrWikiAlreadyExist(err) {
+ } else if repo_model.IsErrWikiAlreadyExist(err) {
ctx.Data["Err_Title"] = true
ctx.RenderWithErr(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form)
} else {
@@ -666,6 +705,8 @@ func NewWikiPost(ctx *context.Context) {
return
}
+ notification.NotifyNewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Message)
+
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(wikiName))
}
@@ -708,6 +749,8 @@ func EditWikiPost(ctx *context.Context) {
return
}
+ notification.NotifyEditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, newWikiName, form.Message)
+
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(newWikiName))
}
@@ -723,6 +766,8 @@ func DeleteWikiPagePost(ctx *context.Context) {
return
}
+ notification.NotifyDeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName)
+
ctx.JSON(http.StatusOK, map[string]interface{}{
"redirect": ctx.Repo.RepoLink + "/wiki/",
})
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index 34f466854fe25..4699f5379adaf 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repo
diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go
new file mode 100644
index 0000000000000..b9aa40bdd22bc
--- /dev/null
+++ b/routers/web/shared/packages/packages.go
@@ -0,0 +1,225 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package packages
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ packages_model "code.gitea.io/gitea/models/packages"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/forms"
+ container_service "code.gitea.io/gitea/services/packages/container"
+)
+
+func SetPackagesContext(ctx *context.Context, owner *user_model.User) {
+ pcrs, err := packages_model.GetCleanupRulesByOwner(ctx, owner.ID)
+ if err != nil {
+ ctx.ServerError("GetCleanupRulesByOwner", err)
+ return
+ }
+
+ ctx.Data["CleanupRules"] = pcrs
+}
+
+func SetRuleAddContext(ctx *context.Context) {
+ setRuleEditContext(ctx, nil)
+}
+
+func SetRuleEditContext(ctx *context.Context, owner *user_model.User) {
+ pcr := getCleanupRuleByContext(ctx, owner)
+ if pcr == nil {
+ return
+ }
+
+ setRuleEditContext(ctx, pcr)
+}
+
+func setRuleEditContext(ctx *context.Context, pcr *packages_model.PackageCleanupRule) {
+ ctx.Data["IsEditRule"] = pcr != nil
+
+ if pcr == nil {
+ pcr = &packages_model.PackageCleanupRule{}
+ }
+ ctx.Data["CleanupRule"] = pcr
+ ctx.Data["AvailableTypes"] = packages_model.TypeList
+}
+
+func PerformRuleAddPost(ctx *context.Context, owner *user_model.User, redirectURL string, template base.TplName) {
+ performRuleEditPost(ctx, owner, nil, redirectURL, template)
+}
+
+func PerformRuleEditPost(ctx *context.Context, owner *user_model.User, redirectURL string, template base.TplName) {
+ pcr := getCleanupRuleByContext(ctx, owner)
+ if pcr == nil {
+ return
+ }
+
+ form := web.GetForm(ctx).(*forms.PackageCleanupRuleForm)
+
+ if form.Action == "remove" {
+ if err := packages_model.DeleteCleanupRuleByID(ctx, pcr.ID); err != nil {
+ ctx.ServerError("DeleteCleanupRuleByID", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("packages.owner.settings.cleanuprules.success.delete"))
+ ctx.Redirect(redirectURL)
+ } else {
+ performRuleEditPost(ctx, owner, pcr, redirectURL, template)
+ }
+}
+
+func performRuleEditPost(ctx *context.Context, owner *user_model.User, pcr *packages_model.PackageCleanupRule, redirectURL string, template base.TplName) {
+ isEditRule := pcr != nil
+
+ if pcr == nil {
+ pcr = &packages_model.PackageCleanupRule{}
+ }
+
+ form := web.GetForm(ctx).(*forms.PackageCleanupRuleForm)
+
+ pcr.Enabled = form.Enabled
+ pcr.OwnerID = owner.ID
+ pcr.KeepCount = form.KeepCount
+ pcr.KeepPattern = form.KeepPattern
+ pcr.RemoveDays = form.RemoveDays
+ pcr.RemovePattern = form.RemovePattern
+ pcr.MatchFullName = form.MatchFullName
+
+ ctx.Data["IsEditRule"] = isEditRule
+ ctx.Data["CleanupRule"] = pcr
+ ctx.Data["AvailableTypes"] = packages_model.TypeList
+
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, template)
+ return
+ }
+
+ if isEditRule {
+ if err := packages_model.UpdateCleanupRule(ctx, pcr); err != nil {
+ ctx.ServerError("UpdateCleanupRule", err)
+ return
+ }
+ } else {
+ pcr.Type = packages_model.Type(form.Type)
+
+ if has, err := packages_model.HasOwnerCleanupRuleForPackageType(ctx, owner.ID, pcr.Type); err != nil {
+ ctx.ServerError("HasOwnerCleanupRuleForPackageType", err)
+ return
+ } else if has {
+ ctx.Data["Err_Type"] = true
+ ctx.HTML(http.StatusOK, template)
+ return
+ }
+
+ var err error
+ if pcr, err = packages_model.InsertCleanupRule(ctx, pcr); err != nil {
+ ctx.ServerError("InsertCleanupRule", err)
+ return
+ }
+ }
+
+ ctx.Flash.Success(ctx.Tr("packages.owner.settings.cleanuprules.success.update"))
+ ctx.Redirect(fmt.Sprintf("%s/rules/%d", redirectURL, pcr.ID))
+}
+
+func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
+ pcr := getCleanupRuleByContext(ctx, owner)
+ if pcr == nil {
+ return
+ }
+
+ if err := pcr.CompiledPattern(); err != nil {
+ ctx.ServerError("CompiledPattern", err)
+ return
+ }
+
+ olderThan := time.Now().AddDate(0, 0, -pcr.RemoveDays)
+
+ packages, err := packages_model.GetPackagesByType(ctx, pcr.OwnerID, pcr.Type)
+ if err != nil {
+ ctx.ServerError("GetPackagesByType", err)
+ return
+ }
+
+ versionsToRemove := make([]*packages_model.PackageDescriptor, 0, 10)
+
+ for _, p := range packages {
+ pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+ PackageID: p.ID,
+ IsInternal: util.OptionalBoolFalse,
+ Sort: packages_model.SortCreatedDesc,
+ Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200),
+ })
+ if err != nil {
+ ctx.ServerError("SearchVersions", err)
+ return
+ }
+ for _, pv := range pvs {
+ if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
+ ctx.ServerError("ShouldBeSkipped", err)
+ return
+ } else if skip {
+ continue
+ }
+
+ toMatch := pv.LowerVersion
+ if pcr.MatchFullName {
+ toMatch = p.LowerName + "/" + pv.LowerVersion
+ }
+
+ if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) {
+ continue
+ }
+ if pv.CreatedUnix.AsLocalTime().After(olderThan) {
+ continue
+ }
+ if pcr.RemovePatternMatcher != nil && !pcr.RemovePatternMatcher.MatchString(toMatch) {
+ continue
+ }
+
+ pd, err := packages_model.GetPackageDescriptor(ctx, pv)
+ if err != nil {
+ ctx.ServerError("GetPackageDescriptor", err)
+ return
+ }
+ versionsToRemove = append(versionsToRemove, pd)
+ }
+ }
+
+ ctx.Data["CleanupRule"] = pcr
+ ctx.Data["VersionsToRemove"] = versionsToRemove
+}
+
+func getCleanupRuleByContext(ctx *context.Context, owner *user_model.User) *packages_model.PackageCleanupRule {
+ id := ctx.FormInt64("id")
+ if id == 0 {
+ id = ctx.ParamsInt64("id")
+ }
+
+ pcr, err := packages_model.GetCleanupRuleByID(ctx, id)
+ if err != nil {
+ if err == packages_model.ErrPackageCleanupRuleNotExist {
+ ctx.NotFound("", err)
+ } else {
+ ctx.ServerError("GetCleanupRuleByID", err)
+ }
+ return nil
+ }
+
+ if pcr != nil && pcr.OwnerID == owner.ID {
+ return pcr
+ }
+
+ ctx.NotFound("", fmt.Errorf("PackageCleanupRule[%v] not associated to owner %v", id, owner))
+
+ return nil
+}
diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go
index 82d72698c606c..2d626c558ed78 100644
--- a/routers/web/swagger_json.go
+++ b/routers/web/swagger_json.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package web
diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go
index c8bca9dc2c1df..20c2ef3e47bfd 100644
--- a/routers/web/user/avatar.go
+++ b/routers/web/user/avatar.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
@@ -30,7 +29,11 @@ func AvatarByUserName(ctx *context.Context) {
var user *user_model.User
if strings.ToLower(userName) != "ghost" {
var err error
- if user, err = user_model.GetUserByName(userName); err != nil {
+ 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/code.go b/routers/web/user/code.go
new file mode 100644
index 0000000000000..81e3e65b4b67a
--- /dev/null
+++ b/routers/web/user/code.go
@@ -0,0 +1,113 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "net/http"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ code_indexer "code.gitea.io/gitea/modules/indexer/code"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+const (
+ tplUserCode base.TplName = "user/code"
+)
+
+// CodeSearch render user/organization code search page
+func CodeSearch(ctx *context.Context) {
+ if !setting.Indexer.RepoIndexerEnabled {
+ ctx.Redirect(ctx.ContextUser.HomeLink())
+ return
+ }
+
+ ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
+ ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+ ctx.Data["Title"] = ctx.Tr("explore.code")
+ ctx.Data["ContextUser"] = ctx.ContextUser
+
+ language := ctx.FormTrim("l")
+ keyword := ctx.FormTrim("q")
+
+ queryType := ctx.FormTrim("t")
+ isMatch := queryType == "match"
+
+ ctx.Data["Keyword"] = keyword
+ ctx.Data["Language"] = language
+ ctx.Data["queryType"] = queryType
+ ctx.Data["IsCodePage"] = true
+
+ if keyword == "" {
+ ctx.HTML(http.StatusOK, tplUserCode)
+ return
+ }
+
+ var (
+ repoIDs []int64
+ err error
+ )
+
+ page := ctx.FormInt("page")
+ if page <= 0 {
+ page = 1
+ }
+
+ repoIDs, err = repo_model.FindUserCodeAccessibleOwnerRepoIDs(ctx, ctx.ContextUser.ID, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("FindUserCodeAccessibleOwnerRepoIDs", err)
+ return
+ }
+
+ var (
+ total int
+ searchResults []*code_indexer.Result
+ searchResultLanguages []*code_indexer.SearchResultLanguages
+ )
+
+ if len(repoIDs) > 0 {
+ total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+ if err != nil {
+ if code_indexer.IsAvailable() {
+ ctx.ServerError("SearchResults", err)
+ return
+ }
+ ctx.Data["CodeIndexerUnavailable"] = true
+ } else {
+ ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable()
+ }
+
+ loadRepoIDs := make([]int64, 0, len(searchResults))
+ for _, result := range searchResults {
+ var find bool
+ for _, id := range loadRepoIDs {
+ if id == result.RepoID {
+ find = true
+ break
+ }
+ }
+ if !find {
+ loadRepoIDs = append(loadRepoIDs, result.RepoID)
+ }
+ }
+
+ repoMaps, err := repo_model.GetRepositoriesMapByIDs(loadRepoIDs)
+ if err != nil {
+ ctx.ServerError("GetRepositoriesMapByIDs", err)
+ return
+ }
+
+ ctx.Data["RepoMaps"] = repoMaps
+ }
+ ctx.Data["SearchResults"] = searchResults
+ ctx.Data["SearchResultLanguages"] = searchResultLanguages
+
+ pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
+ pager.SetDefaultParams(ctx)
+ pager.AddParam(ctx, "l", "Language")
+ ctx.Data["Page"] = pager
+
+ ctx.HTML(http.StatusOK, tplUserCode)
+}
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 33492aa209cff..36d9d4f019f7d 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
@@ -14,7 +13,7 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
@@ -56,7 +55,7 @@ func getDashboardContextUser(ctx *context.Context) *user_model.User {
}
ctx.Data["ContextUser"] = ctxUser
- orgs, err := models.GetUserOrgsList(ctx.Doer)
+ orgs, err := organization.GetUserOrgsList(ctx.Doer)
if err != nil {
ctx.ServerError("GetUserOrgsList", err)
return nil
@@ -78,6 +77,7 @@ func Dashboard(ctx *context.Context) {
ctx.Data["PageIsNews"] = true
cnt, _ := organization.GetOrganizationCount(ctx, ctxUser)
ctx.Data["UserOrgsCount"] = cnt
+ ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
var uid int64
if ctxUser != nil {
@@ -90,7 +90,7 @@ func Dashboard(ctx *context.Context) {
}
if setting.Service.EnableUserHeatmap {
- data, err := models.GetUserHeatmapDataByUserTeam(ctxUser, ctx.Org.Team, ctx.Doer)
+ data, err := activities_model.GetUserHeatmapDataByUserTeam(ctxUser, ctx.Org.Team, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserHeatmapDataByUserTeam", err)
return
@@ -99,40 +99,7 @@ func Dashboard(ctx *context.Context) {
}
var err error
- var mirrors []*repo_model.Repository
- if ctxUser.IsOrganization() {
- var env organization.AccessibleReposEnvironment
- if ctx.Org.Team != nil {
- env = organization.OrgFromUser(ctxUser).AccessibleTeamReposEnv(ctx.Org.Team)
- } else {
- env, err = organization.AccessibleReposEnv(ctx, organization.OrgFromUser(ctxUser), ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("AccessibleReposEnv", err)
- return
- }
- }
- mirrors, err = env.MirrorRepos()
- if err != nil {
- ctx.ServerError("env.MirrorRepos", err)
- return
- }
- } else {
- mirrors, err = repo_model.GetUserMirrorRepositories(ctxUser.ID)
- if err != nil {
- ctx.ServerError("GetUserMirrorRepositories", err)
- return
- }
- }
- ctx.Data["MaxShowRepoNum"] = setting.UI.User.RepoPagingNum
-
- if err := repo_model.MirrorRepositoryList(mirrors).LoadAttributes(); err != nil {
- ctx.ServerError("MirrorRepositoryList.LoadAttributes", err)
- return
- }
- ctx.Data["MirrorCount"] = len(mirrors)
- ctx.Data["Mirrors"] = mirrors
-
- ctx.Data["Feeds"], err = models.GetFeeds(ctx, models.GetFeedsOptions{
+ ctx.Data["Feeds"], err = activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctxUser,
RequestedTeam: ctx.Org.Team,
Actor: ctx.Doer,
@@ -140,6 +107,7 @@ func Dashboard(ctx *context.Context) {
OnlyPerformedBy: false,
IncludeDeleted: false,
Date: ctx.FormString("date"),
+ ListOptions: db.ListOptions{PageSize: setting.UI.FeedPagingNum},
})
if err != nil {
ctx.ServerError("GetFeeds", err)
@@ -165,12 +133,13 @@ func Milestones(ctx *context.Context) {
return
}
- repoOpts := models.SearchRepoOptions{
+ repoOpts := repo_model.SearchRepoOptions{
Actor: ctxUser,
OwnerID: ctxUser.ID,
Private: true,
- AllPublic: false, // Include also all public repositories of users and public organisations
- AllLimited: false, // Include also all public repositories of limited organisations
+ AllPublic: false, // Include also all public repositories of users and public organisations
+ AllLimited: false, // Include also all public repositories of limited organisations
+ Archived: util.OptionalBoolFalse,
HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones
}
@@ -179,7 +148,7 @@ func Milestones(ctx *context.Context) {
}
var (
- userRepoCond = models.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit
+ userRepoCond = repo_model.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit
repoCond = userRepoCond
repoIDs []int64
@@ -232,7 +201,7 @@ func Milestones(ctx *context.Context) {
return
}
- showRepos, _, err := models.SearchRepositoryByCondition(&repoOpts, userRepoCond, false)
+ showRepos, _, err := repo_model.SearchRepositoryByCondition(ctx, &repoOpts, userRepoCond, false)
if err != nil {
ctx.ServerError("SearchRepositoryByCondition", err)
return
@@ -262,7 +231,7 @@ func Milestones(ctx *context.Context) {
return
}
- if milestones[i].Repo.IsTimetrackerEnabled() {
+ if milestones[i].Repo.IsTimetrackerEnabled(ctx) {
err := milestones[i].LoadTotalTrackedTime()
if err != nil {
ctx.ServerError("LoadTotalTrackedTime", err)
@@ -331,6 +300,7 @@ func Pulls(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("pull_requests")
ctx.Data["PageIsPulls"] = true
+ ctx.Data["SingleRepoAction"] = "pull"
buildIssueOverview(ctx, unit.TypePullRequests)
}
@@ -344,6 +314,7 @@ func Issues(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("issues")
ctx.Data["PageIsIssues"] = true
+ ctx.Data["SingleRepoAction"] = "issue"
buildIssueOverview(ctx, unit.TypeIssues)
}
@@ -383,17 +354,17 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
viewType = ctx.FormString("type")
switch viewType {
case "assigned":
- filterMode = models.FilterModeAssign
+ filterMode = issues_model.FilterModeAssign
case "created_by":
- filterMode = models.FilterModeCreate
+ filterMode = issues_model.FilterModeCreate
case "mentioned":
- filterMode = models.FilterModeMention
+ filterMode = issues_model.FilterModeMention
case "review_requested":
- filterMode = models.FilterModeReviewRequested
+ filterMode = issues_model.FilterModeReviewRequested
case "your_repositories":
fallthrough
default:
- filterMode = models.FilterModeYourRepositories
+ filterMode = issues_model.FilterModeYourRepositories
viewType = "your_repositories"
}
@@ -414,7 +385,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
}
isPullList := unitType == unit.TypePullRequests
- opts := &models.IssuesOptions{
+ opts := &issues_model.IssuesOptions{
IsPull: util.OptionalBoolOf(isPullList),
SortType: sortType,
IsArchived: util.OptionalBoolFalse,
@@ -435,7 +406,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// As team:
// - Team org's owns the repository.
// - Team has read permission to repository.
- repoOpts := &models.SearchRepoOptions{
+ repoOpts := &repo_model.SearchRepoOptions{
Actor: ctx.Doer,
OwnerID: ctx.Doer.ID,
Private: true,
@@ -443,33 +414,21 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
AllLimited: false,
}
- if ctxUser.IsOrganization() && ctx.Org.Team != nil {
- repoOpts.TeamID = ctx.Org.Team.ID
+ if team != nil {
+ repoOpts.TeamID = team.ID
}
switch filterMode {
- case models.FilterModeAll:
- case models.FilterModeAssign:
+ case issues_model.FilterModeAll:
+ case issues_model.FilterModeYourRepositories:
+ case issues_model.FilterModeAssign:
opts.AssigneeID = ctx.Doer.ID
- case models.FilterModeCreate:
+ case issues_model.FilterModeCreate:
opts.PosterID = ctx.Doer.ID
- case models.FilterModeMention:
+ case issues_model.FilterModeMention:
opts.MentionedID = ctx.Doer.ID
- case models.FilterModeReviewRequested:
+ case issues_model.FilterModeReviewRequested:
opts.ReviewRequestedID = ctx.Doer.ID
- case models.FilterModeYourRepositories:
- if ctxUser.IsOrganization() && ctx.Org.Team != nil {
- // Fixes a issue whereby the user's ID would be used
- // to check if it's in the team(which possible isn't the case).
- opts.User = nil
- }
- userRepoIDs, _, err := models.SearchRepositoryIDs(repoOpts)
- if err != nil {
- ctx.ServerError("models.SearchRepositoryIDs: %v", err)
- return
- }
-
- opts.RepoIDs = userRepoIDs
}
// keyword holds the search term entered into the search field.
@@ -501,7 +460,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// USING NON-FINAL STATE OF opts FOR A QUERY.
var issueCountByRepo map[int64]int64
if !forceEmpty {
- issueCountByRepo, err = models.CountIssuesByRepo(opts)
+ issueCountByRepo, err = issues_model.CountIssuesByRepo(ctx, opts)
if err != nil {
ctx.ServerError("CountIssuesByRepo", err)
return
@@ -533,7 +492,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// Gets set when clicking filters on the issues overview page.
repoIDs := getRepoIDs(ctx.FormString("repos"))
if len(repoIDs) > 0 {
- opts.RepoIDs = repoIDs
+ opts.RepoCond = builder.In("issue.repo_id", repoIDs)
}
// ------------------------------
@@ -542,15 +501,15 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// Slice of Issues that will be displayed on the overview page
// USING FINAL STATE OF opts FOR A QUERY.
- var issues []*models.Issue
+ var issues []*issues_model.Issue
if !forceEmpty {
- issues, err = models.Issues(opts)
+ issues, err = issues_model.Issues(ctx, opts)
if err != nil {
ctx.ServerError("Issues", err)
return
}
} else {
- issues = []*models.Issue{}
+ issues = []*issues_model.Issue{}
}
// ----------------------------------
@@ -569,7 +528,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
}
// a RepositoryList
- showRepos := models.RepositoryListOfMap(showReposMap)
+ showRepos := repo_model.RepositoryListOfMap(showReposMap)
sort.Sort(showRepos)
// maps pull request IDs to their CommitStatus. Will be posted to ctx.Data.
@@ -579,7 +538,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
}
}
- commitStatus, err := pull_service.GetIssuesLastCommitStatus(ctx, issues)
+ commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
if err != nil {
ctx.ServerError("GetIssuesLastCommitStatus", err)
return
@@ -588,9 +547,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// -------------------------------
// Fill stats to post to ctx.Data.
// -------------------------------
- var issueStats *models.IssueStats
+ var issueStats *issues_model.IssueStats
if !forceEmpty {
- statsOpts := models.UserIssueStatsOptions{
+ statsOpts := issues_model.UserIssueStatsOptions{
UserID: ctx.Doer.ID,
FilterMode: filterMode,
IsPull: isPullList,
@@ -600,31 +559,43 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
LabelIDs: opts.LabelIDs,
Org: org,
Team: team,
+ RepoCond: opts.RepoCond,
}
- if filterMode == models.FilterModeYourRepositories {
- statsOpts.RepoCond = models.SearchRepositoryCondition(repoOpts)
- }
- // Detect when we only should search by team.
- if opts.User == nil {
- statsOpts.UserID = 0
- }
- issueStats, err = models.GetUserIssueStats(statsOpts)
+
+ issueStats, err = issues_model.GetUserIssueStats(statsOpts)
if err != nil {
ctx.ServerError("GetUserIssueStats Shown", err)
return
}
} else {
- issueStats = &models.IssueStats{}
+ issueStats = &issues_model.IssueStats{}
}
// Will be posted to ctx.Data.
var shownIssues int
if !isShowClosed {
shownIssues = int(issueStats.OpenCount)
- ctx.Data["TotalIssueCount"] = shownIssues
} else {
shownIssues = int(issueStats.ClosedCount)
- ctx.Data["TotalIssueCount"] = shownIssues
+ }
+ if len(repoIDs) != 0 {
+ shownIssues = 0
+ for _, repoID := range repoIDs {
+ shownIssues += int(issueCountByRepo[repoID])
+ }
+ }
+
+ var allIssueCount int64
+ for _, issueCount := range issueCountByRepo {
+ allIssueCount += issueCount
+ }
+ ctx.Data["TotalIssueCount"] = allIssueCount
+
+ if len(repoIDs) == 1 {
+ repo := showReposMap[repoIDs[0]]
+ if repo != nil {
+ ctx.Data["SingleRepoLink"] = repo.Link()
+ }
}
ctx.Data["IsShowClosed"] = isShowClosed
@@ -633,7 +604,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
ctx.Data["Issues"] = issues
- approvalCounts, err := models.IssueList(issues).GetApprovalCounts()
+ approvalCounts, err := issues_model.IssueList(issues).GetApprovalCounts(ctx)
if err != nil {
ctx.ServerError("ApprovalCounts", err)
return
@@ -643,11 +614,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
if !ok || len(counts) == 0 {
return 0
}
- reviewTyp := models.ReviewTypeApprove
+ reviewTyp := issues_model.ReviewTypeApprove
if typ == "reject" {
- reviewTyp = models.ReviewTypeReject
+ reviewTyp = issues_model.ReviewTypeReject
} else if typ == "waiting" {
- reviewTyp = models.ReviewTypeRequest
+ reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
if count.Type == reviewTyp {
@@ -656,7 +627,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
}
return 0
}
- ctx.Data["CommitStatus"] = commitStatus
+ ctx.Data["CommitLastStatus"] = lastStatus
+ ctx.Data["CommitStatuses"] = commitStatuses
ctx.Data["Repos"] = showRepos
ctx.Data["Counts"] = issueCountByRepo
ctx.Data["IssueStats"] = issueStats
@@ -717,18 +689,18 @@ func getRepoIDs(reposQuery string) []int64 {
return repoIDs
}
-func issueIDsFromSearch(ctx *context.Context, ctxUser *user_model.User, keyword string, opts *models.IssuesOptions) ([]int64, error) {
+func issueIDsFromSearch(ctx *context.Context, ctxUser *user_model.User, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) {
if len(keyword) == 0 {
return []int64{}, nil
}
- searchRepoIDs, err := models.GetRepoIDsForIssuesOptions(opts, ctxUser)
+ searchRepoIDs, err := issues_model.GetRepoIDsForIssuesOptions(opts, ctxUser)
if err != nil {
- return nil, fmt.Errorf("GetRepoIDsForIssuesOptions: %v", err)
+ return nil, fmt.Errorf("GetRepoIDsForIssuesOptions: %w", err)
}
issueIDsFromSearch, err := issue_indexer.SearchIssuesByKeyword(ctx, searchRepoIDs, keyword)
if err != nil {
- return nil, fmt.Errorf("SearchIssuesByKeyword: %v", err)
+ return nil, fmt.Errorf("SearchIssuesByKeyword: %w", err)
}
return issueIDsFromSearch, nil
diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go
index bf78e00adade8..534b0b26207b6 100644
--- a/routers/web/user/home_test.go
+++ b/routers/web/user/home_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
@@ -8,7 +7,7 @@ import (
"net/http"
"testing"
- "code.gitea.io/gitea/models"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
@@ -26,8 +25,8 @@ func TestArchivedIssues(t *testing.T) {
ctx.Req.Form.Set("state", "open")
// Assume: User 30 has access to two Repos with Issues, one of the Repos being archived.
- repos, _, _ := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctx.Doer})
- assert.Len(t, repos, 2)
+ repos, _, _ := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{Actor: ctx.Doer})
+ assert.Len(t, repos, 3)
IsArchived := make(map[int64]bool)
NumIssues := make(map[int64]int)
for _, repo := range repos {
@@ -76,7 +75,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/main_test.go b/routers/web/user/main_test.go
index 517957a85cd9e..925482a1d2ba7 100644
--- a/routers/web/user/main_test.go
+++ b/routers/web/user/main_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index f7848de90a164..e96b3dd27a95e 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -1,42 +1,52 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
import (
+ goctx "context"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ issue_service "code.gitea.io/gitea/services/issue"
+ pull_service "code.gitea.io/gitea/services/pull"
)
const (
- tplNotification base.TplName = "user/notification/notification"
- tplNotificationDiv base.TplName = "user/notification/notification_div"
+ tplNotification base.TplName = "user/notification/notification"
+ tplNotificationDiv base.TplName = "user/notification/notification_div"
+ tplNotificationSubscriptions base.TplName = "user/notification/notification_subscriptions"
)
// GetNotificationCount is the middleware that sets the notification count in the context
-func GetNotificationCount(c *context.Context) {
- if strings.HasPrefix(c.Req.URL.Path, "/api") {
+func GetNotificationCount(ctx *context.Context) {
+ if strings.HasPrefix(ctx.Req.URL.Path, "/api") {
return
}
- if !c.IsSigned {
+ if !ctx.IsSigned {
return
}
- c.Data["NotificationUnreadCount"] = func() int64 {
- count, err := models.GetNotificationCount(c.Doer, models.NotificationStatusUnread)
+ ctx.Data["NotificationUnreadCount"] = func() int64 {
+ count, err := activities_model.GetNotificationCount(ctx, ctx.Doer, activities_model.NotificationStatusUnread)
if err != nil {
- c.ServerError("GetNotificationCount", err)
+ if err != goctx.Canceled {
+ log.Error("Unable to GetNotificationCount for user:%-v: %v", ctx.Doer, err)
+ }
return -1
}
@@ -45,25 +55,25 @@ func GetNotificationCount(c *context.Context) {
}
// Notifications is the notifications page
-func Notifications(c *context.Context) {
- getNotifications(c)
- if c.Written() {
+func Notifications(ctx *context.Context) {
+ getNotifications(ctx)
+ if ctx.Written() {
return
}
- if c.FormBool("div-only") {
- c.Data["SequenceNumber"] = c.FormString("sequence-number")
- c.HTML(http.StatusOK, tplNotificationDiv)
+ if ctx.FormBool("div-only") {
+ ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number")
+ ctx.HTML(http.StatusOK, tplNotificationDiv)
return
}
- c.HTML(http.StatusOK, tplNotification)
+ ctx.HTML(http.StatusOK, tplNotification)
}
-func getNotifications(c *context.Context) {
+func getNotifications(ctx *context.Context) {
var (
- keyword = c.FormTrim("q")
- status models.NotificationStatus
- page = c.FormInt("page")
- perPage = c.FormInt("perPage")
+ keyword = ctx.FormTrim("q")
+ status activities_model.NotificationStatus
+ page = ctx.FormInt("page")
+ perPage = ctx.FormInt("perPage")
)
if page < 1 {
page = 1
@@ -74,126 +84,328 @@ func getNotifications(c *context.Context) {
switch keyword {
case "read":
- status = models.NotificationStatusRead
+ status = activities_model.NotificationStatusRead
default:
- status = models.NotificationStatusUnread
+ status = activities_model.NotificationStatusUnread
}
- total, err := models.GetNotificationCount(c.Doer, status)
+ total, err := activities_model.GetNotificationCount(ctx, ctx.Doer, status)
if err != nil {
- c.ServerError("ErrGetNotificationCount", err)
+ ctx.ServerError("ErrGetNotificationCount", err)
return
}
// redirect to last page if request page is more than total pages
pager := context.NewPagination(int(total), perPage, page, 5)
if pager.Paginater.Current() < page {
- c.Redirect(fmt.Sprintf("%s/notifications?q=%s&page=%d", setting.AppSubURL, url.QueryEscape(c.FormString("q")), pager.Paginater.Current()))
+ ctx.Redirect(fmt.Sprintf("%s/notifications?q=%s&page=%d", setting.AppSubURL, url.QueryEscape(ctx.FormString("q")), pager.Paginater.Current()))
return
}
- statuses := []models.NotificationStatus{status, models.NotificationStatusPinned}
- notifications, err := models.NotificationsForUser(c.Doer, statuses, page, perPage)
+ statuses := []activities_model.NotificationStatus{status, activities_model.NotificationStatusPinned}
+ notifications, err := activities_model.NotificationsForUser(ctx, ctx.Doer, statuses, page, perPage)
if err != nil {
- c.ServerError("ErrNotificationsForUser", err)
+ ctx.ServerError("ErrNotificationsForUser", err)
return
}
failCount := 0
- repos, failures, err := notifications.LoadRepos()
+ repos, failures, err := notifications.LoadRepos(ctx)
if err != nil {
- c.ServerError("LoadRepos", err)
+ ctx.ServerError("LoadRepos", err)
return
}
notifications = notifications.Without(failures)
- if err := repos.LoadAttributes(); err != nil {
- c.ServerError("LoadAttributes", err)
+ if err := repos.LoadAttributes(); err != nil { // TODO
+ ctx.ServerError("LoadAttributes", err)
return
}
failCount += len(failures)
- failures, err = notifications.LoadIssues()
+ failures, err = notifications.LoadIssues(ctx)
if err != nil {
- c.ServerError("LoadIssues", err)
+ ctx.ServerError("LoadIssues", err)
return
}
notifications = notifications.Without(failures)
failCount += len(failures)
- failures, err = notifications.LoadComments()
+ failures, err = notifications.LoadComments(ctx)
if err != nil {
- c.ServerError("LoadComments", err)
+ ctx.ServerError("LoadComments", err)
return
}
notifications = notifications.Without(failures)
failCount += len(failures)
if failCount > 0 {
- c.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
+ ctx.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
}
- c.Data["Title"] = c.Tr("notifications")
- c.Data["Keyword"] = keyword
- c.Data["Status"] = status
- c.Data["Notifications"] = notifications
+ ctx.Data["Title"] = ctx.Tr("notifications")
+ ctx.Data["Keyword"] = keyword
+ ctx.Data["Status"] = status
+ ctx.Data["Notifications"] = notifications
- pager.SetDefaultParams(c)
- c.Data["Page"] = pager
+ pager.SetDefaultParams(ctx)
+ ctx.Data["Page"] = pager
}
// NotificationStatusPost is a route for changing the status of a notification
-func NotificationStatusPost(c *context.Context) {
+func NotificationStatusPost(ctx *context.Context) {
var (
- notificationID = c.FormInt64("notification_id")
- statusStr = c.FormString("status")
- status models.NotificationStatus
+ notificationID = ctx.FormInt64("notification_id")
+ statusStr = ctx.FormString("status")
+ status activities_model.NotificationStatus
)
switch statusStr {
case "read":
- status = models.NotificationStatusRead
+ status = activities_model.NotificationStatusRead
case "unread":
- status = models.NotificationStatusUnread
+ status = activities_model.NotificationStatusUnread
case "pinned":
- status = models.NotificationStatusPinned
+ status = activities_model.NotificationStatusPinned
default:
- c.ServerError("InvalidNotificationStatus", errors.New("Invalid notification status"))
+ ctx.ServerError("InvalidNotificationStatus", errors.New("Invalid notification status"))
return
}
- if _, err := models.SetNotificationStatus(notificationID, c.Doer, status); err != nil {
- c.ServerError("SetNotificationStatus", err)
+ if _, err := activities_model.SetNotificationStatus(ctx, notificationID, ctx.Doer, status); err != nil {
+ ctx.ServerError("SetNotificationStatus", err)
return
}
- if !c.FormBool("noredirect") {
- url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, url.QueryEscape(c.FormString("page")))
- c.Redirect(url, http.StatusSeeOther)
+ if !ctx.FormBool("noredirect") {
+ url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, url.QueryEscape(ctx.FormString("page")))
+ ctx.Redirect(url, http.StatusSeeOther)
}
- getNotifications(c)
- if c.Written() {
+ getNotifications(ctx)
+ if ctx.Written() {
return
}
- c.Data["Link"] = setting.AppURL + "notifications"
- c.Data["SequenceNumber"] = c.Req.PostFormValue("sequence-number")
+ ctx.Data["Link"] = setting.AppURL + "notifications"
+ ctx.Data["SequenceNumber"] = ctx.Req.PostFormValue("sequence-number")
- c.HTML(http.StatusOK, tplNotificationDiv)
+ ctx.HTML(http.StatusOK, tplNotificationDiv)
}
// NotificationPurgePost is a route for 'purging' the list of notifications - marking all unread as read
-func NotificationPurgePost(c *context.Context) {
- err := models.UpdateNotificationStatuses(c.Doer, models.NotificationStatusUnread, models.NotificationStatusRead)
+func NotificationPurgePost(ctx *context.Context) {
+ err := activities_model.UpdateNotificationStatuses(ctx, ctx.Doer, activities_model.NotificationStatusUnread, activities_model.NotificationStatusRead)
if err != nil {
- c.ServerError("ErrUpdateNotificationStatuses", err)
+ ctx.ServerError("UpdateNotificationStatuses", err)
return
}
- c.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther)
+ ctx.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther)
+}
+
+// NotificationSubscriptions returns the list of subscribed issues
+func NotificationSubscriptions(ctx *context.Context) {
+ page := ctx.FormInt("page")
+ if page < 1 {
+ page = 1
+ }
+
+ sortType := ctx.FormString("sort")
+ ctx.Data["SortType"] = sortType
+
+ state := ctx.FormString("state")
+ if !util.SliceContainsString([]string{"all", "open", "closed"}, state, true) {
+ state = "all"
+ }
+ ctx.Data["State"] = state
+ var showClosed util.OptionalBool
+ switch state {
+ case "all":
+ showClosed = util.OptionalBoolNone
+ case "closed":
+ showClosed = util.OptionalBoolTrue
+ case "open":
+ showClosed = util.OptionalBoolFalse
+ }
+
+ var issueTypeBool util.OptionalBool
+ issueType := ctx.FormString("issueType")
+ switch issueType {
+ case "issues":
+ issueTypeBool = util.OptionalBoolFalse
+ case "pulls":
+ issueTypeBool = util.OptionalBoolTrue
+ default:
+ issueTypeBool = util.OptionalBoolNone
+ }
+ ctx.Data["IssueType"] = issueType
+
+ var labelIDs []int64
+ selectedLabels := ctx.FormString("labels")
+ ctx.Data["Labels"] = selectedLabels
+ if len(selectedLabels) > 0 && selectedLabels != "0" {
+ var err error
+ labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
+ if err != nil {
+ ctx.ServerError("StringsToInt64s", err)
+ return
+ }
+ }
+
+ count, err := issues_model.CountIssues(ctx, &issues_model.IssuesOptions{
+ SubscriberID: ctx.Doer.ID,
+ IsClosed: showClosed,
+ IsPull: issueTypeBool,
+ LabelIDs: labelIDs,
+ })
+ if err != nil {
+ ctx.ServerError("CountIssues", err)
+ return
+ }
+ issues, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
+ ListOptions: db.ListOptions{
+ PageSize: setting.UI.IssuePagingNum,
+ Page: page,
+ },
+ SubscriberID: ctx.Doer.ID,
+ SortType: sortType,
+ IsClosed: showClosed,
+ IsPull: issueTypeBool,
+ LabelIDs: labelIDs,
+ })
+ if err != nil {
+ ctx.ServerError("Issues", err)
+ return
+ }
+
+ commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
+ if err != nil {
+ ctx.ServerError("GetIssuesAllCommitStatus", err)
+ return
+ }
+ ctx.Data["CommitLastStatus"] = lastStatus
+ ctx.Data["CommitStatuses"] = commitStatuses
+ ctx.Data["Issues"] = issues
+
+ ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, "")
+
+ commitStatus, err := pull_service.GetIssuesLastCommitStatus(ctx, issues)
+ if err != nil {
+ ctx.ServerError("GetIssuesLastCommitStatus", err)
+ return
+ }
+ ctx.Data["CommitStatus"] = commitStatus
+
+ issueList := issues_model.IssueList(issues)
+ approvalCounts, err := issueList.GetApprovalCounts(ctx)
+ if err != nil {
+ ctx.ServerError("ApprovalCounts", err)
+ return
+ }
+ ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
+ counts, ok := approvalCounts[issueID]
+ if !ok || len(counts) == 0 {
+ return 0
+ }
+ reviewTyp := issues_model.ReviewTypeApprove
+ if typ == "reject" {
+ reviewTyp = issues_model.ReviewTypeReject
+ } else if typ == "waiting" {
+ reviewTyp = issues_model.ReviewTypeRequest
+ }
+ for _, count := range counts {
+ if count.Type == reviewTyp {
+ return count.Count
+ }
+ }
+ return 0
+ }
+
+ ctx.Data["Status"] = 1
+ ctx.Data["Title"] = ctx.Tr("notification.subscriptions")
+
+ // redirect to last page if request page is more than total pages
+ pager := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, 5)
+ if pager.Paginater.Current() < page {
+ ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current()))
+ return
+ }
+ pager.AddParam(ctx, "sort", "SortType")
+ pager.AddParam(ctx, "state", "State")
+ ctx.Data["Page"] = pager
+
+ ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
+}
+
+// NotificationWatching returns the list of watching repos
+func NotificationWatching(ctx *context.Context) {
+ page := ctx.FormInt("page")
+ if page < 1 {
+ page = 1
+ }
+
+ var orderBy db.SearchOrderBy
+ ctx.Data["SortType"] = ctx.FormString("sort")
+ switch ctx.FormString("sort") {
+ case "newest":
+ orderBy = db.SearchOrderByNewest
+ case "oldest":
+ orderBy = db.SearchOrderByOldest
+ case "recentupdate":
+ orderBy = db.SearchOrderByRecentUpdated
+ case "leastupdate":
+ orderBy = db.SearchOrderByLeastUpdated
+ case "reversealphabetically":
+ orderBy = db.SearchOrderByAlphabeticallyReverse
+ case "alphabetically":
+ orderBy = db.SearchOrderByAlphabetically
+ case "moststars":
+ orderBy = db.SearchOrderByStarsReverse
+ case "feweststars":
+ orderBy = db.SearchOrderByStars
+ case "mostforks":
+ orderBy = db.SearchOrderByForksReverse
+ case "fewestforks":
+ orderBy = db.SearchOrderByForks
+ default:
+ ctx.Data["SortType"] = "recentupdate"
+ orderBy = db.SearchOrderByRecentUpdated
+ }
+
+ repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ ListOptions: db.ListOptions{
+ PageSize: setting.UI.User.RepoPagingNum,
+ Page: page,
+ },
+ Actor: ctx.Doer,
+ Keyword: ctx.FormTrim("q"),
+ OrderBy: orderBy,
+ Private: ctx.IsSigned,
+ WatchedByID: ctx.Doer.ID,
+ Collaborate: util.OptionalBoolFalse,
+ TopicOnly: ctx.FormBool("topic"),
+ IncludeDescription: setting.UI.SearchRepoDescription,
+ })
+ if err != nil {
+ ctx.ServerError("SearchRepository", err)
+ return
+ }
+ total := int(count)
+ ctx.Data["Total"] = total
+ ctx.Data["Repos"] = repos
+
+ // redirect to last page if request page is more than total pages
+ pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5)
+ pager.SetDefaultParams(ctx)
+ ctx.Data["Page"] = pager
+
+ ctx.Data["Status"] = 2
+ ctx.Data["Title"] = ctx.Tr("notification.watching")
+
+ ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
}
// NewAvailable returns the notification counts
-func NewAvailable(ctx *context.APIContext) {
- ctx.JSON(http.StatusOK, api.NotificationCount{New: models.CountUnread(ctx.Doer)})
+func NewAvailable(ctx *context.Context) {
+ ctx.JSON(http.StatusOK, structs.NotificationCount{New: activities_model.CountUnread(ctx, ctx.Doer.ID)})
}
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 04b4e1e8ecd6c..c0aba7583fc0e 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -1,22 +1,23 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
import (
"net/http"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ org_model "code.gitea.io/gitea/models/organization"
packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
packages_service "code.gitea.io/gitea/services/packages"
@@ -43,9 +44,10 @@ func ListPackages(ctx *context.Context) {
PageSize: setting.UI.PackagesPagingNum,
Page: page,
},
- OwnerID: ctx.ContextUser.ID,
- Type: packages_model.Type(packageType),
- Name: packages_model.SearchValue{Value: query},
+ OwnerID: ctx.ContextUser.ID,
+ Type: packages_model.Type(packageType),
+ Name: packages_model.SearchValue{Value: query},
+ IsInternal: util.OptionalBoolFalse,
})
if err != nil {
ctx.ServerError("SearchLatestVersions", err)
@@ -58,6 +60,23 @@ func ListPackages(ctx *context.Context) {
return
}
+ repositoryAccessMap := make(map[int64]bool)
+ for _, pd := range pds {
+ if pd.Repository == nil {
+ continue
+ }
+ if _, has := repositoryAccessMap[pd.Repository.ID]; has {
+ continue
+ }
+
+ permission, err := access_model.GetUserRepoPermission(ctx, pd.Repository, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("GetUserRepoPermission", err)
+ return
+ }
+ repositoryAccessMap[pd.Repository.ID] = permission.HasAccess()
+ }
+
hasPackages, err := packages_model.HasOwnerPackages(ctx, ctx.ContextUser.ID)
if err != nil {
ctx.ServerError("HasOwnerPackages", err)
@@ -66,12 +85,30 @@ func ListPackages(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["IsPackagesPage"] = true
+ ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["Query"] = query
ctx.Data["PackageType"] = packageType
+ ctx.Data["AvailableTypes"] = packages_model.TypeList
ctx.Data["HasPackages"] = hasPackages
ctx.Data["PackageDescriptors"] = pds
ctx.Data["Total"] = total
+ ctx.Data["RepositoryAccessMap"] = repositoryAccessMap
+
+ // TODO: context/org -> HandleOrgAssignment() can not be used
+ if ctx.ContextUser.IsOrganization() {
+ org := org_model.OrgFromUser(ctx.ContextUser)
+ ctx.Data["Org"] = org
+ ctx.Data["OrgLink"] = ctx.ContextUser.OrganisationLink()
+
+ if ctx.Doer != nil {
+ ctx.Data["IsOrganizationMember"], _ = org_model.IsOrganizationMember(ctx, org.ID, ctx.Doer.ID)
+ ctx.Data["IsOrganizationOwner"], _ = org_model.IsOrganizationOwner(ctx, org.ID, ctx.Doer.ID)
+ } else {
+ ctx.Data["IsOrganizationMember"] = false
+ ctx.Data["IsOrganizationOwner"] = false
+ }
+ }
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
pager.AddParam(ctx, "q", "Query")
@@ -94,7 +131,8 @@ func RedirectToLastVersion(ctx *context.Context) {
}
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
- PackageID: p.ID,
+ PackageID: p.ID,
+ IsInternal: util.OptionalBoolFalse,
})
if err != nil {
ctx.ServerError("GetPackageByName", err)
@@ -120,6 +158,7 @@ func ViewPackageVersion(ctx *context.Context) {
ctx.Data["Title"] = pd.Package.Name
ctx.Data["IsPackagesPage"] = true
+ ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["PackageDescriptor"] = pd
@@ -139,8 +178,9 @@ func ViewPackageVersion(ctx *context.Context) {
})
default:
pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
- Paginator: db.NewAbsoluteListOptions(0, 5),
- PackageID: pd.Package.ID,
+ Paginator: db.NewAbsoluteListOptions(0, 5),
+ PackageID: pd.Package.ID,
+ IsInternal: util.OptionalBoolFalse,
})
if err != nil {
ctx.ServerError("SearchVersions", err)
@@ -157,6 +197,17 @@ func ViewPackageVersion(ctx *context.Context) {
ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin()
+ hasRepositoryAccess := false
+ if pd.Repository != nil {
+ permission, err := access_model.GetUserRepoPermission(ctx, pd.Repository, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("GetUserRepoPermission", err)
+ return
+ }
+ hasRepositoryAccess = permission.HasAccess()
+ }
+ ctx.Data["HasRepositoryAccess"] = hasRepositoryAccess
+
ctx.HTML(http.StatusOK, tplPackagesView)
}
@@ -182,18 +233,22 @@ func ListPackageVersions(ctx *context.Context) {
}
query := ctx.FormTrim("q")
+ sort := ctx.FormTrim("sort")
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["IsPackagesPage"] = true
+ ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["PackageDescriptor"] = &packages_model.PackageDescriptor{
Package: p,
Owner: ctx.Package.Owner,
}
ctx.Data["Query"] = query
+ ctx.Data["Sort"] = sort
pagerParams := map[string]string{
- "q": query,
+ "q": query,
+ "sort": sort,
}
var (
@@ -212,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)
@@ -225,6 +281,8 @@ func ListPackageVersions(ctx *context.Context) {
ExactMatch: false,
Value: query,
},
+ IsInternal: util.OptionalBoolFalse,
+ Sort: sort,
})
if err != nil {
ctx.ServerError("SearchVersions", err)
@@ -255,10 +313,11 @@ func PackageSettings(ctx *context.Context) {
ctx.Data["Title"] = pd.Package.Name
ctx.Data["IsPackagesPage"] = true
+ ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["ContextUser"] = ctx.ContextUser
ctx.Data["PackageDescriptor"] = pd
- repos, _, _ := models.GetUserRepositories(&models.SearchRepoOptions{
+ repos, _, _ := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{
Actor: pd.Owner,
Private: true,
})
@@ -278,7 +337,7 @@ func PackageSettingsPost(ctx *context.Context) {
success := func() bool {
repoID := int64(0)
if form.RepoID != 0 {
- repo, err := repo_model.GetRepositoryByID(form.RepoID)
+ repo, err := repo_model.GetRepositoryByID(ctx, form.RepoID)
if err != nil {
log.Error("Error getting repository: %v", err)
return false
@@ -343,5 +402,8 @@ func DownloadPackageFile(ctx *context.Context) {
}
defer s.Close()
- ctx.ServeStream(s, pf.Name)
+ ctx.ServeContent(s, &context.ServeHeaderOptions{
+ Filename: pf.Name,
+ LastModified: pf.CreatedUnix.AsLocalTime(),
+ })
}
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 85870eddf5a33..0002d56de01ba 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -1,7 +1,6 @@
// Copyright 2015 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.
+// SPDX-License-Identifier: MIT
package user
@@ -10,7 +9,7 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
project_model "code.gitea.io/gitea/models/project"
@@ -42,7 +41,7 @@ func Profile(ctx *context.Context) {
}
// check view permissions
- if !user_model.IsUserVisibleToViewer(ctx.ContextUser, ctx.Doer) {
+ if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound("user", fmt.Errorf(ctx.ContextUser.Name))
return
}
@@ -69,7 +68,7 @@ func Profile(ctx *context.Context) {
ctx.Data["IsFollowing"] = isFollowing
if setting.Service.EnableUserHeatmap {
- data, err := models.GetUserHeatmapDataByUser(ctx.ContextUser, ctx.Doer)
+ data, err := activities_model.GetUserHeatmapDataByUser(ctx.ContextUser, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserHeatmapDataByUser", err)
return
@@ -105,6 +104,13 @@ func Profile(ctx *context.Context) {
ctx.Data["Orgs"] = orgs
ctx.Data["HasOrgsVisible"] = organization.HasOrgsVisible(orgs, ctx.Doer)
+ badges, _, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
+ if err != nil {
+ ctx.ServerError("GetUserBadges", err)
+ return
+ }
+ ctx.Data["Badges"] = badges
+
tab := ctx.FormString("tab")
ctx.Data["TabName"] = tab
@@ -157,7 +163,7 @@ func Profile(ctx *context.Context) {
switch tab {
case "followers":
- items, err := user_model.GetUserFollowers(ctx.ContextUser, db.ListOptions{
+ items, count, err := user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
})
@@ -167,9 +173,9 @@ func Profile(ctx *context.Context) {
}
ctx.Data["Cards"] = items
- total = ctx.ContextUser.NumFollowers
+ total = int(count)
case "following":
- items, err := user_model.GetUserFollowing(ctx.ContextUser, db.ListOptions{
+ items, count, err := user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
})
@@ -179,15 +185,16 @@ func Profile(ctx *context.Context) {
}
ctx.Data["Cards"] = items
- total = ctx.ContextUser.NumFollowing
+ total = int(count)
case "activity":
- ctx.Data["Feeds"], err = models.GetFeeds(ctx, models.GetFeedsOptions{
+ ctx.Data["Feeds"], err = activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctx.ContextUser,
Actor: ctx.Doer,
IncludePrivate: showPrivate,
OnlyPerformedBy: true,
IncludeDeleted: false,
Date: ctx.FormString("date"),
+ ListOptions: db.ListOptions{PageSize: setting.UI.FeedPagingNum},
})
if err != nil {
ctx.ServerError("GetFeeds", err)
@@ -195,7 +202,7 @@ func Profile(ctx *context.Context) {
}
case "stars":
ctx.Data["PageIsProfileStarList"] = true
- repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+ repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
@@ -217,7 +224,7 @@ func Profile(ctx *context.Context) {
total = int(count)
case "projects":
- ctx.Data["OpenProjects"], _, err = project_model.GetProjects(project_model.SearchOptions{
+ ctx.Data["OpenProjects"], _, err = project_model.GetProjects(ctx, project_model.SearchOptions{
Page: -1,
IsClosed: util.OptionalBoolFalse,
Type: project_model.TypeIndividual,
@@ -227,7 +234,7 @@ func Profile(ctx *context.Context) {
return
}
case "watching":
- repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+ repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
@@ -249,7 +256,7 @@ func Profile(ctx *context.Context) {
total = int(count)
default:
- repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+ repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
@@ -276,11 +283,13 @@ func Profile(ctx *context.Context) {
pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5)
pager.SetDefaultParams(ctx)
+ pager.AddParam(ctx, "tab", "TabName")
if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" {
pager.AddParam(ctx, "language", "Language")
}
ctx.Data["Page"] = pager
ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
+ ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
ctx.Data["ShowUserEmail"] = len(ctx.ContextUser.Email) > 0 && ctx.IsSigned && (!ctx.ContextUser.KeepEmailPrivate || ctx.ContextUser.ID == ctx.Doer.ID)
diff --git a/routers/web/user/search.go b/routers/web/user/search.go
index 328c7bade4f8b..f9b0e07358645 100644
--- a/routers/web/user/search.go
+++ b/routers/web/user/search.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
@@ -10,7 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
+ "code.gitea.io/gitea/services/convert"
)
// Search search users
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index b2476dff94365..95b3b4040d96e 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package setting
@@ -34,6 +33,7 @@ func Account(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true
ctx.Data["Email"] = ctx.Doer.Email
+ ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail
loadAccountData(ctx)
@@ -105,7 +105,7 @@ func EmailPost(ctx *context.Context) {
// Send activation Email
if ctx.FormString("_method") == "SENDACTIVATION" {
var address string
- if ctx.Cache.IsExist("MailResendLimit_" + ctx.Doer.LowerName) {
+ if setting.CacheService.Enabled && ctx.Cache.IsExist("MailResendLimit_"+ctx.Doer.LowerName) {
log.Error("Send activation: activation still pending")
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
@@ -141,10 +141,12 @@ func EmailPost(ctx *context.Context) {
}
address = email.Email
- if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
+ if setting.CacheService.Enabled {
+ if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ }
}
- ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", address, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
+ ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", address, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
@@ -153,7 +155,8 @@ func EmailPost(ctx *context.Context) {
preference := ctx.FormString("preference")
if !(preference == user_model.EmailNotificationsEnabled ||
preference == user_model.EmailNotificationsOnMention ||
- preference == user_model.EmailNotificationsDisabled) {
+ preference == user_model.EmailNotificationsDisabled ||
+ preference == user_model.EmailNotificationsAndYourOwn) {
log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.Doer.Name)
ctx.ServerError("SetEmailPreference", errors.New("option unrecognized"))
return
@@ -181,7 +184,7 @@ func EmailPost(ctx *context.Context) {
Email: form.Email,
IsActivated: !setting.Service.RegisterEmailConfirm,
}
- if err := user_model.AddEmailAddress(email); err != nil {
+ if err := user_model.AddEmailAddress(ctx, email); err != nil {
if user_model.IsErrEmailAlreadyUsed(err) {
loadAccountData(ctx)
@@ -201,10 +204,12 @@ func EmailPost(ctx *context.Context) {
// Send confirmation email
if setting.Service.RegisterEmailConfirm {
mailer.SendActivateEmailMail(ctx.Doer, email)
- if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
- log.Error("Set cache(MailResendLimit) fail: %v", err)
+ if setting.CacheService.Enabled {
+ if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
+ log.Error("Set cache(MailResendLimit) fail: %v", err)
+ }
}
- ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
+ ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)))
} else {
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
}
@@ -243,7 +248,7 @@ func DeleteAccount(ctx *context.Context) {
return
}
- if err := user.DeleteUser(ctx.Doer); err != nil {
+ if err := user.DeleteUser(ctx, ctx.Doer, false); err != nil {
switch {
case models.IsErrUserOwnRepos(err):
ctx.Flash.Error(ctx.Tr("form.still_own_repo"))
@@ -273,7 +278,7 @@ func loadAccountData(ctx *context.Context) {
user_model.EmailAddress
CanBePrimary bool
}
- pendingActivation := ctx.Cache.IsExist("MailResendLimit_" + ctx.Doer.LowerName)
+ pendingActivation := setting.CacheService.Enabled && ctx.Cache.IsExist("MailResendLimit_"+ctx.Doer.LowerName)
emails := make([]*UserEmail, len(emlist))
for i, em := range emlist {
var email UserEmail
diff --git a/routers/web/user/setting/account_test.go b/routers/web/user/setting/account_test.go
index 005603e7ac851..5fce41f065ba4 100644
--- a/routers/web/user/setting/account_test.go
+++ b/routers/web/user/setting/account_test.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package setting
diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go
index ce2377a997d07..0aaf5920bc6d1 100644
--- a/routers/web/user/setting/adopt.go
+++ b/routers/web/user/setting/adopt.go
@@ -1,16 +1,15 @@
// 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.
+// SPDX-License-Identifier: MIT
package setting
import (
"path/filepath"
- "code.gitea.io/gitea/models"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
repo_service "code.gitea.io/gitea/services/repository"
@@ -32,7 +31,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) {
root := user_model.UserPath(ctxUser.LowerName)
// check not a repo
- has, err := repo_model.IsRepositoryExist(ctxUser, dir)
+ has, err := repo_model.IsRepositoryExist(ctx, ctxUser, dir)
if err != nil {
ctx.ServerError("IsRepositoryExist", err)
return
@@ -46,7 +45,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) {
if has || !isDir {
// Fallthrough to failure mode
} else if action == "adopt" && allowAdopt {
- if _, err := repo_service.AdoptRepository(ctxUser, ctxUser, models.CreateRepoOptions{
+ if _, err := repo_service.AdoptRepository(ctxUser, ctxUser, repo_module.CreateRepoOptions{
Name: dir,
IsPrivate: true,
}); err != nil {
diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go
index b0f599fc45113..b66806ff2de0b 100644
--- a/routers/web/user/setting/applications.go
+++ b/routers/web/user/setting/applications.go
@@ -1,15 +1,13 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/auth"
+ auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
@@ -44,12 +42,18 @@ func ApplicationsPost(ctx *context.Context) {
return
}
- t := &models.AccessToken{
- UID: ctx.Doer.ID,
- Name: form.Name,
+ scope, err := form.GetScope()
+ if err != nil {
+ ctx.ServerError("GetScope", err)
+ return
+ }
+ t := &auth_model.AccessToken{
+ UID: ctx.Doer.ID,
+ Name: form.Name,
+ Scope: scope,
}
- exist, err := models.AccessTokenByNameExists(t)
+ exist, err := auth_model.AccessTokenByNameExists(t)
if err != nil {
ctx.ServerError("AccessTokenByNameExists", err)
return
@@ -60,7 +64,7 @@ func ApplicationsPost(ctx *context.Context) {
return
}
- if err := models.NewAccessToken(t); err != nil {
+ if err := auth_model.NewAccessToken(t); err != nil {
ctx.ServerError("NewAccessToken", err)
return
}
@@ -73,7 +77,7 @@ func ApplicationsPost(ctx *context.Context) {
// DeleteApplication response for delete user access token
func DeleteApplication(ctx *context.Context) {
- if err := models.DeleteAccessTokenByID(ctx.FormInt64("id"), ctx.Doer.ID); err != nil {
+ if err := auth_model.DeleteAccessTokenByID(ctx.FormInt64("id"), ctx.Doer.ID); err != nil {
ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
@@ -85,7 +89,7 @@ func DeleteApplication(ctx *context.Context) {
}
func loadApplicationsData(ctx *context.Context) {
- tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.Doer.ID})
+ tokens, err := auth_model.ListAccessTokens(auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID})
if err != nil {
ctx.ServerError("ListAccessTokens", err)
return
@@ -93,12 +97,12 @@ func loadApplicationsData(ctx *context.Context) {
ctx.Data["Tokens"] = tokens
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
if setting.OAuth2.Enable {
- ctx.Data["Applications"], err = auth.GetOAuth2ApplicationsByUserID(ctx.Doer.ID)
+ ctx.Data["Applications"], err = auth_model.GetOAuth2ApplicationsByUserID(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
return
}
- ctx.Data["Grants"], err = auth.GetOAuth2GrantsByUserID(ctx.Doer.ID)
+ ctx.Data["Grants"], err = auth_model.GetOAuth2GrantsByUserID(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetOAuth2GrantsByUserID", err)
return
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index a8d07ea47a9d4..ec50eef9c18de 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package setting
@@ -100,14 +99,18 @@ func KeysPost(ctx *context.Context) {
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.Data["Err_Signature"] = true
- ctx.Data["KeyID"] = err.(asymkey_model.ErrGPGInvalidTokenSignature).ID
+ keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID
+ ctx.Data["KeyID"] = keyID
+ ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID)
ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
case asymkey_model.IsErrGPGNoEmailFound(err):
loadKeysData(ctx)
ctx.Data["Err_Content"] = true
ctx.Data["Err_Signature"] = true
- ctx.Data["KeyID"] = err.(asymkey_model.ErrGPGNoEmailFound).ID
+ keyID := err.(asymkey_model.ErrGPGNoEmailFound).ID
+ ctx.Data["KeyID"] = keyID
+ ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID)
ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
default:
ctx.ServerError("AddPublicKey", err)
@@ -139,7 +142,9 @@ func KeysPost(ctx *context.Context) {
loadKeysData(ctx)
ctx.Data["VerifyingID"] = form.KeyID
ctx.Data["Err_Signature"] = true
- ctx.Data["KeyID"] = err.(asymkey_model.ErrGPGInvalidTokenSignature).ID
+ keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID
+ ctx.Data["KeyID"] = keyID
+ ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID)
ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
default:
ctx.ServerError("VerifyGPG", err)
diff --git a/routers/web/user/setting/main_test.go b/routers/web/user/setting/main_test.go
index d4df464abd827..c3938b32011c5 100644
--- a/routers/web/user/setting/main_test.go
+++ b/routers/web/user/setting/main_test.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package setting
diff --git a/routers/web/user/setting/oauth2.go b/routers/web/user/setting/oauth2.go
index 76c50852a04e1..93142c21fcfc1 100644
--- a/routers/web/user/setting/oauth2.go
+++ b/routers/web/user/setting/oauth2.go
@@ -1,83 +1,43 @@
// 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.
+// SPDX-License-Identifier: MIT
package setting
import (
- "fmt"
- "net/http"
-
- "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/forms"
)
const (
- tplSettingsOAuthApplications base.TplName = "user/settings/applications_oauth2_edit"
+ tplSettingsOAuthApplicationEdit base.TplName = "user/settings/applications_oauth2_edit"
)
+func newOAuth2CommonHandlers(userID int64) *OAuth2CommonHandlers {
+ return &OAuth2CommonHandlers{
+ OwnerID: userID,
+ BasePathList: setting.AppSubURL + "/user/settings/applications",
+ BasePathEditPrefix: setting.AppSubURL + "/user/settings/applications/oauth2",
+ TplAppEdit: tplSettingsOAuthApplicationEdit,
+ }
+}
+
// OAuthApplicationsPost response for adding a oauth2 application
func OAuthApplicationsPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true
- if ctx.HasError() {
- loadApplicationsData(ctx)
-
- ctx.HTML(http.StatusOK, tplSettingsApplications)
- return
- }
- // TODO validate redirect URI
- app, err := auth.CreateOAuth2Application(auth.CreateOAuth2ApplicationOptions{
- Name: form.Name,
- RedirectURIs: []string{form.RedirectURI},
- UserID: ctx.Doer.ID,
- })
- if err != nil {
- ctx.ServerError("CreateOAuth2Application", err)
- return
- }
- ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"))
- ctx.Data["App"] = app
- ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
- if err != nil {
- ctx.ServerError("GenerateClientSecret", err)
- return
- }
- ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
+ oa := newOAuth2CommonHandlers(ctx.Doer.ID)
+ oa.AddApp(ctx)
}
// OAuthApplicationsEdit response for editing oauth2 application
func OAuthApplicationsEdit(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true
- if ctx.HasError() {
- loadApplicationsData(ctx)
-
- ctx.HTML(http.StatusOK, tplSettingsApplications)
- return
- }
- // 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: ctx.Doer.ID,
- }); err != nil {
- ctx.ServerError("UpdateOAuth2Application", err)
- return
- }
- ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
- ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
+ oa := newOAuth2CommonHandlers(ctx.Doer.ID)
+ oa.EditSave(ctx)
}
// OAuthApplicationsRegenerateSecret handles the post request for regenerating the secret
@@ -85,75 +45,24 @@ func OAuthApplicationsRegenerateSecret(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsApplications"] = true
- app, err := auth.GetOAuth2ApplicationByID(ctx.ParamsInt64("id"))
- if err != nil {
- if auth.IsErrOAuthApplicationNotFound(err) {
- ctx.NotFound("Application not found", err)
- return
- }
- ctx.ServerError("GetOAuth2ApplicationByID", err)
- return
- }
- if app.UID != ctx.Doer.ID {
- ctx.NotFound("Application not found", nil)
- return
- }
- ctx.Data["App"] = app
- ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
- if err != nil {
- ctx.ServerError("GenerateClientSecret", err)
- return
- }
- ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
- ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
+ oa := newOAuth2CommonHandlers(ctx.Doer.ID)
+ oa.RegenerateSecret(ctx)
}
// OAuth2ApplicationShow displays the given application
func OAuth2ApplicationShow(ctx *context.Context) {
- app, err := auth.GetOAuth2ApplicationByID(ctx.ParamsInt64("id"))
- if err != nil {
- if auth.IsErrOAuthApplicationNotFound(err) {
- ctx.NotFound("Application not found", err)
- return
- }
- ctx.ServerError("GetOAuth2ApplicationByID", err)
- return
- }
- if app.UID != ctx.Doer.ID {
- ctx.NotFound("Application not found", nil)
- return
- }
- ctx.Data["App"] = app
- ctx.HTML(http.StatusOK, tplSettingsOAuthApplications)
+ oa := newOAuth2CommonHandlers(ctx.Doer.ID)
+ oa.EditShow(ctx)
}
// DeleteOAuth2Application deletes the given oauth2 application
func DeleteOAuth2Application(ctx *context.Context) {
- if err := auth.DeleteOAuth2Application(ctx.FormInt64("id"), ctx.Doer.ID); err != nil {
- ctx.ServerError("DeleteOAuth2Application", err)
- return
- }
- log.Trace("OAuth2 Application deleted: %s", ctx.Doer.Name)
-
- ctx.Flash.Success(ctx.Tr("settings.remove_oauth2_application_success"))
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": setting.AppSubURL + "/user/settings/applications",
- })
+ oa := newOAuth2CommonHandlers(ctx.Doer.ID)
+ oa.DeleteApp(ctx)
}
// RevokeOAuth2Grant revokes the grant with the given id
func RevokeOAuth2Grant(ctx *context.Context) {
- if ctx.Doer.ID == 0 || ctx.FormInt64("id") == 0 {
- ctx.ServerError("RevokeOAuth2Grant", fmt.Errorf("user id or grant id is zero"))
- return
- }
- if err := auth.RevokeOAuth2Grant(ctx.FormInt64("id"), ctx.Doer.ID); err != nil {
- ctx.ServerError("RevokeOAuth2Grant", err)
- return
- }
-
- ctx.Flash.Success(ctx.Tr("settings.revoke_oauth2_grant_success"))
- ctx.JSON(http.StatusOK, map[string]interface{}{
- "redirect": setting.AppSubURL + "/user/settings/applications",
- })
+ oa := newOAuth2CommonHandlers(ctx.Doer.ID)
+ oa.RevokeGrant(ctx)
}
diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go
new file mode 100644
index 0000000000000..f6ad1b2b381ca
--- /dev/null
+++ b/routers/web/user/setting/oauth2_common.go
@@ -0,0 +1,151 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "fmt"
+ "net/http"
+
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/forms"
+)
+
+type OAuth2CommonHandlers struct {
+ OwnerID int64 // 0 for instance-wide, otherwise OrgID or UserID
+ BasePathList string // the base URL for the application list page, eg: "/user/setting/applications"
+ BasePathEditPrefix string // the base URL for the application edit page, will be appended with app id, eg: "/user/setting/applications/oauth2"
+ TplAppEdit base.TplName // the template for the application edit page
+}
+
+func (oa *OAuth2CommonHandlers) renderEditPage(ctx *context.Context) {
+ app := ctx.Data["App"].(*auth.OAuth2Application)
+ ctx.Data["FormActionPath"] = fmt.Sprintf("%s/%d", oa.BasePathEditPrefix, app.ID)
+ ctx.HTML(http.StatusOK, oa.TplAppEdit)
+}
+
+// AddApp adds an oauth2 application
+func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
+ if ctx.HasError() {
+ // go to the application list page
+ ctx.Redirect(oa.BasePathList)
+ return
+ }
+
+ // TODO validate redirect URI
+ app, err := auth.CreateOAuth2Application(ctx, auth.CreateOAuth2ApplicationOptions{
+ Name: form.Name,
+ RedirectURIs: []string{form.RedirectURI},
+ UserID: oa.OwnerID,
+ ConfidentialClient: form.ConfidentialClient,
+ })
+ if err != nil {
+ ctx.ServerError("CreateOAuth2Application", err)
+ return
+ }
+
+ // render the edit page with secret
+ ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"), true)
+ ctx.Data["App"] = app
+ ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
+ if err != nil {
+ ctx.ServerError("GenerateClientSecret", err)
+ return
+ }
+ oa.renderEditPage(ctx)
+}
+
+// EditShow displays the given application
+func (oa *OAuth2CommonHandlers) EditShow(ctx *context.Context) {
+ app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.ParamsInt64("id"))
+ if err != nil {
+ if auth.IsErrOAuthApplicationNotFound(err) {
+ ctx.NotFound("Application not found", err)
+ return
+ }
+ ctx.ServerError("GetOAuth2ApplicationByID", err)
+ return
+ }
+ if app.UID != oa.OwnerID {
+ ctx.NotFound("Application not found", nil)
+ return
+ }
+ ctx.Data["App"] = app
+ oa.renderEditPage(ctx)
+}
+
+// EditSave saves the oauth2 application
+func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.EditOAuth2ApplicationForm)
+
+ if ctx.HasError() {
+ oa.renderEditPage(ctx)
+ return
+ }
+
+ // 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,
+ ConfidentialClient: form.ConfidentialClient,
+ }); err != nil {
+ ctx.ServerError("UpdateOAuth2Application", err)
+ return
+ }
+ ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
+ ctx.Redirect(oa.BasePathList)
+}
+
+// RegenerateSecret regenerates the secret
+func (oa *OAuth2CommonHandlers) RegenerateSecret(ctx *context.Context) {
+ app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.ParamsInt64("id"))
+ if err != nil {
+ if auth.IsErrOAuthApplicationNotFound(err) {
+ ctx.NotFound("Application not found", err)
+ return
+ }
+ ctx.ServerError("GetOAuth2ApplicationByID", err)
+ return
+ }
+ if app.UID != oa.OwnerID {
+ ctx.NotFound("Application not found", nil)
+ return
+ }
+ ctx.Data["App"] = app
+ ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
+ if err != nil {
+ ctx.ServerError("GenerateClientSecret", err)
+ return
+ }
+ ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"), true)
+ oa.renderEditPage(ctx)
+}
+
+// DeleteApp deletes the given oauth2 application
+func (oa *OAuth2CommonHandlers) DeleteApp(ctx *context.Context) {
+ if err := auth.DeleteOAuth2Application(ctx.ParamsInt64("id"), oa.OwnerID); err != nil {
+ ctx.ServerError("DeleteOAuth2Application", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("settings.remove_oauth2_application_success"))
+ ctx.JSON(http.StatusOK, map[string]interface{}{"redirect": oa.BasePathList})
+}
+
+// RevokeGrant revokes the grant
+func (oa *OAuth2CommonHandlers) RevokeGrant(ctx *context.Context) {
+ if err := auth.RevokeOAuth2Grant(ctx, ctx.ParamsInt64("grantId"), oa.OwnerID); err != nil {
+ ctx.ServerError("RevokeOAuth2Grant", err)
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("settings.revoke_oauth2_grant_success"))
+ ctx.JSON(http.StatusOK, map[string]interface{}{"redirect": oa.BasePathList})
+}
diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go
new file mode 100644
index 0000000000000..f6f7195adf1f2
--- /dev/null
+++ b/routers/web/user/setting/packages.go
@@ -0,0 +1,79 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/setting"
+ shared "code.gitea.io/gitea/routers/web/shared/packages"
+)
+
+const (
+ tplSettingsPackages base.TplName = "user/settings/packages"
+ tplSettingsPackagesRuleEdit base.TplName = "user/settings/packages_cleanup_rules_edit"
+ tplSettingsPackagesRulePreview base.TplName = "user/settings/packages_cleanup_rules_preview"
+)
+
+func Packages(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("packages.title")
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.SetPackagesContext(ctx, ctx.Doer)
+
+ ctx.HTML(http.StatusOK, tplSettingsPackages)
+}
+
+func PackagesRuleAdd(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("packages.title")
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.SetRuleAddContext(ctx)
+
+ ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
+}
+
+func PackagesRuleEdit(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("packages.title")
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.SetRuleEditContext(ctx, ctx.Doer)
+
+ ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit)
+}
+
+func PackagesRuleAddPost(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.PerformRuleAddPost(
+ ctx,
+ ctx.Doer,
+ setting.AppSubURL+"/user/settings/packages",
+ tplSettingsPackagesRuleEdit,
+ )
+}
+
+func PackagesRuleEditPost(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("packages.title")
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.PerformRuleEditPost(
+ ctx,
+ ctx.Doer,
+ setting.AppSubURL+"/user/settings/packages",
+ tplSettingsPackagesRuleEdit,
+ )
+}
+
+func PackagesRulePreview(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("packages.title")
+ ctx.Data["PageIsSettingsPackages"] = true
+
+ shared.SetRulePreviewContext(ctx, ctx.Doer)
+
+ ctx.HTML(http.StatusOK, tplSettingsPackagesRulePreview)
+}
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 0123b9b523396..af161952501b5 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package setting
@@ -15,7 +14,6 @@ import (
"path/filepath"
"strings"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
@@ -24,13 +22,14 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/translation/i18n"
+ "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/agit"
"code.gitea.io/gitea/services/forms"
+ container_service "code.gitea.io/gitea/services/packages/container"
user_service "code.gitea.io/gitea/services/user"
)
@@ -91,6 +90,11 @@ func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName s
return err
}
+ if err := container_service.UpdateRepositoryNames(ctx, user, newName); err != nil {
+ ctx.ServerError("UpdateRepositoryNames", err)
+ return err
+ }
+
log.Trace("User name changed: %s -> %s", user.Name, newName)
return nil
}
@@ -137,7 +141,7 @@ func ProfilePost(ctx *context.Context) {
middleware.SetLocaleCookie(ctx.Resp, ctx.Doer.Language, 0)
log.Trace("User settings updated: %s", ctx.Doer.Name)
- ctx.Flash.Success(i18n.Tr(ctx.Doer.Language, "settings.update_profile_success"))
+ ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).Tr("settings.update_profile_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
}
@@ -157,7 +161,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
if form.Avatar != nil && form.Avatar.Filename != "" {
fr, err := form.Avatar.Open()
if err != nil {
- return fmt.Errorf("Avatar.Open: %v", err)
+ return fmt.Errorf("Avatar.Open: %w", err)
}
defer fr.Close()
@@ -167,7 +171,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
data, err := io.ReadAll(fr)
if err != nil {
- return fmt.Errorf("io.ReadAll: %v", err)
+ return fmt.Errorf("io.ReadAll: %w", err)
}
st := typesniffer.DetectContentType(data)
@@ -175,18 +179,18 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image"))
}
if err = user_service.UploadAvatar(ctxUser, data); err != nil {
- return fmt.Errorf("UploadAvatar: %v", err)
+ return fmt.Errorf("UploadAvatar: %w", err)
}
} else if ctxUser.UseCustomAvatar && ctxUser.Avatar == "" {
// No avatar is uploaded but setting has been changed to enable,
// generate a random one when needed.
- if err := user_model.GenerateRandomAvatar(ctxUser); err != nil {
+ if err := user_model.GenerateRandomAvatar(ctx, ctxUser); err != nil {
log.Error("GenerateRandomAvatar[%d]: %v", ctxUser.ID, err)
}
}
if err := user_model.UpdateUserCols(ctx, ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil {
- return fmt.Errorf("UpdateUser: %v", err)
+ return fmt.Errorf("UpdateUser: %w", err)
}
return nil
@@ -276,17 +280,17 @@ func Repos(ctx *context.Context) {
repos := map[string]*repo_model.Repository{}
// We're going to iterate by pagesize.
root := user_model.UserPath(ctxUser.Name)
- if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+ if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
- if !info.IsDir() || path == root {
+ if !d.IsDir() || path == root {
return nil
}
- name := info.Name()
+ name := d.Name()
if !strings.HasSuffix(name, ".git") {
return filepath.SkipDir
}
@@ -300,11 +304,11 @@ func Repos(ctx *context.Context) {
count++
return filepath.SkipDir
}); err != nil {
- ctx.ServerError("filepath.Walk", err)
+ ctx.ServerError("filepath.WalkDir", err)
return
}
- userRepos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{
+ userRepos, _, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{
Actor: ctxUser,
Private: true,
ListOptions: db.ListOptions{
@@ -319,7 +323,7 @@ func Repos(ctx *context.Context) {
}
for _, repo := range userRepos {
if repo.IsFork {
- if err := repo.GetBaseRepo(); err != nil {
+ if err := repo.GetBaseRepo(ctx); err != nil {
ctx.ServerError("GetBaseRepo", err)
return
}
@@ -329,7 +333,7 @@ func Repos(ctx *context.Context) {
ctx.Data["Dirs"] = repoNames
ctx.Data["ReposMap"] = repos
} else {
- repos, count64, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
+ repos, count64, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
if err != nil {
ctx.ServerError("GetUserRepositories", err)
return
@@ -338,7 +342,7 @@ func Repos(ctx *context.Context) {
for i := range repos {
if repos[i].IsFork {
- if err := repos[i].GetBaseRepo(); err != nil {
+ if err := repos[i].GetBaseRepo(ctx); err != nil {
ctx.ServerError("GetBaseRepo", err)
return
}
@@ -348,7 +352,7 @@ func Repos(ctx *context.Context) {
ctx.Data["Repos"] = repos
}
ctx.Data["Owner"] = ctxUser
- pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
+ pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSettingsRepositories)
@@ -409,7 +413,7 @@ func UpdateUserLang(ctx *context.Context) {
ctx.Data["PageIsSettingsAppearance"] = true
if len(form.Language) != 0 {
- if !util.IsStringInSlice(form.Language, setting.Langs) {
+ if !util.SliceContainsString(setting.Langs, form.Language) {
ctx.Flash.Error(ctx.Tr("settings.update_language_not_found", form.Language))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
return
@@ -426,7 +430,7 @@ func UpdateUserLang(ctx *context.Context) {
middleware.SetLocaleCookie(ctx.Resp, ctx.Doer.Language, 0)
log.Trace("User settings updated: %s", ctx.Doer.Name)
- ctx.Flash.Success(i18n.Tr(ctx.Doer.Language, "settings.update_language_success"))
+ ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).Tr("settings.update_language_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
}
diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go
index 5fd81bae4181b..0cecb1aa37fef 100644
--- a/routers/web/user/setting/security/2fa.go
+++ b/routers/web/user/setting/security/2fa.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package security
diff --git a/routers/web/user/setting/security/openid.go b/routers/web/user/setting/security/openid.go
index 2ecc9b053387a..08fcb6b62343c 100644
--- a/routers/web/user/setting/security/openid.go
+++ b/routers/web/user/setting/security/openid.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package security
@@ -90,7 +89,7 @@ func settingsOpenIDVerify(ctx *context.Context) {
log.Trace("Verified ID: " + id)
oid := &user_model.UserOpenID{UID: ctx.Doer.ID, URI: id}
- if err = user_model.AddUserOpenID(oid); err != nil {
+ if err = user_model.AddUserOpenID(ctx, oid); err != nil {
if user_model.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &forms.AddOpenIDForm{Openid: id})
return
diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go
index a87012c480b4d..db6faaed6e197 100644
--- a/routers/web/user/setting/security/security.go
+++ b/routers/web/user/setting/security/security.go
@@ -1,19 +1,18 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package security
import (
"net/http"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/auth"
+ auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/auth/source/oauth2"
)
const (
@@ -25,7 +24,6 @@ const (
func Security(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
- ctx.Data["RequireU2F"] = true
if ctx.FormString("openid.return_to") != "" {
settingsOpenIDVerify(ctx)
@@ -56,21 +54,21 @@ func DeleteAccountLink(ctx *context.Context) {
}
func loadSecurityData(ctx *context.Context) {
- enrolled, err := auth.HasTwoFactorByUID(ctx.Doer.ID)
+ enrolled, err := auth_model.HasTwoFactorByUID(ctx.Doer.ID)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
}
ctx.Data["TOTPEnrolled"] = enrolled
- credentials, err := auth.GetWebAuthnCredentialsByUID(ctx.Doer.ID)
+ credentials, err := auth_model.GetWebAuthnCredentialsByUID(ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetWebAuthnCredentialsByUID", err)
return
}
ctx.Data["WebAuthnCredentials"] = credentials
- tokens, err := models.ListAccessTokens(models.ListAccessTokensOptions{UserID: ctx.Doer.ID})
+ tokens, err := auth_model.ListAccessTokens(auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID})
if err != nil {
ctx.ServerError("ListAccessTokens", err)
return
@@ -84,9 +82,9 @@ func loadSecurityData(ctx *context.Context) {
}
// map the provider display name with the AuthSource
- sources := make(map[*auth.Source]string)
+ sources := make(map[*auth_model.Source]string)
for _, externalAccount := range accountLinks {
- if authSource, err := auth.GetSourceByID(externalAccount.LoginSourceID); err == nil {
+ if authSource, err := auth_model.GetSourceByID(externalAccount.LoginSourceID); err == nil {
var providerDisplayName string
type DisplayNamed interface {
@@ -109,6 +107,14 @@ func loadSecurityData(ctx *context.Context) {
}
ctx.Data["AccountLinks"] = sources
+ orderedOAuth2Names, oauth2Providers, err := oauth2.GetActiveOAuth2Providers()
+ if err != nil {
+ ctx.ServerError("GetActiveOAuth2Providers", err)
+ return
+ }
+ ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
+ ctx.Data["OAuth2Providers"] = oauth2Providers
+
openid, err := user_model.GetUserOpenIDs(ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go
index bb2d1f733e08c..0054318867461 100644
--- a/routers/web/user/setting/security/webauthn.go
+++ b/routers/web/user/setting/security/webauthn.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package security
@@ -16,8 +15,8 @@ import (
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
- "github.com/duo-labs/webauthn/protocol"
- "github.com/duo-labs/webauthn/webauthn"
+ "github.com/go-webauthn/webauthn/protocol"
+ "github.com/go-webauthn/webauthn/webauthn"
)
// WebAuthnRegister initializes the webauthn registration procedure
diff --git a/routers/web/user/stop_watch.go b/routers/web/user/stop_watch.go
index 4b16c9aeda704..d262c777c30f7 100644
--- a/routers/web/user/stop_watch.go
+++ b/routers/web/user/stop_watch.go
@@ -1,21 +1,20 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
import (
"net/http"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
+ "code.gitea.io/gitea/services/convert"
)
// GetStopwatches get all stopwatches
func GetStopwatches(ctx *context.Context) {
- sws, err := models.GetUserStopwatches(ctx.Doer.ID, db.ListOptions{
+ sws, err := issues_model.GetUserStopwatches(ctx.Doer.ID, db.ListOptions{
Page: ctx.FormInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
})
@@ -24,7 +23,7 @@ func GetStopwatches(ctx *context.Context) {
return
}
- count, err := models.CountUserStopwatches(ctx.Doer.ID)
+ count, err := issues_model.CountUserStopwatches(ctx.Doer.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
diff --git a/routers/web/user/task.go b/routers/web/user/task.go
index fd561cdd4cfcb..3818682403d0f 100644
--- a/routers/web/user/task.go
+++ b/routers/web/user/task.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
@@ -8,16 +7,16 @@ import (
"net/http"
"strconv"
- "code.gitea.io/gitea/models"
+ admin_model "code.gitea.io/gitea/models/admin"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/json"
)
// TaskStatus returns task's status
func TaskStatus(ctx *context.Context) {
- task, opts, err := models.GetMigratingTaskByID(ctx.ParamsInt64("task"), ctx.Doer.ID)
+ task, opts, err := admin_model.GetMigratingTaskByID(ctx.ParamsInt64("task"), ctx.Doer.ID)
if err != nil {
- if models.IsErrTaskDoesNotExist(err) {
+ if admin_model.IsErrTaskDoesNotExist(err) {
ctx.JSON(http.StatusNotFound, map[string]interface{}{
"error": "task `" + strconv.FormatInt(ctx.ParamsInt64("task"), 10) + "` does not exist",
})
@@ -33,9 +32,9 @@ func TaskStatus(ctx *context.Context) {
if task.Message != "" && task.Message[0] == '{' {
// assume message is actually a translatable string
- var translatableMessage models.TranslatableMessage
+ var translatableMessage admin_model.TranslatableMessage
if err := json.Unmarshal([]byte(message), &translatableMessage); err != nil {
- translatableMessage = models.TranslatableMessage{
+ translatableMessage = admin_model.TranslatableMessage{
Format: "migrate.migrating_failed.error",
Args: []interface{}{task.Message},
}
diff --git a/routers/web/web.go b/routers/web/web.go
index 0de6f1372275c..f0fedd0715187 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package web
@@ -12,6 +11,7 @@ import (
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
@@ -27,10 +27,10 @@ import (
"code.gitea.io/gitea/modules/web/routing"
"code.gitea.io/gitea/routers/web/admin"
"code.gitea.io/gitea/routers/web/auth"
- "code.gitea.io/gitea/routers/web/dev"
"code.gitea.io/gitea/routers/web/events"
"code.gitea.io/gitea/routers/web/explore"
"code.gitea.io/gitea/routers/web/feed"
+ "code.gitea.io/gitea/routers/web/healthcheck"
"code.gitea.io/gitea/routers/web/misc"
"code.gitea.io/gitea/routers/web/org"
"code.gitea.io/gitea/routers/web/repo"
@@ -41,7 +41,6 @@ import (
context_service "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/lfs"
- "code.gitea.io/gitea/services/mailer"
_ "code.gitea.io/gitea/modules/session" // to registers all internal adapters
@@ -67,6 +66,7 @@ func CorsHandler() func(next http.Handler) http.Handler {
// setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
AllowedMethods: setting.CORSConfig.Methods,
AllowCredentials: setting.CORSConfig.AllowCredentials,
+ AllowedHeaders: setting.CORSConfig.Headers,
MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
})
}
@@ -84,7 +84,7 @@ func CorsHandler() func(next http.Handler) http.Handler {
// for users that have already signed in.
func buildAuthGroup() *auth_service.Group {
group := auth_service.NewGroup(
- &auth_service.OAuth2{}, // FIXME: this should be removed and only applied in download and oauth realted routers
+ &auth_service.OAuth2{}, // FIXME: this should be removed and only applied in download and oauth related routers
&auth_service.Basic{}, // FIXME: this should be removed and only applied in download and git/lfs routers
&auth_service.Session{},
)
@@ -97,7 +97,7 @@ func buildAuthGroup() *auth_service.Group {
}
// Routes returns all web routes
-func Routes() *web.Route {
+func Routes(ctx gocontext.Context) *web.Route {
routes := web.NewRoute()
routes.Use(web.WrapWithPrefix(public.AssetsURLPathPrefix, public.AssetsHandlerFunc(&public.Options{
@@ -119,13 +119,15 @@ func Routes() *web.Route {
})
routes.Use(sessioner)
- routes.Use(Recovery())
+ ctx, _ = templates.HTMLRenderer(ctx)
+
+ routes.Use(Recovery(ctx))
// We use r.Route here over r.Use because this prevents requests that are not for avatars having to go through this additional handler
routes.Route("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
routes.Route("/repo-avatars/*", "GET, HEAD", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
- // for health check - doeesn't need to be passed through gzip handler
+ // for health check - doesn't need to be passed through gzip handler
routes.Head("/", func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
})
@@ -137,7 +139,7 @@ func Routes() *web.Route {
// redirect default favicon to the path of the custom favicon with a default as a fallback
routes.Get("/favicon.ico", func(w http.ResponseWriter, req *http.Request) {
- http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "/assets/img/favicon.png"), 301)
+ http.Redirect(w, req, path.Join(setting.StaticURLPrefix, "/assets/img/favicon.png"), http.StatusMovedPermanently)
})
common := []interface{}{}
@@ -150,8 +152,6 @@ func Routes() *web.Route {
common = append(common, h)
}
- mailer.InitMailRender(templates.Mailer())
-
if setting.Service.EnableCaptcha {
// The captcha http.Handler should only fire on /captcha/* so we can just mount this on that url
routes.Route("/captcha/*", "GET,HEAD", append(common, captcha.Captchaer(context.GetImageCaptcha()))...)
@@ -191,11 +191,13 @@ func Routes() *web.Route {
rw.WriteHeader(http.StatusOK)
})
+ routes.Get("/api/healthz", healthcheck.Check)
+
// Removed: toolbox.Toolboxer middleware will provide debug information which seems unnecessary
- common = append(common, context.Contexter())
+ common = append(common, context.Contexter(ctx))
group := buildAuthGroup()
- if err := group.Init(); err != nil {
+ if err := group.Init(ctx); err != nil {
log.Error("Could not initialize '%s' auth method, error: %s", group.Name(), err)
}
@@ -232,8 +234,6 @@ func RegisterRoutes(m *web.Route) {
ignExploreSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true})
reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
-
- bindIgnErr := web.Bind
validation.AddBindingRules()
linkAccountEnabled := func(ctx *context.Context) {
@@ -279,16 +279,53 @@ func RegisterRoutes(m *web.Route) {
}
}
+ federationEnabled := func(ctx *context.Context) {
+ if !setting.Federation.Enabled {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ }
+
+ dlSourceEnabled := func(ctx *context.Context) {
+ if setting.Repository.DisableDownloadSourceArchives {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ }
+
+ sitemapEnabled := func(ctx *context.Context) {
+ if !setting.EnableSitemap {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ }
+
+ packagesEnabled := func(ctx *context.Context) {
+ if !setting.Packages.Enabled {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ }
+
+ feedEnabled := func(ctx *context.Context) {
+ if !setting.EnableFeed {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+ }
+
// FIXME: not all routes need go through same middleware.
// Especially some AJAX requests, we can reduce middleware number to improve performance.
// Routers.
// for health check
m.Get("/", Home)
+ m.Get("/sitemap.xml", sitemapEnabled, ignExploreSignIn, HomeSitemap)
m.Group("/.well-known", func() {
m.Get("/openid-configuration", auth.OIDCWellKnown)
- if setting.Federation.Enabled {
+ m.Group("", func() {
m.Get("/nodeinfo", NodeInfoLinks)
- }
+ m.Get("/webfinger", WebfingerQuery)
+ }, federationEnabled)
m.Get("/change-password", func(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/user/settings/account", http.StatusTemporaryRedirect)
})
@@ -299,7 +336,9 @@ func RegisterRoutes(m *web.Route) {
ctx.Redirect(setting.AppSubURL + "/explore/repos")
})
m.Get("/repos", explore.Repos)
+ m.Get("/repos/sitemap-{idx}.xml", sitemapEnabled, explore.Repos)
m.Get("/users", explore.Users)
+ m.Get("/users/sitemap-{idx}.xml", sitemapEnabled, explore.Users)
m.Get("/organizations", explore.Organizations)
m.Get("/code", explore.Code)
m.Get("/topics/search", explore.TopicSearch)
@@ -315,36 +354,32 @@ func RegisterRoutes(m *web.Route) {
// ***** START: User *****
m.Group("/user", func() {
m.Get("/login", auth.SignIn)
- m.Post("/login", bindIgnErr(forms.SignInForm{}), auth.SignInPost)
+ m.Post("/login", web.Bind(forms.SignInForm{}), auth.SignInPost)
m.Group("", func() {
m.Combo("/login/openid").
Get(auth.SignInOpenID).
- Post(bindIgnErr(forms.SignInOpenIDForm{}), auth.SignInOpenIDPost)
+ Post(web.Bind(forms.SignInOpenIDForm{}), auth.SignInOpenIDPost)
}, openIDSignInEnabled)
m.Group("/openid", func() {
m.Combo("/connect").
Get(auth.ConnectOpenID).
- Post(bindIgnErr(forms.ConnectOpenIDForm{}), auth.ConnectOpenIDPost)
+ Post(web.Bind(forms.ConnectOpenIDForm{}), auth.ConnectOpenIDPost)
m.Group("/register", func() {
m.Combo("").
Get(auth.RegisterOpenID, openIDSignUpEnabled).
- Post(bindIgnErr(forms.SignUpOpenIDForm{}), auth.RegisterOpenIDPost)
+ Post(web.Bind(forms.SignUpOpenIDForm{}), auth.RegisterOpenIDPost)
}, openIDSignUpEnabled)
}, openIDSignInEnabled)
m.Get("/sign_up", auth.SignUp)
- m.Post("/sign_up", bindIgnErr(forms.RegisterForm{}), auth.SignUpPost)
- m.Group("/oauth2", func() {
- m.Get("/{provider}", auth.SignInOAuth)
- m.Get("/{provider}/callback", auth.SignInOAuthCallback)
- })
+ m.Post("/sign_up", web.Bind(forms.RegisterForm{}), auth.SignUpPost)
m.Get("/link_account", linkAccountEnabled, auth.LinkAccount)
- m.Post("/link_account_signin", linkAccountEnabled, bindIgnErr(forms.SignInForm{}), auth.LinkAccountPostSignIn)
- m.Post("/link_account_signup", linkAccountEnabled, bindIgnErr(forms.RegisterForm{}), auth.LinkAccountPostRegister)
+ m.Post("/link_account_signin", linkAccountEnabled, web.Bind(forms.SignInForm{}), auth.LinkAccountPostSignIn)
+ m.Post("/link_account_signup", linkAccountEnabled, web.Bind(forms.RegisterForm{}), auth.LinkAccountPostRegister)
m.Group("/two_factor", func() {
m.Get("", auth.TwoFactor)
- m.Post("", bindIgnErr(forms.TwoFactorAuthForm{}), auth.TwoFactorPost)
+ m.Post("", web.Bind(forms.TwoFactorAuthForm{}), auth.TwoFactorPost)
m.Get("/scratch", auth.TwoFactorScratch)
- m.Post("/scratch", bindIgnErr(forms.TwoFactorScratchAuthForm{}), auth.TwoFactorScratchPost)
+ m.Post("/scratch", web.Bind(forms.TwoFactorScratchAuthForm{}), auth.TwoFactorScratchPost)
})
m.Group("/webauthn", func() {
m.Get("", auth.WebAuthn)
@@ -356,34 +391,34 @@ func RegisterRoutes(m *web.Route) {
m.Any("/user/events", routing.MarkLongPolling, events.Events)
m.Group("/login/oauth", func() {
- m.Get("/authorize", bindIgnErr(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
- m.Post("/grant", bindIgnErr(forms.GrantApplicationForm{}), auth.GrantApplicationOAuth)
+ m.Get("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
+ m.Post("/grant", web.Bind(forms.GrantApplicationForm{}), auth.GrantApplicationOAuth)
// TODO manage redirection
- m.Post("/authorize", bindIgnErr(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
+ m.Post("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
}, ignSignInAndCsrf, reqSignIn)
m.Get("/login/oauth/userinfo", ignSignInAndCsrf, auth.InfoOAuth)
- m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, auth.AccessTokenOAuth)
+ m.Post("/login/oauth/access_token", CorsHandler(), web.Bind(forms.AccessTokenForm{}), ignSignInAndCsrf, auth.AccessTokenOAuth)
m.Get("/login/oauth/keys", ignSignInAndCsrf, auth.OIDCKeys)
- m.Post("/login/oauth/introspect", CorsHandler(), bindIgnErr(forms.IntrospectTokenForm{}), ignSignInAndCsrf, auth.IntrospectOAuth)
+ m.Post("/login/oauth/introspect", CorsHandler(), web.Bind(forms.IntrospectTokenForm{}), ignSignInAndCsrf, auth.IntrospectOAuth)
m.Group("/user/settings", func() {
m.Get("", user_setting.Profile)
- m.Post("", bindIgnErr(forms.UpdateProfileForm{}), user_setting.ProfilePost)
+ m.Post("", web.Bind(forms.UpdateProfileForm{}), user_setting.ProfilePost)
m.Get("/change_password", auth.MustChangePassword)
- m.Post("/change_password", bindIgnErr(forms.MustChangePasswordForm{}), auth.MustChangePasswordPost)
- m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), user_setting.AvatarPost)
+ m.Post("/change_password", web.Bind(forms.MustChangePasswordForm{}), auth.MustChangePasswordPost)
+ m.Post("/avatar", web.Bind(forms.AvatarForm{}), user_setting.AvatarPost)
m.Post("/avatar/delete", user_setting.DeleteAvatar)
m.Group("/account", func() {
- m.Combo("").Get(user_setting.Account).Post(bindIgnErr(forms.ChangePasswordForm{}), user_setting.AccountPost)
- m.Post("/email", bindIgnErr(forms.AddEmailForm{}), user_setting.EmailPost)
+ m.Combo("").Get(user_setting.Account).Post(web.Bind(forms.ChangePasswordForm{}), user_setting.AccountPost)
+ m.Post("/email", web.Bind(forms.AddEmailForm{}), user_setting.EmailPost)
m.Post("/email/delete", user_setting.DeleteEmail)
m.Post("/delete", user_setting.DeleteAccount)
})
m.Group("/appearance", func() {
m.Get("", user_setting.Appearance)
- m.Post("/language", bindIgnErr(forms.UpdateLanguageForm{}), user_setting.UpdateUserLang)
+ m.Post("/language", web.Bind(forms.UpdateLanguageForm{}), user_setting.UpdateUserLang)
m.Post("/hidden_comments", user_setting.UpdateUserHiddenComments)
- m.Post("/theme", bindIgnErr(forms.UpdateThemeForm{}), user_setting.UpdateUIThemePost)
+ m.Post("/theme", web.Bind(forms.UpdateThemeForm{}), user_setting.UpdateUIThemePost)
})
m.Group("/security", func() {
m.Get("", security.Security)
@@ -391,15 +426,15 @@ func RegisterRoutes(m *web.Route) {
m.Post("/regenerate_scratch", security.RegenerateScratchTwoFactor)
m.Post("/disable", security.DisableTwoFactor)
m.Get("/enroll", security.EnrollTwoFactor)
- m.Post("/enroll", bindIgnErr(forms.TwoFactorAuthForm{}), security.EnrollTwoFactorPost)
+ m.Post("/enroll", web.Bind(forms.TwoFactorAuthForm{}), security.EnrollTwoFactorPost)
})
m.Group("/webauthn", func() {
- m.Post("/request_register", bindIgnErr(forms.WebauthnRegistrationForm{}), security.WebAuthnRegister)
+ m.Post("/request_register", web.Bind(forms.WebauthnRegistrationForm{}), security.WebAuthnRegister)
m.Post("/register", security.WebauthnRegisterPost)
- m.Post("/delete", bindIgnErr(forms.WebauthnDeleteForm{}), security.WebauthnDelete)
+ m.Post("/delete", web.Bind(forms.WebauthnDeleteForm{}), security.WebauthnDelete)
})
m.Group("/openid", func() {
- m.Post("", bindIgnErr(forms.AddOpenIDForm{}), security.OpenIDPost)
+ m.Post("", web.Bind(forms.AddOpenIDForm{}), security.OpenIDPost)
m.Post("/delete", security.DeleteOpenID)
m.Post("/toggle_visibility", security.ToggleOpenIDVisibility)
}, openIDSignInEnabled)
@@ -407,24 +442,39 @@ func RegisterRoutes(m *web.Route) {
})
m.Group("/applications/oauth2", func() {
m.Get("/{id}", user_setting.OAuth2ApplicationShow)
- m.Post("/{id}", bindIgnErr(forms.EditOAuth2ApplicationForm{}), user_setting.OAuthApplicationsEdit)
+ m.Post("/{id}", web.Bind(forms.EditOAuth2ApplicationForm{}), user_setting.OAuthApplicationsEdit)
m.Post("/{id}/regenerate_secret", user_setting.OAuthApplicationsRegenerateSecret)
- m.Post("", bindIgnErr(forms.EditOAuth2ApplicationForm{}), user_setting.OAuthApplicationsPost)
- m.Post("/delete", user_setting.DeleteOAuth2Application)
- m.Post("/revoke", user_setting.RevokeOAuth2Grant)
+ m.Post("", web.Bind(forms.EditOAuth2ApplicationForm{}), user_setting.OAuthApplicationsPost)
+ m.Post("/{id}/delete", user_setting.DeleteOAuth2Application)
+ m.Post("/{id}/revoke/{grantId}", user_setting.RevokeOAuth2Grant)
})
m.Combo("/applications").Get(user_setting.Applications).
- Post(bindIgnErr(forms.NewAccessTokenForm{}), user_setting.ApplicationsPost)
+ Post(web.Bind(forms.NewAccessTokenForm{}), user_setting.ApplicationsPost)
m.Post("/applications/delete", user_setting.DeleteApplication)
m.Combo("/keys").Get(user_setting.Keys).
- Post(bindIgnErr(forms.AddKeyForm{}), user_setting.KeysPost)
+ Post(web.Bind(forms.AddKeyForm{}), user_setting.KeysPost)
m.Post("/keys/delete", user_setting.DeleteKey)
+ m.Group("/packages", func() {
+ m.Get("", user_setting.Packages)
+ m.Group("/rules", func() {
+ m.Group("/add", func() {
+ m.Get("", user_setting.PackagesRuleAdd)
+ m.Post("", web.Bind(forms.PackageCleanupRuleForm{}), user_setting.PackagesRuleAddPost)
+ })
+ m.Group("/{id}", func() {
+ m.Get("", user_setting.PackagesRuleEdit)
+ m.Post("", web.Bind(forms.PackageCleanupRuleForm{}), user_setting.PackagesRuleEditPost)
+ m.Get("/preview", user_setting.PackagesRulePreview)
+ })
+ })
+ }, packagesEnabled)
m.Get("/organization", user_setting.Organization)
m.Get("/repos", user_setting.Repos)
m.Post("/repos/unadopted", user_setting.AdoptOrDeleteRepository)
}, reqSignIn, func(ctx *context.Context) {
ctx.Data["PageIsUserSettings"] = true
ctx.Data["AllThemes"] = setting.UI.Themes
+ ctx.Data["EnablePackages"] = setting.Packages.Enabled
})
m.Group("/user", func() {
@@ -441,6 +491,10 @@ func RegisterRoutes(m *web.Route) {
m.Get("/task/{task}", reqSignIn, user.TaskStatus)
m.Get("/stopwatches", reqSignIn, user.GetStopwatches)
m.Get("/search", ignExploreSignIn, user.Search)
+ m.Group("/oauth2", func() {
+ m.Get("/{provider}", auth.SignInOAuth)
+ m.Get("/{provider}/callback", auth.SignInOAuthCallback)
+ })
})
// ***** END: User *****
@@ -451,9 +505,14 @@ func RegisterRoutes(m *web.Route) {
// ***** START: Admin *****
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.Post("", adminReq, web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost)
+
+ 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)
@@ -471,10 +530,10 @@ func RegisterRoutes(m *web.Route) {
m.Group("/users", func() {
m.Get("", admin.Users)
- m.Combo("/new").Get(admin.NewUser).Post(bindIgnErr(forms.AdminCreateUserForm{}), admin.NewUserPost)
- m.Combo("/{userid}").Get(admin.EditUser).Post(bindIgnErr(forms.AdminEditUserForm{}), admin.EditUserPost)
+ m.Combo("/new").Get(admin.NewUser).Post(web.Bind(forms.AdminCreateUserForm{}), admin.NewUserPost)
+ m.Combo("/{userid}").Get(admin.EditUser).Post(web.Bind(forms.AdminEditUserForm{}), admin.EditUserPost)
m.Post("/{userid}/delete", admin.DeleteUser)
- m.Post("/{userid}/avatar", bindIgnErr(forms.AvatarForm{}), admin.AvatarPost)
+ m.Post("/{userid}/avatar", web.Bind(forms.AvatarForm{}), admin.AvatarPost)
m.Post("/{userid}/avatar/delete", admin.DeleteAvatar)
})
@@ -493,12 +552,10 @@ func RegisterRoutes(m *web.Route) {
m.Post("/delete", admin.DeleteRepo)
})
- if setting.Packages.Enabled {
- m.Group("/packages", func() {
- m.Get("", admin.Packages)
- m.Post("/delete", admin.DeletePackageVersion)
- })
- }
+ m.Group("/packages", func() {
+ m.Get("", admin.Packages)
+ m.Post("/delete", admin.DeletePackageVersion)
+ }, packagesEnabled)
m.Group("/hooks", func() {
m.Get("", admin.DefaultOrSystemWebhooks)
@@ -507,39 +564,39 @@ func RegisterRoutes(m *web.Route) {
m.Get("", repo.WebHooksEdit)
m.Post("/replay/{uuid}", repo.ReplayWebhook)
})
- m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
- m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
- m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
- m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
- m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
- m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
- m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
- m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
- m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
- m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost)
- m.Post("/packagist/{id}", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost)
+ m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost)
+ m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
+ m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
+ m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
+ m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
+ m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
+ m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
+ m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
+ m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
+ m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost)
+ m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost)
}, webhooksEnabled)
m.Group("/{configType:default-hooks|system-hooks}", func() {
m.Get("/{type}/new", repo.WebhooksNew)
- m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
- m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
- m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
- m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
- m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
- m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
- m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
- m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
- m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
- m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost)
- m.Post("/packagist/new", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost)
+ m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
+ m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
+ m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
+ m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
+ m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
+ m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
+ m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
+ m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
+ m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
+ m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost)
+ m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost)
})
m.Group("/auths", func() {
m.Get("", admin.Authentications)
- m.Combo("/new").Get(admin.NewAuthSource).Post(bindIgnErr(forms.AuthenticationForm{}), admin.NewAuthSourcePost)
+ m.Combo("/new").Get(admin.NewAuthSource).Post(web.Bind(forms.AuthenticationForm{}), admin.NewAuthSourcePost)
m.Combo("/{authid}").Get(admin.EditAuthSource).
- Post(bindIgnErr(forms.AuthenticationForm{}), admin.EditAuthSourcePost)
+ Post(web.Bind(forms.AuthenticationForm{}), admin.EditAuthSourcePost)
m.Post("/{authid}/delete", admin.DeleteAuthSource)
})
@@ -548,37 +605,56 @@ func RegisterRoutes(m *web.Route) {
m.Post("/delete", admin.DeleteNotices)
m.Post("/empty", admin.EmptyNotices)
})
+
+ m.Group("/applications", func() {
+ m.Get("", admin.Applications)
+ m.Post("/oauth2", web.Bind(forms.EditOAuth2ApplicationForm{}), admin.ApplicationsPost)
+ m.Group("/oauth2/{id}", func() {
+ m.Combo("").Get(admin.EditApplication).Post(web.Bind(forms.EditOAuth2ApplicationForm{}), admin.EditApplicationPost)
+ m.Post("/regenerate_secret", admin.ApplicationsRegenerateSecret)
+ m.Post("/delete", admin.DeleteApplication)
+ })
+ }, func(ctx *context.Context) {
+ if !setting.OAuth2.Enable {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ })
+ }, func(ctx *context.Context) {
+ ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
+ ctx.Data["EnablePackages"] = setting.Packages.Enabled
}, adminReq)
// ***** END: Admin *****
m.Group("", func() {
m.Get("/favicon.ico", func(ctx *context.Context) {
- ctx.ServeFile(path.Join(setting.StaticRootPath, "public/img/favicon.png"))
+ ctx.SetServeHeaders(&context.ServeHeaderOptions{
+ Filename: "favicon.png",
+ })
+ http.ServeFile(ctx.Resp, ctx.Req, path.Join(setting.StaticRootPath, "public/img/favicon.png"))
})
m.Group("/{username}", func() {
- m.Get(".png", func(ctx *context.Context) { ctx.Error(http.StatusNotFound) })
+ m.Get(".png", user.AvatarByUserName)
m.Get(".keys", user.ShowSSHKeys)
m.Get(".gpg", user.ShowGPGKeys)
- m.Get(".rss", feed.ShowUserFeedRSS)
- m.Get(".atom", feed.ShowUserFeedAtom)
+ m.Get(".rss", feedEnabled, feed.ShowUserFeedRSS)
+ m.Get(".atom", feedEnabled, feed.ShowUserFeedAtom)
m.Get("", user.Profile)
+ }, func(ctx *context.Context) {
+ ctx.Data["EnableFeed"] = setting.EnableFeed
}, context_service.UserAssignmentWeb())
m.Get("/attachments/{uuid}", repo.GetAttachment)
}, ignSignIn)
m.Post("/{username}", reqSignIn, context_service.UserAssignmentWeb(), user.Action)
- if !setting.IsProd {
- m.Get("/template/*", dev.TemplatePreview)
- }
-
reqRepoAdmin := context.RequireRepoAdmin()
reqRepoCodeWriter := context.RequireRepoWriter(unit.TypeCode)
+ canEnableEditor := context.CanEnableEditor()
reqRepoCodeReader := context.RequireRepoReader(unit.TypeCode)
reqRepoReleaseWriter := context.RequireRepoWriter(unit.TypeReleases)
reqRepoReleaseReader := context.RequireRepoReader(unit.TypeReleases)
reqRepoWikiWriter := context.RequireRepoWriter(unit.TypeWiki)
- reqRepoIssueWriter := context.RequireRepoWriter(unit.TypeIssues)
reqRepoIssueReader := context.RequireRepoReader(unit.TypeIssues)
reqRepoPullsReader := context.RequireRepoReader(unit.TypePullRequests)
reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(unit.TypeIssues, unit.TypePullRequests)
@@ -595,10 +671,21 @@ func RegisterRoutes(m *web.Route) {
}
// ***** START: Organization *****
+ m.Group("/org", func() {
+ m.Group("/{org}", func() {
+ m.Get("/members", org.Members)
+ }, context.OrgAssignment())
+ }, ignSignIn)
+
m.Group("/org", func() {
m.Group("", func() {
m.Get("/create", org.Create)
- m.Post("/create", bindIgnErr(forms.CreateOrgForm{}), org.CreatePost)
+ m.Post("/create", web.Bind(forms.CreateOrgForm{}), org.CreatePost)
+ })
+
+ m.Group("/invite/{token}", func() {
+ m.Get("", org.TeamInvite)
+ m.Post("", org.TeamInvitePost)
})
m.Group("/{org}", func() {
@@ -610,7 +697,6 @@ func RegisterRoutes(m *web.Route) {
m.Get("/pulls/{team}", user.Pulls)
m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones)
- m.Get("/members", org.Members)
m.Post("/members/action/{action}", org.MembersAction)
m.Get("/teams", org.Teams)
}, context.OrgAssignment(true, false, true))
@@ -624,57 +710,95 @@ func RegisterRoutes(m *web.Route) {
m.Group("/{org}", func() {
m.Get("/teams/new", org.NewTeam)
- m.Post("/teams/new", bindIgnErr(forms.CreateTeamForm{}), org.NewTeamPost)
+ m.Post("/teams/new", web.Bind(forms.CreateTeamForm{}), org.NewTeamPost)
m.Get("/teams/-/search", org.SearchTeam)
m.Get("/teams/{team}/edit", org.EditTeam)
- m.Post("/teams/{team}/edit", bindIgnErr(forms.CreateTeamForm{}), org.EditTeamPost)
+ m.Post("/teams/{team}/edit", web.Bind(forms.CreateTeamForm{}), org.EditTeamPost)
m.Post("/teams/{team}/delete", org.DeleteTeam)
m.Group("/settings", func() {
m.Combo("").Get(org.Settings).
- Post(bindIgnErr(forms.UpdateOrgSettingForm{}), org.SettingsPost)
- m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), org.SettingsAvatar)
+ Post(web.Bind(forms.UpdateOrgSettingForm{}), org.SettingsPost)
+ m.Post("/avatar", web.Bind(forms.AvatarForm{}), org.SettingsAvatar)
m.Post("/avatar/delete", org.SettingsDeleteAvatar)
+ m.Group("/applications", func() {
+ m.Get("", org.Applications)
+ m.Post("/oauth2", web.Bind(forms.EditOAuth2ApplicationForm{}), org.OAuthApplicationsPost)
+ m.Group("/oauth2/{id}", func() {
+ m.Combo("").Get(org.OAuth2ApplicationShow).Post(web.Bind(forms.EditOAuth2ApplicationForm{}), org.OAuth2ApplicationEdit)
+ m.Post("/regenerate_secret", org.OAuthApplicationsRegenerateSecret)
+ m.Post("/delete", org.DeleteOAuth2Application)
+ })
+ }, func(ctx *context.Context) {
+ if !setting.OAuth2.Enable {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ })
m.Group("/hooks", func() {
m.Get("", org.Webhooks)
m.Post("/delete", org.DeleteWebhook)
m.Get("/{type}/new", repo.WebhooksNew)
- m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
- m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
- m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
- m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
- m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
- m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
- m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
- m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
- m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
- m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost)
+ m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
+ m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
+ m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
+ m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
+ m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
+ m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
+ m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
+ m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
+ m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
+ m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost)
m.Group("/{id}", func() {
m.Get("", repo.WebHooksEdit)
m.Post("/replay/{uuid}", repo.ReplayWebhook)
})
- m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
- m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
- m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
- m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
- m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
- m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
- m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
- m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
- m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
- m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost)
+ m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost)
+ m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
+ m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
+ m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
+ m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
+ m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
+ m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
+ m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
+ m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
+ m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost)
}, webhooksEnabled)
m.Group("/labels", func() {
m.Get("", org.RetrieveLabels, org.Labels)
- m.Post("/new", bindIgnErr(forms.CreateLabelForm{}), org.NewLabel)
- m.Post("/edit", bindIgnErr(forms.CreateLabelForm{}), org.UpdateLabel)
+ m.Post("/new", web.Bind(forms.CreateLabelForm{}), org.NewLabel)
+ m.Post("/edit", web.Bind(forms.CreateLabelForm{}), org.UpdateLabel)
m.Post("/delete", org.DeleteLabel)
- m.Post("/initialize", bindIgnErr(forms.InitializeLabelsForm{}), org.InitializeLabels)
+ m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), org.InitializeLabels)
+ })
+
+ m.Group("/secrets", func() {
+ m.Get("", org.Secrets)
+ m.Post("", web.Bind(forms.AddSecretForm{}), org.SecretsPost)
+ m.Post("/delete", org.SecretsDelete)
})
m.Route("/delete", "GET,POST", org.SettingsDelete)
+
+ m.Group("/packages", func() {
+ m.Get("", org.Packages)
+ m.Group("/rules", func() {
+ m.Group("/add", func() {
+ m.Get("", org.PackagesRuleAdd)
+ m.Post("", web.Bind(forms.PackageCleanupRuleForm{}), org.PackagesRuleAddPost)
+ })
+ m.Group("/{id}", func() {
+ m.Get("", org.PackagesRuleEdit)
+ m.Post("", web.Bind(forms.PackageCleanupRuleForm{}), org.PackagesRuleEditPost)
+ m.Get("/preview", org.PackagesRulePreview)
+ })
+ })
+ }, packagesEnabled)
+ }, func(ctx *context.Context) {
+ ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
+ ctx.Data["EnablePackages"] = setting.Packages.Enabled
})
}, context.OrgAssignment(true, true))
}, reqSignIn)
@@ -683,12 +807,12 @@ func RegisterRoutes(m *web.Route) {
// ***** START: Repository *****
m.Group("/repo", func() {
m.Get("/create", repo.Create)
- m.Post("/create", bindIgnErr(forms.CreateRepoForm{}), repo.CreatePost)
+ m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost)
m.Get("/migrate", repo.Migrate)
- m.Post("/migrate", bindIgnErr(forms.MigrateRepoForm{}), repo.MigratePost)
+ m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost)
m.Group("/fork", func() {
m.Combo("/{repoid}").Get(repo.Fork).
- Post(bindIgnErr(forms.CreateRepoForm{}), repo.ForkPost)
+ Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost)
}, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader)
m.Get("/search", repo.SearchRepo)
}, reqSignIn)
@@ -705,12 +829,13 @@ func RegisterRoutes(m *web.Route) {
m.Get("/files/{fileid}", user.DownloadPackageFile)
m.Group("/settings", func() {
m.Get("", user.PackageSettings)
- m.Post("", bindIgnErr(forms.PackageSettingForm{}), user.PackageSettingsPost)
+ m.Post("", web.Bind(forms.PackageSettingForm{}), user.PackageSettingsPost)
}, reqPackageAccess(perm.AccessModeWrite))
})
})
- }, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
+ }, ignSignIn, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
}
+ m.Get("/code", user.CodeSearch)
}, context_service.UserAssignmentWeb())
// ***** Release Attachment Download without Signin
@@ -718,9 +843,11 @@ func RegisterRoutes(m *web.Route) {
m.Group("/{username}/{reponame}", func() {
m.Group("/settings", func() {
- m.Combo("").Get(repo.Settings).
- Post(bindIgnErr(forms.RepoSettingForm{}), repo.SettingsPost)
- m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), repo.SettingsAvatar)
+ m.Group("", func() {
+ m.Combo("").Get(repo.Settings).
+ Post(web.Bind(forms.RepoSettingForm{}), repo.SettingsPost)
+ }, repo.SettingsCtxData)
+ m.Post("/avatar", web.Bind(forms.AvatarForm{}), repo.SettingsAvatar)
m.Post("/avatar/delete", repo.SettingsDeleteAvatar)
m.Group("/collaboration", func() {
@@ -734,18 +861,24 @@ func RegisterRoutes(m *web.Route) {
})
m.Group("/branches", func() {
- m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost)
- m.Combo("/*").Get(repo.SettingsProtectedBranch).
- Post(bindIgnErr(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost)
+ m.Post("/", repo.SetDefaultBranchPost)
+ }, repo.MustBeNotEmpty)
+
+ m.Group("/branches", func() {
+ m.Get("/", repo.ProtectedBranchRules)
+ m.Combo("/edit").Get(repo.SettingsProtectedBranch).
+ Post(web.Bind(forms.ProtectBranchForm{}), context.RepoMustNotBeArchived(), repo.SettingsProtectedBranchPost)
+ m.Post("/{id}/delete", repo.DeleteProtectedBranchRulePost)
}, repo.MustBeNotEmpty)
- m.Post("/rename_branch", bindIgnErr(forms.RenameBranchForm{}), context.RepoMustNotBeArchived(), repo.RenameBranchPost)
+
+ m.Post("/rename_branch", web.Bind(forms.RenameBranchForm{}), context.RepoMustNotBeArchived(), repo.RenameBranchPost)
m.Group("/tags", func() {
m.Get("", repo.Tags)
- m.Post("", bindIgnErr(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.NewProtectedTagPost)
+ m.Post("", web.Bind(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.NewProtectedTagPost)
m.Post("/delete", context.RepoMustNotBeArchived(), repo.DeleteProtectedTagPost)
m.Get("/{id}", repo.EditProtectedTag)
- m.Post("/{id}", bindIgnErr(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.EditProtectedTagPost)
+ m.Post("/{id}", web.Bind(forms.ProtectTagForm{}), context.RepoMustNotBeArchived(), repo.EditProtectedTagPost)
})
m.Group("/hooks/git", func() {
@@ -758,39 +891,43 @@ func RegisterRoutes(m *web.Route) {
m.Get("", repo.Webhooks)
m.Post("/delete", repo.DeleteWebhook)
m.Get("/{type}/new", repo.WebhooksNew)
- m.Post("/gitea/new", bindIgnErr(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
- m.Post("/gogs/new", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
- m.Post("/slack/new", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
- m.Post("/discord/new", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
- m.Post("/dingtalk/new", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
- m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
- m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
- m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
- m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
- m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost)
- m.Post("/packagist/new", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost)
+ m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost)
+ m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost)
+ m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost)
+ m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost)
+ m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost)
+ m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost)
+ m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost)
+ m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost)
+ m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost)
+ m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost)
+ m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost)
m.Group("/{id}", func() {
m.Get("", repo.WebHooksEdit)
m.Post("/test", repo.TestWebhook)
m.Post("/replay/{uuid}", repo.ReplayWebhook)
})
- m.Post("/gitea/{id}", bindIgnErr(forms.NewWebhookForm{}), repo.WebHooksEditPost)
- m.Post("/gogs/{id}", bindIgnErr(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
- m.Post("/slack/{id}", bindIgnErr(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
- m.Post("/discord/{id}", bindIgnErr(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
- m.Post("/dingtalk/{id}", bindIgnErr(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
- m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
- m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
- m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
- m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
- m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost)
- m.Post("/packagist/{id}", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost)
+ m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost)
+ m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost)
+ m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost)
+ m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost)
+ m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost)
+ m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost)
+ m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost)
+ m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost)
+ m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost)
+ m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost)
+ m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost)
}, webhooksEnabled)
m.Group("/keys", func() {
m.Combo("").Get(repo.DeployKeys).
- Post(bindIgnErr(forms.AddKeyForm{}), repo.DeployKeysPost)
+ Post(web.Bind(forms.AddKeyForm{}), repo.DeployKeysPost)
m.Post("/delete", repo.DeleteDeployKey)
+ m.Group("/secrets", func() {
+ m.Post("", web.Bind(forms.AddSecretForm{}), repo.SecretsPost)
+ m.Post("/delete", repo.DeleteSecret)
+ })
})
m.Group("/lfs", func() {
@@ -819,10 +956,21 @@ func RegisterRoutes(m *web.Route) {
m.Group("/milestone", func() {
m.Get("/{id}", repo.MilestoneIssuesAndPulls)
}, reqRepoIssuesOrPullsReader, context.RepoRef())
+ m.Get("/find/*", repo.FindFiles)
+ m.Group("/tree-list", func() {
+ m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.TreeList)
+ m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.TreeList)
+ m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.TreeList)
+ })
m.Get("/compare", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists, ignSignIn, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff)
m.Combo("/compare/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists).
Get(ignSignIn, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
- Post(reqSignIn, context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, bindIgnErr(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost)
+ Post(reqSignIn, context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost)
+ m.Group("/{type:issues|pulls}", func() {
+ m.Group("/{index}", func() {
+ m.Get("/info", repo.GetIssueInfo)
+ })
+ })
}, context.RepoAssignment, context.UnitTypes())
// Grouping for those endpoints that do require authentication
@@ -830,7 +978,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("/issues", func() {
m.Group("/new", func() {
m.Combo("").Get(context.RepoRef(), repo.NewIssue).
- Post(bindIgnErr(forms.CreateIssueForm{}), repo.NewIssuePost)
+ Post(web.Bind(forms.CreateIssueForm{}), repo.NewIssuePost)
m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
})
m.Get("/search", repo.ListIssues)
@@ -839,28 +987,28 @@ func RegisterRoutes(m *web.Route) {
// So they can apply their own enable/disable logic on routers.
m.Group("/{type:issues|pulls}", func() {
m.Group("/{index}", func() {
- m.Get("/info", repo.GetIssueInfo)
m.Post("/title", repo.UpdateIssueTitle)
m.Post("/content", repo.UpdateIssueContent)
- m.Post("/deadline", bindIgnErr(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline)
+ m.Post("/deadline", web.Bind(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline)
m.Post("/watch", repo.IssueWatch)
m.Post("/ref", repo.UpdateIssueRef)
+ m.Post("/viewed-files", repo.UpdateViewedFiles)
m.Group("/dependency", func() {
m.Post("/add", repo.AddDependency)
m.Post("/delete", repo.RemoveDependency)
})
- m.Combo("/comments").Post(repo.MustAllowUserComment, bindIgnErr(forms.CreateCommentForm{}), repo.NewComment)
+ m.Combo("/comments").Post(repo.MustAllowUserComment, web.Bind(forms.CreateCommentForm{}), repo.NewComment)
m.Group("/times", func() {
- m.Post("/add", bindIgnErr(forms.AddTimeManuallyForm{}), repo.AddTimeManually)
+ m.Post("/add", web.Bind(forms.AddTimeManuallyForm{}), repo.AddTimeManually)
m.Post("/{timeid}/delete", repo.DeleteTime)
m.Group("/stopwatch", func() {
m.Post("/toggle", repo.IssueStopwatch)
m.Post("/cancel", repo.CancelStopwatch)
})
})
- m.Post("/reactions/{action}", bindIgnErr(forms.ReactionForm{}), repo.ChangeIssueReaction)
- m.Post("/lock", reqRepoIssueWriter, bindIgnErr(forms.IssueLockForm{}), repo.LockIssue)
- m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue)
+ m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeIssueReaction)
+ m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue)
+ m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
m.Post("/delete", reqRepoAdmin, repo.DeleteIssue)
}, context.RepoMustNotBeArchived())
m.Group("/{index}", func() {
@@ -873,10 +1021,10 @@ func RegisterRoutes(m *web.Route) {
m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone)
- m.Post("/projects", reqRepoIssuesOrPullsWriter, repo.UpdateIssueProject)
+ m.Post("/projects", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProject)
m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee)
m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest)
- m.Post("/dismiss_review", reqRepoAdmin, bindIgnErr(forms.DismissReviewForm{}), repo.DismissReview)
+ m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview)
m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus)
m.Post("/resolve_conversation", reqRepoIssuesOrPullsReader, repo.UpdateResolveConversation)
m.Post("/attachments", repo.UploadIssueAttachment)
@@ -885,23 +1033,23 @@ func RegisterRoutes(m *web.Route) {
m.Group("/comments/{id}", func() {
m.Post("", repo.UpdateCommentContent)
m.Post("/delete", repo.DeleteComment)
- m.Post("/reactions/{action}", bindIgnErr(forms.ReactionForm{}), repo.ChangeCommentReaction)
+ m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeCommentReaction)
}, context.RepoMustNotBeArchived())
m.Group("/comments/{id}", func() {
m.Get("/attachments", repo.GetCommentAttachments)
})
- m.Post("/markdown", bindIgnErr(structs.MarkdownOption{}), misc.Markdown)
+ m.Post("/markdown", web.Bind(structs.MarkdownOption{}), misc.Markdown)
m.Group("/labels", func() {
- m.Post("/new", bindIgnErr(forms.CreateLabelForm{}), repo.NewLabel)
- m.Post("/edit", bindIgnErr(forms.CreateLabelForm{}), repo.UpdateLabel)
+ m.Post("/new", web.Bind(forms.CreateLabelForm{}), repo.NewLabel)
+ m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel)
m.Post("/delete", repo.DeleteLabel)
- m.Post("/initialize", bindIgnErr(forms.InitializeLabelsForm{}), repo.InitializeLabels)
+ m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), repo.InitializeLabels)
}, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
m.Group("/milestones", func() {
m.Combo("/new").Get(repo.NewMilestone).
- Post(bindIgnErr(forms.CreateMilestoneForm{}), repo.NewMilestonePost)
+ Post(web.Bind(forms.CreateMilestoneForm{}), repo.NewMilestonePost)
m.Get("/{id}/edit", repo.EditMilestone)
- m.Post("/{id}/edit", bindIgnErr(forms.CreateMilestoneForm{}), repo.EditMilestonePost)
+ m.Post("/{id}/edit", web.Bind(forms.CreateMilestoneForm{}), repo.EditMilestonePost)
m.Post("/{id}/{action}", repo.ChangeMilestoneStatus)
m.Post("/delete", repo.DeleteMilestone)
}, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
@@ -912,32 +1060,32 @@ func RegisterRoutes(m *web.Route) {
m.Group("", func() {
m.Group("", func() {
m.Combo("/_edit/*").Get(repo.EditFile).
- Post(bindIgnErr(forms.EditRepoFileForm{}), repo.EditFilePost)
+ Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
m.Combo("/_new/*").Get(repo.NewFile).
- Post(bindIgnErr(forms.EditRepoFileForm{}), repo.NewFilePost)
- m.Post("/_preview/*", bindIgnErr(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
+ Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost)
+ m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
m.Combo("/_delete/*").Get(repo.DeleteFile).
- Post(bindIgnErr(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
+ Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
m.Combo("/_upload/*", repo.MustBeAbleToUpload).
Get(repo.UploadFile).
- Post(bindIgnErr(forms.UploadRepoFileForm{}), repo.UploadFilePost)
+ Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost)
m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch).
- Post(bindIgnErr(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
+ Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
m.Combo("/_cherrypick/{sha:([a-f0-9]{7,40})}/*").Get(repo.CherryPick).
- Post(bindIgnErr(forms.CherryPickForm{}), repo.CherryPickPost)
- }, context.RepoRefByType(context.RepoRefBranch), repo.MustBeEditable)
+ Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
+ }, repo.MustBeEditable)
m.Group("", func() {
m.Post("/upload-file", repo.UploadFileToServer)
- m.Post("/upload-remove", bindIgnErr(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
- }, context.RepoRef(), repo.MustBeEditable, repo.MustBeAbleToUpload)
- }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
+ m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
+ }, repo.MustBeEditable, repo.MustBeAbleToUpload)
+ }, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived(), repo.MustBeNotEmpty)
m.Group("/branches", func() {
m.Group("/_new", func() {
m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch)
m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch)
m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch)
- }, bindIgnErr(forms.NewBranchForm{}))
+ }, web.Bind(forms.NewBranchForm{}))
m.Post("/delete", repo.DeleteBranchPost)
m.Post("/restore", repo.RestoreBranchPost)
}, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty)
@@ -945,17 +1093,26 @@ func RegisterRoutes(m *web.Route) {
// Releases
m.Group("/{username}/{reponame}", func() {
- m.Get("/tags", repo.TagsList, repo.MustBeNotEmpty,
- reqRepoCodeReader, context.RepoRefByType(context.RepoRefTag))
+ m.Group("/tags", func() {
+ m.Get("", repo.TagsList)
+ m.Get(".rss", feedEnabled, repo.TagsListFeedRSS)
+ m.Get(".atom", feedEnabled, repo.TagsListFeedAtom)
+ }, func(ctx *context.Context) {
+ ctx.Data["EnableFeed"] = setting.EnableFeed
+ }, repo.MustBeNotEmpty, reqRepoCodeReader, context.RepoRefByType(context.RepoRefTag, true))
m.Group("/releases", func() {
m.Get("/", repo.Releases)
m.Get("/tag/*", repo.SingleRelease)
m.Get("/latest", repo.LatestRelease)
+ m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS)
+ m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom)
+ }, func(ctx *context.Context) {
+ ctx.Data["EnableFeed"] = setting.EnableFeed
}, repo.MustBeNotEmpty, reqRepoReleaseReader, context.RepoRefByType(context.RepoRefTag, true))
m.Get("/releases/attachments/{uuid}", repo.GetAttachment, repo.MustBeNotEmpty, reqRepoReleaseReader)
m.Group("/releases", func() {
m.Get("/new", repo.NewRelease)
- m.Post("/new", bindIgnErr(forms.NewReleaseForm{}), repo.NewReleasePost)
+ m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)
m.Post("/delete", repo.DeleteRelease)
m.Post("/attachments", repo.UploadReleaseAttachment)
m.Post("/attachments/remove", repo.DeleteAttachment)
@@ -964,7 +1121,7 @@ func RegisterRoutes(m *web.Route) {
repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, context.RepoRef())
m.Group("/releases", func() {
m.Get("/edit/*", repo.EditRelease)
- m.Post("/edit/*", bindIgnErr(forms.EditReleaseForm{}), repo.EditReleasePost)
+ m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost)
}, reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, func(ctx *context.Context) {
var err error
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
@@ -978,6 +1135,7 @@ func RegisterRoutes(m *web.Route) {
return
}
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
+ ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache())
})
}, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader)
@@ -1012,17 +1170,17 @@ func RegisterRoutes(m *web.Route) {
m.Get("/{id}", repo.ViewProject)
m.Group("", func() {
m.Get("/new", repo.NewProject)
- m.Post("/new", bindIgnErr(forms.CreateProjectForm{}), repo.NewProjectPost)
+ m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
m.Group("/{id}", func() {
- m.Post("", bindIgnErr(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
+ m.Post("", web.Bind(forms.EditProjectBoardForm{}), repo.AddBoardToProjectPost)
m.Post("/delete", repo.DeleteProject)
m.Get("/edit", repo.EditProject)
- m.Post("/edit", bindIgnErr(forms.CreateProjectForm{}), repo.EditProjectPost)
+ m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost)
m.Post("/{action:open|close}", repo.ChangeProjectStatus)
m.Group("/{boardID}", func() {
- m.Put("", bindIgnErr(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
+ m.Put("", web.Bind(forms.EditProjectBoardForm{}), repo.EditProjectBoard)
m.Delete("", repo.DeleteProjectBoard)
m.Post("/default", repo.SetDefaultProjectBoard)
@@ -1038,14 +1196,14 @@ func RegisterRoutes(m *web.Route) {
Post(context.RepoMustNotBeArchived(),
reqSignIn,
reqRepoWikiWriter,
- bindIgnErr(forms.NewWikiForm{}),
+ web.Bind(forms.NewWikiForm{}),
repo.WikiPost)
m.Combo("/*").
Get(repo.Wiki).
Post(context.RepoMustNotBeArchived(),
reqSignIn,
reqRepoWikiWriter,
- bindIgnErr(forms.NewWikiForm{}),
+ web.Bind(forms.NewWikiForm{}),
repo.WikiPost)
m.Get("/commit/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
m.Get("/commit/{sha:[a-f0-9]{7,40}}.{ext:patch|diff}", repo.RawDiff)
@@ -1071,7 +1229,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("/archive", func() {
m.Get("/*", repo.Download)
m.Post("/*", repo.InitiateDownload)
- }, repo.MustBeNotEmpty, reqRepoCodeReader)
+ }, repo.MustBeNotEmpty, dlSourceEnabled, reqRepoCodeReader)
m.Group("/branches", func() {
m.Get("", repo.Branches)
@@ -1096,22 +1254,24 @@ func RegisterRoutes(m *web.Route) {
}
repo.MustBeNotEmpty(ctx)
- return
+ return cancel
})
m.Group("/pulls/{index}", func() {
m.Get(".diff", repo.DownloadPullDiff)
m.Get(".patch", repo.DownloadPullPatch)
m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
- m.Post("/merge", context.RepoMustNotBeArchived(), bindIgnErr(forms.MergePullRequestForm{}), repo.MergePullRequest)
+ m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
+ m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
m.Post("/update", repo.UpdatePullRequest)
+ m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits)
m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)
m.Group("/files", func() {
m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles)
m.Group("/reviews", func() {
m.Get("/new_comment", repo.RenderNewCodeCommentForm)
- m.Post("/comments", bindIgnErr(forms.CodeCommentForm{}), repo.CreateCodeComment)
- m.Post("/submit", bindIgnErr(forms.SubmitReviewForm{}), repo.SubmitReview)
+ m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.CreateCodeComment)
+ m.Post("/submit", web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview)
}, context.RepoMustNotBeArchived())
})
}, repo.MustAllowPulls)
@@ -1134,6 +1294,13 @@ func RegisterRoutes(m *web.Route) {
m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload)
}, repo.MustBeNotEmpty, reqRepoCodeReader)
+ m.Group("/render", func() {
+ m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RenderFile)
+ m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RenderFile)
+ m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RenderFile)
+ m.Get("/blob/{sha}", context.RepoRefByType(context.RepoRefBlob), repo.RenderFile)
+ }, repo.MustBeNotEmpty, reqRepoCodeReader)
+
m.Group("/commits", func() {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits)
@@ -1219,6 +1386,8 @@ func RegisterRoutes(m *web.Route) {
m.Group("/notifications", func() {
m.Get("", user.Notifications)
+ m.Get("/subscriptions", user.NotificationSubscriptions)
+ m.Get("/watching", user.NotificationWatching)
m.Post("/status", user.NotificationStatusPost)
m.Post("/purge", user.NotificationPurgePost)
m.Get("/new", user.NewAvailable)
diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go
new file mode 100644
index 0000000000000..935832aa1f205
--- /dev/null
+++ b/routers/web/webfinger.go
@@ -0,0 +1,117 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package web
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-webfinger-14#section-4.4
+
+type webfingerJRD struct {
+ Subject string `json:"subject,omitempty"`
+ Aliases []string `json:"aliases,omitempty"`
+ Properties map[string]interface{} `json:"properties,omitempty"`
+ Links []*webfingerLink `json:"links,omitempty"`
+}
+
+type webfingerLink struct {
+ Rel string `json:"rel,omitempty"`
+ Type string `json:"type,omitempty"`
+ Href string `json:"href,omitempty"`
+ Titles map[string]string `json:"titles,omitempty"`
+ Properties map[string]interface{} `json:"properties,omitempty"`
+}
+
+// WebfingerQuery returns information about a resource
+// https://datatracker.ietf.org/doc/html/rfc7565
+func WebfingerQuery(ctx *context.Context) {
+ appURL, _ := url.Parse(setting.AppURL)
+
+ resource, err := url.Parse(ctx.FormTrim("resource"))
+ if err != nil {
+ ctx.Error(http.StatusBadRequest)
+ return
+ }
+
+ var u *user_model.User
+
+ switch resource.Scheme {
+ case "acct":
+ // allow only the current host
+ parts := strings.SplitN(resource.Opaque, "@", 2)
+ if len(parts) != 2 {
+ ctx.Error(http.StatusBadRequest)
+ return
+ }
+ if parts[1] != appURL.Host {
+ ctx.Error(http.StatusBadRequest)
+ return
+ }
+
+ u, err = user_model.GetUserByName(ctx, parts[0])
+ case "mailto":
+ u, err = user_model.GetUserByEmailContext(ctx, resource.Opaque)
+ if u != nil && u.KeepEmailPrivate {
+ err = user_model.ErrUserNotExist{}
+ }
+ default:
+ ctx.Error(http.StatusBadRequest)
+ return
+ }
+ if err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ ctx.Error(http.StatusNotFound)
+ } else {
+ log.Error("Error getting user: %s Error: %v", resource.Opaque, err)
+ ctx.Error(http.StatusInternalServerError)
+ }
+ return
+ }
+
+ if !user_model.IsUserVisibleToViewer(ctx, u, ctx.Doer) {
+ ctx.Error(http.StatusNotFound)
+ return
+ }
+
+ aliases := []string{
+ u.HTMLURL(),
+ appURL.String() + "api/v1/activitypub/user/" + url.PathEscape(u.Name),
+ }
+ if !u.KeepEmailPrivate {
+ aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email))
+ }
+
+ links := []*webfingerLink{
+ {
+ Rel: "http://webfinger.net/rel/profile-page",
+ Type: "text/html",
+ Href: u.HTMLURL(),
+ },
+ {
+ Rel: "http://webfinger.net/rel/avatar",
+ Href: u.AvatarLink(),
+ },
+ {
+ Rel: "self",
+ Type: "application/activity+json",
+ Href: appURL.String() + "api/v1/activitypub/user/" + url.PathEscape(u.Name),
+ },
+ }
+
+ ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*")
+ ctx.JSON(http.StatusOK, &webfingerJRD{
+ Subject: fmt.Sprintf("acct:%s@%s", url.QueryEscape(u.Name), appURL.Host),
+ Aliases: aliases,
+ Links: links,
+ })
+}
diff --git a/services/agit/agit.go b/services/agit/agit.go
index 5f8d16172da4a..b61cb6f3f5692 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -1,18 +1,17 @@
// 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.
+// SPDX-License-Identifier: MIT
package agit
import (
+ "context"
"fmt"
- "net/http"
"os"
"strings"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
@@ -20,8 +19,8 @@ import (
pull_service "code.gitea.io/gitea/services/pull"
)
-// ProcRecive handle proc receive work
-func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []private.HookProcReceiveRefResult {
+// ProcReceive handle proc receive work
+func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
// TODO: Add more options?
var (
topicBranch string
@@ -31,10 +30,9 @@ func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []privat
)
results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
- repo := ctx.Repo.Repository
- gitRepo := ctx.Repo.GitRepo
- ownerName := ctx.Repo.Repository.OwnerName
- repoName := ctx.Repo.Repository.Name
+
+ ownerName := repo.OwnerName
+ repoName := repo.Name
topicBranch = opts.GitPushOptions["topic"]
_, forcePush = opts.GitPushOptions["force-push"]
@@ -81,7 +79,7 @@ func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []privat
continue
}
- headBranch := ""
+ var headBranch string
userName := strings.ToLower(opts.UserName)
if len(curentTopicBranch) == 0 {
@@ -97,44 +95,32 @@ func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []privat
headBranch = curentTopicBranch
}
- pr, err := models.GetUnmergedPullRequest(repo.ID, repo.ID, headBranch, baseBranchName, models.PullRequestFlowAGit)
+ pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit)
if err != nil {
- if !models.IsErrPullRequestNotExist(err) {
- log.Error("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %v", ownerName, repoName, err)
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "Err": fmt.Sprintf("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %v", ownerName, repoName, err),
- })
- return nil
+ if !issues_model.IsErrPullRequestNotExist(err) {
+ return nil, fmt.Errorf("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %w", ownerName, repoName, err)
}
// create a new pull request
if len(title) == 0 {
- has := false
+ var has bool
title, has = opts.GitPushOptions["title"]
if !has || len(title) == 0 {
commit, err := gitRepo.GetCommit(opts.NewCommitIDs[i])
if err != nil {
- log.Error("Failed to get commit %s in repository: %s/%s Error: %v", opts.NewCommitIDs[i], ownerName, repoName, err)
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "Err": fmt.Sprintf("Failed to get commit %s in repository: %s/%s Error: %v", opts.NewCommitIDs[i], ownerName, repoName, err),
- })
- return nil
+ return nil, fmt.Errorf("Failed to get commit %s in repository: %s/%s Error: %w", opts.NewCommitIDs[i], ownerName, repoName, err)
}
title = strings.Split(commit.CommitMessage, "\n")[0]
}
description = opts.GitPushOptions["description"]
}
- pusher, err := user_model.GetUserByID(opts.UserID)
+ pusher, err := user_model.GetUserByID(ctx, opts.UserID)
if err != nil {
- log.Error("Failed to get user. Error: %v", err)
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "Err": fmt.Sprintf("Failed to get user. Error: %v", err),
- })
- return nil
+ return nil, fmt.Errorf("Failed to get user. Error: %w", err)
}
- prIssue := &models.Issue{
+ prIssue := &issues_model.Issue{
RepoID: repo.ID,
Title: title,
PosterID: pusher.ID,
@@ -143,7 +129,7 @@ func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []privat
Content: description,
}
- pr := &models.PullRequest{
+ pr := &issues_model.PullRequest{
HeadRepoID: repo.ID,
BaseRepoID: repo.ID,
HeadBranch: headBranch,
@@ -152,17 +138,12 @@ func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []privat
HeadRepo: repo,
BaseRepo: repo,
MergeBase: "",
- Type: models.PullRequestGitea,
- Flow: models.PullRequestFlowAGit,
+ Type: issues_model.PullRequestGitea,
+ Flow: issues_model.PullRequestFlowAGit,
}
if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil {
- if models.IsErrUserDoesNotHaveAccessToRepo(err) {
- ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
- return nil
- }
- ctx.Error(http.StatusInternalServerError, "NewPullRequest", err.Error())
- return nil
+ return nil, err
}
log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
@@ -177,21 +158,13 @@ func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []privat
}
// update exist pull request
- if err := pr.LoadBaseRepo(); err != nil {
- log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "Err": fmt.Sprintf("Unable to load base repository for PR[%d] Error: %v", pr.ID, err),
- })
- return nil
+ if err := pr.LoadBaseRepo(ctx); err != nil {
+ return nil, fmt.Errorf("Unable to load base repository for PR[%d] Error: %w", pr.ID, err)
}
oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil {
- log.Error("Unable to get ref commit id in base repository for PR[%d] Error: %v", pr.ID, err)
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "Err": fmt.Sprintf("Unable to get ref commit id in base repository for PR[%d] Error: %v", pr.ID, err),
- })
- return nil
+ return nil, fmt.Errorf("Unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err)
}
if oldCommitID == opts.NewCommitIDs[i] {
@@ -205,13 +178,9 @@ func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []privat
}
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 {
- log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, opts.NewCommitIDs[i], repo, err)
- ctx.JSON(http.StatusInternalServerError, private.Response{
- Err: fmt.Sprintf("Fail to detect force push: %v", err),
- })
- return nil
+ return nil, fmt.Errorf("Fail to detect force push: %w", err)
} else if len(output) > 0 {
results = append(results, private.HookProcReceiveRefResult{
OriginalRef: opts.RefFullNames[i],
@@ -225,35 +194,23 @@ func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []privat
pr.HeadCommitID = opts.NewCommitIDs[i]
if err = pull_service.UpdateRef(ctx, pr); err != nil {
- log.Error("Failed to update pull ref. Error: %v", err)
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "Err": fmt.Sprintf("Failed to update pull ref. Error: %v", err),
- })
- return nil
+ return nil, fmt.Errorf("Failed to update pull ref. Error: %w", err)
}
pull_service.AddToTaskQueue(pr)
- pusher, err := user_model.GetUserByID(opts.UserID)
+ pusher, err := user_model.GetUserByID(ctx, opts.UserID)
if err != nil {
- log.Error("Failed to get user. Error: %v", err)
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "Err": fmt.Sprintf("Failed to get user. Error: %v", err),
- })
- return nil
+ return nil, fmt.Errorf("Failed to get user. Error: %w", err)
}
- err = pr.LoadIssue()
+ err = pr.LoadIssue(ctx)
if err != nil {
- log.Error("Failed to load pull issue. Error: %v", err)
- ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
- "Err": fmt.Sprintf("Failed to load pull issue. Error: %v", err),
- })
- return nil
+ return nil, fmt.Errorf("Failed to load pull issue. Error: %w", err)
}
- comment, err := models.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i])
+ comment, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i])
if err == nil && comment != nil {
- notification.NotifyPullRequestPushCommits(pusher, pr, comment)
+ notification.NotifyPullRequestPushCommits(ctx, pusher, pr, comment)
}
- notification.NotifyPullRequestSynchronized(pusher, pr)
+ notification.NotifyPullRequestSynchronized(ctx, pusher, pr)
isForcePush := comment != nil && comment.IsForcePush
results = append(results, private.HookProcReceiveRefResult{
@@ -265,12 +222,12 @@ func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []privat
})
}
- return results
+ return results, nil
}
// UserNameChanged handle user name change for agit flow pull
func UserNameChanged(user *user_model.User, newName string) error {
- pulls, err := models.GetAllUnmergedAgitPullRequestByPoster(user.ID)
+ pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(user.ID)
if err != nil {
return err
}
diff --git a/services/asymkey/deploy_key.go b/services/asymkey/deploy_key.go
index aa0925ab13392..f5ca54b72387f 100644
--- a/services/asymkey/deploy_key.go
+++ b/services/asymkey/deploy_key.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package asymkey
@@ -13,7 +12,7 @@ import (
// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
func DeleteDeployKey(doer *user_model.User, id int64) error {
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
diff --git a/services/asymkey/main_test.go b/services/asymkey/main_test.go
index cb3d059456586..3fa88340fd722 100644
--- a/services/asymkey/main_test.go
+++ b/services/asymkey/main_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package asymkey
diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go
index c3f093a808105..01718ebe77c32 100644
--- a/services/asymkey/sign.go
+++ b/services/asymkey/sign.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package asymkey
@@ -9,10 +8,11 @@ import (
"fmt"
"strings"
- "code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
@@ -270,8 +270,8 @@ Loop:
}
// SignMerge determines if we should sign a PR merge commit to the base repository
-func SignMerge(ctx context.Context, pr *models.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) {
- if err := pr.LoadBaseRepo(); err != nil {
+func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to get Base Repo for pull request")
return false, "", nil, err
}
@@ -310,14 +310,14 @@ Loop:
return false, "", nil, &ErrWontSign{twofa}
}
case approved:
- protectedBranch, err := models.GetProtectedBranchBy(repo.ID, pr.BaseBranch)
+ protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pr.BaseBranch)
if err != nil {
return false, "", nil, err
}
if protectedBranch == nil {
return false, "", nil, &ErrWontSign{approved}
}
- if protectedBranch.GetGrantedApprovalsCount(pr) < 1 {
+ if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 {
return false, "", nil, &ErrWontSign{approved}
}
case baseSigned:
diff --git a/services/asymkey/ssh_key.go b/services/asymkey/ssh_key.go
index 1f6b93eb243e3..0809458107b75 100644
--- a/services/asymkey/ssh_key.go
+++ b/services/asymkey/ssh_key.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package asymkey
@@ -26,7 +25,7 @@ func DeletePublicKey(doer *user_model.User, id int64) (err error) {
}
}
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
@@ -42,7 +41,7 @@ func DeletePublicKey(doer *user_model.User, id int64) (err error) {
committer.Close()
if key.Type == asymkey_model.KeyTypePrincipal {
- return asymkey_model.RewriteAllPrincipalKeys()
+ return asymkey_model.RewriteAllPrincipalKeys(db.DefaultContext)
}
return asymkey_model.RewriteAllPublicKeys()
diff --git a/services/asymkey/ssh_key_test.go b/services/asymkey/ssh_key_test.go
index 182371271a4cb..32c31a4332391 100644
--- a/services/asymkey/ssh_key_test.go
+++ b/services/asymkey/ssh_key_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package asymkey
@@ -18,7 +17,7 @@ import (
func TestAddLdapSSHPublicKeys(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
s := &auth.Source{ID: 1}
testCases := []struct {
diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go
index cce36206a765a..7fdacc6aae505 100644
--- a/services/attachment/attachment.go
+++ b/services/attachment/attachment.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package attachment
@@ -25,11 +24,11 @@ func NewAttachment(attach *repo_model.Attachment, file io.Reader) (*repo_model.A
return nil, fmt.Errorf("attachment %s should belong to a repository", attach.Name)
}
- err := db.WithTx(func(ctx context.Context) error {
+ err := db.WithTx(db.DefaultContext, func(ctx context.Context) error {
attach.UUID = uuid.New().String()
size, err := storage.Attachments.Save(attach.RelativePath(), file, -1)
if err != nil {
- return fmt.Errorf("Create: %v", err)
+ return fmt.Errorf("Create: %w", err)
}
attach.Size = size
@@ -40,19 +39,14 @@ func NewAttachment(attach *repo_model.Attachment, file io.Reader) (*repo_model.A
}
// UploadAttachment upload new attachment into storage and update database
-func UploadAttachment(file io.Reader, actorID, repoID, releaseID int64, fileName, allowedTypes string) (*repo_model.Attachment, error) {
+func UploadAttachment(file io.Reader, allowedTypes string, opts *repo_model.Attachment) (*repo_model.Attachment, error) {
buf := make([]byte, 1024)
n, _ := util.ReadAtMost(file, buf)
buf = buf[:n]
- if err := upload.Verify(buf, fileName, allowedTypes); err != nil {
+ if err := upload.Verify(buf, opts.Name, allowedTypes); err != nil {
return nil, err
}
- return NewAttachment(&repo_model.Attachment{
- RepoID: repoID,
- UploaderID: actorID,
- ReleaseID: releaseID,
- Name: fileName,
- }, io.MultiReader(bytes.NewReader(buf), file))
+ return NewAttachment(opts, io.MultiReader(bytes.NewReader(buf), file))
}
diff --git a/services/attachment/attachment_test.go b/services/attachment/attachment_test.go
index ffce5943e5cfd..72d1b2ab3a445 100644
--- a/services/attachment/attachment_test.go
+++ b/services/attachment/attachment_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package attachment
@@ -9,6 +8,7 @@ import (
"path/filepath"
"testing"
+ "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -25,7 +25,7 @@ func TestMain(m *testing.M) {
func TestUploadAttachment(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
fPath := "./attachment_test.go"
f, err := os.Open(fPath)
@@ -39,7 +39,7 @@ func TestUploadAttachment(t *testing.T) {
}, f)
assert.NoError(t, err)
- attachment, err := repo_model.GetAttachmentByUUID(attach.UUID)
+ attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, attach.UUID)
assert.NoError(t, err)
assert.EqualValues(t, user.ID, attachment.UploaderID)
assert.Equal(t, int64(0), attachment.DownloadCount)
diff --git a/services/auth/auth.go b/services/auth/auth.go
index 3a5bb9d27e65b..00e277c41abb2 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
diff --git a/services/auth/auth_test.go b/services/auth/auth_test.go
index 8a9dee6cefe4c..f4e3cdf0d32dc 100644
--- a/services/auth/auth_test.go
+++ b/services/auth/auth_test.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
diff --git a/services/auth/basic.go b/services/auth/basic.go
index 1869662e9277d..5fb80703ab5a4 100644
--- a/services/auth/basic.go
+++ b/services/auth/basic.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
@@ -9,7 +8,7 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models"
+ auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
@@ -41,20 +40,20 @@ func (b *Basic) Name() string {
// "Authorization" header of the request and returns the corresponding user object for that
// name/token on successful validation.
// Returns nil if header is empty or validation fails.
-func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User {
+func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// Basic authentication should only fire on API, Download or on Git or LFSPaths
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(req) {
- return nil
+ return nil, nil
}
baHead := req.Header.Get("Authorization")
if len(baHead) == 0 {
- return nil
+ return nil, nil
}
auths := strings.SplitN(baHead, " ", 2)
if len(auths) != 2 || (strings.ToLower(auths[0]) != "basic") {
- return nil
+ return nil, nil
}
uname, passwd, _ := base.BasicAuthDecode(auths[1])
@@ -75,38 +74,38 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
if uid != 0 {
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
- u, err := user_model.GetUserByID(uid)
+ u, err := user_model.GetUserByID(req.Context(), uid)
if err != nil {
log.Error("GetUserByID: %v", err)
- return nil
+ return nil, err
}
store.GetData()["IsApiToken"] = true
- return u
+ return u, nil
}
- token, err := models.GetAccessTokenBySHA(authToken)
+ token, err := auth_model.GetAccessTokenBySHA(authToken)
if err == nil {
log.Trace("Basic Authorization: Valid AccessToken for user[%d]", uid)
- u, err := user_model.GetUserByID(token.UID)
+ u, err := user_model.GetUserByID(req.Context(), token.UID)
if err != nil {
log.Error("GetUserByID: %v", err)
- return nil
+ return nil, err
}
token.UpdatedUnix = timeutil.TimeStampNow()
- if err = models.UpdateAccessToken(token); err != nil {
+ if err = auth_model.UpdateAccessToken(token); err != nil {
log.Error("UpdateAccessToken: %v", err)
}
store.GetData()["IsApiToken"] = true
- return u
- } else if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) {
+ return u, nil
+ } else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySha: %v", err)
}
if !setting.Service.EnableBasicAuth {
- return nil
+ return nil, nil
}
log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
@@ -115,7 +114,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
if !user_model.IsErrUserNotExist(err) {
log.Error("UserSignIn: %v", err)
}
- return nil
+ return nil, err
}
if skipper, ok := source.Cfg.(LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
@@ -124,5 +123,5 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
log.Trace("Basic Authorization: Logged in user %-v", u)
- return u
+ return u, nil
}
diff --git a/services/auth/group.go b/services/auth/group.go
index 0f40e1a76c9be..0a0330b3aa95e 100644
--- a/services/auth/group.go
+++ b/services/auth/group.go
@@ -1,15 +1,14 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
import (
+ "context"
"net/http"
"reflect"
"strings"
- "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
)
@@ -51,14 +50,14 @@ func (b *Group) Name() string {
}
// Init does nothing as the Basic implementation does not need to allocate any resources
-func (b *Group) Init() error {
+func (b *Group) Init(ctx context.Context) error {
for _, method := range b.methods {
initializable, ok := method.(Initializable)
if !ok {
continue
}
- if err := initializable.Init(); err != nil {
+ if err := initializable.Init(ctx); err != nil {
return err
}
}
@@ -80,23 +79,23 @@ func (b *Group) Free() error {
}
// Verify extracts and validates
-func (b *Group) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User {
- if !db.HasEngine {
- return nil
- }
-
+func (b *Group) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// Try to sign in with each of the enabled plugins
for _, ssoMethod := range b.methods {
- user := ssoMethod.Verify(req, w, store, sess)
+ user, err := ssoMethod.Verify(req, w, store, sess)
+ if err != nil {
+ return nil, err
+ }
+
if user != nil {
if store.GetData()["AuthedMethod"] == nil {
if named, ok := ssoMethod.(Named); ok {
store.GetData()["AuthedMethod"] = named.Name()
}
}
- return user
+ return user, nil
}
}
- return nil
+ return nil, nil
}
diff --git a/services/auth/httpsign.go b/services/auth/httpsign.go
new file mode 100644
index 0000000000000..4d52315381c32
--- /dev/null
+++ b/services/auth/httpsign.go
@@ -0,0 +1,216 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package auth
+
+import (
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/go-fed/httpsig"
+ "golang.org/x/crypto/ssh"
+)
+
+// Ensure the struct implements the interface.
+var (
+ _ Method = &HTTPSign{}
+ _ Named = &HTTPSign{}
+)
+
+// HTTPSign implements the Auth interface and authenticates requests (API requests
+// only) by looking for http signature data in the "Signature" header.
+// more information can be found on https://github.com/go-fed/httpsig
+type HTTPSign struct{}
+
+// Name represents the name of auth method
+func (h *HTTPSign) Name() string {
+ return "httpsign"
+}
+
+// Verify extracts and validates HTTPsign from the Signature header of the request and returns
+// the corresponding user object on successful validation.
+// Returns nil if header is empty or validation fails.
+func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
+ sigHead := req.Header.Get("Signature")
+ if len(sigHead) == 0 {
+ return nil, nil
+ }
+
+ var (
+ publicKey *asymkey_model.PublicKey
+ err error
+ )
+
+ if len(req.Header.Get("X-Ssh-Certificate")) != 0 {
+ // Handle Signature signed by SSH certificates
+ if len(setting.SSH.TrustedUserCAKeys) == 0 {
+ return nil, nil
+ }
+
+ publicKey, err = VerifyCert(req)
+ if err != nil {
+ log.Debug("VerifyCert on request from %s: failed: %v", req.RemoteAddr, err)
+ log.Warn("Failed authentication attempt from %s", req.RemoteAddr)
+ return nil, nil
+ }
+ } else {
+ // Handle Signature signed by Public Key
+ publicKey, err = VerifyPubKey(req)
+ if err != nil {
+ log.Debug("VerifyPubKey on request from %s: failed: %v", req.RemoteAddr, err)
+ log.Warn("Failed authentication attempt from %s", req.RemoteAddr)
+ return nil, nil
+ }
+ }
+
+ u, err := user_model.GetUserByID(req.Context(), publicKey.OwnerID)
+ if err != nil {
+ log.Error("GetUserByID: %v", err)
+ return nil, err
+ }
+
+ store.GetData()["IsApiToken"] = true
+
+ log.Trace("HTTP Sign: Logged in user %-v", u)
+
+ return u, nil
+}
+
+func VerifyPubKey(r *http.Request) (*asymkey_model.PublicKey, error) {
+ verifier, err := httpsig.NewVerifier(r)
+ if err != nil {
+ return nil, fmt.Errorf("httpsig.NewVerifier failed: %s", err)
+ }
+
+ keyID := verifier.KeyId()
+
+ publicKeys, err := asymkey_model.SearchPublicKey(0, keyID)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(publicKeys) == 0 {
+ return nil, fmt.Errorf("no public key found for keyid %s", keyID)
+ }
+
+ sshPublicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKeys[0].Content))
+ if err != nil {
+ return nil, err
+ }
+
+ if err := doVerify(verifier, []ssh.PublicKey{sshPublicKey}); err != nil {
+ return nil, err
+ }
+
+ return publicKeys[0], nil
+}
+
+// VerifyCert verifies the validity of the ssh certificate and returns the publickey of the signer
+// We verify that the certificate is signed with the correct CA
+// We verify that the http request is signed with the private key (of the public key mentioned in the certificate)
+func VerifyCert(r *http.Request) (*asymkey_model.PublicKey, error) {
+ // Get our certificate from the header
+ bcert, err := base64.RawStdEncoding.DecodeString(r.Header.Get("x-ssh-certificate"))
+ if err != nil {
+ return nil, err
+ }
+
+ pk, err := ssh.ParsePublicKey(bcert)
+ if err != nil {
+ return nil, err
+ }
+
+ // Check if it's really a ssh certificate
+ cert, ok := pk.(*ssh.Certificate)
+ if !ok {
+ return nil, fmt.Errorf("no certificate found")
+ }
+
+ c := &ssh.CertChecker{
+ IsUserAuthority: func(auth ssh.PublicKey) bool {
+ marshaled := auth.Marshal()
+
+ for _, k := range setting.SSH.TrustedUserCAKeysParsed {
+ if bytes.Equal(marshaled, k.Marshal()) {
+ return true
+ }
+ }
+
+ return false
+ },
+ }
+
+ // check the CA of the cert
+ if !c.IsUserAuthority(cert.SignatureKey) {
+ return nil, fmt.Errorf("CA check failed")
+ }
+
+ // Create a verifier
+ verifier, err := httpsig.NewVerifier(r)
+ if err != nil {
+ return nil, fmt.Errorf("httpsig.NewVerifier failed: %s", err)
+ }
+
+ // now verify that this request was signed with the private key that matches the certificate public key
+ if err := doVerify(verifier, []ssh.PublicKey{cert.Key}); err != nil {
+ return nil, err
+ }
+
+ // Now for each of the certificate valid principals
+ for _, principal := range cert.ValidPrincipals {
+ // Look in the db for the public key
+ publicKey, err := asymkey_model.SearchPublicKeyByContentExact(r.Context(), principal)
+ if asymkey_model.IsErrKeyNotExist(err) {
+ // No public key matches this principal - try the next principal
+ continue
+ } else if err != nil {
+ // this error will be a db error therefore we can't solve this and we should abort
+ log.Error("SearchPublicKeyByContentExact: %v", err)
+ return nil, err
+ }
+
+ // Validate the cert for this principal
+ if err := c.CheckCert(principal, cert); err != nil {
+ // however, because principal is a member of ValidPrincipals - if this fails then the certificate itself is invalid
+ return nil, err
+ }
+
+ // OK we have a public key for a principal matching a valid certificate whose key has signed this request.
+ return publicKey, nil
+ }
+
+ // No public key matching a principal in the certificate is registered in gitea
+ return nil, fmt.Errorf("no valid principal found")
+}
+
+// doVerify iterates across the provided public keys attempting the verify the current request against each key in turn
+func doVerify(verifier httpsig.Verifier, sshPublicKeys []ssh.PublicKey) error {
+ for _, publicKey := range sshPublicKeys {
+ cryptoPubkey := publicKey.(ssh.CryptoPublicKey).CryptoPublicKey()
+
+ var algos []httpsig.Algorithm
+
+ switch {
+ case strings.HasPrefix(publicKey.Type(), "ssh-ed25519"):
+ algos = []httpsig.Algorithm{httpsig.ED25519}
+ case strings.HasPrefix(publicKey.Type(), "ssh-rsa"):
+ algos = []httpsig.Algorithm{httpsig.RSA_SHA1, httpsig.RSA_SHA256, httpsig.RSA_SHA512}
+ }
+ for _, algo := range algos {
+ if err := verifier.Verify(cryptoPubkey, algo); err == nil {
+ return nil
+ }
+ }
+ }
+
+ return errors.New("verification failed")
+}
diff --git a/services/auth/interface.go b/services/auth/interface.go
index a05ece2078d1d..f2f1aaf39cb09 100644
--- a/services/auth/interface.go
+++ b/services/auth/interface.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
@@ -25,8 +24,9 @@ type Method interface {
// If verification is successful returns either an existing user object (with id > 0)
// or a new user object (with id = 0) populated with the information that was found
// in the authentication data (username or email).
- // Returns nil if verification fails.
- Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User
+ // Second argument returns err if verification fails, otherwise
+ // First return argument returns nil if no matched verification condition
+ Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error)
}
// Initializable represents a structure that requires initialization
@@ -34,7 +34,7 @@ type Method interface {
type Initializable interface {
// Init should be called exactly once before using any of the other methods,
// in order to allow the plugin to allocate necessary resources
- Init() error
+ Init(ctx context.Context) error
}
// Named represents a named thing
diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go
index 42c91fac37593..1be78b85c5efa 100644
--- a/services/auth/oauth2.go
+++ b/services/auth/oauth2.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
@@ -10,8 +9,7 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/auth"
+ auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
@@ -37,8 +35,8 @@ func CheckOAuthAccessToken(accessToken string) int64 {
log.Trace("oauth2.ParseToken: %v", err)
return 0
}
- var grant *auth.OAuth2Grant
- if grant, err = auth.GetOAuth2GrantByID(token.GrantID); err != nil || grant == nil {
+ var grant *auth_model.OAuth2Grant
+ if grant, err = auth_model.GetOAuth2GrantByID(db.DefaultContext, token.GrantID); err != nil || grant == nil {
return 0
}
if token.Type != oauth2.TypeAccessToken {
@@ -61,6 +59,8 @@ func (o *OAuth2) Name() string {
}
// userIDFromToken returns the user id corresponding to the OAuth token.
+// It will set 'IsApiToken' to true if the token is an API token and
+// set 'ApiTokenScope' to the scope of the access token
func (o *OAuth2) userIDFromToken(req *http.Request, store DataStore) int64 {
_ = req.ParseForm()
@@ -88,21 +88,23 @@ func (o *OAuth2) userIDFromToken(req *http.Request, store DataStore) int64 {
uid := CheckOAuthAccessToken(tokenSHA)
if uid != 0 {
store.GetData()["IsApiToken"] = true
+ store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all
}
return uid
}
- t, err := models.GetAccessTokenBySHA(tokenSHA)
+ t, err := auth_model.GetAccessTokenBySHA(tokenSHA)
if err != nil {
- if !models.IsErrAccessTokenNotExist(err) && !models.IsErrAccessTokenEmpty(err) {
+ if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySHA: %v", err)
}
return 0
}
t.UpdatedUnix = timeutil.TimeStampNow()
- if err = models.UpdateAccessToken(t); err != nil {
+ if err = auth_model.UpdateAccessToken(t); err != nil {
log.Error("UpdateAccessToken: %v", err)
}
store.GetData()["IsApiToken"] = true
+ store.GetData()["ApiTokenScope"] = t.Scope
return t.UID
}
@@ -110,31 +112,27 @@ func (o *OAuth2) userIDFromToken(req *http.Request, store DataStore) int64 {
// or the "Authorization" header and returns the corresponding user object for that ID.
// If verification is successful returns an existing user object.
// Returns nil if verification fails.
-func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User {
- if !db.HasEngine {
- return nil
- }
-
+func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) {
- return nil
+ return nil, nil
}
id := o.userIDFromToken(req, store)
if id <= 0 {
- return nil
+ return nil, nil
}
log.Trace("OAuth2 Authorization: Found token for user[%d]", id)
- user, err := user_model.GetUserByID(id)
+ user, err := user_model.GetUserByID(req.Context(), id)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByName: %v", err)
}
- return nil
+ return nil, err
}
log.Trace("OAuth2 Authorization: Logged in user %-v", user)
- return user
+ return user, nil
}
func isAuthenticatedTokenRequest(req *http.Request) bool {
diff --git a/services/auth/reverseproxy.go b/services/auth/reverseproxy.go
index 1b151f6504e33..0206ccdf667df 100644
--- a/services/auth/reverseproxy.go
+++ b/services/auth/reverseproxy.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
@@ -12,6 +11,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/mailer"
@@ -36,11 +36,7 @@ type ReverseProxy struct{}
// getUserName extracts the username from the "setting.ReverseProxyAuthUser" header
func (r *ReverseProxy) getUserName(req *http.Request) string {
- webAuthUser := strings.TrimSpace(req.Header.Get(setting.ReverseProxyAuthUser))
- if len(webAuthUser) == 0 {
- return ""
- }
- return webAuthUser
+ return strings.TrimSpace(req.Header.Get(setting.ReverseProxyAuthUser))
}
// Name represents the name of auth method
@@ -48,28 +44,79 @@ func (r *ReverseProxy) Name() string {
return ReverseProxyMethodName
}
-// Verify extracts the username from the "setting.ReverseProxyAuthUser" header
+// getUserFromAuthUser extracts the username from the "setting.ReverseProxyAuthUser" header
// of the request and returns the corresponding user object for that name.
// Verification of header data is not performed as it should have already been done by
-// the revese proxy.
+// the reverse proxy.
// If a username is available in the "setting.ReverseProxyAuthUser" header an existing
// user object is returned (populated with username or email found in header).
// Returns nil if header is empty.
-func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User {
+func (r *ReverseProxy) getUserFromAuthUser(req *http.Request) (*user_model.User, error) {
username := r.getUserName(req)
if len(username) == 0 {
- return nil
+ return nil, nil
}
log.Trace("ReverseProxy Authorization: Found username: %s", username)
- user, err := user_model.GetUserByName(username)
+ user, err := user_model.GetUserByName(req.Context(), username)
if err != nil {
if !user_model.IsErrUserNotExist(err) || !r.isAutoRegisterAllowed() {
log.Error("GetUserByName: %v", err)
- return nil
+ return nil, err
}
user = r.newUser(req)
}
+ return user, nil
+}
+
+// getEmail extracts the email from the "setting.ReverseProxyAuthEmail" header
+func (r *ReverseProxy) getEmail(req *http.Request) string {
+ return strings.TrimSpace(req.Header.Get(setting.ReverseProxyAuthEmail))
+}
+
+// getUserFromAuthEmail extracts the username from the "setting.ReverseProxyAuthEmail" header
+// of the request and returns the corresponding user object for that email.
+// Verification of header data is not performed as it should have already been done by
+// the reverse proxy.
+// If an email is available in the "setting.ReverseProxyAuthEmail" header an existing
+// user object is returned (populated with the email found in header).
+// Returns nil if header is empty or if "setting.EnableReverseProxyEmail" is disabled.
+func (r *ReverseProxy) getUserFromAuthEmail(req *http.Request) *user_model.User {
+ if !setting.Service.EnableReverseProxyEmail {
+ return nil
+ }
+ email := r.getEmail(req)
+ if len(email) == 0 {
+ return nil
+ }
+ log.Trace("ReverseProxy Authorization: Found email: %s", email)
+
+ user, err := user_model.GetUserByEmail(email)
+ if err != nil {
+ // Do not allow auto-registration, we don't have a username here
+ if !user_model.IsErrUserNotExist(err) {
+ log.Error("GetUserByEmail: %v", err)
+ }
+ return nil
+ }
+ return user
+}
+
+// Verify attempts to load a user object based on headers sent by the reverse proxy.
+// First it will attempt to load it based on the username (see docs for getUserFromAuthUser),
+// and failing that it will attempt to load it based on the email (see docs for getUserFromAuthEmail).
+// Returns nil if the headers are empty or the user is not found.
+func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
+ user, err := r.getUserFromAuthUser(req)
+ if err != nil {
+ return nil, err
+ }
+ if user == nil {
+ user = r.getUserFromAuthEmail(req)
+ if user == nil {
+ return nil, nil
+ }
+ }
// Make sure requests to API paths, attachment downloads, git and LFS do not create a new session
if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isGitRawReleaseOrLFSPath(req) {
@@ -80,7 +127,7 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da
store.GetData()["IsReverseProxy"] = true
log.Trace("ReverseProxy Authorization: Logged in user %-v", user)
- return user
+ return user, nil
}
// isAutoRegisterAllowed checks if EnableReverseProxyAutoRegister setting is true
@@ -104,12 +151,22 @@ func (r *ReverseProxy) newUser(req *http.Request) *user_model.User {
}
}
+ var fullname string
+ if setting.Service.EnableReverseProxyFullName {
+ fullname = req.Header.Get(setting.ReverseProxyAuthFullName)
+ }
+
user := &user_model.User{
Name: username,
Email: email,
- IsActive: true,
+ FullName: fullname,
}
- if err := user_model.CreateUser(user); err != nil {
+
+ overwriteDefault := user_model.CreateUserOverwriteOptions{
+ IsActive: util.OptionalBoolTrue,
+ }
+
+ if err := user_model.CreateUser(user, &overwriteDefault); err != nil {
// FIXME: should I create a system notice?
log.Error("CreateUser: %v", err)
return nil
diff --git a/services/auth/session.go b/services/auth/session.go
index 6a23a176651ff..c751135738106 100644
--- a/services/auth/session.go
+++ b/services/auth/session.go
@@ -1,12 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
import (
"net/http"
+ "code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
)
@@ -29,16 +29,20 @@ func (s *Session) Name() string {
// Verify checks if there is a user uid stored in the session and returns the user
// object for that uid.
// Returns nil if there is no user uid stored in the session.
-func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User {
+func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
user := SessionUser(sess)
if user != nil {
- return user
+ return user, nil
}
- return nil
+ return nil, nil
}
// SessionUser returns the user object corresponding to the "uid" session variable.
func SessionUser(sess SessionStore) *user_model.User {
+ if sess == nil {
+ return nil
+ }
+
// Get user ID
uid := sess.Get("uid")
if uid == nil {
@@ -52,7 +56,7 @@ func SessionUser(sess SessionStore) *user_model.User {
}
// Get user object
- user, err := user_model.GetUserByID(id)
+ user, err := user_model.GetUserByID(db.DefaultContext, id)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserById: %v", err)
diff --git a/services/auth/signin.go b/services/auth/signin.go
index 3ccf68c3a7eab..2af15c4133ccd 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
diff --git a/services/auth/source.go b/services/auth/source.go
index b7108292d5705..aae3a781024aa 100644
--- a/services/auth/source.go
+++ b/services/auth/source.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
diff --git a/services/auth/source/db/assert_interface_test.go b/services/auth/source/db/assert_interface_test.go
index f39aaeb1e41be..62387c78f072d 100644
--- a/services/auth/source/db/assert_interface_test.go
+++ b/services/auth/source/db/assert_interface_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package db_test
diff --git a/services/auth/source/db/authenticate.go b/services/auth/source/db/authenticate.go
index f062f66ae039c..ec89984499862 100644
--- a/services/auth/source/db/authenticate.go
+++ b/services/auth/source/db/authenticate.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package db
diff --git a/services/auth/source/db/source.go b/services/auth/source/db/source.go
index ecab6d5f3570a..3f4113c790479 100644
--- a/services/auth/source/db/source.go
+++ b/services/auth/source/db/source.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package db
diff --git a/services/auth/source/ldap/assert_interface_test.go b/services/auth/source/ldap/assert_interface_test.go
index 8fc6903cf37cf..33347687dc0b6 100644
--- a/services/auth/source/ldap/assert_interface_test.go
+++ b/services/auth/source/ldap/assert_interface_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package ldap_test
diff --git a/services/auth/source/ldap/security_protocol.go b/services/auth/source/ldap/security_protocol.go
index bb0c7770a1d35..af83ce191d7c8 100644
--- a/services/auth/source/ldap/security_protocol.go
+++ b/services/auth/source/ldap/security_protocol.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package ldap
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index ad97e2dd499be..dc4cb2c94031b 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package ldap
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index ddd70627ed3f1..321cf5540d989 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package ldap
@@ -13,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
)
@@ -33,11 +33,11 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
// Update User admin flag if exist
- if isExist, err := user_model.IsUserExist(0, sr.Username); err != nil {
+ if isExist, err := user_model.IsUserExist(db.DefaultContext, 0, sr.Username); err != nil {
return nil, err
} else if isExist {
if user == nil {
- user, err = user_model.GetUserByName(sr.Username)
+ user, err = user_model.GetUserByName(db.DefaultContext, sr.Username)
if err != nil {
return nil, err
}
@@ -85,19 +85,21 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
}
user = &user_model.User{
- LowerName: strings.ToLower(sr.Username),
- Name: sr.Username,
- FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
- Email: sr.Mail,
- LoginType: source.authSource.Type,
- LoginSource: source.authSource.ID,
- LoginName: userName,
- IsActive: true,
- IsAdmin: sr.IsAdmin,
- IsRestricted: sr.IsRestricted,
+ LowerName: strings.ToLower(sr.Username),
+ Name: sr.Username,
+ FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
+ Email: sr.Mail,
+ LoginType: source.authSource.Type,
+ LoginSource: source.authSource.ID,
+ LoginName: userName,
+ IsAdmin: sr.IsAdmin,
+ }
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
+ IsActive: util.OptionalBoolTrue,
}
- err := user_model.CreateUser(user)
+ err := user_model.CreateUser(user, overwriteDefault)
if err != nil {
return user, err
}
diff --git a/services/auth/source/ldap/source_group_sync.go b/services/auth/source/ldap/source_group_sync.go
index e797e015b23bd..95a6084922860 100644
--- a/services/auth/source/ldap/source_group_sync.go
+++ b/services/auth/source/ldap/source_group_sync.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package ldap
diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go
index f2b940cabe026..68ebba391769d 100644
--- a/services/auth/source/ldap/source_search.go
+++ b/services/auth/source/ldap/source_search.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package ldap
@@ -34,7 +33,7 @@ type SearchResult struct {
LdapTeamRemove map[string][]string // organizations teams to remove
}
-func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
+func (source *Source) sanitizedUserQuery(username string) (string, bool) {
// See http://tools.ietf.org/search/rfc4515
badCharacters := "\x00()*\\"
if strings.ContainsAny(username, badCharacters) {
@@ -42,10 +41,10 @@ func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
return "", false
}
- return fmt.Sprintf(ls.Filter, username), true
+ return fmt.Sprintf(source.Filter, username), true
}
-func (ls *Source) sanitizedUserDN(username string) (string, bool) {
+func (source *Source) sanitizedUserDN(username string) (string, bool) {
// See http://tools.ietf.org/search/rfc4514: "special characters"
badCharacters := "\x00()*\\,='\"#+;<>"
if strings.ContainsAny(username, badCharacters) {
@@ -53,10 +52,10 @@ func (ls *Source) sanitizedUserDN(username string) (string, bool) {
return "", false
}
- return fmt.Sprintf(ls.UserDN, username), true
+ return fmt.Sprintf(source.UserDN, username), true
}
-func (ls *Source) sanitizedGroupFilter(group string) (string, bool) {
+func (source *Source) sanitizedGroupFilter(group string) (string, bool) {
// See http://tools.ietf.org/search/rfc4515
badCharacters := "\x00*\\"
if strings.ContainsAny(group, badCharacters) {
@@ -67,7 +66,7 @@ func (ls *Source) sanitizedGroupFilter(group string) (string, bool) {
return group, true
}
-func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) {
+func (source *Source) sanitizedGroupDN(groupDn string) (string, bool) {
// See http://tools.ietf.org/search/rfc4514: "special characters"
badCharacters := "\x00()*\\'\"#+;<>"
if strings.ContainsAny(groupDn, badCharacters) || strings.HasPrefix(groupDn, " ") || strings.HasSuffix(groupDn, " ") {
@@ -78,18 +77,18 @@ func (ls *Source) sanitizedGroupDN(groupDn string) (string, bool) {
return groupDn, true
}
-func (ls *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
+func (source *Source) findUserDN(l *ldap.Conn, name string) (string, bool) {
log.Trace("Search for LDAP user: %s", name)
// A search for the user.
- userFilter, ok := ls.sanitizedUserQuery(name)
+ userFilter, ok := source.sanitizedUserQuery(name)
if !ok {
return "", false
}
- log.Trace("Searching for DN using filter %s and base %s", userFilter, ls.UserBase)
+ log.Trace("Searching for DN using filter %s and base %s", userFilter, source.UserBase)
search := ldap.NewSearchRequest(
- ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0,
+ source.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0,
false, userFilter, []string{}, nil)
// Ensure we found a user
@@ -125,13 +124,13 @@ func dial(source *Source) (*ldap.Conn, error) {
conn, err := ldap.Dial("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port)))
if err != nil {
- return nil, fmt.Errorf("error during Dial: %v", err)
+ return nil, fmt.Errorf("error during Dial: %w", err)
}
if source.SecurityProtocol == SecurityProtocolStartTLS {
if err = conn.StartTLS(tlsConfig); err != nil {
conn.Close()
- return nil, fmt.Errorf("error during StartTLS: %v", err)
+ return nil, fmt.Errorf("error during StartTLS: %w", err)
}
}
@@ -197,11 +196,11 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool {
}
// List all group memberships of a user
-func (ls *Source) listLdapGroupMemberships(l *ldap.Conn, uid string) []string {
+func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string) []string {
var ldapGroups []string
- groupFilter := fmt.Sprintf("(%s=%s)", ls.GroupMemberUID, uid)
+ groupFilter := fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid))
result, err := l.Search(ldap.NewSearchRequest(
- ls.GroupDN,
+ source.GroupDN,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0,
@@ -228,9 +227,9 @@ func (ls *Source) listLdapGroupMemberships(l *ldap.Conn, uid string) []string {
}
// parse LDAP groups and return map of ldap groups to organizations teams
-func (ls *Source) mapLdapGroupsToTeams() map[string]map[string][]string {
+func (source *Source) mapLdapGroupsToTeams() map[string]map[string][]string {
ldapGroupsToTeams := make(map[string]map[string][]string)
- err := json.Unmarshal([]byte(ls.GroupTeamMap), &ldapGroupsToTeams)
+ err := json.Unmarshal([]byte(source.GroupTeamMap), &ldapGroupsToTeams)
if err != nil {
log.Error("Failed to unmarshall LDAP teams map: %v", err)
return ldapGroupsToTeams
@@ -239,15 +238,15 @@ func (ls *Source) mapLdapGroupsToTeams() map[string]map[string][]string {
}
// getMappedMemberships : returns the organizations and teams to modify the users membership
-func (ls *Source) getMappedMemberships(l *ldap.Conn, uid string) (map[string][]string, map[string][]string) {
+func (source *Source) getMappedMemberships(l *ldap.Conn, uid string) (map[string][]string, map[string][]string) {
// get all LDAP group memberships for user
- usersLdapGroups := ls.listLdapGroupMemberships(l, uid)
+ usersLdapGroups := source.listLdapGroupMemberships(l, uid)
// unmarshall LDAP group team map from configs
- ldapGroupsToTeams := ls.mapLdapGroupsToTeams()
+ ldapGroupsToTeams := source.mapLdapGroupsToTeams()
membershipsToAdd := map[string][]string{}
membershipsToRemove := map[string][]string{}
for group, memberships := range ldapGroupsToTeams {
- isUserInGroup := util.IsStringInSlice(group, usersLdapGroups)
+ isUserInGroup := util.SliceContainsString(usersLdapGroups, group)
if isUserInGroup {
for org, teams := range memberships {
membershipsToAdd[org] = teams
@@ -262,26 +261,26 @@ func (ls *Source) getMappedMemberships(l *ldap.Conn, uid string) (map[string][]s
}
// SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
-func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult {
+func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult {
// See https://tools.ietf.org/search/rfc4513#section-5.1.2
if len(passwd) == 0 {
log.Debug("Auth. failed for %s, password cannot be empty", name)
return nil
}
- l, err := dial(ls)
+ l, err := dial(source)
if err != nil {
- log.Error("LDAP Connect error, %s:%v", ls.Host, err)
- ls.Enabled = false
+ log.Error("LDAP Connect error, %s:%v", source.Host, err)
+ source.Enabled = false
return nil
}
defer l.Close()
var userDN string
if directBind {
- log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN)
+ log.Trace("LDAP will bind directly via UserDN template: %s", source.UserDN)
var ok bool
- userDN, ok = ls.sanitizedUserDN(name)
+ userDN, ok = source.sanitizedUserDN(name)
if !ok {
return nil
@@ -292,11 +291,11 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
return nil
}
- if ls.UserBase != "" {
+ if source.UserBase != "" {
// not everyone has a CN compatible with input name so we need to find
// the real userDN in that case
- userDN, ok = ls.findUserDN(l, name)
+ userDN, ok = source.findUserDN(l, name)
if !ok {
return nil
}
@@ -306,24 +305,24 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
var found bool
- if ls.BindDN != "" && ls.BindPassword != "" {
- err := l.Bind(ls.BindDN, ls.BindPassword)
+ if source.BindDN != "" && source.BindPassword != "" {
+ err := l.Bind(source.BindDN, source.BindPassword)
if err != nil {
- log.Debug("Failed to bind as BindDN[%s]: %v", ls.BindDN, err)
+ log.Debug("Failed to bind as BindDN[%s]: %v", source.BindDN, err)
return nil
}
- log.Trace("Bound as BindDN %s", ls.BindDN)
+ log.Trace("Bound as BindDN %s", source.BindDN)
} else {
log.Trace("Proceeding with anonymous LDAP search.")
}
- userDN, found = ls.findUserDN(l, name)
+ userDN, found = source.findUserDN(l, name)
if !found {
return nil
}
}
- if !ls.AttributesInBind {
+ if !source.AttributesInBind {
// binds user (checking password) before looking-up attributes in user context
err = bindUser(l, userDN, passwd)
if err != nil {
@@ -331,26 +330,26 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
}
}
- userFilter, ok := ls.sanitizedUserQuery(name)
+ userFilter, ok := source.sanitizedUserQuery(name)
if !ok {
return nil
}
- isAttributeSSHPublicKeySet := len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
- isAtributeAvatarSet := len(strings.TrimSpace(ls.AttributeAvatar)) > 0
+ isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
+ isAtributeAvatarSet := len(strings.TrimSpace(source.AttributeAvatar)) > 0
- attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}
- if len(strings.TrimSpace(ls.UserUID)) > 0 {
- attribs = append(attribs, ls.UserUID)
+ attribs := []string{source.AttributeUsername, source.AttributeName, source.AttributeSurname, source.AttributeMail}
+ if len(strings.TrimSpace(source.UserUID)) > 0 {
+ attribs = append(attribs, source.UserUID)
}
if isAttributeSSHPublicKeySet {
- attribs = append(attribs, ls.AttributeSSHPublicKey)
+ attribs = append(attribs, source.AttributeSSHPublicKey)
}
if isAtributeAvatarSet {
- attribs = append(attribs, ls.AttributeAvatar)
+ attribs = append(attribs, source.AttributeAvatar)
}
- log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, ls.AttributeAvatar, ls.UserUID, userFilter, userDN)
+ log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v', '%v' with filter '%s' and base '%s'", source.AttributeUsername, source.AttributeName, source.AttributeSurname, source.AttributeMail, source.AttributeSSHPublicKey, source.AttributeAvatar, source.UserUID, userFilter, userDN)
search := ldap.NewSearchRequest(
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
attribs, nil)
@@ -372,30 +371,30 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
var sshPublicKey []string
var Avatar []byte
- username := sr.Entries[0].GetAttributeValue(ls.AttributeUsername)
- firstname := sr.Entries[0].GetAttributeValue(ls.AttributeName)
- surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
- mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
- uid := sr.Entries[0].GetAttributeValue(ls.UserUID)
- if ls.UserUID == "dn" || ls.UserUID == "DN" {
+ username := sr.Entries[0].GetAttributeValue(source.AttributeUsername)
+ firstname := sr.Entries[0].GetAttributeValue(source.AttributeName)
+ surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname)
+ mail := sr.Entries[0].GetAttributeValue(source.AttributeMail)
+ uid := sr.Entries[0].GetAttributeValue(source.UserUID)
+ if source.UserUID == "dn" || source.UserUID == "DN" {
uid = sr.Entries[0].DN
}
// Check group membership
- if ls.GroupsEnabled && ls.GroupFilter != "" {
- groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter)
+ if source.GroupsEnabled && source.GroupFilter != "" {
+ groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
if !ok {
return nil
}
- groupDN, ok := ls.sanitizedGroupDN(ls.GroupDN)
+ groupDN, ok := source.sanitizedGroupDN(source.GroupDN)
if !ok {
return nil
}
- log.Trace("Fetching groups '%v' with filter '%s' and base '%s'", ls.GroupMemberUID, groupFilter, groupDN)
+ log.Trace("Fetching groups '%v' with filter '%s' and base '%s'", source.GroupMemberUID, groupFilter, groupDN)
groupSearch := ldap.NewSearchRequest(
groupDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter,
- []string{ls.GroupMemberUID},
+ []string{source.GroupMemberUID},
nil)
srg, err := l.Search(groupSearch)
@@ -410,8 +409,8 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
isMember := false
Entries:
for _, group := range srg.Entries {
- for _, member := range group.GetAttributeValues(ls.GroupMemberUID) {
- if (ls.UserUID == "dn" && member == sr.Entries[0].DN) || member == uid {
+ for _, member := range group.GetAttributeValues(source.GroupMemberUID) {
+ if (source.UserUID == "dn" && member == sr.Entries[0].DN) || member == uid {
isMember = true
break Entries
}
@@ -425,30 +424,30 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
}
if isAttributeSSHPublicKeySet {
- sshPublicKey = sr.Entries[0].GetAttributeValues(ls.AttributeSSHPublicKey)
+ sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey)
}
- isAdmin := checkAdmin(l, ls, userDN)
+ isAdmin := checkAdmin(l, source, userDN)
var isRestricted bool
if !isAdmin {
- isRestricted = checkRestricted(l, ls, userDN)
- }
-
- if !directBind && ls.AttributesInBind {
- // binds user (checking password) after looking-up attributes in BindDN context
- err = bindUser(l, userDN, passwd)
- if err != nil {
- return nil
- }
+ isRestricted = checkRestricted(l, source, userDN)
}
if isAtributeAvatarSet {
- Avatar = sr.Entries[0].GetRawAttributeValue(ls.AttributeAvatar)
+ Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar)
}
teamsToAdd := make(map[string][]string)
teamsToRemove := make(map[string][]string)
- if ls.GroupsEnabled && (ls.GroupTeamMap != "" || ls.GroupTeamMapRemoval) {
- teamsToAdd, teamsToRemove = ls.getMappedMemberships(l, uid)
+ if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
+ teamsToAdd, teamsToRemove = source.getMappedMemberships(l, uid)
+ }
+
+ if !directBind && source.AttributesInBind {
+ // binds user (checking password) after looking-up attributes in BindDN context
+ err = bindUser(l, userDN, passwd)
+ if err != nil {
+ return nil
+ }
}
return &SearchResult{
@@ -467,52 +466,52 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
}
// UsePagedSearch returns if need to use paged search
-func (ls *Source) UsePagedSearch() bool {
- return ls.SearchPageSize > 0
+func (source *Source) UsePagedSearch() bool {
+ return source.SearchPageSize > 0
}
// SearchEntries : search an LDAP source for all users matching userFilter
-func (ls *Source) SearchEntries() ([]*SearchResult, error) {
- l, err := dial(ls)
+func (source *Source) SearchEntries() ([]*SearchResult, error) {
+ l, err := dial(source)
if err != nil {
- log.Error("LDAP Connect error, %s:%v", ls.Host, err)
- ls.Enabled = false
+ log.Error("LDAP Connect error, %s:%v", source.Host, err)
+ source.Enabled = false
return nil, err
}
defer l.Close()
- if ls.BindDN != "" && ls.BindPassword != "" {
- err := l.Bind(ls.BindDN, ls.BindPassword)
+ if source.BindDN != "" && source.BindPassword != "" {
+ err := l.Bind(source.BindDN, source.BindPassword)
if err != nil {
- log.Debug("Failed to bind as BindDN[%s]: %v", ls.BindDN, err)
+ log.Debug("Failed to bind as BindDN[%s]: %v", source.BindDN, err)
return nil, err
}
- log.Trace("Bound as BindDN %s", ls.BindDN)
+ log.Trace("Bound as BindDN %s", source.BindDN)
} else {
log.Trace("Proceeding with anonymous LDAP search.")
}
- userFilter := fmt.Sprintf(ls.Filter, "*")
+ userFilter := fmt.Sprintf(source.Filter, "*")
- isAttributeSSHPublicKeySet := len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
- isAtributeAvatarSet := len(strings.TrimSpace(ls.AttributeAvatar)) > 0
+ isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0
+ isAtributeAvatarSet := len(strings.TrimSpace(source.AttributeAvatar)) > 0
- attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.UserUID}
+ attribs := []string{source.AttributeUsername, source.AttributeName, source.AttributeSurname, source.AttributeMail, source.UserUID}
if isAttributeSSHPublicKeySet {
- attribs = append(attribs, ls.AttributeSSHPublicKey)
+ attribs = append(attribs, source.AttributeSSHPublicKey)
}
if isAtributeAvatarSet {
- attribs = append(attribs, ls.AttributeAvatar)
+ attribs = append(attribs, source.AttributeAvatar)
}
- log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v' with filter %s and base %s", ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.AttributeSSHPublicKey, ls.AttributeAvatar, userFilter, ls.UserBase)
+ log.Trace("Fetching attributes '%v', '%v', '%v', '%v', '%v', '%v' with filter %s and base %s", source.AttributeUsername, source.AttributeName, source.AttributeSurname, source.AttributeMail, source.AttributeSSHPublicKey, source.AttributeAvatar, userFilter, source.UserBase)
search := ldap.NewSearchRequest(
- ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
+ source.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
attribs, nil)
var sr *ldap.SearchResult
- if ls.UsePagedSearch() {
- sr, err = l.SearchWithPaging(search, ls.SearchPageSize)
+ if source.UsePagedSearch() {
+ sr, err = l.SearchWithPaging(search, source.SearchPageSize)
} else {
sr, err = l.Search(search)
}
@@ -526,30 +525,30 @@ func (ls *Source) SearchEntries() ([]*SearchResult, error) {
for i, v := range sr.Entries {
teamsToAdd := make(map[string][]string)
teamsToRemove := make(map[string][]string)
- if ls.GroupsEnabled && (ls.GroupTeamMap != "" || ls.GroupTeamMapRemoval) {
- userAttributeListedInGroup := v.GetAttributeValue(ls.UserUID)
- if ls.UserUID == "dn" || ls.UserUID == "DN" {
+ if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
+ userAttributeListedInGroup := v.GetAttributeValue(source.UserUID)
+ if source.UserUID == "dn" || source.UserUID == "DN" {
userAttributeListedInGroup = v.DN
}
- teamsToAdd, teamsToRemove = ls.getMappedMemberships(l, userAttributeListedInGroup)
+ teamsToAdd, teamsToRemove = source.getMappedMemberships(l, userAttributeListedInGroup)
}
result[i] = &SearchResult{
- Username: v.GetAttributeValue(ls.AttributeUsername),
- Name: v.GetAttributeValue(ls.AttributeName),
- Surname: v.GetAttributeValue(ls.AttributeSurname),
- Mail: v.GetAttributeValue(ls.AttributeMail),
- IsAdmin: checkAdmin(l, ls, v.DN),
+ Username: v.GetAttributeValue(source.AttributeUsername),
+ Name: v.GetAttributeValue(source.AttributeName),
+ Surname: v.GetAttributeValue(source.AttributeSurname),
+ Mail: v.GetAttributeValue(source.AttributeMail),
+ IsAdmin: checkAdmin(l, source, v.DN),
LdapTeamAdd: teamsToAdd,
LdapTeamRemove: teamsToRemove,
}
if !result[i].IsAdmin {
- result[i].IsRestricted = checkRestricted(l, ls, v.DN)
+ result[i].IsRestricted = checkRestricted(l, source, v.DN)
}
if isAttributeSSHPublicKeySet {
- result[i].SSHPublicKey = v.GetAttributeValues(ls.AttributeSSHPublicKey)
+ result[i].SSHPublicKey = v.GetAttributeValues(source.AttributeSSHPublicKey)
}
if isAtributeAvatarSet {
- result[i].Avatar = v.GetRawAttributeValue(ls.AttributeAvatar)
+ result[i].Avatar = v.GetRawAttributeValue(source.AttributeAvatar)
}
result[i].LowerName = strings.ToLower(result[i].Username)
}
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index 65efed78c17c1..73e8309acaaa3 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package ldap
@@ -15,6 +14,7 @@ import (
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
user_service "code.gitea.io/gitea/services/user"
)
@@ -102,20 +102,21 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
log.Trace("SyncExternalUsers[%s]: Creating user %s", source.authSource.Name, su.Username)
usr = &user_model.User{
- LowerName: su.LowerName,
- Name: su.Username,
- FullName: fullName,
- LoginType: source.authSource.Type,
- LoginSource: source.authSource.ID,
- LoginName: su.Username,
- Email: su.Mail,
- IsAdmin: su.IsAdmin,
- IsRestricted: su.IsRestricted,
- IsActive: true,
+ LowerName: su.LowerName,
+ Name: su.Username,
+ FullName: fullName,
+ LoginType: source.authSource.Type,
+ LoginSource: source.authSource.ID,
+ LoginName: su.Username,
+ Email: su.Mail,
+ IsAdmin: su.IsAdmin,
+ }
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsRestricted: util.OptionalBoolOf(su.IsRestricted),
+ IsActive: util.OptionalBoolTrue,
}
- err = user_model.CreateUser(usr)
-
+ err = user_model.CreateUser(usr, overwriteDefault)
if err != nil {
log.Error("SyncExternalUsers[%s]: Error creating user %s: %v", source.authSource.Name, su.Username, err)
}
@@ -158,7 +159,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
}
usr.IsActive = true
- err = user_model.UpdateUser(usr, emailChanged, "full_name", "email", "is_admin", "is_restricted", "is_active")
+ err = user_model.UpdateUser(ctx, usr, emailChanged, "full_name", "email", "is_admin", "is_restricted", "is_active")
if err != nil {
log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.authSource.Name, usr.Name, err)
}
diff --git a/services/auth/source/ldap/util.go b/services/auth/source/ldap/util.go
index f27de37c87edb..bd11e2d1193fd 100644
--- a/services/auth/source/ldap/util.go
+++ b/services/auth/source/ldap/util.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package ldap
diff --git a/services/auth/source/oauth2/assert_interface_test.go b/services/auth/source/oauth2/assert_interface_test.go
index 0ec7361ca8360..56fe0e4aa810c 100644
--- a/services/auth/source/oauth2/assert_interface_test.go
+++ b/services/auth/source/oauth2/assert_interface_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2_test
diff --git a/services/auth/source/oauth2/init.go b/services/auth/source/oauth2/init.go
index e4eedd34cbdf9..32fe545c90657 100644
--- a/services/auth/source/oauth2/init.go
+++ b/services/auth/source/oauth2/init.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/jwtsigningkey.go b/services/auth/source/oauth2/jwtsigningkey.go
index 24f2c41119c16..93c15743798c2 100644
--- a/services/auth/source/oauth2/jwtsigningkey.go
+++ b/services/auth/source/oauth2/jwtsigningkey.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
@@ -31,11 +30,11 @@ import (
// ErrInvalidAlgorithmType represents an invalid algorithm error.
type ErrInvalidAlgorithmType struct {
- Algorightm string
+ Algorithm string
}
func (err ErrInvalidAlgorithmType) Error() string {
- return fmt.Sprintf("JWT signing algorithm is not supported: %s", err.Algorightm)
+ return fmt.Sprintf("JWT signing algorithm is not supported: %s", err.Algorithm)
}
// JWTSigningKey represents a algorithm/key pair to sign JWTs
@@ -339,7 +338,7 @@ func InitSigningKey() error {
}
if err != nil {
- return fmt.Errorf("Error while loading or creating JWT key: %v", err)
+ return fmt.Errorf("Error while loading or creating JWT key: %w", err)
}
signingKey, err := CreateJWTSigningKey(setting.OAuth2.JWTSigningAlgorithm, key)
@@ -364,7 +363,7 @@ func loadOrCreateSymmetricKey() (interface{}, error) {
return nil, err
}
- setting.CreateOrAppendToCustomConf(func(cfg *ini.File) {
+ setting.CreateOrAppendToCustomConf("oauth2.JWT_SECRET", func(cfg *ini.File) {
secretBase64 := base64.RawURLEncoding.EncodeToString(key)
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64)
})
diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go
index 45851c879990d..7ba370855a964 100644
--- a/services/auth/source/oauth2/providers.go
+++ b/services/auth/source/oauth2/providers.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
@@ -35,7 +34,7 @@ type GothProvider interface {
GothProviderCreator
}
-// ImagedProvider provide an overrided image setting for the provider
+// ImagedProvider provide an overridden image setting for the provider
type ImagedProvider struct {
GothProvider
image string
diff --git a/services/auth/source/oauth2/providers_base.go b/services/auth/source/oauth2/providers_base.go
index b6b6d0bbd250d..61654d8eaa940 100644
--- a/services/auth/source/oauth2/providers_base.go
+++ b/services/auth/source/oauth2/providers_base.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/providers_custom.go b/services/auth/source/oauth2/providers_custom.go
index c3ebdf9df0af7..b5fe1794418d2 100644
--- a/services/auth/source/oauth2/providers_custom.go
+++ b/services/auth/source/oauth2/providers_custom.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/providers_openid.go b/services/auth/source/oauth2/providers_openid.go
index 80ee56d4a46a5..2433cd3c7f10f 100644
--- a/services/auth/source/oauth2/providers_openid.go
+++ b/services/auth/source/oauth2/providers_openid.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/providers_simple.go b/services/auth/source/oauth2/providers_simple.go
index 39d3d74f6ddc1..731c1b59a95eb 100644
--- a/services/auth/source/oauth2/providers_simple.go
+++ b/services/auth/source/oauth2/providers_simple.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index 457686ba1fd4d..0abebc04ecfd2 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/source_authenticate.go b/services/auth/source/oauth2/source_authenticate.go
index fdc18411a75d5..e3e2a9e192f55 100644
--- a/services/auth/source/oauth2/source_authenticate.go
+++ b/services/auth/source/oauth2/source_authenticate.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/source_callout.go b/services/auth/source/oauth2/source_callout.go
index 7447e5d453a56..8d70bee248a11 100644
--- a/services/auth/source/oauth2/source_callout.go
+++ b/services/auth/source/oauth2/source_callout.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/source_name.go b/services/auth/source/oauth2/source_name.go
index 0b794ad650975..eee789eff73ba 100644
--- a/services/auth/source/oauth2/source_name.go
+++ b/services/auth/source/oauth2/source_name.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/source_register.go b/services/auth/source/oauth2/source_register.go
index f61de7e1d69c9..3527d54b65c59 100644
--- a/services/auth/source/oauth2/source_register.go
+++ b/services/auth/source/oauth2/source_register.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/store.go b/services/auth/source/oauth2/store.go
index f00264f997d27..394bf99463594 100644
--- a/services/auth/source/oauth2/store.go
+++ b/services/auth/source/oauth2/store.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/token.go b/services/auth/source/oauth2/token.go
index 0c69913ff4890..c5a064054e5a2 100644
--- a/services/auth/source/oauth2/token.go
+++ b/services/auth/source/oauth2/token.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/oauth2/urlmapping.go b/services/auth/source/oauth2/urlmapping.go
index 43c8dde9a5dc4..d0442d58a8b49 100644
--- a/services/auth/source/oauth2/urlmapping.go
+++ b/services/auth/source/oauth2/urlmapping.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package oauth2
diff --git a/services/auth/source/pam/assert_interface_test.go b/services/auth/source/pam/assert_interface_test.go
index d8754cdf77e1c..8e7648b8d3cbc 100644
--- a/services/auth/source/pam/assert_interface_test.go
+++ b/services/auth/source/pam/assert_interface_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package pam_test
diff --git a/services/auth/source/pam/source.go b/services/auth/source/pam/source.go
index 957c89dc85e53..96b182e1852c5 100644
--- a/services/auth/source/pam/source.go
+++ b/services/auth/source/pam/source.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package pam
diff --git a/services/auth/source/pam/source_authenticate.go b/services/auth/source/pam/source_authenticate.go
index d5bd9409963f0..48cd905a0a5e6 100644
--- a/services/auth/source/pam/source_authenticate.go
+++ b/services/auth/source/pam/source_authenticate.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package pam
@@ -12,6 +11,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/pam"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/mailer"
"github.com/google/uuid"
@@ -58,10 +58,12 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
LoginType: auth.PAM,
LoginSource: source.authSource.ID,
LoginName: userName, // This is what the user typed in
- IsActive: true,
+ }
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsActive: util.OptionalBoolTrue,
}
- if err := user_model.CreateUser(user); err != nil {
+ if err := user_model.CreateUser(user, overwriteDefault); err != nil {
return user, err
}
diff --git a/services/auth/source/smtp/assert_interface_test.go b/services/auth/source/smtp/assert_interface_test.go
index c7fae6431fa18..6c9cde66e14fc 100644
--- a/services/auth/source/smtp/assert_interface_test.go
+++ b/services/auth/source/smtp/assert_interface_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package smtp_test
diff --git a/services/auth/source/smtp/auth.go b/services/auth/source/smtp/auth.go
index 8d0cbb11cdc9a..6446fcd70600d 100644
--- a/services/auth/source/smtp/auth.go
+++ b/services/auth/source/smtp/auth.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package smtp
@@ -95,7 +94,7 @@ func Authenticate(a smtp.Auth, source *Source) error {
hasStartTLS, _ := client.Extension("STARTTLS")
if !source.UseTLS() && hasStartTLS {
if err = client.StartTLS(tlsConfig); err != nil {
- return fmt.Errorf("failed to start StartTLS: %v", err)
+ return fmt.Errorf("failed to start StartTLS: %w", err)
}
}
diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go
index 5e69f912da35b..2a648e421e4ec 100644
--- a/services/auth/source/smtp/source.go
+++ b/services/auth/source/smtp/source.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package smtp
diff --git a/services/auth/source/smtp/source_authenticate.go b/services/auth/source/smtp/source_authenticate.go
index 3be2f1128de2f..36b351198acba 100644
--- a/services/auth/source/smtp/source_authenticate.go
+++ b/services/auth/source/smtp/source_authenticate.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package smtp
@@ -24,7 +23,7 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
idx := strings.Index(userName, "@")
if idx == -1 {
return nil, user_model.ErrUserNotExist{Name: userName}
- } else if !util.IsStringInSlice(userName[idx+1:], strings.Split(source.AllowedDomains, ","), true) {
+ } else if !util.SliceContainsString(strings.Split(source.AllowedDomains, ","), userName[idx+1:], true) {
return nil, user_model.ErrUserNotExist{Name: userName}
}
}
@@ -74,10 +73,12 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
LoginType: auth_model.SMTP,
LoginSource: source.authSource.ID,
LoginName: userName,
- IsActive: true,
+ }
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsActive: util.OptionalBoolTrue,
}
- if err := user_model.CreateUser(user); err != nil {
+ if err := user_model.CreateUser(user, overwriteDefault); err != nil {
return user, err
}
diff --git a/services/auth/source/sspi/assert_interface_test.go b/services/auth/source/sspi/assert_interface_test.go
index 33442451867b9..03d836dd6f86e 100644
--- a/services/auth/source/sspi/assert_interface_test.go
+++ b/services/auth/source/sspi/assert_interface_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package sspi_test
diff --git a/services/auth/source/sspi/source.go b/services/auth/source/sspi/source.go
index e6e63ee1eb8ef..bdd6ef451cc80 100644
--- a/services/auth/source/sspi/source.go
+++ b/services/auth/source/sspi/source.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package sspi
diff --git a/services/auth/sspi_windows.go b/services/auth/sspi_windows.go
index 63e70e61d4335..045834b6911ad 100644
--- a/services/auth/sspi_windows.go
+++ b/services/auth/sspi_windows.go
@@ -1,10 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
import (
+ "context"
"errors"
"net/http"
"strings"
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/auth/source/sspi"
"code.gitea.io/gitea/services/mailer"
@@ -51,21 +52,14 @@ type SSPI struct {
}
// Init creates a new global websspi.Authenticator object
-func (s *SSPI) Init() error {
+func (s *SSPI) Init(ctx context.Context) error {
config := websspi.NewConfig()
var err error
sspiAuth, err = websspi.New(config)
if err != nil {
return err
}
- s.rnd = render.New(render.Options{
- Extensions: []string{".tmpl"},
- Directory: "templates",
- Funcs: templates.NewFuncMap(),
- Asset: templates.GetAsset,
- AssetNames: templates.GetAssetNames,
- IsDevelopment: !setting.IsProd,
- })
+ _, s.rnd = templates.HTMLRenderer(ctx)
return nil
}
@@ -83,15 +77,15 @@ func (s *SSPI) Free() error {
// If authentication is successful, returns the corresponding user object.
// If negotiation should continue or authentication fails, immediately returns a 401 HTTP
// response code, as required by the SPNEGO protocol.
-func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *user_model.User {
+func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
if !s.shouldAuthenticate(req) {
- return nil
+ return nil, nil
}
cfg, err := s.getConfig()
if err != nil {
log.Error("could not get SSPI config: %v", err)
- return nil
+ return nil, err
}
log.Trace("SSPI Authorization: Attempting to authenticate")
@@ -114,7 +108,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
log.Error("%v", err)
}
- return nil
+ return nil, err
}
if outToken != "" {
sspiAuth.AppendAuthenticateHeader(w, outToken)
@@ -122,24 +116,24 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
username := sanitizeUsername(userInfo.Username, cfg)
if len(username) == 0 {
- return nil
+ return nil, nil
}
log.Info("Authenticated as %s\n", username)
- user, err := user_model.GetUserByName(username)
+ user, err := user_model.GetUserByName(req.Context(), username)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByName: %v", err)
- return nil
+ return nil, err
}
if !cfg.AutoCreateUsers {
log.Error("User '%s' not found", username)
- return nil
+ return nil, nil
}
user, err = s.newUser(username, cfg)
if err != nil {
log.Error("CreateUser: %v", err)
- return nil
+ return nil, err
}
}
@@ -149,7 +143,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
}
log.Trace("SSPI Authorization: Logged in user %-v", user)
- return user
+ return user, nil
}
// getConfig retrieves the SSPI configuration from login sources
@@ -179,7 +173,7 @@ func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) {
} else if middleware.IsAPIPath(req) || isAttachmentDownload(req) {
shouldAuth = true
}
- return
+ return shouldAuth
}
// newUser creates a new user object for the purpose of automatic registration
@@ -187,17 +181,20 @@ func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) {
func (s *SSPI) newUser(username string, cfg *sspi.Source) (*user_model.User, error) {
email := gouuid.New().String() + "@localhost.localdomain"
user := &user_model.User{
- Name: username,
- Email: email,
- KeepEmailPrivate: true,
- Passwd: gouuid.New().String(),
- IsActive: cfg.AutoActivateUsers,
- Language: cfg.DefaultLanguage,
- UseCustomAvatar: true,
- Avatar: avatars.DefaultAvatarLink(),
- EmailNotificationsPreference: user_model.EmailNotificationsDisabled,
- }
- if err := user_model.CreateUser(user); err != nil {
+ Name: username,
+ Email: email,
+ Passwd: gouuid.New().String(),
+ Language: cfg.DefaultLanguage,
+ UseCustomAvatar: true,
+ Avatar: avatars.DefaultAvatarLink(),
+ }
+ emailNotificationPreference := user_model.EmailNotificationsDisabled
+ overwriteDefault := &user_model.CreateUserOverwriteOptions{
+ IsActive: util.OptionalBoolOf(cfg.AutoActivateUsers),
+ KeepEmailPrivate: util.OptionalBoolTrue,
+ EmailNotificationsPreference: &emailNotificationPreference,
+ }
+ if err := user_model.CreateUser(user, overwriteDefault); err != nil {
return nil, err
}
diff --git a/services/auth/sync.go b/services/auth/sync.go
index b7f3232a30470..e42e8a51a755b 100644
--- a/services/auth/sync.go
+++ b/services/auth/sync.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package auth
diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go
new file mode 100644
index 0000000000000..15d94e79209eb
--- /dev/null
+++ b/services/automerge/automerge.go
@@ -0,0 +1,263 @@
+// Copyright 2021 Gitea. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package automerge
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ pull_model "code.gitea.io/gitea/models/pull"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/graceful"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/process"
+ "code.gitea.io/gitea/modules/queue"
+ pull_service "code.gitea.io/gitea/services/pull"
+)
+
+// prAutoMergeQueue represents a queue to handle update pull request tests
+var prAutoMergeQueue queue.UniqueQueue
+
+// Init runs the task queue to that handles auto merges
+func Init() error {
+ prAutoMergeQueue = queue.CreateUniqueQueue("pr_auto_merge", handle, "")
+ if prAutoMergeQueue == nil {
+ return fmt.Errorf("Unable to create pr_auto_merge Queue")
+ }
+ go graceful.GetManager().RunWithShutdownFns(prAutoMergeQueue.Run)
+ return nil
+}
+
+// handle passed PR IDs and test the PRs
+func handle(data ...queue.Data) []queue.Data {
+ for _, d := range data {
+ var id int64
+ var sha string
+ if _, err := fmt.Sscanf(d.(string), "%d_%s", &id, &sha); err != nil {
+ log.Error("could not parse data from pr_auto_merge queue (%v): %v", d, err)
+ continue
+ }
+ handlePull(id, sha)
+ }
+ return nil
+}
+
+func addToQueue(pr *issues_model.PullRequest, sha string) {
+ if err := prAutoMergeQueue.PushFunc(fmt.Sprintf("%d_%s", pr.ID, sha), func() error {
+ log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
+ return nil
+ }); err != nil {
+ log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
+ }
+}
+
+// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
+func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string) (scheduled bool, err error) {
+ err = db.WithTx(ctx, func(ctx context.Context) error {
+ lastCommitStatus, err := pull_service.GetPullRequestCommitStatusState(ctx, pull)
+ if err != nil {
+ return err
+ }
+
+ // we don't need to schedule
+ if lastCommitStatus.IsSuccess() {
+ return nil
+ }
+
+ if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message); err != nil {
+ return err
+ }
+ scheduled = true
+
+ _, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer)
+ return err
+ })
+ return scheduled, err
+}
+
+// RemoveScheduledAutoMerge cancels a previously scheduled pull request
+func RemoveScheduledAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest) error {
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ if err := pull_model.DeleteScheduledAutoMerge(ctx, pull.ID); err != nil {
+ return err
+ }
+
+ _, err := issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRUnScheduledToAutoMerge, pull, doer)
+ return err
+ })
+}
+
+// MergeScheduledPullRequest merges a previously scheduled pull request when all checks succeeded
+func MergeScheduledPullRequest(ctx context.Context, sha string, repo *repo_model.Repository) error {
+ pulls, err := getPullRequestsByHeadSHA(ctx, sha, repo, func(pr *issues_model.PullRequest) bool {
+ return !pr.HasMerged && pr.CanAutoMerge()
+ })
+ if err != nil {
+ return err
+ }
+
+ for _, pr := range pulls {
+ addToQueue(pr, sha)
+ }
+
+ return nil
+}
+
+func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
+ gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
+ if err != nil {
+ return nil, err
+ }
+ defer gitRepo.Close()
+
+ refs, err := gitRepo.GetRefsBySha(sha, "")
+ if err != nil {
+ return nil, err
+ }
+
+ pulls := make(map[int64]*issues_model.PullRequest)
+
+ for _, ref := range refs {
+ // Each pull branch starts with refs/pull/ we then go from there to find the index of the pr and then
+ // use that to get the pr.
+ if strings.HasPrefix(ref, git.PullPrefix) {
+ parts := strings.Split(ref[len(git.PullPrefix):], "/")
+
+ // e.g. 'refs/pull/1/head' would be []string{"1", "head"}
+ if len(parts) != 2 {
+ log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
+ continue
+ }
+
+ prIndex, err := strconv.ParseInt(parts[0], 10, 64)
+ if err != nil {
+ log.Error("getPullRequestsByHeadSHA found broken pull ref [%s] on repo [%-v]", ref, repo)
+ continue
+ }
+
+ p, err := issues_model.GetPullRequestByIndex(ctx, repo.ID, prIndex)
+ if err != nil {
+ // If there is no pull request for this branch, we don't try to merge it.
+ if issues_model.IsErrPullRequestNotExist(err) {
+ continue
+ }
+ return nil, err
+ }
+
+ if filter(p) {
+ pulls[p.ID] = p
+ }
+ }
+ }
+
+ return pulls, nil
+}
+
+func handlePull(pullID int64, sha string) {
+ ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(),
+ fmt.Sprintf("Handle AutoMerge of pull[%d] with sha[%s]", pullID, sha))
+ defer finished()
+
+ pr, err := issues_model.GetPullRequestByID(ctx, pullID)
+ if err != nil {
+ log.Error("GetPullRequestByID[%d]: %v", pullID, err)
+ return
+ }
+
+ // Check if there is a scheduled pr in the db
+ exists, scheduledPRM, err := pull_model.GetScheduledMergeByPullID(ctx, pr.ID)
+ if err != nil {
+ log.Error("pull[%d] GetScheduledMergeByPullID: %v", pr.ID, err)
+ return
+ }
+ if !exists {
+ return
+ }
+
+ // Get all checks for this pr
+ // We get the latest sha commit hash again to handle the case where the check of a previous push
+ // did not succeed or was not finished yet.
+
+ if err = pr.LoadHeadRepo(ctx); err != nil {
+ log.Error("pull[%d] LoadHeadRepo: %v", pr.ID, err)
+ return
+ }
+
+ headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
+ if err != nil {
+ log.Error("OpenRepository: %v", err)
+ return
+ }
+ defer headGitRepo.Close()
+
+ headBranchExist := headGitRepo.IsBranchExist(pr.HeadBranch)
+
+ if pr.HeadRepo == nil || !headBranchExist {
+ log.Warn("Head branch of auto merge pr does not exist [HeadRepoID: %d, Branch: %s, PR ID: %d]", pr.HeadRepoID, pr.HeadBranch, pr.ID)
+ return
+ }
+
+ // Check if all checks succeeded
+ pass, err := pull_service.IsPullCommitStatusPass(ctx, pr)
+ if err != nil {
+ log.Error("IsPullCommitStatusPass: %v", err)
+ return
+ }
+ if !pass {
+ log.Info("Scheduled auto merge pr has unsuccessful status checks [PullID: %d]", pr.ID)
+ return
+ }
+
+ // Merge if all checks succeeded
+ doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
+ if err != nil {
+ log.Error("GetUserByIDCtx: %v", err)
+ return
+ }
+
+ perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer)
+ if err != nil {
+ log.Error("GetUserRepoPermission: %v", err)
+ return
+ }
+
+ if err := pull_service.CheckPullMergable(ctx, doer, &perm, pr, false, false); err != nil {
+ if errors.Is(pull_service.ErrUserNotAllowedToMerge, err) {
+ log.Info("PR %d was scheduled to automerge by an unauthorized user", pr.ID)
+ return
+ }
+ log.Error("pull[%d] CheckPullMergable: %v", pr.ID, err)
+ return
+ }
+
+ var baseGitRepo *git.Repository
+ if pr.BaseRepoID == pr.HeadRepoID {
+ baseGitRepo = headGitRepo
+ } else {
+ if err = pr.LoadBaseRepo(ctx); err != nil {
+ log.Error("LoadBaseRepo: %v", err)
+ return
+ }
+
+ baseGitRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
+ if err != nil {
+ log.Error("OpenRepository: %v", err)
+ return
+ }
+ defer baseGitRepo.Close()
+ }
+
+ if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil {
+ log.Error("pull_service.Merge: %v", err)
+ return
+ }
+}
diff --git a/services/comments/comments.go b/services/comments/comments.go
deleted file mode 100644
index c1b3ab73c9f57..0000000000000
--- a/services/comments/comments.go
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright 2019 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package comments
-
-import (
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/notification"
- "code.gitea.io/gitea/modules/timeutil"
-)
-
-// CreateIssueComment creates a plain issue comment.
-func CreateIssueComment(doer *user_model.User, repo *repo_model.Repository, issue *models.Issue, content string, attachments []string) (*models.Comment, error) {
- comment, err := models.CreateComment(&models.CreateCommentOptions{
- Type: models.CommentTypeComment,
- Doer: doer,
- Repo: repo,
- Issue: issue,
- Content: content,
- Attachments: attachments,
- })
- if err != nil {
- return nil, err
- }
-
- mentions, err := models.FindAndUpdateIssueMentions(db.DefaultContext, issue, doer, comment.Content)
- if err != nil {
- return nil, err
- }
-
- notification.NotifyCreateIssueComment(doer, repo, issue, comment, mentions)
-
- return comment, nil
-}
-
-// UpdateComment updates information of comment.
-func UpdateComment(c *models.Comment, doer *user_model.User, oldContent string) error {
- needsContentHistory := c.Content != oldContent &&
- (c.Type == models.CommentTypeComment || c.Type == models.CommentTypeReview || c.Type == models.CommentTypeCode)
- if needsContentHistory {
- hasContentHistory, err := issues.HasIssueContentHistory(db.DefaultContext, c.IssueID, c.ID)
- if err != nil {
- return err
- }
- if !hasContentHistory {
- if err = issues.SaveIssueContentHistory(db.GetEngine(db.DefaultContext), c.PosterID, c.IssueID, c.ID,
- c.CreatedUnix, oldContent, true); err != nil {
- return err
- }
- }
- }
-
- if err := models.UpdateComment(c, doer); err != nil {
- return err
- }
-
- if needsContentHistory {
- err := issues.SaveIssueContentHistory(db.GetEngine(db.DefaultContext), doer.ID, c.IssueID, c.ID, timeutil.TimeStampNow(), c.Content, false)
- if err != nil {
- return err
- }
- }
-
- notification.NotifyUpdateComment(doer, c, oldContent)
-
- return nil
-}
-
-// DeleteComment deletes the comment
-func DeleteComment(doer *user_model.User, comment *models.Comment) error {
- if err := models.DeleteComment(comment); err != nil {
- return err
- }
-
- notification.NotifyDeleteComment(doer, comment)
-
- return nil
-}
diff --git a/services/context/user.go b/services/context/user.go
index c5efd43782aef..9dc84c3ac15e1 100644
--- a/services/context/user.go
+++ b/services/context/user.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package context
@@ -44,7 +43,7 @@ func userAssignment(ctx *context.Context, errCb func(int, string, interface{}))
ctx.ContextUser = ctx.Doer
} else {
var err error
- ctx.ContextUser, err = user_model.GetUserByName(username)
+ ctx.ContextUser, err = user_model.GetUserByName(ctx, username)
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err := user_model.LookupUserRedirect(username); err == nil {
diff --git a/services/convert/attachment.go b/services/convert/attachment.go
new file mode 100644
index 0000000000000..ddba181a1204f
--- /dev/null
+++ b/services/convert/attachment.go
@@ -0,0 +1,30 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ repo_model "code.gitea.io/gitea/models/repo"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToAttachment converts models.Attachment to api.Attachment
+func ToAttachment(a *repo_model.Attachment) *api.Attachment {
+ return &api.Attachment{
+ ID: a.ID,
+ Name: a.Name,
+ Created: a.CreatedUnix.AsTime(),
+ DownloadCount: a.DownloadCount,
+ Size: a.Size,
+ UUID: a.UUID,
+ DownloadURL: a.DownloadURL(),
+ }
+}
+
+func ToAttachments(attachments []*repo_model.Attachment) []*api.Attachment {
+ converted := make([]*api.Attachment, 0, len(attachments))
+ for _, attachment := range attachments {
+ converted = append(converted, ToAttachment(attachment))
+ }
+ return converted
+}
diff --git a/services/convert/convert.go b/services/convert/convert.go
new file mode 100644
index 0000000000000..17f7e3d650679
--- /dev/null
+++ b/services/convert/convert.go
@@ -0,0 +1,431 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/gitdiff"
+)
+
+// ToEmail convert models.EmailAddress to api.Email
+func ToEmail(email *user_model.EmailAddress) *api.Email {
+ return &api.Email{
+ Email: email.Email,
+ Verified: email.IsActivated,
+ Primary: email.IsPrimary,
+ }
+}
+
+// ToBranch convert a git.Commit and git.Branch to an api.Branch
+func ToBranch(repo *repo_model.Repository, b *git.Branch, c *git.Commit, bp *git_model.ProtectedBranch, user *user_model.User, isRepoAdmin bool) (*api.Branch, error) {
+ if bp == nil {
+ var hasPerm bool
+ var canPush bool
+ var err error
+ if user != nil {
+ hasPerm, err = access_model.HasAccessUnit(db.DefaultContext, user, repo, unit.TypeCode, perm.AccessModeWrite)
+ if err != nil {
+ return nil, err
+ }
+
+ perms, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
+ if err != nil {
+ return nil, err
+ }
+ canPush = issues_model.CanMaintainerWriteToBranch(perms, b.Name, user)
+ }
+
+ return &api.Branch{
+ Name: b.Name,
+ Commit: ToPayloadCommit(repo, c),
+ Protected: false,
+ RequiredApprovals: 0,
+ EnableStatusCheck: false,
+ StatusCheckContexts: []string{},
+ UserCanPush: canPush,
+ UserCanMerge: hasPerm,
+ }, nil
+ }
+
+ branch := &api.Branch{
+ Name: b.Name,
+ Commit: ToPayloadCommit(repo, c),
+ Protected: true,
+ RequiredApprovals: bp.RequiredApprovals,
+ EnableStatusCheck: bp.EnableStatusCheck,
+ StatusCheckContexts: bp.StatusCheckContexts,
+ }
+
+ if isRepoAdmin {
+ branch.EffectiveBranchProtectionName = bp.RuleName
+ }
+
+ if user != nil {
+ permission, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
+ if err != nil {
+ return nil, err
+ }
+ bp.Repo = repo
+ branch.UserCanPush = bp.CanUserPush(db.DefaultContext, user)
+ branch.UserCanMerge = git_model.IsUserMergeWhitelisted(db.DefaultContext, bp, user.ID, permission)
+ }
+
+ return branch, nil
+}
+
+// ToBranchProtection convert a ProtectedBranch to api.BranchProtection
+func ToBranchProtection(bp *git_model.ProtectedBranch) *api.BranchProtection {
+ pushWhitelistUsernames, err := user_model.GetUserNamesByIDs(bp.WhitelistUserIDs)
+ if err != nil {
+ log.Error("GetUserNamesByIDs (WhitelistUserIDs): %v", err)
+ }
+ mergeWhitelistUsernames, err := user_model.GetUserNamesByIDs(bp.MergeWhitelistUserIDs)
+ if err != nil {
+ log.Error("GetUserNamesByIDs (MergeWhitelistUserIDs): %v", err)
+ }
+ approvalsWhitelistUsernames, err := user_model.GetUserNamesByIDs(bp.ApprovalsWhitelistUserIDs)
+ if err != nil {
+ log.Error("GetUserNamesByIDs (ApprovalsWhitelistUserIDs): %v", err)
+ }
+ pushWhitelistTeams, err := organization.GetTeamNamesByID(bp.WhitelistTeamIDs)
+ if err != nil {
+ log.Error("GetTeamNamesByID (WhitelistTeamIDs): %v", err)
+ }
+ mergeWhitelistTeams, err := organization.GetTeamNamesByID(bp.MergeWhitelistTeamIDs)
+ if err != nil {
+ log.Error("GetTeamNamesByID (MergeWhitelistTeamIDs): %v", err)
+ }
+ approvalsWhitelistTeams, err := organization.GetTeamNamesByID(bp.ApprovalsWhitelistTeamIDs)
+ if err != nil {
+ log.Error("GetTeamNamesByID (ApprovalsWhitelistTeamIDs): %v", err)
+ }
+
+ branchName := ""
+ if !git_model.IsRuleNameSpecial(bp.RuleName) {
+ branchName = bp.RuleName
+ }
+
+ return &api.BranchProtection{
+ BranchName: branchName,
+ RuleName: bp.RuleName,
+ EnablePush: bp.CanPush,
+ EnablePushWhitelist: bp.EnableWhitelist,
+ PushWhitelistUsernames: pushWhitelistUsernames,
+ PushWhitelistTeams: pushWhitelistTeams,
+ PushWhitelistDeployKeys: bp.WhitelistDeployKeys,
+ EnableMergeWhitelist: bp.EnableMergeWhitelist,
+ MergeWhitelistUsernames: mergeWhitelistUsernames,
+ MergeWhitelistTeams: mergeWhitelistTeams,
+ EnableStatusCheck: bp.EnableStatusCheck,
+ StatusCheckContexts: bp.StatusCheckContexts,
+ RequiredApprovals: bp.RequiredApprovals,
+ EnableApprovalsWhitelist: bp.EnableApprovalsWhitelist,
+ ApprovalsWhitelistUsernames: approvalsWhitelistUsernames,
+ ApprovalsWhitelistTeams: approvalsWhitelistTeams,
+ BlockOnRejectedReviews: bp.BlockOnRejectedReviews,
+ BlockOnOfficialReviewRequests: bp.BlockOnOfficialReviewRequests,
+ BlockOnOutdatedBranch: bp.BlockOnOutdatedBranch,
+ DismissStaleApprovals: bp.DismissStaleApprovals,
+ RequireSignedCommits: bp.RequireSignedCommits,
+ ProtectedFilePatterns: bp.ProtectedFilePatterns,
+ UnprotectedFilePatterns: bp.UnprotectedFilePatterns,
+ Created: bp.CreatedUnix.AsTime(),
+ Updated: bp.UpdatedUnix.AsTime(),
+ }
+}
+
+// ToTag convert a git.Tag to an api.Tag
+func ToTag(repo *repo_model.Repository, t *git.Tag) *api.Tag {
+ return &api.Tag{
+ Name: t.Name,
+ Message: strings.TrimSpace(t.Message),
+ ID: t.ID.String(),
+ Commit: ToCommitMeta(repo, t),
+ ZipballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip"),
+ TarballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz"),
+ }
+}
+
+// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
+func ToVerification(c *git.Commit) *api.PayloadCommitVerification {
+ verif := asymkey_model.ParseCommitWithSignature(c)
+ commitVerification := &api.PayloadCommitVerification{
+ Verified: verif.Verified,
+ Reason: verif.Reason,
+ }
+ if c.Signature != nil {
+ commitVerification.Signature = c.Signature.Signature
+ commitVerification.Payload = c.Signature.Payload
+ }
+ if verif.SigningUser != nil {
+ commitVerification.Signer = &api.PayloadUser{
+ Name: verif.SigningUser.Name,
+ Email: verif.SigningUser.Email,
+ }
+ }
+ return commitVerification
+}
+
+// ToPublicKey convert asymkey_model.PublicKey to api.PublicKey
+func ToPublicKey(apiLink string, key *asymkey_model.PublicKey) *api.PublicKey {
+ return &api.PublicKey{
+ ID: key.ID,
+ Key: key.Content,
+ URL: fmt.Sprintf("%s%d", apiLink, key.ID),
+ Title: key.Name,
+ Fingerprint: key.Fingerprint,
+ Created: key.CreatedUnix.AsTime(),
+ }
+}
+
+// ToGPGKey converts models.GPGKey to api.GPGKey
+func ToGPGKey(key *asymkey_model.GPGKey) *api.GPGKey {
+ subkeys := make([]*api.GPGKey, len(key.SubsKey))
+ for id, k := range key.SubsKey {
+ subkeys[id] = &api.GPGKey{
+ ID: k.ID,
+ PrimaryKeyID: k.PrimaryKeyID,
+ KeyID: k.KeyID,
+ PublicKey: k.Content,
+ Created: k.CreatedUnix.AsTime(),
+ Expires: k.ExpiredUnix.AsTime(),
+ CanSign: k.CanSign,
+ CanEncryptComms: k.CanEncryptComms,
+ CanEncryptStorage: k.CanEncryptStorage,
+ CanCertify: k.CanSign,
+ Verified: k.Verified,
+ }
+ }
+ emails := make([]*api.GPGKeyEmail, len(key.Emails))
+ for i, e := range key.Emails {
+ emails[i] = ToGPGKeyEmail(e)
+ }
+ return &api.GPGKey{
+ ID: key.ID,
+ PrimaryKeyID: key.PrimaryKeyID,
+ KeyID: key.KeyID,
+ PublicKey: key.Content,
+ Created: key.CreatedUnix.AsTime(),
+ Expires: key.ExpiredUnix.AsTime(),
+ Emails: emails,
+ SubsKey: subkeys,
+ CanSign: key.CanSign,
+ CanEncryptComms: key.CanEncryptComms,
+ CanEncryptStorage: key.CanEncryptStorage,
+ CanCertify: key.CanSign,
+ Verified: key.Verified,
+ }
+}
+
+// ToGPGKeyEmail convert models.EmailAddress to api.GPGKeyEmail
+func ToGPGKeyEmail(email *user_model.EmailAddress) *api.GPGKeyEmail {
+ return &api.GPGKeyEmail{
+ Email: email.Email,
+ Verified: email.IsActivated,
+ }
+}
+
+// ToGitHook convert git.Hook to api.GitHook
+func ToGitHook(h *git.Hook) *api.GitHook {
+ return &api.GitHook{
+ Name: h.Name(),
+ IsActive: h.IsActive,
+ Content: h.Content,
+ }
+}
+
+// ToDeployKey convert asymkey_model.DeployKey to api.DeployKey
+func ToDeployKey(apiLink string, key *asymkey_model.DeployKey) *api.DeployKey {
+ return &api.DeployKey{
+ ID: key.ID,
+ KeyID: key.KeyID,
+ Key: key.Content,
+ Fingerprint: key.Fingerprint,
+ URL: fmt.Sprintf("%s%d", apiLink, key.ID),
+ Title: key.Name,
+ Created: key.CreatedUnix.AsTime(),
+ ReadOnly: key.Mode == perm.AccessModeRead, // All deploy keys are read-only.
+ }
+}
+
+// ToOrganization convert user_model.User to api.Organization
+func ToOrganization(org *organization.Organization) *api.Organization {
+ return &api.Organization{
+ ID: org.ID,
+ AvatarURL: org.AsUser().AvatarLink(),
+ Name: org.Name,
+ UserName: org.Name,
+ FullName: org.FullName,
+ Description: org.Description,
+ Website: org.Website,
+ Location: org.Location,
+ Visibility: org.Visibility.String(),
+ RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess,
+ }
+}
+
+// ToTeam convert models.Team to api.Team
+func ToTeam(team *organization.Team, loadOrg ...bool) (*api.Team, error) {
+ teams, err := ToTeams([]*organization.Team{team}, len(loadOrg) != 0 && loadOrg[0])
+ if err != nil || len(teams) == 0 {
+ return nil, err
+ }
+ return teams[0], nil
+}
+
+// ToTeams convert models.Team list to api.Team list
+func ToTeams(teams []*organization.Team, loadOrgs bool) ([]*api.Team, error) {
+ if len(teams) == 0 || teams[0] == nil {
+ return nil, nil
+ }
+
+ cache := make(map[int64]*api.Organization)
+ apiTeams := make([]*api.Team, len(teams))
+ for i := range teams {
+ if err := teams[i].GetUnits(); err != nil {
+ return nil, err
+ }
+
+ apiTeams[i] = &api.Team{
+ ID: teams[i].ID,
+ Name: teams[i].Name,
+ Description: teams[i].Description,
+ IncludesAllRepositories: teams[i].IncludesAllRepositories,
+ CanCreateOrgRepo: teams[i].CanCreateOrgRepo,
+ Permission: teams[i].AccessMode.String(),
+ Units: teams[i].GetUnitNames(),
+ UnitsMap: teams[i].GetUnitsMap(),
+ }
+
+ if loadOrgs {
+ apiOrg, ok := cache[teams[i].OrgID]
+ if !ok {
+ org, err := organization.GetOrgByID(db.DefaultContext, teams[i].OrgID)
+ if err != nil {
+ return nil, err
+ }
+ apiOrg = ToOrganization(org)
+ cache[teams[i].OrgID] = apiOrg
+ }
+ apiTeams[i].Organization = apiOrg
+ }
+ }
+ return apiTeams, nil
+}
+
+// ToAnnotatedTag convert git.Tag to api.AnnotatedTag
+func ToAnnotatedTag(repo *repo_model.Repository, t *git.Tag, c *git.Commit) *api.AnnotatedTag {
+ return &api.AnnotatedTag{
+ Tag: t.Name,
+ SHA: t.ID.String(),
+ Object: ToAnnotatedTagObject(repo, c),
+ Message: t.Message,
+ URL: util.URLJoin(repo.APIURL(), "git/tags", t.ID.String()),
+ Tagger: ToCommitUser(t.Tagger),
+ Verification: ToVerification(c),
+ }
+}
+
+// ToAnnotatedTagObject convert a git.Commit to an api.AnnotatedTagObject
+func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api.AnnotatedTagObject {
+ return &api.AnnotatedTagObject{
+ SHA: commit.ID.String(),
+ Type: string(git.ObjectCommit),
+ URL: util.URLJoin(repo.APIURL(), "git/commits", commit.ID.String()),
+ }
+}
+
+// ToTopicResponse convert from models.Topic to api.TopicResponse
+func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse {
+ return &api.TopicResponse{
+ ID: topic.ID,
+ Name: topic.Name,
+ RepoCount: topic.RepoCount,
+ Created: topic.CreatedUnix.AsTime(),
+ Updated: topic.UpdatedUnix.AsTime(),
+ }
+}
+
+// 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,
+ ConfidentialClient: app.ConfidentialClient,
+ RedirectURIs: app.RedirectURIs,
+ Created: app.CreatedUnix.AsTime(),
+ }
+}
+
+// ToLFSLock convert a LFSLock to api.LFSLock
+func ToLFSLock(ctx context.Context, l *git_model.LFSLock) *api.LFSLock {
+ u, err := user_model.GetUserByID(ctx, l.OwnerID)
+ if err != nil {
+ return nil
+ }
+ return &api.LFSLock{
+ ID: strconv.FormatInt(l.ID, 10),
+ Path: l.Path,
+ LockedAt: l.Created.Round(time.Second),
+ Owner: &api.LFSLockOwner{
+ Name: u.Name,
+ },
+ }
+}
+
+// ToChangedFile convert a gitdiff.DiffFile to api.ChangedFile
+func ToChangedFile(f *gitdiff.DiffFile, repo *repo_model.Repository, commit string) *api.ChangedFile {
+ status := "changed"
+ if f.IsDeleted {
+ status = "deleted"
+ } else if f.IsCreated {
+ status = "added"
+ } else if f.IsRenamed && f.Type == gitdiff.DiffFileCopy {
+ status = "copied"
+ } else if f.IsRenamed && f.Type == gitdiff.DiffFileRename {
+ status = "renamed"
+ } else if f.Addition == 0 && f.Deletion == 0 {
+ status = "unchanged"
+ }
+
+ file := &api.ChangedFile{
+ Filename: f.GetDiffFileName(),
+ Status: status,
+ Additions: f.Addition,
+ Deletions: f.Deletion,
+ Changes: f.Addition + f.Deletion,
+ HTMLURL: fmt.Sprint(repo.HTMLURL(), "/src/commit/", commit, "/", util.PathEscapeSegments(f.GetDiffFileName())),
+ ContentsURL: fmt.Sprint(repo.APIURL(), "/contents/", util.PathEscapeSegments(f.GetDiffFileName()), "?ref=", commit),
+ RawURL: fmt.Sprint(repo.HTMLURL(), "/raw/commit/", commit, "/", util.PathEscapeSegments(f.GetDiffFileName())),
+ }
+
+ if status == "rename" {
+ file.PreviousFilename = f.OldName
+ }
+
+ return file
+}
diff --git a/modules/convert/git_commit.go b/services/convert/git_commit.go
similarity index 84%
rename from modules/convert/git_commit.go
rename to services/convert/git_commit.go
index dfd6cb080ca35..59842e40209b1 100644
--- a/modules/convert/git_commit.go
+++ b/services/convert/git_commit.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package convert
@@ -73,7 +72,7 @@ func ToPayloadCommit(repo *repo_model.Repository, c *git.Commit) *api.PayloadCom
}
// ToCommit convert a git.Commit to api.Commit
-func ToCommit(repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user_model.User) (*api.Commit, error) {
+func ToCommit(repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user_model.User, stat bool) (*api.Commit, error) {
var apiAuthor, apiCommitter *api.User
// Retrieve author and committer information
@@ -133,28 +132,7 @@ func ToCommit(repo *repo_model.Repository, gitRepo *git.Repository, commit *git.
}
}
- // Retrieve files affected by the commit
- fileStatus, err := git.GetCommitFileStatus(gitRepo.Ctx, repo.RepoPath(), commit.ID.String())
- if err != nil {
- return nil, err
- }
- affectedFileList := make([]*api.CommitAffectedFiles, 0, len(fileStatus.Added)+len(fileStatus.Removed)+len(fileStatus.Modified))
- for _, files := range [][]string{fileStatus.Added, fileStatus.Removed, fileStatus.Modified} {
- for _, filename := range files {
- affectedFileList = append(affectedFileList, &api.CommitAffectedFiles{
- Filename: filename,
- })
- }
- }
-
- diff, err := gitdiff.GetDiff(gitRepo, &gitdiff.DiffOptions{
- AfterCommitID: commit.ID.String(),
- })
- if err != nil {
- return nil, err
- }
-
- return &api.Commit{
+ res := &api.Commit{
CommitMeta: &api.CommitMeta{
URL: repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()),
SHA: commit.ID.String(),
@@ -188,11 +166,37 @@ func ToCommit(repo *repo_model.Repository, gitRepo *git.Repository, commit *git.
Author: apiAuthor,
Committer: apiCommitter,
Parents: apiParents,
- Files: affectedFileList,
- Stats: &api.CommitStats{
+ }
+
+ // Retrieve files affected by the commit
+ if stat {
+ fileStatus, err := git.GetCommitFileStatus(gitRepo.Ctx, repo.RepoPath(), commit.ID.String())
+ if err != nil {
+ return nil, err
+ }
+ affectedFileList := make([]*api.CommitAffectedFiles, 0, len(fileStatus.Added)+len(fileStatus.Removed)+len(fileStatus.Modified))
+ for _, files := range [][]string{fileStatus.Added, fileStatus.Removed, fileStatus.Modified} {
+ for _, filename := range files {
+ affectedFileList = append(affectedFileList, &api.CommitAffectedFiles{
+ Filename: filename,
+ })
+ }
+ }
+
+ diff, err := gitdiff.GetDiff(gitRepo, &gitdiff.DiffOptions{
+ AfterCommitID: commit.ID.String(),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ res.Files = affectedFileList
+ res.Stats = &api.CommitStats{
Total: diff.TotalAddition + diff.TotalDeletion,
Additions: diff.TotalAddition,
Deletions: diff.TotalDeletion,
- },
- }, nil
+ }
+ }
+
+ return res, nil
}
diff --git a/modules/convert/git_commit_test.go b/services/convert/git_commit_test.go
similarity index 88%
rename from modules/convert/git_commit_test.go
rename to services/convert/git_commit_test.go
index 118ba3a007a2d..8c4ef88ebe0eb 100644
--- a/modules/convert/git_commit_test.go
+++ b/services/convert/git_commit_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package convert
@@ -19,7 +18,7 @@ import (
func TestToCommitMeta(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+ headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
sha1, _ := git.NewIDFromString("0000000000000000000000000000000000000000")
signature := &git.Signature{Name: "Test Signature", Email: "test@email.com", When: time.Unix(0, 0)}
tag := &git.Tag{
diff --git a/services/convert/issue.go b/services/convert/issue.go
new file mode 100644
index 0000000000000..f3af03ed949ff
--- /dev/null
+++ b/services/convert/issue.go
@@ -0,0 +1,235 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToAPIIssue converts an Issue to API format
+// it assumes some fields assigned with values:
+// Required - Poster, Labels,
+// Optional - Milestone, Assignee, PullRequest
+func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
+ if err := issue.LoadLabels(ctx); err != nil {
+ return &api.Issue{}
+ }
+ if err := issue.LoadPoster(ctx); err != nil {
+ return &api.Issue{}
+ }
+ if err := issue.LoadRepo(ctx); err != nil {
+ return &api.Issue{}
+ }
+ if err := issue.Repo.GetOwner(ctx); err != nil {
+ return &api.Issue{}
+ }
+
+ apiIssue := &api.Issue{
+ ID: issue.ID,
+ URL: issue.APIURL(),
+ HTMLURL: issue.HTMLURL(),
+ Index: issue.Index,
+ Poster: ToUser(issue.Poster, nil),
+ Title: issue.Title,
+ Body: issue.Content,
+ Attachments: ToAttachments(issue.Attachments),
+ Ref: issue.Ref,
+ Labels: ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner),
+ State: issue.State(),
+ IsLocked: issue.IsLocked,
+ Comments: issue.NumComments,
+ Created: issue.CreatedUnix.AsTime(),
+ Updated: issue.UpdatedUnix.AsTime(),
+ }
+
+ apiIssue.Repo = &api.RepositoryMeta{
+ ID: issue.Repo.ID,
+ Name: issue.Repo.Name,
+ Owner: issue.Repo.OwnerName,
+ FullName: issue.Repo.FullName(),
+ }
+
+ if issue.ClosedUnix != 0 {
+ apiIssue.Closed = issue.ClosedUnix.AsTimePtr()
+ }
+
+ if err := issue.LoadMilestone(ctx); err != nil {
+ return &api.Issue{}
+ }
+ if issue.Milestone != nil {
+ apiIssue.Milestone = ToAPIMilestone(issue.Milestone)
+ }
+
+ if err := issue.LoadAssignees(ctx); err != nil {
+ return &api.Issue{}
+ }
+ if len(issue.Assignees) > 0 {
+ for _, assignee := range issue.Assignees {
+ apiIssue.Assignees = append(apiIssue.Assignees, ToUser(assignee, nil))
+ }
+ apiIssue.Assignee = ToUser(issue.Assignees[0], nil) // For compatibility, we're keeping the first assignee as `apiIssue.Assignee`
+ }
+ if issue.IsPull {
+ if err := issue.LoadPullRequest(ctx); err != nil {
+ return &api.Issue{}
+ }
+ apiIssue.PullRequest = &api.PullRequestMeta{
+ HasMerged: issue.PullRequest.HasMerged,
+ }
+ if issue.PullRequest.HasMerged {
+ apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
+ }
+ }
+ if issue.DeadlineUnix != 0 {
+ apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr()
+ }
+
+ return apiIssue
+}
+
+// ToAPIIssueList converts an IssueList to API format
+func ToAPIIssueList(ctx context.Context, il issues_model.IssueList) []*api.Issue {
+ result := make([]*api.Issue, len(il))
+ for i := range il {
+ result[i] = ToAPIIssue(ctx, il[i])
+ }
+ return result
+}
+
+// ToTrackedTime converts TrackedTime to API format
+func ToTrackedTime(ctx context.Context, t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
+ apiT = &api.TrackedTime{
+ ID: t.ID,
+ IssueID: t.IssueID,
+ UserID: t.UserID,
+ Time: t.Time,
+ Created: t.Created,
+ }
+ if t.Issue != nil {
+ apiT.Issue = ToAPIIssue(ctx, t.Issue)
+ }
+ if t.User != nil {
+ apiT.UserName = t.User.Name
+ }
+ return apiT
+}
+
+// ToStopWatches convert Stopwatch list to api.StopWatches
+func ToStopWatches(sws []*issues_model.Stopwatch) (api.StopWatches, error) {
+ result := api.StopWatches(make([]api.StopWatch, 0, len(sws)))
+
+ issueCache := make(map[int64]*issues_model.Issue)
+ repoCache := make(map[int64]*repo_model.Repository)
+ var (
+ issue *issues_model.Issue
+ repo *repo_model.Repository
+ ok bool
+ err error
+ )
+
+ for _, sw := range sws {
+ issue, ok = issueCache[sw.IssueID]
+ if !ok {
+ issue, err = issues_model.GetIssueByID(db.DefaultContext, sw.IssueID)
+ if err != nil {
+ return nil, err
+ }
+ }
+ repo, ok = repoCache[issue.RepoID]
+ if !ok {
+ repo, err = repo_model.GetRepositoryByID(db.DefaultContext, issue.RepoID)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ result = append(result, api.StopWatch{
+ Created: sw.CreatedUnix.AsTime(),
+ Seconds: sw.Seconds(),
+ Duration: sw.Duration(),
+ IssueIndex: issue.Index,
+ IssueTitle: issue.Title,
+ RepoOwnerName: repo.OwnerName,
+ RepoName: repo.Name,
+ })
+ }
+ return result, nil
+}
+
+// ToTrackedTimeList converts TrackedTimeList to API format
+func ToTrackedTimeList(ctx context.Context, tl issues_model.TrackedTimeList) api.TrackedTimeList {
+ result := make([]*api.TrackedTime, 0, len(tl))
+ for _, t := range tl {
+ result = append(result, ToTrackedTime(ctx, t))
+ }
+ return result
+}
+
+// ToLabel converts Label to API format
+func ToLabel(label *issues_model.Label, repo *repo_model.Repository, org *user_model.User) *api.Label {
+ result := &api.Label{
+ ID: label.ID,
+ Name: label.Name,
+ Color: strings.TrimLeft(label.Color, "#"),
+ Description: label.Description,
+ }
+
+ // calculate URL
+ if label.BelongsToRepo() && repo != nil {
+ if repo != nil {
+ result.URL = fmt.Sprintf("%s/labels/%d", repo.APIURL(), label.ID)
+ } else {
+ log.Error("ToLabel did not get repo to calculate url for label with id '%d'", label.ID)
+ }
+ } else { // BelongsToOrg
+ if org != nil {
+ result.URL = fmt.Sprintf("%sapi/v1/orgs/%s/labels/%d", setting.AppURL, url.PathEscape(org.Name), label.ID)
+ } else {
+ log.Error("ToLabel did not get org to calculate url for label with id '%d'", label.ID)
+ }
+ }
+
+ return result
+}
+
+// ToLabelList converts list of Label to API format
+func ToLabelList(labels []*issues_model.Label, repo *repo_model.Repository, org *user_model.User) []*api.Label {
+ result := make([]*api.Label, len(labels))
+ for i := range labels {
+ result[i] = ToLabel(labels[i], repo, org)
+ }
+ return result
+}
+
+// ToAPIMilestone converts Milestone into API Format
+func ToAPIMilestone(m *issues_model.Milestone) *api.Milestone {
+ apiMilestone := &api.Milestone{
+ ID: m.ID,
+ State: m.State(),
+ Title: m.Name,
+ Description: m.Content,
+ OpenIssues: m.NumOpenIssues,
+ ClosedIssues: m.NumClosedIssues,
+ Created: m.CreatedUnix.AsTime(),
+ Updated: m.UpdatedUnix.AsTimePtr(),
+ }
+ if m.IsClosed {
+ apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr()
+ }
+ if m.DeadlineUnix.Year() < 9999 {
+ apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr()
+ }
+ return apiMilestone
+}
diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go
new file mode 100644
index 0000000000000..6044cbcf61343
--- /dev/null
+++ b/services/convert/issue_comment.go
@@ -0,0 +1,175 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToComment converts a issues_model.Comment to the api.Comment format
+func ToComment(c *issues_model.Comment) *api.Comment {
+ return &api.Comment{
+ ID: c.ID,
+ Poster: ToUser(c.Poster, nil),
+ HTMLURL: c.HTMLURL(),
+ IssueURL: c.IssueURL(),
+ PRURL: c.PRURL(),
+ Body: c.Content,
+ Attachments: ToAttachments(c.Attachments),
+ Created: c.CreatedUnix.AsTime(),
+ Updated: c.UpdatedUnix.AsTime(),
+ }
+}
+
+// ToTimelineComment converts a issues_model.Comment to the api.TimelineComment format
+func ToTimelineComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User) *api.TimelineComment {
+ err := c.LoadMilestone(ctx)
+ if err != nil {
+ log.Error("LoadMilestone: %v", err)
+ return nil
+ }
+
+ err = c.LoadAssigneeUserAndTeam()
+ if err != nil {
+ log.Error("LoadAssigneeUserAndTeam: %v", err)
+ return nil
+ }
+
+ err = c.LoadResolveDoer()
+ if err != nil {
+ log.Error("LoadResolveDoer: %v", err)
+ return nil
+ }
+
+ err = c.LoadDepIssueDetails()
+ if err != nil {
+ log.Error("LoadDepIssueDetails: %v", err)
+ return nil
+ }
+
+ err = c.LoadTime()
+ if err != nil {
+ log.Error("LoadTime: %v", err)
+ return nil
+ }
+
+ err = c.LoadLabel()
+ if err != nil {
+ log.Error("LoadLabel: %v", err)
+ return nil
+ }
+
+ comment := &api.TimelineComment{
+ ID: c.ID,
+ Type: c.Type.String(),
+ Poster: ToUser(c.Poster, nil),
+ HTMLURL: c.HTMLURL(),
+ IssueURL: c.IssueURL(),
+ PRURL: c.PRURL(),
+ Body: c.Content,
+ Created: c.CreatedUnix.AsTime(),
+ Updated: c.UpdatedUnix.AsTime(),
+
+ OldProjectID: c.OldProjectID,
+ ProjectID: c.ProjectID,
+
+ OldTitle: c.OldTitle,
+ NewTitle: c.NewTitle,
+
+ OldRef: c.OldRef,
+ NewRef: c.NewRef,
+
+ RefAction: c.RefAction.String(),
+ RefCommitSHA: c.CommitSHA,
+
+ ReviewID: c.ReviewID,
+
+ RemovedAssignee: c.RemovedAssignee,
+ }
+
+ if c.OldMilestone != nil {
+ comment.OldMilestone = ToAPIMilestone(c.OldMilestone)
+ }
+ if c.Milestone != nil {
+ comment.Milestone = ToAPIMilestone(c.Milestone)
+ }
+
+ if c.Time != nil {
+ err = c.Time.LoadAttributes()
+ if err != nil {
+ log.Error("Time.LoadAttributes: %v", err)
+ return nil
+ }
+
+ comment.TrackedTime = ToTrackedTime(ctx, c.Time)
+ }
+
+ if c.RefIssueID != 0 {
+ issue, err := issues_model.GetIssueByID(ctx, c.RefIssueID)
+ if err != nil {
+ log.Error("GetIssueByID(%d): %v", c.RefIssueID, err)
+ return nil
+ }
+ comment.RefIssue = ToAPIIssue(ctx, issue)
+ }
+
+ if c.RefCommentID != 0 {
+ com, err := issues_model.GetCommentByID(ctx, c.RefCommentID)
+ if err != nil {
+ log.Error("GetCommentByID(%d): %v", c.RefCommentID, err)
+ return nil
+ }
+ err = com.LoadPoster(ctx)
+ if err != nil {
+ log.Error("LoadPoster: %v", err)
+ return nil
+ }
+ comment.RefComment = ToComment(com)
+ }
+
+ if c.Label != nil {
+ var org *user_model.User
+ var repo *repo_model.Repository
+ if c.Label.BelongsToOrg() {
+ var err error
+ org, err = user_model.GetUserByID(ctx, c.Label.OrgID)
+ if err != nil {
+ log.Error("GetUserByID(%d): %v", c.Label.OrgID, err)
+ return nil
+ }
+ }
+ if c.Label.BelongsToRepo() {
+ var err error
+ repo, err = repo_model.GetRepositoryByID(ctx, c.Label.RepoID)
+ if err != nil {
+ log.Error("GetRepositoryByID(%d): %v", c.Label.RepoID, err)
+ return nil
+ }
+ }
+ comment.Label = ToLabel(c.Label, repo, org)
+ }
+
+ if c.Assignee != nil {
+ comment.Assignee = ToUser(c.Assignee, nil)
+ }
+ if c.AssigneeTeam != nil {
+ comment.AssigneeTeam, _ = ToTeam(c.AssigneeTeam)
+ }
+
+ if c.ResolveDoer != nil {
+ comment.ResolveDoer = ToUser(c.ResolveDoer, nil)
+ }
+
+ if c.DependentIssue != nil {
+ comment.DependentIssue = ToAPIIssue(ctx, c.DependentIssue)
+ }
+
+ return comment
+}
diff --git a/services/convert/issue_test.go b/services/convert/issue_test.go
new file mode 100644
index 0000000000000..4d780f3f00905
--- /dev/null
+++ b/services/convert/issue_test.go
@@ -0,0 +1,57 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLabel_ToLabel(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: label.RepoID})
+ assert.Equal(t, &api.Label{
+ ID: label.ID,
+ Name: label.Name,
+ Color: "abcdef",
+ URL: fmt.Sprintf("%sapi/v1/repos/user2/repo1/labels/%d", setting.AppURL, label.ID),
+ }, ToLabel(label, repo, nil))
+}
+
+func TestMilestone_APIFormat(t *testing.T) {
+ milestone := &issues_model.Milestone{
+ ID: 3,
+ RepoID: 4,
+ Name: "milestoneName",
+ Content: "milestoneContent",
+ IsClosed: false,
+ NumOpenIssues: 5,
+ NumClosedIssues: 6,
+ CreatedUnix: timeutil.TimeStamp(time.Date(1999, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()),
+ UpdatedUnix: timeutil.TimeStamp(time.Date(1999, time.March, 1, 0, 0, 0, 0, time.UTC).Unix()),
+ DeadlineUnix: timeutil.TimeStamp(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()),
+ }
+ assert.Equal(t, api.Milestone{
+ ID: milestone.ID,
+ State: api.StateOpen,
+ Title: milestone.Name,
+ Description: milestone.Content,
+ OpenIssues: milestone.NumOpenIssues,
+ ClosedIssues: milestone.NumClosedIssues,
+ Created: milestone.CreatedUnix.AsTime(),
+ Updated: milestone.UpdatedUnix.AsTimePtr(),
+ Deadline: milestone.DeadlineUnix.AsTimePtr(),
+ }, *ToAPIMilestone(milestone))
+}
diff --git a/services/convert/main_test.go b/services/convert/main_test.go
new file mode 100644
index 0000000000000..4c8e57bf79489
--- /dev/null
+++ b/services/convert/main_test.go
@@ -0,0 +1,17 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m, &unittest.TestOptions{
+ GiteaRootPath: filepath.Join("..", ".."),
+ })
+}
diff --git a/services/convert/mirror.go b/services/convert/mirror.go
new file mode 100644
index 0000000000000..1dcfc9b64dcfa
--- /dev/null
+++ b/services/convert/mirror.go
@@ -0,0 +1,38 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/git"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToPushMirror convert from repo_model.PushMirror and remoteAddress to api.TopicResponse
+func ToPushMirror(pm *repo_model.PushMirror) (*api.PushMirror, error) {
+ repo := pm.GetRepository()
+ remoteAddress, err := getRemoteAddress(repo, pm.RemoteName)
+ if err != nil {
+ return nil, err
+ }
+ return &api.PushMirror{
+ RepoName: repo.Name,
+ RemoteName: pm.RemoteName,
+ RemoteAddress: remoteAddress,
+ CreatedUnix: pm.CreatedUnix.FormatLong(),
+ LastUpdateUnix: pm.LastUpdateUnix.FormatLong(),
+ LastError: pm.LastError,
+ Interval: pm.Interval.String(),
+ }, nil
+}
+
+func getRemoteAddress(repo *repo_model.Repository, remoteName string) (string, error) {
+ url, err := git.GetRemoteURL(git.DefaultContext, repo.RepoPath(), remoteName)
+ if err != nil {
+ return "", err
+ }
+ // remove confidential information
+ url.User = nil
+ return url.String(), nil
+}
diff --git a/services/convert/notification.go b/services/convert/notification.go
new file mode 100644
index 0000000000000..5d3b078a25d50
--- /dev/null
+++ b/services/convert/notification.go
@@ -0,0 +1,96 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "net/url"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/perm"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToNotificationThread convert a Notification to api.NotificationThread
+func ToNotificationThread(n *activities_model.Notification) *api.NotificationThread {
+ result := &api.NotificationThread{
+ ID: n.ID,
+ Unread: !(n.Status == activities_model.NotificationStatusRead || n.Status == activities_model.NotificationStatusPinned),
+ Pinned: n.Status == activities_model.NotificationStatusPinned,
+ UpdatedAt: n.UpdatedUnix.AsTime(),
+ URL: n.APIURL(),
+ }
+
+ // since user only get notifications when he has access to use minimal access mode
+ if n.Repository != nil {
+ result.Repository = ToRepo(db.DefaultContext, n.Repository, perm.AccessModeRead)
+
+ // This permission is not correct and we should not be reporting it
+ for repository := result.Repository; repository != nil; repository = repository.Parent {
+ repository.Permissions = nil
+ }
+ }
+
+ // handle Subject
+ switch n.Source {
+ case activities_model.NotificationSourceIssue:
+ result.Subject = &api.NotificationSubject{Type: api.NotifySubjectIssue}
+ if n.Issue != nil {
+ result.Subject.Title = n.Issue.Title
+ result.Subject.URL = n.Issue.APIURL()
+ result.Subject.HTMLURL = n.Issue.HTMLURL()
+ result.Subject.State = n.Issue.State()
+ comment, err := n.Issue.GetLastComment()
+ if err == nil && comment != nil {
+ result.Subject.LatestCommentURL = comment.APIURL()
+ result.Subject.LatestCommentHTMLURL = comment.HTMLURL()
+ }
+ }
+ case activities_model.NotificationSourcePullRequest:
+ result.Subject = &api.NotificationSubject{Type: api.NotifySubjectPull}
+ if n.Issue != nil {
+ result.Subject.Title = n.Issue.Title
+ result.Subject.URL = n.Issue.APIURL()
+ result.Subject.HTMLURL = n.Issue.HTMLURL()
+ result.Subject.State = n.Issue.State()
+ comment, err := n.Issue.GetLastComment()
+ if err == nil && comment != nil {
+ result.Subject.LatestCommentURL = comment.APIURL()
+ result.Subject.LatestCommentHTMLURL = comment.HTMLURL()
+ }
+
+ pr, _ := n.Issue.GetPullRequest()
+ if pr != nil && pr.HasMerged {
+ result.Subject.State = "merged"
+ }
+ }
+ case activities_model.NotificationSourceCommit:
+ url := n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID)
+ result.Subject = &api.NotificationSubject{
+ Type: api.NotifySubjectCommit,
+ Title: n.CommitID,
+ URL: url,
+ HTMLURL: url,
+ }
+ case activities_model.NotificationSourceRepository:
+ result.Subject = &api.NotificationSubject{
+ Type: api.NotifySubjectRepository,
+ Title: n.Repository.FullName(),
+ // FIXME: this is a relative URL, rather useless and inconsistent, but keeping for backwards compat
+ URL: n.Repository.Link(),
+ HTMLURL: n.Repository.HTMLURL(),
+ }
+ }
+
+ return result
+}
+
+// ToNotifications convert list of Notification to api.NotificationThread list
+func ToNotifications(nl activities_model.NotificationList) []*api.NotificationThread {
+ result := make([]*api.NotificationThread, 0, len(nl))
+ for _, n := range nl {
+ result = append(result, ToNotificationThread(n))
+ }
+ return result
+}
diff --git a/services/convert/package.go b/services/convert/package.go
new file mode 100644
index 0000000000000..68ae6f4e62dbf
--- /dev/null
+++ b/services/convert/package.go
@@ -0,0 +1,52 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models/packages"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToPackage convert a packages.PackageDescriptor to api.Package
+func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_model.User) (*api.Package, error) {
+ var repo *api.Repository
+ if pd.Repository != nil {
+ permission, err := access_model.GetUserRepoPermission(ctx, pd.Repository, doer)
+ if err != nil {
+ return nil, err
+ }
+
+ if permission.HasAccess() {
+ repo = ToRepo(ctx, pd.Repository, permission.AccessMode)
+ }
+ }
+
+ return &api.Package{
+ ID: pd.Version.ID,
+ Owner: ToUser(pd.Owner, doer),
+ Repository: repo,
+ Creator: ToUser(pd.Creator, doer),
+ Type: string(pd.Package.Type),
+ Name: pd.Package.Name,
+ Version: pd.Version.Version,
+ CreatedAt: pd.Version.CreatedUnix.AsTime(),
+ }, nil
+}
+
+// ToPackageFile converts packages.PackageFileDescriptor to api.PackageFile
+func ToPackageFile(pfd *packages.PackageFileDescriptor) *api.PackageFile {
+ return &api.PackageFile{
+ ID: pfd.File.ID,
+ Size: pfd.Blob.Size,
+ Name: pfd.File.Name,
+ HashMD5: pfd.Blob.HashMD5,
+ HashSHA1: pfd.Blob.HashSHA1,
+ HashSHA256: pfd.Blob.HashSHA256,
+ HashSHA512: pfd.Blob.HashSHA512,
+ }
+}
diff --git a/services/convert/pull.go b/services/convert/pull.go
new file mode 100644
index 0000000000000..cdf72e78057f5
--- /dev/null
+++ b/services/convert/pull.go
@@ -0,0 +1,208 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "fmt"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToAPIPullRequest assumes following fields have been assigned with valid values:
+// Required - Issue
+// Optional - Merger
+func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest {
+ var (
+ baseBranch *git.Branch
+ headBranch *git.Branch
+ baseCommit *git.Commit
+ err error
+ )
+
+ if err = pr.Issue.LoadRepo(ctx); err != nil {
+ log.Error("pr.Issue.LoadRepo[%d]: %v", pr.ID, err)
+ return nil
+ }
+
+ apiIssue := ToAPIIssue(ctx, pr.Issue)
+ if err := pr.LoadBaseRepo(ctx); err != nil {
+ log.Error("GetRepositoryById[%d]: %v", pr.ID, err)
+ return nil
+ }
+
+ if err := pr.LoadHeadRepo(ctx); err != nil {
+ log.Error("GetRepositoryById[%d]: %v", pr.ID, err)
+ return nil
+ }
+
+ p, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer)
+ if err != nil {
+ log.Error("GetUserRepoPermission[%d]: %v", pr.BaseRepoID, err)
+ p.AccessMode = perm.AccessModeNone
+ }
+
+ apiPullRequest := &api.PullRequest{
+ ID: pr.ID,
+ URL: pr.Issue.HTMLURL(),
+ Index: pr.Index,
+ Poster: apiIssue.Poster,
+ Title: apiIssue.Title,
+ Body: apiIssue.Body,
+ Labels: apiIssue.Labels,
+ Milestone: apiIssue.Milestone,
+ Assignee: apiIssue.Assignee,
+ Assignees: apiIssue.Assignees,
+ State: apiIssue.State,
+ IsLocked: apiIssue.IsLocked,
+ Comments: apiIssue.Comments,
+ HTMLURL: pr.Issue.HTMLURL(),
+ DiffURL: pr.Issue.DiffURL(),
+ PatchURL: pr.Issue.PatchURL(),
+ HasMerged: pr.HasMerged,
+ MergeBase: pr.MergeBase,
+ Mergeable: pr.Mergeable(),
+ Deadline: apiIssue.Deadline,
+ Created: pr.Issue.CreatedUnix.AsTimePtr(),
+ Updated: pr.Issue.UpdatedUnix.AsTimePtr(),
+
+ AllowMaintainerEdit: pr.AllowMaintainerEdit,
+
+ Base: &api.PRBranchInfo{
+ Name: pr.BaseBranch,
+ Ref: pr.BaseBranch,
+ RepoID: pr.BaseRepoID,
+ Repository: ToRepo(ctx, pr.BaseRepo, p.AccessMode),
+ },
+ Head: &api.PRBranchInfo{
+ Name: pr.HeadBranch,
+ Ref: fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index),
+ RepoID: -1,
+ },
+ }
+
+ if pr.Issue.ClosedUnix != 0 {
+ apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr()
+ }
+
+ gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
+ if err != nil {
+ log.Error("OpenRepository[%s]: %v", pr.BaseRepo.RepoPath(), err)
+ return nil
+ }
+ defer gitRepo.Close()
+
+ baseBranch, err = gitRepo.GetBranch(pr.BaseBranch)
+ if err != nil && !git.IsErrBranchNotExist(err) {
+ log.Error("GetBranch[%s]: %v", pr.BaseBranch, err)
+ return nil
+ }
+
+ if err == nil {
+ baseCommit, err = baseBranch.GetCommit()
+ if err != nil && !git.IsErrNotExist(err) {
+ log.Error("GetCommit[%s]: %v", baseBranch.Name, err)
+ return nil
+ }
+
+ if err == nil {
+ apiPullRequest.Base.Sha = baseCommit.ID.String()
+ }
+ }
+
+ if pr.Flow == issues_model.PullRequestFlowAGit {
+ gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
+ if err != nil {
+ log.Error("OpenRepository[%s]: %v", pr.GetGitRefName(), err)
+ return nil
+ }
+ defer gitRepo.Close()
+
+ apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(pr.GetGitRefName())
+ if err != nil {
+ log.Error("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err)
+ return nil
+ }
+ apiPullRequest.Head.RepoID = pr.BaseRepoID
+ apiPullRequest.Head.Repository = apiPullRequest.Base.Repository
+ apiPullRequest.Head.Name = ""
+ }
+
+ if pr.HeadRepo != nil && pr.Flow == issues_model.PullRequestFlowGithub {
+ p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer)
+ if err != nil {
+ log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err)
+ p.AccessMode = perm.AccessModeNone
+ }
+
+ apiPullRequest.Head.RepoID = pr.HeadRepo.ID
+ apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p.AccessMode)
+
+ headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
+ if err != nil {
+ log.Error("OpenRepository[%s]: %v", pr.HeadRepo.RepoPath(), err)
+ return nil
+ }
+ defer headGitRepo.Close()
+
+ headBranch, err = headGitRepo.GetBranch(pr.HeadBranch)
+ if err != nil && !git.IsErrBranchNotExist(err) {
+ log.Error("GetBranch[%s]: %v", pr.HeadBranch, err)
+ return nil
+ }
+
+ if git.IsErrBranchNotExist(err) {
+ headCommitID, err := headGitRepo.GetRefCommitID(apiPullRequest.Head.Ref)
+ if err != nil && !git.IsErrNotExist(err) {
+ log.Error("GetCommit[%s]: %v", pr.HeadBranch, err)
+ return nil
+ }
+ if err == nil {
+ apiPullRequest.Head.Sha = headCommitID
+ }
+ } else {
+ commit, err := headBranch.GetCommit()
+ if err != nil && !git.IsErrNotExist(err) {
+ log.Error("GetCommit[%s]: %v", headBranch.Name, err)
+ return nil
+ }
+ if err == nil {
+ apiPullRequest.Head.Ref = pr.HeadBranch
+ apiPullRequest.Head.Sha = commit.ID.String()
+ }
+ }
+ }
+
+ if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 {
+ baseGitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
+ if err != nil {
+ log.Error("OpenRepository[%s]: %v", pr.BaseRepo.RepoPath(), err)
+ return nil
+ }
+ defer baseGitRepo.Close()
+ refs, err := baseGitRepo.GetRefsFiltered(apiPullRequest.Head.Ref)
+ if err != nil {
+ log.Error("GetRefsFiltered[%s]: %v", apiPullRequest.Head.Ref, err)
+ return nil
+ } else if len(refs) == 0 {
+ log.Error("unable to resolve PR head ref")
+ } else {
+ apiPullRequest.Head.Sha = refs[0].Object.String()
+ }
+ }
+
+ if pr.HasMerged {
+ apiPullRequest.Merged = pr.MergedUnix.AsTimePtr()
+ apiPullRequest.MergedCommitID = &pr.MergedCommitID
+ apiPullRequest.MergedBy = ToUser(pr.Merger, nil)
+ }
+
+ return apiPullRequest
+}
diff --git a/modules/convert/pull_review.go b/services/convert/pull_review.go
similarity index 76%
rename from modules/convert/pull_review.go
rename to services/convert/pull_review.go
index 962aae58bb34f..66c5018ee2a9c 100644
--- a/modules/convert/pull_review.go
+++ b/services/convert/pull_review.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package convert
@@ -8,13 +7,13 @@ import (
"context"
"strings"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
)
// ToPullReview convert a review to api format
-func ToPullReview(ctx context.Context, r *models.Review, doer *user_model.User) (*api.PullReview, error) {
+func ToPullReview(ctx context.Context, r *issues_model.Review, doer *user_model.User) (*api.PullReview, error) {
if err := r.LoadAttributes(ctx); err != nil {
if !user_model.IsErrUserNotExist(err) {
return nil, err
@@ -22,10 +21,15 @@ func ToPullReview(ctx context.Context, r *models.Review, doer *user_model.User)
r.Reviewer = user_model.NewGhostUser()
}
+ apiTeam, err := ToTeam(r.ReviewerTeam)
+ if err != nil {
+ return nil, err
+ }
+
result := &api.PullReview{
ID: r.ID,
Reviewer: ToUser(r.Reviewer, doer),
- ReviewerTeam: ToTeam(r.ReviewerTeam),
+ ReviewerTeam: apiTeam,
State: api.ReviewStateUnknown,
Body: r.Content,
CommitID: r.CommitID,
@@ -34,20 +38,21 @@ func ToPullReview(ctx context.Context, r *models.Review, doer *user_model.User)
Dismissed: r.Dismissed,
CodeCommentsCount: r.GetCodeCommentsCount(),
Submitted: r.CreatedUnix.AsTime(),
+ Updated: r.UpdatedUnix.AsTime(),
HTMLURL: r.HTMLURL(),
HTMLPullURL: r.Issue.HTMLURL(),
}
switch r.Type {
- case models.ReviewTypeApprove:
+ case issues_model.ReviewTypeApprove:
result.State = api.ReviewStateApproved
- case models.ReviewTypeReject:
+ case issues_model.ReviewTypeReject:
result.State = api.ReviewStateRequestChanges
- case models.ReviewTypeComment:
+ case issues_model.ReviewTypeComment:
result.State = api.ReviewStateComment
- case models.ReviewTypePending:
+ case issues_model.ReviewTypePending:
result.State = api.ReviewStatePending
- case models.ReviewTypeRequest:
+ case issues_model.ReviewTypeRequest:
result.State = api.ReviewStateRequestReview
}
@@ -55,11 +60,11 @@ func ToPullReview(ctx context.Context, r *models.Review, doer *user_model.User)
}
// ToPullReviewList convert a list of review to it's api format
-func ToPullReviewList(ctx context.Context, rl []*models.Review, doer *user_model.User) ([]*api.PullReview, error) {
+func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user_model.User) ([]*api.PullReview, error) {
result := make([]*api.PullReview, 0, len(rl))
for i := range rl {
// show pending reviews only for the user who created them
- if rl[i].Type == models.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) {
+ if rl[i].Type == issues_model.ReviewTypePending && !(doer.IsAdmin || doer.ID == rl[i].ReviewerID) {
continue
}
r, err := ToPullReview(ctx, rl[i], doer)
@@ -72,7 +77,7 @@ func ToPullReviewList(ctx context.Context, rl []*models.Review, doer *user_model
}
// ToPullReviewCommentList convert the CodeComments of an review to it's api format
-func ToPullReviewCommentList(ctx context.Context, review *models.Review, doer *user_model.User) ([]*api.PullReviewComment, error) {
+func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, doer *user_model.User) ([]*api.PullReviewComment, error) {
if err := review.LoadAttributes(ctx); err != nil {
if !user_model.IsErrUserNotExist(err) {
return nil, err
diff --git a/services/convert/pull_test.go b/services/convert/pull_test.go
new file mode 100644
index 0000000000000..0915d096e66c6
--- /dev/null
+++ b/services/convert/pull_test.go
@@ -0,0 +1,48 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPullRequest_APIFormat(t *testing.T) {
+ // with HeadRepo
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
+ assert.NoError(t, pr.LoadAttributes(db.DefaultContext))
+ assert.NoError(t, pr.LoadIssue(db.DefaultContext))
+ apiPullRequest := ToAPIPullRequest(git.DefaultContext, pr, nil)
+ assert.NotNil(t, apiPullRequest)
+ assert.EqualValues(t, &structs.PRBranchInfo{
+ Name: "branch1",
+ Ref: "refs/pull/2/head",
+ Sha: "4a357436d925b5c974181ff12a994538ddc5a269",
+ RepoID: 1,
+ Repository: ToRepo(db.DefaultContext, headRepo, perm.AccessModeRead),
+ }, apiPullRequest.Head)
+
+ // withOut HeadRepo
+ pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
+ assert.NoError(t, pr.LoadIssue(db.DefaultContext))
+ assert.NoError(t, pr.LoadAttributes(db.DefaultContext))
+ // simulate fork deletion
+ pr.HeadRepo = nil
+ pr.HeadRepoID = 100000
+ apiPullRequest = ToAPIPullRequest(git.DefaultContext, pr, nil)
+ assert.NotNil(t, apiPullRequest)
+ assert.Nil(t, apiPullRequest.Head.Repository)
+ assert.EqualValues(t, -1, apiPullRequest.Head.RepoID)
+}
diff --git a/services/convert/release.go b/services/convert/release.go
new file mode 100644
index 0000000000000..3afa53c03f5fa
--- /dev/null
+++ b/services/convert/release.go
@@ -0,0 +1,30 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ repo_model "code.gitea.io/gitea/models/repo"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToRelease convert a repo_model.Release to api.Release
+func ToRelease(r *repo_model.Release) *api.Release {
+ return &api.Release{
+ ID: r.ID,
+ TagName: r.TagName,
+ Target: r.Target,
+ Title: r.Title,
+ Note: r.Note,
+ URL: r.APIURL(),
+ HTMLURL: r.HTMLURL(),
+ TarURL: r.TarURL(),
+ ZipURL: r.ZipURL(),
+ IsDraft: r.IsDraft,
+ IsPrerelease: r.IsPrerelease,
+ CreatedAt: r.CreatedUnix.AsTime(),
+ PublishedAt: r.CreatedUnix.AsTime(),
+ Publisher: ToUser(r.Publisher, nil),
+ Attachments: ToAttachments(r.Attachments),
+ }
+}
diff --git a/services/convert/repository.go b/services/convert/repository.go
new file mode 100644
index 0000000000000..ce53a6669237c
--- /dev/null
+++ b/services/convert/repository.go
@@ -0,0 +1,202 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "time"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
+ unit_model "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToRepo converts a Repository to api.Repository
+func ToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.AccessMode) *api.Repository {
+ return innerToRepo(ctx, repo, mode, false)
+}
+
+func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.AccessMode, isParent bool) *api.Repository {
+ var parent *api.Repository
+
+ cloneLink := repo.CloneLink()
+ permission := &api.Permission{
+ Admin: mode >= perm.AccessModeAdmin,
+ Push: mode >= perm.AccessModeWrite,
+ Pull: mode >= perm.AccessModeRead,
+ }
+ if !isParent {
+ err := repo.GetBaseRepo(ctx)
+ if err != nil {
+ return nil
+ }
+ if repo.BaseRepo != nil {
+ parent = innerToRepo(ctx, repo.BaseRepo, mode, true)
+ }
+ }
+
+ // check enabled/disabled units
+ hasIssues := false
+ var externalTracker *api.ExternalTracker
+ var internalTracker *api.InternalTracker
+ if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err == nil {
+ config := unit.IssuesConfig()
+ hasIssues = true
+ internalTracker = &api.InternalTracker{
+ EnableTimeTracker: config.EnableTimetracker,
+ AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
+ EnableIssueDependencies: config.EnableDependencies,
+ }
+ } else if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalTracker); err == nil {
+ config := unit.ExternalTrackerConfig()
+ hasIssues = true
+ externalTracker = &api.ExternalTracker{
+ ExternalTrackerURL: config.ExternalTrackerURL,
+ ExternalTrackerFormat: config.ExternalTrackerFormat,
+ ExternalTrackerStyle: config.ExternalTrackerStyle,
+ ExternalTrackerRegexpPattern: config.ExternalTrackerRegexpPattern,
+ }
+ }
+ hasWiki := false
+ var externalWiki *api.ExternalWiki
+ if _, err := repo.GetUnit(ctx, unit_model.TypeWiki); err == nil {
+ hasWiki = true
+ } else if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalWiki); err == nil {
+ hasWiki = true
+ config := unit.ExternalWikiConfig()
+ externalWiki = &api.ExternalWiki{
+ ExternalWikiURL: config.ExternalWikiURL,
+ }
+ }
+ hasPullRequests := false
+ ignoreWhitespaceConflicts := false
+ allowMerge := false
+ allowRebase := false
+ allowRebaseMerge := false
+ allowSquash := false
+ allowRebaseUpdate := false
+ defaultDeleteBranchAfterMerge := false
+ defaultMergeStyle := repo_model.MergeStyleMerge
+ if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil {
+ config := unit.PullRequestsConfig()
+ hasPullRequests = true
+ ignoreWhitespaceConflicts = config.IgnoreWhitespaceConflicts
+ allowMerge = config.AllowMerge
+ allowRebase = config.AllowRebase
+ allowRebaseMerge = config.AllowRebaseMerge
+ allowSquash = config.AllowSquash
+ allowRebaseUpdate = config.AllowRebaseUpdate
+ defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
+ defaultMergeStyle = config.GetDefaultMergeStyle()
+ }
+ hasProjects := false
+ if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
+ hasProjects = true
+ }
+
+ if err := repo.GetOwner(ctx); err != nil {
+ return nil
+ }
+
+ numReleases, _ := repo_model.GetReleaseCountByRepoID(ctx, repo.ID, repo_model.FindReleasesOptions{IncludeDrafts: false, IncludeTags: false})
+
+ mirrorInterval := ""
+ var mirrorUpdated time.Time
+ if repo.IsMirror {
+ var err error
+ repo.Mirror, err = repo_model.GetMirrorByRepoID(ctx, repo.ID)
+ if err == nil {
+ mirrorInterval = repo.Mirror.Interval.String()
+ mirrorUpdated = repo.Mirror.UpdatedUnix.AsTime()
+ }
+ }
+
+ var transfer *api.RepoTransfer
+ if repo.Status == repo_model.RepositoryPendingTransfer {
+ t, err := models.GetPendingRepositoryTransfer(ctx, repo)
+ if err != nil && !models.IsErrNoPendingTransfer(err) {
+ log.Warn("GetPendingRepositoryTransfer: %v", err)
+ } else {
+ if err := t.LoadAttributes(ctx); err != nil {
+ log.Warn("LoadAttributes of RepoTransfer: %v", err)
+ } else {
+ transfer = ToRepoTransfer(t)
+ }
+ }
+ }
+
+ var language string
+ if repo.PrimaryLanguage != nil {
+ language = repo.PrimaryLanguage.Language
+ }
+
+ repoAPIURL := repo.APIURL()
+
+ return &api.Repository{
+ ID: repo.ID,
+ Owner: ToUserWithAccessMode(repo.Owner, mode),
+ Name: repo.Name,
+ FullName: repo.FullName(),
+ Description: repo.Description,
+ Private: repo.IsPrivate,
+ Template: repo.IsTemplate,
+ Empty: repo.IsEmpty,
+ Archived: repo.IsArchived,
+ Size: int(repo.Size / 1024),
+ Fork: repo.IsFork,
+ Parent: parent,
+ Mirror: repo.IsMirror,
+ HTMLURL: repo.HTMLURL(),
+ SSHURL: cloneLink.SSH,
+ CloneURL: cloneLink.HTTPS,
+ OriginalURL: repo.SanitizedOriginalURL(),
+ Website: repo.Website,
+ Language: language,
+ LanguagesURL: repoAPIURL + "/languages",
+ Stars: repo.NumStars,
+ Forks: repo.NumForks,
+ Watchers: repo.NumWatches,
+ OpenIssues: repo.NumOpenIssues,
+ OpenPulls: repo.NumOpenPulls,
+ Releases: int(numReleases),
+ DefaultBranch: repo.DefaultBranch,
+ Created: repo.CreatedUnix.AsTime(),
+ Updated: repo.UpdatedUnix.AsTime(),
+ Permissions: permission,
+ HasIssues: hasIssues,
+ ExternalTracker: externalTracker,
+ InternalTracker: internalTracker,
+ HasWiki: hasWiki,
+ HasProjects: hasProjects,
+ ExternalWiki: externalWiki,
+ HasPullRequests: hasPullRequests,
+ IgnoreWhitespaceConflicts: ignoreWhitespaceConflicts,
+ AllowMerge: allowMerge,
+ AllowRebase: allowRebase,
+ AllowRebaseMerge: allowRebaseMerge,
+ AllowSquash: allowSquash,
+ AllowRebaseUpdate: allowRebaseUpdate,
+ DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
+ DefaultMergeStyle: string(defaultMergeStyle),
+ AvatarURL: repo.AvatarLink(),
+ Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
+ MirrorInterval: mirrorInterval,
+ MirrorUpdated: mirrorUpdated,
+ RepoTransfer: transfer,
+ }
+}
+
+// ToRepoTransfer convert a models.RepoTransfer to a structs.RepeTransfer
+func ToRepoTransfer(t *models.RepoTransfer) *api.RepoTransfer {
+ teams, _ := ToTeams(t.Teams, false)
+
+ return &api.RepoTransfer{
+ Doer: ToUser(t.Doer, nil),
+ Recipient: ToUser(t.Recipient, nil),
+ Teams: teams,
+ }
+}
diff --git a/services/convert/status.go b/services/convert/status.go
new file mode 100644
index 0000000000000..84ca1665d2aa1
--- /dev/null
+++ b/services/convert/status.go
@@ -0,0 +1,57 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+
+ git_model "code.gitea.io/gitea/models/git"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToCommitStatus converts git_model.CommitStatus to api.CommitStatus
+func ToCommitStatus(ctx context.Context, status *git_model.CommitStatus) *api.CommitStatus {
+ apiStatus := &api.CommitStatus{
+ Created: status.CreatedUnix.AsTime(),
+ Updated: status.CreatedUnix.AsTime(),
+ State: status.State,
+ TargetURL: status.TargetURL,
+ Description: status.Description,
+ ID: status.Index,
+ URL: status.APIURL(ctx),
+ Context: status.Context,
+ }
+
+ if status.CreatorID != 0 {
+ creator, _ := user_model.GetUserByID(ctx, status.CreatorID)
+ apiStatus.Creator = ToUser(creator, nil)
+ }
+
+ return apiStatus
+}
+
+// ToCombinedStatus converts List of CommitStatus to a CombinedStatus
+func ToCombinedStatus(ctx context.Context, statuses []*git_model.CommitStatus, repo *api.Repository) *api.CombinedStatus {
+ if len(statuses) == 0 {
+ return nil
+ }
+
+ retStatus := &api.CombinedStatus{
+ SHA: statuses[0].SHA,
+ TotalCount: len(statuses),
+ Repository: repo,
+ URL: "",
+ }
+
+ retStatus.Statuses = make([]*api.CommitStatus, 0, len(statuses))
+ for _, status := range statuses {
+ retStatus.Statuses = append(retStatus.Statuses, ToCommitStatus(ctx, status))
+ if status.State.NoBetterThan(retStatus.State) {
+ retStatus.State = status.State
+ }
+ }
+
+ return retStatus
+}
diff --git a/modules/convert/user.go b/services/convert/user.go
similarity index 87%
rename from modules/convert/user.go
rename to services/convert/user.go
index dc4a8c49c7769..6b90539fd9814 100644
--- a/modules/convert/user.go
+++ b/services/convert/user.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package convert
@@ -73,6 +72,7 @@ func toUser(user *user_model.User, signed, authed bool) *api.User {
// only site admin will get these information and possibly user himself
if authed {
result.IsAdmin = user.IsAdmin
+ result.LoginName = user.LoginName
result.LastLogin = user.LastLoginUnix.AsTime()
result.Language = user.Language
result.IsActive = user.IsActive
@@ -95,3 +95,12 @@ func User2UserSettings(user *user_model.User) api.UserSettings {
DiffViewStyle: user.DiffViewStyle,
}
}
+
+// ToUserAndPermission return User and its collaboration permission for a repository
+func ToUserAndPermission(user, doer *user_model.User, accessMode perm.AccessMode) api.RepoCollaboratorPermission {
+ return api.RepoCollaboratorPermission{
+ User: ToUser(user, doer),
+ Permission: accessMode.String(),
+ RoleName: accessMode.String(),
+ }
+}
diff --git a/services/convert/user_test.go b/services/convert/user_test.go
new file mode 100644
index 0000000000000..c3ab4187b724b
--- /dev/null
+++ b/services/convert/user_test.go
@@ -0,0 +1,39 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUser_ToUser(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1, IsAdmin: true})
+
+ apiUser := toUser(user1, true, true)
+ assert.True(t, apiUser.IsAdmin)
+ assert.Contains(t, apiUser.AvatarURL, "://")
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2, IsAdmin: false})
+
+ apiUser = toUser(user2, true, true)
+ assert.False(t, apiUser.IsAdmin)
+
+ apiUser = toUser(user1, false, false)
+ assert.False(t, apiUser.IsAdmin)
+ assert.EqualValues(t, api.VisibleTypePublic.String(), apiUser.Visibility)
+
+ user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate})
+
+ apiUser = toUser(user31, true, true)
+ assert.False(t, apiUser.IsAdmin)
+ assert.EqualValues(t, api.VisibleTypePrivate.String(), apiUser.Visibility)
+}
diff --git a/modules/convert/utils.go b/services/convert/utils.go
similarity index 90%
rename from modules/convert/utils.go
rename to services/convert/utils.go
index 52fbcf547f72e..cdce60831c9a9 100644
--- a/modules/convert/utils.go
+++ b/services/convert/utils.go
@@ -1,7 +1,6 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Copyright 2016 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package convert
diff --git a/modules/convert/utils_test.go b/services/convert/utils_test.go
similarity index 87%
rename from modules/convert/utils_test.go
rename to services/convert/utils_test.go
index e0ab15dfd838a..d1ec5980ceabb 100644
--- a/modules/convert/utils_test.go
+++ b/services/convert/utils_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package convert
diff --git a/modules/convert/wiki.go b/services/convert/wiki.go
similarity index 94%
rename from modules/convert/wiki.go
rename to services/convert/wiki.go
index 1112da43f8a62..20d76162c7113 100644
--- a/modules/convert/wiki.go
+++ b/services/convert/wiki.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package convert
diff --git a/services/cron/cron.go b/services/cron/cron.go
index ebbcd75b6df63..bda8f12f122e5 100644
--- a/services/cron/cron.go
+++ b/services/cron/cron.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package cron
@@ -13,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/sync"
+ "code.gitea.io/gitea/modules/translation"
"github.com/gogs/cron"
)
@@ -63,7 +63,7 @@ type TaskTableRow struct {
task *Task
}
-func (t *TaskTableRow) FormatLastMessage(locale string) string {
+func (t *TaskTableRow) FormatLastMessage(locale translation.Locale) string {
if t.Status == "finished" {
return t.task.GetConfig().FormatMessage(locale, t.Name, t.Status, t.LastDoer)
}
diff --git a/services/cron/setting.go b/services/cron/setting.go
index 9b59a562f709a..952a4d17ab658 100644
--- a/services/cron/setting.go
+++ b/services/cron/setting.go
@@ -1,13 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package cron
import (
"time"
- "code.gitea.io/gitea/modules/translation/i18n"
+ "code.gitea.io/gitea/modules/translation"
)
// Config represents a basic configuration interface that cron task
@@ -15,7 +14,7 @@ type Config interface {
IsEnabled() bool
DoRunAtStart() bool
GetSchedule() string
- FormatMessage(locale, name, status, doer string, args ...interface{}) string
+ FormatMessage(locale translation.Locale, name, status, doer string, args ...interface{}) string
DoNoticeOnSuccess() bool
}
@@ -69,9 +68,9 @@ func (b *BaseConfig) DoNoticeOnSuccess() bool {
// FormatMessage returns a message for the task
// Please note the `status` string will be concatenated with `admin.dashboard.cron.` and `admin.dashboard.task.` to provide locale messages. Similarly `name` will be composed with `admin.dashboard.` to provide the locale name for the task.
-func (b *BaseConfig) FormatMessage(locale, name, status, doer string, args ...interface{}) string {
+func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer string, args ...interface{}) string {
realArgs := make([]interface{}, 0, len(args)+2)
- realArgs = append(realArgs, i18n.Tr(locale, "admin.dashboard."+name))
+ realArgs = append(realArgs, locale.Tr("admin.dashboard."+name))
if doer == "" {
realArgs = append(realArgs, "(Cron)")
} else {
@@ -81,7 +80,7 @@ func (b *BaseConfig) FormatMessage(locale, name, status, doer string, args ...in
realArgs = append(realArgs, args...)
}
if doer == "" {
- return i18n.Tr(locale, "admin.dashboard.cron."+status, realArgs...)
+ return locale.Tr("admin.dashboard.cron."+status, realArgs...)
}
- return i18n.Tr(locale, "admin.dashboard.task."+status, realArgs...)
+ return locale.Tr("admin.dashboard.task."+status, realArgs...)
}
diff --git a/services/cron/tasks.go b/services/cron/tasks.go
index 2252ad21e2549..1c5493c8983d7 100644
--- a/services/cron/tasks.go
+++ b/services/cron/tasks.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cron
@@ -10,8 +9,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"
@@ -94,7 +93,7 @@ func (t *Task) RunWithUser(doer *user_model.User, config Config) {
doerName = doer.Name
}
- ctx, _, finished := pm.AddContext(baseCtx, config.FormatMessage("en-US", t.Name, "process", doerName))
+ ctx, _, finished := pm.AddContext(baseCtx, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "process", doerName))
defer finished()
if err := t.fun(ctx, doer, config); err != nil {
@@ -114,7 +113,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("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 +126,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("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)
}
}
@@ -148,7 +147,7 @@ func RegisterTask(name string, config Config, fun func(context.Context, *user_mo
log.Debug("Registering task: %s", name)
i18nKey := "admin.dashboard." + name
- if _, ok := translation.TryTr("en-US", i18nKey); !ok {
+ if value := translation.NewLocale("en-US").Tr(i18nKey); value == i18nKey {
return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey)
}
diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go
index 6f3fcb42c3108..05aef6623d4f7 100644
--- a/services/cron/tasks_basic.go
+++ b/services/cron/tasks_basic.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cron
@@ -9,8 +8,10 @@ import (
"time"
"code.gitea.io/gitea/models"
+ 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"
@@ -57,7 +58,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.GitFsckRepos(ctx, rhcConfig.Timeout, args)
})
}
@@ -109,7 +115,7 @@ func registerDeletedBranchesCleanup() {
OlderThan: 24 * time.Hour,
}, func(ctx context.Context, _ *user_model.User, config Config) error {
realConfig := config.(*OlderThanConfig)
- models.RemoveOldDeletedBranches(ctx, realConfig.OlderThan)
+ git_model.RemoveOldDeletedBranches(ctx, realConfig.OlderThan)
return nil
})
}
@@ -155,7 +161,9 @@ func registerCleanupPackages() {
}
func initBasicTasks() {
- registerUpdateMirrorTask()
+ if setting.Mirror.Enabled {
+ registerUpdateMirrorTask()
+ }
registerRepoHealthCheck()
registerCheckRepoStats()
registerArchiveCleanup()
diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go
index 2d1bf532342b4..520d940edf3c5 100644
--- a/services/cron/tasks_extended.go
+++ b/services/cron/tasks_extended.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package cron
@@ -8,10 +7,12 @@ import (
"context"
"time"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/admin"
+ activities_model "code.gitea.io/gitea/models/activities"
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"
@@ -26,7 +27,7 @@ func registerDeleteInactiveUsers() {
RunAtStart: false,
Schedule: "@annually",
},
- OlderThan: 0 * time.Second,
+ OlderThan: time.Minute * time.Duration(setting.Service.ActiveCodeLives),
}, func(ctx context.Context, _ *user_model.User, config Config) error {
olderThanConfig := config.(*OlderThanConfig)
return user_service.DeleteInactiveUsers(ctx, olderThanConfig.OlderThan)
@@ -59,7 +60,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...)
})
}
@@ -79,7 +85,7 @@ func registerRewriteAllPrincipalKeys() {
RunAtStart: false,
Schedule: "@every 72h",
}, func(_ context.Context, _ *user_model.User, _ Config) error {
- return asymkey_model.RewriteAllPrincipalKeys()
+ return asymkey_model.RewriteAllPrincipalKeys(db.DefaultContext)
})
}
@@ -133,7 +139,7 @@ func registerDeleteOldActions() {
OlderThan: 365 * 24 * time.Hour,
}, func(ctx context.Context, _ *user_model.User, config Config) error {
olderThanConfig := config.(*OlderThanConfig)
- return models.DeleteOldActions(olderThanConfig.OlderThan)
+ return activities_model.DeleteOldActions(olderThanConfig.OlderThan)
})
}
@@ -165,7 +171,49 @@ 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)
+ })
+}
+
+func registerGCLFS() {
+ if !setting.LFS.StartServer {
+ return
+ }
+ type GCLFSConfig struct {
+ OlderThanConfig
+ LastUpdatedMoreThanAgo time.Duration
+ NumberToCheckPerRepo int64
+ ProportionToCheckPerRepo float64
+ }
+
+ RegisterTaskFatal("gc_lfs", &GCLFSConfig{
+ OlderThanConfig: OlderThanConfig{
+ BaseConfig: BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@every 24h",
+ },
+ // Only attempt to garbage collect lfs meta objects older than a week as the order of git lfs upload
+ // and git object upload is not necessarily guaranteed. It's possible to imagine a situation whereby
+ // an LFS object is uploaded but the git branch is not uploaded immediately, or there are some rapid
+ // changes in new branches that might lead to lfs objects becoming temporarily unassociated with git
+ // objects.
+ //
+ // It is likely that a week is potentially excessive but it should definitely be enough that any
+ // unassociated LFS object is genuinely unassociated.
+ OlderThan: 24 * time.Hour * 7,
+ },
+ // Only GC things that haven't been looked at in the past 3 days
+ LastUpdatedMoreThanAgo: 24 * time.Hour * 3,
+ NumberToCheckPerRepo: 100,
+ ProportionToCheckPerRepo: 0.6,
+ }, func(ctx context.Context, _ *user_model.User, config Config) error {
+ gcLFSConfig := config.(*GCLFSConfig)
+ return repo_service.GarbageCollectLFSMetaObjects(ctx, repo_service.GarbageCollectLFSMetaObjectsOptions{
+ AutoFix: true,
+ OlderThan: time.Now().Add(-gcLFSConfig.OlderThan),
+ UpdatedLessRecentlyThan: time.Now().Add(-gcLFSConfig.LastUpdatedMoreThanAgo),
+ })
})
}
@@ -182,4 +230,5 @@ func initExtendedTasks() {
registerDeleteOldActions()
registerUpdateGiteaChecker()
registerDeleteOldSystemNotices()
+ registerGCLFS()
}
diff --git a/services/externalaccount/link.go b/services/externalaccount/link.go
index 95ada225421a1..dcdc57ee489d8 100644
--- a/services/externalaccount/link.go
+++ b/services/externalaccount/link.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package externalaccount
diff --git a/services/externalaccount/user.go b/services/externalaccount/user.go
index e5cd443770c08..87d2e02b48582 100644
--- a/services/externalaccount/user.go
+++ b/services/externalaccount/user.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package externalaccount
diff --git a/services/forms/admin.go b/services/forms/admin.go
index 5abef0550e39a..a749f863f3dc8 100644
--- a/services/forms/admin.go
+++ b/services/forms/admin.go
@@ -1,6 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package forms
@@ -18,7 +17,7 @@ import (
type AdminCreateUserForm struct {
LoginType string `binding:"Required"`
LoginName string
- UserName string `binding:"Required;AlphaDashDot;MaxSize(40)"`
+ UserName string `binding:"Required;Username;MaxSize(40)"`
Email string `binding:"Required;Email;MaxSize(254)"`
Password string `binding:"MaxSize(255)"`
SendNotify bool
@@ -35,7 +34,7 @@ func (f *AdminCreateUserForm) Validate(req *http.Request, errs binding.Errors) b
// AdminEditUserForm form for admin to create user
type AdminEditUserForm struct {
LoginType string `binding:"Required"`
- UserName string `binding:"AlphaDashDot;MaxSize(40)"`
+ UserName string `binding:"Username;MaxSize(40)"`
LoginName string
FullName string `binding:"MaxSize(100)"`
Email string `binding:"Required;Email;MaxSize(254)"`
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index 7e7c75675299b..0cede07f951ee 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -1,6 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package forms
diff --git a/services/forms/org.go b/services/forms/org.go
index dec2dbfa6555a..d7535313713e3 100644
--- a/services/forms/org.go
+++ b/services/forms/org.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package forms
@@ -24,7 +23,7 @@ import (
// CreateOrgForm form for creating organization
type CreateOrgForm struct {
- OrgName string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"`
+ OrgName string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"`
Visibility structs.VisibleType
RepoAdminChangeTeamAccess bool
}
@@ -37,7 +36,7 @@ func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding
// UpdateOrgSettingForm form for updating organization settings
type UpdateOrgSettingForm struct {
- Name string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"`
+ Name string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"`
FullName string `binding:"MaxSize(100)"`
Description string `binding:"MaxSize(255)"`
Website string `binding:"ValidUrl;MaxSize(255)"`
diff --git a/services/forms/package_form.go b/services/forms/package_form.go
new file mode 100644
index 0000000000000..734bb05dc69f7
--- /dev/null
+++ b/services/forms/package_form.go
@@ -0,0 +1,30 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package forms
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/web/middleware"
+
+ "gitea.com/go-chi/binding"
+)
+
+type PackageCleanupRuleForm struct {
+ ID int64
+ Enabled bool
+ Type string `binding:"Required;In(composer,conan,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"`
+ KeepCount int `binding:"In(0,1,5,10,25,50,100)"`
+ KeepPattern string `binding:"RegexPattern"`
+ RemoveDays int `binding:"In(0,7,14,30,60,90,180)"`
+ RemovePattern string `binding:"RegexPattern"`
+ MatchFullName bool
+ Action string `binding:"Required;In(save,remove)"`
+}
+
+func (f *PackageCleanupRuleForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
+ ctx := context.GetContext(req)
+ return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
+}
diff --git a/services/forms/repo_branch_form.go b/services/forms/repo_branch_form.go
index f9262aaede77a..bf1183fc43411 100644
--- a/services/forms/repo_branch_form.go
+++ b/services/forms/repo_branch_form.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package forms
@@ -16,6 +15,7 @@ import (
// NewBranchForm form for creating a new branch
type NewBranchForm struct {
NewBranchName string `binding:"Required;MaxSize(100);GitRefName"`
+ CurrentPath string
CreateTag bool
}
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 80123e9af3228..b7687af2b5af4 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// 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.
+// SPDX-License-Identifier: MIT
package forms
@@ -11,12 +10,13 @@ import (
"strings"
"code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/routers/utils"
+ "code.gitea.io/gitea/services/webhook"
"gitea.com/go-chi/binding"
)
@@ -33,7 +33,7 @@ type CreateRepoForm struct {
UID int64 `binding:"Required"`
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
Private bool
- Description string `binding:"MaxSize(255)"`
+ Description string `binding:"MaxSize(2048)"`
DefaultBranch string `binding:"GitRefName;MaxSize(100)"`
AutoInit bool
Gitignores string
@@ -75,7 +75,7 @@ type MigrateRepoForm struct {
LFS bool `json:"lfs"`
LFSEndpoint string `json:"lfs_endpoint"`
Private bool `json:"private"`
- Description string `json:"description" binding:"MaxSize(255)"`
+ Description string `json:"description" binding:"MaxSize(2048)"`
Wiki bool `json:"wiki"`
Milestones bool `json:"milestones"`
Labels bool `json:"labels"`
@@ -101,7 +101,7 @@ func ParseRemoteAddr(remoteAddr, authUsername, authPassword string) (string, err
strings.HasPrefix(remoteAddr, "git://") {
u, err := url.Parse(remoteAddr)
if err != nil {
- return "", &models.ErrInvalidCloneAddr{IsURLError: true}
+ return "", &models.ErrInvalidCloneAddr{IsURLError: true, Host: remoteAddr}
}
if len(authUsername)+len(authPassword) > 0 {
u.User = url.UserPassword(authUsername, authPassword)
@@ -114,25 +114,27 @@ func ParseRemoteAddr(remoteAddr, authUsername, authPassword string) (string, err
// RepoSettingForm form for changing repository settings
type RepoSettingForm struct {
- RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
- Description string `binding:"MaxSize(255)"`
- Website string `binding:"ValidUrl;MaxSize(255)"`
- Interval string
- MirrorAddress string
- MirrorUsername string
- MirrorPassword string
- LFS bool `form:"mirror_lfs"`
- LFSEndpoint string `form:"mirror_lfs_endpoint"`
- PushMirrorID string
- PushMirrorAddress string
- PushMirrorUsername string
- PushMirrorPassword string
- PushMirrorInterval string
- Private bool
- Template bool
- EnablePrune bool
+ RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
+ Description string `binding:"MaxSize(2048)"`
+ Website string `binding:"ValidUrl;MaxSize(1024)"`
+ Interval string
+ MirrorAddress string
+ MirrorUsername string
+ MirrorPassword string
+ LFS bool `form:"mirror_lfs"`
+ LFSEndpoint string `form:"mirror_lfs_endpoint"`
+ PushMirrorID string
+ PushMirrorAddress string
+ PushMirrorUsername string
+ PushMirrorPassword string
+ PushMirrorSyncOnCommit bool
+ PushMirrorInterval string
+ Private bool
+ Template bool
+ EnablePrune bool
// Advanced settings
+ EnableCode bool
EnableWiki bool
EnableExternalWiki bool
ExternalWikiURL string
@@ -141,8 +143,10 @@ type RepoSettingForm struct {
ExternalTrackerURL string
TrackerURLFormat string
TrackerIssueStyle string
+ ExternalTrackerRegexpPattern string
EnableCloseIssuesViaCommitInAnyBranch bool
EnableProjects bool
+ EnablePackages bool
EnablePulls bool
PullsIgnoreWhitespace bool
PullsAllowMerge bool
@@ -182,7 +186,7 @@ func (f *RepoSettingForm) Validate(req *http.Request, errs binding.Errors) bindi
// ProtectBranchForm form for changing protected branch settings
type ProtectBranchForm struct {
- Protected bool
+ RuleName string `binding:"Required"`
EnablePush string
WhitelistUsers string
WhitelistTeams string
@@ -211,12 +215,12 @@ func (f *ProtectBranchForm) Validate(req *http.Request, errs binding.Errors) bin
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
-// __ __ ___. .__ .__ __
-// / \ / \ ____\_ |__ | |__ | |__ ____ | | __
-// \ \/\/ // __ \| __ \| | \| | \ / _ \| |/ /
-// \ /\ ___/| \_\ \ Y \ Y ( <_> ) <
-// \__/\ / \___ >___ /___| /___| /\____/|__|_ \
-// \/ \/ \/ \/ \/ \/
+// __ __ ___. .__ __
+// / \ / \ ____\_ |__ | |__ ____ ____ | | __
+// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
+// \ /\ ___/| \_\ \ Y ( <_> | <_> ) <
+// \__/\ / \___ >___ /___| /\____/ \____/|__|_ \
+// \/ \/ \/ \/ \/
// WebhookForm form for changing web hook
type WebhookForm struct {
@@ -238,10 +242,12 @@ type WebhookForm struct {
PullRequestComment bool
PullRequestReview bool
PullRequestSync bool
+ Wiki bool
Repository bool
Package bool
Active bool
BranchFilter string `binding:"GlobPattern"`
+ AuthorizationHeader string
}
// PushOnly if the hook will be triggered when push
@@ -301,14 +307,16 @@ type NewSlackHookForm struct {
// Validate validates the fields
func (f *NewSlackHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetContext(req)
+ if !webhook.IsValidSlackChannel(strings.TrimSpace(f.Channel)) {
+ errs = append(errs, binding.Error{
+ FieldNames: []string{"Channel"},
+ Classification: "",
+ Message: ctx.Tr("repo.settings.add_webhook.invalid_channel_name"),
+ })
+ }
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
-// HasInvalidChannel validates the channel name is in the right format
-func (f NewSlackHookForm) HasInvalidChannel() bool {
- return !utils.IsValidSlackChannel(f.Channel)
-}
-
// NewDiscordHookForm form for creating discord hook
type NewDiscordHookForm struct {
PayloadURL string `binding:"Required;ValidUrl"`
@@ -352,7 +360,6 @@ func (f *NewTelegramHookForm) Validate(req *http.Request, errs binding.Errors) b
type NewMatrixHookForm struct {
HomeserverURL string `binding:"Required;ValidUrl"`
RoomID string `binding:"Required"`
- AccessToken string `binding:"Required"`
MessageType int
WebhookForm
}
@@ -422,15 +429,16 @@ func (f *NewPackagistHookForm) Validate(req *http.Request, errs binding.Errors)
// CreateIssueForm form for creating issue
type CreateIssueForm struct {
- Title string `binding:"Required;MaxSize(255)"`
- LabelIDs string `form:"label_ids"`
- AssigneeIDs string `form:"assignee_ids"`
- Ref string `form:"ref"`
- MilestoneID int64
- ProjectID int64
- AssigneeID int64
- Content string
- Files []string
+ Title string `binding:"Required;MaxSize(255)"`
+ LabelIDs string `form:"label_ids"`
+ AssigneeIDs string `form:"assignee_ids"`
+ Ref string `form:"ref"`
+ MilestoneID int64
+ ProjectID int64
+ AssigneeID int64
+ Content string
+ Files []string
+ AllowMaintainerEdit bool
}
// Validate validates the fields
@@ -590,6 +598,7 @@ type MergePullRequestForm struct {
MergeCommitID string // only used for manually-merged
HeadCommitID string `json:"head_commit_id,omitempty"`
ForceMerge *bool `json:"force_merge,omitempty"`
+ MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
DeleteBranchAfterMerge bool `json:"delete_branch_after_merge,omitempty"`
}
@@ -599,31 +608,6 @@ func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
-// SetDefaults if not provided for mergestyle and commit message
-func (f *MergePullRequestForm) SetDefaults(pr *models.PullRequest) (err error) {
- if f.Do == "" {
- f.Do = "merge"
- }
-
- f.MergeTitleField = strings.TrimSpace(f.MergeTitleField)
- if len(f.MergeTitleField) == 0 {
- switch f.Do {
- case "merge", "rebase-merge":
- f.MergeTitleField, err = pr.GetDefaultMergeMessage()
- case "squash":
- f.MergeTitleField, err = pr.GetDefaultSquashMessage()
- }
- }
-
- f.MergeMessageField = strings.TrimSpace(f.MergeMessageField)
- if len(f.MergeMessageField) > 0 {
- f.MergeTitleField += "\n\n" + f.MergeMessageField
- f.MergeMessageField = ""
- }
-
- return
-}
-
// CodeCommentForm form for adding code comments for PRs
type CodeCommentForm struct {
Origin string `binding:"Required;In(timeline,diff)"`
@@ -645,7 +629,7 @@ func (f *CodeCommentForm) Validate(req *http.Request, errs binding.Errors) bindi
// SubmitReviewForm for submitting a finished code review
type SubmitReviewForm struct {
Content string
- Type string `binding:"Required;In(approve,comment,reject)"`
+ Type string
CommitID string
Files []string
}
@@ -656,17 +640,19 @@ func (f *SubmitReviewForm) Validate(req *http.Request, errs binding.Errors) bind
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
-// ReviewType will return the corresponding reviewtype for type
-func (f SubmitReviewForm) ReviewType() models.ReviewType {
+// ReviewType will return the corresponding ReviewType for type
+func (f SubmitReviewForm) ReviewType() issues_model.ReviewType {
switch f.Type {
case "approve":
- return models.ReviewTypeApprove
+ return issues_model.ReviewTypeApprove
case "comment":
- return models.ReviewTypeComment
+ return issues_model.ReviewTypeComment
case "reject":
- return models.ReviewTypeReject
+ return issues_model.ReviewTypeReject
+ case "":
+ return issues_model.ReviewTypeComment // default to comment when doing quick-submit (Ctrl+Enter) on the review form
default:
- return models.ReviewTypeUnknown
+ return issues_model.ReviewTypeUnknown
}
}
@@ -674,7 +660,7 @@ func (f SubmitReviewForm) ReviewType() models.ReviewType {
func (f SubmitReviewForm) HasEmptyContent() bool {
reviewType := f.ReviewType()
- return (reviewType == models.ReviewTypeComment || reviewType == models.ReviewTypeReject) &&
+ return (reviewType == issues_model.ReviewTypeComment || reviewType == issues_model.ReviewTypeReject) &&
len(strings.TrimSpace(f.Content)) == 0
}
@@ -684,6 +670,11 @@ type DismissReviewForm struct {
Message string
}
+// UpdateAllowEditsForm form for changing if PR allows edits from maintainers
+type UpdateAllowEditsForm struct {
+ AllowMaintainerEdit bool
+}
+
// __________ .__
// \______ \ ____ | | ____ _____ ______ ____
// | _// __ \| | _/ __ \\__ \ / ___// __ \
diff --git a/services/forms/repo_form_test.go b/services/forms/repo_form_test.go
index 198437f7ea322..2c5a8e2c0fc52 100644
--- a/services/forms/repo_form_test.go
+++ b/services/forms/repo_form_test.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package forms
diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go
index 337e7fe1ea646..1209d2346f466 100644
--- a/services/forms/repo_tag_form.go
+++ b/services/forms/repo_tag_form.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package forms
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index 405b4a9a490f2..285bc398b26c5 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -1,7 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package forms
@@ -10,6 +9,7 @@ import (
"net/http"
"strings"
+ auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
@@ -40,7 +40,8 @@ type InstallForm struct {
AppURL string `binding:"Required"`
LogRootPath string `binding:"Required"`
- SMTPHost string
+ SMTPAddr string
+ SMTPPort string
SMTPFrom string
SMTPUser string `binding:"OmitEmpty;MaxSize(254)" locale:"install.mailer_user"`
SMTPPasswd string
@@ -59,11 +60,12 @@ type InstallForm struct {
DefaultKeepEmailPrivate bool
DefaultAllowCreateOrganization bool
DefaultEnableTimetracking bool
+ EnableUpdateChecker bool
NoReplyAddress string
PasswordAlgorithm string
- AdminName string `binding:"OmitEmpty;AlphaDashDot;MaxSize(30)" locale:"install.admin_name"`
+ AdminName string `binding:"OmitEmpty;Username;MaxSize(30)" locale:"install.admin_name"`
AdminPasswd string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"`
AdminConfirmPasswd string
AdminEmail string `binding:"OmitEmpty;MinSize(3);MaxSize(254);Include(@)" locale:"install.admin_email"`
@@ -89,12 +91,10 @@ func (f *InstallForm) Validate(req *http.Request, errs binding.Errors) binding.E
// RegisterForm form for registering
type RegisterForm struct {
- UserName string `binding:"Required;AlphaDashDot;MaxSize(40)"`
- Email string `binding:"Required;MaxSize(254)"`
- Password string `binding:"MaxSize(255)"`
- Retype string
- GRecaptchaResponse string `form:"g-recaptcha-response"`
- HcaptchaResponse string `form:"h-captcha-response"`
+ UserName string `binding:"Required;Username;MaxSize(40)"`
+ Email string `binding:"Required;MaxSize(254)"`
+ Password string `binding:"MaxSize(255)"`
+ Retype string
}
// Validate validates the fields
@@ -240,7 +240,7 @@ func (f *IntrospectTokenForm) Validate(req *http.Request, errs binding.Errors) b
// UpdateProfileForm form for updating profile
type UpdateProfileForm struct {
- Name string `binding:"AlphaDashDot;MaxSize(40)"`
+ Name string `binding:"Username;MaxSize(40)"`
FullName string `binding:"MaxSize(100)"`
KeepEmailPrivate bool
Website string `binding:"ValidSiteUrl;MaxSize(255)"`
@@ -364,9 +364,22 @@ func (f *AddKeyForm) Validate(req *http.Request, errs binding.Errors) binding.Er
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
+// AddSecretForm for adding secrets
+type AddSecretForm struct {
+ Title string `binding:"Required;MaxSize(50)"`
+ Content string `binding:"Required"`
+}
+
+// Validate validates the fields
+func (f *AddSecretForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
+ ctx := context.GetContext(req)
+ return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
+}
+
// NewAccessTokenForm form for creating access token
type NewAccessTokenForm struct {
- Name string `binding:"Required;MaxSize(255)"`
+ Name string `binding:"Required;MaxSize(255)"`
+ Scope []string
}
// Validate validates the fields
@@ -375,10 +388,17 @@ func (f *NewAccessTokenForm) Validate(req *http.Request, errs binding.Errors) bi
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
+func (f *NewAccessTokenForm) GetScope() (auth_model.AccessTokenScope, error) {
+ scope := strings.Join(f.Scope, ",")
+ s, err := auth_model.AccessTokenScope(scope).Normalize()
+ return s, err
+}
+
// 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/forms/user_form_auth_openid.go b/services/forms/user_form_auth_openid.go
index fd3368d303a90..f95eb98405a5c 100644
--- a/services/forms/user_form_auth_openid.go
+++ b/services/forms/user_form_auth_openid.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package forms
@@ -27,10 +26,8 @@ func (f *SignInOpenIDForm) Validate(req *http.Request, errs binding.Errors) bind
// SignUpOpenIDForm form for signin up with OpenID
type SignUpOpenIDForm struct {
- UserName string `binding:"Required;AlphaDashDot;MaxSize(40)"`
- Email string `binding:"Required;Email;MaxSize(254)"`
- GRecaptchaResponse string `form:"g-recaptcha-response"`
- HcaptchaResponse string `form:"h-captcha-response"`
+ UserName string `binding:"Required;Username;MaxSize(40)"`
+ Email string `binding:"Required;Email;MaxSize(254)"`
}
// Validate validates the fields
diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go
index e0c26e8ddf880..7eb800a020185 100644
--- a/services/forms/user_form_hidden_comments.go
+++ b/services/forms/user_form_hidden_comments.go
@@ -1,75 +1,74 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package forms
import (
"math/big"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
)
-type hiddenCommentTypeGroupsType map[string][]models.CommentType
+type hiddenCommentTypeGroupsType map[string][]issues_model.CommentType
// hiddenCommentTypeGroups maps the group names to comment types, these group names comes from the Web UI (appearance.tmpl)
var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{
"reference": {
- /*3*/ models.CommentTypeIssueRef,
- /*4*/ models.CommentTypeCommitRef,
- /*5*/ models.CommentTypeCommentRef,
- /*6*/ models.CommentTypePullRef,
+ /*3*/ issues_model.CommentTypeIssueRef,
+ /*4*/ issues_model.CommentTypeCommitRef,
+ /*5*/ issues_model.CommentTypeCommentRef,
+ /*6*/ issues_model.CommentTypePullRef,
},
"label": {
- /*7*/ models.CommentTypeLabel,
+ /*7*/ issues_model.CommentTypeLabel,
},
"milestone": {
- /*8*/ models.CommentTypeMilestone,
+ /*8*/ issues_model.CommentTypeMilestone,
},
"assignee": {
- /*9*/ models.CommentTypeAssignees,
+ /*9*/ issues_model.CommentTypeAssignees,
},
"title": {
- /*10*/ models.CommentTypeChangeTitle,
+ /*10*/ issues_model.CommentTypeChangeTitle,
},
"branch": {
- /*11*/ models.CommentTypeDeleteBranch,
- /*25*/ models.CommentTypeChangeTargetBranch,
+ /*11*/ issues_model.CommentTypeDeleteBranch,
+ /*25*/ issues_model.CommentTypeChangeTargetBranch,
},
"time_tracking": {
- /*12*/ models.CommentTypeStartTracking,
- /*13*/ models.CommentTypeStopTracking,
- /*14*/ models.CommentTypeAddTimeManual,
- /*15*/ models.CommentTypeCancelTracking,
- /*26*/ models.CommentTypeDeleteTimeManual,
+ /*12*/ issues_model.CommentTypeStartTracking,
+ /*13*/ issues_model.CommentTypeStopTracking,
+ /*14*/ issues_model.CommentTypeAddTimeManual,
+ /*15*/ issues_model.CommentTypeCancelTracking,
+ /*26*/ issues_model.CommentTypeDeleteTimeManual,
},
"deadline": {
- /*16*/ models.CommentTypeAddedDeadline,
- /*17*/ models.CommentTypeModifiedDeadline,
- /*18*/ models.CommentTypeRemovedDeadline,
+ /*16*/ issues_model.CommentTypeAddedDeadline,
+ /*17*/ issues_model.CommentTypeModifiedDeadline,
+ /*18*/ issues_model.CommentTypeRemovedDeadline,
},
"dependency": {
- /*19*/ models.CommentTypeAddDependency,
- /*20*/ models.CommentTypeRemoveDependency,
+ /*19*/ issues_model.CommentTypeAddDependency,
+ /*20*/ issues_model.CommentTypeRemoveDependency,
},
"lock": {
- /*23*/ models.CommentTypeLock,
- /*24*/ models.CommentTypeUnlock,
+ /*23*/ issues_model.CommentTypeLock,
+ /*24*/ issues_model.CommentTypeUnlock,
},
"review_request": {
- /*27*/ models.CommentTypeReviewRequest,
+ /*27*/ issues_model.CommentTypeReviewRequest,
},
"pull_request_push": {
- /*29*/ models.CommentTypePullRequestPush,
+ /*29*/ issues_model.CommentTypePullRequestPush,
},
"project": {
- /*30*/ models.CommentTypeProject,
- /*31*/ models.CommentTypeProjectBoard,
+ /*30*/ issues_model.CommentTypeProject,
+ /*31*/ issues_model.CommentTypeProjectBoard,
},
"issue_ref": {
- /*33*/ models.CommentTypeChangeIssueRef,
+ /*33*/ issues_model.CommentTypeChangeIssueRef,
},
}
diff --git a/services/forms/user_form_test.go b/services/forms/user_form_test.go
index 9f67143d12c81..225686f0fea05 100644
--- a/services/forms/user_form_test.go
+++ b/services/forms/user_form_test.go
@@ -1,12 +1,13 @@
// Copyright 2018 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package forms
import (
+ "strconv"
"testing"
+ auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@@ -84,3 +85,28 @@ func TestRegisterForm_IsDomainAllowed_BlocklistedEmail(t *testing.T) {
assert.Equal(t, v.valid, form.IsEmailDomainAllowed())
}
}
+
+func TestNewAccessTokenForm_GetScope(t *testing.T) {
+ tests := []struct {
+ form NewAccessTokenForm
+ scope auth_model.AccessTokenScope
+ expectedErr error
+ }{
+ {
+ form: NewAccessTokenForm{Name: "test", Scope: []string{"repo"}},
+ scope: "repo",
+ },
+ {
+ form: NewAccessTokenForm{Name: "test", Scope: []string{"repo", "user"}},
+ scope: "repo,user",
+ },
+ }
+
+ for i, test := range tests {
+ t.Run(strconv.Itoa(i), func(t *testing.T) {
+ scope, err := test.form.GetScope()
+ assert.Equal(t, test.expectedErr, err)
+ assert.Equal(t, test.scope, scope)
+ })
+ }
+}
diff --git a/services/gitdiff/csv.go b/services/gitdiff/csv.go
index 642c9937ddb8b..5781d7e9094ae 100644
--- a/services/gitdiff/csv.go
+++ b/services/gitdiff/csv.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package gitdiff
diff --git a/services/gitdiff/csv_test.go b/services/gitdiff/csv_test.go
index 2c15b42d915b9..ac53e2d1efaec 100644
--- a/services/gitdiff/csv_test.go
+++ b/services/gitdiff/csv_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package gitdiff
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index 1df16e50167cf..d3ee93ec945b7 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package gitdiff
@@ -15,21 +14,24 @@ import (
"io"
"net/url"
"os"
- "regexp"
"sort"
"strings"
"time"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
+ pull_model "code.gitea.io/gitea/models/pull"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/analyze"
+ "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/translation"
"github.com/sergi/go-diff/diffmatchpatch"
stdcharset "golang.org/x/net/html/charset"
@@ -37,7 +39,7 @@ import (
"golang.org/x/text/transform"
)
-// DiffLineType represents the type of a DiffLine.
+// DiffLineType represents the type of DiffLine.
type DiffLineType uint8
// DiffLineType possible values.
@@ -48,7 +50,7 @@ const (
DiffLineSection
)
-// DiffFileType represents the type of a DiffFile.
+// DiffFileType represents the type of DiffFile.
type DiffFileType uint8
// DiffFileType possible values.
@@ -79,7 +81,7 @@ type DiffLine struct {
Match int
Type DiffLineType
Content string
- Comments []*models.Comment
+ Comments []*issues_model.Comment
SectionInfo *DiffLineSectionInfo
}
@@ -97,12 +99,12 @@ type DiffLineSectionInfo struct {
// BlobExcerptChunkSize represent max lines of excerpt
const BlobExcerptChunkSize = 20
-// GetType returns the type of a DiffLine.
+// GetType returns the type of DiffLine.
func (d *DiffLine) GetType() int {
return int(d.Type)
}
-// CanComment returns whether or not a line can get commented
+// CanComment returns whether a line can get commented
func (d *DiffLine) CanComment() bool {
return len(d.Comments) == 0 && d.Type != DiffLineSection
}
@@ -167,11 +169,11 @@ func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int
}
// escape a line's content or return needed for copy/paste purposes
-func getLineContent(content string) DiffInline {
+func getLineContent(content string, locale translation.Locale) DiffInline {
if len(content) > 0 {
- return DiffInlineWithUnicodeEscape(template.HTML(html.EscapeString(content)))
+ return DiffInlineWithUnicodeEscape(template.HTML(html.EscapeString(content)), locale)
}
- return DiffInline{Content: " "}
+ return DiffInline{EscapeStatus: &charset.EscapeStatus{}, Content: " "}
}
// DiffSection represents a section of a DiffFile.
@@ -188,287 +190,13 @@ var (
codeTagSuffix = []byte(``)
)
-var (
- unfinishedtagRegex = regexp.MustCompile(`<[^>]*$`)
- trailingSpanRegex = regexp.MustCompile(`]?$`)
- entityRegex = regexp.MustCompile(`&[#]*?[0-9[:alpha:]]*$`)
-)
-
-// shouldWriteInline represents combinations where we manually write inline changes
-func shouldWriteInline(diff diffmatchpatch.Diff, lineType DiffLineType) bool {
- if true &&
- diff.Type == diffmatchpatch.DiffEqual ||
- diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd ||
- diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel {
- return true
- }
- return false
-}
-
-func fixupBrokenSpans(diffs []diffmatchpatch.Diff) []diffmatchpatch.Diff {
- // Create a new array to store our fixed up blocks
- fixedup := make([]diffmatchpatch.Diff, 0, len(diffs))
-
- // semantically label some numbers
- const insert, delete, equal = 0, 1, 2
-
- // record the positions of the last type of each block in the fixedup blocks
- last := []int{-1, -1, -1}
- operation := []diffmatchpatch.Operation{diffmatchpatch.DiffInsert, diffmatchpatch.DiffDelete, diffmatchpatch.DiffEqual}
-
- // create a writer for insert and deletes
- toWrite := []strings.Builder{
- {},
- {},
- }
-
- // make some flags for insert and delete
- unfinishedTag := []bool{false, false}
- unfinishedEnt := []bool{false, false}
-
- // store stores the provided text in the writer for the typ
- store := func(text string, typ int) {
- (&(toWrite[typ])).WriteString(text)
- }
-
- // hasStored returns true if there is stored content
- hasStored := func(typ int) bool {
- return (&toWrite[typ]).Len() > 0
- }
-
- // stored will return that content
- stored := func(typ int) string {
- return (&toWrite[typ]).String()
- }
-
- // empty will empty the stored content
- empty := func(typ int) {
- (&toWrite[typ]).Reset()
- }
-
- // pop will remove the stored content appending to a diff block for that typ
- pop := func(typ int, fixedup []diffmatchpatch.Diff) []diffmatchpatch.Diff {
- if hasStored(typ) {
- if last[typ] > last[equal] {
- fixedup[last[typ]].Text += stored(typ)
- } else {
- fixedup = append(fixedup, diffmatchpatch.Diff{
- Type: operation[typ],
- Text: stored(typ),
- })
- }
- empty(typ)
- }
- return fixedup
- }
-
- // Now we walk the provided diffs and check the type of each block in turn
- for _, diff := range diffs {
-
- typ := delete // flag for handling insert or delete typs
- switch diff.Type {
- case diffmatchpatch.DiffEqual:
- // First check if there is anything stored
- if hasStored(insert) || hasStored(delete) {
- // There are two reasons for storing content:
- // 1. Unfinished Entity <- Could be more efficient here by not doing this if we're looking for a tag
- if unfinishedEnt[insert] || unfinishedEnt[delete] {
- // we look for a ';' to finish an entity
- idx := strings.IndexRune(diff.Text, ';')
- if idx >= 0 {
- // if we find a ';' store the preceding content to both insert and delete
- store(diff.Text[:idx+1], insert)
- store(diff.Text[:idx+1], delete)
-
- // and remove it from this block
- diff.Text = diff.Text[idx+1:]
-
- // reset the ent flags
- unfinishedEnt[insert] = false
- unfinishedEnt[delete] = false
- } else {
- // otherwise store it all on insert and delete
- store(diff.Text, insert)
- store(diff.Text, delete)
- // and empty this block
- diff.Text = ""
- }
- }
- // 2. Unfinished Tag
- if unfinishedTag[insert] || unfinishedTag[delete] {
- // we look for a '>' to finish a tag
- idx := strings.IndexRune(diff.Text, '>')
- if idx >= 0 {
- store(diff.Text[:idx+1], insert)
- store(diff.Text[:idx+1], delete)
- diff.Text = diff.Text[idx+1:]
- unfinishedTag[insert] = false
- unfinishedTag[delete] = false
- } else {
- store(diff.Text, insert)
- store(diff.Text, delete)
- diff.Text = ""
- }
- }
-
- // If we've completed the required tag/entities
- if !(unfinishedTag[insert] || unfinishedTag[delete] || unfinishedEnt[insert] || unfinishedEnt[delete]) {
- // pop off the stack
- fixedup = pop(insert, fixedup)
- fixedup = pop(delete, fixedup)
- }
-
- // If that has left this diff block empty then shortcut
- if len(diff.Text) == 0 {
- continue
- }
- }
-
- // check if this block ends in an unfinished tag?
- idx := unfinishedtagRegex.FindStringIndex(diff.Text)
- if idx != nil {
- unfinishedTag[insert] = true
- unfinishedTag[delete] = true
- } else {
- // otherwise does it end in an unfinished entity?
- idx = entityRegex.FindStringIndex(diff.Text)
- if idx != nil {
- unfinishedEnt[insert] = true
- unfinishedEnt[delete] = true
- }
- }
-
- // If there is an unfinished component
- if idx != nil {
- // Store the fragment
- store(diff.Text[idx[0]:], insert)
- store(diff.Text[idx[0]:], delete)
- // and remove it from this block
- diff.Text = diff.Text[:idx[0]]
- }
-
- // If that hasn't left the block empty
- if len(diff.Text) > 0 {
- // store the position of the last equal block and store it in our diffs
- last[equal] = len(fixedup)
- fixedup = append(fixedup, diff)
- }
- continue
- case diffmatchpatch.DiffInsert:
- typ = insert
- fallthrough
- case diffmatchpatch.DiffDelete:
- // First check if there is anything stored for this type
- if hasStored(typ) {
- // if there is prepend it to this block, empty the storage and reset our flags
- diff.Text = stored(typ) + diff.Text
- empty(typ)
- unfinishedEnt[typ] = false
- unfinishedTag[typ] = false
- }
-
- // check if this block ends in an unfinished tag
- idx := unfinishedtagRegex.FindStringIndex(diff.Text)
- if idx != nil {
- unfinishedTag[typ] = true
- } else {
- // otherwise does it end in an unfinished entity
- idx = entityRegex.FindStringIndex(diff.Text)
- if idx != nil {
- unfinishedEnt[typ] = true
- }
- }
-
- // If there is an unfinished component
- if idx != nil {
- // Store the fragment
- store(diff.Text[idx[0]:], typ)
- // and remove it from this block
- diff.Text = diff.Text[:idx[0]]
- }
-
- // If that hasn't left the block empty
- if len(diff.Text) > 0 {
- // if the last block of this type was after the last equal block
- if last[typ] > last[equal] {
- // store this blocks content on that block
- fixedup[last[typ]].Text += diff.Text
- } else {
- // otherwise store the position of the last block of this type and store the block
- last[typ] = len(fixedup)
- fixedup = append(fixedup, diff)
- }
- }
- continue
- }
- }
-
- // pop off any remaining stored content
- fixedup = pop(insert, fixedup)
- fixedup = pop(delete, fixedup)
-
- return fixedup
-}
-
-func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineType) DiffInline {
+func diffToHTML(lineWrapperTags []string, diffs []diffmatchpatch.Diff, lineType DiffLineType) string {
buf := bytes.NewBuffer(nil)
- match := ""
-
- diffs = fixupBrokenSpans(diffs)
-
+ // restore the line wrapper tags and , if necessary
+ for _, tag := range lineWrapperTags {
+ buf.WriteString(tag)
+ }
for _, diff := range diffs {
- if shouldWriteInline(diff, lineType) {
- if len(match) > 0 {
- diff.Text = match + diff.Text
- match = ""
- }
- // Chroma HTML syntax highlighting is done before diffing individual lines in order to maintain consistency.
- // Since inline changes might split in the middle of a chroma span tag or HTML entity, make we manually put it back together
- // before writing so we don't try insert added/removed code spans in the middle of one of those
- // and create broken HTML. This is done by moving incomplete HTML forward until it no longer matches our pattern of
- // a line ending with an incomplete HTML entity or partial/opening .
-
- // EX:
- // diffs[{Type: dmp.DiffDelete, Text: "language }]
-
- // After first iteration
- // diffs[{Type: dmp.DiffDelete, Text: "language "}, //write out
- // {Type: dmp.DiffEqual, Text: ", }]
-
- // After second iteration
- // {Type: dmp.DiffEqual, Text: ""}, // write out
- // {Type: dmp.DiffDelete, Text: ", }]
-
- // Final
- // {Type: dmp.DiffDelete, Text: ", }]
- // end up writing ,
- // Instead of lass="p",
-
- m := trailingSpanRegex.FindStringSubmatchIndex(diff.Text)
- if m != nil {
- match = diff.Text[m[0]:m[1]]
- diff.Text = strings.TrimSuffix(diff.Text, match)
- }
- m = entityRegex.FindStringSubmatchIndex(diff.Text)
- if m != nil {
- match = diff.Text[m[0]:m[1]]
- diff.Text = strings.TrimSuffix(diff.Text, match)
- }
- // Print an existing closing span first before opening added/remove-code span so it doesn't unintentionally close it
- if strings.HasPrefix(diff.Text, " ") {
- buf.WriteString(" ")
- diff.Text = strings.TrimPrefix(diff.Text, "")
- }
- // If we weren't able to fix it then this should avoid broken HTML by not inserting more spans below
- // The previous/next diff section will contain the rest of the tag that is missing here
- if strings.Count(diff.Text, "<") != strings.Count(diff.Text, ">") {
- buf.WriteString(diff.Text)
- continue
- }
- }
switch {
case diff.Type == diffmatchpatch.DiffEqual:
buf.WriteString(diff.Text)
@@ -482,7 +210,10 @@ func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineT
buf.Write(codeTagSuffix)
}
}
- return DiffInlineWithUnicodeEscape(template.HTML(buf.String()))
+ for range lineWrapperTags {
+ buf.WriteString("")
+ }
+ return buf.String()
}
// GetLine gets a specific line by type (add or del) and file line number
@@ -536,26 +267,27 @@ func init() {
// DiffInline is a struct that has a content and escape status
type DiffInline struct {
- EscapeStatus charset.EscapeStatus
+ EscapeStatus *charset.EscapeStatus
Content template.HTML
}
// DiffInlineWithUnicodeEscape makes a DiffInline with hidden unicode characters escaped
-func DiffInlineWithUnicodeEscape(s template.HTML) DiffInline {
- status, content := charset.EscapeControlString(string(s))
+func DiffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) DiffInline {
+ status, content := charset.EscapeControlHTML(string(s), locale)
return DiffInline{EscapeStatus: status, Content: template.HTML(content)}
}
// DiffInlineWithHighlightCode makes a DiffInline with code highlight and hidden unicode characters escaped
-func DiffInlineWithHighlightCode(fileName, language, code string) DiffInline {
- status, content := charset.EscapeControlString(highlight.Code(fileName, language, code))
+func DiffInlineWithHighlightCode(fileName, language, code string, locale translation.Locale) DiffInline {
+ highlighted, _ := highlight.Code(fileName, language, code)
+ status, content := charset.EscapeControlHTML(highlighted, locale)
return DiffInline{EscapeStatus: status, Content: template.HTML(content)}
}
// GetComputedInlineDiffFor computes inline diff for the given line.
-func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) DiffInline {
+func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, locale translation.Locale) DiffInline {
if setting.Git.DisableDiffHighlight {
- return getLineContent(diffLine.Content[1:])
+ return getLineContent(diffLine.Content[1:], locale)
}
var (
@@ -572,55 +304,60 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) Dif
// try to find equivalent diff line. ignore, otherwise
switch diffLine.Type {
case DiffLineSection:
- return getLineContent(diffLine.Content[1:])
+ return getLineContent(diffLine.Content[1:], locale)
case DiffLineAdd:
compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
if compareDiffLine == nil {
- return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:])
+ return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale)
}
diff1 = compareDiffLine.Content
diff2 = diffLine.Content
case DiffLineDel:
compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
if compareDiffLine == nil {
- return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:])
+ return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale)
}
diff1 = diffLine.Content
diff2 = compareDiffLine.Content
default:
if strings.IndexByte(" +-", diffLine.Content[0]) > -1 {
- return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:])
+ return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale)
}
- return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content)
+ return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content, locale)
}
- diffRecord := diffMatchPatch.DiffMain(highlight.Code(diffSection.FileName, language, diff1[1:]), highlight.Code(diffSection.FileName, language, diff2[1:]), true)
- diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
-
- return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type)
+ hcd := newHighlightCodeDiff()
+ diffRecord := hcd.diffWithHighlight(diffSection.FileName, language, diff1[1:], diff2[1:])
+ // it seems that Gitea doesn't need the line wrapper of Chroma, so do not add them back
+ // if the line wrappers are still needed in the future, it can be added back by "diffToHTML(hcd.lineWrapperTags. ...)"
+ diffHTML := diffToHTML(nil, diffRecord, diffLine.Type)
+ return DiffInlineWithUnicodeEscape(template.HTML(diffHTML), locale)
}
// DiffFile represents a file diff.
type DiffFile struct {
- Name string
- OldName string
- Index int
- Addition, Deletion int
- Type DiffFileType
- IsCreated bool
- IsDeleted bool
- IsBin bool
- IsLFSFile bool
- IsRenamed bool
- IsAmbiguous bool
- IsSubmodule bool
- Sections []*DiffSection
- IsIncomplete bool
- IsIncompleteLineTooLong bool
- IsProtected bool
- IsGenerated bool
- IsVendored bool
- Language string
+ Name string
+ NameHash string
+ OldName string
+ Index int
+ Addition, Deletion int
+ Type DiffFileType
+ IsCreated bool
+ IsDeleted bool
+ IsBin bool
+ IsLFSFile bool
+ IsRenamed bool
+ IsAmbiguous bool
+ IsSubmodule bool
+ Sections []*DiffSection
+ IsIncomplete bool
+ IsIncompleteLineTooLong bool
+ IsProtected bool
+ IsGenerated bool
+ IsVendored bool
+ IsViewed bool // User specific
+ HasChangedSinceLastReview bool // User specific
+ Language string
}
// GetType returns type of diff file.
@@ -663,6 +400,18 @@ func (diffFile *DiffFile) GetTailSection(gitRepo *git.Repository, leftCommitID,
return tailSection
}
+// GetDiffFileName returns the name of the diff file, or its old name in case it was deleted
+func (diffFile *DiffFile) GetDiffFileName() string {
+ if diffFile.Name == "" {
+ return diffFile.OldName
+ }
+ return diffFile.Name
+}
+
+func (diffFile *DiffFile) ShouldBeHidden() bool {
+ return diffFile.IsGenerated || diffFile.IsViewed
+}
+
func getCommitFileLineCount(commit *git.Commit, filePath string) int {
blob, err := commit.GetBlobByPath(filePath)
if err != nil {
@@ -677,15 +426,17 @@ func getCommitFileLineCount(commit *git.Commit, filePath string) int {
// Diff represents a difference between two git trees.
type Diff struct {
- Start, End string
- NumFiles, TotalAddition, TotalDeletion int
- Files []*DiffFile
- IsIncomplete bool
+ Start, End string
+ NumFiles int
+ TotalAddition, TotalDeletion int
+ Files []*DiffFile
+ IsIncomplete bool
+ NumViewedFiles int // user-specific
}
// LoadComments loads comments into each line
-func (diff *Diff) LoadComments(ctx context.Context, issue *models.Issue, currentUser *user_model.User) error {
- allComments, err := models.FetchCodeComments(ctx, issue, currentUser)
+func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User) error {
+ allComments, err := issues_model.FetchCodeComments(ctx, issue, currentUser)
if err != nil {
return err
}
@@ -935,7 +686,6 @@ parsingLoop:
break curFileLoop
}
}
-
}
// TODO: There are numerous issues with this:
@@ -947,6 +697,8 @@ parsingLoop:
diffLineTypeBuffers[DiffLineAdd] = new(bytes.Buffer)
diffLineTypeBuffers[DiffLineDel] = new(bytes.Buffer)
for _, f := range diff.Files {
+ f.NameHash = base.EncodeSha1(f.Name)
+
for _, buffer := range diffLineTypeBuffers {
buffer.Reset()
}
@@ -990,7 +742,7 @@ parsingLoop:
func skipToNextDiffHead(input *bufio.Reader) (line string, err error) {
// need to skip until the next cmdDiffHead
- isFragment, wasFragment := false, false
+ var isFragment, wasFragment bool
var lineBytes []byte
for {
lineBytes, isFragment, err = input.ReadLine()
@@ -1015,7 +767,7 @@ func skipToNextDiffHead(input *bufio.Reader) (line string, err error) {
}
line += tail
}
- return
+ return line, err
}
func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio.Reader) (lineBytes []byte, isFragment bool, err error) {
@@ -1199,8 +951,8 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
} else if curFileLFSPrefix && strings.HasPrefix(line[1:], lfs.MetaFileOidPrefix) {
oid := strings.TrimPrefix(line[1:], lfs.MetaFileOidPrefix)
if len(oid) == 64 {
- m := &models.LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}}
- count, err := db.Count(m)
+ m := &git_model.LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}}
+ count, err := db.CountByBean(db.DefaultContext, m)
if err == nil && count > 0 {
curFile.IsBin = true
@@ -1236,8 +988,7 @@ func createDiffFile(diff *Diff, line string) *DiffFile {
rd := strings.NewReader(line[len(cmdDiffHead):] + " ")
curFile.Type = DiffFileChange
- oldNameAmbiguity := false
- newNameAmbiguity := false
+ var oldNameAmbiguity, newNameAmbiguity bool
curFile.OldName, oldNameAmbiguity = readFileName(rd)
curFile.Name, newNameAmbiguity = readFileName(rd)
@@ -1269,7 +1020,7 @@ func readFileName(rd *strings.Reader) (string, bool) {
if char == '"' {
fmt.Fscanf(rd, "%q ", &name)
if len(name) == 0 {
- log.Error("Reader has no file name: %v", rd)
+ log.Error("Reader has no file name: reader=%+v", rd)
return "", true
}
@@ -1291,7 +1042,7 @@ func readFileName(rd *strings.Reader) (string, bool) {
}
}
if len(name) < 2 {
- log.Error("Unable to determine name from reader: %v", rd)
+ log.Error("Unable to determine name from reader: reader=%+v", rd)
return "", true
}
return name[2:], ambiguity
@@ -1305,7 +1056,7 @@ type DiffOptions struct {
MaxLines int
MaxLineCharacters int
MaxFiles int
- WhitespaceBehavior string
+ WhitespaceBehavior git.CmdArg
DirectComparison bool
}
@@ -1331,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 {
@@ -1339,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 {
@@ -1350,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
}
@@ -1360,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()
@@ -1375,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{
@@ -1396,37 +1149,8 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff
}
diff.Start = opts.SkipTo
- var checker *git.CheckAttributeReader
-
- if git.CheckGitVersionAtLeast("1.7.8") == nil {
- indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(opts.AfterCommitID)
- if err == nil {
- defer deleteTemporaryFile()
-
- checker = &git.CheckAttributeReader{
- Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
- Repo: gitRepo,
- IndexFile: indexFilename,
- WorkTree: worktree,
- }
- ctx, cancel := context.WithCancel(gitRepo.Ctx)
- if err := checker.Init(ctx); err != nil {
- log.Error("Unable to open checker for %s. Error: %v", opts.AfterCommitID, err)
- } else {
- go func() {
- err := checker.Run()
- if err != nil && err != ctx.Err() {
- log.Error("Unable to open checker for %s. Error: %v", opts.AfterCommitID, err)
- }
- cancel()
- }()
- }
- defer func() {
- _ = checker.Close()
- cancel()
- }()
- }
- }
+ checker, deferable := gitRepo.CheckAttributeReader(opts.AfterCommitID)
+ defer deferable()
for _, diffFile := range diff.Files {
@@ -1456,8 +1180,6 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff
} else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
diffFile.Language = language
}
- } else {
- log.Error("Unexpected error: %v", err)
}
}
@@ -1479,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 {
@@ -1497,8 +1219,77 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff
return diff, nil
}
+// SyncAndGetUserSpecificDiff is like GetDiff, except that user specific data such as which files the given user has already viewed on the given PR will also be set
+// Additionally, the database asynchronously is updated if files have changed since the last review
+func SyncAndGetUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.PullRequest, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
+ diff, err := GetDiff(gitRepo, opts, files...)
+ if err != nil {
+ return nil, err
+ }
+ review, err := pull_model.GetNewestReviewState(ctx, userID, pull.ID)
+ if err != nil || review == nil || review.UpdatedFiles == nil {
+ return diff, err
+ }
+
+ latestCommit := opts.AfterCommitID
+ if latestCommit == "" {
+ latestCommit = pull.HeadBranch // opts.AfterCommitID is preferred because it handles PRs from forks correctly and the branch name doesn't
+ }
+
+ 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 {
+ 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)
+outer:
+ for _, diffFile := range diff.Files {
+ fileViewedState := review.UpdatedFiles[diffFile.GetDiffFileName()]
+
+ // Check whether it was previously detected that the file has changed since the last review
+ if fileViewedState == pull_model.HasChanged {
+ diffFile.HasChangedSinceLastReview = true
+ continue
+ }
+
+ filename := diffFile.GetDiffFileName()
+
+ // Check explicitly whether the file has changed since the last review
+ for _, changedFile := range changedFiles {
+ diffFile.HasChangedSinceLastReview = filename == changedFile
+ if diffFile.HasChangedSinceLastReview {
+ filesChangedSinceLastDiff[filename] = pull_model.HasChanged
+ continue outer // We don't want to check if the file is viewed here as that would fold the file, which is in this case unwanted
+ }
+ }
+ // Check whether the file has already been viewed
+ if fileViewedState == pull_model.Viewed {
+ diffFile.IsViewed = true
+ diff.NumViewedFiles++
+ }
+ }
+
+ // Explicitly store files that have changed in the database, if any is present at all.
+ // This has the benefit that the "Has Changed" attribute will be present as long as the user does not explicitly mark this file as viewed, so it will even survive a page reload after marking another file as viewed.
+ // On the other hand, this means that even if a commit reverting an unseen change is committed, the file will still be seen as changed.
+ if len(filesChangedSinceLastDiff) > 0 {
+ err := pull_model.UpdateReviewState(ctx, review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff)
+ if err != nil {
+ log.Warn("Could not update review for user %d, pull %d, commit %s and the changed files %v: %v", review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff, err)
+ return nil, err
+ }
+ }
+
+ return diff, err
+}
+
// CommentAsDiff returns c.Patch as *Diff
-func CommentAsDiff(c *models.Comment) (*Diff, error) {
+func CommentAsDiff(c *issues_model.Comment) (*Diff, error) {
diff, err := ParsePatch(setting.Git.MaxGitDiffLines,
setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch), "")
if err != nil {
@@ -1516,7 +1307,7 @@ func CommentAsDiff(c *models.Comment) (*Diff, error) {
}
// CommentMustAsDiff executes AsDiff and logs the error instead of returning
-func CommentMustAsDiff(c *models.Comment) *Diff {
+func CommentMustAsDiff(c *issues_model.Comment) *Diff {
if c == nil {
return nil
}
@@ -1533,7 +1324,7 @@ func CommentMustAsDiff(c *models.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",
@@ -1542,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 3457785e5de60..267f0e4cff6ad 100644
--- a/services/gitdiff/gitdiff_test.go
+++ b/services/gitdiff/gitdiff_test.go
@@ -1,109 +1,41 @@
// 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.
+// SPDX-License-Identifier: MIT
package gitdiff
import (
"fmt"
- "html/template"
"strconv"
"strings"
"testing"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
dmp "github.com/sergi/go-diff/diffmatchpatch"
"github.com/stretchr/testify/assert"
- "gopkg.in/ini.v1"
)
-func assertEqual(t *testing.T, s1 string, s2 template.HTML) {
- if s1 != string(s2) {
- t.Errorf("Did not receive expected results:\nExpected: %s\nActual: %s", s1, s2)
- }
-}
-
func TestDiffToHTML(t *testing.T) {
- setting.Cfg = ini.Empty()
- assertEqual(t, "foo bar biz", diffToHTML("", []dmp.Diff{
+ assert.Equal(t, "foo bar biz", diffToHTML(nil, []dmp.Diff{
{Type: dmp.DiffEqual, Text: "foo "},
{Type: dmp.DiffInsert, Text: "bar"},
{Type: dmp.DiffDelete, Text: " baz"},
{Type: dmp.DiffEqual, Text: " biz"},
- }, DiffLineAdd).Content)
+ }, DiffLineAdd))
- assertEqual(t, "foo bar biz", diffToHTML("", []dmp.Diff{
+ assert.Equal(t, "foo bar biz", diffToHTML(nil, []dmp.Diff{
{Type: dmp.DiffEqual, Text: "foo "},
{Type: dmp.DiffDelete, Text: "bar"},
{Type: dmp.DiffInsert, Text: " baz"},
{Type: dmp.DiffEqual, Text: " biz"},
- }, DiffLineDel).Content)
-
- assertEqual(t, "if ! nohl && ( lexer != nil || r . GuessLanguage ) { ", diffToHTML("", []dmp.Diff{
- {Type: dmp.DiffEqual, Text: "if ! nohl && ( lexer != nil"},
- {Type: dmp.DiffInsert, Text: " || r . GuessLanguage )"},
- {Type: dmp.DiffEqual, Text: " { "},
- }, DiffLineAdd).Content)
-
- assertEqual(t, "tagURL := fmt . Sprintf ( "## [%s](%s/%s/%s/%s?q=&type=all&state=closed&milestone=%d) - %s" , ge . Milestone\" , ge . BaseURL , ge . Owner , ge . Repo , from , milestoneID , time . Now ( ) . Format ( "2006-01-02" ) ) ", diffToHTML("", []dmp.Diff{
- {Type: dmp.DiffEqual, Text: "tagURL := fmt . Sprintf ( "## [%s](%s/%s/%s/%s?q=&type=all&state=closed&milestone=%d) - %s" , ge . Milestone\""},
- {Type: dmp.DiffInsert, Text: "f\">getGiteaTagURL ( client"},
- {Type: dmp.DiffEqual, Text: " , ge . BaseURL , ge . Owner , ge . Repo , "},
- {Type: dmp.DiffDelete, Text: "from , milestoneID , time . Now ( ) . Format ( "2006-01-02" )"},
- {Type: dmp.DiffInsert, Text: "ge . Milestone , from , milestoneID"},
- {Type: dmp.DiffEqual, Text: " ) "},
- }, DiffLineDel).Content)
-
- assertEqual(t, "r . WrapperRenderer ( w , language , true , attrs , false ) ", diffToHTML("", []dmp.Diff{
- {Type: dmp.DiffEqual, Text: "r . WrapperRenderer ( w , "},
- {Type: dmp.DiffDelete, Text: "language , true , attrs"},
- {Type: dmp.DiffEqual, Text: " , false ) "},
- }, DiffLineDel).Content)
-
- assertEqual(t, "language , true , attrs , false ) ", diffToHTML("", []dmp.Diff{
- {Type: dmp.DiffInsert, Text: "language, true , attrs"},
- {Type: dmp.DiffEqual, Text: " , false ) "},
- }, DiffLineAdd).Content)
-
- assertEqual(t, "print ( " // " , sys . argv ) ", diffToHTML("", []dmp.Diff{
- {Type: dmp.DiffEqual, Text: "print "},
- {Type: dmp.DiffInsert, Text: "( "},
- {Type: dmp.DiffEqual, Text: "" // " , sys . argv "},
- {Type: dmp.DiffInsert, Text: ") "},
- }, DiffLineAdd).Content)
-
- assertEqual(t, "sh 'useradd -u $(stat -c "%u" .gitignore) jenkins' ", diffToHTML("", []dmp.Diff{
- {Type: dmp.DiffEqual, Text: "sh "},
- {Type: dmp.DiffDelete, Text: "4;useradd -u 111 jenkins""},
- {Type: dmp.DiffInsert, Text: "9;useradd -u $(stat -c "%u" .gitignore) jenkins'"},
- {Type: dmp.DiffEqual, Text: ";"},
- }, DiffLineAdd).Content)
-
- assertEqual(t, " <h4 class="release-list-title df ac" > ", diffToHTML("", []dmp.Diff{
- {Type: dmp.DiffEqual, Text: " <h"},
- {Type: dmp.DiffInsert, Text: "4 class="},
- {Type: dmp.DiffEqual, Text: "3"},
- {Type: dmp.DiffInsert, Text: "4;release-list-title df ac""},
- {Type: dmp.DiffEqual, Text: "> "},
- }, DiffLineAdd).Content)
+ }, DiffLineDel))
}
func TestParsePatch_skipTo(t *testing.T) {
@@ -592,7 +524,6 @@ index 0000000..6bb8f39
if err != nil {
t.Errorf("ParsePatch failed: %s", err)
}
- println(result)
diff2 := `diff --git "a/A \\ B" "b/A \\ B"
--- "a/A \\ B"
@@ -669,8 +600,8 @@ func setupDefaultDiff() *Diff {
func TestDiff_LoadComments(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue)
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
diff := setupDefaultDiff()
assert.NoError(t, diff.LoadComments(db.DefaultContext, issue, user))
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 2)
@@ -678,15 +609,15 @@ func TestDiff_LoadComments(t *testing.T) {
func TestDiffLine_CanComment(t *testing.T) {
assert.False(t, (&DiffLine{Type: DiffLineSection}).CanComment())
- assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*models.Comment{{Content: "bla"}}}).CanComment())
+ assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*issues_model.Comment{{Content: "bla"}}}).CanComment())
assert.True(t, (&DiffLine{Type: DiffLineAdd}).CanComment())
assert.True(t, (&DiffLine{Type: DiffLineDel}).CanComment())
assert.True(t, (&DiffLine{Type: DiffLinePlain}).CanComment())
}
func TestDiffLine_GetCommentSide(t *testing.T) {
- assert.Equal(t, "previous", (&DiffLine{Comments: []*models.Comment{{Line: -3}}}).GetCommentSide())
- assert.Equal(t, "proposed", (&DiffLine{Comments: []*models.Comment{{Line: 3}}}).GetCommentSide())
+ assert.Equal(t, "previous", (&DiffLine{Comments: []*issues_model.Comment{{Line: -3}}}).GetCommentSide())
+ assert.Equal(t, "proposed", (&DiffLine{Comments: []*issues_model.Comment{{Line: 3}}}).GetCommentSide())
}
func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
@@ -695,7 +626,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",
@@ -712,18 +643,6 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
}
}
-func TestDiffToHTML_14231(t *testing.T) {
- setting.Cfg = ini.Empty()
- diffRecord := diffMatchPatch.DiffMain(highlight.Code("main.v", "", " run()\n"), highlight.Code("main.v", "", " run(db)\n"), true)
- diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
-
- expected := ` run ( db )
- `
- output := diffToHTML("main.v", diffRecord, DiffLineAdd)
-
- assertEqual(t, expected, output.Content)
-}
-
func TestNoCrashes(t *testing.T) {
type testcase struct {
gitdiff string
diff --git a/services/gitdiff/highlightdiff.go b/services/gitdiff/highlightdiff.go
new file mode 100644
index 0000000000000..f1e2b1d3cb31a
--- /dev/null
+++ b/services/gitdiff/highlightdiff.go
@@ -0,0 +1,222 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gitdiff
+
+import (
+ "strings"
+
+ "code.gitea.io/gitea/modules/highlight"
+
+ "github.com/sergi/go-diff/diffmatchpatch"
+)
+
+// token is a html tag or entity, eg: "", " ", "<"
+func extractHTMLToken(s string) (before, token, after string, valid bool) {
+ for pos1 := 0; pos1 < len(s); pos1++ {
+ if s[pos1] == '<' {
+ pos2 := strings.IndexByte(s[pos1:], '>')
+ if pos2 == -1 {
+ return "", "", s, false
+ }
+ return s[:pos1], s[pos1 : pos1+pos2+1], s[pos1+pos2+1:], true
+ } else if s[pos1] == '&' {
+ pos2 := strings.IndexByte(s[pos1:], ';')
+ if pos2 == -1 {
+ return "", "", s, false
+ }
+ return s[:pos1], s[pos1 : pos1+pos2+1], s[pos1+pos2+1:], true
+ }
+ }
+ return "", "", s, true
+}
+
+// highlightCodeDiff is used to do diff with highlighted HTML code.
+// It totally depends on Chroma's valid HTML output and its structure, do not use these functions for other purposes.
+// The HTML tags and entities will be replaced by Unicode placeholders: "{TEXT} " => "\uE000{TEXT}\uE001"
+// These Unicode placeholders are friendly to the diff.
+// Then after diff, the placeholders in diff result will be recovered to the HTML tags and entities.
+// It's guaranteed that the tags in final diff result are paired correctly.
+type highlightCodeDiff struct {
+ placeholderBegin rune
+ placeholderMaxCount int
+ placeholderIndex int
+ placeholderTokenMap map[rune]string
+ tokenPlaceholderMap map[string]rune
+
+ placeholderOverflowCount int
+
+ lineWrapperTags []string
+}
+
+func newHighlightCodeDiff() *highlightCodeDiff {
+ return &highlightCodeDiff{
+ placeholderBegin: rune(0x100000), // Plane 16: Supplementary Private Use Area B (U+100000..U+10FFFD)
+ placeholderMaxCount: 64000,
+ placeholderTokenMap: map[rune]string{},
+ tokenPlaceholderMap: map[string]rune{},
+ }
+}
+
+// nextPlaceholder returns 0 if no more placeholder can be used
+// the diff is done line by line, usually there are only a few (no more than 10) placeholders in one line
+// so the placeholderMaxCount is impossible to be exhausted in real cases.
+func (hcd *highlightCodeDiff) nextPlaceholder() rune {
+ for hcd.placeholderIndex < hcd.placeholderMaxCount {
+ r := hcd.placeholderBegin + rune(hcd.placeholderIndex)
+ hcd.placeholderIndex++
+ // only use non-existing (not used by code) rune as placeholders
+ if _, ok := hcd.placeholderTokenMap[r]; !ok {
+ return r
+ }
+ }
+ return 0 // no more available placeholder
+}
+
+func (hcd *highlightCodeDiff) isInPlaceholderRange(r rune) bool {
+ return hcd.placeholderBegin <= r && r < hcd.placeholderBegin+rune(hcd.placeholderMaxCount)
+}
+
+func (hcd *highlightCodeDiff) collectUsedRunes(code string) {
+ for _, r := range code {
+ if hcd.isInPlaceholderRange(r) {
+ // put the existing rune (used by code) in map, then this rune won't be used a placeholder anymore.
+ hcd.placeholderTokenMap[r] = ""
+ }
+ }
+}
+
+func (hcd *highlightCodeDiff) diffWithHighlight(filename, language, codeA, codeB string) []diffmatchpatch.Diff {
+ hcd.collectUsedRunes(codeA)
+ hcd.collectUsedRunes(codeB)
+
+ highlightCodeA, _ := highlight.Code(filename, language, codeA)
+ highlightCodeB, _ := highlight.Code(filename, language, codeB)
+
+ highlightCodeA = hcd.convertToPlaceholders(highlightCodeA)
+ highlightCodeB = hcd.convertToPlaceholders(highlightCodeB)
+
+ diffs := diffMatchPatch.DiffMain(highlightCodeA, highlightCodeB, true)
+ diffs = diffMatchPatch.DiffCleanupEfficiency(diffs)
+
+ for i := range diffs {
+ hcd.recoverOneDiff(&diffs[i])
+ }
+ return diffs
+}
+
+// convertToPlaceholders totally depends on Chroma's valid HTML output and its structure, do not use these functions for other purposes.
+func (hcd *highlightCodeDiff) convertToPlaceholders(htmlCode string) string {
+ var tagStack []string
+ res := strings.Builder{}
+
+ firstRunForLineTags := hcd.lineWrapperTags == nil
+
+ var beforeToken, token string
+ var valid bool
+
+ // the standard chroma highlight HTML is " ... "
+ for {
+ beforeToken, token, htmlCode, valid = extractHTMLToken(htmlCode)
+ if !valid || token == "" {
+ break
+ }
+ // write the content before the token into result string, and consume the token in the string
+ res.WriteString(beforeToken)
+
+ // the line wrapper tags should be removed before diff
+ if strings.HasPrefix(token, `")
+ continue
+ }
+
+ var tokenInMap string
+ if strings.HasSuffix(token, "") { // for closing tag
+ if len(tagStack) == 0 {
+ break // invalid diff result, no opening tag but see closing tag
+ }
+ // make sure the closing tag in map is related to the open tag, to make the diff algorithm can match the opening/closing tags
+ // the closing tag will be recorded in the map by key " " for ""
+ tokenInMap = token + ""
+ tagStack = tagStack[:len(tagStack)-1]
+ } else if token[0] == '<' { // for opening tag
+ tokenInMap = token
+ tagStack = append(tagStack, token)
+ } else if token[0] == '&' { // for html entity
+ tokenInMap = token
+ } // else: impossible
+
+ // remember the placeholder and token in the map
+ placeholder, ok := hcd.tokenPlaceholderMap[tokenInMap]
+ if !ok {
+ placeholder = hcd.nextPlaceholder()
+ if placeholder != 0 {
+ hcd.tokenPlaceholderMap[tokenInMap] = placeholder
+ hcd.placeholderTokenMap[placeholder] = tokenInMap
+ }
+ }
+
+ if placeholder != 0 {
+ res.WriteRune(placeholder) // use the placeholder to replace the token
+ } else {
+ // unfortunately, all private use runes has been exhausted, no more placeholder could be used, no more converting
+ // usually, the exhausting won't occur in real cases, the magnitude of used placeholders is not larger than that of the CSS classes outputted by chroma.
+ hcd.placeholderOverflowCount++
+ if strings.HasPrefix(token, "&") {
+ // when the token is a html entity, something must be outputted even if there is no placeholder.
+ res.WriteRune(0xFFFD) // replacement character TODO: how to handle this case more gracefully?
+ res.WriteString(token[1:]) // still output the entity code part, otherwise there will be no diff result.
+ }
+ }
+ }
+
+ // write the remaining string
+ res.WriteString(htmlCode)
+ return res.String()
+}
+
+func (hcd *highlightCodeDiff) recoverOneDiff(diff *diffmatchpatch.Diff) {
+ sb := strings.Builder{}
+ var tagStack []string
+
+ for _, r := range diff.Text {
+ token, ok := hcd.placeholderTokenMap[r]
+ if !ok || token == "" {
+ sb.WriteRune(r) // if the rune is not a placeholder, write it as it is
+ continue
+ }
+ var tokenToRecover string
+ if strings.HasPrefix(token, "") { // for closing tag
+ // only get the tag itself, ignore the trailing comment (for how the comment is generated, see the code in `convert` function)
+ tokenToRecover = token[:strings.IndexByte(token, '>')+1]
+ if len(tagStack) == 0 {
+ continue // if no opening tag in stack yet, skip the closing tag
+ }
+ tagStack = tagStack[:len(tagStack)-1]
+ } else if token[0] == '<' { // for opening tag
+ tokenToRecover = token
+ tagStack = append(tagStack, token)
+ } else if token[0] == '&' { // for html entity
+ tokenToRecover = token
+ } // else: impossible
+ sb.WriteString(tokenToRecover)
+ }
+
+ if len(tagStack) > 0 {
+ // close all opening tags
+ for i := len(tagStack) - 1; i >= 0; i-- {
+ tagToClose := tagStack[i]
+ // get the closing tag " " from "" or ""
+ pos := strings.IndexAny(tagToClose, " >")
+ if pos != -1 {
+ sb.WriteString("" + tagToClose[1:pos] + ">")
+ } // else: impossible. every tag was pushed into the stack by the code above and is valid HTML opening tag
+ }
+ }
+
+ diff.Text = sb.String()
+}
diff --git a/services/gitdiff/highlightdiff_test.go b/services/gitdiff/highlightdiff_test.go
new file mode 100644
index 0000000000000..545a060e20491
--- /dev/null
+++ b/services/gitdiff/highlightdiff_test.go
@@ -0,0 +1,125 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gitdiff
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/sergi/go-diff/diffmatchpatch"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDiffWithHighlight(t *testing.T) {
+ hcd := newHighlightCodeDiff()
+ diffs := hcd.diffWithHighlight(
+ "main.v", "",
+ " run('<>')\n",
+ " run(db)\n",
+ )
+
+ expected := ` run ( ' < > ' ) `
+ output := diffToHTML(nil, diffs, DiffLineDel)
+ assert.Equal(t, expected, output)
+
+ expected = ` run ( db ) `
+ output = diffToHTML(nil, diffs, DiffLineAdd)
+ assert.Equal(t, expected, output)
+
+ hcd = newHighlightCodeDiff()
+ hcd.placeholderTokenMap['O'] = ""
+ hcd.placeholderTokenMap['C'] = " "
+ diff := diffmatchpatch.Diff{}
+
+ diff.Text = "OC"
+ hcd.recoverOneDiff(&diff)
+ assert.Equal(t, " ", diff.Text)
+
+ diff.Text = "O"
+ hcd.recoverOneDiff(&diff)
+ assert.Equal(t, " ", diff.Text)
+
+ diff.Text = "C"
+ hcd.recoverOneDiff(&diff)
+ assert.Equal(t, "", diff.Text)
+}
+
+func TestDiffWithHighlightPlaceholder(t *testing.T) {
+ hcd := newHighlightCodeDiff()
+ diffs := hcd.diffWithHighlight(
+ "main.js", "",
+ "a='\U00100000'",
+ "a='\U0010FFFD''",
+ )
+ assert.Equal(t, "", hcd.placeholderTokenMap[0x00100000])
+ assert.Equal(t, "", hcd.placeholderTokenMap[0x0010FFFD])
+
+ expected := fmt.Sprintf(`a = ' %s '`, "\U00100000")
+ output := diffToHTML(hcd.lineWrapperTags, diffs, DiffLineDel)
+ assert.Equal(t, expected, output)
+
+ hcd = newHighlightCodeDiff()
+ diffs = hcd.diffWithHighlight(
+ "main.js", "",
+ "a='\U00100000'",
+ "a='\U0010FFFD'",
+ )
+ expected = fmt.Sprintf(`a = ' %s '`, "\U0010FFFD")
+ output = diffToHTML(nil, diffs, DiffLineAdd)
+ assert.Equal(t, expected, output)
+}
+
+func TestDiffWithHighlightPlaceholderExhausted(t *testing.T) {
+ hcd := newHighlightCodeDiff()
+ hcd.placeholderMaxCount = 0
+ diffs := hcd.diffWithHighlight(
+ "main.js", "",
+ "'",
+ ``,
+ )
+ output := diffToHTML(nil, diffs, DiffLineDel)
+ expected := fmt.Sprintf(`%s#39; `, "\uFFFD")
+ assert.Equal(t, expected, output)
+
+ hcd = newHighlightCodeDiff()
+ hcd.placeholderMaxCount = 0
+ diffs = hcd.diffWithHighlight(
+ "main.js", "",
+ "a < b",
+ "a > b",
+ )
+ output = diffToHTML(nil, diffs, DiffLineDel)
+ expected = fmt.Sprintf(`a %sl t; b`, "\uFFFD")
+ assert.Equal(t, expected, output)
+
+ output = diffToHTML(nil, diffs, DiffLineAdd)
+ expected = fmt.Sprintf(`a %sg t; b`, "\uFFFD")
+ assert.Equal(t, expected, output)
+}
+
+func TestDiffWithHighlightTagMatch(t *testing.T) {
+ totalOverflow := 0
+ for i := 0; i < 100; i++ {
+ hcd := newHighlightCodeDiff()
+ hcd.placeholderMaxCount = i
+ diffs := hcd.diffWithHighlight(
+ "main.js", "",
+ "a='1'",
+ "b='2'",
+ )
+ totalOverflow += hcd.placeholderOverflowCount
+
+ output := diffToHTML(nil, diffs, DiffLineDel)
+ c1 := strings.Count(output, " 0 && len(ref.Name) > 0 {
- refRepo, err = models.GetRepositoryFromMatch(ref.Owner, ref.Name)
+ refRepo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ref.Owner, ref.Name)
if err != nil {
+ if repo_model.IsErrRepoNotExist(err) {
+ log.Warn("Repository referenced in commit but does not exist: %v", err)
+ } else {
+ log.Error("repo_model.GetRepositoryByOwnerAndName: %v", err)
+ }
continue
}
} else {
@@ -130,16 +138,15 @@ func UpdateIssuesCommit(doer *user_model.User, repo *repo_model.Repository, comm
continue
}
- perm, err := models.GetUserRepoPermission(refRepo, doer)
+ perm, err := access_model.GetUserRepoPermission(db.DefaultContext, refRepo, doer)
if err != nil {
return err
}
key := markKey{ID: refIssue.ID, Action: ref.Action}
- if refMarked[key] {
+ if !refMarked.Add(key) {
continue
}
- refMarked[key] = true
// FIXME: this kind of condition is all over the code, it should be consolidated in a single place
canclose := perm.IsAdmin() || perm.IsOwner() || perm.CanWriteIssuesOrPulls(refIssue.IsPull) || refIssue.PosterID == doer.ID
@@ -151,7 +158,7 @@ func UpdateIssuesCommit(doer *user_model.User, repo *repo_model.Repository, comm
}
message := fmt.Sprintf(`%s `, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
- if err = models.CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil {
+ if err = CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil {
return err
}
diff --git a/services/issue/commit_test.go b/services/issue/commit_test.go
index 37283a78907f5..590e7adce99f2 100644
--- a/services/issue/commit_test.go
+++ b/services/issue/commit_test.go
@@ -1,13 +1,13 @@
// 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.
+// SPDX-License-Identifier: MIT
package issue
import (
"testing"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
+ issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -46,24 +46,24 @@ func TestUpdateIssuesCommit(t *testing.T) {
},
}
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repo.Owner = user
- commentBean := &models.Comment{
- Type: models.CommentTypeCommitRef,
+ commentBean := &issues_model.Comment{
+ Type: issues_model.CommentTypeCommitRef,
CommitSHA: "abcdef1",
PosterID: user.ID,
IssueID: 1,
}
- issueBean := &models.Issue{RepoID: repo.ID, Index: 4}
+ issueBean := &issues_model.Issue{RepoID: repo.ID, Index: 4}
unittest.AssertNotExistsBean(t, commentBean)
- unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
+ unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
unittest.AssertExistsAndLoadBean(t, commentBean)
unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
- unittest.CheckConsistencyFor(t, &models.Action{})
+ unittest.CheckConsistencyFor(t, &activities_model.Action{})
// Test that push to a non-default branch closes no issue.
pushCommits = []*repository.PushCommit{
@@ -76,21 +76,21 @@ func TestUpdateIssuesCommit(t *testing.T) {
Message: "close #1",
},
}
- repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository)
- commentBean = &models.Comment{
- Type: models.CommentTypeCommitRef,
+ repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+ commentBean = &issues_model.Comment{
+ Type: issues_model.CommentTypeCommitRef,
CommitSHA: "abcdef1",
PosterID: user.ID,
IssueID: 6,
}
- issueBean = &models.Issue{RepoID: repo.ID, Index: 1}
+ issueBean = &issues_model.Issue{RepoID: repo.ID, Index: 1}
unittest.AssertNotExistsBean(t, commentBean)
- unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1")
+ unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1")
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, "non-existing-branch"))
unittest.AssertExistsAndLoadBean(t, commentBean)
unittest.AssertNotExistsBean(t, issueBean, "is_closed=1")
- unittest.CheckConsistencyFor(t, &models.Action{})
+ unittest.CheckConsistencyFor(t, &activities_model.Action{})
pushCommits = []*repository.PushCommit{
{
@@ -102,21 +102,21 @@ func TestUpdateIssuesCommit(t *testing.T) {
Message: "close " + setting.AppURL + repo.FullName() + "/pulls/1",
},
}
- repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository)
- commentBean = &models.Comment{
- Type: models.CommentTypeCommitRef,
+ repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+ commentBean = &issues_model.Comment{
+ Type: issues_model.CommentTypeCommitRef,
CommitSHA: "abcdef3",
PosterID: user.ID,
IssueID: 6,
}
- issueBean = &models.Issue{RepoID: repo.ID, Index: 1}
+ issueBean = &issues_model.Issue{RepoID: repo.ID, Index: 1}
unittest.AssertNotExistsBean(t, commentBean)
- unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1")
+ unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 1}, "is_closed=1")
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
unittest.AssertExistsAndLoadBean(t, commentBean)
unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
- unittest.CheckConsistencyFor(t, &models.Action{})
+ unittest.CheckConsistencyFor(t, &activities_model.Action{})
}
func TestUpdateIssuesCommit_Colon(t *testing.T) {
@@ -132,21 +132,21 @@ func TestUpdateIssuesCommit_Colon(t *testing.T) {
},
}
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repo.Owner = user
- issueBean := &models.Issue{RepoID: repo.ID, Index: 4}
+ issueBean := &issues_model.Issue{RepoID: repo.ID, Index: 4}
- unittest.AssertNotExistsBean(t, &models.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
+ unittest.AssertNotExistsBean(t, &issues_model.Issue{RepoID: repo.ID, Index: 2}, "is_closed=1")
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
- unittest.CheckConsistencyFor(t, &models.Action{})
+ unittest.CheckConsistencyFor(t, &activities_model.Action{})
}
func TestUpdateIssuesCommit_Issue5957(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Test that push to a non-default branch closes an issue.
pushCommits := []*repository.PushCommit{
@@ -160,27 +160,27 @@ func TestUpdateIssuesCommit_Issue5957(t *testing.T) {
},
}
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
- commentBean := &models.Comment{
- Type: models.CommentTypeCommitRef,
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+ commentBean := &issues_model.Comment{
+ Type: issues_model.CommentTypeCommitRef,
CommitSHA: "abcdef1",
PosterID: user.ID,
IssueID: 7,
}
- issueBean := &models.Issue{RepoID: repo.ID, Index: 2, ID: 7}
+ issueBean := &issues_model.Issue{RepoID: repo.ID, Index: 2, ID: 7}
unittest.AssertNotExistsBean(t, commentBean)
unittest.AssertNotExistsBean(t, issueBean, "is_closed=1")
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, "non-existing-branch"))
unittest.AssertExistsAndLoadBean(t, commentBean)
unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
- unittest.CheckConsistencyFor(t, &models.Action{})
+ unittest.CheckConsistencyFor(t, &activities_model.Action{})
}
func TestUpdateIssuesCommit_AnotherRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Test that a push to default branch closes issue in another repo
// If the user also has push permissions to that repo
@@ -195,27 +195,27 @@ func TestUpdateIssuesCommit_AnotherRepo(t *testing.T) {
},
}
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
- commentBean := &models.Comment{
- Type: models.CommentTypeCommitRef,
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+ commentBean := &issues_model.Comment{
+ Type: issues_model.CommentTypeCommitRef,
CommitSHA: "abcdef1",
PosterID: user.ID,
IssueID: 1,
}
- issueBean := &models.Issue{RepoID: 1, Index: 1, ID: 1}
+ issueBean := &issues_model.Issue{RepoID: 1, Index: 1, ID: 1}
unittest.AssertNotExistsBean(t, commentBean)
unittest.AssertNotExistsBean(t, issueBean, "is_closed=1")
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
unittest.AssertExistsAndLoadBean(t, commentBean)
unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
- unittest.CheckConsistencyFor(t, &models.Action{})
+ unittest.CheckConsistencyFor(t, &activities_model.Action{})
}
func TestUpdateIssuesCommit_AnotherRepo_FullAddress(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Test that a push to default branch closes issue in another repo
// If the user also has push permissions to that repo
@@ -230,27 +230,27 @@ func TestUpdateIssuesCommit_AnotherRepo_FullAddress(t *testing.T) {
},
}
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
- commentBean := &models.Comment{
- Type: models.CommentTypeCommitRef,
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+ commentBean := &issues_model.Comment{
+ Type: issues_model.CommentTypeCommitRef,
CommitSHA: "abcdef1",
PosterID: user.ID,
IssueID: 1,
}
- issueBean := &models.Issue{RepoID: 1, Index: 1, ID: 1}
+ issueBean := &issues_model.Issue{RepoID: 1, Index: 1, ID: 1}
unittest.AssertNotExistsBean(t, commentBean)
unittest.AssertNotExistsBean(t, issueBean, "is_closed=1")
assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch))
unittest.AssertExistsAndLoadBean(t, commentBean)
unittest.AssertExistsAndLoadBean(t, issueBean, "is_closed=1")
- unittest.CheckConsistencyFor(t, &models.Action{})
+ unittest.CheckConsistencyFor(t, &activities_model.Action{})
}
func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10}).(*user_model.User)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
// Test that a push with close reference *can not* close issue
// If the committer doesn't have push rights in that repo
@@ -273,21 +273,21 @@ func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) {
},
}
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 6}).(*repo_model.Repository)
- commentBean := &models.Comment{
- Type: models.CommentTypeCommitRef,
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 6})
+ commentBean := &issues_model.Comment{
+ Type: issues_model.CommentTypeCommitRef,
CommitSHA: "abcdef3",
PosterID: user.ID,
IssueID: 6,
}
- commentBean2 := &models.Comment{
- Type: models.CommentTypeCommitRef,
+ commentBean2 := &issues_model.Comment{
+ Type: issues_model.CommentTypeCommitRef,
CommitSHA: "abcdef4",
PosterID: user.ID,
IssueID: 6,
}
- issueBean := &models.Issue{RepoID: 3, Index: 1, ID: 6}
+ issueBean := &issues_model.Issue{RepoID: 3, Index: 1, ID: 6}
unittest.AssertNotExistsBean(t, commentBean)
unittest.AssertNotExistsBean(t, commentBean2)
@@ -296,5 +296,5 @@ func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) {
unittest.AssertNotExistsBean(t, commentBean)
unittest.AssertNotExistsBean(t, commentBean2)
unittest.AssertNotExistsBean(t, issueBean, "is_closed=1")
- unittest.CheckConsistencyFor(t, &models.Action{})
+ unittest.CheckConsistencyFor(t, &activities_model.Action{})
}
diff --git a/services/issue/content.go b/services/issue/content.go
index a60878479b391..819ac3f20f6ba 100644
--- a/services/issue/content.go
+++ b/services/issue/content.go
@@ -1,24 +1,24 @@
// 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.
+// SPDX-License-Identifier: MIT
package issue
import (
- "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/notification"
)
// ChangeContent changes issue content, as the given user.
-func ChangeContent(issue *models.Issue, doer *user_model.User, content string) (err error) {
+func ChangeContent(issue *issues_model.Issue, doer *user_model.User, content string) (err error) {
oldContent := issue.Content
- if err := models.ChangeIssueContent(issue, doer, content); err != nil {
+ if err := issues_model.ChangeIssueContent(issue, doer, content); err != nil {
return err
}
- notification.NotifyIssueChangeContent(doer, issue, oldContent)
+ notification.NotifyIssueChangeContent(db.DefaultContext, doer, issue, oldContent)
return nil
}
diff --git a/services/issue/issue.go b/services/issue/issue.go
index 6bc39599793d8..b91ee4fc18b07 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -1,24 +1,27 @@
// 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.
+// SPDX-License-Identifier: MIT
package issue
import (
"fmt"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
"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"
- "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/storage"
)
// NewIssue creates new issue with labels for repository.
-func NewIssue(repo *repo_model.Repository, issue *models.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
- if err := models.NewIssue(repo, issue, labelIDs, uuids); err != nil {
+func NewIssue(repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
+ if err := issues_model.NewIssue(repo, issue, labelIDs, uuids); err != nil {
return err
}
@@ -28,46 +31,46 @@ func NewIssue(repo *repo_model.Repository, issue *models.Issue, labelIDs []int64
}
}
- mentions, err := models.FindAndUpdateIssueMentions(db.DefaultContext, issue, issue.Poster, issue.Content)
+ mentions, err := issues_model.FindAndUpdateIssueMentions(db.DefaultContext, issue, issue.Poster, issue.Content)
if err != nil {
return err
}
- notification.NotifyNewIssue(issue, mentions)
+ notification.NotifyNewIssue(db.DefaultContext, issue, mentions)
if len(issue.Labels) > 0 {
- notification.NotifyIssueChangeLabels(issue.Poster, issue, issue.Labels, nil)
+ notification.NotifyIssueChangeLabels(db.DefaultContext, issue.Poster, issue, issue.Labels, nil)
}
if issue.Milestone != nil {
- notification.NotifyIssueChangeMilestone(issue.Poster, issue, 0)
+ notification.NotifyIssueChangeMilestone(db.DefaultContext, issue.Poster, issue, 0)
}
return nil
}
// ChangeTitle changes the title of this issue, as the given user.
-func ChangeTitle(issue *models.Issue, doer *user_model.User, title string) (err error) {
+func ChangeTitle(issue *issues_model.Issue, doer *user_model.User, title string) (err error) {
oldTitle := issue.Title
issue.Title = title
- if err = models.ChangeIssueTitle(issue, doer, oldTitle); err != nil {
+ if err = issues_model.ChangeIssueTitle(issue, doer, oldTitle); err != nil {
return
}
- notification.NotifyIssueChangeTitle(doer, issue, oldTitle)
+ notification.NotifyIssueChangeTitle(db.DefaultContext, doer, issue, oldTitle)
return nil
}
// ChangeIssueRef changes the branch of this issue, as the given user.
-func ChangeIssueRef(issue *models.Issue, doer *user_model.User, ref string) error {
+func ChangeIssueRef(issue *issues_model.Issue, doer *user_model.User, ref string) error {
oldRef := issue.Ref
issue.Ref = ref
- if err := models.ChangeIssueRef(issue, doer, oldRef); err != nil {
+ if err := issues_model.ChangeIssueRef(issue, doer, oldRef); err != nil {
return err
}
- notification.NotifyIssueChangeRef(doer, issue, oldRef)
+ notification.NotifyIssueChangeRef(db.DefaultContext, doer, issue, oldRef)
return nil
}
@@ -78,7 +81,7 @@ func ChangeIssueRef(issue *models.Issue, doer *user_model.User, ref string) erro
// "assignees" (array): Logins for Users to assign to this issue.
// Pass one or more user logins to replace the set of assignees on this Issue.
// Send an empty array ([]) to clear all assignees from the Issue.
-func UpdateAssignees(issue *models.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
+func UpdateAssignees(issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) {
var allNewAssignees []*user_model.User
// Keep the old assignee thingy for compatibility reasons
@@ -99,7 +102,7 @@ func UpdateAssignees(issue *models.Issue, oneAssignee string, multipleAssignees
// Loop through all assignees to add them
for _, assigneeName := range multipleAssignees {
- assignee, err := user_model.GetUserByName(assigneeName)
+ assignee, err := user_model.GetUserByName(db.DefaultContext, assigneeName)
if err != nil {
return err
}
@@ -124,46 +127,46 @@ func UpdateAssignees(issue *models.Issue, oneAssignee string, multipleAssignees
}
}
- return
+ return err
}
// DeleteIssue deletes an issue
-func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *models.Issue) error {
+func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue) error {
// load issue before deleting it
- if err := issue.LoadAttributes(); err != nil {
+ if err := issue.LoadAttributes(gitRepo.Ctx); err != nil {
return err
}
- if err := issue.LoadPullRequest(); err != nil {
+ if err := issue.LoadPullRequest(gitRepo.Ctx); err != nil {
return err
}
// delete entries in database
- if err := models.DeleteIssue(issue); err != nil {
+ if err := deleteIssue(issue); err != nil {
return err
}
// delete pull request related git data
if issue.IsPull {
- if err := gitRepo.RemoveReference(fmt.Sprintf("%s%d", git.PullPrefix, issue.PullRequest.Index)); err != nil {
+ if err := gitRepo.RemoveReference(fmt.Sprintf("%s%d/head", git.PullPrefix, issue.PullRequest.Index)); err != nil {
return err
}
}
- notification.NotifyDeleteIssue(doer, issue)
+ notification.NotifyDeleteIssue(gitRepo.Ctx, doer, issue)
return nil
}
// AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue.
// Also checks for access of assigned user
-func AddAssigneeIfNotAssigned(issue *models.Issue, doer *user_model.User, assigneeID int64) (err error) {
- assignee, err := user_model.GetUserByID(assigneeID)
+func AddAssigneeIfNotAssigned(issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (err error) {
+ assignee, err := user_model.GetUserByID(db.DefaultContext, assigneeID)
if err != nil {
return err
}
// Check if the user is already assigned
- isAssigned, err := models.IsUserAssignedToIssue(issue, assignee)
+ isAssigned, err := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, assignee)
if err != nil {
return err
}
@@ -172,12 +175,12 @@ func AddAssigneeIfNotAssigned(issue *models.Issue, doer *user_model.User, assign
return nil
}
- valid, err := models.CanBeAssigned(assignee, issue.Repo, issue.IsPull)
+ valid, err := access_model.CanBeAssigned(db.DefaultContext, assignee, issue.Repo, issue.IsPull)
if err != nil {
return err
}
if !valid {
- return models.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name}
+ return repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name}
}
_, _, err = ToggleAssignee(issue, doer, assigneeID)
@@ -190,14 +193,100 @@ func AddAssigneeIfNotAssigned(issue *models.Issue, doer *user_model.User, assign
// GetRefEndNamesAndURLs retrieves the ref end names (e.g. refs/heads/branch-name -> branch-name)
// and their respective URLs.
-func GetRefEndNamesAndURLs(issues []*models.Issue, repoLink string) (map[int64]string, map[int64]string) {
+func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[int64]string, map[int64]string) {
issueRefEndNames := make(map[int64]string, len(issues))
issueRefURLs := make(map[int64]string, len(issues))
for _, issue := range issues {
if issue.Ref != "" {
issueRefEndNames[issue.ID] = git.RefEndName(issue.Ref)
- issueRefURLs[issue.ID] = git.RefURL(repoLink, util.PathEscapeSegments(issue.Ref))
+ issueRefURLs[issue.ID] = git.RefURL(repoLink, issue.Ref)
}
}
return issueRefEndNames, issueRefURLs
}
+
+// deleteIssue deletes the issue
+func deleteIssue(issue *issues_model.Issue) error {
+ ctx, committer, err := db.TxContext(db.DefaultContext)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ e := db.GetEngine(ctx)
+ if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
+ return err
+ }
+
+ // update the total issue numbers
+ if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil {
+ return err
+ }
+ // if the issue is closed, update the closed issue numbers
+ if issue.IsClosed {
+ if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil {
+ 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
+ }
+
+ // find attachments related to this issue and remove them
+ if err := issue.LoadAttributes(ctx); err != nil {
+ return err
+ }
+
+ for i := range issue.Attachments {
+ system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath())
+ }
+
+ // delete all database data still assigned to this issue
+ if err := issues_model.DeleteInIssue(ctx, issue.ID,
+ &issues_model.ContentHistory{},
+ &issues_model.Comment{},
+ &issues_model.IssueLabel{},
+ &issues_model.IssueDependency{},
+ &issues_model.IssueAssignees{},
+ &issues_model.IssueUser{},
+ &activities_model.Notification{},
+ &issues_model.Reaction{},
+ &issues_model.IssueWatch{},
+ &issues_model.Stopwatch{},
+ &issues_model.TrackedTime{},
+ &project_model.ProjectIssue{},
+ &repo_model.Attachment{},
+ &issues_model.PullRequest{},
+ ); err != nil {
+ return err
+ }
+
+ // References to this issue in other issues
+ if _, err := db.DeleteByBean(ctx, &issues_model.Comment{
+ RefIssueID: issue.ID,
+ }); err != nil {
+ return err
+ }
+
+ // Delete dependencies for issues in other repositories
+ if _, err := db.DeleteByBean(ctx, &issues_model.IssueDependency{
+ DependencyID: issue.ID,
+ }); err != nil {
+ return err
+ }
+
+ // delete from dependent issues
+ if _, err := db.DeleteByBean(ctx, &issues_model.Comment{
+ DependentIssueID: issue.ID,
+ }); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
diff --git a/services/issue/issue_test.go b/services/issue/issue_test.go
index caae773616a7f..b67d2e2e79a25 100644
--- a/services/issue/issue_test.go
+++ b/services/issue/issue_test.go
@@ -1,19 +1,22 @@
// 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.
+// SPDX-License-Identifier: MIT
package issue
import (
"testing"
- "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestGetRefEndNamesAndURLs(t *testing.T) {
- issues := []*models.Issue{
+ issues := []*issues_model.Issue{
{ID: 1, Ref: "refs/heads/branch1"},
{ID: 2, Ref: "refs/tags/tag1"},
{ID: 3, Ref: "c0ffee"},
@@ -28,3 +31,56 @@ func TestGetRefEndNamesAndURLs(t *testing.T) {
3: repoLink + "/src/commit/c0ffee",
}, urls)
}
+
+func TestIssue_DeleteIssue(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ issueIDs, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 5, len(issueIDs))
+
+ issue := &issues_model.Issue{
+ RepoID: 1,
+ ID: issueIDs[2],
+ }
+
+ err = deleteIssue(issue)
+ assert.NoError(t, err)
+ issueIDs, err = issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 4, len(issueIDs))
+
+ // check attachment removal
+ attachments, err := repo_model.GetAttachmentsByIssueID(db.DefaultContext, 4)
+ assert.NoError(t, err)
+ issue, err = issues_model.GetIssueByID(db.DefaultContext, 4)
+ assert.NoError(t, err)
+ err = deleteIssue(issue)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 2, len(attachments))
+ for i := range attachments {
+ attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, attachments[i].UUID)
+ assert.Error(t, err)
+ assert.True(t, repo_model.IsErrAttachmentNotExist(err))
+ assert.Nil(t, attachment)
+ }
+
+ // check issue dependencies
+ user, err := user_model.GetUserByID(db.DefaultContext, 1)
+ assert.NoError(t, err)
+ issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
+ assert.NoError(t, err)
+ issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2)
+ assert.NoError(t, err)
+ err = issues_model.CreateIssueDependency(user, issue1, issue2)
+ assert.NoError(t, err)
+ left, err := issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
+ assert.NoError(t, err)
+ assert.False(t, left)
+
+ err = deleteIssue(issue2)
+ assert.NoError(t, err)
+ left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
+ assert.NoError(t, err)
+ assert.True(t, left)
+}
diff --git a/services/issue/label.go b/services/issue/label.go
index e72e1cb521cd2..c18abbfcda3d2 100644
--- a/services/issue/label.go
+++ b/services/issue/label.go
@@ -1,83 +1,93 @@
// 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.
+// SPDX-License-Identifier: MIT
package issue
import (
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/notification"
)
// ClearLabels clears all of an issue's labels
-func ClearLabels(issue *models.Issue, doer *user_model.User) (err error) {
- if err = models.ClearIssueLabels(issue, doer); err != nil {
+func ClearLabels(issue *issues_model.Issue, doer *user_model.User) (err error) {
+ if err = issues_model.ClearIssueLabels(issue, doer); err != nil {
return
}
- notification.NotifyIssueClearLabels(doer, issue)
+ notification.NotifyIssueClearLabels(db.DefaultContext, doer, issue)
return nil
}
// AddLabel adds a new label to the issue.
-func AddLabel(issue *models.Issue, doer *user_model.User, label *models.Label) error {
- if err := models.NewIssueLabel(issue, label, doer); err != nil {
+func AddLabel(issue *issues_model.Issue, doer *user_model.User, label *issues_model.Label) error {
+ if err := issues_model.NewIssueLabel(issue, label, doer); err != nil {
return err
}
- notification.NotifyIssueChangeLabels(doer, issue, []*models.Label{label}, nil)
+ notification.NotifyIssueChangeLabels(db.DefaultContext, doer, issue, []*issues_model.Label{label}, nil)
return nil
}
// AddLabels adds a list of new labels to the issue.
-func AddLabels(issue *models.Issue, doer *user_model.User, labels []*models.Label) error {
- if err := models.NewIssueLabels(issue, labels, doer); err != nil {
+func AddLabels(issue *issues_model.Issue, doer *user_model.User, labels []*issues_model.Label) error {
+ if err := issues_model.NewIssueLabels(issue, labels, doer); err != nil {
return err
}
- notification.NotifyIssueChangeLabels(doer, issue, labels, nil)
+ notification.NotifyIssueChangeLabels(db.DefaultContext, doer, issue, labels, nil)
return nil
}
// RemoveLabel removes a label from issue by given ID.
-func RemoveLabel(issue *models.Issue, doer *user_model.User, label *models.Label) error {
- if err := issue.LoadRepo(db.DefaultContext); err != nil {
+func RemoveLabel(issue *issues_model.Issue, doer *user_model.User, label *issues_model.Label) error {
+ ctx, committer, err := db.TxContext(db.DefaultContext)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err := issue.LoadRepo(ctx); err != nil {
return err
}
- perm, err := models.GetUserRepoPermission(issue.Repo, doer)
+ perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
if err != nil {
return err
}
if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
if label.OrgID > 0 {
- return models.ErrOrgLabelNotExist{}
+ return issues_model.ErrOrgLabelNotExist{}
}
- return models.ErrRepoLabelNotExist{}
+ return issues_model.ErrRepoLabelNotExist{}
+ }
+
+ if err := issues_model.DeleteIssueLabel(ctx, issue, label, doer); err != nil {
+ return err
}
- if err := models.DeleteIssueLabel(issue, label, doer); err != nil {
+ if err := committer.Commit(); err != nil {
return err
}
- notification.NotifyIssueChangeLabels(doer, issue, nil, []*models.Label{label})
+ notification.NotifyIssueChangeLabels(db.DefaultContext, doer, issue, nil, []*issues_model.Label{label})
return nil
}
// ReplaceLabels removes all current labels and add new labels to the issue.
-func ReplaceLabels(issue *models.Issue, doer *user_model.User, labels []*models.Label) error {
- old, err := models.GetLabelsByIssueID(issue.ID)
+func ReplaceLabels(issue *issues_model.Issue, doer *user_model.User, labels []*issues_model.Label) error {
+ old, err := issues_model.GetLabelsByIssueID(db.DefaultContext, issue.ID)
if err != nil {
return err
}
- if err := models.ReplaceIssueLabels(issue, labels, doer); err != nil {
+ if err := issues_model.ReplaceIssueLabels(issue, labels, doer); err != nil {
return err
}
- notification.NotifyIssueChangeLabels(doer, issue, labels, old)
+ notification.NotifyIssueChangeLabels(db.DefaultContext, doer, issue, labels, old)
return nil
}
diff --git a/services/issue/label_test.go b/services/issue/label_test.go
index 73e30e894f80e..af220601f145c 100644
--- a/services/issue/label_test.go
+++ b/services/issue/label_test.go
@@ -1,13 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package issue
import (
"testing"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -27,15 +26,15 @@ func TestIssue_AddLabels(t *testing.T) {
}
for _, test := range tests {
assert.NoError(t, unittest.PrepareTestDatabase())
- issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: test.issueID}).(*models.Issue)
- labels := make([]*models.Label, len(test.labelIDs))
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: test.issueID})
+ labels := make([]*issues_model.Label, len(test.labelIDs))
for i, labelID := range test.labelIDs {
- labels[i] = unittest.AssertExistsAndLoadBean(t, &models.Label{ID: labelID}).(*models.Label)
+ labels[i] = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID})
}
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID}).(*user_model.User)
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID})
assert.NoError(t, AddLabels(issue, doer, labels))
for _, labelID := range test.labelIDs {
- unittest.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: test.issueID, LabelID: labelID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: test.issueID, LabelID: labelID})
}
}
}
@@ -53,10 +52,10 @@ func TestIssue_AddLabel(t *testing.T) {
}
for _, test := range tests {
assert.NoError(t, unittest.PrepareTestDatabase())
- issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: test.issueID}).(*models.Issue)
- label := unittest.AssertExistsAndLoadBean(t, &models.Label{ID: test.labelID}).(*models.Label)
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID}).(*user_model.User)
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: test.issueID})
+ label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: test.labelID})
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID})
assert.NoError(t, AddLabel(issue, doer, label))
- unittest.AssertExistsAndLoadBean(t, &models.IssueLabel{IssueID: test.issueID, LabelID: test.labelID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: test.issueID, LabelID: test.labelID})
}
}
diff --git a/services/issue/main_test.go b/services/issue/main_test.go
index 689ae744f6819..0f2427122feb0 100644
--- a/services/issue/main_test.go
+++ b/services/issue/main_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package issue
diff --git a/services/issue/milestone.go b/services/issue/milestone.go
index 287f8ae2854e6..a9be8bd887871 100644
--- a/services/issue/milestone.go
+++ b/services/issue/milestone.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package issue
@@ -8,15 +7,25 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/notification"
)
-func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *models.Issue, oldMilestoneID int64) error {
- if err := models.UpdateIssueCols(ctx, issue, "milestone_id"); err != nil {
+func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) error {
+ // Only check if milestone exists if we don't remove it.
+ if issue.MilestoneID > 0 {
+ has, err := issues_model.HasMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
+ if err != nil {
+ return fmt.Errorf("HasMilestoneByRepoID: %w", err)
+ }
+ if !has {
+ return fmt.Errorf("HasMilestoneByRepoID: issue doesn't exist")
+ }
+ }
+
+ if err := issues_model.UpdateIssueCols(ctx, issue, "milestone_id"); err != nil {
return err
}
@@ -37,15 +46,15 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *mo
return err
}
- opts := &models.CreateCommentOptions{
- Type: models.CommentTypeMilestone,
+ opts := &issues_model.CreateCommentOptions{
+ Type: issues_model.CommentTypeMilestone,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
OldMilestoneID: oldMilestoneID,
MilestoneID: issue.MilestoneID,
}
- if _, err := models.CreateCommentCtx(ctx, opts); err != nil {
+ if _, err := issues_model.CreateComment(ctx, opts); err != nil {
return err
}
}
@@ -54,8 +63,8 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *mo
}
// ChangeMilestoneAssign changes assignment of milestone for issue.
-func ChangeMilestoneAssign(issue *models.Issue, doer *user_model.User, oldMilestoneID int64) (err error) {
- ctx, committer, err := db.TxContext()
+func ChangeMilestoneAssign(issue *issues_model.Issue, doer *user_model.User, oldMilestoneID int64) (err error) {
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
@@ -66,10 +75,10 @@ func ChangeMilestoneAssign(issue *models.Issue, doer *user_model.User, oldMilest
}
if err = committer.Commit(); err != nil {
- return fmt.Errorf("Commit: %v", err)
+ return fmt.Errorf("Commit: %w", err)
}
- notification.NotifyIssueChangeMilestone(doer, issue, oldMilestoneID)
+ notification.NotifyIssueChangeMilestone(db.DefaultContext, doer, issue, oldMilestoneID)
return nil
}
diff --git a/services/issue/milestone_test.go b/services/issue/milestone_test.go
index 80e37a8acd3e1..069117d1f1c25 100644
--- a/services/issue/milestone_test.go
+++ b/services/issue/milestone_test.go
@@ -1,13 +1,11 @@
// 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.
+// SPDX-License-Identifier: MIT
package issue
import (
"testing"
- "code.gitea.io/gitea/models"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -17,19 +15,19 @@ import (
func TestChangeMilestoneAssign(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{RepoID: 1}).(*models.Issue)
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 1})
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
assert.NotNil(t, issue)
assert.NotNil(t, doer)
oldMilestoneID := issue.MilestoneID
issue.MilestoneID = 2
assert.NoError(t, ChangeMilestoneAssign(issue, doer, oldMilestoneID))
- unittest.AssertExistsAndLoadBean(t, &models.Comment{
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
IssueID: issue.ID,
- Type: models.CommentTypeMilestone,
+ Type: issues_model.CommentTypeMilestone,
MilestoneID: issue.MilestoneID,
OldMilestoneID: oldMilestoneID,
})
- unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &models.Issue{})
+ unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &issues_model.Issue{})
}
diff --git a/services/issue/status.go b/services/issue/status.go
index 1af4508b09f57..782ce0bd9675c 100644
--- a/services/issue/status.go
+++ b/services/issue/status.go
@@ -1,23 +1,30 @@
// 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.
+// SPDX-License-Identifier: MIT
package issue
import (
- "code.gitea.io/gitea/models"
+ "context"
+
"code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
)
// ChangeStatus changes issue status to open or closed.
-func ChangeStatus(issue *models.Issue, doer *user_model.User, closed bool) error {
- comment, err := models.ChangeIssueStatus(issue, doer, closed)
+func ChangeStatus(issue *issues_model.Issue, doer *user_model.User, closed bool) error {
+ return changeStatusCtx(db.DefaultContext, issue, doer, closed)
+}
+
+// changeStatusCtx changes issue status to open or closed.
+// TODO: if context is not db.DefaultContext we get a deadlock!!!
+func changeStatusCtx(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, closed bool) error {
+ comment, err := issues_model.ChangeIssueStatus(ctx, issue, doer, closed)
if err != nil {
- if models.IsErrDependenciesLeft(err) && closed {
- if err := models.FinishIssueStopwatchIfPossible(db.DefaultContext, doer, issue); err != nil {
+ if issues_model.IsErrDependenciesLeft(err) && closed {
+ if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil {
log.Error("Unable to stop stopwatch for issue[%d]#%d: %v", issue.ID, issue.Index, err)
}
}
@@ -25,12 +32,12 @@ func ChangeStatus(issue *models.Issue, doer *user_model.User, closed bool) error
}
if closed {
- if err := models.FinishIssueStopwatchIfPossible(db.DefaultContext, doer, issue); err != nil {
+ if err := issues_model.FinishIssueStopwatchIfPossible(ctx, doer, issue); err != nil {
return err
}
}
- notification.NotifyIssueChangeStatus(doer, issue, comment, closed)
+ notification.NotifyIssueChangeStatus(ctx, doer, issue, comment, closed)
return nil
}
diff --git a/services/lfs/locks.go b/services/lfs/locks.go
index fa51470d62626..d963d9ab574fb 100644
--- a/services/lfs/locks.go
+++ b/services/lfs/locks.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package lfs
@@ -9,20 +8,20 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
- "code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/json"
lfs_module "code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/convert"
)
-func handleLockListOut(ctx *context.Context, repo *repo_model.Repository, lock *models.LFSLock, err error) {
+func handleLockListOut(ctx *context.Context, repo *repo_model.Repository, lock *git_model.LFSLock, err error) {
if err != nil {
- if models.IsErrLFSLockNotExist(err) {
+ if git_model.IsErrLFSLockNotExist(err) {
ctx.JSON(http.StatusOK, api.LFSLockList{
Locks: []*api.LFSLock{},
})
@@ -40,7 +39,7 @@ func handleLockListOut(ctx *context.Context, repo *repo_model.Repository, lock *
return
}
ctx.JSON(http.StatusOK, api.LFSLockList{
- Locks: []*api.LFSLock{convert.ToLFSLock(lock)},
+ Locks: []*api.LFSLock{convert.ToLFSLock(ctx, lock)},
})
}
@@ -48,7 +47,7 @@ func handleLockListOut(ctx *context.Context, repo *repo_model.Repository, lock *
func GetListLockHandler(ctx *context.Context) {
rv := getRequestContext(ctx)
- repository, err := repo_model.GetRepositoryByOwnerAndName(rv.User, rv.Repo)
+ repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rv.User, rv.Repo)
if err != nil {
log.Debug("Could not find repository: %s/%s - %s", rv.User, rv.Repo, err)
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
@@ -57,7 +56,7 @@ func GetListLockHandler(ctx *context.Context) {
})
return
}
- repository.MustOwner()
+ repository.MustOwner(ctx)
authenticated := authenticate(ctx, repository, rv.Authorization, true, false)
if !authenticated {
@@ -88,8 +87,8 @@ func GetListLockHandler(ctx *context.Context) {
})
return
}
- lock, err := models.GetLFSLockByID(v)
- if err != nil && !models.IsErrLFSLockNotExist(err) {
+ lock, err := git_model.GetLFSLockByID(ctx, v)
+ if err != nil && !git_model.IsErrLFSLockNotExist(err) {
log.Error("Unable to get lock with ID[%s]: Error: %v", v, err)
}
handleLockListOut(ctx, repository, lock, err)
@@ -98,8 +97,8 @@ func GetListLockHandler(ctx *context.Context) {
path := ctx.FormString("path")
if path != "" { // Case where we request a specific id
- lock, err := models.GetLFSLock(repository, path)
- if err != nil && !models.IsErrLFSLockNotExist(err) {
+ lock, err := git_model.GetLFSLock(ctx, repository, path)
+ if err != nil && !git_model.IsErrLFSLockNotExist(err) {
log.Error("Unable to get lock for repository %-v with path %s: Error: %v", repository, path, err)
}
handleLockListOut(ctx, repository, lock, err)
@@ -107,7 +106,7 @@ func GetListLockHandler(ctx *context.Context) {
}
// If no query params path or id
- lockList, err := models.GetLFSLockByRepoID(repository.ID, cursor, limit)
+ lockList, err := git_model.GetLFSLockByRepoID(ctx, repository.ID, cursor, limit)
if err != nil {
log.Error("Unable to list locks for repository ID[%d]: Error: %v", repository.ID, err)
ctx.JSON(http.StatusInternalServerError, api.LFSLockError{
@@ -118,7 +117,7 @@ func GetListLockHandler(ctx *context.Context) {
lockListAPI := make([]*api.LFSLock, len(lockList))
next := ""
for i, l := range lockList {
- lockListAPI[i] = convert.ToLFSLock(l)
+ lockListAPI[i] = convert.ToLFSLock(ctx, l)
}
if limit > 0 && len(lockList) == limit {
next = strconv.Itoa(cursor + 1)
@@ -135,7 +134,7 @@ func PostLockHandler(ctx *context.Context) {
repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
authorization := ctx.Req.Header.Get("Authorization")
- repository, err := repo_model.GetRepositoryByOwnerAndName(userName, repoName)
+ repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName)
if err != nil {
log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
@@ -144,7 +143,7 @@ func PostLockHandler(ctx *context.Context) {
})
return
}
- repository.MustOwner()
+ repository.MustOwner(ctx)
authenticated := authenticate(ctx, repository, authorization, true, true)
if !authenticated {
@@ -168,19 +167,19 @@ func PostLockHandler(ctx *context.Context) {
return
}
- lock, err := models.CreateLFSLock(repository, &models.LFSLock{
+ lock, err := git_model.CreateLFSLock(ctx, repository, &git_model.LFSLock{
Path: req.Path,
OwnerID: ctx.Doer.ID,
})
if err != nil {
- if models.IsErrLFSLockAlreadyExist(err) {
+ if git_model.IsErrLFSLockAlreadyExist(err) {
ctx.JSON(http.StatusConflict, api.LFSLockError{
- Lock: convert.ToLFSLock(lock),
+ Lock: convert.ToLFSLock(ctx, lock),
Message: "already created lock",
})
return
}
- if models.IsErrLFSUnauthorizedAction(err) {
+ if git_model.IsErrLFSUnauthorizedAction(err) {
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
Message: "You must have push access to create locks : " + err.Error(),
@@ -193,7 +192,7 @@ func PostLockHandler(ctx *context.Context) {
})
return
}
- ctx.JSON(http.StatusCreated, api.LFSLockResponse{Lock: convert.ToLFSLock(lock)})
+ ctx.JSON(http.StatusCreated, api.LFSLockResponse{Lock: convert.ToLFSLock(ctx, lock)})
}
// VerifyLockHandler list locks for verification
@@ -202,7 +201,7 @@ func VerifyLockHandler(ctx *context.Context) {
repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
authorization := ctx.Req.Header.Get("Authorization")
- repository, err := repo_model.GetRepositoryByOwnerAndName(userName, repoName)
+ repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName)
if err != nil {
log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
@@ -211,7 +210,7 @@ func VerifyLockHandler(ctx *context.Context) {
})
return
}
- repository.MustOwner()
+ repository.MustOwner(ctx)
authenticated := authenticate(ctx, repository, authorization, true, true)
if !authenticated {
@@ -234,7 +233,7 @@ func VerifyLockHandler(ctx *context.Context) {
} else if limit < 0 {
limit = 0
}
- lockList, err := models.GetLFSLockByRepoID(repository.ID, cursor, limit)
+ lockList, err := git_model.GetLFSLockByRepoID(ctx, repository.ID, cursor, limit)
if err != nil {
log.Error("Unable to list locks for repository ID[%d]: Error: %v", repository.ID, err)
ctx.JSON(http.StatusInternalServerError, api.LFSLockError{
@@ -250,9 +249,9 @@ func VerifyLockHandler(ctx *context.Context) {
lockTheirsListAPI := make([]*api.LFSLock, 0, len(lockList))
for _, l := range lockList {
if l.OwnerID == ctx.Doer.ID {
- lockOursListAPI = append(lockOursListAPI, convert.ToLFSLock(l))
+ lockOursListAPI = append(lockOursListAPI, convert.ToLFSLock(ctx, l))
} else {
- lockTheirsListAPI = append(lockTheirsListAPI, convert.ToLFSLock(l))
+ lockTheirsListAPI = append(lockTheirsListAPI, convert.ToLFSLock(ctx, l))
}
}
ctx.JSON(http.StatusOK, api.LFSLockListVerify{
@@ -268,7 +267,7 @@ func UnLockHandler(ctx *context.Context) {
repoName := strings.TrimSuffix(ctx.Params("reponame"), ".git")
authorization := ctx.Req.Header.Get("Authorization")
- repository, err := repo_model.GetRepositoryByOwnerAndName(userName, repoName)
+ repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, userName, repoName)
if err != nil {
log.Error("Unable to get repository: %s/%s Error: %v", userName, repoName, err)
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
@@ -277,7 +276,7 @@ func UnLockHandler(ctx *context.Context) {
})
return
}
- repository.MustOwner()
+ repository.MustOwner(ctx)
authenticated := authenticate(ctx, repository, authorization, true, true)
if !authenticated {
@@ -301,9 +300,9 @@ func UnLockHandler(ctx *context.Context) {
return
}
- lock, err := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), repository, ctx.Doer, req.Force)
+ lock, err := git_model.DeleteLFSLockByID(ctx, ctx.ParamsInt64("lid"), repository, ctx.Doer, req.Force)
if err != nil {
- if models.IsErrLFSUnauthorizedAction(err) {
+ if git_model.IsErrLFSUnauthorizedAction(err) {
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
ctx.JSON(http.StatusUnauthorized, api.LFSLockError{
Message: "You must have push access to delete locks : " + err.Error(),
@@ -316,5 +315,5 @@ func UnLockHandler(ctx *context.Context) {
})
return
}
- ctx.JSON(http.StatusOK, api.LFSLockResponse{Lock: convert.ToLFSLock(lock)})
+ ctx.JSON(http.StatusOK, api.LFSLockResponse{Lock: convert.ToLFSLock(ctx, lock)})
}
diff --git a/services/lfs/server.go b/services/lfs/server.go
index 633aa0a695275..320c8e728116d 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -1,10 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package lfs
import (
+ stdCtx "context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
@@ -18,8 +18,9 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -196,8 +197,8 @@ func BatchHandler(ctx *context.Context) {
return
}
- meta, err := models.GetLFSMetaObjectByOid(repository.ID, p.Oid)
- if err != nil && err != models.ErrLFSObjectNotExist {
+ meta, err := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid)
+ if err != nil && err != git_model.ErrLFSObjectNotExist {
log.Error("Unable to get LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
writeStatus(ctx, http.StatusInternalServerError)
return
@@ -222,14 +223,14 @@ func BatchHandler(ctx *context.Context) {
}
if exists && meta == nil {
- accessible, err := models.LFSObjectAccessible(ctx.Doer, p.Oid)
+ accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
if err != nil {
log.Error("Unable to check if LFS MetaObject [%s] is accessible. Error: %v", p.Oid, err)
writeStatus(ctx, http.StatusInternalServerError)
return
}
if accessible {
- _, err := models.NewLFSMetaObject(&models.LFSMetaObject{Pointer: p, RepositoryID: repository.ID})
+ _, err := git_model.NewLFSMetaObject(ctx, &git_model.LFSMetaObject{Pointer: p, RepositoryID: repository.ID})
if err != nil {
log.Error("Unable to create LFS MetaObject [%s] for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err)
writeStatus(ctx, http.StatusInternalServerError)
@@ -296,7 +297,7 @@ func UploadHandler(ctx *context.Context) {
uploadOrVerify := func() error {
if exists {
- accessible, err := models.LFSObjectAccessible(ctx.Doer, p.Oid)
+ accessible, err := git_model.LFSObjectAccessible(ctx, ctx.Doer, p.Oid)
if err != nil {
log.Error("Unable to check if LFS MetaObject [%s] is accessible. Error: %v", p.Oid, err)
return err
@@ -322,7 +323,7 @@ func UploadHandler(ctx *context.Context) {
log.Error("Error putting LFS MetaObject [%s] into content store. Error: %v", p.Oid, err)
return err
}
- _, err := models.NewLFSMetaObject(&models.LFSMetaObject{Pointer: p, RepositoryID: repository.ID})
+ _, err := git_model.NewLFSMetaObject(ctx, &git_model.LFSMetaObject{Pointer: p, RepositoryID: repository.ID})
return err
}
@@ -334,7 +335,7 @@ func UploadHandler(ctx *context.Context) {
} else {
writeStatus(ctx, http.StatusInternalServerError)
}
- if _, err = models.RemoveLFSMetaObjectByOid(repository.ID, p.Oid); err != nil {
+ if _, err = git_model.RemoveLFSMetaObjectByOid(ctx, repository.ID, p.Oid); err != nil {
log.Error("Error whilst removing metaobject for LFS OID[%s]: %v", p.Oid, err)
}
return
@@ -385,7 +386,7 @@ func getRequestContext(ctx *context.Context) *requestContext {
}
}
-func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module.Pointer, requireWrite bool) *models.LFSMetaObject {
+func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module.Pointer, requireWrite bool) *git_model.LFSMetaObject {
if !p.IsValid() {
log.Info("Attempt to access invalid LFS OID[%s] in %s/%s", p.Oid, rc.User, rc.Repo)
writeStatusMessage(ctx, http.StatusUnprocessableEntity, "Oid or size are invalid")
@@ -397,7 +398,7 @@ func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module
return nil
}
- meta, err := models.GetLFSMetaObjectByOid(repository.ID, p.Oid)
+ meta, err := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid)
if err != nil {
log.Error("Unable to get LFS OID[%s] Error: %v", p.Oid, err)
writeStatus(ctx, http.StatusNotFound)
@@ -408,7 +409,7 @@ func getAuthenticatedMeta(ctx *context.Context, rc *requestContext, p lfs_module
}
func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requireWrite bool) *repo_model.Repository {
- repository, err := repo_model.GetRepositoryByOwnerAndName(rc.User, rc.Repo)
+ repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, rc.User, rc.Repo)
if err != nil {
log.Error("Unable to get repository: %s/%s Error: %v", rc.User, rc.Repo, err)
writeStatus(ctx, http.StatusNotFound)
@@ -437,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}
@@ -488,7 +496,7 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
}
// ctx.IsSigned is unnecessary here, this will be checked in perm.CanAccess
- perm, err := models.GetUserRepoPermission(repository, ctx.Doer)
+ perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
if err != nil {
log.Error("Unable to GetUserRepoPermission for user %-v in repo %-v Error: %v", ctx.Doer, repository)
return false
@@ -499,7 +507,7 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
return true
}
- user, err := parseToken(authorization, repository, accessMode)
+ user, err := parseToken(ctx, authorization, repository, accessMode)
if err != nil {
// Most of these are Warn level - the true internal server errors are logged in parseToken already
log.Warn("Authentication failure for provided token with Error: %v", err)
@@ -509,7 +517,7 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
return true
}
-func handleLFSToken(tokenSHA string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
+func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
if !strings.Contains(tokenSHA, ".") {
return nil, nil
}
@@ -536,7 +544,7 @@ func handleLFSToken(tokenSHA string, target *repo_model.Repository, mode perm.Ac
return nil, fmt.Errorf("invalid token claim")
}
- u, err := user_model.GetUserByID(claims.UserID)
+ u, err := user_model.GetUserByID(ctx, claims.UserID)
if err != nil {
log.Error("Unable to GetUserById[%d]: Error: %v", claims.UserID, err)
return nil, err
@@ -544,7 +552,7 @@ func handleLFSToken(tokenSHA string, target *repo_model.Repository, mode perm.Ac
return u, nil
}
-func parseToken(authorization string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
+func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
if authorization == "" {
return nil, fmt.Errorf("no token")
}
@@ -558,7 +566,7 @@ func parseToken(authorization string, target *repo_model.Repository, mode perm.A
case "bearer":
fallthrough
case "token":
- return handleLFSToken(tokenSHA, target, mode)
+ return handleLFSToken(ctx, tokenSHA, target, mode)
}
return nil, fmt.Errorf("token not found")
}
diff --git a/services/mailer/incoming/incoming.go b/services/mailer/incoming/incoming.go
new file mode 100644
index 0000000000000..2653e80586d71
--- /dev/null
+++ b/services/mailer/incoming/incoming.go
@@ -0,0 +1,375 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package incoming
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ net_mail "net/mail"
+ "regexp"
+ "strings"
+ "time"
+
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/process"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/mailer/token"
+
+ "github.com/dimiro1/reply"
+ "github.com/emersion/go-imap"
+ "github.com/emersion/go-imap/client"
+ "github.com/jhillyerd/enmime"
+)
+
+var (
+ addressTokenRegex *regexp.Regexp
+ referenceTokenRegex *regexp.Regexp
+)
+
+func Init(ctx context.Context) error {
+ if !setting.IncomingEmail.Enabled {
+ return nil
+ }
+
+ var err error
+ addressTokenRegex, err = regexp.Compile(
+ fmt.Sprintf(
+ `\A%s\z`,
+ strings.Replace(regexp.QuoteMeta(setting.IncomingEmail.ReplyToAddress), regexp.QuoteMeta(setting.IncomingEmail.TokenPlaceholder), "(.+)", 1),
+ ),
+ )
+ if err != nil {
+ return err
+ }
+ referenceTokenRegex, err = regexp.Compile(fmt.Sprintf(`\Areply-(.+)@%s\z`, regexp.QuoteMeta(setting.Domain)))
+ if err != nil {
+ return err
+ }
+
+ go func() {
+ ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Incoming Email", process.SystemProcessType, true)
+ defer finished()
+
+ // This background job processes incoming emails. It uses the IMAP IDLE command to get notified about incoming emails.
+ // The following loop restarts the processing logic after errors until ctx indicates to stop.
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ if err := processIncomingEmails(ctx); err != nil {
+ log.Error("Error while processing incoming emails: %v", err)
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.NewTimer(10 * time.Second).C:
+ }
+ }
+ }
+ }()
+
+ return nil
+}
+
+// processIncomingEmails is the "main" method with the wait/process loop
+func processIncomingEmails(ctx context.Context) error {
+ server := fmt.Sprintf("%s:%d", setting.IncomingEmail.Host, setting.IncomingEmail.Port)
+
+ var c *client.Client
+ var err error
+ if setting.IncomingEmail.UseTLS {
+ c, err = client.DialTLS(server, &tls.Config{InsecureSkipVerify: setting.IncomingEmail.SkipTLSVerify})
+ } else {
+ c, err = client.Dial(server)
+ }
+ if err != nil {
+ return fmt.Errorf("could not connect to server '%s': %w", server, err)
+ }
+
+ if err := c.Login(setting.IncomingEmail.Username, setting.IncomingEmail.Password); err != nil {
+ return fmt.Errorf("could not login: %w", err)
+ }
+ defer func() {
+ if err := c.Logout(); err != nil {
+ log.Error("Logout from incoming email server failed: %v", err)
+ }
+ }()
+
+ if _, err := c.Select(setting.IncomingEmail.Mailbox, false); err != nil {
+ return fmt.Errorf("selecting box '%s' failed: %w", setting.IncomingEmail.Mailbox, err)
+ }
+
+ // The following loop processes messages. If there are no messages available, IMAP IDLE is used to wait for new messages.
+ // This process is repeated until an IMAP error occurs or ctx indicates to stop.
+
+ for {
+ select {
+ case <-ctx.Done():
+ return nil
+ default:
+ if err := processMessages(ctx, c); err != nil {
+ return fmt.Errorf("could not process messages: %w", err)
+ }
+ if err := waitForUpdates(ctx, c); err != nil {
+ return fmt.Errorf("wait for updates failed: %w", err)
+ }
+ select {
+ case <-ctx.Done():
+ return nil
+ case <-time.NewTimer(time.Second).C:
+ }
+ }
+ }
+}
+
+// waitForUpdates uses IMAP IDLE to wait for new emails
+func waitForUpdates(ctx context.Context, c *client.Client) error {
+ updates := make(chan client.Update, 1)
+
+ c.Updates = updates
+ defer func() {
+ c.Updates = nil
+ }()
+
+ errs := make(chan error, 1)
+ stop := make(chan struct{})
+ go func() {
+ errs <- c.Idle(stop, nil)
+ }()
+
+ stopped := false
+ for {
+ select {
+ case update := <-updates:
+ switch update.(type) {
+ case *client.MailboxUpdate:
+ if !stopped {
+ close(stop)
+ stopped = true
+ }
+ default:
+ }
+ case err := <-errs:
+ if err != nil {
+ return fmt.Errorf("imap idle failed: %w", err)
+ }
+ return nil
+ case <-ctx.Done():
+ return nil
+ }
+ }
+}
+
+// processMessages searches unread mails and processes them.
+func processMessages(ctx context.Context, c *client.Client) error {
+ criteria := imap.NewSearchCriteria()
+ criteria.WithoutFlags = []string{imap.SeenFlag}
+ criteria.Smaller = setting.IncomingEmail.MaximumMessageSize
+ ids, err := c.Search(criteria)
+ if err != nil {
+ return fmt.Errorf("imap search failed: %w", err)
+ }
+
+ if len(ids) == 0 {
+ return nil
+ }
+
+ seqset := new(imap.SeqSet)
+ seqset.AddNum(ids...)
+ messages := make(chan *imap.Message, 10)
+
+ section := &imap.BodySectionName{}
+
+ errs := make(chan error, 1)
+ go func() {
+ errs <- c.Fetch(
+ seqset,
+ []imap.FetchItem{section.FetchItem()},
+ messages,
+ )
+ }()
+
+ handledSet := new(imap.SeqSet)
+loop:
+ for {
+ select {
+ case <-ctx.Done():
+ break loop
+ case msg, ok := <-messages:
+ if !ok {
+ if setting.IncomingEmail.DeleteHandledMessage && !handledSet.Empty() {
+ if err := c.Store(
+ handledSet,
+ imap.FormatFlagsOp(imap.AddFlags, true),
+ []interface{}{imap.DeletedFlag},
+ nil,
+ ); err != nil {
+ return fmt.Errorf("imap store failed: %w", err)
+ }
+
+ if err := c.Expunge(nil); err != nil {
+ return fmt.Errorf("imap expunge failed: %w", err)
+ }
+ }
+ return nil
+ }
+
+ err := func() error {
+ r := msg.GetBody(section)
+ if r == nil {
+ return fmt.Errorf("could not get body from message: %w", err)
+ }
+
+ env, err := enmime.ReadEnvelope(r)
+ if err != nil {
+ return fmt.Errorf("could not read envelope: %w", err)
+ }
+
+ if isAutomaticReply(env) {
+ log.Debug("Skipping automatic email reply")
+ return nil
+ }
+
+ t := searchTokenInHeaders(env)
+ if t == "" {
+ log.Debug("Incoming email token not found in headers")
+ return nil
+ }
+
+ handlerType, user, payload, err := token.ExtractToken(ctx, t)
+ if err != nil {
+ if _, ok := err.(*token.ErrToken); ok {
+ log.Info("Invalid incoming email token: %v", err)
+ return nil
+ }
+ return err
+ }
+
+ handler, ok := handlers[handlerType]
+ if !ok {
+ return fmt.Errorf("unexpected handler type: %v", handlerType)
+ }
+
+ content := getContentFromMailReader(env)
+
+ if err := handler.Handle(ctx, content, user, payload); err != nil {
+ return fmt.Errorf("could not handle message: %w", err)
+ }
+
+ handledSet.AddNum(msg.SeqNum)
+
+ return nil
+ }()
+ if err != nil {
+ log.Error("Error while processing incoming email[%v]: %v", msg.Uid, err)
+ }
+ }
+ }
+
+ if err := <-errs; err != nil {
+ return fmt.Errorf("imap fetch failed: %w", err)
+ }
+
+ return nil
+}
+
+// isAutomaticReply tests if the headers indicate an automatic reply
+func isAutomaticReply(env *enmime.Envelope) bool {
+ autoSubmitted := env.GetHeader("Auto-Submitted")
+ if autoSubmitted != "" && autoSubmitted != "no" {
+ return true
+ }
+ autoReply := env.GetHeader("X-Autoreply")
+ if autoReply == "yes" {
+ return true
+ }
+ autoRespond := env.GetHeader("X-Autorespond")
+ return autoRespond != ""
+}
+
+// searchTokenInHeaders looks for the token in To, Delivered-To and References
+func searchTokenInHeaders(env *enmime.Envelope) string {
+ if addressTokenRegex != nil {
+ to, _ := env.AddressList("To")
+
+ token := searchTokenInAddresses(to)
+ if token != "" {
+ return token
+ }
+
+ deliveredTo, _ := env.AddressList("Delivered-To")
+
+ token = searchTokenInAddresses(deliveredTo)
+ if token != "" {
+ return token
+ }
+ }
+
+ references := env.GetHeader("References")
+ for {
+ begin := strings.IndexByte(references, '<')
+ if begin == -1 {
+ break
+ }
+ begin++
+
+ end := strings.IndexByte(references, '>')
+ if end == -1 || begin > end {
+ break
+ }
+
+ match := referenceTokenRegex.FindStringSubmatch(references[begin:end])
+ if len(match) == 2 {
+ return match[1]
+ }
+
+ references = references[end+1:]
+ }
+
+ return ""
+}
+
+// searchTokenInAddresses looks for the token in an address
+func searchTokenInAddresses(addresses []*net_mail.Address) string {
+ for _, address := range addresses {
+ match := addressTokenRegex.FindStringSubmatch(address.Address)
+ if len(match) != 2 {
+ continue
+ }
+
+ return match[1]
+ }
+
+ return ""
+}
+
+type MailContent struct {
+ Content string
+ Attachments []*Attachment
+}
+
+type Attachment struct {
+ Name string
+ Content []byte
+}
+
+// getContentFromMailReader grabs the plain content and the attachments from the mail.
+// A potential reply/signature gets stripped from the content.
+func getContentFromMailReader(env *enmime.Envelope) *MailContent {
+ attachments := make([]*Attachment, 0, len(env.Attachments))
+ for _, attachment := range env.Attachments {
+ attachments = append(attachments, &Attachment{
+ Name: attachment.FileName,
+ Content: attachment.Content,
+ })
+ }
+
+ return &MailContent{
+ Content: reply.FromText(env.Text),
+ Attachments: attachments,
+ }
+}
diff --git a/services/mailer/incoming/incoming_handler.go b/services/mailer/incoming/incoming_handler.go
new file mode 100644
index 0000000000000..173b362a5505f
--- /dev/null
+++ b/services/mailer/incoming/incoming_handler.go
@@ -0,0 +1,171 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package incoming
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/upload"
+ "code.gitea.io/gitea/modules/util"
+ attachment_service "code.gitea.io/gitea/services/attachment"
+ issue_service "code.gitea.io/gitea/services/issue"
+ incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
+ "code.gitea.io/gitea/services/mailer/token"
+ pull_service "code.gitea.io/gitea/services/pull"
+)
+
+type MailHandler interface {
+ Handle(ctx context.Context, content *MailContent, doer *user_model.User, payload []byte) error
+}
+
+var handlers = map[token.HandlerType]MailHandler{
+ token.ReplyHandlerType: &ReplyHandler{},
+ token.UnsubscribeHandlerType: &UnsubscribeHandler{},
+}
+
+// ReplyHandler handles incoming emails to create a reply from them
+type ReplyHandler struct{}
+
+func (h *ReplyHandler) Handle(ctx context.Context, content *MailContent, doer *user_model.User, payload []byte) error {
+ if doer == nil {
+ return util.NewInvalidArgumentErrorf("doer can't be nil")
+ }
+
+ ref, err := incoming_payload.GetReferenceFromPayload(ctx, payload)
+ if err != nil {
+ return err
+ }
+
+ var issue *issues_model.Issue
+
+ switch r := ref.(type) {
+ case *issues_model.Issue:
+ issue = r
+ case *issues_model.Comment:
+ comment := r
+
+ if err := comment.LoadIssue(ctx); err != nil {
+ return err
+ }
+
+ issue = comment.Issue
+ default:
+ return util.NewInvalidArgumentErrorf("unsupported reply reference: %v", ref)
+ }
+
+ if err := issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
+ if err != nil {
+ return err
+ }
+
+ if !perm.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsLocked && !doer.IsAdmin {
+ log.Debug("can't write issue or pull")
+ return nil
+ }
+
+ switch r := ref.(type) {
+ case *issues_model.Issue:
+ attachmentIDs := make([]string, 0, len(content.Attachments))
+ if setting.Attachment.Enabled {
+ for _, attachment := range content.Attachments {
+ a, err := attachment_service.UploadAttachment(bytes.NewReader(attachment.Content), setting.Attachment.AllowedTypes, &repo_model.Attachment{
+ Name: attachment.Name,
+ UploaderID: doer.ID,
+ RepoID: issue.Repo.ID,
+ })
+ if err != nil {
+ if upload.IsErrFileTypeForbidden(err) {
+ log.Info("Skipping disallowed attachment type: %s", attachment.Name)
+ continue
+ }
+ return err
+ }
+ attachmentIDs = append(attachmentIDs, a.UUID)
+ }
+ }
+
+ if content.Content == "" && len(attachmentIDs) == 0 {
+ return nil
+ }
+
+ _, err = issue_service.CreateIssueComment(ctx, doer, issue.Repo, issue, content.Content, attachmentIDs)
+ if err != nil {
+ return fmt.Errorf("CreateIssueComment failed: %w", err)
+ }
+ case *issues_model.Comment:
+ comment := r
+
+ if content.Content == "" {
+ return nil
+ }
+
+ if comment.Type == issues_model.CommentTypeCode {
+ _, err := pull_service.CreateCodeComment(
+ ctx,
+ doer,
+ nil,
+ issue,
+ comment.Line,
+ content.Content,
+ comment.TreePath,
+ false,
+ comment.ReviewID,
+ "",
+ )
+ if err != nil {
+ return fmt.Errorf("CreateCodeComment failed: %w", err)
+ }
+ }
+ }
+ return nil
+}
+
+// UnsubscribeHandler handles unwatching issues/pulls
+type UnsubscribeHandler struct{}
+
+func (h *UnsubscribeHandler) Handle(ctx context.Context, _ *MailContent, doer *user_model.User, payload []byte) error {
+ if doer == nil {
+ return util.NewInvalidArgumentErrorf("doer can't be nil")
+ }
+
+ ref, err := incoming_payload.GetReferenceFromPayload(ctx, payload)
+ if err != nil {
+ return err
+ }
+
+ switch r := ref.(type) {
+ case *issues_model.Issue:
+ issue := r
+
+ if err := issue.LoadRepo(ctx); err != nil {
+ return err
+ }
+
+ perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
+ if err != nil {
+ return err
+ }
+
+ if !perm.CanReadIssuesOrPulls(issue.IsPull) {
+ log.Debug("can't read issue or pull")
+ return nil
+ }
+
+ return issues_model.CreateOrUpdateIssueWatch(doer.ID, issue.ID, false)
+ }
+
+ return fmt.Errorf("unsupported unsubscribe reference: %v", ref)
+}
diff --git a/services/mailer/incoming/incoming_test.go b/services/mailer/incoming/incoming_test.go
new file mode 100644
index 0000000000000..5d84848e3f45e
--- /dev/null
+++ b/services/mailer/incoming/incoming_test.go
@@ -0,0 +1,138 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package incoming
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/jhillyerd/enmime"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIsAutomaticReply(t *testing.T) {
+ cases := []struct {
+ Headers map[string]string
+ Expected bool
+ }{
+ {
+ Headers: map[string]string{},
+ Expected: false,
+ },
+ {
+ Headers: map[string]string{
+ "Auto-Submitted": "no",
+ },
+ Expected: false,
+ },
+ {
+ Headers: map[string]string{
+ "Auto-Submitted": "yes",
+ },
+ Expected: true,
+ },
+ {
+ Headers: map[string]string{
+ "X-Autoreply": "no",
+ },
+ Expected: false,
+ },
+ {
+ Headers: map[string]string{
+ "X-Autoreply": "yes",
+ },
+ Expected: true,
+ },
+ {
+ Headers: map[string]string{
+ "X-Autorespond": "yes",
+ },
+ Expected: true,
+ },
+ }
+
+ for _, c := range cases {
+ b := enmime.Builder().
+ From("Dummy", "dummy@gitea.io").
+ To("Dummy", "dummy@gitea.io")
+ for k, v := range c.Headers {
+ b = b.Header(k, v)
+ }
+ root, err := b.Build()
+ assert.NoError(t, err)
+ env, err := enmime.EnvelopeFromPart(root)
+ assert.NoError(t, err)
+
+ assert.Equal(t, c.Expected, isAutomaticReply(env))
+ }
+}
+
+func TestGetContentFromMailReader(t *testing.T) {
+ mailString := "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
+ "\r\n" +
+ "--message-boundary\r\n" +
+ "Content-Type: multipart/alternative; boundary=text-boundary\r\n" +
+ "\r\n" +
+ "--text-boundary\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Content-Disposition: inline\r\n" +
+ "\r\n" +
+ "mail content\r\n" +
+ "--text-boundary--\r\n" +
+ "--message-boundary\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Content-Disposition: attachment; filename=attachment.txt\r\n" +
+ "\r\n" +
+ "attachment content\r\n" +
+ "--message-boundary--\r\n"
+
+ env, err := enmime.ReadEnvelope(strings.NewReader(mailString))
+ assert.NoError(t, err)
+ content := getContentFromMailReader(env)
+ assert.Equal(t, "mail content", content.Content)
+ assert.Len(t, content.Attachments, 1)
+ assert.Equal(t, "attachment.txt", content.Attachments[0].Name)
+ assert.Equal(t, []byte("attachment content"), content.Attachments[0].Content)
+
+ mailString = "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
+ "\r\n" +
+ "--message-boundary\r\n" +
+ "Content-Type: multipart/alternative; boundary=text-boundary\r\n" +
+ "\r\n" +
+ "--text-boundary\r\n" +
+ "Content-Type: text/html\r\n" +
+ "Content-Disposition: inline\r\n" +
+ "\r\n" +
+ "mail content
\r\n" +
+ "--text-boundary--\r\n" +
+ "--message-boundary--\r\n"
+
+ env, err = enmime.ReadEnvelope(strings.NewReader(mailString))
+ assert.NoError(t, err)
+ content = getContentFromMailReader(env)
+ assert.Equal(t, "mail content", content.Content)
+ assert.Empty(t, content.Attachments)
+
+ mailString = "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
+ "\r\n" +
+ "--message-boundary\r\n" +
+ "Content-Type: multipart/alternative; boundary=text-boundary\r\n" +
+ "\r\n" +
+ "--text-boundary\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Content-Disposition: inline\r\n" +
+ "\r\n" +
+ "mail content without signature\r\n" +
+ "--\r\n" +
+ "signature\r\n" +
+ "--text-boundary--\r\n" +
+ "--message-boundary--\r\n"
+
+ env, err = enmime.ReadEnvelope(strings.NewReader(mailString))
+ assert.NoError(t, err)
+ content = getContentFromMailReader(env)
+ assert.NoError(t, err)
+ assert.Equal(t, "mail content without signature", content.Content)
+ assert.Empty(t, content.Attachments)
+}
diff --git a/services/mailer/incoming/payload/payload.go b/services/mailer/incoming/payload/payload.go
new file mode 100644
index 0000000000000..eb82f5c3ed321
--- /dev/null
+++ b/services/mailer/incoming/payload/payload.go
@@ -0,0 +1,70 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package payload
+
+import (
+ "context"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/modules/util"
+)
+
+const replyPayloadVersion1 byte = 1
+
+type payloadReferenceType byte
+
+const (
+ payloadReferenceIssue payloadReferenceType = iota
+ payloadReferenceComment
+)
+
+// CreateReferencePayload creates data which GetReferenceFromPayload resolves to the reference again.
+func CreateReferencePayload(reference interface{}) ([]byte, error) {
+ var refType payloadReferenceType
+ var refID int64
+
+ switch r := reference.(type) {
+ case *issues_model.Issue:
+ refType = payloadReferenceIssue
+ refID = r.ID
+ case *issues_model.Comment:
+ refType = payloadReferenceComment
+ refID = r.ID
+ default:
+ return nil, util.NewInvalidArgumentErrorf("unsupported reference type: %T", r)
+ }
+
+ payload, err := util.PackData(refType, refID)
+ if err != nil {
+ return nil, err
+ }
+
+ return append([]byte{replyPayloadVersion1}, payload...), nil
+}
+
+// GetReferenceFromPayload resolves the reference from the payload
+func GetReferenceFromPayload(ctx context.Context, payload []byte) (interface{}, error) {
+ if len(payload) < 1 {
+ return nil, util.NewInvalidArgumentErrorf("payload to small")
+ }
+
+ if payload[0] != replyPayloadVersion1 {
+ return nil, util.NewInvalidArgumentErrorf("unsupported payload version")
+ }
+
+ var ref payloadReferenceType
+ var id int64
+ if err := util.UnpackData(payload[1:], &ref, &id); err != nil {
+ return nil, err
+ }
+
+ switch ref {
+ case payloadReferenceIssue:
+ return issues_model.GetIssueByID(ctx, id)
+ case payloadReferenceComment:
+ return issues_model.GetCommentByID(ctx, id)
+ default:
+ return nil, util.NewInvalidArgumentErrorf("unsupported reference type: %T", ref)
+ }
+}
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index a5b60f71ec311..7c7ad54714ceb 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -1,7 +1,6 @@
// Copyright 2016 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.
+// SPDX-License-Identifier: MIT
package mailer
@@ -17,8 +16,8 @@ import (
texttmpl "text/template"
"time"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
+ activities_model "code.gitea.io/gitea/models/activities"
+ issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
@@ -30,6 +29,8 @@ import (
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
+ incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
+ "code.gitea.io/gitea/services/mailer/token"
"gopkg.in/gomail.v2"
)
@@ -54,12 +55,6 @@ var (
subjectRemoveSpaces = regexp.MustCompile(`[\s]+`)
)
-// InitMailRender initializes the mail renderer
-func InitMailRender(subjectTpl *texttmpl.Template, bodyTpl *template.Template) {
- subjectTemplates = subjectTpl
- bodyTemplates = bodyTpl
-}
-
// SendTestMail sends a test mail
func SendTestMail(email string) error {
if setting.MailService == nil {
@@ -74,12 +69,12 @@ func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, s
locale := translation.NewLocale(language)
data := map[string]interface{}{
"DisplayName": u.DisplayName(),
- "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, language),
- "ResetPwdCodeLives": timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, language),
+ "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
+ "ResetPwdCodeLives": timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, locale),
"Code": code,
"Language": locale.Language(),
// helper
- "i18n": locale,
+ "locale": locale,
"Str2html": templates.Str2html,
"DotEscape": templates.DotEscape,
}
@@ -125,12 +120,12 @@ func SendActivateEmailMail(u *user_model.User, email *user_model.EmailAddress) {
locale := translation.NewLocale(u.Language)
data := map[string]interface{}{
"DisplayName": u.DisplayName(),
- "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale.Language()),
+ "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
"Code": u.GenerateEmailActivateCode(email.Email),
"Email": email.Email,
"Language": locale.Language(),
// helper
- "i18n": locale,
+ "locale": locale,
"Str2html": templates.Str2html,
"DotEscape": templates.DotEscape,
}
@@ -161,7 +156,7 @@ func SendRegisterNotifyMail(u *user_model.User) {
"Username": u.Name,
"Language": locale.Language(),
// helper
- "i18n": locale,
+ "locale": locale,
"Str2html": templates.Str2html,
"DotEscape": templates.DotEscape,
}
@@ -195,7 +190,7 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository)
"Link": repo.HTMLURL(),
"Language": locale.Language(),
// helper
- "i18n": locale,
+ "locale": locale,
"Str2html": templates.Str2html,
"DotEscape": templates.DotEscape,
}
@@ -220,10 +215,10 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
prefix string
// Fall back subject for bad templates, make sure subject is never empty
fallback string
- reviewComments []*models.Comment
+ reviewComments []*issues_model.Comment
)
- commentType := models.CommentTypeComment
+ commentType := issues_model.CommentTypeComment
if ctx.Comment != nil {
commentType = ctx.Comment.Type
link = ctx.Issue.HTMLURL() + "#" + ctx.Comment.HashTag()
@@ -231,7 +226,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
link = ctx.Issue.HTMLURL()
}
- reviewType := models.ReviewTypeComment
+ reviewType := issues_model.ReviewTypeComment
if ctx.Comment != nil && ctx.Comment.Review != nil {
reviewType = ctx.Comment.Review.Type
}
@@ -254,7 +249,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
fallback = prefix + fallbackMailSubject(ctx.Issue)
if ctx.Comment != nil && ctx.Comment.Review != nil {
- reviewComments = make([]*models.Comment, 0, 10)
+ reviewComments = make([]*issues_model.Comment, 0, 10)
for _, lines := range ctx.Comment.Review.CodeComments {
for _, comments := range lines {
reviewComments = append(reviewComments, comments...)
@@ -270,7 +265,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
"Issue": ctx.Issue,
"Comment": ctx.Comment,
"IsPull": ctx.Issue.IsPull,
- "User": ctx.Issue.Repo.MustOwner(),
+ "User": ctx.Issue.Repo.MustOwner(ctx),
"Repo": ctx.Issue.Repo.FullName(),
"Doer": ctx.Doer,
"IsMention": fromMention,
@@ -279,14 +274,15 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
"ActionName": actName,
"ReviewComments": reviewComments,
"Language": locale.Language(),
+ "CanReply": setting.IncomingEmail.Enabled && commentType != issues_model.CommentTypePullRequestPush,
// helper
- "i18n": locale,
+ "locale": locale,
"Str2html": templates.Str2html,
"DotEscape": templates.DotEscape,
}
var mailSubject bytes.Buffer
- if err := subjectTemplates.ExecuteTemplate(&mailSubject, string(tplName), mailMeta); err == nil {
+ if err := subjectTemplates.ExecuteTemplate(&mailSubject, tplName, mailMeta); err == nil {
subject = sanitizeSubject(mailSubject.String())
if subject == "" {
subject = fallback
@@ -301,22 +297,65 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
var mailBody bytes.Buffer
- if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplName), mailMeta); err != nil {
- log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/body", err)
+ if err := bodyTemplates.ExecuteTemplate(&mailBody, tplName, mailMeta); err != nil {
+ log.Error("ExecuteTemplate [%s]: %v", tplName+"/body", err)
}
// Make sure to compose independent messages to avoid leaking user emails
msgID := createReference(ctx.Issue, ctx.Comment, ctx.ActionType)
- reference := createReference(ctx.Issue, nil, models.ActionType(0))
+ reference := createReference(ctx.Issue, nil, activities_model.ActionType(0))
+
+ var replyPayload []byte
+ if ctx.Comment != nil && ctx.Comment.Type == issues_model.CommentTypeCode {
+ replyPayload, err = incoming_payload.CreateReferencePayload(ctx.Comment)
+ } else {
+ replyPayload, err = incoming_payload.CreateReferencePayload(ctx.Issue)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ unsubscribePayload, err := incoming_payload.CreateReferencePayload(ctx.Issue)
+ if err != nil {
+ return nil, err
+ }
msgs := make([]*Message, 0, len(recipients))
for _, recipient := range recipients {
msg := NewMessageFrom([]string{recipient.Email}, ctx.Doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info)
- msg.SetHeader("Message-ID", "<"+msgID+">")
- msg.SetHeader("In-Reply-To", "<"+reference+">")
- msg.SetHeader("References", "<"+reference+">")
+ msg.SetHeader("Message-ID", msgID)
+ msg.SetHeader("In-Reply-To", reference)
+
+ references := []string{reference}
+ listUnsubscribe := []string{"<" + ctx.Issue.HTMLURL() + ">"}
+
+ if setting.IncomingEmail.Enabled {
+ if ctx.Comment != nil {
+ token, err := token.CreateToken(token.ReplyHandlerType, recipient, replyPayload)
+ if err != nil {
+ log.Error("CreateToken failed: %v", err)
+ } else {
+ replyAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1)
+ msg.ReplyTo = replyAddress
+ msg.SetHeader("List-Post", fmt.Sprintf("", replyAddress))
+
+ references = append(references, fmt.Sprintf("", token, setting.Domain))
+ }
+ }
+
+ token, err := token.CreateToken(token.UnsubscribeHandlerType, recipient, unsubscribePayload)
+ if err != nil {
+ log.Error("CreateToken failed: %v", err)
+ } else {
+ unsubAddress := strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1)
+ listUnsubscribe = append(listUnsubscribe, "")
+ }
+ }
+
+ msg.SetHeader("References", references...)
+ msg.SetHeader("List-Unsubscribe", listUnsubscribe...)
for key, value := range generateAdditionalHeaders(ctx, actType, recipient) {
msg.SetHeader(key, value)
@@ -328,7 +367,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
return msgs, nil
}
-func createReference(issue *models.Issue, comment *models.Comment, actionType models.ActionType) string {
+func createReference(issue *issues_model.Issue, comment *issues_model.Comment, actionType activities_model.ActionType) string {
var path string
if issue.IsPull {
path = "pulls"
@@ -341,18 +380,18 @@ func createReference(issue *models.Issue, comment *models.Comment, actionType mo
extra = fmt.Sprintf("/comment/%d", comment.ID)
} else {
switch actionType {
- case models.ActionCloseIssue, models.ActionClosePullRequest:
+ case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
extra = fmt.Sprintf("/close/%d", time.Now().UnixNano()/1e6)
- case models.ActionReopenIssue, models.ActionReopenPullRequest:
+ case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
extra = fmt.Sprintf("/reopen/%d", time.Now().UnixNano()/1e6)
- case models.ActionMergePullRequest:
+ case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
extra = fmt.Sprintf("/merge/%d", time.Now().UnixNano()/1e6)
- case models.ActionPullRequestReadyForReview:
+ case activities_model.ActionPullRequestReadyForReview:
extra = fmt.Sprintf("/ready/%d", time.Now().UnixNano()/1e6)
}
}
- return fmt.Sprintf("%s/%s/%d%s@%s", issue.Repo.FullName(), path, issue.Index, extra, setting.Domain)
+ return fmt.Sprintf("<%s/%s/%d%s@%s>", issue.Repo.FullName(), path, issue.Index, extra, setting.Domain)
}
func generateAdditionalHeaders(ctx *mailCommentContext, reason string, recipient *user_model.User) map[string]string {
@@ -364,9 +403,8 @@ func generateAdditionalHeaders(ctx *mailCommentContext, reason string, recipient
// https://datatracker.ietf.org/doc/html/rfc2369
"List-Archive": fmt.Sprintf("<%s>", repo.HTMLURL()),
- //"List-Post": https://github.com/go-gitea/gitea/pull/13585
- "List-Unsubscribe": ctx.Issue.HTMLURL(),
+ "X-Mailer": "Gitea",
"X-Gitea-Reason": reason,
"X-Gitea-Sender": ctx.Doer.DisplayName(),
"X-Gitea-Recipient": recipient.DisplayName(),
@@ -399,13 +437,13 @@ func sanitizeSubject(subject string) string {
}
// SendIssueAssignedMail composes and sends issue assigned email
-func SendIssueAssignedMail(issue *models.Issue, doer *user_model.User, content string, comment *models.Comment, recipients []*user_model.User) error {
+func SendIssueAssignedMail(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, content string, comment *issues_model.Comment, recipients []*user_model.User) error {
if setting.MailService == nil {
// No mail service configured
return nil
}
- if err := issue.LoadRepo(db.DefaultContext); err != nil {
+ if err := issue.LoadRepo(ctx); err != nil {
log.Error("Unable to load repo [%d] for issue #%d [%d]. Error: %v", issue.RepoID, issue.Index, issue.ID, err)
return err
}
@@ -421,10 +459,10 @@ func SendIssueAssignedMail(issue *models.Issue, doer *user_model.User, content s
for lang, tos := range langMap {
msgs, err := composeIssueCommentMessages(&mailCommentContext{
- Context: context.TODO(), // TODO: use a correct context
+ Context: ctx,
Issue: issue,
Doer: doer,
- ActionType: models.ActionType(0),
+ ActionType: activities_model.ActionType(0),
Content: content,
Comment: comment,
}, lang, tos, false, "issue assigned")
@@ -437,9 +475,9 @@ func SendIssueAssignedMail(issue *models.Issue, doer *user_model.User, content s
}
// actionToTemplate returns the type and name of the action facing the user
-// (slightly different from models.ActionType) and the name of the template to use (based on availability)
-func actionToTemplate(issue *models.Issue, actionType models.ActionType,
- commentType models.CommentType, reviewType models.ReviewType,
+// (slightly different from activities_model.ActionType) and the name of the template to use (based on availability)
+func actionToTemplate(issue *issues_model.Issue, actionType activities_model.ActionType,
+ commentType issues_model.CommentType, reviewType issues_model.ReviewType,
) (typeName, name, template string) {
if issue.IsPull {
typeName = "pull"
@@ -447,36 +485,36 @@ func actionToTemplate(issue *models.Issue, actionType models.ActionType,
typeName = "issue"
}
switch actionType {
- case models.ActionCreateIssue, models.ActionCreatePullRequest:
+ case activities_model.ActionCreateIssue, activities_model.ActionCreatePullRequest:
name = "new"
- case models.ActionCommentIssue, models.ActionCommentPull:
+ case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
name = "comment"
- case models.ActionCloseIssue, models.ActionClosePullRequest:
+ case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
name = "close"
- case models.ActionReopenIssue, models.ActionReopenPullRequest:
+ case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
name = "reopen"
- case models.ActionMergePullRequest:
+ case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
name = "merge"
- case models.ActionPullReviewDismissed:
+ case activities_model.ActionPullReviewDismissed:
name = "review_dismissed"
- case models.ActionPullRequestReadyForReview:
+ case activities_model.ActionPullRequestReadyForReview:
name = "ready_for_review"
default:
switch commentType {
- case models.CommentTypeReview:
+ case issues_model.CommentTypeReview:
switch reviewType {
- case models.ReviewTypeApprove:
+ case issues_model.ReviewTypeApprove:
name = "approve"
- case models.ReviewTypeReject:
+ case issues_model.ReviewTypeReject:
name = "reject"
default:
name = "review"
}
- case models.CommentTypeCode:
+ case issues_model.CommentTypeCode:
name = "code"
- case models.CommentTypeAssignees:
+ case issues_model.CommentTypeAssignees:
name = "assigned"
- case models.CommentTypePullRequestPush:
+ case issues_model.CommentTypePullRequestPush:
name = "push"
default:
name = "default"
@@ -496,5 +534,5 @@ func actionToTemplate(issue *models.Issue, actionType models.ActionType,
if !ok {
template = "issue/default"
}
- return
+ return typeName, name, template
}
diff --git a/services/mailer/mail_comment.go b/services/mailer/mail_comment.go
index baecd2a101664..1812441d5a23c 100644
--- a/services/mailer/mail_comment.go
+++ b/services/mailer/mail_comment.go
@@ -1,27 +1,28 @@
// 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.
+// SPDX-License-Identifier: MIT
package mailer
import (
"context"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
+ issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
// MailParticipantsComment sends new comment emails to repository watchers and mentioned people.
-func MailParticipantsComment(ctx context.Context, c *models.Comment, opType models.ActionType, issue *models.Issue, mentions []*user_model.User) error {
+func MailParticipantsComment(ctx context.Context, c *issues_model.Comment, opType activities_model.ActionType, issue *issues_model.Issue, mentions []*user_model.User) error {
if setting.MailService == nil {
// No mail service configured
return nil
}
content := c.Content
- if c.Type == models.CommentTypePullRequestPush {
+ if c.Type == issues_model.CommentTypePullRequestPush {
content = ""
}
if err := mailIssueCommentToParticipants(
@@ -39,20 +40,20 @@ func MailParticipantsComment(ctx context.Context, c *models.Comment, opType mode
}
// MailMentionsComment sends email to users mentioned in a code comment
-func MailMentionsComment(ctx context.Context, pr *models.PullRequest, c *models.Comment, mentions []*user_model.User) (err error) {
+func MailMentionsComment(ctx context.Context, pr *issues_model.PullRequest, c *issues_model.Comment, mentions []*user_model.User) (err error) {
if setting.MailService == nil {
// No mail service configured
return nil
}
- visited := make(map[int64]bool, len(mentions)+1)
- visited[c.Poster.ID] = true
+ visited := make(container.Set[int64], len(mentions)+1)
+ visited.Add(c.Poster.ID)
if err = mailIssueCommentBatch(
&mailCommentContext{
Context: ctx,
Issue: pr.Issue,
Doer: c.Poster,
- ActionType: models.ActionCommentPull,
+ ActionType: activities_model.ActionCommentPull,
Content: c.Content,
Comment: c,
}, mentions, visited, true); err != nil {
diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go
index c24edf50c93a7..be5279aac5c51 100644
--- a/services/mailer/mail_issue.go
+++ b/services/mailer/mail_issue.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package mailer
@@ -8,25 +7,29 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
+ issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
-func fallbackMailSubject(issue *models.Issue) string {
+func fallbackMailSubject(issue *issues_model.Issue) string {
return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index)
}
type mailCommentContext struct {
context.Context
- Issue *models.Issue
- Doer *user_model.User
- ActionType models.ActionType
- Content string
- Comment *models.Comment
+ Issue *issues_model.Issue
+ Doer *user_model.User
+ ActionType activities_model.ActionType
+ Content string
+ Comment *issues_model.Comment
+ ForceDoerNotification bool
}
const (
@@ -41,13 +44,13 @@ const (
func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_model.User) error {
// Required by the mail composer; make sure to load these before calling the async function
if err := ctx.Issue.LoadRepo(ctx); err != nil {
- return fmt.Errorf("LoadRepo(): %v", err)
+ return fmt.Errorf("LoadRepo: %w", err)
}
- if err := ctx.Issue.LoadPoster(); err != nil {
- return fmt.Errorf("LoadPoster(): %v", err)
+ if err := ctx.Issue.LoadPoster(ctx); err != nil {
+ return fmt.Errorf("LoadPoster: %w", err)
}
- if err := ctx.Issue.LoadPullRequest(); err != nil {
- return fmt.Errorf("LoadPullRequest(): %v", err)
+ if err := ctx.Issue.LoadPullRequest(ctx); err != nil {
+ return fmt.Errorf("LoadPullRequest: %w", err)
}
// Enough room to avoid reallocations
@@ -57,67 +60,67 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo
unfiltered[0] = ctx.Issue.PosterID
// =========== Assignees ===========
- ids, err := models.GetAssigneeIDsByIssue(ctx.Issue.ID)
+ ids, err := issues_model.GetAssigneeIDsByIssue(ctx, ctx.Issue.ID)
if err != nil {
- return fmt.Errorf("GetAssigneeIDsByIssue(%d): %v", ctx.Issue.ID, err)
+ return fmt.Errorf("GetAssigneeIDsByIssue(%d): %w", ctx.Issue.ID, err)
}
unfiltered = append(unfiltered, ids...)
// =========== Participants (i.e. commenters, reviewers) ===========
- ids, err = models.GetParticipantsIDsByIssueID(ctx.Issue.ID)
+ ids, err = issues_model.GetParticipantsIDsByIssueID(ctx, ctx.Issue.ID)
if err != nil {
- return fmt.Errorf("GetParticipantsIDsByIssueID(%d): %v", ctx.Issue.ID, err)
+ return fmt.Errorf("GetParticipantsIDsByIssueID(%d): %w", ctx.Issue.ID, err)
}
unfiltered = append(unfiltered, ids...)
// =========== Issue watchers ===========
- ids, err = models.GetIssueWatchersIDs(ctx.Issue.ID, true)
+ ids, err = issues_model.GetIssueWatchersIDs(ctx, ctx.Issue.ID, true)
if err != nil {
- return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err)
+ return fmt.Errorf("GetIssueWatchersIDs(%d): %w", ctx.Issue.ID, err)
}
unfiltered = append(unfiltered, ids...)
// =========== Repo watchers ===========
// Make repo watchers last, since it's likely the list with the most users
- if !(ctx.Issue.IsPull && ctx.Issue.PullRequest.IsWorkInProgress() && ctx.ActionType != models.ActionCreatePullRequest) {
+ if !(ctx.Issue.IsPull && ctx.Issue.PullRequest.IsWorkInProgress() && ctx.ActionType != activities_model.ActionCreatePullRequest) {
ids, err = repo_model.GetRepoWatchersIDs(ctx, ctx.Issue.RepoID)
if err != nil {
- return fmt.Errorf("GetRepoWatchersIDs(%d): %v", ctx.Issue.RepoID, err)
+ return fmt.Errorf("GetRepoWatchersIDs(%d): %w", ctx.Issue.RepoID, err)
}
unfiltered = append(ids, unfiltered...)
}
- visited := make(map[int64]bool, len(unfiltered)+len(mentions)+1)
+ visited := make(container.Set[int64], len(unfiltered)+len(mentions)+1)
// Avoid mailing the doer
- visited[ctx.Doer.ID] = true
+ if ctx.Doer.EmailNotificationsPreference != user_model.EmailNotificationsAndYourOwn && !ctx.ForceDoerNotification {
+ visited.Add(ctx.Doer.ID)
+ }
// =========== Mentions ===========
if err = mailIssueCommentBatch(ctx, mentions, visited, true); err != nil {
- return fmt.Errorf("mailIssueCommentBatch() mentions: %v", err)
+ return fmt.Errorf("mailIssueCommentBatch() mentions: %w", err)
}
// Avoid mailing explicit unwatched
- ids, err = models.GetIssueWatchersIDs(ctx.Issue.ID, false)
+ ids, err = issues_model.GetIssueWatchersIDs(ctx, ctx.Issue.ID, false)
if err != nil {
- return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err)
- }
- for _, i := range ids {
- visited[i] = true
+ return fmt.Errorf("GetIssueWatchersIDs(%d): %w", ctx.Issue.ID, err)
}
+ visited.AddMultiple(ids...)
- unfilteredUsers, err := user_model.GetMaileableUsersByIDs(unfiltered, false)
+ unfilteredUsers, err := user_model.GetMaileableUsersByIDs(ctx, unfiltered, false)
if err != nil {
return err
}
if err = mailIssueCommentBatch(ctx, unfilteredUsers, visited, false); err != nil {
- return fmt.Errorf("mailIssueCommentBatch(): %v", err)
+ return fmt.Errorf("mailIssueCommentBatch(): %w", err)
}
return nil
}
-func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, visited map[int64]bool, fromMention bool) error {
+func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, visited container.Set[int64], fromMention bool) error {
checkUnit := unit.TypeIssues
if ctx.Issue.IsPull {
checkUnit = unit.TypePullRequests
@@ -132,20 +135,18 @@ func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, vi
// At this point we exclude:
// user that don't have all mails enabled or users only get mail on mention and this is one ...
if !(user.EmailNotificationsPreference == user_model.EmailNotificationsEnabled ||
+ user.EmailNotificationsPreference == user_model.EmailNotificationsAndYourOwn ||
fromMention && user.EmailNotificationsPreference == user_model.EmailNotificationsOnMention) {
continue
}
// if we have already visited this user we exclude them
- if _, ok := visited[user.ID]; ok {
+ if !visited.Add(user.ID) {
continue
}
- // now mark them as visited
- visited[user.ID] = true
-
// test if this user is allowed to see the issue/pull
- if !models.CheckRepoUnitUser(ctx.Issue.Repo, user, checkUnit) {
+ if !access_model.CheckRepoUnitUser(ctx, ctx.Issue.Repo, user, checkUnit) {
continue
}
@@ -171,26 +172,28 @@ func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, vi
// MailParticipants sends new issue thread created emails to repository watchers
// and mentioned people.
-func MailParticipants(issue *models.Issue, doer *user_model.User, opType models.ActionType, mentions []*user_model.User) error {
+func MailParticipants(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, opType activities_model.ActionType, mentions []*user_model.User) error {
if setting.MailService == nil {
// No mail service configured
return nil
}
content := issue.Content
- if opType == models.ActionCloseIssue || opType == models.ActionClosePullRequest ||
- opType == models.ActionReopenIssue || opType == models.ActionReopenPullRequest ||
- opType == models.ActionMergePullRequest {
+ if opType == activities_model.ActionCloseIssue || opType == activities_model.ActionClosePullRequest ||
+ opType == activities_model.ActionReopenIssue || opType == activities_model.ActionReopenPullRequest ||
+ opType == activities_model.ActionMergePullRequest || opType == activities_model.ActionAutoMergePullRequest {
content = ""
}
+ forceDoerNotification := opType == activities_model.ActionAutoMergePullRequest
if err := mailIssueCommentToParticipants(
&mailCommentContext{
- Context: context.TODO(), // TODO: use a correct context
- Issue: issue,
- Doer: doer,
- ActionType: opType,
- Content: content,
- Comment: nil,
+ Context: ctx,
+ Issue: issue,
+ Doer: doer,
+ ActionType: opType,
+ Content: content,
+ Comment: nil,
+ ForceDoerNotification: forceDoerNotification,
}, mentions); err != nil {
log.Error("mailIssueCommentToParticipants: %v", err)
}
diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go
index b6bddeac045c3..96227270c875e 100644
--- a/services/mailer/mail_release.go
+++ b/services/mailer/mail_release.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package mailer
@@ -8,7 +7,6 @@ import (
"bytes"
"context"
- "code.gitea.io/gitea/models"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
@@ -24,8 +22,8 @@ const (
tplNewReleaseMail base.TplName = "release"
)
-// MailNewRelease send new release notify to all all repo watchers.
-func MailNewRelease(ctx context.Context, rel *models.Release) {
+// 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
return
@@ -37,7 +35,7 @@ func MailNewRelease(ctx context.Context, rel *models.Release) {
return
}
- recipients, err := user_model.GetMaileableUsersByIDs(watcherIDList, false)
+ recipients, err := user_model.GetMaileableUsersByIDs(ctx, watcherIDList, false)
if err != nil {
log.Error("user_model.GetMaileableUsersByIDs: %v", err)
return
@@ -55,7 +53,7 @@ func MailNewRelease(ctx context.Context, rel *models.Release) {
}
}
-func mailNewRelease(ctx context.Context, lang string, tos []string, rel *models.Release) {
+func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_model.Release) {
locale := translation.NewLocale(lang)
var err error
@@ -75,7 +73,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *models.
"Subject": subject,
"Language": locale.Language(),
// helper
- "i18n": locale,
+ "locale": locale,
"Str2html": templates.Str2html,
"DotEscape": templates.DotEscape,
}
diff --git a/services/mailer/mail_repo.go b/services/mailer/mail_repo.go
index 7f856f2d403e0..5fa13f5044027 100644
--- a/services/mailer/mail_repo.go
+++ b/services/mailer/mail_repo.go
@@ -1,14 +1,13 @@
// 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.
+// SPDX-License-Identifier: MIT
package mailer
import (
"bytes"
+ "context"
"fmt"
- "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
@@ -18,14 +17,14 @@ import (
)
// SendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created
-func SendRepoTransferNotifyMail(doer, newOwner *user_model.User, repo *repo_model.Repository) error {
+func SendRepoTransferNotifyMail(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
if setting.MailService == nil {
// No mail service configured
return nil
}
if newOwner.IsOrganization() {
- users, err := organization.GetUsersWhoCanCreateOrgRepo(db.DefaultContext, newOwner.ID)
+ users, err := organization.GetUsersWhoCanCreateOrgRepo(ctx, newOwner.ID)
if err != nil {
return err
}
@@ -74,7 +73,7 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
"Language": locale.Language(),
"Destination": destination,
// helper
- "i18n": locale,
+ "locale": locale,
"Str2html": templates.Str2html,
"DotEscape": templates.DotEscape,
}
diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go
new file mode 100644
index 0000000000000..54e82b02343e1
--- /dev/null
+++ b/services/mailer/mail_team_invite.go
@@ -0,0 +1,61 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+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.GetUserByID(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/mailer/mail_test.go b/services/mailer/mail_test.go
index baf426146acf3..64f2f740ca2db 100644
--- a/services/mailer/mail_test.go
+++ b/services/mailer/mail_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package mailer
@@ -9,12 +8,14 @@ import (
"context"
"fmt"
"html/template"
+ "regexp"
"strings"
"testing"
texttmpl "text/template"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -46,7 +47,7 @@ const bodyTpl = `
`
-func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *models.Issue, comment *models.Comment) {
+func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment) {
assert.NoError(t, unittest.PrepareTestDatabase())
mailService := setting.Mailer{
From: "test@gitea.com",
@@ -55,55 +56,58 @@ func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Re
setting.MailService = &mailService
setting.Domain = "localhost"
- doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
- repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, Owner: doer}).(*repo_model.Repository)
- issue = unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
+ doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, Owner: doer})
+ issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1, Repo: repo, Poster: doer})
assert.NoError(t, issue.LoadRepo(db.DefaultContext))
- comment = unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
- return
+ comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2, Issue: issue})
+ return doer, repo, issue, comment
}
func TestComposeIssueCommentMessage(t *testing.T) {
doer, _, issue, comment := prepareMailerTest(t)
- stpl := texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl))
- btpl := template.Must(template.New("issue/comment").Parse(bodyTpl))
- InitMailRender(stpl, btpl)
+ setting.IncomingEmail.Enabled = true
+ defer func() { setting.IncomingEmail.Enabled = false }()
+
+ subjectTemplates = texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl))
+ bodyTemplates = template.Must(template.New("issue/comment").Parse(bodyTpl))
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
msgs, err := composeIssueCommentMessages(&mailCommentContext{
Context: context.TODO(), // TODO: use a correct context
- Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue,
+ Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
Content: "test body", Comment: comment,
}, "en-US", recipients, false, "issue comment")
assert.NoError(t, err)
assert.Len(t, msgs, 2)
gomailMsg := msgs[0].ToMessage()
- mailto := gomailMsg.GetHeader("To")
- subject := gomailMsg.GetHeader("Subject")
- messageID := gomailMsg.GetHeader("Message-ID")
- inReplyTo := gomailMsg.GetHeader("In-Reply-To")
- references := gomailMsg.GetHeader("References")
-
- assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field")
- assert.Equal(t, "Re: ", subject[0][:4], "Comment reply subject should contain Re:")
- assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject[0])
- assert.Equal(t, "", inReplyTo[0], "In-Reply-To header doesn't match")
- assert.Equal(t, "", references[0], "References header doesn't match")
- assert.Equal(t, "", messageID[0], "Message-ID header doesn't match")
+ replyTo := gomailMsg.GetHeader("Reply-To")[0]
+ subject := gomailMsg.GetHeader("Subject")[0]
+
+ assert.Len(t, gomailMsg.GetHeader("To"), 1, "exactly one recipient is expected in the To field")
+ tokenRegex := regexp.MustCompile(`\Aincoming\+(.+)@localhost\z`)
+ assert.Regexp(t, tokenRegex, replyTo)
+ token := tokenRegex.FindAllStringSubmatch(replyTo, 1)[0][1]
+ assert.Equal(t, "Re: ", subject[:4], "Comment reply subject should contain Re:")
+ assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject)
+ assert.Equal(t, "", gomailMsg.GetHeader("In-Reply-To")[0], "In-Reply-To header doesn't match")
+ assert.ElementsMatch(t, []string{"", ""}, gomailMsg.GetHeader("References"), "References header doesn't match")
+ assert.Equal(t, "", gomailMsg.GetHeader("Message-ID")[0], "Message-ID header doesn't match")
+ assert.Equal(t, "", gomailMsg.GetHeader("List-Post")[0])
+ assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 2) // url + mailto
}
func TestComposeIssueMessage(t *testing.T) {
doer, _, issue, _ := prepareMailerTest(t)
- stpl := texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
- btpl := template.Must(template.New("issue/new").Parse(bodyTpl))
- InitMailRender(stpl, btpl)
+ subjectTemplates = texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
+ bodyTemplates = template.Must(template.New("issue/new").Parse(bodyTpl))
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
msgs, err := composeIssueCommentMessages(&mailCommentContext{
Context: context.TODO(), // TODO: use a correct context
- Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue,
+ Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
Content: "test body",
}, "en-US", recipients, false, "issue create")
assert.NoError(t, err)
@@ -121,23 +125,23 @@ func TestComposeIssueMessage(t *testing.T) {
assert.Equal(t, "", inReplyTo[0], "In-Reply-To header doesn't match")
assert.Equal(t, "", references[0], "References header doesn't match")
assert.Equal(t, "", messageID[0], "Message-ID header doesn't match")
+ assert.Empty(t, gomailMsg.GetHeader("List-Post")) // incoming mail feature disabled
+ assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 1) // url without mailto
}
func TestTemplateSelection(t *testing.T) {
doer, repo, issue, comment := prepareMailerTest(t)
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
- stpl := texttmpl.Must(texttmpl.New("issue/default").Parse("issue/default/subject"))
- texttmpl.Must(stpl.New("issue/new").Parse("issue/new/subject"))
- texttmpl.Must(stpl.New("pull/comment").Parse("pull/comment/subject"))
- texttmpl.Must(stpl.New("issue/close").Parse("")) // Must default to fallback subject
+ subjectTemplates = texttmpl.Must(texttmpl.New("issue/default").Parse("issue/default/subject"))
+ texttmpl.Must(subjectTemplates.New("issue/new").Parse("issue/new/subject"))
+ texttmpl.Must(subjectTemplates.New("pull/comment").Parse("pull/comment/subject"))
+ texttmpl.Must(subjectTemplates.New("issue/close").Parse("")) // Must default to fallback subject
- btpl := template.Must(template.New("issue/default").Parse("issue/default/body"))
- template.Must(btpl.New("issue/new").Parse("issue/new/body"))
- template.Must(btpl.New("pull/comment").Parse("pull/comment/body"))
- template.Must(btpl.New("issue/close").Parse("issue/close/body"))
-
- InitMailRender(stpl, btpl)
+ bodyTemplates = template.Must(template.New("issue/default").Parse("issue/default/body"))
+ template.Must(bodyTemplates.New("issue/new").Parse("issue/new/body"))
+ template.Must(bodyTemplates.New("pull/comment").Parse("pull/comment/body"))
+ template.Must(bodyTemplates.New("issue/close").Parse("issue/close/body"))
expect := func(t *testing.T, msg *Message, expSubject, expBody string) {
subject := msg.ToMessage().GetHeader("Subject")
@@ -150,30 +154,30 @@ func TestTemplateSelection(t *testing.T) {
msg := testComposeIssueCommentMessage(t, &mailCommentContext{
Context: context.TODO(), // TODO: use a correct context
- Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue,
+ Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
Content: "test body",
}, recipients, false, "TestTemplateSelection")
expect(t, msg, "issue/new/subject", "issue/new/body")
msg = testComposeIssueCommentMessage(t, &mailCommentContext{
Context: context.TODO(), // TODO: use a correct context
- Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue,
+ Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
Content: "test body", Comment: comment,
}, recipients, false, "TestTemplateSelection")
expect(t, msg, "issue/default/subject", "issue/default/body")
- pull := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 2, Repo: repo, Poster: doer}).(*models.Issue)
- comment = unittest.AssertExistsAndLoadBean(t, &models.Comment{ID: 4, Issue: pull}).(*models.Comment)
+ pull := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2, Repo: repo, Poster: doer})
+ comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4, Issue: pull})
msg = testComposeIssueCommentMessage(t, &mailCommentContext{
Context: context.TODO(), // TODO: use a correct context
- Issue: pull, Doer: doer, ActionType: models.ActionCommentPull,
+ Issue: pull, Doer: doer, ActionType: activities_model.ActionCommentPull,
Content: "test body", Comment: comment,
}, recipients, false, "TestTemplateSelection")
expect(t, msg, "pull/comment/subject", "pull/comment/body")
msg = testComposeIssueCommentMessage(t, &mailCommentContext{
Context: context.TODO(), // TODO: use a correct context
- Issue: issue, Doer: doer, ActionType: models.ActionCloseIssue,
+ Issue: issue, Doer: doer, ActionType: activities_model.ActionCloseIssue,
Content: "test body", Comment: comment,
}, recipients, false, "TestTemplateSelection")
expect(t, msg, "Re: [user2/repo1] issue1 (#1)", "issue/close/body")
@@ -183,12 +187,11 @@ func TestTemplateServices(t *testing.T) {
doer, _, issue, comment := prepareMailerTest(t)
assert.NoError(t, issue.LoadRepo(db.DefaultContext))
- expect := func(t *testing.T, issue *models.Issue, comment *models.Comment, doer *user_model.User,
- actionType models.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string,
+ expect := func(t *testing.T, issue *issues_model.Issue, comment *issues_model.Comment, doer *user_model.User,
+ actionType activities_model.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string,
) {
- stpl := texttmpl.Must(texttmpl.New("issue/default").Parse(tplSubject))
- btpl := template.Must(template.New("issue/default").Parse(tplBody))
- InitMailRender(stpl, btpl)
+ subjectTemplates = texttmpl.Must(texttmpl.New("issue/default").Parse(tplSubject))
+ bodyTemplates = template.Must(template.New("issue/default").Parse(tplBody))
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
msg := testComposeIssueCommentMessage(t, &mailCommentContext{
@@ -206,19 +209,19 @@ func TestTemplateServices(t *testing.T) {
assert.Contains(t, wholemsg, "\r\n"+expBody+"\r\n")
}
- expect(t, issue, comment, doer, models.ActionCommentIssue, false,
+ expect(t, issue, comment, doer, activities_model.ActionCommentIssue, false,
"{{.SubjectPrefix}}[{{.Repo}}]: @{{.Doer.Name}} commented on #{{.Issue.Index}} - {{.Issue.Title}}",
"//{{.ActionType}},{{.ActionName}},{{if .IsMention}}norender{{end}}//",
"Re: [user2/repo1]: @user2 commented on #1 - issue1",
"//issue,comment,//")
- expect(t, issue, comment, doer, models.ActionCommentIssue, true,
+ expect(t, issue, comment, doer, activities_model.ActionCommentIssue, true,
"{{if .IsMention}}must render{{end}}",
"//subject is: {{.Subject}}//",
"must render",
"//subject is: must render//")
- expect(t, issue, comment, doer, models.ActionCommentIssue, true,
+ expect(t, issue, comment, doer, activities_model.ActionCommentIssue, true,
"{{.FallbackSubject}}",
"//{{.SubjectPrefix}}//",
"Re: [user2/repo1] issue1 (#1)",
@@ -243,7 +246,6 @@ func TestGenerateAdditionalHeaders(t *testing.T) {
expected := map[string]string{
"List-ID": "user2/repo1 ",
"List-Archive": "",
- "List-Unsubscribe": "https://try.gitea.io/user2/repo1/issues/1",
"X-Gitea-Reason": "dummy-reason",
"X-Gitea-Sender": "< Ur Tw ><",
"X-Gitea-Recipient": "Test",
@@ -268,97 +270,96 @@ func Test_createReference(t *testing.T) {
pullIssue.IsPull = true
type args struct {
- issue *models.Issue
- comment *models.Comment
- actionType models.ActionType
+ issue *issues_model.Issue
+ comment *issues_model.Comment
+ actionType activities_model.ActionType
}
tests := []struct {
name string
args args
prefix string
- suffix string
}{
{
name: "Open Issue",
args: args{
issue: issue,
- actionType: models.ActionCreateIssue,
+ actionType: activities_model.ActionCreateIssue,
},
- prefix: fmt.Sprintf("%s/issues/%d@%s", issue.Repo.FullName(), issue.Index, setting.Domain),
+ prefix: fmt.Sprintf("<%s/issues/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain),
},
{
name: "Open Pull",
args: args{
issue: pullIssue,
- actionType: models.ActionCreatePullRequest,
+ actionType: activities_model.ActionCreatePullRequest,
},
- prefix: fmt.Sprintf("%s/pulls/%d@%s", issue.Repo.FullName(), issue.Index, setting.Domain),
+ prefix: fmt.Sprintf("<%s/pulls/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain),
},
{
name: "Comment Issue",
args: args{
issue: issue,
comment: comment,
- actionType: models.ActionCommentIssue,
+ actionType: activities_model.ActionCommentIssue,
},
- prefix: fmt.Sprintf("%s/issues/%d/comment/%d@%s", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
+ prefix: fmt.Sprintf("<%s/issues/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
},
{
name: "Comment Pull",
args: args{
issue: pullIssue,
comment: comment,
- actionType: models.ActionCommentPull,
+ actionType: activities_model.ActionCommentPull,
},
- prefix: fmt.Sprintf("%s/pulls/%d/comment/%d@%s", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
+ prefix: fmt.Sprintf("<%s/pulls/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
},
{
name: "Close Issue",
args: args{
issue: issue,
- actionType: models.ActionCloseIssue,
+ actionType: activities_model.ActionCloseIssue,
},
- prefix: fmt.Sprintf("%s/issues/%d/close/", issue.Repo.FullName(), issue.Index),
+ prefix: fmt.Sprintf("<%s/issues/%d/close/", issue.Repo.FullName(), issue.Index),
},
{
name: "Close Pull",
args: args{
issue: pullIssue,
- actionType: models.ActionClosePullRequest,
+ actionType: activities_model.ActionClosePullRequest,
},
- prefix: fmt.Sprintf("%s/pulls/%d/close/", issue.Repo.FullName(), issue.Index),
+ prefix: fmt.Sprintf("<%s/pulls/%d/close/", issue.Repo.FullName(), issue.Index),
},
{
name: "Reopen Issue",
args: args{
issue: issue,
- actionType: models.ActionReopenIssue,
+ actionType: activities_model.ActionReopenIssue,
},
- prefix: fmt.Sprintf("%s/issues/%d/reopen/", issue.Repo.FullName(), issue.Index),
+ prefix: fmt.Sprintf("<%s/issues/%d/reopen/", issue.Repo.FullName(), issue.Index),
},
{
name: "Reopen Pull",
args: args{
issue: pullIssue,
- actionType: models.ActionReopenPullRequest,
+ actionType: activities_model.ActionReopenPullRequest,
},
- prefix: fmt.Sprintf("%s/pulls/%d/reopen/", issue.Repo.FullName(), issue.Index),
+ prefix: fmt.Sprintf("<%s/pulls/%d/reopen/", issue.Repo.FullName(), issue.Index),
},
{
name: "Merge Pull",
args: args{
issue: pullIssue,
- actionType: models.ActionMergePullRequest,
+ actionType: activities_model.ActionMergePullRequest,
},
- prefix: fmt.Sprintf("%s/pulls/%d/merge/", issue.Repo.FullName(), issue.Index),
+ prefix: fmt.Sprintf("<%s/pulls/%d/merge/", issue.Repo.FullName(), issue.Index),
},
{
name: "Ready Pull",
args: args{
issue: pullIssue,
- actionType: models.ActionPullRequestReadyForReview,
+ actionType: activities_model.ActionPullRequestReadyForReview,
},
- prefix: fmt.Sprintf("%s/pulls/%d/ready/", issue.Repo.FullName(), issue.Index),
+ prefix: fmt.Sprintf("<%s/pulls/%d/ready/", issue.Repo.FullName(), issue.Index),
},
}
for _, tt := range tests {
@@ -367,9 +368,6 @@ func Test_createReference(t *testing.T) {
if !strings.HasPrefix(got, tt.prefix) {
t.Errorf("createReference() = %v, want %v", got, tt.prefix)
}
- if !strings.HasSuffix(got, tt.suffix) {
- t.Errorf("createReference() = %v, want %v", got, tt.prefix)
- }
})
}
}
diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go
index 3ca9b50fc6147..4e03afb96142c 100644
--- a/services/mailer/mailer.go
+++ b/services/mailer/mailer.go
@@ -1,12 +1,12 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// 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.
+// SPDX-License-Identifier: MIT
package mailer
import (
"bytes"
+ "context"
"crypto/tls"
"fmt"
"hash/fnv"
@@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
"github.com/jaytaylor/html2text"
"gopkg.in/gomail.v2"
@@ -35,6 +36,7 @@ type Message struct {
FromAddress string
FromDisplayName string
To []string
+ ReplyTo string
Subject string
Date time.Time
Body string
@@ -46,6 +48,9 @@ func (m *Message) ToMessage() *gomail.Message {
msg := gomail.NewMessage()
msg.SetAddressHeader("From", m.FromAddress, m.FromDisplayName)
msg.SetHeader("To", m.To...)
+ if m.ReplyTo != "" {
+ msg.SetHeader("Reply-To", m.ReplyTo)
+ }
for header := range m.Headers {
msg.SetHeader(header, m.Headers[header]...)
}
@@ -147,65 +152,82 @@ type smtpSender struct{}
func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
opts := setting.MailService
- host, port, err := net.SplitHostPort(opts.Host)
- if err != nil {
- return err
+ var network string
+ var address string
+ if opts.Protocol == "smtp+unix" {
+ network = "unix"
+ address = opts.SMTPAddr
+ } else {
+ network = "tcp"
+ address = net.JoinHostPort(opts.SMTPAddr, opts.SMTPPort)
}
- tlsconfig := &tls.Config{
- InsecureSkipVerify: opts.SkipVerify,
- ServerName: host,
+ conn, err := net.Dial(network, address)
+ if err != nil {
+ return fmt.Errorf("failed to establish network connection to SMTP server: %w", err)
}
+ defer conn.Close()
- if opts.UseCertificate {
- cert, err := tls.LoadX509KeyPair(opts.CertFile, opts.KeyFile)
- if err != nil {
- return err
+ var tlsconfig *tls.Config
+ if opts.Protocol == "smtps" || opts.Protocol == "smtp+starttls" {
+ tlsconfig = &tls.Config{
+ InsecureSkipVerify: opts.ForceTrustServerCert,
+ ServerName: opts.SMTPAddr,
}
- tlsconfig.Certificates = []tls.Certificate{cert}
- }
- conn, err := net.Dial("tcp", net.JoinHostPort(host, port))
- if err != nil {
- return err
+ if opts.UseClientCert {
+ cert, err := tls.LoadX509KeyPair(opts.ClientCertFile, opts.ClientKeyFile)
+ if err != nil {
+ return fmt.Errorf("could not load SMTP client certificate: %w", err)
+ }
+ tlsconfig.Certificates = []tls.Certificate{cert}
+ }
}
- defer conn.Close()
- isSecureConn := opts.IsTLSEnabled || (strings.HasSuffix(port, "465"))
- // Start TLS directly if the port ends with 465 (SMTPS protocol)
- if isSecureConn {
+ if opts.Protocol == "smtps" {
conn = tls.Client(conn, tlsconfig)
}
+ host := "localhost"
+ if opts.Protocol == "smtp+unix" {
+ host = opts.SMTPAddr
+ }
client, err := smtp.NewClient(conn, host)
if err != nil {
- return fmt.Errorf("NewClient: %v", err)
+ return fmt.Errorf("could not initiate SMTP session: %w", err)
}
- if !opts.DisableHelo {
+ if opts.EnableHelo {
hostname := opts.HeloHostname
if len(hostname) == 0 {
hostname, err = os.Hostname()
if err != nil {
- return err
+ return fmt.Errorf("could not retrieve system hostname: %w", err)
}
}
if err = client.Hello(hostname); err != nil {
- return fmt.Errorf("Hello: %v", err)
+ return fmt.Errorf("failed to issue HELO command: %w", err)
}
}
- // If not using SMTPS, always use STARTTLS if available
- hasStartTLS, _ := client.Extension("STARTTLS")
- if !isSecureConn && hasStartTLS {
- if err = client.StartTLS(tlsconfig); err != nil {
- return fmt.Errorf("StartTLS: %v", err)
+ if opts.Protocol == "smtp+starttls" {
+ hasStartTLS, _ := client.Extension("STARTTLS")
+ if hasStartTLS {
+ if err = client.StartTLS(tlsconfig); err != nil {
+ return fmt.Errorf("failed to start TLS connection: %w", err)
+ }
+ } else {
+ log.Warn("StartTLS requested, but SMTP server does not support it; falling back to regular SMTP")
}
}
canAuth, options := client.Extension("AUTH")
- if canAuth && len(opts.User) > 0 {
+ if len(opts.User) > 0 {
+ if !canAuth {
+ return fmt.Errorf("SMTP server does not support AUTH, but credentials provided")
+ }
+
var auth smtp.Auth
if strings.Contains(options, "CRAM-MD5") {
@@ -219,34 +241,34 @@ func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
if auth != nil {
if err = client.Auth(auth); err != nil {
- return fmt.Errorf("Auth: %v", err)
+ return fmt.Errorf("failed to authenticate SMTP: %w", err)
}
}
}
if opts.OverrideEnvelopeFrom {
if err = client.Mail(opts.EnvelopeFrom); err != nil {
- return fmt.Errorf("Mail: %v", err)
+ return fmt.Errorf("failed to issue MAIL command: %w", err)
}
} else {
if err = client.Mail(from); err != nil {
- return fmt.Errorf("Mail: %v", err)
+ return fmt.Errorf("failed to issue MAIL command: %w", err)
}
}
for _, rec := range to {
if err = client.Rcpt(rec); err != nil {
- return fmt.Errorf("Rcpt: %v", err)
+ return fmt.Errorf("failed to issue RCPT command: %w", err)
}
}
w, err := client.Data()
if err != nil {
- return fmt.Errorf("Data: %v", err)
+ return fmt.Errorf("failed to issue DATA command: %w", err)
} else if _, err = msg.WriteTo(w); err != nil {
- return fmt.Errorf("WriteTo: %v", err)
+ return fmt.Errorf("SMTP write failed: %w", err)
} else if err = w.Close(); err != nil {
- return fmt.Errorf("Close: %v", err)
+ return fmt.Errorf("SMTP close failed: %w", err)
}
return client.Quit()
@@ -281,6 +303,7 @@ func (s *sendmailSender) Send(from string, to []string, msg io.WriterTo) error {
if err != nil {
return err
}
+ process.SetSysProcAttribute(cmd)
if err = cmd.Start(); err != nil {
_ = pipe.Close()
@@ -329,7 +352,7 @@ var mailQueue queue.Queue
var Sender gomail.Sender
// NewContext start mail queue service
-func NewContext() {
+func NewContext(ctx context.Context) {
// Need to check if mailQueue is nil because in during reinstall (user had installed
// before but switched install lock off), this function will be called again
// while mail queue is already processing tasks, and produces a race condition.
@@ -337,13 +360,13 @@ func NewContext() {
return
}
- switch setting.MailService.MailerType {
- case "smtp":
- Sender = &smtpSender{}
+ switch setting.MailService.Protocol {
case "sendmail":
Sender = &sendmailSender{}
case "dummy":
Sender = &dummySender{}
+ default:
+ Sender = &smtpSender{}
}
mailQueue = queue.CreateQueue("mail", func(data ...queue.Data) []queue.Data {
@@ -361,6 +384,8 @@ func NewContext() {
}, &Message{})
go graceful.GetManager().RunWithShutdownFns(mailQueue.Run)
+
+ subjectTemplates, bodyTemplates = templates.Mailer(ctx)
}
// SendAsync send mail asynchronously
diff --git a/services/mailer/mailer_test.go b/services/mailer/mailer_test.go
index b94fce8443ae8..79c5231218dbb 100644
--- a/services/mailer/mailer_test.go
+++ b/services/mailer/mailer_test.go
@@ -1,6 +1,5 @@
// Copyright 2021 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package mailer
diff --git a/services/mailer/main_test.go b/services/mailer/main_test.go
index 0bd154113f039..16a6a26545636 100644
--- a/services/mailer/main_test.go
+++ b/services/mailer/main_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package mailer
diff --git a/services/mailer/token/token.go b/services/mailer/token/token.go
new file mode 100644
index 0000000000000..8a5a762d6b5fd
--- /dev/null
+++ b/services/mailer/token/token.go
@@ -0,0 +1,128 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package token
+
+import (
+ "context"
+ crypto_hmac "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base32"
+ "fmt"
+ "time"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/util"
+)
+
+// A token is a verifiable container describing an action.
+//
+// A token has a dynamic length depending on the contained data and has the following structure:
+// | Token Version | User ID | HMAC | Payload |
+//
+// The payload is verifiable by the generated HMAC using the user secret. It contains:
+// | Timestamp | Action/Handler Type | Action/Handler Data |
+
+const (
+ tokenVersion1 byte = 1
+ tokenLifetimeInYears int = 1
+)
+
+type HandlerType byte
+
+const (
+ UnknownHandlerType HandlerType = iota
+ ReplyHandlerType
+ UnsubscribeHandlerType
+)
+
+var encodingWithoutPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
+
+type ErrToken struct {
+ context string
+}
+
+func (err *ErrToken) Error() string {
+ return "invalid email token: " + err.context
+}
+
+func (err *ErrToken) Unwrap() error {
+ return util.ErrInvalidArgument
+}
+
+// CreateToken creates a token for the action/user tuple
+func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, error) {
+ payload, err := util.PackData(
+ time.Now().AddDate(tokenLifetimeInYears, 0, 0).Unix(),
+ ht,
+ data,
+ )
+ if err != nil {
+ return "", err
+ }
+
+ packagedData, err := util.PackData(
+ user.ID,
+ generateHmac([]byte(user.Rands), payload),
+ payload,
+ )
+ if err != nil {
+ return "", err
+ }
+
+ return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil
+}
+
+// ExtractToken extracts the action/user tuple from the token and verifies the content
+func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.User, []byte, error) {
+ data, err := encodingWithoutPadding.DecodeString(token)
+ if err != nil {
+ return UnknownHandlerType, nil, nil, err
+ }
+
+ if len(data) < 1 {
+ return UnknownHandlerType, nil, nil, &ErrToken{"no data"}
+ }
+
+ if data[0] != tokenVersion1 {
+ return UnknownHandlerType, nil, nil, &ErrToken{fmt.Sprintf("unsupported token version: %v", data[0])}
+ }
+
+ var userID int64
+ var hmac []byte
+ var payload []byte
+ if err := util.UnpackData(data[1:], &userID, &hmac, &payload); err != nil {
+ return UnknownHandlerType, nil, nil, err
+ }
+
+ user, err := user_model.GetUserByID(ctx, userID)
+ if err != nil {
+ return UnknownHandlerType, nil, nil, err
+ }
+
+ if !crypto_hmac.Equal(hmac, generateHmac([]byte(user.Rands), payload)) {
+ return UnknownHandlerType, nil, nil, &ErrToken{"verification failed"}
+ }
+
+ var expiresUnix int64
+ var handlerType HandlerType
+ var innerPayload []byte
+ if err := util.UnpackData(payload, &expiresUnix, &handlerType, &innerPayload); err != nil {
+ return UnknownHandlerType, nil, nil, err
+ }
+
+ if time.Unix(expiresUnix, 0).Before(time.Now()) {
+ return UnknownHandlerType, nil, nil, &ErrToken{"token expired"}
+ }
+
+ return handlerType, user, innerPayload, nil
+}
+
+// generateHmac creates a trunkated HMAC for the given payload
+func generateHmac(secret, payload []byte) []byte {
+ mac := crypto_hmac.New(sha256.New, secret)
+ mac.Write(payload)
+ hmac := mac.Sum(nil)
+
+ return hmac[:10] // RFC2104 recommends not using less then 80 bits
+}
diff --git a/services/markup/main_test.go b/services/markup/main_test.go
new file mode 100644
index 0000000000000..ce892435a1aed
--- /dev/null
+++ b/services/markup/main_test.go
@@ -0,0 +1,18 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+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 0000000000000..2897f203a90fb
--- /dev/null
+++ b/services/markup/processorhelper.go
@@ -0,0 +1,32 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+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 0000000000000..6c9c1c27e72fb
--- /dev/null
+++ b/services/markup/processorhelper_test.go
@@ -0,0 +1,52 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+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/codebase.go b/services/migrations/codebase.go
index bb74c0a49d1dd..c02c8e13a2e3a 100644
--- a/services/migrations/codebase.go
+++ b/services/migrations/codebase.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
@@ -107,9 +106,24 @@ func NewCodebaseDownloader(ctx context.Context, projectURL *url.URL, project, re
commitMap: make(map[string]string),
}
+ log.Trace("Create Codebase downloader. BaseURL: %s Project: %s RepoName: %s", baseURL, project, repoName)
return downloader
}
+// String implements Stringer
+func (d *CodebaseDownloader) String() string {
+ return fmt.Sprintf("migration from codebase server %s %s/%s", d.baseURL, d.project, d.repoName)
+}
+
+// ColorFormat provides a basic color format for a GogsDownloader
+func (d *CodebaseDownloader) ColorFormat(s fmt.State) {
+ if d == nil {
+ log.ColorFprintf(s, "")
+ return
+ }
+ log.ColorFprintf(s, "migration from codebase server %s %s/%s", d.baseURL, d.project, d.repoName)
+}
+
// FormatCloneURL add authentication into remote URLs
func (d *CodebaseDownloader) FormatCloneURL(opts base.MigrateOptions, remoteAddr string) (string, error) {
return opts.CloneAddr, nil
@@ -451,8 +465,8 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq
Value int64 `xml:",chardata"`
Type string `xml:"type,attr"`
} `xml:"id"`
- SourceRef string `xml:"source-ref"`
- TargetRef string `xml:"target-ref"`
+ SourceRef string `xml:"source-ref"` // NOTE: from the documentation these are actually just branches NOT full refs
+ TargetRef string `xml:"target-ref"` // NOTE: from the documentation these are actually just branches NOT full refs
Subject string `xml:"subject"`
Status string `xml:"status"`
UserID struct {
@@ -564,6 +578,9 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq
Comments: comments[1:],
},
})
+
+ // SECURITY: Ensure that the PR is safe
+ _ = CheckAndEnsureSafePR(pullRequests[len(pullRequests)-1], d.baseURL.String(), d)
}
return pullRequests, true, nil
diff --git a/services/migrations/codebase_test.go b/services/migrations/codebase_test.go
index cb70a2bf75936..68721e06410f1 100644
--- a/services/migrations/codebase_test.go
+++ b/services/migrations/codebase_test.go
@@ -1,12 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
import (
"context"
- "fmt"
"net/url"
"os"
"testing"
@@ -40,7 +38,7 @@ func TestCodebaseDownloadRepo(t *testing.T) {
AuthPassword: apiPassword,
})
if err != nil {
- t.Fatal(fmt.Sprintf("Error creating Codebase downloader: %v", err))
+ t.Fatalf("Error creating Codebase downloader: %v", err)
}
repo, err := downloader.GetRepoInfo()
assert.NoError(t, err)
diff --git a/services/migrations/common.go b/services/migrations/common.go
new file mode 100644
index 0000000000000..34d7c93ddf0b7
--- /dev/null
+++ b/services/migrations/common.go
@@ -0,0 +1,81 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package migrations
+
+import (
+ "fmt"
+ "strings"
+
+ 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"
+)
+
+// WarnAndNotice will log the provided message and send a repository notice
+func WarnAndNotice(fmtStr string, args ...interface{}) {
+ log.Warn(fmtStr, args...)
+ if err := system_model.CreateRepositoryNotice(fmt.Sprintf(fmtStr, args...)); err != nil {
+ log.Error("create repository notice failed: ", err)
+ }
+}
+
+func hasBaseURL(toCheck, baseURL string) bool {
+ if len(baseURL) > 0 && baseURL[len(baseURL)-1] != '/' {
+ baseURL += "/"
+ }
+ return strings.HasPrefix(toCheck, baseURL)
+}
+
+// CheckAndEnsureSafePR will check that a given PR is safe to download
+func CheckAndEnsureSafePR(pr *base.PullRequest, commonCloneBaseURL string, g base.Downloader) bool {
+ valid := true
+ // SECURITY: the patchURL must be checked to have the same baseURL as the current to prevent open redirect
+ if pr.PatchURL != "" && !hasBaseURL(pr.PatchURL, commonCloneBaseURL) {
+ // TODO: Should we check that this url has the expected format for a patch url?
+ WarnAndNotice("PR #%d in %s has invalid PatchURL: %s baseURL: %s", pr.Number, g, pr.PatchURL, commonCloneBaseURL)
+ pr.PatchURL = ""
+ valid = false
+ }
+
+ // SECURITY: the headCloneURL must be checked to have the same baseURL as the current to prevent open redirect
+ if pr.Head.CloneURL != "" && !hasBaseURL(pr.Head.CloneURL, commonCloneBaseURL) {
+ // TODO: Should we check that this url has the expected format for a patch url?
+ WarnAndNotice("PR #%d in %s has invalid HeadCloneURL: %s baseURL: %s", pr.Number, g, pr.Head.CloneURL, commonCloneBaseURL)
+ pr.Head.CloneURL = ""
+ valid = false
+ }
+
+ // SECURITY: SHAs Must be a SHA
+ if pr.MergeCommitSHA != "" && !git.IsValidSHAPattern(pr.MergeCommitSHA) {
+ WarnAndNotice("PR #%d in %s has invalid MergeCommitSHA: %s", pr.Number, g, pr.MergeCommitSHA)
+ pr.MergeCommitSHA = ""
+ }
+ if pr.Head.SHA != "" && !git.IsValidSHAPattern(pr.Head.SHA) {
+ WarnAndNotice("PR #%d in %s has invalid HeadSHA: %s", pr.Number, g, pr.Head.SHA)
+ pr.Head.SHA = ""
+ valid = false
+ }
+ if pr.Base.SHA != "" && !git.IsValidSHAPattern(pr.Base.SHA) {
+ WarnAndNotice("PR #%d in %s has invalid BaseSHA: %s", pr.Number, g, pr.Base.SHA)
+ pr.Base.SHA = ""
+ valid = false
+ }
+
+ // SECURITY: Refs must be valid refs or SHAs
+ if pr.Head.Ref != "" && !git.IsValidRefPattern(pr.Head.Ref) {
+ WarnAndNotice("PR #%d in %s has invalid HeadRef: %s", pr.Number, g, pr.Head.Ref)
+ pr.Head.Ref = ""
+ valid = false
+ }
+ if pr.Base.Ref != "" && !git.IsValidRefPattern(pr.Base.Ref) {
+ WarnAndNotice("PR #%d in %s has invalid BaseRef: %s", pr.Number, g, pr.Base.Ref)
+ pr.Base.Ref = ""
+ valid = false
+ }
+
+ pr.EnsuredSafe = true
+
+ return valid
+}
diff --git a/services/migrations/dump.go b/services/migrations/dump.go
index 6410aa1ee0854..cc8518d4a25c6 100644
--- a/services/migrations/dump.go
+++ b/services/migrations/dump.go
@@ -1,17 +1,16 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
import (
"context"
+ "errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
- "path"
"path/filepath"
"strconv"
"strings"
@@ -25,7 +24,8 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
- "gopkg.in/yaml.v2"
+ "github.com/google/uuid"
+ "gopkg.in/yaml.v3"
)
var _ base.Uploader = &RepositoryDumper{}
@@ -46,7 +46,7 @@ type RepositoryDumper struct {
reviewFiles map[int64]*os.File
gitRepo *git.Repository
- prHeadCache map[string]struct{}
+ prHeadCache map[string]string
}
// NewRepositoryDumper creates an gitea Uploader
@@ -61,7 +61,7 @@ func NewRepositoryDumper(ctx context.Context, baseDir, repoOwner, repoName strin
baseDir: baseDir,
repoOwner: repoOwner,
repoName: repoName,
- prHeadCache: make(map[string]struct{}),
+ prHeadCache: make(map[string]string),
commentFiles: make(map[int64]*os.File),
reviewFiles: make(map[int64]*os.File),
}, nil
@@ -156,7 +156,10 @@ func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOp
SkipTLSVerify: setting.Migrations.SkipTLSVerify,
})
if err != nil {
- return fmt.Errorf("Clone: %v", err)
+ return fmt.Errorf("Clone: %w", err)
+ }
+ if err := git.WriteCommitGraph(g.ctx, repoPath); err != nil {
+ return err
}
if opts.Wiki {
@@ -164,7 +167,7 @@ func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOp
wikiRemotePath := repository.WikiRemoteURL(g.ctx, remoteAddr)
if len(wikiRemotePath) > 0 {
if err := os.MkdirAll(wikiPath, os.ModePerm); err != nil {
- return fmt.Errorf("Failed to remove %s: %v", wikiPath, err)
+ return fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
}
if err := git.Clone(g.ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
@@ -176,8 +179,10 @@ func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOp
}); err != nil {
log.Warn("Clone wiki: %v", err)
if err := os.RemoveAll(wikiPath); err != nil {
- return fmt.Errorf("Failed to remove %s: %v", wikiPath, err)
+ return fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
}
+ } else if err := git.WriteCommitGraph(g.ctx, wikiPath); err != nil {
+ return err
}
}
}
@@ -290,8 +295,10 @@ func (g *RepositoryDumper) CreateReleases(releases ...*base.Release) error {
}
for _, asset := range release.Assets {
attachLocalPath := filepath.Join(attachDir, asset.Name)
- // download attachment
+ // SECURITY: We cannot check the DownloadURL and DownloadFunc are safe here
+ // ... we must assume that they are safe and simply download the attachment
+ // download attachment
err := func(attachPath string) error {
var rc io.ReadCloser
var err error
@@ -311,7 +318,7 @@ func (g *RepositoryDumper) CreateReleases(releases ...*base.Release) error {
fw, err := os.Create(attachPath)
if err != nil {
- return fmt.Errorf("Create: %v", err)
+ return fmt.Errorf("create: %w", err)
}
defer fw.Close()
@@ -379,27 +386,29 @@ func (g *RepositoryDumper) createItems(dir string, itemFiles map[int64]*os.File,
}
for number, items := range itemsMap {
- var err error
- itemFile := itemFiles[number]
- if itemFile == nil {
- itemFile, err = os.Create(filepath.Join(dir, fmt.Sprintf("%d.yml", number)))
- if err != nil {
- return err
- }
- itemFiles[number] = itemFile
- }
-
- bs, err := yaml.Marshal(items)
- if err != nil {
+ if err := g.encodeItems(number, items, dir, itemFiles); err != nil {
return err
}
+ }
+
+ return nil
+}
- if _, err := itemFile.Write(bs); err != nil {
+func (g *RepositoryDumper) encodeItems(number int64, items []interface{}, dir string, itemFiles map[int64]*os.File) error {
+ itemFile := itemFiles[number]
+ if itemFile == nil {
+ var err error
+ itemFile, err = os.Create(filepath.Join(dir, fmt.Sprintf("%d.yml", number)))
+ if err != nil {
return err
}
+ itemFiles[number] = itemFile
}
- return nil
+ encoder := yaml.NewEncoder(itemFile)
+ defer encoder.Close()
+
+ return encoder.Encode(items)
}
// CreateComments creates comments of issues
@@ -412,102 +421,175 @@ func (g *RepositoryDumper) CreateComments(comments ...*base.Comment) error {
return g.createItems(g.commentDir(), g.commentFiles, commentsMap)
}
-// CreatePullRequests creates pull requests
-func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error {
- for _, pr := range prs {
- // download patch file
- err := func() error {
- u, err := g.setURLToken(pr.PatchURL)
- if err != nil {
- return err
- }
- resp, err := http.Get(u)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
- pullDir := filepath.Join(g.gitPath(), "pulls")
- if err = os.MkdirAll(pullDir, os.ModePerm); err != nil {
- return err
- }
- fPath := filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number))
- f, err := os.Create(fPath)
- if err != nil {
- return err
- }
- defer f.Close()
- if _, err = io.Copy(f, resp.Body); err != nil {
- return err
- }
- pr.PatchURL = "git/pulls/" + fmt.Sprintf("%d.patch", pr.Number)
+func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error {
+ // SECURITY: this pr must have been ensured safe
+ if !pr.EnsuredSafe {
+ log.Error("PR #%d in %s/%s has not been checked for safety ... We will ignore this.", pr.Number, g.repoOwner, g.repoName)
+ return fmt.Errorf("unsafe PR #%d", pr.Number)
+ }
+ // First we download the patch file
+ err := func() error {
+ // if the patchURL is empty there is nothing to download
+ if pr.PatchURL == "" {
return nil
- }()
+ }
+
+ // SECURITY: We will assume that the pr.PatchURL has been checked
+ // pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe
+ u, err := g.setURLToken(pr.PatchURL)
if err != nil {
return err
}
- // set head information
- pullHead := filepath.Join(g.gitPath(), "refs", "pull", fmt.Sprintf("%d", pr.Number))
- if err := os.MkdirAll(pullHead, os.ModePerm); err != nil {
+ // SECURITY: We will assume that the pr.PatchURL has been checked
+ // pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe
+ resp, err := http.Get(u) // TODO: This probably needs to use the downloader as there may be rate limiting issues here
+ if err != nil {
return err
}
- p, err := os.Create(filepath.Join(pullHead, "head"))
- if err != nil {
+ defer resp.Body.Close()
+ pullDir := filepath.Join(g.gitPath(), "pulls")
+ if err = os.MkdirAll(pullDir, os.ModePerm); err != nil {
return err
}
- _, err = p.WriteString(pr.Head.SHA)
- p.Close()
+ fPath := filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number))
+ f, err := os.Create(fPath)
if err != nil {
return err
}
+ defer f.Close()
- if pr.IsForkPullRequest() && pr.State != "closed" {
- if pr.Head.OwnerName != "" {
- remote := pr.Head.OwnerName
- _, ok := g.prHeadCache[remote]
- if !ok {
- // git remote add
- // TODO: how to handle private CloneURL?
- err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
- if err != nil {
- log.Error("AddRemote failed: %s", err)
- } else {
- g.prHeadCache[remote] = struct{}{}
- ok = true
- }
- }
+ // TODO: Should there be limits on the size of this file?
+ if _, err = io.Copy(f, resp.Body); err != nil {
+ return err
+ }
+ pr.PatchURL = "git/pulls/" + fmt.Sprintf("%d.patch", pr.Number)
- if ok {
- _, _, err = git.NewCommand(g.ctx, "fetch", remote, pr.Head.Ref).RunStdString(&git.RunOpts{Dir: g.gitPath()})
- if err != nil {
- log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err)
- } else {
- // a new branch name with will be created to as new head branch
- ref := path.Join(pr.Head.OwnerName, pr.Head.Ref)
- headBranch := filepath.Join(g.gitPath(), "refs", "heads", ref)
- if err := os.MkdirAll(filepath.Dir(headBranch), os.ModePerm); err != nil {
- return err
- }
- b, err := os.Create(headBranch)
- if err != nil {
- return err
- }
- _, err = b.WriteString(pr.Head.SHA)
- b.Close()
- if err != nil {
- return err
- }
- pr.Head.Ref = ref
- }
+ return nil
+ }()
+ if err != nil {
+ log.Error("PR #%d in %s/%s unable to download patch: %v", pr.Number, g.repoOwner, g.repoName, err)
+ return err
+ }
+
+ isFork := pr.IsForkPullRequest()
+
+ // Even if it's a forked repo PR, we have to change head info as the same as the base info
+ oldHeadOwnerName := pr.Head.OwnerName
+ pr.Head.OwnerName, pr.Head.RepoName = pr.Base.OwnerName, pr.Base.RepoName
+
+ if !isFork || pr.State == "closed" {
+ return nil
+ }
+
+ // OK we want to fetch the current head as a branch from its CloneURL
+
+ // 1. Is there a head clone URL available?
+ // 2. Is there a head ref available?
+ 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").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)
+ }
+ }
+ return nil
+ }
+
+ // 3. We need to create a remote for this clone url
+ // ... maybe we already have a name for this remote
+ remote, ok := g.prHeadCache[pr.Head.CloneURL+":"]
+ if !ok {
+ // ... let's try ownername as a reasonable name
+ remote = oldHeadOwnerName
+ if !git.IsValidRefPattern(remote) {
+ // ... let's try something less nice
+ remote = "head-pr-" + strconv.FormatInt(pr.Number, 10)
+ }
+ // ... now add the remote
+ err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
+ if err != nil {
+ log.Error("PR #%d in %s/%s AddRemote[%s] failed: %v", pr.Number, g.repoOwner, g.repoName, remote, err)
+ } else {
+ g.prHeadCache[pr.Head.CloneURL+":"] = remote
+ ok = true
+ }
+ }
+ if !ok {
+ // Set head information if pr.Head.SHA is available
+ if pr.Head.SHA != "" {
+ _, _, 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)
+ }
+ }
+
+ return nil
+ }
+
+ // 4. Check if we already have this ref?
+ localRef, ok := g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref]
+ if !ok {
+ // ... We would normally name this migrated branch as / but we need to ensure that is safe
+ localRef = git.SanitizeRefPattern(oldHeadOwnerName + "/" + pr.Head.Ref)
+
+ // ... Now we must assert that this does not exist
+ if g.gitRepo.IsBranchExist(localRef) {
+ localRef = "head-pr-" + strconv.FormatInt(pr.Number, 10) + "/" + localRef
+ i := 0
+ for g.gitRepo.IsBranchExist(localRef) {
+ if i > 5 {
+ // ... We tried, we really tried but this is just a seriously unfriendly repo
+ return fmt.Errorf("unable to create unique local reference from %s", pr.Head.Ref)
}
+ // OK just try some uuids!
+ localRef = git.SanitizeRefPattern("head-pr-" + strconv.FormatInt(pr.Number, 10) + uuid.New().String())
+ i++
}
}
- // whatever it's a forked repo PR, we have to change head info as the same as the base info
- pr.Head.OwnerName = pr.Base.OwnerName
- pr.Head.RepoName = pr.Base.RepoName
+
+ fetchArg := pr.Head.Ref + ":" + git.BranchPrefix + localRef
+ if strings.HasPrefix(fetchArg, "-") {
+ fetchArg = git.BranchPrefix + fetchArg
+ }
+
+ _, _, 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
+ // (This last step will likely fail but we should try to do as much as we can.)
+ } else {
+ // Cache the localRef as the Head.Ref - if we've failed we can always try again.
+ g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref] = localRef
+ }
+ }
+
+ // Set the pr.Head.Ref to the localRef
+ pr.Head.Ref = localRef
+
+ // 5. Now if pr.Head.SHA == "" we should recover this to the head of this branch
+ if pr.Head.SHA == "" {
+ headSha, err := g.gitRepo.GetBranchCommitID(localRef)
+ if err != nil {
+ log.Error("unable to get head SHA of local head for PR #%d from %s in %s/%s. Error: %v", pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err)
+ return nil
+ }
+ pr.Head.SHA = headSha
+ }
+ if pr.Head.SHA != "" {
+ _, _, 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)
+ }
}
+ return nil
+}
+
+// CreatePullRequests creates pull requests
+func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error {
var err error
if g.pullrequestFile == nil {
if err := os.MkdirAll(g.baseDir, os.ModePerm); err != nil {
@@ -519,16 +601,22 @@ func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error {
}
}
- bs, err := yaml.Marshal(prs)
- if err != nil {
- return err
- }
+ encoder := yaml.NewEncoder(g.pullrequestFile)
+ defer encoder.Close()
- if _, err := g.pullrequestFile.Write(bs); err != nil {
- return err
+ count := 0
+ for i := 0; i < len(prs); i++ {
+ pr := prs[i]
+ if err := g.handlePullRequest(pr); err != nil {
+ log.Error("PR #%d in %s/%s failed - skipping", pr.Number, g.repoOwner, g.repoName, err)
+ continue
+ }
+ prs[count] = pr
+ count++
}
+ prs = prs[:count]
- return nil
+ return encoder.Encode(prs)
}
// CreateReviews create pull request reviews
@@ -554,6 +642,10 @@ func (g *RepositoryDumper) Finish() error {
// DumpRepository dump repository according MigrateOptions to a local directory
func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.MigrateOptions) error {
+ doer, err := user_model.GetAdminUser()
+ if err != nil {
+ return err
+ }
downloader, err := newDownloader(ctx, ownerName, opts)
if err != nil {
return err
@@ -563,7 +655,7 @@ func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.Mi
return err
}
- if err := migrateRepository(downloader, uploader, opts, nil); err != nil {
+ if err := migrateRepository(doer, downloader, uploader, opts, nil); err != nil {
if err1 := uploader.Rollback(); err1 != nil {
log.Error("rollback failed: %v", err1)
}
@@ -572,7 +664,7 @@ func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.Mi
return nil
}
-func updateOptionsUnits(opts *base.MigrateOptions, units []string) {
+func updateOptionsUnits(opts *base.MigrateOptions, units []string) error {
if len(units) == 0 {
opts.Wiki = true
opts.Issues = true
@@ -584,7 +676,9 @@ func updateOptionsUnits(opts *base.MigrateOptions, units []string) {
opts.ReleaseAssets = true
} else {
for _, unit := range units {
- switch strings.ToLower(unit) {
+ switch strings.ToLower(strings.TrimSpace(unit)) {
+ case "":
+ continue
case "wiki":
opts.Wiki = true
case "issues":
@@ -601,9 +695,12 @@ func updateOptionsUnits(opts *base.MigrateOptions, units []string) {
opts.Comments = true
case "pull_requests":
opts.PullRequests = true
+ default:
+ return errors.New("invalid unit: " + unit)
}
}
}
+ return nil
}
// RestoreRepository restore a repository from the disk directory
@@ -626,9 +723,11 @@ func RestoreRepository(ctx context.Context, baseDir, ownerName, repoName string,
migrateOpts := base.MigrateOptions{
GitServiceType: structs.GitServiceType(tp),
}
- updateOptionsUnits(&migrateOpts, units)
+ if err := updateOptionsUnits(&migrateOpts, units); err != nil {
+ return err
+ }
- if err = migrateRepository(downloader, uploader, migrateOpts, nil); err != nil {
+ if err = migrateRepository(doer, downloader, uploader, migrateOpts, nil); err != nil {
if err1 := uploader.Rollback(); err1 != nil {
log.Error("rollback failed: %v", err1)
}
diff --git a/services/migrations/error.go b/services/migrations/error.go
index 3b3f975012f1e..46d303ac8ee8c 100644
--- a/services/migrations/error.go
+++ b/services/migrations/error.go
@@ -1,14 +1,13 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2018 Jonas Franz. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package migrations
import (
"errors"
- "github.com/google/go-github/v39/github"
+ "github.com/google/go-github/v45/github"
)
// ErrRepoNotCreated returns the error that repository not created
diff --git a/services/migrations/git.go b/services/migrations/git.go
index 3198f934caee0..22ffd5e765891 100644
--- a/services/migrations/git.go
+++ b/services/migrations/git.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
diff --git a/services/migrations/gitbucket.go b/services/migrations/gitbucket.go
index 92b6cac73806b..cc3d4fc93674a 100644
--- a/services/migrations/gitbucket.go
+++ b/services/migrations/gitbucket.go
@@ -1,14 +1,15 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
import (
"context"
+ "fmt"
"net/url"
"strings"
+ "code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/structs"
)
@@ -32,11 +33,16 @@ func (f *GitBucketDownloaderFactory) New(ctx context.Context, opts base.MigrateO
return nil, err
}
- baseURL := u.Scheme + "://" + u.Host
fields := strings.Split(u.Path, "/")
- oldOwner := fields[1]
- oldName := strings.TrimSuffix(fields[2], ".git")
+ if len(fields) < 2 {
+ return nil, fmt.Errorf("invalid path: %s", u.Path)
+ }
+ baseURL := u.Scheme + "://" + u.Host + strings.TrimSuffix(strings.Join(fields[:len(fields)-2], "/"), "/git")
+
+ oldOwner := fields[len(fields)-2]
+ oldName := strings.TrimSuffix(fields[len(fields)-1], ".git")
+ log.Trace("Create GitBucket downloader. BaseURL: %s RepoOwner: %s RepoName: %s", baseURL, oldOwner, oldName)
return NewGitBucketDownloader(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil
}
@@ -51,10 +57,25 @@ type GitBucketDownloader struct {
*GithubDownloaderV3
}
+// String implements Stringer
+func (g *GitBucketDownloader) String() string {
+ return fmt.Sprintf("migration from gitbucket server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
+}
+
+// ColorFormat provides a basic color format for a GitBucketDownloader
+func (g *GitBucketDownloader) ColorFormat(s fmt.State) {
+ if g == nil {
+ log.ColorFprintf(s, "")
+ return
+ }
+ log.ColorFprintf(s, "migration from gitbucket server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
+}
+
// NewGitBucketDownloader creates a GitBucket downloader
func NewGitBucketDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GitBucketDownloader {
githubDownloader := NewGithubDownloaderV3(ctx, baseURL, userName, password, token, repoOwner, repoName)
githubDownloader.SkipReactions = true
+ githubDownloader.SkipReviews = true
return &GitBucketDownloader{
githubDownloader,
}
diff --git a/services/migrations/gitea_downloader.go b/services/migrations/gitea_downloader.go
index 3c02e112ca73e..470090b5010a5 100644
--- a/services/migrations/gitea_downloader.go
+++ b/services/migrations/gitea_downloader.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
@@ -14,7 +13,6 @@ import (
"strings"
"time"
- admin_model "code.gitea.io/gitea/models/admin"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/structs"
@@ -71,6 +69,7 @@ type GiteaDownloader struct {
base.NullDownloader
ctx context.Context
client *gitea_sdk.Client
+ baseURL string
repoOwner string
repoName string
pagination bool
@@ -78,8 +77,9 @@ type GiteaDownloader struct {
}
// NewGiteaDownloader creates a gitea Downloader via gitea API
-// Use either a username/password or personal token. token is preferred
-// Note: Public access only allows very basic access
+//
+// Use either a username/password or personal token. token is preferred
+// Note: Public access only allows very basic access
func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, password, token string) (*GiteaDownloader, error) {
giteaClient, err := gitea_sdk.NewClient(
baseURL,
@@ -116,6 +116,7 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo
return &GiteaDownloader{
ctx: ctx,
client: giteaClient,
+ baseURL: baseURL,
repoOwner: path[0],
repoName: path[1],
pagination: paginationSupport,
@@ -128,6 +129,20 @@ func (g *GiteaDownloader) SetContext(ctx context.Context) {
g.ctx = ctx
}
+// String implements Stringer
+func (g *GiteaDownloader) String() string {
+ return fmt.Sprintf("migration from gitea server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
+}
+
+// ColorFormat provides a basic color format for a GiteaDownloader
+func (g *GiteaDownloader) ColorFormat(s fmt.State) {
+ if g == nil {
+ log.ColorFprintf(s, "")
+ return
+ }
+ log.ColorFprintf(s, "migration from gitea server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
+}
+
// GetRepoInfo returns a repository information
func (g *GiteaDownloader) GetRepoInfo() (*base.Repository, error) {
if g == nil {
@@ -283,6 +298,12 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
if err != nil {
return nil, err
}
+
+ if !hasBaseURL(asset.DownloadURL, g.baseURL) {
+ WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, asset.DownloadURL)
+ return io.NopCloser(strings.NewReader(asset.DownloadURL)), nil
+ }
+
// FIXME: for a private download?
req, err := http.NewRequest("GET", asset.DownloadURL, nil)
if err != nil {
@@ -386,7 +407,7 @@ func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, err
Type: gitea_sdk.IssueTypeIssue,
})
if err != nil {
- return nil, false, fmt.Errorf("error while listing issues: %v", err)
+ return nil, false, fmt.Errorf("error while listing issues: %w", err)
}
for _, issue := range issues {
@@ -402,11 +423,7 @@ func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, err
reactions, err := g.getIssueReactions(issue.Index)
if err != nil {
- log.Warn("Unable to load reactions during migrating issue #%d to %s/%s. Error: %v", issue.Index, g.repoOwner, g.repoName, err)
- if err2 := admin_model.CreateRepositoryNotice(
- fmt.Sprintf("Unable to load reactions during migrating issue #%d to %s/%s. Error: %v", issue.Index, g.repoOwner, g.repoName, err)); err2 != nil {
- log.Error("create repository notice failed: ", err2)
- }
+ WarnAndNotice("Unable to load reactions during migrating issue #%d in %s. Error: %v", issue.Index, g, err)
}
var assignees []string
@@ -458,17 +475,13 @@ func (g *GiteaDownloader) GetComments(commentable base.Commentable) ([]*base.Com
Page: i,
}})
if err != nil {
- return nil, false, fmt.Errorf("error while listing comments for issue #%d. Error: %v", commentable.GetForeignIndex(), err)
+ return nil, false, fmt.Errorf("error while listing comments for issue #%d. Error: %w", commentable.GetForeignIndex(), err)
}
for _, comment := range comments {
reactions, err := g.getCommentReactions(comment.ID)
if err != nil {
- log.Warn("Unable to load comment reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", commentable.GetForeignIndex(), comment.ID, g.repoOwner, g.repoName, err)
- if err2 := admin_model.CreateRepositoryNotice(
- fmt.Sprintf("Unable to load reactions during migrating issue #%d for comment %d to %s/%s. Error: %v", commentable.GetForeignIndex(), comment.ID, g.repoOwner, g.repoName, err)); err2 != nil {
- log.Error("create repository notice failed: ", err2)
- }
+ WarnAndNotice("Unable to load comment reactions during migrating issue #%d for comment %d in %s. Error: %v", commentable.GetForeignIndex(), comment.ID, g, err)
}
allComments = append(allComments, &base.Comment{
@@ -506,7 +519,7 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques
State: gitea_sdk.StateAll,
})
if err != nil {
- return nil, false, fmt.Errorf("error while listing pull requests (page: %d, pagesize: %d). Error: %v", page, perPage, err)
+ return nil, false, fmt.Errorf("error while listing pull requests (page: %d, pagesize: %d). Error: %w", page, perPage, err)
}
for _, pr := range prs {
var milestone string
@@ -543,11 +556,7 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques
reactions, err := g.getIssueReactions(pr.Index)
if err != nil {
- log.Warn("Unable to load reactions during migrating pull #%d to %s/%s. Error: %v", pr.Index, g.repoOwner, g.repoName, err)
- if err2 := admin_model.CreateRepositoryNotice(
- fmt.Sprintf("Unable to load reactions during migrating pull #%d to %s/%s. Error: %v", pr.Index, g.repoOwner, g.repoName, err)); err2 != nil {
- log.Error("create repository notice failed: ", err2)
- }
+ WarnAndNotice("Unable to load reactions during migrating pull #%d in %s. Error: %v", pr.Index, g, err)
}
var assignees []string
@@ -604,6 +613,8 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques
},
ForeignIndex: pr.Index,
})
+ // SECURITY: Ensure that the PR is safe
+ _ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g)
}
isEnd := len(prs) < perPage
@@ -639,6 +650,11 @@ func (g *GiteaDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review
}
for _, pr := range prl {
+ if pr.Reviewer == nil {
+ // Presumably this is a team review which we cannot migrate at present but we have to skip this review as otherwise the review will be mapped on to an incorrect user.
+ // TODO: handle team reviews
+ continue
+ }
rcl, _, err := g.client.ListPullReviewComments(g.repoOwner, g.repoName, reviewable.GetForeignIndex(), pr.ID)
if err != nil {
@@ -664,7 +680,7 @@ func (g *GiteaDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review
})
}
- allReviews = append(allReviews, &base.Review{
+ review := &base.Review{
ID: pr.ID,
IssueIndex: reviewable.GetLocalIndex(),
ReviewerID: pr.Reviewer.ID,
@@ -675,7 +691,9 @@ func (g *GiteaDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review
CreatedAt: pr.Submitted,
State: string(pr.State),
Comments: reviewComments,
- })
+ }
+
+ allReviews = append(allReviews, review)
}
if len(prl) < g.maxPerPage {
diff --git a/services/migrations/gitea_downloader_test.go b/services/migrations/gitea_downloader_test.go
index 601b0a7c79331..c37c70947e36f 100644
--- a/services/migrations/gitea_downloader_test.go
+++ b/services/migrations/gitea_downloader_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 8529f24895c64..23aa4ac2ca9d6 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -1,7 +1,6 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2018 Jonas Franz. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package migrations
@@ -10,6 +9,7 @@ import (
"fmt"
"io"
"os"
+ "path"
"path/filepath"
"strconv"
"strings"
@@ -17,7 +17,6 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/foreignreference"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
@@ -32,7 +31,7 @@ import (
"code.gitea.io/gitea/modules/uri"
"code.gitea.io/gitea/services/pull"
- gouuid "github.com/google/uuid"
+ "github.com/google/uuid"
)
var _ base.Uploader = &GiteaLocalUploader{}
@@ -44,14 +43,14 @@ type GiteaLocalUploader struct {
repoOwner string
repoName string
repo *repo_model.Repository
- labels map[string]*models.Label
+ labels map[string]*issues_model.Label
milestones map[string]int64
- issues map[int64]*models.Issue
+ issues map[int64]*issues_model.Issue
gitRepo *git.Repository
- prHeadCache map[string]struct{}
+ prHeadCache map[string]string
sameApp bool
userMap map[int64]int64 // external user id mapping to user id
- prCache map[int64]*models.PullRequest
+ prCache map[int64]*issues_model.PullRequest
gitServiceType structs.GitServiceType
}
@@ -62,12 +61,12 @@ func NewGiteaLocalUploader(ctx context.Context, doer *user_model.User, repoOwner
doer: doer,
repoOwner: repoOwner,
repoName: repoName,
- labels: make(map[string]*models.Label),
+ labels: make(map[string]*issues_model.Label),
milestones: make(map[string]int64),
- issues: make(map[int64]*models.Issue),
- prHeadCache: make(map[string]struct{}),
+ issues: make(map[int64]*issues_model.Issue),
+ prHeadCache: make(map[string]string),
userMap: make(map[int64]int64),
- prCache: make(map[int64]*models.PullRequest),
+ prCache: make(map[int64]*issues_model.PullRequest),
}
}
@@ -75,31 +74,31 @@ func NewGiteaLocalUploader(ctx context.Context, doer *user_model.User, repoOwner
func (g *GiteaLocalUploader) MaxBatchInsertSize(tp string) int {
switch tp {
case "issue":
- return db.MaxBatchInsertSize(new(models.Issue))
+ return db.MaxBatchInsertSize(new(issues_model.Issue))
case "comment":
- return db.MaxBatchInsertSize(new(models.Comment))
+ return db.MaxBatchInsertSize(new(issues_model.Comment))
case "milestone":
return db.MaxBatchInsertSize(new(issues_model.Milestone))
case "label":
- return db.MaxBatchInsertSize(new(models.Label))
+ return db.MaxBatchInsertSize(new(issues_model.Label))
case "release":
- return db.MaxBatchInsertSize(new(models.Release))
+ return db.MaxBatchInsertSize(new(repo_model.Release))
case "pullrequest":
- return db.MaxBatchInsertSize(new(models.PullRequest))
+ return db.MaxBatchInsertSize(new(issues_model.PullRequest))
}
return 10
}
// CreateRepo creates a repository
func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error {
- owner, err := user_model.GetUserByName(g.repoOwner)
+ owner, err := user_model.GetUserByName(g.ctx, g.repoOwner)
if err != nil {
return err
}
var r *repo_model.Repository
if opts.MigrateToRepoID <= 0 {
- r, err = repo_module.CreateRepository(g.doer, owner, models.CreateRepoOptions{
+ r, err = repo_module.CreateRepository(g.doer, owner, repo_module.CreateRepoOptions{
Name: g.repoName,
Description: repo.Description,
OriginalURL: repo.OriginalURL,
@@ -109,7 +108,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
Status: repo_model.RepositoryBeingMigrated,
})
} else {
- r, err = repo_model.GetRepositoryByID(opts.MigrateToRepoID)
+ r, err = repo_model.GetRepositoryByID(g.ctx, opts.MigrateToRepoID)
}
if err != nil {
return err
@@ -125,7 +124,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
Mirror: repo.IsMirror,
LFS: opts.LFS,
LFSEndpoint: opts.LFSEndpoint,
- CloneAddr: repo.CloneURL,
+ CloneAddr: repo.CloneURL, // SECURITY: we will assume that this has already been checked
Private: repo.IsPrivate,
Wiki: opts.Wiki,
Releases: opts.Releases, // if didn't get releases, then sync them from tags
@@ -150,13 +149,15 @@ func (g *GiteaLocalUploader) Close() {
// CreateTopics creates topics
func (g *GiteaLocalUploader) CreateTopics(topics ...string) error {
- // ignore topics to long for the db
+ // Ignore topics too long for the db
c := 0
- for i := range topics {
- if len(topics[i]) <= 50 {
- topics[c] = topics[i]
- c++
+ for _, topic := range topics {
+ if len(topic) > 50 {
+ continue
}
+
+ topics[c] = topic
+ c++
}
topics = topics[:c]
return repo_model.SaveTopics(g.repo.ID, topics...)
@@ -215,17 +216,23 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err
// CreateLabels creates labels
func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error {
- lbs := make([]*models.Label, 0, len(labels))
+ lbs := make([]*issues_model.Label, 0, len(labels))
for _, label := range labels {
- lbs = append(lbs, &models.Label{
+ // We must validate color here:
+ if !issues_model.LabelColorPattern.MatchString("#" + label.Color) {
+ log.Warn("Invalid label color: #%s for label: %s in migration to %s/%s", label.Color, label.Name, g.repoOwner, g.repoName)
+ label.Color = "ffffff"
+ }
+
+ lbs = append(lbs, &issues_model.Label{
RepoID: g.repo.ID,
Name: label.Name,
Description: label.Description,
- Color: fmt.Sprintf("#%s", label.Color),
+ Color: "#" + label.Color,
})
}
- err := models.NewLabels(lbs...)
+ err := issues_model.NewLabels(lbs...)
if err != nil {
return err
}
@@ -237,7 +244,7 @@ func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error {
// CreateReleases creates releases
func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
- rels := make([]*models.Release, 0, len(releases))
+ rels := make([]*repo_model.Release, 0, len(releases))
for _, release := range releases {
if release.Created.IsZero() {
if !release.Published.IsZero() {
@@ -247,13 +254,22 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
}
}
- rel := models.Release{
+ // SECURITY: The TagName must be a valid git ref
+ if release.TagName != "" && !git.IsValidRefPattern(release.TagName) {
+ release.TagName = ""
+ }
+
+ // SECURITY: The TargetCommitish must be a valid git ref
+ if release.TargetCommitish != "" && !git.IsValidRefPattern(release.TargetCommitish) {
+ release.TargetCommitish = ""
+ }
+
+ rel := repo_model.Release{
RepoID: g.repo.ID,
TagName: release.TagName,
LowerTagName: strings.ToLower(release.TagName),
Target: release.TargetCommitish,
Title: release.Name,
- Sha1: release.TargetCommitish,
Note: release.Body,
IsDraft: release.Draft,
IsPrerelease: release.Prerelease,
@@ -265,15 +281,18 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
return err
}
- // calc NumCommits if no draft
- if !release.Draft {
+ // calc NumCommits if possible
+ if rel.TagName != "" {
commit, err := g.gitRepo.GetTagCommit(rel.TagName)
- if err != nil {
- return fmt.Errorf("GetTagCommit[%v]: %v", rel.TagName, err)
- }
- rel.NumCommits, err = commit.CommitsCount()
- if err != nil {
- return fmt.Errorf("CommitsCount: %v", err)
+ if !git.IsErrNotExist(err) {
+ if err != nil {
+ return fmt.Errorf("GetTagCommit[%v]: %w", rel.TagName, err)
+ }
+ rel.Sha1 = commit.ID.String()
+ rel.NumCommits, err = commit.CommitsCount()
+ if err != nil {
+ return fmt.Errorf("CommitsCount: %w", err)
+ }
}
}
@@ -286,14 +305,15 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
}
}
attach := repo_model.Attachment{
- UUID: gouuid.New().String(),
+ UUID: uuid.New().String(),
Name: asset.Name,
DownloadCount: int64(*asset.DownloadCount),
Size: int64(*asset.Size),
CreatedUnix: timeutil.TimeStamp(asset.Created.Unix()),
}
- // download attachment
+ // SECURITY: We cannot check the DownloadURL and DownloadFunc are safe here
+ // ... we must assume that they are safe and simply download the attachment
err := func() error {
// asset.DownloadURL maybe a local file
var rc io.ReadCloser
@@ -336,9 +356,9 @@ func (g *GiteaLocalUploader) SyncTags() error {
// CreateIssues creates issues
func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
- iss := make([]*models.Issue, 0, len(issues))
+ iss := make([]*issues_model.Issue, 0, len(issues))
for _, issue := range issues {
- var labels []*models.Label
+ var labels []*issues_model.Label
for _, label := range issue.Labels {
lb, ok := g.labels[label.Name]
if ok {
@@ -363,7 +383,13 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
}
}
- is := models.Issue{
+ // SECURITY: issue.Ref needs to be a valid reference
+ if !git.IsValidRefPattern(issue.Ref) {
+ log.Warn("Invalid issue.Ref[%s] in issue #%d in %s/%s", issue.Ref, issue.Number, g.repoOwner, g.repoName)
+ issue.Ref = ""
+ }
+
+ is := issues_model.Issue{
RepoID: g.repo.ID,
Repo: g.repo,
Index: issue.Number,
@@ -376,12 +402,6 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
Labels: labels,
CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()),
UpdatedUnix: timeutil.TimeStamp(issue.Updated.Unix()),
- ForeignReference: &foreignreference.ForeignReference{
- LocalIndex: issue.GetLocalIndex(),
- ForeignIndex: strconv.FormatInt(issue.GetForeignIndex(), 10),
- RepoID: g.repo.ID,
- Type: foreignreference.TypeIssue,
- },
}
if err := g.remapUser(issue, &is); err != nil {
@@ -420,9 +440,9 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
// CreateComments creates comments of issues
func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
- cms := make([]*models.Comment, 0, len(comments))
+ cms := make([]*issues_model.Comment, 0, len(comments))
for _, comment := range comments {
- var issue *models.Issue
+ var issue *issues_model.Issue
issue, ok := g.issues[comment.IssueIndex]
if !ok {
return fmt.Errorf("comment references non existent IssueIndex %d", comment.IssueIndex)
@@ -435,9 +455,9 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
comment.Updated = comment.Created
}
- cm := models.Comment{
+ cm := issues_model.Comment{
IssueID: issue.ID,
- Type: models.CommentTypeComment,
+ Type: issues_model.CommentTypeComment,
Content: comment.Content,
CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()),
UpdatedUnix: timeutil.TimeStamp(comment.Updated.Unix()),
@@ -470,7 +490,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
// CreatePullRequests creates pull requests
func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error {
- gprs := make([]*models.PullRequest, 0, len(prs))
+ gprs := make([]*issues_model.PullRequest, 0, len(prs))
for _, pr := range prs {
gpr, err := g.newPullRequest(pr)
if err != nil {
@@ -494,102 +514,151 @@ func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error
}
func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head string, err error) {
- // download patch file
+ // SECURITY: this pr must have been must have been ensured safe
+ if !pr.EnsuredSafe {
+ log.Error("PR #%d in %s/%s has not been checked for safety.", pr.Number, g.repoOwner, g.repoName)
+ return "", fmt.Errorf("the PR[%d] was not checked for safety", pr.Number)
+ }
+
+ // Anonymous function to download the patch file (allows us to use defer)
err = func() error {
+ // if the patchURL is empty there is nothing to download
if pr.PatchURL == "" {
return nil
}
- // pr.PatchURL maybe a local file
- ret, err := uri.Open(pr.PatchURL)
+
+ // SECURITY: We will assume that the pr.PatchURL has been checked
+ // pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe
+ ret, err := uri.Open(pr.PatchURL) // TODO: This probably needs to use the downloader as there may be rate limiting issues here
if err != nil {
return err
}
defer ret.Close()
+
pullDir := filepath.Join(g.repo.RepoPath(), "pulls")
if err = os.MkdirAll(pullDir, os.ModePerm); err != nil {
return err
}
+
f, err := os.Create(filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number)))
if err != nil {
return err
}
defer f.Close()
+
+ // TODO: Should there be limits on the size of this file?
_, err = io.Copy(f, ret)
+
return err
}()
if err != nil {
return "", err
}
- // set head information
- pullHead := filepath.Join(g.repo.RepoPath(), "refs", "pull", fmt.Sprintf("%d", pr.Number))
- if err := os.MkdirAll(pullHead, os.ModePerm); err != nil {
- return "", err
- }
- p, err := os.Create(filepath.Join(pullHead, "head"))
- if err != nil {
- return "", err
- }
- _, err = p.WriteString(pr.Head.SHA)
- p.Close()
- if err != nil {
- return "", err
- }
-
head = "unknown repository"
if pr.IsForkPullRequest() && pr.State != "closed" {
- if pr.Head.OwnerName != "" {
- remote := pr.Head.OwnerName
- _, ok := g.prHeadCache[remote]
- if !ok {
- // git remote add
- err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
- if err != nil {
- log.Error("AddRemote failed: %s", err)
- } else {
- g.prHeadCache[remote] = struct{}{}
- ok = true
- }
+ // OK we want to fetch the current head as a branch from its CloneURL
+
+ // 1. Is there a head clone URL available?
+ // 2. Is there a head ref available?
+ if pr.Head.CloneURL == "" || pr.Head.Ref == "" {
+ return head, nil
+ }
+
+ // 3. We need to create a remote for this clone url
+ // ... maybe we already have a name for this remote
+ remote, ok := g.prHeadCache[pr.Head.CloneURL+":"]
+ if !ok {
+ // ... let's try ownername as a reasonable name
+ remote = pr.Head.OwnerName
+ if !git.IsValidRefPattern(remote) {
+ // ... let's try something less nice
+ remote = "head-pr-" + strconv.FormatInt(pr.Number, 10)
+ }
+ // ... now add the remote
+ err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
+ if err != nil {
+ log.Error("PR #%d in %s/%s AddRemote[%s] failed: %v", pr.Number, g.repoOwner, g.repoName, remote, err)
+ } else {
+ g.prHeadCache[pr.Head.CloneURL+":"] = remote
+ ok = true
}
+ }
+ if !ok {
+ return head, nil
+ }
- if ok {
- _, _, err = git.NewCommand(g.ctx, "fetch", remote, pr.Head.Ref).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
- if err != nil {
- log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err)
- } else {
- headBranch := filepath.Join(g.repo.RepoPath(), "refs", "heads", pr.Head.OwnerName, pr.Head.Ref)
- if err := os.MkdirAll(filepath.Dir(headBranch), os.ModePerm); err != nil {
- return "", err
- }
- b, err := os.Create(headBranch)
- if err != nil {
- return "", err
- }
- _, err = b.WriteString(pr.Head.SHA)
- b.Close()
- if err != nil {
- return "", err
+ // 4. Check if we already have this ref?
+ localRef, ok := g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref]
+ if !ok {
+ // ... We would normally name this migrated branch as / but we need to ensure that is safe
+ localRef = git.SanitizeRefPattern(pr.Head.OwnerName + "/" + pr.Head.Ref)
+
+ // ... Now we must assert that this does not exist
+ if g.gitRepo.IsBranchExist(localRef) {
+ localRef = "head-pr-" + strconv.FormatInt(pr.Number, 10) + "/" + localRef
+ i := 0
+ for g.gitRepo.IsBranchExist(localRef) {
+ if i > 5 {
+ // ... We tried, we really tried but this is just a seriously unfriendly repo
+ return head, nil
}
- head = pr.Head.OwnerName + "/" + pr.Head.Ref
+ // OK just try some uuids!
+ localRef = git.SanitizeRefPattern("head-pr-" + strconv.FormatInt(pr.Number, 10) + uuid.New().String())
+ i++
}
}
+
+ fetchArg := pr.Head.Ref + ":" + git.BranchPrefix + localRef
+ if strings.HasPrefix(fetchArg, "-") {
+ fetchArg = git.BranchPrefix + fetchArg
+ }
+
+ _, _, 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
+ }
+ g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref] = localRef
+ head = localRef
}
- } else {
+
+ // 5. Now if pr.Head.SHA == "" we should recover this to the head of this branch
+ if pr.Head.SHA == "" {
+ headSha, err := g.gitRepo.GetBranchCommitID(localRef)
+ if err != nil {
+ log.Error("unable to get head SHA of local head for PR #%d from %s in %s/%s. Error: %v", pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err)
+ return head, nil
+ }
+ pr.Head.SHA = headSha
+ }
+
+ _, _, 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
+ }
+
+ return head, nil
+ }
+
+ if pr.Head.Ref != "" {
head = pr.Head.Ref
- // Ensure the closed PR SHA still points to an existing ref
- _, _, err = git.NewCommand(g.ctx, "rev-list", "--quiet", "-1", pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
+ }
+
+ // Ensure the closed PR SHA still points to an existing ref
+ if pr.Head.SHA == "" {
+ // 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").AddDynamicArguments(pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
if err != nil {
- if pr.Head.SHA != "" {
- // Git update-ref remove bad references with a relative path
- log.Warn("Deprecated local head, removing : %v", pr.Head.SHA)
- err = g.gitRepo.RemoveReference(pr.GetGitRefName())
- } else {
- // The SHA is empty, remove the head file
- log.Warn("Empty reference, removing : %v", pullHead)
- err = os.Remove(filepath.Join(pullHead, "head"))
- }
+ // 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").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
if err != nil {
- log.Error("Cannot remove local head ref, %v", err)
+ 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)
}
}
}
@@ -597,8 +666,8 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head
return head, nil
}
-func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullRequest, error) {
- var labels []*models.Label
+func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model.PullRequest, error) {
+ var labels []*issues_model.Label
for _, label := range pr.Labels {
lb, ok := g.labels[label.Name]
if ok {
@@ -613,6 +682,20 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
return nil, fmt.Errorf("updateGitForPullRequest: %w", err)
}
+ // Now we may need to fix the mergebase
+ if pr.Base.SHA == "" {
+ if pr.Base.Ref != "" && pr.Head.SHA != "" {
+ // A PR against a tag base does not make sense - therefore pr.Base.Ref must be a branch
+ // TODO: should we be checking for the refs/heads/ prefix on the pr.Base.Ref? (i.e. are these actually branches or refs)
+ pr.Base.SHA, _, err = g.gitRepo.GetMergeBase("", git.BranchPrefix+pr.Base.Ref, pr.Head.SHA)
+ if err != nil {
+ log.Error("Cannot determine the merge base for PR #%d in %s/%s. Error: %v", pr.Number, g.repoOwner, g.repoName, err)
+ }
+ } else {
+ log.Error("Cannot determine the merge base for PR #%d in %s/%s. Not enough information", pr.Number, g.repoOwner, g.repoName)
+ }
+ }
+
if pr.Created.IsZero() {
if pr.Closed != nil {
pr.Created = *pr.Closed
@@ -626,7 +709,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
pr.Updated = pr.Created
}
- issue := models.Issue{
+ issue := issues_model.Issue{
RepoID: g.repo.ID,
Repo: g.repo,
Title: pr.Title,
@@ -657,7 +740,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
issue.Reactions = append(issue.Reactions, &res)
}
- pullRequest := models.PullRequest{
+ pullRequest := issues_model.PullRequest{
HeadRepoID: g.repo.ID,
HeadBranch: head,
BaseRepoID: g.repo.ID,
@@ -683,26 +766,28 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR
return &pullRequest, nil
}
-func convertReviewState(state string) models.ReviewType {
+func convertReviewState(state string) issues_model.ReviewType {
switch state {
case base.ReviewStatePending:
- return models.ReviewTypePending
+ return issues_model.ReviewTypePending
case base.ReviewStateApproved:
- return models.ReviewTypeApprove
+ return issues_model.ReviewTypeApprove
case base.ReviewStateChangesRequested:
- return models.ReviewTypeReject
+ return issues_model.ReviewTypeReject
case base.ReviewStateCommented:
- return models.ReviewTypeComment
+ return issues_model.ReviewTypeComment
+ case base.ReviewStateRequestReview:
+ return issues_model.ReviewTypeRequest
default:
- return models.ReviewTypePending
+ return issues_model.ReviewTypePending
}
}
// CreateReviews create pull request reviews of currently migrated issues
func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
- cms := make([]*models.Review, 0, len(reviews))
+ cms := make([]*issues_model.Review, 0, len(reviews))
for _, review := range reviews {
- var issue *models.Issue
+ var issue *issues_model.Issue
issue, ok := g.issues[review.IssueIndex]
if !ok {
return fmt.Errorf("review references non existent IssueIndex %d", review.IssueIndex)
@@ -711,7 +796,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
review.CreatedAt = time.Unix(int64(issue.CreatedUnix), 0)
}
- cm := models.Review{
+ cm := issues_model.Review{
Type: convertReviewState(review.State),
IssueID: issue.ID,
Content: review.Content,
@@ -724,16 +809,29 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
return err
}
+ cms = append(cms, &cm)
+
// get pr
pr, ok := g.prCache[issue.ID]
if !ok {
var err error
- pr, err = models.GetPullRequestByIssueIDWithNoAttributes(issue.ID)
+ pr, err = issues_model.GetPullRequestByIssueIDWithNoAttributes(issue.ID)
if err != nil {
return err
}
g.prCache[issue.ID] = pr
}
+ if pr.MergeBase == "" {
+ // No mergebase -> no basis for any patches
+ log.Warn("PR #%d in %s/%s: does not have a merge base, all review comments will be ignored", pr.Index, g.repoOwner, g.repoName)
+ continue
+ }
+
+ headCommitID, err := g.gitRepo.GetRefCommitID(pr.GetGitRefName())
+ if err != nil {
+ log.Warn("PR #%d GetRefCommitID[%s] in %s/%s: %v, all review comments will be ignored", pr.Index, pr.GetGitRefName(), g.repoOwner, g.repoName, err)
+ continue
+ }
for _, comment := range review.Comments {
line := comment.Line
@@ -742,11 +840,9 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
} else {
_, _, line, _ = git.ParseDiffHunkString(comment.DiffHunk)
}
- headCommitID, err := g.gitRepo.GetRefCommitID(pr.GetGitRefName())
- if err != nil {
- log.Warn("GetRefCommitID[%s]: %v, the review comment will be ignored", pr.GetGitRefName(), err)
- continue
- }
+
+ // SECURITY: The TreePath must be cleaned!
+ comment.TreePath = path.Clean("/" + comment.TreePath)[1:]
var patch string
reader, writer := io.Pipe()
@@ -754,15 +850,15 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
_ = reader.Close()
_ = writer.Close()
}()
- go func() {
+ go func(comment *base.ReviewComment) {
if err := git.GetRepoRawDiffForFile(g.gitRepo, pr.MergeBase, headCommitID, git.RawDiffNormal, comment.TreePath, writer); err != nil {
// We should ignore the error since the commit maybe removed when force push to the pull request
log.Warn("GetRepoRawDiffForFile failed when migrating [%s, %s, %s, %s]: %v", g.gitRepo.Path, pr.MergeBase, headCommitID, comment.TreePath, err)
}
_ = writer.Close()
- }()
+ }(comment)
- patch, _ = git.CutDiffAroundLine(reader, int64((&models.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
+ patch, _ = git.CutDiffAroundLine(reader, int64((&issues_model.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
if comment.CreatedAt.IsZero() {
comment.CreatedAt = review.CreatedAt
@@ -771,8 +867,13 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
comment.UpdatedAt = comment.CreatedAt
}
- c := models.Comment{
- Type: models.CommentTypeCode,
+ if !git.IsValidSHAPattern(comment.CommitID) {
+ log.Warn("Invalid comment CommitID[%s] on comment[%d] in PR #%d of %s/%s replaced with %s", comment.CommitID, pr.Index, g.repoOwner, g.repoName, headCommitID)
+ comment.CommitID = headCommitID
+ }
+
+ c := issues_model.Comment{
+ Type: issues_model.CommentTypeCode,
IssueID: issue.ID,
Content: comment.Content,
Line: int64(line + comment.Position - 1),
@@ -789,11 +890,9 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
cm.Comments = append(cm.Comments, &c)
}
-
- cms = append(cms, &cm)
}
- return models.InsertReviews(cms)
+ return issues_model.InsertReviews(cms)
}
// Rollback when migrating failed, this will rollback all the changes.
@@ -814,7 +913,7 @@ func (g *GiteaLocalUploader) Finish() error {
}
// update issue_index
- if err := models.RecalculateIssueIndexForRepo(g.repo.ID); err != nil {
+ if err := issues_model.RecalculateIssueIndexForRepo(g.repo.ID); err != nil {
return err
}
@@ -823,7 +922,7 @@ func (g *GiteaLocalUploader) Finish() error {
}
g.repo.Status = repo_model.RepositoryReady
- return repo_model.UpdateRepositoryCols(g.repo, "status")
+ return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "status")
}
func (g *GiteaLocalUploader) remapUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) error {
diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go
index 51c7ad9717e72..6a942b9b57637 100644
--- a/services/migrations/gitea_uploader_test.go
+++ b/services/migrations/gitea_uploader_test.go
@@ -1,7 +1,6 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2018 Jonas Franz. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package migrations
@@ -15,7 +14,6 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
@@ -37,15 +35,16 @@ func TestGiteaUploadRepo(t *testing.T) {
unittest.PrepareTestEnv(t)
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
var (
- downloader = NewGithubDownloaderV3(context.Background(), "https://github.com", "", "", "", "go-xorm", "builder")
+ ctx = context.Background()
+ downloader = NewGithubDownloaderV3(ctx, "https://github.com", "", "", "", "go-xorm", "builder")
repoName = "builder-" + time.Now().Format("2006-01-02-15-04-05")
uploader = NewGiteaLocalUploader(graceful.GetManager().HammerContext(), user, user.Name, repoName)
)
- err := migrateRepository(downloader, uploader, base.MigrateOptions{
+ err := migrateRepository(user, downloader, uploader, base.MigrateOptions{
CloneAddr: "https://github.com/go-xorm/builder",
RepoName: repoName,
AuthUsername: "",
@@ -62,7 +61,7 @@ func TestGiteaUploadRepo(t *testing.T) {
}, nil)
assert.NoError(t, err)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, Name: repoName}).(*repo_model.Repository)
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, Name: repoName})
assert.True(t, repo.HasWiki())
assert.EqualValues(t, repo_model.RepositoryReady, repo.Status)
@@ -80,11 +79,11 @@ func TestGiteaUploadRepo(t *testing.T) {
assert.NoError(t, err)
assert.Empty(t, milestones)
- labels, err := models.GetLabelsByRepoID(repo.ID, "", db.ListOptions{})
+ labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
assert.NoError(t, err)
assert.Len(t, labels, 12)
- releases, err := models.GetReleasesByRepoID(repo.ID, models.FindReleasesOptions{
+ releases, err := repo_model.GetReleasesByRepoID(db.DefaultContext, repo.ID, repo_model.FindReleasesOptions{
ListOptions: db.ListOptions{
PageSize: 10,
Page: 0,
@@ -94,7 +93,7 @@ func TestGiteaUploadRepo(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, releases, 8)
- releases, err = models.GetReleasesByRepoID(repo.ID, models.FindReleasesOptions{
+ releases, err = repo_model.GetReleasesByRepoID(db.DefaultContext, repo.ID, repo_model.FindReleasesOptions{
ListOptions: db.ListOptions{
PageSize: 10,
Page: 0,
@@ -104,30 +103,30 @@ func TestGiteaUploadRepo(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, releases, 1)
- issues, err := models.Issues(&models.IssuesOptions{
- RepoIDs: []int64{repo.ID},
+ issues, err := issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{
+ RepoID: repo.ID,
IsPull: util.OptionalBoolFalse,
SortType: "oldest",
})
assert.NoError(t, err)
assert.Len(t, issues, 15)
- assert.NoError(t, issues[0].LoadDiscussComments())
+ assert.NoError(t, issues[0].LoadDiscussComments(db.DefaultContext))
assert.Empty(t, issues[0].Comments)
- pulls, _, err := models.PullRequests(repo.ID, &models.PullRequestsOptions{
+ pulls, _, err := issues_model.PullRequests(repo.ID, &issues_model.PullRequestsOptions{
SortType: "oldest",
})
assert.NoError(t, err)
assert.Len(t, pulls, 30)
- assert.NoError(t, pulls[0].LoadIssue())
- assert.NoError(t, pulls[0].Issue.LoadDiscussComments())
+ assert.NoError(t, pulls[0].LoadIssue(db.DefaultContext))
+ assert.NoError(t, pulls[0].Issue.LoadDiscussComments(db.DefaultContext))
assert.Len(t, pulls[0].Issue.Comments, 2)
}
func TestGiteaUploadRemapLocalUser(t *testing.T) {
unittest.PrepareTestEnv(t)
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repoName := "migrated"
uploader := NewGiteaLocalUploader(context.Background(), doer, doer.Name, repoName)
@@ -145,7 +144,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) {
// The externalID does not match any existing user, everything
// belongs to the doer
//
- target := models.Release{}
+ target := repo_model.Release{}
uploader.userMap = make(map[int64]int64)
err := uploader.remapUser(&source, &target)
assert.NoError(t, err)
@@ -156,7 +155,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) {
// everything belongs to the doer
//
source.PublisherID = user.ID
- target = models.Release{}
+ target = repo_model.Release{}
uploader.userMap = make(map[int64]int64)
err = uploader.remapUser(&source, &target)
assert.NoError(t, err)
@@ -167,7 +166,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) {
// belongs to the existing user
//
source.PublisherName = user.Name
- target = models.Release{}
+ target = repo_model.Release{}
uploader.userMap = make(map[int64]int64)
err = uploader.remapUser(&source, &target)
assert.NoError(t, err)
@@ -176,7 +175,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) {
func TestGiteaUploadRemapExternalUser(t *testing.T) {
unittest.PrepareTestEnv(t)
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
repoName := "migrated"
uploader := NewGiteaLocalUploader(context.Background(), doer, doer.Name, repoName)
@@ -196,7 +195,7 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
// by the doer
//
uploader.userMap = make(map[int64]int64)
- target := models.Release{}
+ target := repo_model.Release{}
err := uploader.remapUser(&source, &target)
assert.NoError(t, err)
assert.EqualValues(t, doer.ID, target.GetUserID())
@@ -204,7 +203,7 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
//
// Link the external ID to an existing user
//
- linkedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+ linkedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
externalLoginUser := &user_model.ExternalLoginUser{
ExternalID: strconv.FormatInt(externalID, 10),
UserID: linkedUser.ID,
@@ -219,7 +218,7 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
// the migrated data
//
uploader.userMap = make(map[int64]int64)
- target = models.Release{}
+ target = repo_model.Release{}
err = uploader.remapUser(&source, &target)
assert.NoError(t, err)
assert.EqualValues(t, linkedUser.ID, target.GetUserID())
@@ -231,10 +230,10 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
//
// fromRepo master
//
- fromRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+ 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 +257,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))
@@ -272,17 +271,17 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
headSHA, err := fromGitRepo.GetBranchCommitID(headRef)
assert.NoError(t, err)
- fromRepoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: fromRepo.OwnerID}).(*user_model.User)
+ fromRepoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: fromRepo.OwnerID})
//
// forkRepo branch2
//
forkHeadRef := "branch2"
- forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8}).(*repo_model.Repository)
- assert.NoError(t, git.CloneWithArgs(git.DefaultContext, fromRepo.RepoPath(), forkRepo.RepoPath(), []string{}, git.CloneRepoOptions{
+ forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8})
+ 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))
@@ -390,7 +389,7 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
},
},
assertContent: func(t *testing.T, content string) {
- assert.Contains(t, content, "AddRemote failed")
+ assert.Contains(t, content, "AddRemote")
},
},
{
@@ -439,7 +438,7 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
},
},
assertContent: func(t *testing.T, content string) {
- assert.Contains(t, content, "Empty reference, removing")
+ assert.Contains(t, content, "Empty reference")
assert.NotContains(t, content, "Cannot remove local head")
},
},
@@ -467,7 +466,6 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
},
assertContent: func(t *testing.T, content string) {
assert.Contains(t, content, "Deprecated local head")
- assert.Contains(t, content, "Cannot remove local head")
},
},
{
@@ -504,6 +502,8 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
logger.SetLogger("buffer", "buffer", "{}")
defer logger.DelLogger("buffer")
+ testCase.pr.EnsuredSafe = true
+
head, err := uploader.updateGitForPullRequest(&testCase.pr)
assert.NoError(t, err)
assert.EqualValues(t, testCase.head, head)
diff --git a/services/migrations/github.go b/services/migrations/github.go
index faf0cf0794179..d34ad13b95a71 100644
--- a/services/migrations/github.go
+++ b/services/migrations/github.go
@@ -1,7 +1,6 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2018 Jonas Franz. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package migrations
@@ -15,13 +14,14 @@ import (
"strings"
"time"
+ "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/proxy"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
- "github.com/google/go-github/v39/github"
+ "github.com/google/go-github/v45/github"
"golang.org/x/oauth2"
)
@@ -51,7 +51,7 @@ func (f *GithubDownloaderV3Factory) New(ctx context.Context, opts base.MigrateOp
oldOwner := fields[1]
oldName := strings.TrimSuffix(fields[2], ".git")
- log.Trace("Create github downloader: %s/%s", oldOwner, oldName)
+ log.Trace("Create github downloader BaseURL: %s %s/%s", baseURL, oldOwner, oldName)
return NewGithubDownloaderV3(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil
}
@@ -67,6 +67,7 @@ type GithubDownloaderV3 struct {
base.NullDownloader
ctx context.Context
clients []*github.Client
+ baseURL string
repoOwner string
repoName string
userName string
@@ -75,12 +76,14 @@ type GithubDownloaderV3 struct {
curClientIdx int
maxPerPage int
SkipReactions bool
+ SkipReviews bool
}
// NewGithubDownloaderV3 creates a github Downloader via github v3 API
func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 {
downloader := GithubDownloaderV3{
userName: userName,
+ baseURL: baseURL,
password: password,
ctx: ctx,
repoOwner: repoOwner,
@@ -118,6 +121,20 @@ func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, tok
return &downloader
}
+// String implements Stringer
+func (g *GithubDownloaderV3) String() string {
+ return fmt.Sprintf("migration from github server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
+}
+
+// ColorFormat provides a basic color format for a GithubDownloader
+func (g *GithubDownloaderV3) ColorFormat(s fmt.State) {
+ if g == nil {
+ log.ColorFprintf(s, "")
+ return
+ }
+ log.ColorFprintf(s, "migration from github server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
+}
+
func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) {
githubClient := github.NewClient(client)
if baseURL != "https://github.com" {
@@ -291,10 +308,14 @@ func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) {
}
func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) *base.Release {
+ // GitHub allows commitish to be a reference.
+ // In this case, we need to remove the prefix, i.e. convert "refs/heads/main" to "main".
+ targetCommitish := strings.TrimPrefix(rel.GetTargetCommitish(), git.BranchPrefix)
+
r := &base.Release{
Name: rel.GetName(),
TagName: rel.GetTagName(),
- TargetCommitish: rel.GetTargetCommitish(),
+ TargetCommitish: targetCommitish,
Draft: rel.GetDraft(),
Prerelease: rel.GetPrerelease(),
Created: rel.GetCreatedAt().Time,
@@ -322,33 +343,44 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease)
Updated: asset.UpdatedAt.Time,
DownloadFunc: func() (io.ReadCloser, error) {
g.waitAndPickClient()
- asset, redirectURL, err := g.getClient().Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, assetID, nil)
+ readCloser, redirectURL, err := g.getClient().Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, assetID, nil)
if err != nil {
return nil, err
}
if err := g.RefreshRate(); err != nil {
log.Error("g.getClient().RateLimits: %s", err)
}
- if asset == nil {
- if redirectURL != "" {
- g.waitAndPickClient()
- req, err := http.NewRequestWithContext(g.ctx, "GET", redirectURL, nil)
- if err != nil {
- return nil, err
- }
- resp, err := httpClient.Do(req)
- err1 := g.RefreshRate()
- if err1 != nil {
- log.Error("g.getClient().RateLimits: %s", err1)
- }
- if err != nil {
- return nil, err
- }
- return resp.Body, nil
- }
- return nil, fmt.Errorf("No release asset found for %d", assetID)
+
+ if readCloser != nil {
+ return readCloser, nil
+ }
+
+ if redirectURL == "" {
+ return nil, fmt.Errorf("no release asset found for %d", assetID)
+ }
+
+ // Prevent open redirect
+ if !hasBaseURL(redirectURL, g.baseURL) &&
+ !hasBaseURL(redirectURL, "https://objects.githubusercontent.com/") {
+ WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.GetID(), g, redirectURL)
+
+ return io.NopCloser(strings.NewReader(redirectURL)), nil
+ }
+
+ g.waitAndPickClient()
+ req, err := http.NewRequestWithContext(g.ctx, "GET", redirectURL, nil)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := httpClient.Do(req)
+ err1 := g.RefreshRate()
+ if err1 != nil {
+ log.Error("g.RefreshRate(): %s", err1)
}
- return asset, nil
+ if err != nil {
+ return nil, err
+ }
+ return resp.Body, nil
},
})
}
@@ -400,7 +432,7 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool,
g.waitAndPickClient()
issues, resp, err := g.getClient().Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt)
if err != nil {
- return nil, false, fmt.Errorf("error while listing repos: %v", err)
+ return nil, false, fmt.Errorf("error while listing repos: %w", err)
}
log.Trace("Request get issues %d/%d, but in fact get %d", perPage, page, len(issues))
g.setRate(&resp.Rate)
@@ -496,7 +528,7 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.
g.waitAndPickClient()
comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(commentable.GetForeignIndex()), opt)
if err != nil {
- return nil, fmt.Errorf("error while listing repos: %v", err)
+ return nil, fmt.Errorf("error while listing repos: %w", err)
}
g.setRate(&resp.Rate)
for _, comment := range comments {
@@ -568,7 +600,7 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment,
g.waitAndPickClient()
comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, 0, opt)
if err != nil {
- return nil, false, fmt.Errorf("error while listing repos: %v", err)
+ return nil, false, fmt.Errorf("error while listing repos: %w", err)
}
isEnd := resp.NextPage == 0
@@ -636,7 +668,7 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
g.waitAndPickClient()
prs, resp, err := g.getClient().PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt)
if err != nil {
- return nil, false, fmt.Errorf("error while listing repos: %v", err)
+ return nil, false, fmt.Errorf("error while listing repos: %w", err)
}
log.Trace("Request get pull requests %d/%d, but in fact get %d", perPage, page, len(prs))
g.setRate(&resp.Rate)
@@ -697,7 +729,7 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
SHA: pr.GetHead().GetSHA(),
OwnerName: pr.GetHead().GetUser().GetLogin(),
RepoName: pr.GetHead().GetRepo().GetName(),
- CloneURL: pr.GetHead().GetRepo().GetCloneURL(),
+ CloneURL: pr.GetHead().GetRepo().GetCloneURL(), // see below for SECURITY related issues here
},
Base: base.PullRequestBranch{
Ref: pr.GetBase().GetRef(),
@@ -705,10 +737,13 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
RepoName: pr.GetBase().GetRepo().GetName(),
OwnerName: pr.GetBase().GetUser().GetLogin(),
},
- PatchURL: pr.GetPatchURL(),
+ PatchURL: pr.GetPatchURL(), // see below for SECURITY related issues here
Reactions: reactions,
ForeignIndex: int64(*pr.Number),
})
+
+ // SECURITY: Ensure that the PR is safe
+ _ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g)
}
return allPRs, len(prs) < perPage, nil
@@ -775,14 +810,18 @@ func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullReques
// GetReviews returns pull requests review
func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) {
allReviews := make([]*base.Review, 0, g.maxPerPage)
+ if g.SkipReviews {
+ return allReviews, nil
+ }
opt := &github.ListOptions{
PerPage: g.maxPerPage,
}
+ // Get approve/request change reviews
for {
g.waitAndPickClient()
reviews, resp, err := g.getClient().PullRequests.ListReviews(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt)
if err != nil {
- return nil, fmt.Errorf("error while listing repos: %v", err)
+ return nil, fmt.Errorf("error while listing repos: %w", err)
}
g.setRate(&resp.Rate)
for _, review := range reviews {
@@ -796,7 +835,7 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev
g.waitAndPickClient()
reviewComments, resp, err := g.getClient().PullRequests.ListReviewComments(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), review.GetID(), opt2)
if err != nil {
- return nil, fmt.Errorf("error while listing repos: %v", err)
+ return nil, fmt.Errorf("error while listing repos: %w", err)
}
g.setRate(&resp.Rate)
@@ -817,5 +856,28 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev
}
opt.Page = resp.NextPage
}
+ // Get requested reviews
+ for {
+ g.waitAndPickClient()
+ reviewers, resp, err := g.getClient().PullRequests.ListReviewers(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt)
+ if err != nil {
+ return nil, fmt.Errorf("error while listing repos: %w", err)
+ }
+ g.setRate(&resp.Rate)
+ for _, user := range reviewers.Users {
+ r := &base.Review{
+ ReviewerID: user.GetID(),
+ ReviewerName: user.GetLogin(),
+ State: base.ReviewStateRequestReview,
+ IssueIndex: reviewable.GetLocalIndex(),
+ }
+ allReviews = append(allReviews, r)
+ }
+ // TODO: Handle Team requests
+ if resp.NextPage == 0 {
+ break
+ }
+ opt.Page = resp.NextPage
+ }
return allReviews, nil
}
diff --git a/services/migrations/github_test.go b/services/migrations/github_test.go
index 90c1fcaef5b6b..2b89e6dc0fd16 100644
--- a/services/migrations/github_test.go
+++ b/services/migrations/github_test.go
@@ -1,7 +1,6 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2018 Jonas Franz. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package migrations
@@ -18,7 +17,11 @@ import (
func TestGitHubDownloadRepo(t *testing.T) {
GithubLimitRateRemaining = 3 // Wait at 3 remaining since we could have 3 CI in //
- downloader := NewGithubDownloaderV3(context.Background(), "https://github.com", "", "", os.Getenv("GITHUB_READ_TOKEN"), "go-gitea", "test_repo")
+ token := os.Getenv("GITHUB_READ_TOKEN")
+ if token == "" {
+ t.Skip("Skipping GitHub migration test because GITHUB_READ_TOKEN is empty")
+ }
+ downloader := NewGithubDownloaderV3(context.Background(), "https://github.com", "", "", token, "go-gitea", "test_repo")
err := downloader.RefreshRate()
assert.NoError(t, err)
diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go
index 549e3cb659c9a..8034869a4ae4b 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
@@ -63,6 +62,7 @@ type GitlabDownloader struct {
base.NullDownloader
ctx context.Context
client *gitlab.Client
+ baseURL string
repoID int
repoName string
issueCount int64
@@ -70,8 +70,9 @@ type GitlabDownloader struct {
}
// NewGitlabDownloader creates a gitlab Downloader via gitlab API
-// Use either a username/password, personal token entered into the username field, or anonymous/public access
-// Note: Public access only allows very basic access
+//
+// Use either a username/password, personal token entered into the username field, or anonymous/public access
+// Note: Public access only allows very basic access
func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, username, password, token string) (*GitlabDownloader, error) {
gitlabClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient()))
// Only use basic auth if token is blank and password is NOT
@@ -124,12 +125,27 @@ func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, username, passw
return &GitlabDownloader{
ctx: ctx,
client: gitlabClient,
+ baseURL: baseURL,
repoID: gr.ID,
repoName: gr.Name,
maxPerPage: 100,
}, nil
}
+// String implements Stringer
+func (g *GitlabDownloader) String() string {
+ return fmt.Sprintf("migration from gitlab server %s [%d]/%s", g.baseURL, g.repoID, g.repoName)
+}
+
+// ColorFormat provides a basic color format for a GitlabDownloader
+func (g *GitlabDownloader) ColorFormat(s fmt.State) {
+ if g == nil {
+ log.ColorFprintf(s, "")
+ return
+ }
+ log.ColorFprintf(s, "migration from gitlab server %s [%d]/%s", g.baseURL, g.repoID, g.repoName)
+}
+
// SetContext set context
func (g *GitlabDownloader) SetContext(ctx context.Context) {
g.ctx = ctx
@@ -307,6 +323,11 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
return nil, err
}
+ if !hasBaseURL(link.URL, g.baseURL) {
+ WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.ID, g, link.URL)
+ return io.NopCloser(strings.NewReader(link.URL)), nil
+ }
+
req, err := http.NewRequest("GET", link.URL, nil)
if err != nil {
return nil, err
@@ -331,8 +352,10 @@ func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) {
releases := make([]*base.Release, 0, perPage)
for i := 1; ; i++ {
ls, _, err := g.client.Releases.ListReleases(g.repoID, &gitlab.ListReleasesOptions{
- Page: i,
- PerPage: perPage,
+ ListOptions: gitlab.ListOptions{
+ Page: i,
+ PerPage: perPage,
+ },
}, nil, gitlab.WithContext(g.ctx))
if err != nil {
return nil, err
@@ -353,7 +376,8 @@ type gitlabIssueContext struct {
}
// GetIssues returns issues according start and limit
-// Note: issue label description and colors are not supported by the go-gitlab library at this time
+//
+// Note: issue label description and colors are not supported by the go-gitlab library at this time
func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
state := "all"
sort := "asc"
@@ -375,7 +399,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
issues, _, err := g.client.Issues.ListProjectIssues(g.repoID, opt, nil, gitlab.WithContext(g.ctx))
if err != nil {
- return nil, false, fmt.Errorf("error while listing issues: %v", err)
+ return nil, false, fmt.Errorf("error while listing issues: %w", err)
}
for _, issue := range issues {
@@ -396,7 +420,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
for {
awards, _, err := g.client.AwardEmoji.ListIssueAwardEmoji(g.repoID, issue.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
if err != nil {
- return nil, false, fmt.Errorf("error while listing issue awards: %v", err)
+ return nil, false, fmt.Errorf("error while listing issue awards: %w", err)
}
for i := range awards {
@@ -464,7 +488,7 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
}
if err != nil {
- return nil, false, fmt.Errorf("error while listing comments: %v %v", g.repoID, err)
+ return nil, false, fmt.Errorf("error while listing comments: %v %w", g.repoID, err)
}
for _, comment := range comments {
// Flatten comment threads
@@ -518,7 +542,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
prs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.repoID, opt, nil, gitlab.WithContext(g.ctx))
if err != nil {
- return nil, false, fmt.Errorf("error while listing merge requests: %v", err)
+ return nil, false, fmt.Errorf("error while listing merge requests: %w", err)
}
for _, pr := range prs {
@@ -560,7 +584,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
for {
awards, _, err := g.client.AwardEmoji.ListMergeRequestAwardEmoji(g.repoID, pr.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
if err != nil {
- return nil, false, fmt.Errorf("error while listing merge requests awards: %v", err)
+ return nil, false, fmt.Errorf("error while listing merge requests awards: %w", err)
}
for i := range awards {
@@ -610,6 +634,9 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
ForeignIndex: int64(pr.IID),
Context: gitlabIssueContext{IsMergeRequest: true},
})
+
+ // SECURITY: Ensure that the PR is safe
+ _ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g)
}
return allPRs, len(prs) < perPage, nil
diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go
index e63d674186df6..1d8c5989bb534 100644
--- a/services/migrations/gitlab_test.go
+++ b/services/migrations/gitlab_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
@@ -34,7 +33,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
downloader, err := NewGitlabDownloader(context.Background(), "https://gitlab.com", "gitea/test_repo", "", "", gitlabPersonalAccessToken)
if err != nil {
- t.Fatal(fmt.Sprintf("NewGitlabDownloader is nil: %v", err))
+ t.Fatalf("NewGitlabDownloader is nil: %v", err)
}
repo, err := downloader.GetRepoInfo()
assert.NoError(t, err)
diff --git a/services/migrations/gogs.go b/services/migrations/gogs.go
index a28033218eac5..d01934ac6d525 100644
--- a/services/migrations/gogs.go
+++ b/services/migrations/gogs.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
@@ -73,6 +72,20 @@ type GogsDownloader struct {
transport http.RoundTripper
}
+// String implements Stringer
+func (g *GogsDownloader) String() string {
+ return fmt.Sprintf("migration from gogs server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
+}
+
+// ColorFormat provides a basic color format for a GogsDownloader
+func (g *GogsDownloader) ColorFormat(s fmt.State) {
+ if g == nil {
+ log.ColorFprintf(s, "")
+ return
+ }
+ log.ColorFprintf(s, "migration from gogs server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
+}
+
// SetContext set context
func (g *GogsDownloader) SetContext(ctx context.Context) {
g.ctx = ctx
@@ -209,7 +222,7 @@ func (g *GogsDownloader) getIssues(page int, state string) ([]*base.Issue, bool,
State: state,
})
if err != nil {
- return nil, false, fmt.Errorf("error while listing repos: %v", err)
+ return nil, false, fmt.Errorf("error while listing repos: %w", err)
}
for _, issue := range issues {
@@ -228,7 +241,7 @@ func (g *GogsDownloader) GetComments(commentable base.Commentable) ([]*base.Comm
comments, err := g.client.ListIssueComments(g.repoOwner, g.repoName, commentable.GetForeignIndex())
if err != nil {
- return nil, false, fmt.Errorf("error while listing repos: %v", err)
+ return nil, false, fmt.Errorf("error while listing repos: %w", err)
}
for _, comment := range comments {
if len(comment.Body) == 0 || comment.Poster == nil {
diff --git a/services/migrations/gogs_test.go b/services/migrations/gogs_test.go
index 501161b610dad..610af183de219 100644
--- a/services/migrations/gogs_test.go
+++ b/services/migrations/gogs_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
diff --git a/services/migrations/http_client.go b/services/migrations/http_client.go
index 0d683693a1b6f..9e3caec191f2d 100644
--- a/services/migrations/http_client.go
+++ b/services/migrations/http_client.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
diff --git a/services/migrations/main_test.go b/services/migrations/main_test.go
index ad9bc9c731eb7..30875f6e5ba4c 100644
--- a/services/migrations/main_test.go
+++ b/services/migrations/main_test.go
@@ -1,7 +1,6 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2018 Jonas Franz. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package migrations
diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go
index b550be4ce75f4..a3b9d1cfa8fa5 100644
--- a/services/migrations/migrate.go
+++ b/services/migrations/migrate.go
@@ -1,7 +1,6 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2018 Jonas Franz. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package migrations
@@ -14,8 +13,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"
@@ -44,7 +43,7 @@ func IsMigrateURLAllowed(remoteURL string, doer *user_model.User) error {
// Remote address can be HTTP/HTTPS/Git URL or local path.
u, err := url.Parse(remoteURL)
if err != nil {
- return &models.ErrInvalidCloneAddr{IsURLError: true}
+ return &models.ErrInvalidCloneAddr{IsURLError: true, Host: remoteURL}
}
if u.Scheme == "file" || u.Scheme == "" {
@@ -81,11 +80,13 @@ func IsMigrateURLAllowed(remoteURL string, doer *user_model.User) error {
err = nil //nolint
hostName = u.Host
}
- addrList, err := net.LookupIP(hostName)
- if err != nil {
- return &models.ErrInvalidCloneAddr{Host: u.Host, NotResolvedIP: true}
- }
+ // some users only use proxy, there is no DNS resolver. it's safe to ignore the LookupIP error
+ addrList, _ := net.LookupIP(hostName)
+ return checkByAllowBlockList(hostName, addrList)
+}
+
+func checkByAllowBlockList(hostName string, addrList []net.IP) error {
var ipAllowed bool
var ipBlocked bool
for _, addr := range addrList {
@@ -94,12 +95,12 @@ func IsMigrateURLAllowed(remoteURL string, doer *user_model.User) error {
}
var blockedError error
if blockList.MatchHostName(hostName) || ipBlocked {
- blockedError = &models.ErrInvalidCloneAddr{Host: u.Host, IsPermissionDenied: true}
+ blockedError = &models.ErrInvalidCloneAddr{Host: hostName, IsPermissionDenied: true}
}
- // if we have an allow-list, check the allow-list first
+ // if we have an allow-list, check the allow-list before return to get the more accurate error
if !allowList.IsEmpty() {
if !allowList.MatchHostName(hostName) && !ipAllowed {
- return &models.ErrInvalidCloneAddr{Host: u.Host, IsPermissionDenied: true}
+ return &models.ErrInvalidCloneAddr{Host: hostName, IsPermissionDenied: true}
}
}
// otherwise, we always follow the blocked list
@@ -126,11 +127,11 @@ func MigrateRepository(ctx context.Context, doer *user_model.User, ownerName str
uploader := NewGiteaLocalUploader(ctx, doer, ownerName, opts.RepoName)
uploader.gitServiceType = opts.GitServiceType
- if err := migrateRepository(downloader, uploader, opts, messenger); err != nil {
+ if err := migrateRepository(doer, downloader, uploader, opts, messenger); err != nil {
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
@@ -175,7 +176,7 @@ func newDownloader(ctx context.Context, ownerName string, opts base.MigrateOptio
// migrateRepository will download information and then upload it to Uploader, this is a simple
// process for small repository. For a big repository, save all the data to disk
// before upload is better
-func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions, messenger base.Messenger) error {
+func migrateRepository(doer *user_model.User, downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions, messenger base.Messenger) error {
if messenger == nil {
messenger = base.NilMessenger
}
@@ -196,6 +197,27 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts
return err
}
+ // SECURITY: If the downloader is not a RepositoryRestorer then we need to recheck the CloneURL
+ if _, ok := downloader.(*RepositoryRestorer); !ok {
+ // Now the clone URL can be rewritten by the downloader so we must recheck
+ if err := IsMigrateURLAllowed(repo.CloneURL, doer); err != nil {
+ return err
+ }
+
+ // SECURITY: Ensure that we haven't been redirected from an external to a local filesystem
+ // Now we know all of these must parse
+ cloneAddrURL, _ := url.Parse(opts.CloneAddr)
+ cloneURL, _ := url.Parse(repo.CloneURL)
+
+ if cloneURL.Scheme == "file" || cloneURL.Scheme == "" {
+ if cloneAddrURL.Scheme != "file" && cloneAddrURL.Scheme != "" {
+ return fmt.Errorf("repo info has changed from external to local filesystem")
+ }
+ }
+
+ // We don't actually need to check the OriginalURL as it isn't used anywhere
+ }
+
log.Trace("migrating git data from %s", repo.CloneURL)
messenger("repo.migrate.migrating_git")
if err = uploader.CreateRepo(repo, opts); err != nil {
@@ -475,5 +497,12 @@ func Init() error {
allowList.AppendBuiltin(hostmatcher.MatchBuiltinPrivate)
allowList.AppendBuiltin(hostmatcher.MatchBuiltinLoopback)
}
+ // TODO: at the moment, if ALLOW_LOCALNETWORKS=false, ALLOWED_DOMAINS=domain.com, and domain.com has IP 127.0.0.1, then it's still allowed.
+ // if we want to block such case, the private&loopback should be added to the blockList when ALLOW_LOCALNETWORKS=false
+
+ if setting.Proxy.Enabled && setting.Proxy.ProxyURLFixed != nil {
+ allowList.AppendPattern(setting.Proxy.ProxyURLFixed.Host)
+ }
+
return nil
}
diff --git a/services/migrations/migrate_test.go b/services/migrations/migrate_test.go
index d09c184d91e0d..03efa6185b2f5 100644
--- a/services/migrations/migrate_test.go
+++ b/services/migrations/migrate_test.go
@@ -1,10 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
import (
+ "net"
"path/filepath"
"testing"
@@ -18,8 +18,8 @@ import (
func TestMigrateWhiteBlocklist(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"}).(*user_model.User)
- nonAdminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}).(*user_model.User)
+ adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
+ nonAdminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
setting.Migrations.AllowedDomains = "github.com"
setting.Migrations.AllowLocalNetworks = false
@@ -74,3 +74,42 @@ func TestMigrateWhiteBlocklist(t *testing.T) {
setting.ImportLocalPaths = old
}
+
+func TestAllowBlockList(t *testing.T) {
+ init := func(allow, block string, local bool) {
+ setting.Migrations.AllowedDomains = allow
+ setting.Migrations.BlockedDomains = block
+ setting.Migrations.AllowLocalNetworks = local
+ assert.NoError(t, Init())
+ }
+
+ // default, allow all external, block none, no local networks
+ init("", "", false)
+ assert.NoError(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("1.2.3.4")}))
+ assert.Error(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("127.0.0.1")}))
+
+ // allow all including local networks (it could lead to SSRF in production)
+ init("", "", true)
+ assert.NoError(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("1.2.3.4")}))
+ assert.NoError(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("127.0.0.1")}))
+
+ // allow wildcard, block some subdomains. if the domain name is allowed, then the local network check is skipped
+ init("*.domain.com", "blocked.domain.com", false)
+ assert.NoError(t, checkByAllowBlockList("sub.domain.com", []net.IP{net.ParseIP("1.2.3.4")}))
+ assert.NoError(t, checkByAllowBlockList("sub.domain.com", []net.IP{net.ParseIP("127.0.0.1")}))
+ assert.Error(t, checkByAllowBlockList("blocked.domain.com", []net.IP{net.ParseIP("1.2.3.4")}))
+ assert.Error(t, checkByAllowBlockList("sub.other.com", []net.IP{net.ParseIP("1.2.3.4")}))
+
+ // allow wildcard (it could lead to SSRF in production)
+ init("*", "", false)
+ assert.NoError(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("1.2.3.4")}))
+ assert.NoError(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("127.0.0.1")}))
+
+ // local network can still be blocked
+ init("*", "127.0.0.*", false)
+ assert.NoError(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("1.2.3.4")}))
+ assert.Error(t, checkByAllowBlockList("domain.com", []net.IP{net.ParseIP("127.0.0.1")}))
+
+ // reset
+ init("", "", false)
+}
diff --git a/services/migrations/onedev.go b/services/migrations/onedev.go
index d4b30939ce954..d4b1b73d37c27 100644
--- a/services/migrations/onedev.go
+++ b/services/migrations/onedev.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
@@ -38,7 +37,7 @@ func (f *OneDevDownloaderFactory) New(ctx context.Context, opts base.MigrateOpti
return nil, err
}
- repoName := ""
+ var repoName string
fields := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(fields) == 2 && fields[0] == "projects" {
@@ -110,6 +109,20 @@ func NewOneDevDownloader(ctx context.Context, baseURL *url.URL, username, passwo
return downloader
}
+// String implements Stringer
+func (d *OneDevDownloader) String() string {
+ return fmt.Sprintf("migration from oneDev server %s [%d]/%s", d.baseURL, d.repoID, d.repoName)
+}
+
+// ColorFormat provides a basic color format for a OneDevDownloader
+func (d *OneDevDownloader) ColorFormat(s fmt.State) {
+ if d == nil {
+ log.ColorFprintf(s, "")
+ return
+ }
+ log.ColorFprintf(s, "migration from oneDev server %s [%d]/%s", d.baseURL, d.repoID, d.repoName)
+}
+
func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string, result interface{}) error {
u, err := d.baseURL.Parse(endpoint)
if err != nil {
@@ -542,6 +555,9 @@ func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
ForeignIndex: pr.ID,
Context: onedevIssueContext{IsPullRequest: true},
})
+
+ // SECURITY: Ensure that the PR is safe
+ _ = CheckAndEnsureSafePR(pullRequests[len(pullRequests)-1], d.baseURL.String(), d)
}
return pullRequests, len(pullRequests) == 0, nil
diff --git a/services/migrations/onedev_test.go b/services/migrations/onedev_test.go
index 0cf1ab852ca45..48412fec64abb 100644
--- a/services/migrations/onedev_test.go
+++ b/services/migrations/onedev_test.go
@@ -1,12 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
import (
"context"
- "fmt"
"net/http"
"net/url"
"testing"
@@ -26,7 +24,7 @@ func TestOneDevDownloadRepo(t *testing.T) {
u, _ := url.Parse("https://code.onedev.io")
downloader := NewOneDevDownloader(context.Background(), u, "", "", "go-gitea-test_repo")
if err != nil {
- t.Fatal(fmt.Sprintf("NewOneDevDownloader is nil: %v", err))
+ t.Fatalf("NewOneDevDownloader is nil: %v", err)
}
repo, err := downloader.GetRepoInfo()
assert.NoError(t, err)
diff --git a/services/migrations/restore.go b/services/migrations/restore.go
index 8c9654a7e3c2b..fd337b22c7ac2 100644
--- a/services/migrations/restore.go
+++ b/services/migrations/restore.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
@@ -13,7 +12,7 @@ import (
base "code.gitea.io/gitea/modules/migration"
- "gopkg.in/yaml.v2"
+ "gopkg.in/yaml.v3"
)
// RepositoryRestorer implements an Downloader from the local directory
@@ -243,6 +242,7 @@ func (r *RepositoryRestorer) GetPullRequests(page, perPage int) ([]*base.PullReq
}
for _, pr := range pulls {
pr.PatchURL = "file://" + filepath.Join(r.baseDir, pr.PatchURL)
+ CheckAndEnsureSafePR(pr, "", r)
}
return pulls, true, nil
}
diff --git a/services/migrations/update.go b/services/migrations/update.go
index cdec73cd1bb65..48b61885e8dc7 100644
--- a/services/migrations/update.go
+++ b/services/migrations/update.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package migrations
diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go
index edc5a84d22c3f..9e569a70e3ebc 100644
--- a/services/mirror/mirror.go
+++ b/services/mirror/mirror.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package mirror
@@ -11,38 +10,21 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
+ mirror_module "code.gitea.io/gitea/modules/mirror"
"code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
)
-var mirrorQueue queue.UniqueQueue
-
-// SyncType type of sync request
-type SyncType int
-
-const (
- // PullMirrorType for pull mirrors
- PullMirrorType SyncType = iota
- // PushMirrorType for push mirrors
- PushMirrorType
-)
-
-// SyncRequest for the mirror queue
-type SyncRequest struct {
- Type SyncType
- ReferenceID int64 // RepoID for pull mirror, MirrorID fro push mirror
-}
-
// doMirrorSync causes this request to mirror itself
-func doMirrorSync(ctx context.Context, req *SyncRequest) {
+func doMirrorSync(ctx context.Context, req *mirror_module.SyncRequest) {
if req.ReferenceID == 0 {
log.Warn("Skipping mirror sync request, no mirror ID was specified")
return
}
switch req.Type {
- case PushMirrorType:
+ case mirror_module.PushMirrorType:
_ = SyncPushMirror(ctx, req.ReferenceID)
- case PullMirrorType:
+ case mirror_module.PullMirrorType:
_ = SyncPullMirror(ctx, req.ReferenceID)
default:
log.Error("Unknown Request type in queue: %v for MirrorID[%d]", req.Type, req.ReferenceID)
@@ -60,28 +42,26 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
log.Trace("Doing: Update")
handler := func(idx int, bean interface{}) error {
- var item SyncRequest
var repo *repo_model.Repository
+ var mirrorType mirror_module.SyncType
+ var referenceID int64
+
if m, ok := bean.(*repo_model.Mirror); ok {
- if m.Repo == nil {
+ if m.GetRepository() == nil {
log.Error("Disconnected mirror found: %d", m.ID)
return nil
}
repo = m.Repo
- item = SyncRequest{
- Type: PullMirrorType,
- ReferenceID: m.RepoID,
- }
+ mirrorType = mirror_module.PullMirrorType
+ referenceID = m.RepoID
} else if m, ok := bean.(*repo_model.PushMirror); ok {
- if m.Repo == nil {
+ if m.GetRepository() == nil {
log.Error("Disconnected push-mirror found: %d", m.ID)
return nil
}
repo = m.Repo
- item = SyncRequest{
- Type: PushMirrorType,
- ReferenceID: m.ID,
- }
+ mirrorType = mirror_module.PushMirrorType
+ referenceID = m.ID
} else {
log.Error("Unknown bean: %v", bean)
return nil
@@ -95,9 +75,9 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
}
// Push to the Queue
- if err := mirrorQueue.Push(&item); err != nil {
+ if err := mirror_module.PushToQueue(mirrorType, referenceID); err != nil {
if err == queue.ErrAlreadyInQueue {
- if item.Type == PushMirrorType {
+ if mirrorType == mirror_module.PushMirrorType {
log.Trace("PushMirrors for %-v already queued for sync", repo)
} else {
log.Trace("PullMirrors for %-v already queued for sync", repo)
@@ -125,7 +105,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
pushMirrorsRequested := 0
if pushLimit != 0 {
- if err := repo_model.PushMirrorsIterate(pushLimit, func(idx int, bean interface{}) error {
+ if err := repo_model.PushMirrorsIterate(ctx, pushLimit, func(idx int, bean interface{}) error {
if err := handler(idx, bean); err != nil {
return err
}
@@ -142,7 +122,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
func queueHandle(data ...queue.Data) []queue.Data {
for _, datum := range data {
- req := datum.(*SyncRequest)
+ req := datum.(*mirror_module.SyncRequest)
doMirrorSync(graceful.GetManager().ShutdownContext(), req)
}
return nil
@@ -150,43 +130,5 @@ func queueHandle(data ...queue.Data) []queue.Data {
// InitSyncMirrors initializes a go routine to sync the mirrors
func InitSyncMirrors() {
- if !setting.Mirror.Enabled {
- return
- }
- mirrorQueue = queue.CreateUniqueQueue("mirror", queueHandle, new(SyncRequest))
-
- go graceful.GetManager().RunWithShutdownFns(mirrorQueue.Run)
-}
-
-// StartToMirror adds repoID to mirror queue
-func StartToMirror(repoID int64) {
- if !setting.Mirror.Enabled {
- return
- }
- go func() {
- err := mirrorQueue.Push(&SyncRequest{
- Type: PullMirrorType,
- ReferenceID: repoID,
- })
- if err != nil {
- log.Error("Unable to push sync request for to the queue for pull mirror repo[%d]: Error: %v", repoID, err)
- return
- }
- }()
-}
-
-// AddPushMirrorToQueue adds the push mirror to the queue
-func AddPushMirrorToQueue(mirrorID int64) {
- if !setting.Mirror.Enabled {
- return
- }
- go func() {
- err := mirrorQueue.Push(&SyncRequest{
- Type: PushMirrorType,
- ReferenceID: mirrorID,
- })
- if err != nil {
- log.Error("Unable to push sync request to the queue for pull mirror repo[%d]: Error: %v", mirrorID, err)
- }
- }()
+ mirror_module.StartSyncMirrors(queueHandle)
}
diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
index ecd031b387145..98e8d122a5dfa 100644
--- a/services/mirror/mirror_pull.go
+++ b/services/mirror/mirror_pull.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package mirror
@@ -10,9 +9,9 @@ import (
"strings"
"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"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/lfs"
@@ -31,14 +30,14 @@ const gitShortEmptySha = "0000000"
// UpdateAddress writes new address to Git repository and database
func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error {
remoteName := m.GetRemoteName()
- repoPath := m.Repo.RepoPath()
+ 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 +52,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 {
@@ -71,7 +70,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
}
m.Repo.OriginalURL = addr
- return repo_model.UpdateRepositoryCols(m.Repo, "original_url")
+ return repo_model.UpdateRepositoryCols(ctx, m.Repo, "original_url")
}
// mirrorSyncResult contains information of a updated reference.
@@ -169,7 +168,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 +187,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,15 +203,16 @@ 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()))
- remoteAddr, remoteErr := git.GetRemoteAddress(ctx, repoPath, m.GetRemoteName())
+ remoteURL, remoteErr := git.GetRemoteURL(ctx, repoPath, m.GetRemoteName())
if remoteErr != nil {
log.Error("SyncMirrors [repo: %-v]: GetRemoteAddress Error %v", m.Repo, remoteErr)
+ return nil, false
}
stdoutBuilder := strings.Builder{}
@@ -266,7 +266,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
@@ -291,7 +291,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
if m.LFS && setting.LFS.StartServer {
log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
- endpoint := lfs.DetermineEndpoint(remoteAddr.String(), m.LFSEndpoint)
+ endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint)
lfsClient := lfs.NewClient(endpoint, nil)
if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err)
@@ -300,7 +300,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
gitRepo.Close()
log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo)
- if err := models.UpdateRepoSize(ctx, m.Repo); err != nil {
+ if err := repo_module.UpdateRepoSize(ctx, m.Repo); err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to update size for mirror repository: %v", m.Repo, err)
}
@@ -308,7 +308,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,
@@ -335,7 +335,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,
@@ -355,7 +355,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
@@ -395,11 +395,12 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
log.Error("PANIC whilst SyncMirrors[repo_id: %d] Panic: %v\nStacktrace: %s", repoID, err, log.Stack(2))
}()
- m, err := repo_model.GetMirrorByRepoID(repoID)
+ m, err := repo_model.GetMirrorByRepoID(ctx, repoID)
if err != nil {
log.Error("SyncMirrors [repo_id: %v]: unable to GetMirrorByRepoID: %v", repoID, err)
return false
}
+ _ = m.GetRepository() // force load repository of mirror
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Syncing Mirror %s/%s", m.Repo.OwnerName, m.Repo.Name))
defer finished()
@@ -415,7 +416,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
log.Trace("SyncMirrors [repo: %-v]: Scheduling next update", m.Repo)
m.ScheduleNextUpdate()
- if err = repo_model.UpdateMirror(m); err != nil {
+ if err = repo_model.UpdateMirror(ctx, m); err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to UpdateMirror with next update date: %v", m.Repo, err)
return false
}
@@ -457,18 +458,18 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
log.Error("SyncMirrors [repo: %-v]: unable to GetRefCommitID [ref_name: %s]: %v", m.Repo, result.refName, err)
continue
}
- notification.NotifySyncPushCommits(m.Repo.MustOwner(), m.Repo, &repo_module.PushUpdateOptions{
+ notification.NotifySyncPushCommits(ctx, m.Repo.MustOwner(ctx), m.Repo, &repo_module.PushUpdateOptions{
RefFullName: result.refName,
OldCommitID: git.EmptySHA,
NewCommitID: commitID,
}, repo_module.NewPushCommits())
- notification.NotifySyncCreateRef(m.Repo.MustOwner(), m.Repo, tp, result.refName, commitID)
+ notification.NotifySyncCreateRef(ctx, m.Repo.MustOwner(ctx), m.Repo, tp, result.refName, commitID)
continue
}
// Delete reference
if result.newCommitID == gitShortEmptySha {
- notification.NotifySyncDeleteRef(m.Repo.MustOwner(), m.Repo, tp, result.refName)
+ notification.NotifySyncDeleteRef(ctx, m.Repo.MustOwner(ctx), m.Repo, tp, result.refName)
continue
}
@@ -496,7 +497,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
theCommits.CompareURL = m.Repo.ComposeCompareURL(oldCommitID, newCommitID)
- notification.NotifySyncPushCommits(m.Repo.MustOwner(), m.Repo, &repo_module.PushUpdateOptions{
+ notification.NotifySyncPushCommits(ctx, m.Repo.MustOwner(ctx), m.Repo, &repo_module.PushUpdateOptions{
RefFullName: result.refName,
OldCommitID: oldCommitID,
NewCommitID: newCommitID,
@@ -566,7 +567,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
@@ -574,10 +575,10 @@ func checkAndUpdateEmptyRepository(m *repo_model.Mirror, gitRepo *git.Repository
}
m.Repo.IsEmpty = false
// Update the is empty and default_branch columns
- if err := repo_model.UpdateRepositoryCols(m.Repo, "default_branch", "is_empty"); err != nil {
+ 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 5c0c14c6271e5..c0c68a3f54161 100644
--- a/services/mirror/mirror_push.go
+++ b/services/mirror/mirror_push.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package mirror
@@ -29,7 +28,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 +37,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 +64,8 @@ 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 {
return err
@@ -93,12 +93,14 @@ func SyncPushMirror(ctx context.Context, mirrorID int64) bool {
log.Error("PANIC whilst syncPushMirror[%d] Panic: %v\nStacktrace: %s", mirrorID, err, log.Stack(2))
}()
- m, err := repo_model.GetPushMirrorByID(mirrorID)
+ m, err := repo_model.GetPushMirror(ctx, repo_model.PushMirrorOptions{ID: mirrorID})
if err != nil {
log.Error("GetPushMirrorByID [%d]: %v", mirrorID, err)
return false
}
+ _ = m.GetRepository()
+
m.LastError = ""
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Syncing PushMirror %s/%s to %s", m.Repo.OwnerName, m.Repo.Name, m.RemoteName))
@@ -113,7 +115,7 @@ func SyncPushMirror(ctx context.Context, mirrorID int64) bool {
m.LastUpdateUnix = timeutil.TimeStampNow()
- if err := repo_model.UpdatePushMirror(m); err != nil {
+ if err := repo_model.UpdatePushMirror(ctx, m); err != nil {
log.Error("UpdatePushMirror [%d]: %v", m.ID, err)
return false
@@ -128,7 +130,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
performPush := func(path string) error {
- remoteAddr, err := git.GetRemoteAddress(ctx, path, m.RemoteName)
+ remoteURL, err := git.GetRemoteURL(ctx, path, m.RemoteName)
if err != nil {
log.Error("GetRemoteAddress(%s) Error %v", path, err)
return errors.New("Unexpected error")
@@ -144,7 +146,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
}
defer gitRepo.Close()
- endpoint := lfs.DetermineEndpoint(remoteAddr.String(), "")
+ endpoint := lfs.DetermineEndpoint(remoteURL.String(), "")
lfsClient := lfs.NewClient(endpoint, nil)
if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil {
return util.SanitizeErrorCredentialURLs(err)
diff --git a/services/org/org.go b/services/org/org.go
index d7b3019e74ab9..e45fb305debe8 100644
--- a/services/org/org.go
+++ b/services/org/org.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package org
@@ -19,29 +18,29 @@ import (
// DeleteOrganization completely and permanently deletes everything of organization.
func DeleteOrganization(org *organization.Organization) error {
- ctx, commiter, err := db.TxContext()
+ ctx, commiter, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer commiter.Close()
// Check ownership of repository.
- count, err := repo_model.GetRepositoryCount(ctx, org.ID)
+ count, err := repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{OwnerID: org.ID})
if err != nil {
- return fmt.Errorf("GetRepositoryCount: %v", err)
+ return fmt.Errorf("GetRepositoryCount: %w", err)
} else if count > 0 {
return models.ErrUserOwnRepos{UID: org.ID}
}
// Check ownership of packages.
if ownsPackages, err := packages_model.HasOwnerPackages(ctx, org.ID); err != nil {
- return fmt.Errorf("HasOwnerPackages: %v", err)
+ return fmt.Errorf("HasOwnerPackages: %w", err)
} else if ownsPackages {
return models.ErrUserOwnPackages{UID: org.ID}
}
if err := organization.DeleteOrganization(ctx, org); err != nil {
- return fmt.Errorf("DeleteOrganization: %v", err)
+ return fmt.Errorf("DeleteOrganization: %w", err)
}
if err := commiter.Commit(); err != nil {
@@ -54,13 +53,13 @@ func DeleteOrganization(org *organization.Organization) error {
path := user_model.UserPath(org.Name)
if err := util.RemoveAll(path); err != nil {
- return fmt.Errorf("Failed to RemoveAll %s: %v", path, err)
+ return fmt.Errorf("Failed to RemoveAll %s: %w", path, err)
}
if len(org.Avatar) > 0 {
avatarPath := org.CustomAvatarRelativePath()
if err := storage.Avatars.Delete(avatarPath); err != nil {
- return fmt.Errorf("Failed to remove %s: %v", avatarPath, err)
+ return fmt.Errorf("Failed to remove %s: %w", avatarPath, err)
}
}
diff --git a/services/org/org_test.go b/services/org/org_test.go
index 7f90d85807a70..cc22595c6f198 100644
--- a/services/org/org_test.go
+++ b/services/org/org_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package org
@@ -24,18 +23,18 @@ func TestMain(m *testing.M) {
func TestDeleteOrganization(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6}).(*organization.Organization)
+ org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6})
assert.NoError(t, DeleteOrganization(org))
unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6})
unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6})
unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6})
- org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}).(*organization.Organization)
+ org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
err := DeleteOrganization(org)
assert.Error(t, err)
assert.True(t, models.IsErrUserOwnRepos(err))
- user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5}).(*organization.Organization)
+ user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5})
assert.Error(t, DeleteOrganization(user))
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
}
diff --git a/services/org/repo.go b/services/org/repo.go
new file mode 100644
index 0000000000000..179249c7a8d0f
--- /dev/null
+++ b/services/org/repo.go
@@ -0,0 +1,27 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "context"
+ "errors"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+)
+
+// TeamAddRepository adds new repository to team of organization.
+func TeamAddRepository(t *organization.Team, repo *repo_model.Repository) (err error) {
+ if repo.OwnerID != t.OrgID {
+ return errors.New("repository does not belong to organization")
+ } else if models.HasRepository(t, repo.ID) {
+ return nil
+ }
+
+ return db.WithTx(db.DefaultContext, func(ctx context.Context) error {
+ return models.AddRepository(ctx, t, repo)
+ })
+}
diff --git a/services/org/repo_test.go b/services/org/repo_test.go
new file mode 100644
index 0000000000000..40b0d17077b6b
--- /dev/null
+++ b/services/org/repo_test.go
@@ -0,0 +1,33 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTeam_AddRepository(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ testSuccess := func(teamID, repoID int64) {
+ team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
+ assert.NoError(t, TeamAddRepository(team, repo))
+ unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repoID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &repo_model.Repository{ID: repoID})
+ }
+ testSuccess(2, 3)
+ testSuccess(2, 5)
+
+ team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ assert.Error(t, TeamAddRepository(team, repo))
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: 1}, &repo_model.Repository{ID: 1})
+}
diff --git a/services/org/team_invite.go b/services/org/team_invite.go
new file mode 100644
index 0000000000000..3f28044dbf65f
--- /dev/null
+++ b/services/org/team_invite.go
@@ -0,0 +1,22 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+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/packages/auth.go b/services/packages/auth.go
index 50212fccfd327..a7acdaf1c3a9b 100644
--- a/services/packages/auth.go
+++ b/services/packages/auth.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package packages
@@ -11,6 +10,7 @@ import (
"time"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/golang-jwt/jwt/v4"
@@ -42,9 +42,15 @@ func CreateAuthorizationToken(u *user_model.User) (string, error) {
}
func ParseAuthorizationToken(req *http.Request) (int64, error) {
- parts := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
+ h := req.Header.Get("Authorization")
+ if h == "" {
+ return 0, nil
+ }
+
+ parts := strings.SplitN(h, " ", 2)
if len(parts) != 2 {
- return 0, fmt.Errorf("no token")
+ log.Error("split token failed: %s", h)
+ return 0, fmt.Errorf("split token failed")
}
token, err := jwt.ParseWithClaims(parts[1], &packageClaims{}, func(t *jwt.Token) (interface{}, error) {
diff --git a/services/packages/container/blob_uploader.go b/services/packages/container/blob_uploader.go
index 762f9e52598f8..ba92b0507343a 100644
--- a/services/packages/container/blob_uploader.go
+++ b/services/packages/container/blob_uploader.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package container
diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go
index 390a0b7b052d5..d6d4d152c83d1 100644
--- a/services/packages/container/cleanup.go
+++ b/services/packages/container/cleanup.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package container
@@ -10,6 +9,8 @@ import (
packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container"
+ container_module "code.gitea.io/gitea/modules/packages/container"
+ "code.gitea.io/gitea/modules/packages/container/oci"
"code.gitea.io/gitea/modules/util"
)
@@ -59,7 +60,7 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e
ExactMatch: true,
Value: container_model.UploadVersion,
},
- IsInternal: true,
+ IsInternal: util.OptionalBoolTrue,
HasFiles: util.OptionalBoolFalse,
})
if err != nil {
@@ -78,3 +79,31 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e
return nil
}
+
+func ShouldBeSkipped(ctx context.Context, pcr *packages_model.PackageCleanupRule, p *packages_model.Package, pv *packages_model.PackageVersion) (bool, error) {
+ // Always skip the "latest" tag
+ if pv.LowerVersion == "latest" {
+ return true, nil
+ }
+
+ // Check if the version is a digest (or untagged)
+ if oci.Digest(pv.LowerVersion).Validate() {
+ // Check if there is another manifest referencing this version
+ has, err := packages_model.ExistVersion(ctx, &packages_model.PackageSearchOptions{
+ PackageID: p.ID,
+ Properties: map[string]string{
+ container_module.PropertyManifestReference: pv.LowerVersion,
+ },
+ })
+ if err != nil {
+ return false, err
+ }
+
+ // Skip it if the version is referenced
+ if has {
+ return true, nil
+ }
+ }
+
+ return false, nil
+}
diff --git a/services/packages/container/common.go b/services/packages/container/common.go
new file mode 100644
index 0000000000000..5a14ed5b7a03b
--- /dev/null
+++ b/services/packages/container/common.go
@@ -0,0 +1,35 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package container
+
+import (
+ "context"
+ "strings"
+
+ packages_model "code.gitea.io/gitea/models/packages"
+ user_model "code.gitea.io/gitea/models/user"
+ container_module "code.gitea.io/gitea/modules/packages/container"
+)
+
+// UpdateRepositoryNames updates the repository name property for all packages of the specific owner
+func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwnerName string) error {
+ ps, err := packages_model.GetPackagesByType(ctx, owner.ID, packages_model.TypeContainer)
+ if err != nil {
+ return err
+ }
+
+ newOwnerName = strings.ToLower(newOwnerName)
+
+ for _, p := range ps {
+ if err := packages_model.DeletePropertyByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil {
+ return err
+ }
+
+ if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, newOwnerName+"/"+p.LowerName); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/services/packages/packages.go b/services/packages/packages.go
index 7f25fce5b85cc..49f5a2fac4e21 100644
--- a/services/packages/packages.go
+++ b/services/packages/packages.go
@@ -1,11 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package packages
import (
"context"
+ "encoding/hex"
+ "errors"
"fmt"
"io"
"strings"
@@ -13,14 +14,23 @@ import (
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
+ repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
packages_module "code.gitea.io/gitea/modules/packages"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
container_service "code.gitea.io/gitea/services/packages/container"
)
+var (
+ ErrQuotaTypeSize = errors.New("maximum allowed package type size exceeded")
+ ErrQuotaTotalSize = errors.New("maximum allowed package storage quota exceeded")
+ ErrQuotaTotalCount = errors.New("maximum allowed package count exceeded")
+)
+
// PackageInfo describes a package
type PackageInfo struct {
Owner *user_model.User
@@ -32,10 +42,11 @@ type PackageInfo struct {
// PackageCreationInfo describes a package to create
type PackageCreationInfo struct {
PackageInfo
- SemverCompatible bool
- Creator *user_model.User
- Metadata interface{}
- Properties map[string]string
+ SemverCompatible bool
+ Creator *user_model.User
+ Metadata interface{}
+ PackageProperties map[string]string
+ VersionProperties map[string]string
}
// PackageFileInfo describes a package file
@@ -47,6 +58,7 @@ type PackageFileInfo struct {
// PackageFileCreationInfo describes a package file to create
type PackageFileCreationInfo struct {
PackageFileInfo
+ Creator *user_model.User
Data packages_module.HashedSizeReader
IsLead bool
Properties map[string]string
@@ -64,7 +76,7 @@ func CreatePackageOrAddFileToExisting(pvci *PackageCreationInfo, pfci *PackageFi
}
func createPackageAndAddFile(pvci *PackageCreationInfo, pfci *PackageFileCreationInfo, allowDuplicate bool) (*packages_model.PackageVersion, *packages_model.PackageFile, error) {
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return nil, nil, err
}
@@ -75,7 +87,7 @@ func createPackageAndAddFile(pvci *PackageCreationInfo, pfci *PackageFileCreatio
return nil, nil, err
}
- pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, pfci)
+ pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, &pvci.PackageInfo, pfci)
removeBlob := false
defer func() {
if blobCreated && removeBlob {
@@ -96,20 +108,21 @@ func createPackageAndAddFile(pvci *PackageCreationInfo, pfci *PackageFileCreatio
}
if created {
- pd, err := packages_model.GetPackageDescriptor(ctx, pv)
+ pd, err := packages_model.GetPackageDescriptor(db.DefaultContext, pv)
if err != nil {
return nil, nil, err
}
- notification.NotifyPackageCreate(pvci.Creator, pd)
+ notification.NotifyPackageCreate(db.DefaultContext, pvci.Creator, pd)
}
return pv, pf, nil
}
func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, allowDuplicate bool) (*packages_model.PackageVersion, bool, error) {
- log.Trace("Creating package: %v, %v, %v, %s, %s, %+v, %v", pvci.Creator.ID, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version, pvci.Properties, allowDuplicate)
+ log.Trace("Creating package: %v, %v, %v, %s, %s, %+v, %+v, %v", pvci.Creator.ID, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version, pvci.PackageProperties, pvci.VersionProperties, allowDuplicate)
+ packageCreated := true
p := &packages_model.Package{
OwnerID: pvci.Owner.ID,
Type: pvci.PackageType,
@@ -119,18 +132,29 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
}
var err error
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
- if err != packages_model.ErrDuplicatePackage {
+ if err == packages_model.ErrDuplicatePackage {
+ packageCreated = false
+ } else {
log.Error("Error inserting package: %v", err)
return nil, false, err
}
}
+ if packageCreated {
+ for name, value := range pvci.PackageProperties {
+ if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, name, value); err != nil {
+ log.Error("Error setting package property: %v", err)
+ return nil, false, err
+ }
+ }
+ }
+
metadataJSON, err := json.Marshal(pvci.Metadata)
if err != nil {
return nil, false, err
}
- created := true
+ versionCreated := true
pv := &packages_model.PackageVersion{
PackageID: p.ID,
CreatorID: pvci.Creator.ID,
@@ -140,7 +164,7 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
}
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
if err == packages_model.ErrDuplicatePackageVersion {
- created = false
+ versionCreated = false
}
if err != packages_model.ErrDuplicatePackageVersion || !allowDuplicate {
log.Error("Error inserting package: %v", err)
@@ -148,8 +172,12 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
}
}
- if created {
- for name, value := range pvci.Properties {
+ if versionCreated {
+ if err := checkCountQuotaExceeded(ctx, pvci.Creator, pvci.Owner); err != nil {
+ return nil, false, err
+ }
+
+ for name, value := range pvci.VersionProperties {
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, name, value); err != nil {
log.Error("Error setting package version property: %v", err)
return nil, false, err
@@ -157,12 +185,12 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
}
}
- return pv, created, nil
+ return pv, versionCreated, nil
}
// AddFileToExistingPackage adds a file to an existing package. If the package does not exist, ErrPackageNotExist is returned
func AddFileToExistingPackage(pvi *PackageInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageVersion, *packages_model.PackageFile, error) {
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return nil, nil, err
}
@@ -173,7 +201,7 @@ func AddFileToExistingPackage(pvi *PackageInfo, pfci *PackageFileCreationInfo) (
return nil, nil, err
}
- pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, pfci)
+ pf, pb, blobCreated, err := addFileToPackageVersion(ctx, pv, pvi, pfci)
removeBlob := false
defer func() {
if removeBlob {
@@ -202,16 +230,20 @@ func NewPackageBlob(hsr packages_module.HashedSizeReader) *packages_model.Packag
return &packages_model.PackageBlob{
Size: hsr.Size(),
- HashMD5: fmt.Sprintf("%x", hashMD5),
- HashSHA1: fmt.Sprintf("%x", hashSHA1),
- HashSHA256: fmt.Sprintf("%x", hashSHA256),
- HashSHA512: fmt.Sprintf("%x", hashSHA512),
+ HashMD5: hex.EncodeToString(hashMD5),
+ HashSHA1: hex.EncodeToString(hashSHA1),
+ HashSHA256: hex.EncodeToString(hashSHA256),
+ HashSHA512: hex.EncodeToString(hashSHA512),
}
}
-func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
+func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pvi *PackageInfo, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
log.Trace("Adding package file: %v, %s", pv.ID, pfci.Filename)
+ if err := checkSizeQuotaExceeded(ctx, pfci.Creator, pvi.Owner, pvi.PackageType, pfci.Data.Size()); err != nil {
+ return nil, nil, false, err
+ }
+
pb, exists, err := packages_model.GetOrInsertBlob(ctx, NewPackageBlob(pfci.Data))
if err != nil {
log.Error("Error inserting package blob: %v", err)
@@ -270,6 +302,80 @@ func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVers
return pf, pb, !exists, nil
}
+func checkCountQuotaExceeded(ctx context.Context, doer, owner *user_model.User) error {
+ if doer.IsAdmin {
+ return nil
+ }
+
+ if setting.Packages.LimitTotalOwnerCount > -1 {
+ totalCount, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
+ OwnerID: owner.ID,
+ IsInternal: util.OptionalBoolFalse,
+ })
+ if err != nil {
+ log.Error("CountVersions failed: %v", err)
+ return err
+ }
+ if totalCount > setting.Packages.LimitTotalOwnerCount {
+ return ErrQuotaTotalCount
+ }
+ }
+
+ return nil
+}
+
+func checkSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, packageType packages_model.Type, uploadSize int64) error {
+ if doer.IsAdmin {
+ return nil
+ }
+
+ var typeSpecificSize int64
+ switch packageType {
+ case packages_model.TypeComposer:
+ typeSpecificSize = setting.Packages.LimitSizeComposer
+ case packages_model.TypeConan:
+ typeSpecificSize = setting.Packages.LimitSizeConan
+ case packages_model.TypeContainer:
+ typeSpecificSize = setting.Packages.LimitSizeContainer
+ case packages_model.TypeGeneric:
+ typeSpecificSize = setting.Packages.LimitSizeGeneric
+ case packages_model.TypeHelm:
+ typeSpecificSize = setting.Packages.LimitSizeHelm
+ case packages_model.TypeMaven:
+ typeSpecificSize = setting.Packages.LimitSizeMaven
+ case packages_model.TypeNpm:
+ typeSpecificSize = setting.Packages.LimitSizeNpm
+ case packages_model.TypeNuGet:
+ typeSpecificSize = setting.Packages.LimitSizeNuGet
+ case packages_model.TypePub:
+ typeSpecificSize = setting.Packages.LimitSizePub
+ case packages_model.TypePyPI:
+ typeSpecificSize = setting.Packages.LimitSizePyPI
+ case packages_model.TypeRubyGems:
+ typeSpecificSize = setting.Packages.LimitSizeRubyGems
+ case packages_model.TypeVagrant:
+ typeSpecificSize = setting.Packages.LimitSizeVagrant
+ }
+ if typeSpecificSize > -1 && typeSpecificSize < uploadSize {
+ return ErrQuotaTypeSize
+ }
+
+ if setting.Packages.LimitTotalOwnerSize > -1 {
+ totalSize, err := packages_model.CalculateBlobSize(ctx, &packages_model.PackageFileSearchOptions{
+ OwnerID: owner.ID,
+ })
+ if err != nil {
+ log.Error("CalculateBlobSize failed: %v", err)
+ return err
+ }
+ if totalSize+uploadSize > setting.Packages.LimitTotalOwnerSize {
+ return ErrQuotaTotalSize
+ }
+ }
+
+ return nil
+}
+
// RemovePackageVersionByNameAndVersion deletes a package version and all associated files
func RemovePackageVersionByNameAndVersion(doer *user_model.User, pvi *PackageInfo) error {
pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version)
@@ -282,7 +388,7 @@ func RemovePackageVersionByNameAndVersion(doer *user_model.User, pvi *PackageInf
// RemovePackageVersion deletes the package version and all associated files
func RemovePackageVersion(doer *user_model.User, pv *packages_model.PackageVersion) error {
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
@@ -303,7 +409,7 @@ func RemovePackageVersion(doer *user_model.User, pv *packages_model.PackageVersi
return err
}
- notification.NotifyPackageDelete(doer, pd)
+ notification.NotifyPackageDelete(db.DefaultContext, doer, pd)
return nil
}
@@ -337,20 +443,96 @@ func DeletePackageFile(ctx context.Context, pf *packages_model.PackageFile) erro
}
// Cleanup removes expired package data
-func Cleanup(unused context.Context, olderThan time.Duration) error {
- ctx, committer, err := db.TxContext()
+func Cleanup(taskCtx context.Context, olderThan time.Duration) error {
+ ctx, committer, err := db.TxContext(taskCtx)
if err != nil {
return err
}
defer committer.Close()
+ err = packages_model.IterateEnabledCleanupRules(ctx, func(ctx context.Context, pcr *packages_model.PackageCleanupRule) error {
+ select {
+ case <-taskCtx.Done():
+ return db.ErrCancelledf("While processing package cleanup rules")
+ default:
+ }
+
+ if err := pcr.CompiledPattern(); err != nil {
+ return fmt.Errorf("CleanupRule [%d]: CompilePattern failed: %w", pcr.ID, err)
+ }
+
+ olderThan := time.Now().AddDate(0, 0, -pcr.RemoveDays)
+
+ packages, err := packages_model.GetPackagesByType(ctx, pcr.OwnerID, pcr.Type)
+ if err != nil {
+ return fmt.Errorf("CleanupRule [%d]: GetPackagesByType failed: %w", pcr.ID, err)
+ }
+
+ for _, p := range packages {
+ pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+ PackageID: p.ID,
+ IsInternal: util.OptionalBoolFalse,
+ Sort: packages_model.SortCreatedDesc,
+ Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200),
+ })
+ if err != nil {
+ return fmt.Errorf("CleanupRule [%d]: SearchVersions failed: %w", pcr.ID, err)
+ }
+ for _, pv := range pvs {
+ if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
+ return fmt.Errorf("CleanupRule [%d]: container.ShouldBeSkipped failed: %w", pcr.ID, err)
+ } else if skip {
+ log.Debug("Rule[%d]: keep '%s/%s' (container)", pcr.ID, p.Name, pv.Version)
+ continue
+ }
+
+ toMatch := pv.LowerVersion
+ if pcr.MatchFullName {
+ toMatch = p.LowerName + "/" + pv.LowerVersion
+ }
+
+ if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) {
+ log.Debug("Rule[%d]: keep '%s/%s' (keep pattern)", pcr.ID, p.Name, pv.Version)
+ continue
+ }
+ if pv.CreatedUnix.AsLocalTime().After(olderThan) {
+ log.Debug("Rule[%d]: keep '%s/%s' (remove days)", pcr.ID, p.Name, pv.Version)
+ continue
+ }
+ if pcr.RemovePatternMatcher != nil && !pcr.RemovePatternMatcher.MatchString(toMatch) {
+ log.Debug("Rule[%d]: keep '%s/%s' (remove pattern)", pcr.ID, p.Name, pv.Version)
+ continue
+ }
+
+ log.Debug("Rule[%d]: remove '%s/%s'", pcr.ID, p.Name, pv.Version)
+
+ if err := DeletePackageVersionAndReferences(ctx, pv); err != nil {
+ return fmt.Errorf("CleanupRule [%d]: DeletePackageVersionAndReferences failed: %w", pcr.ID, err)
+ }
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
if err := container_service.Cleanup(ctx, olderThan); err != nil {
return err
}
- if err := packages_model.DeletePackagesIfUnreferenced(ctx); err != nil {
+ ps, err := packages_model.FindUnreferencedPackages(ctx)
+ if err != nil {
return err
}
+ for _, p := range ps {
+ if err := packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID); err != nil {
+ return err
+ }
+ if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil {
+ return err
+ }
+ }
pbs, err := packages_model.FindExpiredUnreferencedBlobs(ctx, olderThan)
if err != nil {
@@ -378,7 +560,7 @@ func Cleanup(unused context.Context, olderThan time.Duration) error {
}
// GetFileStreamByPackageNameAndVersion returns the content of the specific package file
-func GetFileStreamByPackageNameAndVersion(ctx context.Context, pvi *PackageInfo, pfi *PackageFileInfo) (io.ReadCloser, *packages_model.PackageFile, error) {
+func GetFileStreamByPackageNameAndVersion(ctx context.Context, pvi *PackageInfo, pfi *PackageFileInfo) (io.ReadSeekCloser, *packages_model.PackageFile, error) {
log.Trace("Getting package file stream: %v, %v, %s, %s, %s, %s", pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version, pfi.Filename, pfi.CompositeKey)
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version)
@@ -394,7 +576,7 @@ func GetFileStreamByPackageNameAndVersion(ctx context.Context, pvi *PackageInfo,
}
// GetFileStreamByPackageVersionAndFileID returns the content of the specific package file
-func GetFileStreamByPackageVersionAndFileID(ctx context.Context, owner *user_model.User, versionID, fileID int64) (io.ReadCloser, *packages_model.PackageFile, error) {
+func GetFileStreamByPackageVersionAndFileID(ctx context.Context, owner *user_model.User, versionID, fileID int64) (io.ReadSeekCloser, *packages_model.PackageFile, error) {
log.Trace("Getting package file stream: %v, %v, %v", owner.ID, versionID, fileID)
pv, err := packages_model.GetVersionByID(ctx, versionID)
@@ -425,8 +607,8 @@ func GetFileStreamByPackageVersionAndFileID(ctx context.Context, owner *user_mod
}
// GetFileStreamByPackageVersion returns the content of the specific package file
-func GetFileStreamByPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pfi *PackageFileInfo) (io.ReadCloser, *packages_model.PackageFile, error) {
- pf, err := packages_model.GetFileForVersionByName(db.DefaultContext, pv.ID, pfi.Filename, pfi.CompositeKey)
+func GetFileStreamByPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pfi *PackageFileInfo) (io.ReadSeekCloser, *packages_model.PackageFile, error) {
+ pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, pfi.Filename, pfi.CompositeKey)
if err != nil {
return nil, nil, err
}
@@ -435,7 +617,7 @@ func GetFileStreamByPackageVersion(ctx context.Context, pv *packages_model.Packa
}
// GetPackageFileStream returns the content of the specific package file
-func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) (io.ReadCloser, *packages_model.PackageFile, error) {
+func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) (io.ReadSeekCloser, *packages_model.PackageFile, error) {
pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
if err != nil {
return nil, nil, err
@@ -451,3 +633,31 @@ func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) (
}
return s, pf, err
}
+
+// RemoveAllPackages for User
+func RemoveAllPackages(ctx context.Context, userID int64) (int, error) {
+ count := 0
+ for {
+ pkgVersions, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+ Paginator: &db.ListOptions{
+ PageSize: repo_model.RepositoryListDefaultPageSize,
+ Page: 1,
+ },
+ OwnerID: userID,
+ IsInternal: util.OptionalBoolNone,
+ })
+ if err != nil {
+ return count, fmt.Errorf("GetOwnedPackages[%d]: %w", userID, err)
+ }
+ if len(pkgVersions) == 0 {
+ break
+ }
+ for _, pv := range pkgVersions {
+ if err := DeletePackageVersionAndReferences(ctx, pv); err != nil {
+ return count, fmt.Errorf("unable to delete package %d:%s[%d]. Error: %w", pv.PackageID, pv.Version, pv.ID, err)
+ }
+ count++
+ }
+ }
+ return count, nil
+}
diff --git a/services/pull/check.go b/services/pull/check.go
index 253417072ce06..db86378909c49 100644
--- a/services/pull/check.go
+++ b/services/pull/check.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
@@ -14,6 +13,10 @@ import (
"strings"
"code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -28,23 +31,24 @@ import (
asymkey_service "code.gitea.io/gitea/services/asymkey"
)
-// prQueue represents a queue to handle update pull request tests
-var prQueue queue.UniqueQueue
+// prPatchCheckerQueue represents a queue to handle update pull request tests
+var prPatchCheckerQueue queue.UniqueQueue
var (
- ErrIsClosed = errors.New("pull is cosed")
- ErrUserNotAllowedToMerge = errors.New("user not allowed to merge")
+ ErrIsClosed = errors.New("pull is closed")
+ ErrUserNotAllowedToMerge = models.ErrDisallowedToMerge{}
ErrHasMerged = errors.New("has already been merged")
ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged")
+ ErrIsChecking = errors.New("cannot merge while conflict checking is in progress")
ErrNotMergableState = errors.New("not in mergeable state")
ErrDependenciesLeft = errors.New("is blocked by an open dependency")
)
// AddToTaskQueue adds itself to pull request test task queue.
-func AddToTaskQueue(pr *models.PullRequest) {
- err := prQueue.PushFunc(strconv.FormatInt(pr.ID, 10), func() error {
- pr.Status = models.PullRequestStatusChecking
- err := pr.UpdateColsIfNotMerged("status")
+func AddToTaskQueue(pr *issues_model.PullRequest) {
+ err := prPatchCheckerQueue.PushFunc(strconv.FormatInt(pr.ID, 10), func() error {
+ pr.Status = issues_model.PullRequestStatusChecking
+ err := pr.UpdateColsIfNotMerged(db.DefaultContext, "status")
if err != nil {
log.Error("AddToTaskQueue.UpdateCols[%d].(add to queue): %v", pr.ID, err)
} else {
@@ -58,70 +62,77 @@ func AddToTaskQueue(pr *models.PullRequest) {
}
// CheckPullMergable check if the pull mergable based on all conditions (branch protection, merge options, ...)
-func CheckPullMergable(ctx context.Context, doer *user_model.User, perm *models.Permission, pr *models.PullRequest, manuallMerge, force bool) error {
- if pr.HasMerged {
- return ErrHasMerged
- }
+func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, manuallMerge, force bool) error {
+ return db.WithTx(stdCtx, func(ctx context.Context) error {
+ if pr.HasMerged {
+ return ErrHasMerged
+ }
- if err := pr.LoadIssue(); err != nil {
- return err
- } else if pr.Issue.IsClosed {
- return ErrIsClosed
- }
+ if err := pr.LoadIssue(ctx); err != nil {
+ return err
+ } else if pr.Issue.IsClosed {
+ return ErrIsClosed
+ }
- if allowedMerge, err := IsUserAllowedToMerge(pr, *perm, doer); err != nil {
- return err
- } else if !allowedMerge {
- return ErrUserNotAllowedToMerge
- }
+ if allowedMerge, err := IsUserAllowedToMerge(ctx, pr, *perm, doer); err != nil {
+ return err
+ } else if !allowedMerge {
+ return ErrUserNotAllowedToMerge
+ }
- if manuallMerge {
- // don't check rules to "auto merge", doer is going to mark this pull as merged manually
- return nil
- }
+ if manuallMerge {
+ // don't check rules to "auto merge", doer is going to mark this pull as merged manually
+ return nil
+ }
- if pr.IsWorkInProgress() {
- return ErrIsWorkInProgress
- }
+ if pr.IsWorkInProgress() {
+ return ErrIsWorkInProgress
+ }
- if !pr.CanAutoMerge() {
- return ErrNotMergableState
- }
+ if !pr.CanAutoMerge() && !pr.IsEmpty() {
+ return ErrNotMergableState
+ }
- if err := CheckPRReadyToMerge(ctx, pr, false); err != nil {
- if models.IsErrDisallowedToMerge(err) {
- if force {
- if isRepoAdmin, err := models.IsUserRepoAdmin(pr.BaseRepo, doer); err != nil {
- return err
- } else if !isRepoAdmin {
- return ErrUserNotAllowedToMerge
+ if pr.IsChecking() {
+ return ErrIsChecking
+ }
+
+ if err := CheckPullBranchProtections(ctx, pr, false); err != nil {
+ if models.IsErrDisallowedToMerge(err) {
+ if force {
+ if isRepoAdmin, err2 := access_model.IsUserRepoAdmin(ctx, pr.BaseRepo, doer); err2 != nil {
+ return err2
+ } else if !isRepoAdmin {
+ return err
+ }
}
+ } else {
+ return err
}
- } else {
- return err
}
- }
- if _, err := isSignedIfRequired(ctx, pr, doer); err != nil {
- return err
- }
+ if _, err := isSignedIfRequired(ctx, pr, doer); err != nil {
+ return err
+ }
- if noDeps, err := models.IssueNoDependenciesLeft(pr.Issue); err != nil {
- return err
- } else if !noDeps {
- return ErrDependenciesLeft
- }
+ if noDeps, err := issues_model.IssueNoDependenciesLeft(ctx, pr.Issue); err != nil {
+ return err
+ } else if !noDeps {
+ return ErrDependenciesLeft
+ }
- return nil
+ return nil
+ })
}
// isSignedIfRequired check if merge will be signed if required
-func isSignedIfRequired(ctx context.Context, pr *models.PullRequest, doer *user_model.User) (bool, error) {
- if err := pr.LoadProtectedBranch(); err != nil {
+func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) (bool, error) {
+ pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
+ if err != nil {
return false, err
}
- if pr.ProtectedBranch == nil || !pr.ProtectedBranch.RequireSignedCommits {
+ if pb == nil || !pb.RequireSignedCommits {
return true, nil
}
@@ -132,20 +143,20 @@ func isSignedIfRequired(ctx context.Context, pr *models.PullRequest, doer *user_
// checkAndUpdateStatus checks if pull request is possible to leaving checking status,
// and set to be either conflict or mergeable.
-func checkAndUpdateStatus(pr *models.PullRequest) {
+func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) {
// Status is not changed to conflict means mergeable.
- if pr.Status == models.PullRequestStatusChecking {
- pr.Status = models.PullRequestStatusMergeable
+ if pr.Status == issues_model.PullRequestStatusChecking {
+ pr.Status = issues_model.PullRequestStatusMergeable
}
// Make sure there is no waiting test to process before leaving the checking status.
- has, err := prQueue.Has(strconv.FormatInt(pr.ID, 10))
+ has, err := prPatchCheckerQueue.Has(strconv.FormatInt(pr.ID, 10))
if err != nil {
log.Error("Unable to check if the queue is waiting to reprocess pr.ID %d. Error: %v", pr.ID, err)
}
if !has {
- if err := pr.UpdateColsIfNotMerged("merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
+ if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
log.Error("Update[%d]: %v", pr.ID, err)
}
}
@@ -153,18 +164,18 @@ func checkAndUpdateStatus(pr *models.PullRequest) {
// getMergeCommit checks if a pull request got merged
// Returns the git.Commit of the pull request if merged
-func getMergeCommit(ctx context.Context, pr *models.PullRequest) (*git.Commit, error) {
+func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Commit, error) {
if pr.BaseRepo == nil {
var err error
- pr.BaseRepo, err = repo_model.GetRepositoryByID(pr.BaseRepoID)
+ pr.BaseRepo, err = repo_model.GetRepositoryByID(ctx, pr.BaseRepoID)
if err != nil {
- return nil, fmt.Errorf("GetRepositoryByID: %v", err)
+ return nil, fmt.Errorf("GetRepositoryByID: %w", err)
}
}
indexTmpPath, err := os.MkdirTemp(os.TempDir(), "gitea-"+pr.BaseRepo.Name)
if err != nil {
- return nil, fmt.Errorf("Failed to create temp dir for repository %s: %v", pr.BaseRepo.RepoPath(), err)
+ return nil, fmt.Errorf("Failed to create temp dir for repository %s: %w", pr.BaseRepo.RepoPath(), err)
}
defer func() {
if err := util.RemoveAll(indexTmpPath); err != nil {
@@ -175,45 +186,45 @@ func getMergeCommit(ctx context.Context, pr *models.PullRequest) (*git.Commit, e
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
if strings.Contains(err.Error(), "exit status 1") {
return nil, nil
}
- return nil, fmt.Errorf("git merge-base --is-ancestor: %v", err)
+ return nil, fmt.Errorf("git merge-base --is-ancestor: %w", err)
}
commitIDBytes, err := os.ReadFile(pr.BaseRepo.RepoPath() + "/" + headFile)
if err != nil {
- return nil, fmt.Errorf("ReadFile(%s): %v", headFile, err)
+ return nil, fmt.Errorf("ReadFile(%s): %w", headFile, err)
}
commitID := string(commitIDBytes)
- if len(commitID) < 40 {
+ if len(commitID) < git.SHAFullLength {
return nil, fmt.Errorf(`ReadFile(%s): invalid commit-ID "%s"`, headFile, commitID)
}
- cmd := commitID[:40] + ".." + pr.BaseBranch
+ cmd := commitID[:git.SHAFullLength] + ".." + 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)
- } else if len(mergeCommit) < 40 {
+ return nil, fmt.Errorf("git rev-list --ancestry-path --merges --reverse: %w", err)
+ } else if len(mergeCommit) < git.SHAFullLength {
// PR was maybe fast-forwarded, so just use last commit of PR
- mergeCommit = commitID[:40]
+ mergeCommit = commitID[:git.SHAFullLength]
}
gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
if err != nil {
- return nil, fmt.Errorf("OpenRepository: %v", err)
+ return nil, fmt.Errorf("OpenRepository: %w", err)
}
defer gitRepo.Close()
- commit, err := gitRepo.GetCommit(mergeCommit[:40])
+ commit, err := gitRepo.GetCommit(mergeCommit[:git.SHAFullLength])
if err != nil {
- return nil, fmt.Errorf("GetMergeCommit[%v]: %v", mergeCommit[:40], err)
+ return nil, fmt.Errorf("GetMergeCommit[%v]: %w", mergeCommit[:git.SHAFullLength], err)
}
return commit, nil
@@ -221,13 +232,13 @@ func getMergeCommit(ctx context.Context, pr *models.PullRequest) (*git.Commit, e
// manuallyMerged checks if a pull request got manually merged
// When a pull request got manually merged mark the pull request as merged
-func manuallyMerged(ctx context.Context, pr *models.PullRequest) bool {
- if err := pr.LoadBaseRepo(); err != nil {
+func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("PullRequest[%d].LoadBaseRepo: %v", pr.ID, err)
return false
}
- if unit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests); err == nil {
+ if unit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests); err == nil {
config := unit.PullRequestsConfig()
if !config.AutodetectManualMerge {
return false
@@ -245,7 +256,7 @@ func manuallyMerged(ctx context.Context, pr *models.PullRequest) bool {
if commit != nil {
pr.MergedCommitID = commit.ID.String()
pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix())
- pr.Status = models.PullRequestStatusManuallyMerged
+ pr.Status = issues_model.PullRequestStatusManuallyMerged
merger, _ := user_model.GetUserByEmail(commit.Author.Email)
// When the commit author is unknown set the BaseRepo owner as merger
@@ -261,14 +272,14 @@ func manuallyMerged(ctx context.Context, pr *models.PullRequest) bool {
pr.Merger = merger
pr.MergerID = merger.ID
- if merged, err := pr.SetMerged(); err != nil {
+ if merged, err := pr.SetMerged(ctx); err != nil {
log.Error("PullRequest[%d].setMerged : %v", pr.ID, err)
return false
} else if !merged {
return false
}
- notification.NotifyMergePullRequest(pr, merger)
+ notification.NotifyMergePullRequest(ctx, merger, pr)
log.Info("manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s", pr.ID, pr.BaseRepo.Name, pr.BaseBranch, commit.ID.String())
return true
@@ -278,7 +289,7 @@ func manuallyMerged(ctx context.Context, pr *models.PullRequest) bool {
// InitializePullRequests checks and tests untested patches of pull requests.
func InitializePullRequests(ctx context.Context) {
- prs, err := models.GetPullRequestIDsByCheckStatus(models.PullRequestStatusChecking)
+ prs, err := issues_model.GetPullRequestIDsByCheckStatus(issues_model.PullRequestStatusChecking)
if err != nil {
log.Error("Find Checking PRs: %v", err)
return
@@ -288,7 +299,7 @@ func InitializePullRequests(ctx context.Context) {
case <-ctx.Done():
return
default:
- if err := prQueue.PushFunc(strconv.FormatInt(prID, 10), func() error {
+ if err := prPatchCheckerQueue.PushFunc(strconv.FormatInt(prID, 10), func() error {
log.Trace("Adding PR ID: %d to the pull requests patch checking queue", prID)
return nil
}); err != nil {
@@ -309,10 +320,12 @@ func handle(data ...queue.Data) []queue.Data {
}
func testPR(id int64) {
+ pullWorkingPool.CheckIn(fmt.Sprint(id))
+ defer pullWorkingPool.CheckOut(fmt.Sprint(id))
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("Test PR[%d] from patch checking queue", id))
defer finished()
- pr, err := models.GetPullRequestByID(id)
+ pr, err := issues_model.GetPullRequestByID(ctx, id)
if err != nil {
log.Error("GetPullRequestByID[%d]: %v", id, err)
return
@@ -328,18 +341,18 @@ func testPR(id int64) {
if err := TestPatch(pr); err != nil {
log.Error("testPatch[%d]: %v", pr.ID, err)
- pr.Status = models.PullRequestStatusError
+ pr.Status = issues_model.PullRequestStatusError
if err := pr.UpdateCols("status"); err != nil {
log.Error("update pr [%d] status to PullRequestStatusError failed: %v", pr.ID, err)
}
return
}
- checkAndUpdateStatus(pr)
+ checkAndUpdateStatus(ctx, pr)
}
-// CheckPrsForBaseBranch check all pulls with bseBrannch
-func CheckPrsForBaseBranch(baseRepo *repo_model.Repository, baseBranchName string) error {
- prs, err := models.GetUnmergedPullRequestsByBaseInfo(baseRepo.ID, baseBranchName)
+// CheckPRsForBaseBranch check all pulls with baseBrannch
+func CheckPRsForBaseBranch(baseRepo *repo_model.Repository, baseBranchName string) error {
+ prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(baseRepo.ID, baseBranchName)
if err != nil {
return err
}
@@ -353,13 +366,13 @@ func CheckPrsForBaseBranch(baseRepo *repo_model.Repository, baseBranchName strin
// Init runs the task queue to test all the checking status pull requests
func Init() error {
- prQueue = queue.CreateUniqueQueue("pr_patch_checker", handle, "")
+ prPatchCheckerQueue = queue.CreateUniqueQueue("pr_patch_checker", handle, "")
- if prQueue == nil {
+ if prPatchCheckerQueue == nil {
return fmt.Errorf("Unable to create pr_patch_checker Queue")
}
- go graceful.GetManager().RunWithShutdownFns(prQueue.Run)
+ go graceful.GetManager().RunWithShutdownFns(prPatchCheckerQueue.Run)
go graceful.GetManager().RunWithShutdownContext(InitializePullRequests)
return nil
}
diff --git a/services/pull/check_test.go b/services/pull/check_test.go
index 65bcb9c0e44db..590065250f3d1 100644
--- a/services/pull/check_test.go
+++ b/services/pull/check_test.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
@@ -10,7 +9,7 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/queue"
@@ -41,21 +40,21 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
queueShutdown := []func(){}
queueTerminate := []func(){}
- prQueue = q.(queue.UniqueQueue)
+ prPatchCheckerQueue = q.(queue.UniqueQueue)
- pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest)
+ pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
AddToTaskQueue(pr)
assert.Eventually(t, func() bool {
- pr = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest)
- return pr.Status == models.PullRequestStatusChecking
+ pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
+ return pr.Status == issues_model.PullRequestStatusChecking
}, 1*time.Second, 100*time.Millisecond)
- has, err := prQueue.Has(strconv.FormatInt(pr.ID, 10))
+ has, err := prPatchCheckerQueue.Has(strconv.FormatInt(pr.ID, 10))
assert.True(t, has)
assert.NoError(t, err)
- prQueue.Run(func(shutdown func()) {
+ prPatchCheckerQueue.Run(func(shutdown func()) {
queueShutdown = append(queueShutdown, shutdown)
}, func(terminate func()) {
queueTerminate = append(queueTerminate, terminate)
@@ -68,12 +67,12 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
assert.Fail(t, "Timeout: nothing was added to pullRequestQueue")
}
- has, err = prQueue.Has(strconv.FormatInt(pr.ID, 10))
+ has, err = prPatchCheckerQueue.Has(strconv.FormatInt(pr.ID, 10))
assert.False(t, has)
assert.NoError(t, err)
- pr = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest)
- assert.Equal(t, models.PullRequestStatusChecking, pr.Status)
+ pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
+ assert.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
for _, callback := range queueShutdown {
callback()
@@ -82,5 +81,5 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
callback()
}
- prQueue = nil
+ prPatchCheckerQueue = nil
}
diff --git a/services/pull/comment.go b/services/pull/comment.go
new file mode 100644
index 0000000000000..068aca6cd128b
--- /dev/null
+++ b/services/pull/comment.go
@@ -0,0 +1,162 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull
+
+import (
+ "context"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/json"
+ issue_service "code.gitea.io/gitea/services/issue"
+)
+
+type commitBranchCheckItem struct {
+ Commit *git.Commit
+ Checked bool
+}
+
+func commitBranchCheck(gitRepo *git.Repository, startCommit *git.Commit, endCommitID, baseBranch string, commitList map[string]*commitBranchCheckItem) error {
+ if startCommit.ID.String() == endCommitID {
+ return nil
+ }
+
+ checkStack := make([]string, 0, 10)
+ checkStack = append(checkStack, startCommit.ID.String())
+
+ for len(checkStack) > 0 {
+ commitID := checkStack[0]
+ checkStack = checkStack[1:]
+
+ item, ok := commitList[commitID]
+ if !ok {
+ continue
+ }
+
+ if item.Commit.ID.String() == endCommitID {
+ continue
+ }
+
+ if err := item.Commit.LoadBranchName(); err != nil {
+ return err
+ }
+
+ if item.Commit.Branch == baseBranch {
+ continue
+ }
+
+ if item.Checked {
+ continue
+ }
+
+ item.Checked = true
+
+ parentNum := item.Commit.ParentCount()
+ for i := 0; i < parentNum; i++ {
+ parentCommit, err := item.Commit.Parent(i)
+ if err != nil {
+ return err
+ }
+ checkStack = append(checkStack, parentCommit.ID.String())
+ }
+ }
+ return nil
+}
+
+// getCommitIDsFromRepo get commit IDs from repo in between oldCommitID and newCommitID
+// isForcePush will be true if oldCommit isn't on the branch
+// Commit on baseBranch will skip
+func getCommitIDsFromRepo(ctx context.Context, repo *repo_model.Repository, oldCommitID, newCommitID, baseBranch string) (commitIDs []string, isForcePush bool, err error) {
+ repoPath := repo.RepoPath()
+ gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repoPath)
+ if err != nil {
+ return nil, false, err
+ }
+ defer closer.Close()
+
+ oldCommit, err := gitRepo.GetCommit(oldCommitID)
+ if err != nil {
+ return nil, false, err
+ }
+
+ if err = oldCommit.LoadBranchName(); err != nil {
+ return nil, false, err
+ }
+
+ if len(oldCommit.Branch) == 0 {
+ commitIDs = make([]string, 2)
+ commitIDs[0] = oldCommitID
+ commitIDs[1] = newCommitID
+
+ return commitIDs, true, err
+ }
+
+ newCommit, err := gitRepo.GetCommit(newCommitID)
+ if err != nil {
+ return nil, false, err
+ }
+
+ commits, err := newCommit.CommitsBeforeUntil(oldCommitID)
+ if err != nil {
+ return nil, false, err
+ }
+
+ commitIDs = make([]string, 0, len(commits))
+ commitChecks := make(map[string]*commitBranchCheckItem)
+
+ for _, commit := range commits {
+ commitChecks[commit.ID.String()] = &commitBranchCheckItem{
+ Commit: commit,
+ Checked: false,
+ }
+ }
+
+ if err = commitBranchCheck(gitRepo, newCommit, oldCommitID, baseBranch, commitChecks); err != nil {
+ return
+ }
+
+ for i := len(commits) - 1; i >= 0; i-- {
+ commitID := commits[i].ID.String()
+ if item, ok := commitChecks[commitID]; ok && item.Checked {
+ commitIDs = append(commitIDs, commitID)
+ }
+ }
+
+ return commitIDs, isForcePush, err
+}
+
+// CreatePushPullComment create push code to pull base comment
+func CreatePushPullComment(ctx context.Context, pusher *user_model.User, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (comment *issues_model.Comment, err error) {
+ if pr.HasMerged || oldCommitID == "" || newCommitID == "" {
+ return nil, nil
+ }
+
+ ops := &issues_model.CreateCommentOptions{
+ Type: issues_model.CommentTypePullRequestPush,
+ Doer: pusher,
+ Repo: pr.BaseRepo,
+ }
+
+ var data issues_model.PushActionContent
+
+ data.CommitIDs, data.IsForcePush, err = getCommitIDsFromRepo(ctx, pr.BaseRepo, oldCommitID, newCommitID, pr.BaseBranch)
+ if err != nil {
+ return nil, err
+ }
+
+ ops.Issue = pr.Issue
+
+ dataJSON, err := json.Marshal(data)
+ if err != nil {
+ return nil, err
+ }
+
+ ops.Content = string(dataJSON)
+
+ comment, err = issue_service.CreateComment(ops)
+
+ return comment, err
+}
diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index be8df0c9b1588..bfdb3f7291b82 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -1,15 +1,15 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
import (
"context"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/structs"
@@ -17,9 +17,9 @@ import (
)
// MergeRequiredContextsCommitStatus returns a commit status state for given required contexts
-func MergeRequiredContextsCommitStatus(commitStatuses []*models.CommitStatus, requiredContexts []string) structs.CommitStatusState {
+func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, requiredContexts []string) structs.CommitStatusState {
if len(requiredContexts) == 0 {
- status := models.CalcCommitStatus(commitStatuses)
+ status := git_model.CalcCommitStatus(commitStatuses)
if status != nil {
return status.State
}
@@ -38,7 +38,7 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*models.CommitStatus, re
if targetStatus == "" {
targetStatus = structs.CommitStatusPending
- commitStatuses = append(commitStatuses, &models.CommitStatus{
+ commitStatuses = append(commitStatuses, &git_model.CommitStatus{
State: targetStatus,
Context: ctx,
Description: "Pending",
@@ -52,10 +52,10 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*models.CommitStatus, re
}
// IsCommitStatusContextSuccess returns true if all required status check contexts succeed.
-func IsCommitStatusContextSuccess(commitStatuses []*models.CommitStatus, requiredContexts []string) bool {
+func IsCommitStatusContextSuccess(commitStatuses []*git_model.CommitStatus, requiredContexts []string) bool {
// If no specific context is required, require that last commit status is a success
if len(requiredContexts) == 0 {
- status := models.CalcCommitStatus(commitStatuses)
+ status := git_model.CalcCommitStatus(commitStatuses)
if status == nil || status.State != structs.CommitStatusSuccess {
return false
}
@@ -82,11 +82,12 @@ func IsCommitStatusContextSuccess(commitStatuses []*models.CommitStatus, require
}
// IsPullCommitStatusPass returns if all required status checks PASS
-func IsPullCommitStatusPass(ctx context.Context, pr *models.PullRequest) (bool, error) {
- if err := pr.LoadProtectedBranch(); err != nil {
+func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) (bool, error) {
+ pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
+ if err != nil {
return false, errors.Wrap(err, "GetLatestCommitStatus")
}
- if pr.ProtectedBranch == nil || !pr.ProtectedBranch.EnableStatusCheck {
+ if pb == nil || !pb.EnableStatusCheck {
return true, nil
}
@@ -98,9 +99,9 @@ func IsPullCommitStatusPass(ctx context.Context, pr *models.PullRequest) (bool,
}
// GetPullRequestCommitStatusState returns pull request merged commit status state
-func GetPullRequestCommitStatusState(ctx context.Context, pr *models.PullRequest) (structs.CommitStatusState, error) {
+func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (structs.CommitStatusState, error) {
// Ensure HeadRepo is loaded
- if err := pr.LoadHeadRepo(); err != nil {
+ if err := pr.LoadHeadRepo(ctx); err != nil {
return "", errors.Wrap(err, "LoadHeadRepo")
}
@@ -111,15 +112,15 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *models.PullRequest
}
defer closer.Close()
- if pr.Flow == models.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) {
+ if pr.Flow == issues_model.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) {
return "", errors.New("Head branch does not exist, can not merge")
}
- if pr.Flow == models.PullRequestFlowAGit && !git.IsReferenceExist(headGitRepo.Ctx, headGitRepo.Path, pr.GetGitRefName()) {
+ if pr.Flow == issues_model.PullRequestFlowAGit && !git.IsReferenceExist(ctx, headGitRepo.Path, pr.GetGitRefName()) {
return "", errors.New("Head branch does not exist, can not merge")
}
var sha string
- if pr.Flow == models.PullRequestFlowGithub {
+ if pr.Flow == issues_model.PullRequestFlowGithub {
sha, err = headGitRepo.GetBranchCommitID(pr.HeadBranch)
} else {
sha, err = headGitRepo.GetRefCommitID(pr.GetGitRefName())
@@ -128,14 +129,23 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *models.PullRequest
return "", err
}
- if err := pr.LoadBaseRepo(); err != nil {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
return "", errors.Wrap(err, "LoadBaseRepo")
}
- commitStatuses, _, err := models.GetLatestCommitStatus(pr.BaseRepo.ID, sha, db.ListOptions{})
+ commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptions{})
if err != nil {
return "", errors.Wrap(err, "GetLatestCommitStatus")
}
- return MergeRequiredContextsCommitStatus(commitStatuses, pr.ProtectedBranch.StatusCheckContexts), nil
+ pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
+ if err != nil {
+ return "", errors.Wrap(err, "LoadProtectedBranch")
+ }
+ var requiredContexts []string
+ if pb != nil {
+ requiredContexts = pb.StatusCheckContexts
+ }
+
+ return MergeRequiredContextsCommitStatus(commitStatuses, requiredContexts), nil
}
diff --git a/services/pull/edits.go b/services/pull/edits.go
new file mode 100644
index 0000000000000..c7550dcb07394
--- /dev/null
+++ b/services/pull/edits.go
@@ -0,0 +1,40 @@
+// Copyright 2022 The Gitea Authors.
+// All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull
+
+import (
+ "context"
+ "errors"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ unit_model "code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
+)
+
+var ErrUserHasNoPermissionForAction = errors.New("user not allowed to do this action")
+
+// SetAllowEdits allow edits from maintainers to PRs
+func SetAllowEdits(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, allow bool) error {
+ if doer == nil || !pr.Issue.IsPoster(doer.ID) {
+ return ErrUserHasNoPermissionForAction
+ }
+
+ if err := pr.LoadHeadRepo(ctx); err != nil {
+ return err
+ }
+
+ permission, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer)
+ if err != nil {
+ return err
+ }
+
+ if !permission.CanWrite(unit_model.TypeCode) {
+ return ErrUserHasNoPermissionForAction
+ }
+
+ pr.AllowMaintainerEdit = allow
+ return issues_model.UpdateAllowEdits(ctx, pr)
+}
diff --git a/services/pull/lfs.go b/services/pull/lfs.go
index fada9b6121fa5..dc4ca006e4915 100644
--- a/services/pull/lfs.go
+++ b/services/pull/lfs.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
@@ -12,14 +11,16 @@ import (
"strconv"
"sync"
- "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/git/pipeline"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
)
// LFSPush pushes lfs objects referred to in new commits in the head repository from the base repository
-func LFSPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string, pr *models.PullRequest) error {
+func LFSPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string, pr *issues_model.PullRequest) error {
// Now we have to implement git lfs push
// git rev-list --objects --filter=blob:limit=1k HEAD --not base
// pass blob shas in to git cat-file --batch-check (possibly unnecessary)
@@ -67,7 +68,7 @@ func LFSPush(ctx context.Context, tmpBasePath, mergeHeadSHA, mergeBaseSHA string
return nil
}
-func createLFSMetaObjectsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pr *models.PullRequest) {
+func createLFSMetaObjectsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pr *issues_model.PullRequest) {
defer wg.Done()
defer catFileBatchReader.Close()
@@ -115,8 +116,8 @@ func createLFSMetaObjectsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg
}
// Then we need to check that this pointer is in the db
- if _, err := models.GetLFSMetaObjectByOid(pr.HeadRepo.ID, pointer.Oid); err != nil {
- if err == models.ErrLFSObjectNotExist {
+ if _, err := git_model.GetLFSMetaObjectByOid(db.DefaultContext, pr.HeadRepo.ID, pointer.Oid); err != nil {
+ if err == git_model.ErrLFSObjectNotExist {
log.Warn("During merge of: %d in %-v, there is a pointer to LFS Oid: %s which although present in the LFS store is not associated with the head repo %-v", pr.Index, pr.BaseRepo, pointer.Oid, pr.HeadRepo)
continue
}
@@ -126,9 +127,9 @@ func createLFSMetaObjectsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg
// OK we have a pointer that is associated with the head repo
// and is actually a file in the LFS
// Therefore it should be associated with the base repo
- meta := &models.LFSMetaObject{Pointer: pointer}
+ meta := &git_model.LFSMetaObject{Pointer: pointer}
meta.RepositoryID = pr.BaseRepoID
- if _, err := models.NewLFSMetaObject(meta); err != nil {
+ if _, err := git_model.NewLFSMetaObject(db.DefaultContext, meta); err != nil {
_ = catFileBatchReader.CloseWithError(err)
break
}
diff --git a/services/pull/main_test.go b/services/pull/main_test.go
index 5471686e72981..2014b192754fa 100644
--- a/services/pull/main_test.go
+++ b/services/pull/main_test.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
diff --git a/services/pull/merge.go b/services/pull/merge.go
index 0c615d93c8516..d0ec943cfa6b8 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
@@ -13,37 +12,152 @@ import (
"os"
"path/filepath"
"regexp"
+ "strconv"
"strings"
"time"
"code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/references"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
asymkey_service "code.gitea.io/gitea/services/asymkey"
issue_service "code.gitea.io/gitea/services/issue"
)
+// GetDefaultMergeMessage returns default message used when merging pull request
+func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle) (message, body string, err error) {
+ if err := pr.LoadHeadRepo(ctx); err != nil {
+ return "", "", err
+ }
+ if err := pr.LoadBaseRepo(ctx); err != nil {
+ return "", "", err
+ }
+ if pr.BaseRepo == nil {
+ return "", "", repo_model.ErrRepoNotExist{ID: pr.BaseRepoID}
+ }
+
+ if err := pr.LoadIssue(ctx); err != nil {
+ return "", "", err
+ }
+
+ isExternalTracker := pr.BaseRepo.UnitEnabled(ctx, unit.TypeExternalTracker)
+ issueReference := "#"
+ if isExternalTracker {
+ issueReference = "!"
+ }
+
+ if mergeStyle != "" {
+ templateFilepath := fmt.Sprintf(".gitea/default_merge_message/%s_TEMPLATE.md", strings.ToUpper(string(mergeStyle)))
+ commit, err := baseGitRepo.GetBranchCommit(pr.BaseRepo.DefaultBranch)
+ if err != nil {
+ return "", "", err
+ }
+ templateContent, err := commit.GetFileContent(templateFilepath, setting.Repository.PullRequest.DefaultMergeMessageSize)
+ if err != nil {
+ if !git.IsErrNotExist(err) {
+ return "", "", err
+ }
+ } else {
+ vars := map[string]string{
+ "BaseRepoOwnerName": pr.BaseRepo.OwnerName,
+ "BaseRepoName": pr.BaseRepo.Name,
+ "BaseBranch": pr.BaseBranch,
+ "HeadRepoOwnerName": "",
+ "HeadRepoName": "",
+ "HeadBranch": pr.HeadBranch,
+ "PullRequestTitle": pr.Issue.Title,
+ "PullRequestDescription": pr.Issue.Content,
+ "PullRequestPosterName": pr.Issue.Poster.Name,
+ "PullRequestIndex": strconv.FormatInt(pr.Index, 10),
+ "PullRequestReference": fmt.Sprintf("%s%d", issueReference, pr.Index),
+ }
+ if pr.HeadRepo != nil {
+ vars["HeadRepoOwnerName"] = pr.HeadRepo.OwnerName
+ vars["HeadRepoName"] = pr.HeadRepo.Name
+ }
+ refs, err := pr.ResolveCrossReferences(ctx)
+ if err == nil {
+ closeIssueIndexes := make([]string, 0, len(refs))
+ closeWord := "close"
+ if len(setting.Repository.PullRequest.CloseKeywords) > 0 {
+ closeWord = setting.Repository.PullRequest.CloseKeywords[0]
+ }
+ for _, ref := range refs {
+ if ref.RefAction == references.XRefActionCloses {
+ closeIssueIndexes = append(closeIssueIndexes, fmt.Sprintf("%s %s%d", closeWord, issueReference, ref.Issue.Index))
+ }
+ }
+ if len(closeIssueIndexes) > 0 {
+ vars["ClosingIssues"] = strings.Join(closeIssueIndexes, ", ")
+ } else {
+ vars["ClosingIssues"] = ""
+ }
+ }
+ message, body = expandDefaultMergeMessage(templateContent, vars)
+ return message, body, nil
+ }
+ }
+
+ // Squash merge has a different from other styles.
+ if mergeStyle == repo_model.MergeStyleSquash {
+ return fmt.Sprintf("%s (%s%d)", pr.Issue.Title, issueReference, pr.Issue.Index), "", nil
+ }
+
+ if pr.BaseRepoID == pr.HeadRepoID {
+ return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), "", nil
+ }
+
+ if pr.HeadRepo == nil {
+ return fmt.Sprintf("Merge pull request '%s' (%s%d) from :%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), "", nil
+ }
+
+ return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), "", nil
+}
+
+func expandDefaultMergeMessage(template string, vars map[string]string) (message, body string) {
+ message = strings.TrimSpace(template)
+ if splits := strings.SplitN(message, "\n", 2); len(splits) == 2 {
+ message = splits[0]
+ body = strings.TrimSpace(splits[1])
+ }
+ mapping := func(s string) string { return vars[s] }
+ return os.Expand(message, mapping), os.Expand(body, mapping)
+}
+
// Merge merges pull request to base repository.
// Caller should check PR is ready to be merged (review and status checks)
-// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
-func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (err error) {
- if err = pr.LoadHeadRepo(); err != nil {
+func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error {
+ if err := pr.LoadHeadRepo(ctx); err != nil {
log.Error("LoadHeadRepo: %v", err)
- return fmt.Errorf("LoadHeadRepo: %v", err)
- } else if err = pr.LoadBaseRepo(); err != nil {
+ return fmt.Errorf("LoadHeadRepo: %w", err)
+ } else if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
- return fmt.Errorf("LoadBaseRepo: %v", err)
+ return fmt.Errorf("LoadBaseRepo: %w", err)
+ }
+
+ pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
+ defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
+
+ // Removing an auto merge pull and ignore if not exist
+ if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) {
+ return err
}
- prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests)
+ prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
return err
@@ -59,7 +173,10 @@ func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, b
go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
}()
- pr.MergedCommitID, err = rawMerge(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message)
+ // Run the merge in the hammer context to prevent cancellation
+ hammerCtx := graceful.GetManager().HammerContext()
+
+ pr.MergedCommitID, err = rawMerge(hammerCtx, pr, doer, mergeStyle, expectedHeadCommitID, message)
if err != nil {
return err
}
@@ -68,62 +185,59 @@ func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, b
pr.Merger = doer
pr.MergerID = doer.ID
- if _, err := pr.SetMerged(); err != nil {
- log.Error("setMerged [%d]: %v", pr.ID, err)
+ if _, err := pr.SetMerged(hammerCtx); err != nil {
+ log.Error("SetMerged [%d]: %v", pr.ID, err)
}
- if err := pr.LoadIssue(); err != nil {
- log.Error("loadIssue [%d]: %v", pr.ID, err)
+ if err := pr.LoadIssue(hammerCtx); err != nil {
+ log.Error("LoadIssue [%d]: %v", pr.ID, err)
}
- if err := pr.Issue.LoadRepo(ctx); err != nil {
- log.Error("loadRepo for issue [%d]: %v", pr.ID, err)
+ if err := pr.Issue.LoadRepo(hammerCtx); err != nil {
+ log.Error("LoadRepo for issue [%d]: %v", pr.ID, err)
}
- if err := pr.Issue.Repo.GetOwner(ctx); err != nil {
- log.Error("GetOwner for issue repo [%d]: %v", pr.ID, err)
+ if err := pr.Issue.Repo.GetOwner(hammerCtx); err != nil {
+ log.Error("GetOwner for PR [%d]: %v", pr.ID, err)
}
- notification.NotifyMergePullRequest(pr, doer)
+ if wasAutoMerged {
+ notification.NotifyAutoMergePullRequest(hammerCtx, doer, pr)
+ } else {
+ notification.NotifyMergePullRequest(hammerCtx, doer, pr)
+ }
// Reset cached commit count
cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true))
// Resolve cross references
- refs, err := pr.ResolveCrossReferences()
+ refs, err := pr.ResolveCrossReferences(hammerCtx)
if err != nil {
log.Error("ResolveCrossReferences: %v", err)
return nil
}
for _, ref := range refs {
- if err = ref.LoadIssue(); err != nil {
+ if err = ref.LoadIssue(hammerCtx); err != nil {
return err
}
- if err = ref.Issue.LoadRepo(ctx); err != nil {
+ if err = ref.Issue.LoadRepo(hammerCtx); err != nil {
return err
}
close := ref.RefAction == references.XRefActionCloses
if close != ref.Issue.IsClosed {
if err = issue_service.ChangeStatus(ref.Issue, doer, close); err != nil {
// Allow ErrDependenciesLeft
- if !models.IsErrDependenciesLeft(err) {
+ if !issues_model.IsErrDependenciesLeft(err) {
return err
}
}
}
}
-
return nil
}
// rawMerge perform the merge operation without changing any pull information in database
-func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) {
- err := git.LoadGitVersion()
- if err != nil {
- log.Error("git.LoadGitVersion: %v", err)
- return "", fmt.Errorf("Unable to get git version: %v", err)
- }
-
+func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) {
// Clone base repo.
tmpBasePath, err := createTemporaryRepo(ctx, pr)
if err != nil {
@@ -131,7 +245,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
return "", err
}
defer func() {
- if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("Merge: RemoveTemporaryPath: %s", err)
}
}()
@@ -141,10 +255,10 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
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)
+ return "", fmt.Errorf("getDiffTree: %w", err)
}
if strings.TrimSpace(trackingCommitID) != expectedHeadCommitID {
return "", models.ErrSHADoesNotMatch{
@@ -160,30 +274,23 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
sparseCheckoutList, err := getDiffTree(ctx, tmpBasePath, baseBranch, trackingBranch)
if err != nil {
log.Error("getDiffTree(%s, %s, %s): %v", tmpBasePath, baseBranch, trackingBranch, err)
- return "", fmt.Errorf("getDiffTree: %v", err)
+ return "", fmt.Errorf("getDiffTree: %w", err)
}
infoPath := filepath.Join(tmpBasePath, ".git", "info")
if err := os.MkdirAll(infoPath, 0o700); err != nil {
log.Error("Unable to create .git/info in %s: %v", tmpBasePath, err)
- return "", fmt.Errorf("Unable to create .git/info in tmpBasePath: %v", err)
+ return "", fmt.Errorf("Unable to create .git/info in tmpBasePath: %w", err)
}
sparseCheckoutListPath := filepath.Join(infoPath, "sparse-checkout")
if err := os.WriteFile(sparseCheckoutListPath, []byte(sparseCheckoutList), 0o600); err != nil {
log.Error("Unable to write .git/info/sparse-checkout file in %s: %v", tmpBasePath, err)
- return "", fmt.Errorf("Unable to write .git/info/sparse-checkout file in tmpBasePath: %v", err)
+ return "", fmt.Errorf("Unable to write .git/info/sparse-checkout file in tmpBasePath: %w", err)
}
- var gitConfigCommand func() *git.Command
- if git.CheckGitVersionAtLeast("1.8.0") == nil {
- gitConfigCommand = func() *git.Command {
- return git.NewCommand(ctx, "config", "--local")
- }
- } else {
- gitConfigCommand = func() *git.Command {
- return git.NewCommand(ctx, "config")
- }
+ gitConfigCommand := func() *git.Command {
+ return git.NewCommand(ctx, "config", "--local")
}
// Switch off LFS process (set required, clean and smudge here also)
@@ -194,7 +301,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
Stderr: &errbuf,
}); err != nil {
log.Error("git config [filter.lfs.process -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("git config [filter.lfs.process -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("git config [filter.lfs.process -> <> ]: %w\n%s\n%s", err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
@@ -206,7 +313,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
Stderr: &errbuf,
}); err != nil {
log.Error("git config [filter.lfs.required -> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("git config [filter.lfs.required -> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("git config [filter.lfs.required -> ]: %w\n%s\n%s", err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
@@ -218,7 +325,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
Stderr: &errbuf,
}); err != nil {
log.Error("git config [filter.lfs.clean -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("git config [filter.lfs.clean -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("git config [filter.lfs.clean -> <> ]: %w\n%s\n%s", err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
@@ -230,7 +337,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
Stderr: &errbuf,
}); err != nil {
log.Error("git config [filter.lfs.smudge -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("git config [filter.lfs.smudge -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("git config [filter.lfs.smudge -> <> ]: %w\n%s\n%s", err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
@@ -242,7 +349,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
Stderr: &errbuf,
}); err != nil {
log.Error("git config [core.sparseCheckout -> true ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("git config [core.sparsecheckout -> true]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("git config [core.sparsecheckout -> true]: %w\n%s\n%s", err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
@@ -255,7 +362,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
Stderr: &errbuf,
}); err != nil {
log.Error("git read-tree HEAD: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("Unable to read base branch in to the index: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("Unable to read base branch in to the index: %w\n%s\n%s", err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
@@ -264,17 +371,15 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
committer := sig
// Determine if we should sign
- signArg := ""
- if git.CheckGitVersionAtLeast("1.7.9") == nil {
- sign, keyID, signer, _ := asymkey_service.SignMerge(ctx, pr, doer, tmpBasePath, "HEAD", trackingBranch)
- if sign {
- signArg = "-S" + keyID
- if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
- committer = signer
- }
- } else if git.CheckGitVersionAtLeast("2.0.0") == nil {
- signArg = "--no-gpg-sign"
+ var signArg git.CmdArg
+ sign, keyID, signer, _ := asymkey_service.SignMerge(ctx, pr, doer, tmpBasePath, "HEAD", trackingBranch)
+ if sign {
+ signArg = git.CmdArg("-S" + keyID)
+ if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
+ committer = signer
}
+ } else {
+ signArg = git.CmdArg("--no-gpg-sign")
}
commitTimeStr := time.Now().Format(time.RFC3339)
@@ -292,7 +397,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
// 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
@@ -308,20 +413,20 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
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,
Stderr: &errbuf,
}); err != nil {
log.Error("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
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,
@@ -341,7 +446,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
if readErr != nil {
// Abandon this attempt to handle the error
log.Error("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
commitSha = strings.TrimSpace(string(commitShaBytes))
ok = true
@@ -351,7 +456,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
if !ok {
log.Error("Unable to determine failing commit sha for this rebase message. Cannot cast as models.ErrRebaseConflicts.")
log.Error("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
log.Debug("RebaseConflict at %s [%s:%s -> %s:%s]: %v\n%s\n%s", commitSha, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
return "", models.ErrRebaseConflicts{
@@ -363,7 +468,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
}
}
log.Error("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
@@ -374,14 +479,14 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
}
// 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,
Stderr: &errbuf,
}); err != nil {
log.Error("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
@@ -392,7 +497,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
} 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 {
@@ -407,19 +512,19 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
}
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
}
- if err = pr.Issue.LoadPoster(); err != nil {
+ if err = pr.Issue.LoadPoster(ctx); err != nil {
log.Error("LoadPoster: %v", err)
- return "", fmt.Errorf("LoadPoster: %v", err)
+ return "", fmt.Errorf("LoadPoster: %w", err)
}
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,
@@ -427,14 +532,17 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
Stderr: &errbuf,
}); err != nil {
log.Error("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())
- 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())
+ return "", fmt.Errorf("git commit [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
} else {
if setting.Repository.PullRequest.AddCoCommitterTrailers && committer.String() != sig.String() {
// 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,
@@ -442,7 +550,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
Stderr: &errbuf,
}); err != nil {
log.Error("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())
- 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())
+ return "", fmt.Errorf("git commit [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
}
outbuf.Reset()
@@ -454,15 +562,15 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
// OK we should cache our current head and origin/headbranch
mergeHeadSHA, err := git.GetFullCommitID(ctx, tmpBasePath, "HEAD")
if err != nil {
- return "", fmt.Errorf("Failed to get full commit id for HEAD: %v", err)
+ return "", fmt.Errorf("Failed to get full commit id for HEAD: %w", err)
}
mergeBaseSHA, err := git.GetFullCommitID(ctx, tmpBasePath, "original_"+baseBranch)
if err != nil {
- return "", fmt.Errorf("Failed to get full commit id for origin/%s: %v", pr.BaseBranch, err)
+ return "", fmt.Errorf("Failed to get full commit id for origin/%s: %w", pr.BaseBranch, err)
}
mergeCommitID, err := git.GetFullCommitID(ctx, tmpBasePath, baseBranch)
if err != nil {
- return "", fmt.Errorf("Failed to get full commit id for the new merge: %v", err)
+ return "", fmt.Errorf("Failed to get full commit id for the new merge: %w", err)
}
// Now it's questionable about where this should go - either after or before the push
@@ -487,7 +595,7 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
headUser = pr.HeadRepo.Owner
}
- env = models.FullPushingEnvironment(
+ env = repo_module.FullPushingEnvironment(
headUser,
doer,
pr.BaseRepo,
@@ -498,12 +606,14 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
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.
+ // TODO: this cause an api call to "/api/internal/hook/post-receive/...",
+ // that prevents us from doint the whole merge in one db transaction
if err := pushCmd.Run(&git.RunOpts{
Env: env,
Dir: tmpBasePath,
@@ -533,10 +643,10 @@ func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User
return mergeCommitID, nil
}
-func commitAndSignNoAuthor(ctx context.Context, pr *models.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,
@@ -544,10 +654,10 @@ func commitAndSignNoAuthor(ctx context.Context, pr *models.PullRequest, message,
Stderr: &errbuf,
}); err != nil {
log.Error("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())
- 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())
+ return fmt.Errorf("git commit [%s:%s -> %s:%s]: %w\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,
@@ -555,13 +665,13 @@ func commitAndSignNoAuthor(ctx context.Context, pr *models.PullRequest, message,
Stderr: &errbuf,
}); err != nil {
log.Error("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())
- 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())
+ return fmt.Errorf("git commit [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
}
return nil
}
-func runMergeCommand(pr *models.PullRequest, mergeStyle repo_model.MergeStyle, cmd *git.Command, tmpBasePath string) error {
+func runMergeCommand(pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle, cmd *git.Command, tmpBasePath string) error {
var outbuf, errbuf strings.Builder
if err := cmd.Run(&git.RunOpts{
Dir: tmpBasePath,
@@ -588,7 +698,7 @@ func runMergeCommand(pr *models.PullRequest, mergeStyle repo_model.MergeStyle, c
}
}
log.Error("git merge [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
- return fmt.Errorf("git merge [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
+ return fmt.Errorf("git merge [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
return nil
@@ -600,7 +710,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,
@@ -645,33 +755,34 @@ func getDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string) (
}
// IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections
-func IsUserAllowedToMerge(pr *models.PullRequest, p models.Permission, user *user_model.User) (bool, error) {
+func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) {
if user == nil {
return false, nil
}
- err := pr.LoadProtectedBranch()
+ pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return false, err
}
- if (p.CanWrite(unit.TypeCode) && pr.ProtectedBranch == nil) || (pr.ProtectedBranch != nil && models.IsUserMergeWhitelisted(pr.ProtectedBranch, user.ID, p)) {
+ if (p.CanWrite(unit.TypeCode) && pb == nil) || (pb != nil && git_model.IsUserMergeWhitelisted(ctx, pb, user.ID, p)) {
return true, nil
}
return false, nil
}
-// CheckPRReadyToMerge checks whether the PR is ready to be merged (reviews and status checks)
-func CheckPRReadyToMerge(ctx context.Context, pr *models.PullRequest, skipProtectedFilesCheck bool) (err error) {
- if err = pr.LoadBaseRepo(); err != nil {
- return fmt.Errorf("LoadBaseRepo: %v", err)
+// CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks)
+func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (err error) {
+ if err = pr.LoadBaseRepo(ctx); err != nil {
+ return fmt.Errorf("LoadBaseRepo: %w", err)
}
- if err = pr.LoadProtectedBranch(); err != nil {
+ pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
+ if err != nil {
return fmt.Errorf("LoadProtectedBranch: %v", err)
}
- if pr.ProtectedBranch == nil {
+ if pb == nil {
return nil
}
@@ -685,23 +796,23 @@ func CheckPRReadyToMerge(ctx context.Context, pr *models.PullRequest, skipProtec
}
}
- if !pr.ProtectedBranch.HasEnoughApprovals(pr) {
+ if !issues_model.HasEnoughApprovals(ctx, pb, pr) {
return models.ErrDisallowedToMerge{
Reason: "Does not have enough approvals",
}
}
- if pr.ProtectedBranch.MergeBlockedByRejectedReview(pr) {
+ if issues_model.MergeBlockedByRejectedReview(ctx, pb, pr) {
return models.ErrDisallowedToMerge{
Reason: "There are requested changes",
}
}
- if pr.ProtectedBranch.MergeBlockedByOfficialReviewRequests(pr) {
+ if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pr) {
return models.ErrDisallowedToMerge{
Reason: "There are official review requests",
}
}
- if pr.ProtectedBranch.MergeBlockedByOutdatedBranch(pr) {
+ if issues_model.MergeBlockedByOutdatedBranch(pb, pr) {
return models.ErrDisallowedToMerge{
Reason: "The head branch is behind the base branch",
}
@@ -711,7 +822,7 @@ func CheckPRReadyToMerge(ctx context.Context, pr *models.PullRequest, skipProtec
return nil
}
- if pr.ProtectedBranch.MergeBlockedByProtectedFiles(pr) {
+ if pb.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) {
return models.ErrDisallowedToMerge{
Reason: "Changed protected files",
}
@@ -721,52 +832,64 @@ func CheckPRReadyToMerge(ctx context.Context, pr *models.PullRequest, skipProtec
}
// MergedManually mark pr as merged manually
-func MergedManually(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, commitID string) (err error) {
- prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests)
- if err != nil {
- return
- }
- prConfig := prUnit.PullRequestsConfig()
+func MergedManually(pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, commitID string) error {
+ pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
+ defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
- // Check if merge style is correct and allowed
- if !prConfig.IsMergeStyleAllowed(repo_model.MergeStyleManuallyMerged) {
- return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: repo_model.MergeStyleManuallyMerged}
- }
+ if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
+ return err
+ }
+ prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
+ if err != nil {
+ return err
+ }
+ prConfig := prUnit.PullRequestsConfig()
- if len(commitID) < 40 {
- return fmt.Errorf("Wrong commit ID")
- }
+ // Check if merge style is correct and allowed
+ if !prConfig.IsMergeStyleAllowed(repo_model.MergeStyleManuallyMerged) {
+ return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: repo_model.MergeStyleManuallyMerged}
+ }
- commit, err := baseGitRepo.GetCommit(commitID)
- if err != nil {
- if git.IsErrNotExist(err) {
+ if len(commitID) < git.SHAFullLength {
return fmt.Errorf("Wrong commit ID")
}
- return
- }
- ok, err := baseGitRepo.IsCommitInBranch(commitID, pr.BaseBranch)
- if err != nil {
- return
- }
- if !ok {
- return fmt.Errorf("Wrong commit ID")
- }
+ commit, err := baseGitRepo.GetCommit(commitID)
+ if err != nil {
+ if git.IsErrNotExist(err) {
+ return fmt.Errorf("Wrong commit ID")
+ }
+ return err
+ }
+ commitID = commit.ID.String()
- pr.MergedCommitID = commitID
- pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix())
- pr.Status = models.PullRequestStatusManuallyMerged
- pr.Merger = doer
- pr.MergerID = doer.ID
+ ok, err := baseGitRepo.IsCommitInBranch(commitID, pr.BaseBranch)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ return fmt.Errorf("Wrong commit ID")
+ }
+
+ pr.MergedCommitID = commitID
+ pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix())
+ pr.Status = issues_model.PullRequestStatusManuallyMerged
+ pr.Merger = doer
+ pr.MergerID = doer.ID
- merged := false
- if merged, err = pr.SetMerged(); err != nil {
- return
- } else if !merged {
- return fmt.Errorf("SetMerged failed")
+ var merged bool
+ if merged, err = pr.SetMerged(ctx); err != nil {
+ return err
+ } else if !merged {
+ return fmt.Errorf("SetMerged failed")
+ }
+ return nil
+ }); err != nil {
+ return err
}
- notification.NotifyMergePullRequest(pr, doer)
- log.Info("manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s", pr.ID, pr.BaseRepo.Name, pr.BaseBranch, commit.ID.String())
+ notification.NotifyMergePullRequest(baseGitRepo.Ctx, doer, pr)
+ log.Info("manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s", pr.ID, pr.BaseRepo.Name, pr.BaseBranch, commitID)
return nil
}
diff --git a/services/pull/merge_test.go b/services/pull/merge_test.go
new file mode 100644
index 0000000000000..6df6f55d46115
--- /dev/null
+++ b/services/pull/merge_test.go
@@ -0,0 +1,67 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_expandDefaultMergeMessage(t *testing.T) {
+ type args struct {
+ template string
+ vars map[string]string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ wantBody string
+ }{
+ {
+ name: "single line",
+ args: args{
+ template: "Merge ${PullRequestTitle}",
+ vars: map[string]string{
+ "PullRequestTitle": "PullRequestTitle",
+ "PullRequestDescription": "Pull\nRequest\nDescription\n",
+ },
+ },
+ want: "Merge PullRequestTitle",
+ wantBody: "",
+ },
+ {
+ name: "multiple lines",
+ args: args{
+ template: "Merge ${PullRequestTitle}\nDescription:\n\n${PullRequestDescription}\n",
+ vars: map[string]string{
+ "PullRequestTitle": "PullRequestTitle",
+ "PullRequestDescription": "Pull\nRequest\nDescription\n",
+ },
+ },
+ want: "Merge PullRequestTitle",
+ wantBody: "Description:\n\nPull\nRequest\nDescription\n",
+ },
+ {
+ name: "leading newlines",
+ args: args{
+ template: "\n\n\nMerge ${PullRequestTitle}\n\n\nDescription:\n\n${PullRequestDescription}\n",
+ vars: map[string]string{
+ "PullRequestTitle": "PullRequestTitle",
+ "PullRequestDescription": "Pull\nRequest\nDescription\n",
+ },
+ },
+ want: "Merge PullRequestTitle",
+ wantBody: "Description:\n\nPull\nRequest\nDescription\n",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, got1 := expandDefaultMergeMessage(tt.args.template, tt.args.vars)
+ assert.Equalf(t, tt.want, got, "expandDefaultMergeMessage(%v, %v)", tt.args.template, tt.args.vars)
+ assert.Equalf(t, tt.wantBody, got1, "expandDefaultMergeMessage(%v, %v)", tt.args.template, tt.args.vars)
+ })
+ }
+}
diff --git a/services/pull/patch.go b/services/pull/patch.go
index f86141aa7aecf..26a72a7371bf1 100644
--- a/services/pull/patch.go
+++ b/services/pull/patch.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
@@ -15,32 +14,37 @@ import (
"strings"
"code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob"
)
// DownloadDiffOrPatch will write the patch for the pr to the writer
-func DownloadDiffOrPatch(ctx context.Context, pr *models.PullRequest, w io.Writer, patch, binary bool) error {
- if err := pr.LoadBaseRepo(); err != nil {
+func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io.Writer, patch, binary bool) error {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to load base repository ID %d for pr #%d [%d]", pr.BaseRepoID, pr.Index, pr.ID)
return err
}
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, pr.BaseRepo.RepoPath())
if err != nil {
- return fmt.Errorf("OpenRepository: %v", err)
+ return fmt.Errorf("OpenRepository: %w", err)
}
defer closer.Close()
if err := gitRepo.GetDiffOrPatch(pr.MergeBase, pr.GetGitRefName(), w, patch, binary); err != nil {
log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
- return fmt.Errorf("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
+ return fmt.Errorf("Unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
}
return nil
}
@@ -50,10 +54,12 @@ var patchErrorSuffices = []string{
": patch does not apply",
": already exists in working directory",
"unrecognized input",
+ ": No such file or directory",
+ ": does not exist in index",
}
// TestPatch will test whether a simple patch will apply
-func TestPatch(pr *models.PullRequest) error {
+func TestPatch(pr *issues_model.PullRequest) error {
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("TestPatch: Repo[%d]#%d", pr.BaseRepoID, pr.Index))
defer finished()
@@ -64,14 +70,14 @@ func TestPatch(pr *models.PullRequest) error {
return err
}
defer func() {
- if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("Merge: RemoveTemporaryPath: %s", err)
}
}()
gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
if err != nil {
- return fmt.Errorf("OpenRepository: %v", err)
+ return fmt.Errorf("OpenRepository: %w", err)
}
defer gitRepo.Close()
@@ -81,18 +87,26 @@ func TestPatch(pr *models.PullRequest) error {
var err2 error
pr.MergeBase, err2 = gitRepo.GetRefCommitID(git.BranchPrefix + "base")
if err2 != nil {
- return fmt.Errorf("GetMergeBase: %v and can't find commit ID for base: %v", err, err2)
+ return fmt.Errorf("GetMergeBase: %v and can't find commit ID for base: %w", err, err2)
}
}
pr.MergeBase = strings.TrimSpace(pr.MergeBase)
+ if pr.HeadCommitID, err = gitRepo.GetRefCommitID(git.BranchPrefix + "tracking"); err != nil {
+ return fmt.Errorf("GetBranchCommitID: can't find commit ID for head: %w", err)
+ }
+
+ if pr.HeadCommitID == pr.MergeBase {
+ pr.Status = issues_model.PullRequestStatusAncestor
+ return nil
+ }
// 2. Check for conflicts
- if conflicts, err := checkConflicts(ctx, pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == models.PullRequestStatusEmpty {
+ if conflicts, err := checkConflicts(ctx, pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == issues_model.PullRequestStatusEmpty {
return err
}
// 3. Check for protected files changes
- if err = checkPullFilesProtection(pr, gitRepo); err != nil {
+ if err = checkPullFilesProtection(ctx, pr, gitRepo); err != nil {
return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err)
}
@@ -100,7 +114,7 @@ func TestPatch(pr *models.PullRequest) error {
log.Trace("Found %d protected files changed", len(pr.ChangedProtectedFiles))
}
- pr.Status = models.PullRequestStatusMergeable
+ pr.Status = issues_model.PullRequestStatusMergeable
return nil
}
@@ -114,6 +128,7 @@ func (e *errMergeConflict) Error() string {
}
func attemptMerge(ctx context.Context, file *unmergedFile, tmpBasePath string, gitRepo *git.Repository) error {
+ log.Trace("Attempt to merge:\n%v", file)
switch {
case file.stage1 != nil && (file.stage2 == nil || file.stage3 == nil):
// 1. Deleted in one or both:
@@ -166,7 +181,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)
}
@@ -175,7 +190,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)
}
@@ -183,7 +198,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)
}
@@ -193,13 +208,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
}
@@ -223,9 +238,9 @@ 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)
+ return false, nil, fmt.Errorf("unable to run read-tree -m! Error: %w", err)
}
// Then we use git ls-files -u to list the unmerged files and collate the triples in unmergedfiles
@@ -269,44 +284,57 @@ func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repo
return conflict, conflictedFiles, nil
}
-func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) {
+func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) {
// 1. checkConflicts resets the conflict status - therefore - reset the conflict status
pr.ConflictedFiles = nil
// 2. AttemptThreeWayMerge first - this is much quicker than plain patch to base
description := fmt.Sprintf("PR[%d] %s/%s#%d", pr.ID, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Index)
- conflict, _, err := AttemptThreeWayMerge(ctx,
+ conflict, conflictFiles, err := AttemptThreeWayMerge(ctx,
tmpBasePath, gitRepo, pr.MergeBase, "base", "tracking", description)
if err != nil {
return false, err
}
if !conflict {
+ // No conflicts detected so we need to check if the patch is empty...
+ // a. Write the newly merged tree and check the new tree-hash
var treeHash string
treeHash, _, err = git.NewCommand(ctx, "write-tree").RunStdString(&git.RunOpts{Dir: tmpBasePath})
if err != nil {
- return false, err
+ lsfiles, _, _ := git.NewCommand(ctx, "ls-files", "-u").RunStdString(&git.RunOpts{Dir: tmpBasePath})
+ return false, fmt.Errorf("unable to write unconflicted tree: %w\n`git ls-files -u`:\n%s", err, lsfiles)
}
treeHash = strings.TrimSpace(treeHash)
baseTree, err := gitRepo.GetTree("base")
if err != nil {
return false, err
}
+
+ // b. compare the new tree-hash with the base tree hash
if treeHash == baseTree.ID.String() {
log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
- pr.Status = models.PullRequestStatusEmpty
+ pr.Status = issues_model.PullRequestStatusEmpty
}
return false, nil
}
- // 3. OK read-tree has failed so we need to try a different thing - this might actually succeed where the above fails due to whitespace handling.
+ // 3. OK the three-way merge method has detected conflicts
+ // 3a. Are still testing with GitApply? If not set the conflict status and move on
+ if !setting.Repository.PullRequest.TestConflictingPatchesWithGitApply {
+ pr.Status = issues_model.PullRequestStatusConflict
+ pr.ConflictedFiles = conflictFiles
+
+ log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
+ return true, nil
+ }
- // 3a. Create a plain patch from head to base
+ // 3b. Create a plain patch from head to base
tmpPatchFile, err := os.CreateTemp("", "patch")
if err != nil {
log.Error("Unable to create temporary patch file! Error: %v", err)
- return false, fmt.Errorf("unable to create temporary patch file! Error: %v", err)
+ return false, fmt.Errorf("unable to create temporary patch file! Error: %w", err)
}
defer func() {
_ = util.Remove(tmpPatchFile.Name())
@@ -315,20 +343,20 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re
if err := gitRepo.GetDiffBinary(pr.MergeBase, "tracking", tmpPatchFile); err != nil {
tmpPatchFile.Close()
log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
- return false, fmt.Errorf("unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
+ return false, fmt.Errorf("unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
}
stat, err := tmpPatchFile.Stat()
if err != nil {
tmpPatchFile.Close()
- return false, fmt.Errorf("unable to stat patch file: %v", err)
+ return false, fmt.Errorf("unable to stat patch file: %w", err)
}
patchPath := tmpPatchFile.Name()
tmpPatchFile.Close()
- // 3b. if the size of that patch is 0 - there can be no conflicts!
+ // 3c. if the size of that patch is 0 - there can be no conflicts!
if stat.Size() == 0 {
log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
- pr.Status = models.PullRequestStatusEmpty
+ pr.Status = issues_model.PullRequestStatusEmpty
return false, nil
}
@@ -337,18 +365,18 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re
// 4. Read the base branch in to the index of the temporary repository
_, _, err = git.NewCommand(gitRepo.Ctx, "read-tree", "base").RunStdString(&git.RunOpts{Dir: tmpBasePath})
if err != nil {
- return false, fmt.Errorf("git read-tree %s: %v", pr.BaseBranch, err)
+ return false, fmt.Errorf("git read-tree %s: %w", pr.BaseBranch, err)
}
// 5. Now get the pull request configuration to check if we need to ignore whitespace
- prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests)
+ prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
return false, err
}
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")
}
@@ -357,7 +385,7 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re
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:
@@ -370,7 +398,7 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re
stderrReader, stderrWriter, err := os.Pipe()
if err != nil {
log.Error("Unable to open stderr pipe: %v", err)
- return false, fmt.Errorf("unable to open stderr pipe: %v", err)
+ return false, fmt.Errorf("unable to open stderr pipe: %w", err)
}
defer func() {
_ = stderrReader.Close()
@@ -397,16 +425,17 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re
const appliedPatchPrefix = "Applied patch to '"
const withConflicts = "' with conflicts."
- conflictMap := map[string]bool{}
+ conflicts := make(container.Set[string])
// Now scan the output from the command
scanner := bufio.NewScanner(stderrReader)
for scanner.Scan() {
line := scanner.Text()
+ log.Trace("PullRequest[%d].testPatch: stderr: %s", pr.ID, line)
if strings.HasPrefix(line, prefix) {
conflict = true
filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0])
- conflictMap[filepath] = true
+ conflicts.Add(filepath)
} else if is3way && line == threewayFailed {
conflict = true
} else if strings.HasPrefix(line, errorPrefix) {
@@ -415,7 +444,7 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re
if strings.HasSuffix(line, suffix) {
filepath := strings.TrimSpace(strings.TrimSuffix(line[len(errorPrefix):], suffix))
if filepath != "" {
- conflictMap[filepath] = true
+ conflicts.Add(filepath)
}
break
}
@@ -424,18 +453,18 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re
conflict = true
filepath := strings.TrimPrefix(strings.TrimSuffix(line, withConflicts), appliedPatchPrefix)
if filepath != "" {
- conflictMap[filepath] = true
+ conflicts.Add(filepath)
}
}
// only list 10 conflicted files
- if len(conflictMap) >= 10 {
+ if len(conflicts) >= 10 {
break
}
}
- if len(conflictMap) > 0 {
- pr.ConflictedFiles = make([]string, 0, len(conflictMap))
- for key := range conflictMap {
+ if len(conflicts) > 0 {
+ pr.ConflictedFiles = make([]string, 0, len(conflicts))
+ for key := range conflicts {
pr.ConflictedFiles = append(pr.ConflictedFiles, key)
}
}
@@ -444,15 +473,17 @@ func checkConflicts(ctx context.Context, pr *models.PullRequest, gitRepo *git.Re
},
})
- // 9. If there is a conflict the `git apply` command will return a non-zero error code - so there will be a positive error.
- if err != nil {
+ // 9. Check if the found conflictedfiles is non-zero, "err" could be non-nil, so we should ignore it if we found conflicts.
+ // Note: `"err" could be non-nil` is due that if enable 3-way merge, it doesn't return any error on found conflicts.
+ if len(pr.ConflictedFiles) > 0 {
if conflict {
- pr.Status = models.PullRequestStatusConflict
+ pr.Status = issues_model.PullRequestStatusConflict
log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
return true, nil
}
- return false, fmt.Errorf("git apply --check: %v", err)
+ } else if err != nil {
+ return false, fmt.Errorf("git apply --check: %w", err)
}
return false, nil
}
@@ -513,23 +544,23 @@ func CheckUnprotectedFiles(repo *git.Repository, oldCommitID, newCommitID string
}
// checkPullFilesProtection check if pr changed protected files and save results
-func checkPullFilesProtection(pr *models.PullRequest, gitRepo *git.Repository) error {
- if pr.Status == models.PullRequestStatusEmpty {
+func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) error {
+ if pr.Status == issues_model.PullRequestStatusEmpty {
pr.ChangedProtectedFiles = nil
return nil
}
- if err := pr.LoadProtectedBranch(); err != nil {
+ pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
+ if err != nil {
return err
}
- if pr.ProtectedBranch == nil {
+ if pb == nil {
pr.ChangedProtectedFiles = nil
return nil
}
- var err error
- pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.MergeBase, "tracking", pr.ProtectedBranch.GetProtectedFilePatterns(), 10, os.Environ())
+ pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.MergeBase, "tracking", pb.GetProtectedFilePatterns(), 10, os.Environ())
if err != nil && !models.IsErrFilePathProtected(err) {
return err
}
diff --git a/services/pull/patch_unmerged.go b/services/pull/patch_unmerged.go
index 38394191429cd..c60c48d92320c 100644
--- a/services/pull/patch_unmerged.go
+++ b/services/pull/patch_unmerged.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
@@ -42,6 +41,17 @@ func (line *lsFileLine) SameAs(other *lsFileLine) bool {
line.path == other.path
}
+// String provides a string representation for logging
+func (line *lsFileLine) String() string {
+ if line == nil {
+ return ""
+ }
+ if line.err != nil {
+ return fmt.Sprintf("%d %s %s %s %v", line.stage, line.mode, line.path, line.sha, line.err)
+ }
+ return fmt.Sprintf("%d %s %s %s", line.stage, line.mode, line.path, line.sha)
+}
+
// readUnmergedLsFileLines calls git ls-files -u -z and parses the lines into mode-sha-stage-path quadruplets
// it will push these to the provided channel closing it at the end
func readUnmergedLsFileLines(ctx context.Context, tmpBasePath string, outputChan chan *lsFileLine) {
@@ -53,7 +63,7 @@ func readUnmergedLsFileLines(ctx context.Context, tmpBasePath string, outputChan
lsFilesReader, lsFilesWriter, err := os.Pipe()
if err != nil {
log.Error("Unable to open stderr pipe: %v", err)
- outputChan <- &lsFileLine{err: fmt.Errorf("unable to open stderr pipe: %v", err)}
+ outputChan <- &lsFileLine{err: fmt.Errorf("unable to open stderr pipe: %w", err)}
return
}
defer func() {
@@ -106,7 +116,7 @@ func readUnmergedLsFileLines(ctx context.Context, tmpBasePath string, outputChan
},
})
if err != nil {
- outputChan <- &lsFileLine{err: fmt.Errorf("git ls-files -u -z: %v", git.ConcatenateError(err, stderr.String()))}
+ outputChan <- &lsFileLine{err: fmt.Errorf("git ls-files -u -z: %w", git.ConcatenateError(err, stderr.String()))}
}
}
@@ -118,6 +128,17 @@ type unmergedFile struct {
err error
}
+// String provides a string representation of the an unmerged file for logging
+func (u *unmergedFile) String() string {
+ if u == nil {
+ return ""
+ }
+ if u.err != nil {
+ return fmt.Sprintf("error: %v\n%v\n%v\n%v", u.err, u.stage1, u.stage2, u.stage3)
+ }
+ return fmt.Sprintf("%v\n%v\n%v", u.stage1, u.stage2, u.stage3)
+}
+
// unmergedFiles will collate the output from readUnstagedLsFileLines in to file triplets and send them
// to the provided channel, closing at the end.
func unmergedFiles(ctx context.Context, tmpBasePath string, unmerged chan *unmergedFile) {
@@ -138,9 +159,10 @@ func unmergedFiles(ctx context.Context, tmpBasePath string, unmerged chan *unmer
next := &unmergedFile{}
for line := range lsFileLineChan {
+ log.Trace("Got line: %v Current State:\n%v", line, next)
if line.err != nil {
log.Error("Unable to run ls-files -u -z! Error: %v", line.err)
- unmerged <- &unmergedFile{err: fmt.Errorf("unable to run ls-files -u -z! Error: %v", line.err)}
+ unmerged <- &unmergedFile{err: fmt.Errorf("unable to run ls-files -u -z! Error: %w", line.err)}
return
}
@@ -149,7 +171,7 @@ func unmergedFiles(ctx context.Context, tmpBasePath string, unmerged chan *unmer
case 0:
// Should not happen as this represents successfully merged file - we will tolerate and ignore though
case 1:
- if next.stage1 != nil {
+ if next.stage1 != nil || next.stage2 != nil || next.stage3 != nil {
// We need to handle the unstaged file stage1,stage2,stage3
unmerged <- next
}
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 0537964b9de70..08f70a5e4ef02 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
@@ -16,20 +15,28 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/process"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/sync"
issue_service "code.gitea.io/gitea/services/issue"
)
+// TODO: use clustered lock (unique queue? or *abuse* cache)
+var pullWorkingPool = sync.NewExclusivePool()
+
// NewPullRequest creates new pull request with labels for repository.
-func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *models.Issue, labelIDs []int64, uuids []string, pr *models.PullRequest, assigneeIDs []int64) error {
+func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issues_model.Issue, labelIDs []int64, uuids []string, pr *issues_model.PullRequest, assigneeIDs []int64) error {
if err := TestPatch(pr); err != nil {
return err
}
@@ -41,7 +48,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
pr.CommitsAhead = divergence.Ahead
pr.CommitsBehind = divergence.Behind
- if err := models.NewPullRequest(ctx, repo, pull, labelIDs, uuids, pr); err != nil {
+ if err := issues_model.NewPullRequest(ctx, repo, pull, labelIDs, uuids, pr); err != nil {
return err
}
@@ -60,7 +67,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
prCtx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("NewPullRequest: %s:%d", repo.FullName(), pr.Index))
defer finished()
- if pr.Flow == models.PullRequestFlowGithub {
+ if pr.Flow == issues_model.PullRequestFlowGithub {
err = PushToBaseRepo(prCtx, pr)
} else {
err = UpdateRef(prCtx, pr)
@@ -69,17 +76,17 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
return err
}
- mentions, err := models.FindAndUpdateIssueMentions(ctx, pull, pull.Poster, pull.Content)
+ mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, pull, pull.Poster, pull.Content)
if err != nil {
return err
}
- notification.NotifyNewPullRequest(pr, mentions)
+ notification.NotifyNewPullRequest(prCtx, pr, mentions)
if len(pull.Labels) > 0 {
- notification.NotifyIssueChangeLabels(pull.Poster, pull, pull.Labels, nil)
+ notification.NotifyIssueChangeLabels(prCtx, pull.Poster, pull, pull.Labels, nil)
}
if pull.Milestone != nil {
- notification.NotifyIssueChangeMilestone(pull.Poster, pull, 0)
+ notification.NotifyIssueChangeMilestone(prCtx, pull.Poster, pull, 0)
}
// add first push codes comment
@@ -96,7 +103,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
}
if len(compareInfo.Commits) > 0 {
- data := models.PushActionContent{IsForcePush: false}
+ data := issues_model.PushActionContent{IsForcePush: false}
data.CommitIDs = make([]string, 0, len(compareInfo.Commits))
for i := len(compareInfo.Commits) - 1; i >= 0; i-- {
data.CommitIDs = append(data.CommitIDs, compareInfo.Commits[i].ID.String())
@@ -107,8 +114,8 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
return err
}
- ops := &models.CreateCommentOptions{
- Type: models.CommentTypePullRequestPush,
+ ops := &issues_model.CreateCommentOptions{
+ Type: issues_model.CommentTypePullRequestPush,
Doer: pull.Poster,
Repo: repo,
Issue: pr.Issue,
@@ -116,21 +123,24 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *mode
Content: string(dataJSON),
}
- _, _ = models.CreateComment(ops)
+ _, _ = issue_service.CreateComment(ops)
}
return nil
}
// ChangeTargetBranch changes the target branch of this pull request, as the given user.
-func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_model.User, targetBranch string) (err error) {
+func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, targetBranch string) (err error) {
+ pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
+ defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
+
// Current target branch is already the same
if pr.BaseBranch == targetBranch {
return nil
}
if pr.Issue.IsClosed {
- return models.ErrIssueIsClosed{
+ return issues_model.ErrIssueIsClosed{
ID: pr.Issue.ID,
RepoID: pr.Issue.RepoID,
Index: pr.Issue.Index,
@@ -161,9 +171,9 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_
}
// Check if pull request for the new target branch already exists
- existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, models.PullRequestFlowGithub)
+ existingPr, err := issues_model.GetUnmergedPullRequest(ctx, pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, issues_model.PullRequestFlowGithub)
if existingPr != nil {
- return models.ErrPullRequestAlreadyExists{
+ return issues_model.ErrPullRequestAlreadyExists{
ID: existingPr.ID,
IssueID: existingPr.Index,
HeadRepoID: existingPr.HeadRepoID,
@@ -172,7 +182,7 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_
BaseBranch: existingPr.BaseBranch,
}
}
- if err != nil && !models.IsErrPullRequestNotExist(err) {
+ if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
return err
}
@@ -187,8 +197,8 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_
// Update target branch, PR diff and status
// This is the same as checkAndUpdateStatus in check service, but also updates base_branch
- if pr.Status == models.PullRequestStatusChecking {
- pr.Status = models.PullRequestStatusMergeable
+ if pr.Status == issues_model.PullRequestStatusChecking {
+ pr.Status = issues_model.PullRequestStatusMergeable
}
// Update Commit Divergence
@@ -199,38 +209,38 @@ func ChangeTargetBranch(ctx context.Context, pr *models.PullRequest, doer *user_
pr.CommitsAhead = divergence.Ahead
pr.CommitsBehind = divergence.Behind
- if err := pr.UpdateColsIfNotMerged("merge_base", "status", "conflicted_files", "changed_protected_files", "base_branch", "commits_ahead", "commits_behind"); err != nil {
+ if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files", "base_branch", "commits_ahead", "commits_behind"); err != nil {
return err
}
// Create comment
- options := &models.CreateCommentOptions{
- Type: models.CommentTypeChangeTargetBranch,
+ options := &issues_model.CreateCommentOptions{
+ Type: issues_model.CommentTypeChangeTargetBranch,
Doer: doer,
Repo: pr.Issue.Repo,
Issue: pr.Issue,
OldRef: oldBranch,
NewRef: targetBranch,
}
- if _, err = models.CreateComment(options); err != nil {
- return fmt.Errorf("CreateChangeTargetBranchComment: %v", err)
+ if _, err = issue_service.CreateComment(options); err != nil {
+ return fmt.Errorf("CreateChangeTargetBranchComment: %w", err)
}
return nil
}
-func checkForInvalidation(ctx context.Context, requests models.PullRequestList, repoID int64, doer *user_model.User, branch string) error {
- repo, err := repo_model.GetRepositoryByID(repoID)
+func checkForInvalidation(ctx context.Context, requests issues_model.PullRequestList, repoID int64, doer *user_model.User, branch string) error {
+ repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
- return fmt.Errorf("GetRepositoryByID: %v", err)
+ return fmt.Errorf("GetRepositoryByIDCtx: %w", err)
}
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
- return fmt.Errorf("git.OpenRepository: %v", err)
+ return fmt.Errorf("git.OpenRepository: %w", err)
}
go func() {
// FIXME: graceful: We need to tell the manager we're doing something...
- err := requests.InvalidateCodeComments(doer, gitRepo, branch)
+ err := InvalidateCodeComments(ctx, requests, doer, gitRepo, branch)
if err != nil {
log.Error("PullRequestList.InvalidateCodeComments: %v", err)
}
@@ -246,16 +256,16 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
// There is no sensible way to shut this down ":-("
// If you don't let it run all the way then you will lose data
- // FIXME: graceful: AddTestPullRequestTask needs to become a queue!
+ // TODO: graceful: AddTestPullRequestTask needs to become a queue!
- prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
+ prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
if err != nil {
log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
return
}
if isSync {
- requests := models.PullRequestList(prs)
+ requests := issues_model.PullRequestList(prs)
if err = requests.LoadAttributes(); err != nil {
log.Error("PullRequestList.LoadAttributes: %v", err)
}
@@ -271,18 +281,18 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
}
if changed {
// Mark old reviews as stale if diff to mergebase has changed
- if err := models.MarkReviewsAsStale(pr.IssueID); err != nil {
+ if err := issues_model.MarkReviewsAsStale(pr.IssueID); err != nil {
log.Error("MarkReviewsAsStale: %v", err)
}
}
- if err := models.MarkReviewsAsNotStale(pr.IssueID, newCommitID); err != nil {
+ if err := issues_model.MarkReviewsAsNotStale(pr.IssueID, newCommitID); err != nil {
log.Error("MarkReviewsAsNotStale: %v", err)
}
divergence, err := GetDiverging(ctx, pr)
if err != nil {
log.Error("GetDiverging: %v", err)
} else {
- err = pr.UpdateCommitDivergence(divergence.Ahead, divergence.Behind)
+ err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
if err != nil {
log.Error("UpdateCommitDivergence: %v", err)
}
@@ -290,14 +300,14 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
}
pr.Issue.PullRequest = pr
- notification.NotifyPullRequestSynchronized(doer, pr)
+ notification.NotifyPullRequestSynchronized(ctx, doer, pr)
}
}
}
for _, pr := range prs {
log.Trace("Updating PR[%d]: composing new test task", pr.ID)
- if pr.Flow == models.PullRequestFlowGithub {
+ if pr.Flow == issues_model.PullRequestFlowGithub {
if err := PushToBaseRepo(ctx, pr); err != nil {
log.Error("PushToBaseRepo: %v", err)
continue
@@ -307,14 +317,14 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
}
AddToTaskQueue(pr)
- comment, err := models.CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID)
+ comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID)
if err == nil && comment != nil {
- notification.NotifyPullRequestPushCommits(doer, pr, comment)
+ notification.NotifyPullRequestPushCommits(ctx, doer, pr, comment)
}
}
log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch)
- prs, err = models.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
+ prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
if err != nil {
log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
return
@@ -328,7 +338,7 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
log.Error("GetDiverging: %v", err)
}
} else {
- err = pr.UpdateCommitDivergence(divergence.Ahead, divergence.Behind)
+ err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
if err != nil {
log.Error("UpdateCommitDivergence: %v", err)
}
@@ -340,28 +350,28 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
// checkIfPRContentChanged checks if diff to target branch has changed by push
// A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
-func checkIfPRContentChanged(ctx context.Context, pr *models.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
- if err = pr.LoadHeadRepo(); err != nil {
- return false, fmt.Errorf("LoadHeadRepo: %v", err)
+func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
+ if err = pr.LoadHeadRepo(ctx); err != nil {
+ return false, fmt.Errorf("LoadHeadRepo: %w", err)
} else if pr.HeadRepo == nil {
// corrupt data assumed changed
return true, nil
}
- if err = pr.LoadBaseRepo(); err != nil {
- return false, fmt.Errorf("LoadBaseRepo: %v", err)
+ if err = pr.LoadBaseRepo(ctx); err != nil {
+ return false, fmt.Errorf("LoadBaseRepo: %w", err)
}
headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
if err != nil {
- return false, fmt.Errorf("OpenRepository: %v", err)
+ return false, fmt.Errorf("OpenRepository: %w", err)
}
defer headGitRepo.Close()
// Add a temporary remote.
tmpRemote := "checkIfPRContentChanged-" + fmt.Sprint(time.Now().UnixNano())
if err = headGitRepo.AddRemote(tmpRemote, pr.BaseRepo.RepoPath(), true); err != nil {
- return false, fmt.Errorf("AddRemote: %s/%s-%s: %v", pr.HeadRepo.OwnerName, pr.HeadRepo.Name, tmpRemote, err)
+ return false, fmt.Errorf("AddRemote: %s/%s-%s: %w", pr.HeadRepo.OwnerName, pr.HeadRepo.Name, tmpRemote, err)
}
defer func() {
if err := headGitRepo.RemoveRemote(tmpRemote); err != nil {
@@ -371,7 +381,7 @@ func checkIfPRContentChanged(ctx context.Context, pr *models.PullRequest, oldCom
// To synchronize repo and get a base ref
_, base, err := headGitRepo.GetMergeBase(tmpRemote, pr.BaseBranch, pr.HeadBranch)
if err != nil {
- return false, fmt.Errorf("GetMergeBase: %v", err)
+ return false, fmt.Errorf("GetMergeBase: %w", err)
}
diffBefore := &bytes.Buffer{}
@@ -383,7 +393,7 @@ func checkIfPRContentChanged(ctx context.Context, pr *models.PullRequest, oldCom
}
if err := headGitRepo.GetDiffFromMergeBase(base, newCommitID, diffAfter); err != nil {
// New commit should be found
- return false, fmt.Errorf("GetDiffFromMergeBase: %v", err)
+ return false, fmt.Errorf("GetDiffFromMergeBase: %w", err)
}
diffBeforeLines := bufio.NewScanner(diffBefore)
@@ -412,30 +422,30 @@ func checkIfPRContentChanged(ctx context.Context, pr *models.PullRequest, oldCom
// PushToBaseRepo pushes commits from branches of head repository to
// corresponding branches of base repository.
// FIXME: Only push branches that are actually updates?
-func PushToBaseRepo(ctx context.Context, pr *models.PullRequest) (err error) {
+func PushToBaseRepo(ctx context.Context, pr *issues_model.PullRequest) (err error) {
return pushToBaseRepoHelper(ctx, pr, "")
}
-func pushToBaseRepoHelper(ctx context.Context, pr *models.PullRequest, prefixHeadBranch string) (err error) {
+func pushToBaseRepoHelper(ctx context.Context, pr *issues_model.PullRequest, prefixHeadBranch string) (err error) {
log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitRefName())
- if err := pr.LoadHeadRepo(); err != nil {
+ if err := pr.LoadHeadRepo(ctx); err != nil {
log.Error("Unable to load head repository for PR[%d] Error: %v", pr.ID, err)
return err
}
headRepoPath := pr.HeadRepo.RepoPath()
- if err := pr.LoadBaseRepo(); err != nil {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
return err
}
baseRepoPath := pr.BaseRepo.RepoPath()
- if err = pr.LoadIssue(); err != nil {
- return fmt.Errorf("unable to load issue %d for pr %d: %v", pr.IssueID, pr.ID, err)
+ if err = pr.LoadIssue(ctx); err != nil {
+ return fmt.Errorf("unable to load issue %d for pr %d: %w", pr.IssueID, pr.ID, err)
}
- if err = pr.Issue.LoadPoster(); err != nil {
- return fmt.Errorf("unable to load poster %d for pr %d: %v", pr.Issue.PosterID, pr.ID, err)
+ if err = pr.Issue.LoadPoster(ctx); err != nil {
+ return fmt.Errorf("unable to load poster %d for pr %d: %w", pr.Issue.PosterID, pr.ID, err)
}
gitRefName := pr.GetGitRefName()
@@ -445,7 +455,7 @@ func pushToBaseRepoHelper(ctx context.Context, pr *models.PullRequest, prefixHea
Branch: prefixHeadBranch + pr.HeadBranch + ":" + gitRefName,
Force: true,
// Use InternalPushingEnvironment here because we know that pre-receive and post-receive do not run on a refs/pulls/...
- Env: models.InternalPushingEnvironment(pr.Issue.Poster, pr.BaseRepo),
+ Env: repo_module.InternalPushingEnvironment(pr.Issue.Poster, pr.BaseRepo),
}); err != nil {
if git.IsErrPushOutOfDate(err) {
// This should not happen as we're using force!
@@ -465,21 +475,21 @@ func pushToBaseRepoHelper(ctx context.Context, pr *models.PullRequest, prefixHea
return err
}
log.Error("Unable to push PR head for %s#%d (%-v:%s) due to Error: %v", pr.BaseRepo.FullName(), pr.Index, pr.BaseRepo, gitRefName, err)
- return fmt.Errorf("Push: %s:%s %s:%s %v", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), gitRefName, err)
+ return fmt.Errorf("Push: %s:%s %s:%s %w", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), gitRefName, err)
}
return nil
}
// UpdateRef update refs/pull/id/head directly for agit flow pull request
-func UpdateRef(ctx context.Context, pr *models.PullRequest) (err error) {
+func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) {
log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitRefName())
- if err := pr.LoadBaseRepo(); err != nil {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
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)
}
@@ -505,24 +515,24 @@ func (errs errlist) Error() string {
// CloseBranchPulls close all the pull requests who's head branch is the branch
func CloseBranchPulls(doer *user_model.User, repoID int64, branch string) error {
- prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
+ prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repoID, branch)
if err != nil {
return err
}
- prs2, err := models.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
+ prs2, err := issues_model.GetUnmergedPullRequestsByBaseInfo(repoID, branch)
if err != nil {
return err
}
prs = append(prs, prs2...)
- if err := models.PullRequestList(prs).LoadAttributes(); err != nil {
+ if err := issues_model.PullRequestList(prs).LoadAttributes(); err != nil {
return err
}
var errs errlist
for _, pr := range prs {
- if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !models.IsErrPullWasClosed(err) && !models.IsErrDependenciesLeft(err) {
+ if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !issues_model.IsErrPullWasClosed(err) && !issues_model.IsErrDependenciesLeft(err) {
errs = append(errs, err)
}
}
@@ -541,12 +551,12 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
var errs errlist
for _, branch := range branches {
- prs, err := models.GetUnmergedPullRequestsByHeadInfo(repo.ID, branch.Name)
+ prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(repo.ID, branch.Name)
if err != nil {
return err
}
- if err = models.PullRequestList(prs).LoadAttributes(); err != nil {
+ if err = issues_model.PullRequestList(prs).LoadAttributes(); err != nil {
return err
}
@@ -556,7 +566,7 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
if pr.BaseRepoID == repo.ID {
continue
}
- if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !models.IsErrPullWasClosed(err) {
+ if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !issues_model.IsErrPullWasClosed(err) {
errs = append(errs, err)
}
}
@@ -571,22 +581,22 @@ func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *re
var commitMessageTrailersPattern = regexp.MustCompile(`(?:^|\n\n)(?:[\w-]+[ \t]*:[^\n]+\n*(?:[ \t]+[^\n]+\n*)*)+$`)
// GetSquashMergeCommitMessages returns the commit messages between head and merge base (if there is one)
-func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) string {
- if err := pr.LoadIssue(); err != nil {
+func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequest) string {
+ if err := pr.LoadIssue(ctx); err != nil {
log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
return ""
}
- if err := pr.Issue.LoadPoster(); err != nil {
+ if err := pr.Issue.LoadPoster(ctx); err != nil {
log.Error("Cannot load poster %d for pr id %d, index %d Error: %v", pr.Issue.PosterID, pr.ID, pr.Index, err)
return ""
}
if pr.HeadRepo == nil {
var err error
- pr.HeadRepo, err = repo_model.GetRepositoryByID(pr.HeadRepoID)
+ pr.HeadRepo, err = repo_model.GetRepositoryByID(ctx, pr.HeadRepoID)
if err != nil {
- log.Error("GetRepositoryById[%d]: %v", pr.HeadRepoID, err)
+ log.Error("GetRepositoryByIdCtx[%d]: %v", pr.HeadRepoID, err)
return ""
}
}
@@ -599,7 +609,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) s
defer closer.Close()
var headCommit *git.Commit
- if pr.Flow == models.PullRequestFlowGithub {
+ if pr.Flow == issues_model.PullRequestFlowGithub {
headCommit, err = gitRepo.GetBranchCommit(pr.HeadBranch)
} else {
pr.HeadCommitID, err = gitRepo.GetRefCommitID(pr.GetGitRefName())
@@ -630,7 +640,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) s
posterSig := pr.Issue.Poster.NewGitSig().String()
- authorsMap := map[string]bool{}
+ uniqueAuthors := make(container.Set[string])
authors := make([]string, 0, len(commits))
stringBuilder := strings.Builder{}
@@ -677,9 +687,8 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) s
}
authorString := commit.Author.String()
- if !authorsMap[authorString] && authorString != posterSig {
+ if uniqueAuthors.Add(authorString) && authorString != posterSig {
authors = append(authors, authorString)
- authorsMap[authorString] = true
}
}
@@ -699,9 +708,8 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) s
}
for _, commit := range commits {
authorString := commit.Author.String()
- if !authorsMap[authorString] && authorString != posterSig {
+ if uniqueAuthors.Add(authorString) && authorString != posterSig {
authors = append(authors, authorString)
- authorsMap[authorString] = true
}
}
skip += limit
@@ -726,18 +734,25 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *models.PullRequest) s
return stringBuilder.String()
}
-// GetIssuesLastCommitStatus returns a map
-func GetIssuesLastCommitStatus(ctx context.Context, issues models.IssueList) (map[int64]*models.CommitStatus, error) {
- if err := issues.LoadPullRequests(); err != nil {
- return nil, err
+// GetIssuesLastCommitStatus returns a map of issue ID to the most recent commit's latest status
+func GetIssuesLastCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64]*git_model.CommitStatus, error) {
+ _, lastStatus, err := GetIssuesAllCommitStatus(ctx, issues)
+ return lastStatus, err
+}
+
+// GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status
+func GetIssuesAllCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64][]*git_model.CommitStatus, map[int64]*git_model.CommitStatus, error) {
+ if err := issues.LoadPullRequests(ctx); err != nil {
+ return nil, nil, err
}
- if _, err := issues.LoadRepositories(); err != nil {
- return nil, err
+ if _, err := issues.LoadRepositories(ctx); err != nil {
+ return nil, nil, err
}
var (
gitRepos = make(map[int64]*git.Repository)
- res = make(map[int64]*models.CommitStatus)
+ res = make(map[int64][]*git_model.CommitStatus)
+ lastRes = make(map[int64]*git_model.CommitStatus)
err error
)
defer func() {
@@ -760,34 +775,33 @@ func GetIssuesLastCommitStatus(ctx context.Context, issues models.IssueList) (ma
gitRepos[issue.RepoID] = gitRepo
}
- status, err := getLastCommitStatus(gitRepo, issue.PullRequest)
+ statuses, lastStatus, err := getAllCommitStatus(gitRepo, issue.PullRequest)
if err != nil {
- log.Error("getLastCommitStatus: cant get last commit of pull [%d]: %v", issue.PullRequest.ID, err)
+ log.Error("getAllCommitStatus: cant get commit statuses of pull [%d]: %v", issue.PullRequest.ID, err)
continue
}
- res[issue.PullRequest.ID] = status
+ res[issue.PullRequest.ID] = statuses
+ lastRes[issue.PullRequest.ID] = lastStatus
}
- return res, nil
+ return res, lastRes, nil
}
-// getLastCommitStatus get pr's last commit status. PR's last commit status is the head commit id's last commit status
-func getLastCommitStatus(gitRepo *git.Repository, pr *models.PullRequest) (status *models.CommitStatus, err error) {
- sha, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
- if err != nil {
- return nil, err
+// getAllCommitStatus get pr's commit statuses.
+func getAllCommitStatus(gitRepo *git.Repository, pr *issues_model.PullRequest) (statuses []*git_model.CommitStatus, lastStatus *git_model.CommitStatus, err error) {
+ sha, shaErr := gitRepo.GetRefCommitID(pr.GetGitRefName())
+ if shaErr != nil {
+ return nil, nil, shaErr
}
- statusList, _, err := models.GetLatestCommitStatus(pr.BaseRepo.ID, sha, db.ListOptions{})
- if err != nil {
- return nil, err
- }
- return models.CalcCommitStatus(statusList), nil
+ statuses, _, err = git_model.GetLatestCommitStatus(db.DefaultContext, pr.BaseRepo.ID, sha, db.ListOptions{})
+ lastStatus = git_model.CalcCommitStatus(statuses)
+ return statuses, lastStatus, err
}
// IsHeadEqualWithBranch returns if the commits of branchName are available in pull request head
-func IsHeadEqualWithBranch(ctx context.Context, pr *models.PullRequest, branchName string) (bool, error) {
+func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, branchName string) (bool, error) {
var err error
- if err = pr.LoadBaseRepo(); err != nil {
+ if err = pr.LoadBaseRepo(ctx); err != nil {
return false, err
}
baseGitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, pr.BaseRepo.RepoPath())
@@ -801,7 +815,7 @@ func IsHeadEqualWithBranch(ctx context.Context, pr *models.PullRequest, branchNa
return false, err
}
- if err = pr.LoadHeadRepo(); err != nil {
+ if err = pr.LoadHeadRepo(ctx); err != nil {
return false, err
}
var headGitRepo *git.Repository
@@ -818,7 +832,7 @@ func IsHeadEqualWithBranch(ctx context.Context, pr *models.PullRequest, branchNa
}
var headCommit *git.Commit
- if pr.Flow == models.PullRequestFlowGithub {
+ if pr.Flow == issues_model.PullRequestFlowGithub {
headCommit, err = headGitRepo.GetBranchCommit(pr.HeadBranch)
if err != nil {
return false, err
diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go
index 81627ebb77bcf..d63227a7d5e9c 100644
--- a/services/pull/pull_test.go
+++ b/services/pull/pull_test.go
@@ -1,13 +1,19 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
import (
"testing"
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/git"
+
"github.com/stretchr/testify/assert"
)
@@ -29,3 +35,57 @@ func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) {
assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob "))
assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value"))
}
+
+func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
+
+ assert.NoError(t, pr.LoadBaseRepo(db.DefaultContext))
+ gitRepo, err := git.OpenRepository(git.DefaultContext, pr.BaseRepo.RepoPath())
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+
+ mergeMessage, _, err := GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
+ assert.NoError(t, err)
+ assert.Equal(t, "Merge pull request 'issue3' (#3) from branch2 into master", mergeMessage)
+
+ pr.BaseRepoID = 1
+ pr.HeadRepoID = 2
+ mergeMessage, _, err = GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
+ assert.NoError(t, err)
+ assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo1:branch2 into master", mergeMessage)
+}
+
+func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ externalTracker := repo_model.RepoUnit{
+ Type: unit.TypeExternalTracker,
+ Config: &repo_model.ExternalTrackerConfig{
+ ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}",
+ },
+ }
+ baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ baseRepo.Units = []*repo_model.RepoUnit{&externalTracker}
+
+ pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2, BaseRepo: baseRepo})
+
+ assert.NoError(t, pr.LoadBaseRepo(db.DefaultContext))
+ gitRepo, err := git.OpenRepository(git.DefaultContext, pr.BaseRepo.RepoPath())
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+
+ mergeMessage, _, err := GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
+ assert.NoError(t, err)
+
+ assert.Equal(t, "Merge pull request 'issue3' (!3) from branch2 into master", mergeMessage)
+
+ pr.BaseRepoID = 1
+ pr.HeadRepoID = 2
+ pr.BaseRepo = nil
+ pr.HeadRepo = nil
+ mergeMessage, _, err = GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
+ assert.NoError(t, err)
+
+ assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo2:branch2 into master", mergeMessage)
+}
diff --git a/services/pull/review.go b/services/pull/review.go
index e7e6f3135ba91..ca386ca6b027f 100644
--- a/services/pull/review.go
+++ b/services/pull/review.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
@@ -12,18 +11,67 @@ import (
"regexp"
"strings"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ issue_service "code.gitea.io/gitea/services/issue"
)
+var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`)
+
+// checkInvalidation checks if the line of code comment got changed by another commit.
+// If the line got changed the comment is going to be invalidated.
+func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error {
+ // FIXME differentiate between previous and proposed line
+ commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine()))
+ if err != nil && (strings.Contains(err.Error(), "fatal: no such path") || notEnoughLines.MatchString(err.Error())) {
+ c.Invalidated = true
+ return issues_model.UpdateCommentInvalidate(ctx, c)
+ }
+ if err != nil {
+ return err
+ }
+ if c.CommitSHA != "" && c.CommitSHA != commit.ID.String() {
+ c.Invalidated = true
+ return issues_model.UpdateCommentInvalidate(ctx, c)
+ }
+ return nil
+}
+
+// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change
+func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestList, doer *user_model.User, repo *git.Repository, branch string) error {
+ if len(prs) == 0 {
+ return nil
+ }
+ issueIDs := prs.GetIssueIDs()
+ var codeComments []*issues_model.Comment
+
+ if err := db.Find(ctx, &issues_model.FindCommentsOptions{
+ ListOptions: db.ListOptions{
+ ListAll: true,
+ },
+ Type: issues_model.CommentTypeCode,
+ Invalidated: util.OptionalBoolFalse,
+ IssueIDs: issueIDs,
+ }, &codeComments); err != nil {
+ return fmt.Errorf("find code comments: %v", err)
+ }
+ for _, comment := range codeComments {
+ if err := checkInvalidation(ctx, comment, doer, repo, branch); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// CreateCodeComment creates a comment on the code line
-func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *models.Issue, line int64, content, treePath string, isReview bool, replyReviewID int64, latestCommitID string) (*models.Comment, error) {
+func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, isReview bool, replyReviewID int64, latestCommitID string) (*issues_model.Comment, error) {
var (
existsReview bool
err error
@@ -37,7 +85,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
if !isReview && replyReviewID != 0 {
// It's not part of a review; maybe a reply to a review comment or a single comment.
// Check if there are reviews for that line already; if there are, this is a reply
- if existsReview, err = models.ReviewExists(issue, treePath, line); err != nil {
+ if existsReview, err = issues_model.ReviewExists(issue, treePath, line); err != nil {
return nil, err
}
}
@@ -61,24 +109,24 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
return nil, err
}
- mentions, err := models.FindAndUpdateIssueMentions(ctx, issue, doer, comment.Content)
+ mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comment.Content)
if err != nil {
return nil, err
}
- notification.NotifyCreateIssueComment(doer, issue.Repo, issue, comment, mentions)
+ notification.NotifyCreateIssueComment(ctx, doer, issue.Repo, issue, comment, mentions)
return comment, nil
}
- review, err := models.GetCurrentReview(doer, issue)
+ review, err := issues_model.GetCurrentReview(ctx, doer, issue)
if err != nil {
- if !models.IsErrReviewNotExist(err) {
+ if !issues_model.IsErrReviewNotExist(err) {
return nil, err
}
- if review, err = models.CreateReview(models.CreateReviewOptions{
- Type: models.ReviewTypePending,
+ if review, err = issues_model.CreateReview(ctx, issues_model.CreateReviewOptions{
+ Type: issues_model.ReviewTypePending,
Reviewer: doer,
Issue: issue,
Official: false,
@@ -103,7 +151,7 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
if !isReview && !existsReview {
// Submit the review we've just created so the comment shows up in the issue view
- if _, _, err = SubmitReview(ctx, doer, gitRepo, issue, models.ReviewTypeComment, "", latestCommitID, nil); err != nil {
+ if _, _, err = SubmitReview(ctx, doer, gitRepo, issue, issues_model.ReviewTypeComment, "", latestCommitID, nil); err != nil {
return nil, err
}
}
@@ -113,21 +161,19 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
return comment, nil
}
-var notEnoughLines = regexp.MustCompile(`exit status 128 - fatal: file .* has only \d+ lines?`)
-
// createCodeComment creates a plain code comment at the specified line / path
-func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *models.Issue, content, treePath string, line, reviewID int64) (*models.Comment, error) {
+func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, treePath string, line, reviewID int64) (*issues_model.Comment, error) {
var commitID, patch string
- if err := issue.LoadPullRequest(); err != nil {
- return nil, fmt.Errorf("GetPullRequestByIssueID: %v", err)
+ if err := issue.LoadPullRequest(ctx); err != nil {
+ return nil, fmt.Errorf("LoadPullRequest: %w", err)
}
pr := issue.PullRequest
- if err := pr.LoadBaseRepo(); err != nil {
- return nil, fmt.Errorf("LoadHeadRepo: %v", err)
+ if err := pr.LoadBaseRepo(ctx); err != nil {
+ return nil, fmt.Errorf("LoadBaseRepo: %w", err)
}
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, pr.BaseRepo.RepoPath())
if err != nil {
- return nil, fmt.Errorf("RepositoryFromContextOrOpen: %v", err)
+ return nil, fmt.Errorf("RepositoryFromContextOrOpen: %w", err)
}
defer closer.Close()
@@ -135,11 +181,11 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
head := pr.GetGitRefName()
if line > 0 {
if reviewID != 0 {
- first, err := models.FindComments(&models.FindCommentsOptions{
+ first, err := issues_model.FindComments(ctx, &issues_model.FindCommentsOptions{
ReviewID: reviewID,
Line: line,
TreePath: treePath,
- Type: models.CommentTypeCode,
+ Type: issues_model.CommentTypeCode,
ListOptions: db.ListOptions{
PageSize: 1,
Page: 1,
@@ -149,14 +195,14 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
commitID = first[0].CommitSHA
invalidated = first[0].Invalidated
patch = first[0].Patch
- } else if err != nil && !models.IsErrCommentNotExist(err) {
- return nil, fmt.Errorf("Find first comment for %d line %d path %s. Error: %v", reviewID, line, treePath, err)
+ } else if err != nil && !issues_model.IsErrCommentNotExist(err) {
+ return nil, fmt.Errorf("Find first comment for %d line %d path %s. Error: %w", reviewID, line, treePath, err)
} else {
- review, err := models.GetReviewByID(reviewID)
+ review, err := issues_model.GetReviewByID(ctx, reviewID)
if err == nil && len(review.CommitID) > 0 {
head = review.CommitID
- } else if err != nil && !models.IsErrReviewNotExist(err) {
- return nil, fmt.Errorf("GetReviewByID %d. Error: %v", reviewID, err)
+ } else if err != nil && !issues_model.IsErrReviewNotExist(err) {
+ return nil, fmt.Errorf("GetReviewByID %d. Error: %w", reviewID, err)
}
}
}
@@ -169,7 +215,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
if err == nil {
commitID = commit.ID.String()
} else if !(strings.Contains(err.Error(), "exit status 128 - fatal: no such path") || notEnoughLines.MatchString(err.Error())) {
- return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %v", pr.GetGitRefName(), gitRepo.Path, treePath, line, err)
+ return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %w", pr.GetGitRefName(), gitRepo.Path, treePath, line, err)
}
}
}
@@ -178,7 +224,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
if len(patch) == 0 && reviewID != 0 {
headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil {
- return nil, fmt.Errorf("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err)
+ return nil, fmt.Errorf("GetRefCommitID[%s]: %w", pr.GetGitRefName(), err)
}
if len(commitID) == 0 {
commitID = headCommitID
@@ -190,20 +236,20 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
}()
go func() {
if err := git.GetRepoRawDiffForFile(gitRepo, pr.MergeBase, headCommitID, git.RawDiffNormal, treePath, writer); err != nil {
- _ = writer.CloseWithError(fmt.Errorf("GetRawDiffForLine[%s, %s, %s, %s]: %v", gitRepo.Path, pr.MergeBase, headCommitID, treePath, err))
+ _ = writer.CloseWithError(fmt.Errorf("GetRawDiffForLine[%s, %s, %s, %s]: %w", gitRepo.Path, pr.MergeBase, headCommitID, treePath, err))
return
}
_ = writer.Close()
}()
- patch, err = git.CutDiffAroundLine(reader, int64((&models.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
+ patch, err = git.CutDiffAroundLine(reader, int64((&issues_model.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
if err != nil {
log.Error("Error whilst generating patch: %v", err)
return nil, err
}
}
- return models.CreateComment(&models.CreateCommentOptions{
- Type: models.CommentTypeCode,
+ return issue_service.CreateComment(&issues_model.CreateCommentOptions{
+ Type: issues_model.CommentTypeCode,
Doer: doer,
Repo: repo,
Issue: issue,
@@ -218,14 +264,14 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
}
// SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist
-func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *models.Issue, reviewType models.ReviewType, content, commitID string, attachmentUUIDs []string) (*models.Review, *models.Comment, error) {
+func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, reviewType issues_model.ReviewType, content, commitID string, attachmentUUIDs []string) (*issues_model.Review, *issues_model.Comment, error) {
pr, err := issue.GetPullRequest()
if err != nil {
return nil, nil, err
}
var stale bool
- if reviewType != models.ReviewTypeApprove && reviewType != models.ReviewTypeReject {
+ if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject {
stale = false
} else {
headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
@@ -243,26 +289,26 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
}
}
- review, comm, err := models.SubmitReview(doer, issue, reviewType, content, commitID, stale, attachmentUUIDs)
+ review, comm, err := issues_model.SubmitReview(doer, issue, reviewType, content, commitID, stale, attachmentUUIDs)
if err != nil {
return nil, nil, err
}
- mentions, err := models.FindAndUpdateIssueMentions(ctx, issue, doer, comm.Content)
+ mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, comm.Content)
if err != nil {
return nil, nil, err
}
- notification.NotifyPullRequestReview(pr, review, comm, mentions)
+ notification.NotifyPullRequestReview(ctx, pr, review, comm, mentions)
for _, lines := range review.CodeComments {
for _, comments := range lines {
for _, codeComment := range comments {
- mentions, err := models.FindAndUpdateIssueMentions(ctx, issue, doer, codeComment.Content)
+ mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, doer, codeComment.Content)
if err != nil {
return nil, nil, err
}
- notification.NotifyPullRequestCodeComment(pr, codeComment, mentions)
+ notification.NotifyPullRequestCodeComment(ctx, pr, codeComment, mentions)
}
}
}
@@ -271,39 +317,61 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
}
// DismissReview dismissing stale review by repo admin
-func DismissReview(ctx context.Context, reviewID int64, message string, doer *user_model.User, isDismiss bool) (comment *models.Comment, err error) {
- review, err := models.GetReviewByID(reviewID)
+func DismissReview(ctx context.Context, reviewID, repoID int64, message string, doer *user_model.User, isDismiss, dismissPriors bool) (comment *issues_model.Comment, err error) {
+ review, err := issues_model.GetReviewByID(ctx, reviewID)
if err != nil {
return
}
- if review.Type != models.ReviewTypeApprove && review.Type != models.ReviewTypeReject {
+ if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject {
return nil, fmt.Errorf("not need to dismiss this review because it's type is not Approve or change request")
}
- if err = models.DismissReview(review, isDismiss); err != nil {
+ // load data for notify
+ if err = review.LoadAttributes(ctx); err != nil {
+ return nil, err
+ }
+
+ // Check if the review's repoID is the one we're currently expecting.
+ if review.Issue.RepoID != repoID {
+ return nil, fmt.Errorf("reviews's repository is not the same as the one we expect")
+ }
+
+ if err = issues_model.DismissReview(review, isDismiss); err != nil {
return
}
+ if dismissPriors {
+ reviews, err := issues_model.GetReviews(ctx, &issues_model.GetReviewOptions{
+ IssueID: review.IssueID,
+ ReviewerID: review.ReviewerID,
+ Dismissed: util.OptionalBoolFalse,
+ })
+ if err != nil {
+ return nil, err
+ }
+ for _, oldReview := range reviews {
+ if err = issues_model.DismissReview(oldReview, true); err != nil {
+ return nil, err
+ }
+ }
+ }
+
if !isDismiss {
return nil, nil
}
- // load data for notify
- if err = review.LoadAttributes(ctx); err != nil {
- return
- }
- if err = review.Issue.LoadPullRequest(); err != nil {
+ if err = review.Issue.LoadPullRequest(ctx); err != nil {
return
}
- if err = review.Issue.LoadAttributes(); err != nil {
+ if err = review.Issue.LoadAttributes(ctx); err != nil {
return
}
- comment, err = models.CreateComment(&models.CreateCommentOptions{
+ comment, err = issue_service.CreateComment(&issues_model.CreateCommentOptions{
Doer: doer,
Content: message,
- Type: models.CommentTypeDismissReview,
+ Type: issues_model.CommentTypeDismissReview,
ReviewID: review.ID,
Issue: review.Issue,
Repo: review.Issue.Repo,
@@ -316,7 +384,7 @@ func DismissReview(ctx context.Context, reviewID int64, message string, doer *us
comment.Poster = doer
comment.Issue = review.Issue
- notification.NotifyPullRevieweDismiss(doer, review, comment)
+ notification.NotifyPullReviewDismiss(ctx, doer, review, comment)
- return
+ return comment, err
}
diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go
index 22ef53937d8ac..d49a15cea00ad 100644
--- a/services/pull/temp_repo.go
+++ b/services/pull/temp_repo.go
@@ -1,7 +1,6 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
@@ -13,25 +12,27 @@ import (
"strings"
"code.gitea.io/gitea/models"
+ issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
)
// createTemporaryRepo creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch
// it also create a second base branch called "original_base"
-func createTemporaryRepo(ctx context.Context, pr *models.PullRequest) (string, error) {
- if err := pr.LoadHeadRepo(); err != nil {
+func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (string, error) {
+ if err := pr.LoadHeadRepo(ctx); err != nil {
log.Error("LoadHeadRepo: %v", err)
- return "", fmt.Errorf("LoadHeadRepo: %v", err)
+ return "", fmt.Errorf("LoadHeadRepo: %w", err)
} else if pr.HeadRepo == nil {
log.Error("Pr %d HeadRepo %d does not exist", pr.ID, pr.HeadRepoID)
return "", &repo_model.ErrRepoNotExist{
ID: pr.HeadRepoID,
}
- } else if err := pr.LoadBaseRepo(); err != nil {
+ } else if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
- return "", fmt.Errorf("LoadBaseRepo: %v", err)
+ return "", fmt.Errorf("LoadBaseRepo: %w", err)
} else if pr.BaseRepo == nil {
log.Error("Pr %d BaseRepo %d does not exist", pr.ID, pr.BaseRepoID)
return "", &repo_model.ErrRepoNotExist{
@@ -39,14 +40,14 @@ func createTemporaryRepo(ctx context.Context, pr *models.PullRequest) (string, e
}
} else if err := pr.HeadRepo.GetOwner(ctx); err != nil {
log.Error("HeadRepo.GetOwner: %v", err)
- return "", fmt.Errorf("HeadRepo.GetOwner: %v", err)
+ return "", fmt.Errorf("HeadRepo.GetOwner: %w", err)
} else if err := pr.BaseRepo.GetOwner(ctx); err != nil {
log.Error("BaseRepo.GetOwner: %v", err)
- return "", fmt.Errorf("BaseRepo.GetOwner: %v", err)
+ return "", fmt.Errorf("BaseRepo.GetOwner: %w", err)
}
// Clone base repo.
- tmpBasePath, err := models.CreateTemporaryPath("pull")
+ tmpBasePath, err := repo_module.CreateTemporaryPath("pull")
if err != nil {
log.Error("CreateTemporaryPath: %v", err)
return "", err
@@ -57,7 +58,7 @@ func createTemporaryRepo(ctx context.Context, pr *models.PullRequest) (string, e
if err := git.InitRepository(ctx, tmpBasePath, false); err != nil {
log.Error("git init tmpBasePath: %v", err)
- if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err)
}
return "", err
@@ -85,77 +86,77 @@ func createTemporaryRepo(ctx context.Context, pr *models.PullRequest) (string, e
if err := addCacheRepo(tmpBasePath, baseRepoPath); err != nil {
log.Error("Unable to add base repository to temporary repo [%s -> %s]: %v", pr.BaseRepo.FullName(), tmpBasePath, err)
- if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err)
}
- return "", fmt.Errorf("Unable to add base repository to temporary repo [%s -> tmpBasePath]: %v", pr.BaseRepo.FullName(), err)
+ return "", fmt.Errorf("Unable to add base repository to temporary repo [%s -> tmpBasePath]: %w", pr.BaseRepo.FullName(), err)
}
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,
Stderr: &errbuf,
}); err != nil {
log.Error("Unable to add base repository as origin [%s -> %s]: %v\n%s\n%s", pr.BaseRepo.FullName(), tmpBasePath, err, outbuf.String(), errbuf.String())
- if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err)
}
- return "", fmt.Errorf("Unable to add base repository as origin [%s -> tmpBasePath]: %v\n%s\n%s", pr.BaseRepo.FullName(), err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("Unable to add base repository as origin [%s -> tmpBasePath]: %w\n%s\n%s", pr.BaseRepo.FullName(), err, outbuf.String(), errbuf.String())
}
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,
Stderr: &errbuf,
}); err != nil {
log.Error("Unable to fetch origin base branch [%s:%s -> base, original_base in %s]: %v:\n%s\n%s", pr.BaseRepo.FullName(), pr.BaseBranch, tmpBasePath, err, outbuf.String(), errbuf.String())
- if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err)
}
- return "", fmt.Errorf("Unable to fetch origin base branch [%s:%s -> base, original_base in tmpBasePath]: %v\n%s\n%s", pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("Unable to fetch origin base branch [%s:%s -> base, original_base in tmpBasePath]: %w\n%s\n%s", pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
}
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,
Stderr: &errbuf,
}); err != nil {
log.Error("Unable to set HEAD as base branch [%s]: %v\n%s\n%s", tmpBasePath, err, outbuf.String(), errbuf.String())
- if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err)
}
- return "", fmt.Errorf("Unable to set HEAD as base branch [tmpBasePath]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("Unable to set HEAD as base branch [tmpBasePath]: %w\n%s\n%s", err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
if err := addCacheRepo(tmpBasePath, headRepoPath); err != nil {
log.Error("Unable to add head repository to temporary repo [%s -> %s]: %v", pr.HeadRepo.FullName(), tmpBasePath, err)
- if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err)
}
- return "", fmt.Errorf("Unable to head base repository to temporary repo [%s -> tmpBasePath]: %v", pr.HeadRepo.FullName(), err)
+ return "", fmt.Errorf("Unable to head base repository to temporary repo [%s -> tmpBasePath]: %w", 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,
Stderr: &errbuf,
}); err != nil {
log.Error("Unable to add head repository as head_repo [%s -> %s]: %v\n%s\n%s", pr.HeadRepo.FullName(), tmpBasePath, err, outbuf.String(), errbuf.String())
- if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err)
}
- return "", fmt.Errorf("Unable to add head repository as head_repo [%s -> tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("Unable to add head repository as head_repo [%s -> tmpBasePath]: %w\n%s\n%s", pr.HeadRepo.FullName(), err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
@@ -163,20 +164,20 @@ func createTemporaryRepo(ctx context.Context, pr *models.PullRequest) (string, e
trackingBranch := "tracking"
// Fetch head branch
var headBranch string
- if pr.Flow == models.PullRequestFlowGithub {
+ if pr.Flow == issues_model.PullRequestFlowGithub {
headBranch = git.BranchPrefix + pr.HeadBranch
- } else if len(pr.HeadCommitID) == 40 { // for not created pull request
+ } else if len(pr.HeadCommitID) == git.SHAFullLength { // for not created pull request
headBranch = pr.HeadCommitID
} 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,
Stderr: &errbuf,
}); err != nil {
- if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err)
}
if !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) {
@@ -185,7 +186,7 @@ func createTemporaryRepo(ctx context.Context, pr *models.PullRequest) (string, e
}
}
log.Error("Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, tmpBasePath, err, outbuf.String(), errbuf.String())
- return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), headBranch, err, outbuf.String(), errbuf.String())
+ return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %w\n%s\n%s", pr.HeadRepo.FullName(), headBranch, err, outbuf.String(), errbuf.String())
}
outbuf.Reset()
errbuf.Reset()
diff --git a/services/pull/update.go b/services/pull/update.go
index 2ad58ecd29ac8..9e29f63c7c857 100644
--- a/services/pull/update.go
+++ b/services/pull/update.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package pull
@@ -9,26 +8,33 @@ import (
"fmt"
"code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
)
// Update updates pull request with base branch.
-func Update(ctx context.Context, pull *models.PullRequest, doer *user_model.User, message string, rebase bool) error {
+func Update(ctx context.Context, pull *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error {
var (
- pr *models.PullRequest
+ pr *issues_model.PullRequest
style repo_model.MergeStyle
)
+ pullWorkingPool.CheckIn(fmt.Sprint(pull.ID))
+ defer pullWorkingPool.CheckOut(fmt.Sprint(pull.ID))
+
if rebase {
pr = pull
style = repo_model.MergeStyleRebaseUpdate
} else {
// use merge functions but switch repo's and branch's
- pr = &models.PullRequest{
+ pr = &issues_model.PullRequest{
HeadRepoID: pull.BaseRepoID,
BaseRepoID: pull.HeadRepoID,
HeadBranch: pull.BaseBranch,
@@ -37,17 +43,17 @@ func Update(ctx context.Context, pull *models.PullRequest, doer *user_model.User
style = repo_model.MergeStyleMerge
}
- if pull.Flow == models.PullRequestFlowAGit {
+ if pull.Flow == issues_model.PullRequestFlowAGit {
// TODO: Not support update agit flow pull request's head branch
return fmt.Errorf("Not support update agit flow pull request's head branch")
}
- if err := pr.LoadHeadRepo(); err != nil {
+ if err := pr.LoadHeadRepo(ctx); err != nil {
log.Error("LoadHeadRepo: %v", err)
- return fmt.Errorf("LoadHeadRepo: %v", err)
- } else if err = pr.LoadBaseRepo(); err != nil {
+ return fmt.Errorf("LoadHeadRepo: %w", err)
+ } else if err = pr.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
- return fmt.Errorf("LoadBaseRepo: %v", err)
+ return fmt.Errorf("LoadBaseRepo: %w", err)
}
diffCount, err := GetDiverging(ctx, pull)
@@ -71,34 +77,46 @@ func Update(ctx context.Context, pull *models.PullRequest, doer *user_model.User
}
// IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections
-func IsUserAllowedToUpdate(pull *models.PullRequest, user *user_model.User) (mergeAllowed, rebaseAllowed bool, err error) {
- if pull.Flow == models.PullRequestFlowAGit {
+func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (mergeAllowed, rebaseAllowed bool, err error) {
+ if pull.Flow == issues_model.PullRequestFlowAGit {
return false, false, nil
}
if user == nil {
return false, false, nil
}
- headRepoPerm, err := models.GetUserRepoPermission(pull.HeadRepo, user)
+ headRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, user)
if err != nil {
+ if repo_model.IsErrUnitTypeNotExist(err) {
+ return false, false, nil
+ }
+ return false, false, err
+ }
+
+ if err := pull.LoadBaseRepo(ctx); err != nil {
return false, false, err
}
- pr := &models.PullRequest{
+ pr := &issues_model.PullRequest{
HeadRepoID: pull.BaseRepoID,
+ HeadRepo: pull.BaseRepo,
BaseRepoID: pull.HeadRepoID,
+ BaseRepo: pull.HeadRepo,
HeadBranch: pull.BaseBranch,
BaseBranch: pull.HeadBranch,
}
- err = pr.LoadProtectedBranch()
+ pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
if err != nil {
return false, false, err
}
// can't do rebase on protected branch because need force push
- if pr.ProtectedBranch == nil {
- prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests)
+ if pb == nil {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
+ return false, false, err
+ }
+ prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
return false, false, err
@@ -107,25 +125,42 @@ func IsUserAllowedToUpdate(pull *models.PullRequest, user *user_model.User) (mer
}
// Update function need push permission
- if pr.ProtectedBranch != nil && !pr.ProtectedBranch.CanUserPush(user.ID) {
- return false, false, nil
+ if pb != nil {
+ pb.Repo = pull.BaseRepo
+ if !pb.CanUserPush(ctx, user) {
+ return false, false, nil
+ }
}
- mergeAllowed, err = IsUserAllowedToMerge(pr, headRepoPerm, user)
+ baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, user)
if err != nil {
return false, false, err
}
+ mergeAllowed, err = IsUserAllowedToMerge(ctx, pr, headRepoPerm, user)
+ if err != nil {
+ return false, false, err
+ }
+
+ if pull.AllowMaintainerEdit {
+ mergeAllowedMaintainer, err := IsUserAllowedToMerge(ctx, pr, baseRepoPerm, user)
+ if err != nil {
+ return false, false, err
+ }
+
+ mergeAllowed = mergeAllowed || mergeAllowedMaintainer
+ }
+
return mergeAllowed, rebaseAllowed, nil
}
// GetDiverging determines how many commits a PR is ahead or behind the PR base branch
-func GetDiverging(ctx context.Context, pr *models.PullRequest) (*git.DivergeObject, error) {
+func GetDiverging(ctx context.Context, pr *issues_model.PullRequest) (*git.DivergeObject, error) {
log.Trace("GetDiverging[%d]: compare commits", pr.ID)
- if err := pr.LoadBaseRepo(); err != nil {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
return nil, err
}
- if err := pr.LoadHeadRepo(); err != nil {
+ if err := pr.LoadHeadRepo(ctx); err != nil {
return nil, err
}
@@ -137,7 +172,7 @@ func GetDiverging(ctx context.Context, pr *models.PullRequest) (*git.DivergeObje
return nil, err
}
defer func() {
- if err := models.RemoveTemporaryPath(tmpRepo); err != nil {
+ if err := repo_module.RemoveTemporaryPath(tmpRepo); err != nil {
log.Error("Merge: RemoveTemporaryPath: %s", err)
}
}()
diff --git a/services/release/release.go b/services/release/release.go
index 0372e3a6906a0..13042cd3ac2e8 100644
--- a/services/release/release.go
+++ b/services/release/release.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package release
@@ -12,31 +11,37 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
)
-func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool, error) {
+func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Release, msg string) (bool, error) {
var created bool
// Only actual create when publish.
if !rel.IsDraft {
if !gitRepo.IsTagExist(rel.TagName) {
- if err := rel.LoadAttributes(); err != nil {
+ if err := rel.LoadAttributes(ctx); err != nil {
log.Error("LoadAttributes: %v", err)
return false, err
}
- protectedTags, err := models.GetProtectedTags(rel.Repo.ID)
+ protectedTags, err := git_model.GetProtectedTags(ctx, rel.Repo.ID)
if err != nil {
- return false, fmt.Errorf("GetProtectedTags: %v", err)
+ return false, fmt.Errorf("GetProtectedTags: %w", err)
}
- isAllowed, err := models.IsUserAllowedToControlTag(protectedTags, rel.TagName, rel.PublisherID)
+
+ // Trim '--' prefix to prevent command line argument vulnerability.
+ rel.TagName = strings.TrimPrefix(rel.TagName, "--")
+ isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID)
if err != nil {
return false, err
}
@@ -48,11 +53,9 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool,
commit, err := gitRepo.GetCommit(rel.Target)
if err != nil {
- return false, fmt.Errorf("createTag::GetCommit[%v]: %v", rel.Target, err)
+ return false, fmt.Errorf("createTag::GetCommit[%v]: %w", rel.Target, err)
}
- // Trim '--' prefix to prevent command line argument vulnerability.
- rel.TagName = strings.TrimPrefix(rel.TagName, "--")
if len(msg) > 0 {
if err = gitRepo.CreateAnnotatedTag(rel.TagName, msg, commit.ID.String()); err != nil {
if strings.Contains(err.Error(), "is not a valid tag name") {
@@ -78,28 +81,28 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool,
commits.CompareURL = rel.Repo.ComposeCompareURL(git.EmptySHA, commit.ID.String())
notification.NotifyPushCommits(
- rel.Publisher, rel.Repo,
+ ctx, rel.Publisher, rel.Repo,
&repository.PushUpdateOptions{
RefFullName: git.TagPrefix + rel.TagName,
OldCommitID: git.EmptySHA,
NewCommitID: commit.ID.String(),
}, commits)
- notification.NotifyCreateRef(rel.Publisher, rel.Repo, "tag", git.TagPrefix+rel.TagName, commit.ID.String())
+ notification.NotifyCreateRef(ctx, rel.Publisher, rel.Repo, "tag", git.TagPrefix+rel.TagName, commit.ID.String())
rel.CreatedUnix = timeutil.TimeStampNow()
}
commit, err := gitRepo.GetTagCommit(rel.TagName)
if err != nil {
- return false, fmt.Errorf("GetTagCommit: %v", err)
+ return false, fmt.Errorf("GetTagCommit: %w", err)
}
rel.Sha1 = commit.ID.String()
rel.NumCommits, err = commit.CommitsCount()
if err != nil {
- return false, fmt.Errorf("CommitsCount: %v", err)
+ return false, fmt.Errorf("CommitsCount: %w", err)
}
if rel.PublisherID <= 0 {
- u, err := user_model.GetUserByEmail(commit.Author.Email)
+ u, err := user_model.GetUserByEmailContext(ctx, commit.Author.Email)
if err == nil {
rel.PublisherID = u.ID
}
@@ -111,31 +114,31 @@ func createTag(gitRepo *git.Repository, rel *models.Release, msg string) (bool,
}
// CreateRelease creates a new release of repository.
-func CreateRelease(gitRepo *git.Repository, rel *models.Release, attachmentUUIDs []string, msg string) error {
- isExist, err := models.IsReleaseExist(rel.RepoID, rel.TagName)
+func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentUUIDs []string, msg string) error {
+ has, err := repo_model.IsReleaseExist(gitRepo.Ctx, rel.RepoID, rel.TagName)
if err != nil {
return err
- } else if isExist {
- return models.ErrReleaseAlreadyExist{
+ } else if has {
+ return repo_model.ErrReleaseAlreadyExist{
TagName: rel.TagName,
}
}
- if _, err = createTag(gitRepo, rel, msg); err != nil {
+ if _, err = createTag(gitRepo.Ctx, gitRepo, rel, msg); err != nil {
return err
}
rel.LowerTagName = strings.ToLower(rel.TagName)
- if err = models.InsertRelease(rel); err != nil {
+ if err = db.Insert(gitRepo.Ctx, rel); err != nil {
return err
}
- if err = models.AddReleaseAttachments(gitRepo.Ctx, rel.ID, attachmentUUIDs); err != nil {
+ if err = repo_model.AddReleaseAttachments(gitRepo.Ctx, rel.ID, attachmentUUIDs); err != nil {
return err
}
if !rel.IsDraft {
- notification.NotifyNewRelease(rel)
+ notification.NotifyNewRelease(gitRepo.Ctx, rel)
}
return nil
@@ -143,10 +146,10 @@ func CreateRelease(gitRepo *git.Repository, rel *models.Release, attachmentUUIDs
// CreateNewTag creates a new repository tag
func CreateNewTag(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commit, tagName, msg string) error {
- isExist, err := models.IsReleaseExist(repo.ID, tagName)
+ has, err := repo_model.IsReleaseExist(ctx, repo.ID, tagName)
if err != nil {
return err
- } else if isExist {
+ } else if has {
return models.ErrTagAlreadyExists{
TagName: tagName,
}
@@ -158,7 +161,7 @@ func CreateNewTag(ctx context.Context, doer *user_model.User, repo *repo_model.R
}
defer closer.Close()
- rel := &models.Release{
+ rel := &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: doer.ID,
@@ -170,63 +173,62 @@ func CreateNewTag(ctx context.Context, doer *user_model.User, repo *repo_model.R
IsTag: true,
}
- if _, err = createTag(gitRepo, rel, msg); err != nil {
+ if _, err = createTag(ctx, gitRepo, rel, msg); err != nil {
return err
}
- if err = models.InsertRelease(rel); err != nil {
- return err
- }
-
- return err
+ return db.Insert(ctx, rel)
}
// UpdateRelease updates information, attachments of a release and will create tag if it's not a draft and tag not exist.
// addAttachmentUUIDs accept a slice of new created attachments' uuids which will be reassigned release_id as the created release
// delAttachmentUUIDs accept a slice of attachments' uuids which will be deleted from the release
// editAttachments accept a map of attachment uuid to new attachment name which will be updated with attachments.
-func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *models.Release,
+func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *repo_model.Release,
addAttachmentUUIDs, delAttachmentUUIDs []string, editAttachments map[string]string,
) (err error) {
if rel.ID == 0 {
return errors.New("UpdateRelease only accepts an exist release")
}
- isCreated, err := createTag(gitRepo, rel, "")
+ isCreated, err := createTag(gitRepo.Ctx, gitRepo, rel, "")
if err != nil {
return err
}
rel.LowerTagName = strings.ToLower(rel.TagName)
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
- if err = models.UpdateRelease(ctx, rel); err != nil {
+ if err = repo_model.UpdateRelease(ctx, rel); err != nil {
return err
}
- if err = models.AddReleaseAttachments(ctx, rel.ID, addAttachmentUUIDs); err != nil {
- return fmt.Errorf("AddReleaseAttachments: %v", err)
+ if err = repo_model.AddReleaseAttachments(ctx, rel.ID, addAttachmentUUIDs); err != nil {
+ return fmt.Errorf("AddReleaseAttachments: %w", err)
}
- deletedUUIDsMap := make(map[string]bool)
+ deletedUUIDs := make(container.Set[string])
if len(delAttachmentUUIDs) > 0 {
// Check attachments
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, delAttachmentUUIDs)
if err != nil {
- return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %v", delAttachmentUUIDs, err)
+ return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", delAttachmentUUIDs, err)
}
for _, attach := range attachments {
if attach.ReleaseID != rel.ID {
- return errors.New("delete attachement of release permission denied")
+ return util.SilentWrap{
+ Message: "delete attachment of release permission denied",
+ Err: util.ErrPermissionDenied,
+ }
}
- deletedUUIDsMap[attach.UUID] = true
+ deletedUUIDs.Add(attach.UUID)
}
if _, err := repo_model.DeleteAttachments(ctx, attachments, false); err != nil {
- return fmt.Errorf("DeleteAttachments [uuids: %v]: %v", delAttachmentUUIDs, err)
+ return fmt.Errorf("DeleteAttachments [uuids: %v]: %w", delAttachmentUUIDs, err)
}
}
@@ -238,16 +240,19 @@ func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *models.R
// Check attachments
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, updateAttachmentsList)
if err != nil {
- return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %v", updateAttachmentsList, err)
+ return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", updateAttachmentsList, err)
}
for _, attach := range attachments {
if attach.ReleaseID != rel.ID {
- return errors.New("update attachement of release permission denied")
+ return util.SilentWrap{
+ Message: "update attachment of release permission denied",
+ Err: util.ErrPermissionDenied,
+ }
}
}
for uuid, newName := range editAttachments {
- if !deletedUUIDsMap[uuid] {
+ if !deletedUUIDs.Contains(uuid) {
if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{
UUID: uuid,
Name: newName,
@@ -273,12 +278,12 @@ func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *models.R
}
if !isCreated {
- notification.NotifyUpdateRelease(doer, rel)
+ notification.NotifyUpdateRelease(gitRepo.Ctx, doer, rel)
return
}
if !rel.IsDraft {
- notification.NotifyNewRelease(rel)
+ notification.NotifyNewRelease(gitRepo.Ctx, rel)
}
return err
@@ -286,51 +291,65 @@ func UpdateRelease(doer *user_model.User, gitRepo *git.Repository, rel *models.R
// DeleteReleaseByID deletes a release and corresponding Git tag by given ID.
func DeleteReleaseByID(ctx context.Context, id int64, doer *user_model.User, delTag bool) error {
- rel, err := models.GetReleaseByID(id)
+ rel, err := repo_model.GetReleaseByID(ctx, id)
if err != nil {
- return fmt.Errorf("GetReleaseByID: %v", err)
+ return fmt.Errorf("GetReleaseByID: %w", err)
}
- repo, err := repo_model.GetRepositoryByID(rel.RepoID)
+ repo, err := repo_model.GetRepositoryByID(ctx, rel.RepoID)
if err != nil {
- return fmt.Errorf("GetRepositoryByID: %v", err)
+ return fmt.Errorf("GetRepositoryByID: %w", err)
}
if delTag {
- if stdout, _, err := git.NewCommand(ctx, "tag", "-d", rel.TagName).
+ protectedTags, err := git_model.GetProtectedTags(ctx, rel.RepoID)
+ if err != nil {
+ return fmt.Errorf("GetProtectedTags: %w", err)
+ }
+ isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID)
+ if err != nil {
+ return err
+ }
+ if !isAllowed {
+ return models.ErrProtectedTagName{
+ TagName: 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)
- return fmt.Errorf("git tag -d: %v", err)
+ return fmt.Errorf("git tag -d: %w", err)
}
notification.NotifyPushCommits(
- doer, repo,
+ ctx, doer, repo,
&repository.PushUpdateOptions{
RefFullName: git.TagPrefix + rel.TagName,
OldCommitID: rel.Sha1,
NewCommitID: git.EmptySHA,
}, repository.NewPushCommits())
- notification.NotifyDeleteRef(doer, repo, "tag", git.TagPrefix+rel.TagName)
+ notification.NotifyDeleteRef(ctx, doer, repo, "tag", git.TagPrefix+rel.TagName)
- if err := models.DeleteReleaseByID(id); err != nil {
- return fmt.Errorf("DeleteReleaseByID: %v", err)
+ if err := repo_model.DeleteReleaseByID(ctx, id); err != nil {
+ return fmt.Errorf("DeleteReleaseByID: %w", err)
}
} else {
rel.IsTag = true
- if err = models.UpdateRelease(ctx, rel); err != nil {
- return fmt.Errorf("Update: %v", err)
+ if err = repo_model.UpdateRelease(ctx, rel); err != nil {
+ return fmt.Errorf("Update: %w", err)
}
}
rel.Repo = repo
- if err = rel.LoadAttributes(); err != nil {
- return fmt.Errorf("LoadAttributes: %v", err)
+ if err = rel.LoadAttributes(ctx); err != nil {
+ return fmt.Errorf("LoadAttributes: %w", err)
}
- if err := repo_model.DeleteAttachmentsByRelease(rel.ID); err != nil {
- return fmt.Errorf("DeleteAttachments: %v", err)
+ if err := repo_model.DeleteAttachmentsByRelease(ctx, rel.ID); err != nil {
+ return fmt.Errorf("DeleteAttachments: %w", err)
}
for i := range rel.Attachments {
@@ -340,7 +359,7 @@ func DeleteReleaseByID(ctx context.Context, id int64, doer *user_model.User, del
}
}
- notification.NotifyDeleteRelease(doer, rel)
+ notification.NotifyDeleteRelease(ctx, doer, rel)
return nil
}
diff --git a/services/release/release_test.go b/services/release/release_test.go
index 19d985491f151..9b8aaa364983a 100644
--- a/services/release/release_test.go
+++ b/services/release/release_test.go
@@ -1,6 +1,5 @@
// Copyright 2018 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.
+// SPDX-License-Identifier: MIT
package release
@@ -10,7 +9,7 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -29,15 +28,15 @@ func TestMain(m *testing.M) {
func TestRelease_Create(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repoPath := repo_model.RepoPath(user.Name, repo.Name)
gitRepo, err := git.OpenRepository(git.DefaultContext, repoPath)
assert.NoError(t, err)
defer gitRepo.Close()
- assert.NoError(t, CreateRelease(gitRepo, &models.Release{
+ assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -51,7 +50,7 @@ func TestRelease_Create(t *testing.T) {
IsTag: false,
}, nil, ""))
- assert.NoError(t, CreateRelease(gitRepo, &models.Release{
+ assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -65,7 +64,7 @@ func TestRelease_Create(t *testing.T) {
IsTag: false,
}, nil, ""))
- assert.NoError(t, CreateRelease(gitRepo, &models.Release{
+ assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -79,7 +78,7 @@ func TestRelease_Create(t *testing.T) {
IsTag: false,
}, nil, ""))
- assert.NoError(t, CreateRelease(gitRepo, &models.Release{
+ assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -93,7 +92,7 @@ func TestRelease_Create(t *testing.T) {
IsTag: false,
}, nil, ""))
- assert.NoError(t, CreateRelease(gitRepo, &models.Release{
+ assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -114,7 +113,7 @@ func TestRelease_Create(t *testing.T) {
}, strings.NewReader("testtest"))
assert.NoError(t, err)
- release := models.Release{
+ release := repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -133,8 +132,8 @@ func TestRelease_Create(t *testing.T) {
func TestRelease_Update(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repoPath := repo_model.RepoPath(user.Name, repo.Name)
gitRepo, err := git.OpenRepository(git.DefaultContext, repoPath)
@@ -142,7 +141,7 @@ func TestRelease_Update(t *testing.T) {
defer gitRepo.Close()
// Test a changed release
- assert.NoError(t, CreateRelease(gitRepo, &models.Release{
+ assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -155,18 +154,18 @@ func TestRelease_Update(t *testing.T) {
IsPrerelease: false,
IsTag: false,
}, nil, ""))
- release, err := models.GetRelease(repo.ID, "v1.1.1")
+ release, err := repo_model.GetRelease(repo.ID, "v1.1.1")
assert.NoError(t, err)
releaseCreatedUnix := release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Note = "Changed note"
assert.NoError(t, UpdateRelease(user, gitRepo, release, nil, nil, nil))
- release, err = models.GetReleaseByID(release.ID)
+ release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
assert.NoError(t, err)
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test a changed draft
- assert.NoError(t, CreateRelease(gitRepo, &models.Release{
+ assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -179,18 +178,18 @@ func TestRelease_Update(t *testing.T) {
IsPrerelease: false,
IsTag: false,
}, nil, ""))
- release, err = models.GetRelease(repo.ID, "v1.2.1")
+ release, err = repo_model.GetRelease(repo.ID, "v1.2.1")
assert.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
assert.NoError(t, UpdateRelease(user, gitRepo, release, nil, nil, nil))
- release, err = models.GetReleaseByID(release.ID)
+ release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
assert.NoError(t, err)
assert.Less(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test a changed pre-release
- assert.NoError(t, CreateRelease(gitRepo, &models.Release{
+ assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -203,19 +202,19 @@ func TestRelease_Update(t *testing.T) {
IsPrerelease: true,
IsTag: false,
}, nil, ""))
- release, err = models.GetRelease(repo.ID, "v1.3.1")
+ release, err = repo_model.GetRelease(repo.ID, "v1.3.1")
assert.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
release.Note = "Changed note"
assert.NoError(t, UpdateRelease(user, gitRepo, release, nil, nil, nil))
- release, err = models.GetReleaseByID(release.ID)
+ release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
assert.NoError(t, err)
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test create release
- release = &models.Release{
+ release = &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -235,7 +234,7 @@ func TestRelease_Update(t *testing.T) {
tagName := release.TagName
assert.NoError(t, UpdateRelease(user, gitRepo, release, nil, nil, nil))
- release, err = models.GetReleaseByID(release.ID)
+ release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
assert.NoError(t, err)
assert.Equal(t, tagName, release.TagName)
@@ -248,7 +247,7 @@ func TestRelease_Update(t *testing.T) {
assert.NoError(t, err)
assert.NoError(t, UpdateRelease(user, gitRepo, release, []string{attach.UUID}, nil, nil))
- assert.NoError(t, models.GetReleaseAttachments(release))
+ assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
assert.Len(t, release.Attachments, 1)
assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
@@ -259,7 +258,7 @@ func TestRelease_Update(t *testing.T) {
attach.UUID: "test2.txt",
}))
release.Attachments = nil
- assert.NoError(t, models.GetReleaseAttachments(release))
+ assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
assert.Len(t, release.Attachments, 1)
assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
@@ -268,15 +267,15 @@ func TestRelease_Update(t *testing.T) {
// delete the attachment
assert.NoError(t, UpdateRelease(user, gitRepo, release, nil, []string{attach.UUID}, nil))
release.Attachments = nil
- assert.NoError(t, models.GetReleaseAttachments(release))
+ assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
assert.Empty(t, release.Attachments)
}
func TestRelease_createTag(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repoPath := repo_model.RepoPath(user.Name, repo.Name)
gitRepo, err := git.OpenRepository(git.DefaultContext, repoPath)
@@ -284,7 +283,7 @@ func TestRelease_createTag(t *testing.T) {
defer gitRepo.Close()
// Test a changed release
- release := &models.Release{
+ release := &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -297,18 +296,18 @@ func TestRelease_createTag(t *testing.T) {
IsPrerelease: false,
IsTag: false,
}
- _, err = createTag(gitRepo, release, "")
+ _, err = createTag(db.DefaultContext, gitRepo, release, "")
assert.NoError(t, err)
assert.NotEmpty(t, release.CreatedUnix)
releaseCreatedUnix := release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Note = "Changed note"
- _, err = createTag(gitRepo, release, "")
+ _, err = createTag(db.DefaultContext, gitRepo, release, "")
assert.NoError(t, err)
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test a changed draft
- release = &models.Release{
+ release = &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -321,17 +320,17 @@ func TestRelease_createTag(t *testing.T) {
IsPrerelease: false,
IsTag: false,
}
- _, err = createTag(gitRepo, release, "")
+ _, err = createTag(db.DefaultContext, gitRepo, release, "")
assert.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
- _, err = createTag(gitRepo, release, "")
+ _, err = createTag(db.DefaultContext, gitRepo, release, "")
assert.NoError(t, err)
assert.Less(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
// Test a changed pre-release
- release = &models.Release{
+ release = &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
@@ -344,21 +343,21 @@ func TestRelease_createTag(t *testing.T) {
IsPrerelease: true,
IsTag: false,
}
- _, err = createTag(gitRepo, release, "")
+ _, err = createTag(db.DefaultContext, gitRepo, release, "")
assert.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
release.Note = "Changed note"
- _, err = createTag(gitRepo, release, "")
+ _, err = createTag(db.DefaultContext, gitRepo, release, "")
assert.NoError(t, err)
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
}
func TestCreateNewTag(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.NoError(t, CreateNewTag(git.DefaultContext, user, repo, "master", "v2.0",
"v2.0 is released \n\n BUGFIX: .... \n\n 123"))
diff --git a/services/repository/adopt.go b/services/repository/adopt.go
index b287d94f9dc63..8ebf2b6a3e60f 100644
--- a/services/repository/adopt.go
+++ b/services/repository/adopt.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
@@ -8,13 +7,14 @@ import (
"context"
"fmt"
"os"
+ "path"
"path/filepath"
"strings"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
@@ -26,7 +26,7 @@ import (
)
// AdoptRepository adopts pre-existing repository files for the user/organization.
-func AdoptRepository(doer, u *user_model.User, opts models.CreateRepoOptions) (*repo_model.Repository, error) {
+func AdoptRepository(doer, u *user_model.User, opts repo_module.CreateRepoOptions) (*repo_model.Repository, error) {
if !doer.IsAdmin && !u.CanCreateRepo() {
return nil, repo_model.ErrReachLimitOfRepo{
Limit: u.MaxRepoCreation,
@@ -53,7 +53,7 @@ func AdoptRepository(doer, u *user_model.User, opts models.CreateRepoOptions) (*
IsEmpty: !opts.AutoInit,
}
- if err := db.WithTx(func(ctx context.Context) error {
+ if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error {
repoPath := repo_model.RepoPath(u.Name, repo.Name)
isExist, err := util.IsExist(repoPath)
if err != nil {
@@ -67,20 +67,20 @@ func AdoptRepository(doer, u *user_model.User, opts models.CreateRepoOptions) (*
}
}
- if err := models.CreateRepository(ctx, doer, u, repo, true); err != nil {
+ if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, true); err != nil {
return err
}
if err := adoptRepository(ctx, repoPath, doer, repo, opts); err != nil {
- return fmt.Errorf("createDelegateHooks: %v", err)
+ return fmt.Errorf("createDelegateHooks: %w", err)
}
- if err := models.CheckDaemonExportOK(ctx, repo); err != nil {
- return fmt.Errorf("checkDaemonExportOK: %v", err)
+ if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
+ return fmt.Errorf("checkDaemonExportOK: %w", err)
}
// Initialize Issue Labels if selected
if len(opts.IssueLabels) > 0 {
if err := repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
- return fmt.Errorf("InitializeLabels: %v", err)
+ return fmt.Errorf("InitializeLabels: %w", err)
}
}
@@ -88,19 +88,19 @@ func AdoptRepository(doer, u *user_model.User, opts models.CreateRepoOptions) (*
SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
- return fmt.Errorf("CreateRepository(git update-server-info): %v", err)
+ return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
}
return nil
}); err != nil {
return nil, err
}
- notification.NotifyCreateRepository(doer, u, repo)
+ notification.NotifyCreateRepository(db.DefaultContext, doer, u, repo)
return repo, nil
}
-func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts models.CreateRepoOptions) (err error) {
+func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts repo_module.CreateRepoOptions) (err error) {
isExist, err := util.IsExist(repoPath)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
@@ -111,13 +111,13 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r
}
if err := repo_module.CreateDelegateHooks(repoPath); err != nil {
- return fmt.Errorf("createDelegateHooks: %v", err)
+ return fmt.Errorf("createDelegateHooks: %w", err)
}
// Re-fetch the repository from database before updating it (else it would
// override changes that were done earlier with sql)
- if repo, err = repo_model.GetRepositoryByIDCtx(ctx, repo.ID); err != nil {
- return fmt.Errorf("getRepositoryByID: %v", err)
+ if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
+ return fmt.Errorf("getRepositoryByID: %w", err)
}
repo.IsEmpty = false
@@ -125,7 +125,7 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r
// Don't bother looking this repo in the context it won't be there
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
- return fmt.Errorf("openRepository: %v", err)
+ return fmt.Errorf("openRepository: %w", err)
}
defer gitRepo.Close()
@@ -133,18 +133,16 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r
repo.DefaultBranch = opts.DefaultBranch
if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
- return fmt.Errorf("setDefaultBranch: %v", err)
+ return fmt.Errorf("setDefaultBranch: %w", err)
}
} else {
repo.DefaultBranch, err = gitRepo.GetDefaultBranch()
if err != nil {
repo.DefaultBranch = setting.Repository.DefaultBranch
if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
- return fmt.Errorf("setDefaultBranch: %v", err)
+ return fmt.Errorf("setDefaultBranch: %w", err)
}
}
-
- repo.DefaultBranch = strings.TrimPrefix(repo.DefaultBranch, git.BranchPrefix)
}
branches, _, _ := gitRepo.GetBranchNames(0, 0)
found := false
@@ -178,12 +176,12 @@ func adoptRepository(ctx context.Context, repoPath string, u *user_model.User, r
}
if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil {
- return fmt.Errorf("setDefaultBranch: %v", err)
+ return fmt.Errorf("setDefaultBranch: %w", err)
}
}
- if err = models.UpdateRepositoryCtx(ctx, repo, false); err != nil {
- return fmt.Errorf("updateRepository: %v", err)
+ if err = repo_module.UpdateRepository(ctx, repo, false); err != nil {
+ return fmt.Errorf("updateRepository: %w", err)
}
return nil
@@ -208,7 +206,7 @@ func DeleteUnadoptedRepository(doer, u *user_model.User, repoName string) error
}
}
- if exist, err := repo_model.IsRepositoryExist(u, repoName); err != nil {
+ if exist, err := repo_model.IsRepositoryExist(db.DefaultContext, u, repoName); err != nil {
return err
} else if exist {
return repo_model.ErrRepoAlreadyExist{
@@ -220,25 +218,25 @@ func DeleteUnadoptedRepository(doer, u *user_model.User, repoName string) error
return util.RemoveAll(repoPath)
}
-type unadoptedRrepositories struct {
+type unadoptedRepositories struct {
repositories []string
index int
start int
end int
}
-func (unadopted *unadoptedRrepositories) add(repository string) {
+func (unadopted *unadoptedRepositories) add(repository string) {
if unadopted.index >= unadopted.start && unadopted.index < unadopted.end {
unadopted.repositories = append(unadopted.repositories, repository)
}
unadopted.index++
}
-func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unadopted *unadoptedRrepositories) error {
+func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unadopted *unadoptedRepositories) error {
if len(repoNamesToCheck) == 0 {
return nil
}
- ctxUser, err := user_model.GetUserByName(userName)
+ ctxUser, err := user_model.GetUserByName(db.DefaultContext, userName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
log.Debug("Missing user: %s", userName)
@@ -246,7 +244,7 @@ func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unad
}
return err
}
- repos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{
+ repos, _, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{
Actor: ctxUser,
Private: true,
ListOptions: db.ListOptions{
@@ -260,13 +258,13 @@ func checkUnadoptedRepositories(userName string, repoNamesToCheck []string, unad
if len(repos) == len(repoNamesToCheck) {
return nil
}
- repoNames := make(map[string]bool, len(repos))
+ repoNames := make(container.Set[string], len(repos))
for _, repo := range repos {
- repoNames[repo.LowerName] = true
+ repoNames.Add(repo.LowerName)
}
for _, repoName := range repoNamesToCheck {
- if _, ok := repoNames[repoName]; !ok {
- unadopted.add(filepath.Join(userName, repoName))
+ if !repoNames.Contains(repoName) {
+ unadopted.add(path.Join(userName, repoName)) // These are not used as filepaths - but as reponames - therefore use path.Join not filepath.Join
}
}
return nil
@@ -294,7 +292,7 @@ func ListUnadoptedRepositories(query string, opts *db.ListOptions) ([]string, in
var repoNamesToCheck []string
start := (opts.Page - 1) * opts.PageSize
- unadopted := &unadoptedRrepositories{
+ unadopted := &unadoptedRepositories{
repositories: make([]string, 0, opts.PageSize),
start: start,
end: start + opts.PageSize,
@@ -305,14 +303,16 @@ func ListUnadoptedRepositories(query string, opts *db.ListOptions) ([]string, in
// We're going to iterate by pagesize.
root := filepath.Clean(setting.RepoRootPath)
- if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+ if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
- if !info.IsDir() || path == root {
+ if !d.IsDir() || path == root {
return nil
}
+ name := d.Name()
+
if !strings.ContainsRune(path[len(root)+1:], filepath.Separator) {
// Got a new user
if err = checkUnadoptedRepositories(userName, repoNamesToCheck, unadopted); err != nil {
@@ -320,16 +320,14 @@ func ListUnadoptedRepositories(query string, opts *db.ListOptions) ([]string, in
}
repoNamesToCheck = repoNamesToCheck[:0]
- if !globUser.Match(info.Name()) {
+ if !globUser.Match(name) {
return filepath.SkipDir
}
- userName = info.Name()
+ userName = name
return nil
}
- name := info.Name()
-
if !strings.HasSuffix(name, ".git") {
return filepath.SkipDir
}
@@ -339,7 +337,7 @@ func ListUnadoptedRepositories(query string, opts *db.ListOptions) ([]string, in
}
repoNamesToCheck = append(repoNamesToCheck, name)
- if len(repoNamesToCheck) > setting.Database.IterateBufferSize {
+ if len(repoNamesToCheck) >= setting.Database.IterateBufferSize {
if err = checkUnadoptedRepositories(userName, repoNamesToCheck, unadopted); err != nil {
return err
}
diff --git a/services/repository/adopt_test.go b/services/repository/adopt_test.go
index 685bfe9bc4824..be8897693eb8f 100644
--- a/services/repository/adopt_test.go
+++ b/services/repository/adopt_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
@@ -19,7 +18,7 @@ import (
func TestCheckUnadoptedRepositories_Add(t *testing.T) {
start := 10
end := 20
- unadopted := &unadoptedRrepositories{
+ unadopted := &unadoptedRepositories{
start: start,
end: end,
index: 0,
@@ -39,7 +38,7 @@ func TestCheckUnadoptedRepositories(t *testing.T) {
//
// Non existent user
//
- unadopted := &unadoptedRrepositories{start: 0, end: 100}
+ unadopted := &unadoptedRepositories{start: 0, end: 100}
err := checkUnadoptedRepositories("notauser", []string{"repo"}, unadopted)
assert.NoError(t, err)
assert.Equal(t, 0, len(unadopted.repositories))
@@ -50,14 +49,14 @@ func TestCheckUnadoptedRepositories(t *testing.T) {
userName := "user2"
repoName := "repo2"
unadoptedRepoName := "unadopted"
- unadopted = &unadoptedRrepositories{start: 0, end: 100}
+ unadopted = &unadoptedRepositories{start: 0, end: 100}
err = checkUnadoptedRepositories(userName, []string{repoName, unadoptedRepoName}, unadopted)
assert.NoError(t, err)
assert.Equal(t, []string{path.Join(userName, unadoptedRepoName)}, unadopted.repositories)
//
// Existing (adopted) repository is not returned
//
- unadopted = &unadoptedRrepositories{start: 0, end: 100}
+ unadopted = &unadoptedRepositories{start: 0, end: 100}
err = checkUnadoptedRepositories(userName, []string{repoName}, unadopted)
assert.NoError(t, err)
assert.Equal(t, 0, len(unadopted.repositories))
diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go
index 7c2cf237d5152..1da4425cfc6e9 100644
--- a/services/repository/archiver/archiver.go
+++ b/services/repository/archiver/archiver.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package archiver
@@ -57,6 +56,21 @@ func (ErrUnknownArchiveFormat) Is(err error) bool {
return ok
}
+// RepoRefNotFoundError is returned when a requested reference (commit, tag) was not found.
+type RepoRefNotFoundError struct {
+ RefName string
+}
+
+// Error implements error.
+func (e RepoRefNotFoundError) Error() string {
+ return fmt.Sprintf("unrecognized repository reference: %s", e.RefName)
+}
+
+func (e RepoRefNotFoundError) Is(err error) bool {
+ _, ok := err.(RepoRefNotFoundError)
+ return ok
+}
+
// NewRequest creates an archival request, based on the URI. The
// resulting ArchiveRequest is suitable for being passed to ArchiveRepository()
// if it's determined that the request still needs to be satisfied.
@@ -103,7 +117,7 @@ func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest
}
}
} else {
- return nil, fmt.Errorf("Unknow ref %s type", r.refName)
+ return nil, RepoRefNotFoundError{RefName: r.refName}
}
return r, nil
@@ -115,8 +129,51 @@ func (aReq *ArchiveRequest) GetArchiveName() string {
return strings.ReplaceAll(aReq.refName, "/", "-") + "." + aReq.Type.String()
}
+// Await awaits the completion of an ArchiveRequest. If the archive has
+// already been prepared the method returns immediately. Otherwise an archiver
+// process will be started and its completion awaited. On success the returned
+// RepoArchiver may be used to download the archive. Note that even if the
+// context is cancelled/times out a started archiver will still continue to run
+// in the background.
+func (aReq *ArchiveRequest) Await(ctx context.Context) (*repo_model.RepoArchiver, error) {
+ archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
+ if err != nil {
+ return nil, fmt.Errorf("models.GetRepoArchiver: %w", err)
+ }
+
+ if archiver != nil && archiver.Status == repo_model.ArchiverReady {
+ // Archive already generated, we're done.
+ return archiver, nil
+ }
+
+ if err := StartArchive(aReq); err != nil {
+ return nil, fmt.Errorf("archiver.StartArchive: %w", err)
+ }
+
+ poll := time.NewTicker(time.Second * 1)
+ defer poll.Stop()
+
+ for {
+ select {
+ case <-graceful.GetManager().HammerContext().Done():
+ // System stopped.
+ return nil, graceful.GetManager().HammerContext().Err()
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-poll.C:
+ archiver, err = repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
+ if err != nil {
+ return nil, fmt.Errorf("repo_model.GetRepoArchiver: %w", err)
+ }
+ if archiver != nil && archiver.Status == repo_model.ArchiverReady {
+ return archiver, nil
+ }
+ }
+ }
+}
+
func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
- txCtx, committer, err := db.TxContext()
+ txCtx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return nil, err
}
@@ -147,11 +204,7 @@ func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
}
}
- rPath, err := archiver.RelativePath()
- if err != nil {
- return nil, err
- }
-
+ rPath := archiver.RelativePath()
_, err = storage.RepoArchives.Stat(rPath)
if err == nil {
if archiver.Status == repo_model.ArchiverGenerating {
@@ -164,7 +217,7 @@ func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
}
if !errors.Is(err, os.ErrNotExist) {
- return nil, fmt.Errorf("unable to stat archive: %v", err)
+ return nil, fmt.Errorf("unable to stat archive: %w", err)
}
rd, w := io.Pipe()
@@ -172,10 +225,10 @@ func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
w.Close()
rd.Close()
}()
- done := make(chan error)
- repo, err := repo_model.GetRepositoryByID(archiver.RepoID)
+ done := make(chan error, 1) // Ensure that there is some capacity which will ensure that the goroutine below can always finish
+ repo, err := repo_model.GetRepositoryByID(ctx, archiver.RepoID)
if err != nil {
- return nil, fmt.Errorf("archiver.LoadRepo failed: %v", err)
+ return nil, fmt.Errorf("archiver.LoadRepo failed: %w", err)
}
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
@@ -214,7 +267,7 @@ func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
// TODO: add submodule data to zip
if _, err := storage.RepoArchives.Save(rPath, rd, -1); err != nil {
- return nil, fmt.Errorf("unable to write archive: %v", err)
+ return nil, fmt.Errorf("unable to write archive: %w", err)
}
err = <-done
@@ -284,13 +337,10 @@ func StartArchive(request *ArchiveRequest) error {
}
func deleteOldRepoArchiver(ctx context.Context, archiver *repo_model.RepoArchiver) error {
- p, err := archiver.RelativePath()
- if err != nil {
- return err
- }
if err := repo_model.DeleteRepoArchiver(ctx, archiver); err != nil {
return err
}
+ p := archiver.RelativePath()
if err := storage.RepoArchives.Delete(p); err != nil {
log.Error("delete repo archive file failed: %v", err)
}
diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go
index 24437ce76c2c0..3cd6e813512bc 100644
--- a/services/repository/archiver/archiver_test.go
+++ b/services/repository/archiver/archiver_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package archiver
diff --git a/services/repository/avatar.go b/services/repository/avatar.go
index f51a312e1769d..5fe8bd2c72f88 100644
--- a/services/repository/avatar.go
+++ b/services/repository/avatar.go
@@ -1,12 +1,10 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
import (
"context"
- "crypto/md5"
"fmt"
"image/png"
"io"
@@ -28,12 +26,12 @@ func UploadAvatar(repo *repo_model.Repository, data []byte) error {
return err
}
- newAvatar := fmt.Sprintf("%d-%x", repo.ID, md5.Sum(data))
+ newAvatar := avatar.HashAvatar(repo.ID, data)
if repo.Avatar == newAvatar { // upload the same picture
return nil
}
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
@@ -44,8 +42,8 @@ func UploadAvatar(repo *repo_model.Repository, data []byte) error {
// Users can upload the same image to other repo - prefix it with ID
// Then repo will be removed - only it avatar file will be removed
repo.Avatar = newAvatar
- if err := repo_model.UpdateRepositoryColsCtx(ctx, repo, "avatar"); err != nil {
- return fmt.Errorf("UploadAvatar: Update repository avatar: %v", err)
+ if err := repo_model.UpdateRepositoryCols(ctx, repo, "avatar"); err != nil {
+ return fmt.Errorf("UploadAvatar: Update repository avatar: %w", err)
}
if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error {
@@ -54,12 +52,12 @@ func UploadAvatar(repo *repo_model.Repository, data []byte) error {
}
return err
}); err != nil {
- return fmt.Errorf("UploadAvatar %s failed: Failed to remove old repo avatar %s: %v", repo.RepoPath(), newAvatar, err)
+ return fmt.Errorf("UploadAvatar %s failed: Failed to remove old repo avatar %s: %w", repo.RepoPath(), newAvatar, err)
}
if len(oldAvatarPath) > 0 {
if err := storage.RepoAvatars.Delete(oldAvatarPath); err != nil {
- return fmt.Errorf("UploadAvatar: Failed to remove old repo avatar %s: %v", oldAvatarPath, err)
+ return fmt.Errorf("UploadAvatar: Failed to remove old repo avatar %s: %w", oldAvatarPath, err)
}
}
@@ -76,19 +74,19 @@ func DeleteAvatar(repo *repo_model.Repository) error {
avatarPath := repo.CustomAvatarRelativePath()
log.Trace("DeleteAvatar[%d]: %s", repo.ID, avatarPath)
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
repo.Avatar = ""
- if err := repo_model.UpdateRepositoryColsCtx(ctx, repo, "avatar"); err != nil {
- return fmt.Errorf("DeleteAvatar: Update repository avatar: %v", err)
+ if err := repo_model.UpdateRepositoryCols(ctx, repo, "avatar"); err != nil {
+ return fmt.Errorf("DeleteAvatar: Update repository avatar: %w", err)
}
if err := storage.RepoAvatars.Delete(avatarPath); err != nil {
- return fmt.Errorf("DeleteAvatar: Failed to remove %s: %v", avatarPath, err)
+ return fmt.Errorf("DeleteAvatar: Failed to remove %s: %w", avatarPath, err)
}
return committer.Commit()
@@ -96,7 +94,7 @@ func DeleteAvatar(repo *repo_model.Repository) error {
// RemoveRandomAvatars removes the randomly generated avatars that were created for repositories
func RemoveRandomAvatars(ctx context.Context) error {
- return repo_model.IterateRepository(func(repository *repo_model.Repository) error {
+ return db.Iterate(ctx, nil, func(ctx context.Context, repository *repo_model.Repository) error {
select {
case <-ctx.Done():
return db.ErrCancelledf("before random avatars removed for %s", repository.FullName())
@@ -117,5 +115,5 @@ func generateAvatar(ctx context.Context, templateRepo, generateRepo *repo_model.
return err
}
- return repo_model.UpdateRepositoryColsCtx(ctx, generateRepo, "avatar")
+ return repo_model.UpdateRepositoryCols(ctx, generateRepo, "avatar")
}
diff --git a/services/repository/avatar_test.go b/services/repository/avatar_test.go
index efad392a2dbc0..5ec899ec3f9a7 100644
--- a/services/repository/avatar_test.go
+++ b/services/repository/avatar_test.go
@@ -1,19 +1,17 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
import (
"bytes"
- "crypto/md5"
- "fmt"
"image"
"image/png"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/avatar"
"github.com/stretchr/testify/assert"
)
@@ -25,11 +23,11 @@ func TestUploadAvatar(t *testing.T) {
png.Encode(&buff, myImage)
assert.NoError(t, unittest.PrepareTestDatabase())
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}).(*repo_model.Repository)
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
err := UploadAvatar(repo, buff.Bytes())
assert.NoError(t, err)
- assert.Equal(t, fmt.Sprintf("%d-%x", 10, md5.Sum(buff.Bytes())), repo.Avatar)
+ assert.Equal(t, avatar.HashAvatar(10, buff.Bytes()), repo.Avatar)
}
func TestUploadBigAvatar(t *testing.T) {
@@ -39,7 +37,7 @@ func TestUploadBigAvatar(t *testing.T) {
png.Encode(&buff, myImage)
assert.NoError(t, unittest.PrepareTestDatabase())
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}).(*repo_model.Repository)
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
err := UploadAvatar(repo, buff.Bytes())
assert.Error(t, err)
@@ -52,7 +50,7 @@ func TestDeleteAvatar(t *testing.T) {
png.Encode(&buff, myImage)
assert.NoError(t, unittest.PrepareTestDatabase())
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}).(*repo_model.Repository)
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
err := UploadAvatar(repo, buff.Bytes())
assert.NoError(t, err)
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 6667cdee61a2d..291fb4a92b374 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
@@ -11,6 +10,8 @@ import (
"strings"
"code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
@@ -35,13 +36,13 @@ func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_mode
if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
Remote: repo.RepoPath(),
- Branch: fmt.Sprintf("%s:%s%s", oldBranchName, git.BranchPrefix, branchName),
- Env: models.PushingEnvironment(doer, repo),
+ Branch: fmt.Sprintf("%s%s:%s%s", git.BranchPrefix, oldBranchName, git.BranchPrefix, branchName),
+ Env: repo_module.PushingEnvironment(doer, repo),
}); err != nil {
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
return err
}
- return fmt.Errorf("Push: %v", err)
+ return fmt.Errorf("Push: %w", err)
}
return nil
@@ -93,12 +94,12 @@ func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo
if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
Remote: repo.RepoPath(),
Branch: fmt.Sprintf("%s:%s%s", commit, git.BranchPrefix, branchName),
- Env: models.PushingEnvironment(doer, repo),
+ Env: repo_module.PushingEnvironment(doer, repo),
}); err != nil {
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
return err
}
- return fmt.Errorf("Push: %v", err)
+ return fmt.Errorf("Push: %w", err)
}
return nil
@@ -118,7 +119,7 @@ func RenameBranch(repo *repo_model.Repository, doer *user_model.User, gitRepo *g
return "from_not_exist", nil
}
- if err := models.RenameBranch(repo, from, to, func(isDefault bool) error {
+ if err := git_model.RenameBranch(db.DefaultContext, repo, from, to, func(isDefault bool) error {
err2 := gitRepo.RenameBranch(from, to)
if err2 != nil {
return err2
@@ -140,16 +141,15 @@ func RenameBranch(repo *repo_model.Repository, doer *user_model.User, gitRepo *g
return "", err
}
- notification.NotifyDeleteRef(doer, repo, "branch", git.BranchPrefix+from)
- notification.NotifyCreateRef(doer, repo, "branch", git.BranchPrefix+to, refID)
+ notification.NotifyDeleteRef(db.DefaultContext, doer, repo, "branch", git.BranchPrefix+from)
+ notification.NotifyCreateRef(db.DefaultContext, doer, repo, "branch", git.BranchPrefix+to, refID)
return "", nil
}
// enmuerates all branch related errors
var (
- ErrBranchIsDefault = errors.New("branch is default")
- ErrBranchIsProtected = errors.New("branch is protected")
+ ErrBranchIsDefault = errors.New("branch is default")
)
// DeleteBranch delete branch
@@ -158,13 +158,12 @@ func DeleteBranch(doer *user_model.User, repo *repo_model.Repository, gitRepo *g
return ErrBranchIsDefault
}
- isProtected, err := models.IsProtectedBranch(repo.ID, branchName)
+ isProtected, err := git_model.IsBranchProtected(db.DefaultContext, repo.ID, branchName)
if err != nil {
return err
}
-
if isProtected {
- return ErrBranchIsProtected
+ return git_model.ErrBranchIsProtected
}
commit, err := gitRepo.GetBranchCommit(branchName)
@@ -196,7 +195,7 @@ func DeleteBranch(doer *user_model.User, repo *repo_model.Repository, gitRepo *g
log.Error("Update: %v", err)
}
- if err := models.AddDeletedBranch(repo.ID, branchName, commit.ID.String(), doer.ID); err != nil {
+ if err := git_model.AddDeletedBranch(db.DefaultContext, repo.ID, branchName, commit.ID.String(), doer.ID); err != nil {
log.Warn("AddDeletedBranch: %v", err)
}
diff --git a/services/repository/cache.go b/services/repository/cache.go
index 5b0c929be1a48..6fd4fa7250f50 100644
--- a/services/repository/cache.go
+++ b/services/repository/cache.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
@@ -34,15 +33,13 @@ func CacheRef(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Rep
return err
}
- commitsCount, err := cache.GetInt64(repo.GetCommitsCountCacheKey(getRefName(fullRefName), true), commit.CommitsCount)
- if err != nil {
- return err
+ if gitRepo.LastCommitCache == nil {
+ commitsCount, err := cache.GetInt64(repo.GetCommitsCountCacheKey(getRefName(fullRefName), true), commit.CommitsCount)
+ if err != nil {
+ return err
+ }
+ gitRepo.LastCommitCache = git.NewLastCommitCache(commitsCount, repo.FullName(), gitRepo, cache.GetCache())
}
- if commitsCount < setting.CacheService.LastCommit.CommitsCount {
- return nil
- }
-
- commitCache := git.NewLastCommitCache(repo.FullName(), gitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache())
- return commitCache.CacheCommit(ctx, commit)
+ return commit.CacheCommit(ctx)
}
diff --git a/services/repository/check.go b/services/repository/check.go
index 6fb86d0dc3c25..e9d65aea4a8ce 100644
--- a/services/repository/check.go
+++ b/services/repository/check.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
@@ -11,41 +10,32 @@ 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"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
-// GitFsck calls 'git fsck' to check repository health.
-func GitFsck(ctx context.Context, timeout time.Duration, args []string) error {
+// GitFsckRepos calls 'git fsck' to check repository health.
+func GitFsckRepos(ctx context.Context, timeout time.Duration, args []git.CmdArg) error {
log.Trace("Doing: GitFsck")
if err := db.Iterate(
ctx,
- new(repo_model.Repository),
builder.Expr("id>0 AND is_fsck_enabled=?", true),
- func(idx int, bean interface{}) error {
- repo := bean.(*repo_model.Repository)
+ func(ctx context.Context, repo *repo_model.Repository) error {
select {
case <-ctx.Done():
return db.ErrCancelledf("before fsck of %s", repo.FullName())
default:
}
- log.Trace("Running health check on repository %v", repo)
- 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 {
- log.Error("CreateRepositoryNotice: %v", err)
- }
- }
- return nil
+ return GitFsckRepo(ctx, repo, timeout, args)
},
); err != nil {
log.Trace("Error: GitFsck: %v", err)
@@ -56,48 +46,35 @@ func GitFsck(ctx context.Context, timeout time.Duration, args []string) error {
return nil
}
+// GitFsckRepo calls 'git fsck' to check an individual repository's health.
+func GitFsckRepo(ctx context.Context, repo *repo_model.Repository, timeout time.Duration, args []git.CmdArg) error {
+ log.Trace("Running health check on repository %-v", repo)
+ 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 = system_model.CreateRepositoryNotice("Failed to health check repository (%s): %v", repo.FullName(), err); err != nil {
+ log.Error("CreateRepositoryNotice: %v", err)
+ }
+ }
+ return nil
+}
+
// 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,
- new(repo_model.Repository),
builder.Gt{"id": 0},
- func(idx int, bean interface{}) error {
- repo := bean.(*repo_model.Repository)
+ func(ctx context.Context, repo *repo_model.Repository) error {
select {
case <-ctx.Done():
return db.ErrCancelledf("before GC of %s", repo.FullName())
default:
}
- log.Trace("Running git gc on %v", repo)
- command := git.NewCommand(ctx, args...).
- SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName()))
- var stdout string
- var err error
- stdout, _, err = command.RunStdString(&git.RunOpts{Timeout: timeout, Dir: repo.RepoPath()})
-
- 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 {
- log.Error("CreateRepositoryNotice: %v", err)
- }
- return fmt.Errorf("Repository garbage collection failed in repo: %s: Error: %v", repo.FullName(), err)
- }
-
- // Now update the size of the repository
- if err := models.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 {
- log.Error("CreateRepositoryNotice: %v", err)
- }
- return fmt.Errorf("Updating size as part of garbage collection failed in repo: %s: Error: %v", repo.FullName(), err)
- }
-
+ // we can ignore the error here because it will be logged in GitGCRepo
+ _ = GitGcRepo(ctx, repo, timeout, args)
return nil
},
); err != nil {
@@ -108,14 +85,43 @@ func GitGcRepos(ctx context.Context, timeout time.Duration, args ...string) erro
return nil
}
+// GitGcRepo calls 'git gc' to remove unnecessary files and optimize the local repository
+func GitGcRepo(ctx context.Context, repo *repo_model.Repository, timeout time.Duration, args []git.CmdArg) error {
+ log.Trace("Running git gc on %-v", repo)
+ command := git.NewCommand(ctx, args...).
+ SetDescription(fmt.Sprintf("Repository Garbage Collection: %s", repo.FullName()))
+ var stdout string
+ var err error
+ stdout, _, err = command.RunStdString(&git.RunOpts{Timeout: timeout, Dir: repo.RepoPath()})
+
+ 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 := system_model.CreateRepositoryNotice(desc); err != nil {
+ log.Error("CreateRepositoryNotice: %v", err)
+ }
+ return fmt.Errorf("Repository garbage collection failed in repo: %s: Error: %w", repo.FullName(), err)
+ }
+
+ // Now update the size of the repository
+ 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 := 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: %w", repo.FullName(), err)
+ }
+
+ return nil
+}
+
func gatherMissingRepoRecords(ctx context.Context) ([]*repo_model.Repository, error) {
repos := make([]*repo_model.Repository, 0, 10)
if err := db.Iterate(
ctx,
- new(repo_model.Repository),
builder.Gt{"id": 0},
- func(idx int, bean interface{}) error {
- repo := bean.(*repo_model.Repository)
+ func(ctx context.Context, repo *repo_model.Repository) error {
select {
case <-ctx.Done():
return db.ErrCancelledf("during gathering missing repo records before checking %s", repo.FullName())
@@ -134,7 +140,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
@@ -161,8 +167,8 @@ 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 {
+ log.Error("Failed to DeleteRepository %-v: Error: %v", repo, err)
+ if err2 := system_model.CreateRepositoryNotice("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil {
log.Error("CreateRepositoryNotice: %v", err)
}
}
@@ -190,7 +196,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/cherry_pick.go b/services/repository/files/cherry_pick.go
index 0107d99e665a6..6bc67e2636992 100644
--- a/services/repository/files/cherry_pick.go
+++ b/services/repository/files/cherry_pick.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -51,7 +50,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
} else {
lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID)
if err != nil {
- return nil, fmt.Errorf("CherryPick: Invalid last commit ID: %v", err)
+ return nil, fmt.Errorf("CherryPick: Invalid last commit ID: %w", err)
}
opts.LastCommitID = lastCommitID.String()
if commit.ID.String() != opts.LastCommitID {
@@ -81,7 +80,7 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
conflict, _, err := pull.AttemptThreeWayMerge(ctx,
t.basePath, t.gitRepo, base, opts.LastCommitID, right, description)
if err != nil {
- return nil, fmt.Errorf("failed to three-way merge %s onto %s: %v", right, opts.OldBranch, err)
+ return nil, fmt.Errorf("failed to three-way merge %s onto %s: %w", right, opts.OldBranch, err)
}
if conflict {
diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go
index e7604e3f924d7..9d237f1e222b5 100644
--- a/services/repository/files/commit.go
+++ b/services/repository/files/commit.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -8,40 +7,50 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/services/automerge"
)
// CreateCommitStatus creates a new CommitStatus given a bunch of parameters
// NOTE: All text-values will be trimmed from whitespaces.
// Requires: Repo, Creator, SHA
-func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *models.CommitStatus) error {
+func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creator *user_model.User, sha string, status *git_model.CommitStatus) error {
repoPath := repo.RepoPath()
// confirm that commit is exist
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath())
if err != nil {
- return fmt.Errorf("OpenRepository[%s]: %v", repoPath, err)
+ return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err)
}
defer closer.Close()
- if _, err := gitRepo.GetCommit(sha); err != nil {
+ if commit, err := gitRepo.GetCommit(sha); err != nil {
gitRepo.Close()
- return fmt.Errorf("GetCommit[%s]: %v", sha, err)
+ return fmt.Errorf("GetCommit[%s]: %w", sha, err)
+ } else if len(sha) != git.SHAFullLength {
+ // use complete commit sha
+ sha = commit.ID.String()
}
gitRepo.Close()
- if err := models.NewCommitStatus(models.NewCommitStatusOptions{
+ if err := git_model.NewCommitStatus(ctx, git_model.NewCommitStatusOptions{
Repo: repo,
Creator: creator,
SHA: sha,
CommitStatus: status,
}); err != nil {
- return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", repo.ID, creator.ID, sha, err)
+ return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
+ }
+
+ if status.State.IsSuccess() {
+ if err := automerge.MergeScheduledPullRequest(ctx, sha, repo); err != nil {
+ return fmt.Errorf("MergeScheduledPullRequest[repo_id: %d, user_id: %d, sha: %s]: %w", repo.ID, creator.ID, sha, err)
+ }
}
return nil
diff --git a/services/repository/files/content.go b/services/repository/files/content.go
index 9037a84349e5a..a311625f31593 100644
--- a/services/repository/files/content.go
+++ b/services/repository/files/content.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -101,6 +100,22 @@ func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, treePat
return fileList, nil
}
+// GetObjectTypeFromTreeEntry check what content is behind it
+func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
+ switch {
+ case entry.IsDir():
+ return ContentTypeDir
+ case entry.IsSubModule():
+ return ContentTypeSubmodule
+ case entry.IsExecutable(), entry.IsRegular():
+ return ContentTypeRegular
+ case entry.IsLink():
+ return ContentTypeLink
+ default:
+ return ""
+ }
+}
+
// GetContents gets the meta data on a file's contents. Ref can be a branch, commit or tag
func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref string, forList bool) (*api.ContentsResponse, error) {
if ref == "" {
@@ -149,13 +164,24 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
}
selfURLString := selfURL.String()
+ err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(ref, refType != git.ObjectCommit), repo.FullName(), commitID)
+ if err != nil {
+ return nil, err
+ }
+
+ lastCommit, err := commit.GetCommitByPath(treePath)
+ if err != nil {
+ return nil, err
+ }
+
// All content types have these fields in populated
contentsResponse := &api.ContentsResponse{
- Name: entry.Name(),
- Path: treePath,
- SHA: entry.ID.String(),
- Size: entry.Size(),
- URL: &selfURLString,
+ Name: entry.Name(),
+ Path: treePath,
+ SHA: entry.ID.String(),
+ LastCommitSHA: lastCommit.ID.String(),
+ Size: entry.Size(),
+ URL: &selfURLString,
Links: &api.FileLinksResponse{
Self: &selfURLString,
},
@@ -164,7 +190,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
// Now populate the rest of the ContentsResponse based on entry type
if entry.IsRegular() || entry.IsExecutable() {
contentsResponse.Type = string(ContentTypeRegular)
- if blobResponse, err := GetBlobBySHA(ctx, repo, entry.ID.String()); err != nil {
+ if blobResponse, err := GetBlobBySHA(ctx, repo, gitRepo, entry.ID.String()); err != nil {
return nil, err
} else if !forList {
// We don't show the content if we are getting a list of FileContentResponses
@@ -220,12 +246,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
}
// GetBlobBySHA get the GitBlobResponse of a repository using a sha hash.
-func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, sha string) (*api.GitBlobResponse, error) {
- gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath())
- if err != nil {
- return nil, err
- }
- defer closer.Close()
+func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) {
gitBlob, err := gitRepo.GetBlob(sha)
if err != nil {
return nil, err
diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go
index 8a3e589bdf9b1..a43b71cf31e1a 100644
--- a/services/repository/files/content_test.go
+++ b/services/repository/files/content_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -8,7 +7,9 @@ import (
"path/filepath"
"testing"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
@@ -31,17 +32,18 @@ func getExpectedReadmeContentsResponse() *api.ContentsResponse {
gitURL := "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/" + sha
downloadURL := "https://try.gitea.io/user2/repo1/raw/branch/master/" + treePath
return &api.ContentsResponse{
- Name: treePath,
- Path: treePath,
- SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
- Type: "file",
- Size: 30,
- Encoding: &encoding,
- Content: &content,
- URL: &selfURL,
- HTMLURL: &htmlURL,
- GitURL: &gitURL,
- DownloadURL: &downloadURL,
+ Name: treePath,
+ Path: treePath,
+ SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
+ LastCommitSHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ Type: "file",
+ Size: 30,
+ Encoding: &encoding,
+ Content: &content,
+ URL: &selfURL,
+ HTMLURL: &htmlURL,
+ GitURL: &gitURL,
+ DownloadURL: &downloadURL,
Links: &api.FileLinksResponse{
Self: &selfURL,
GitURL: &gitURL,
@@ -234,7 +236,12 @@ func TestGetBlobBySHA(t *testing.T) {
ctx.SetParams(":id", "1")
ctx.SetParams(":sha", sha)
- gbr, err := GetBlobBySHA(ctx, ctx.Repo.Repository, ctx.Params(":sha"))
+ gitRepo, err := git.OpenRepository(ctx, repo_model.RepoPath(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name))
+ if err != nil {
+ t.Fail()
+ }
+
+ gbr, err := GetBlobBySHA(ctx, ctx.Repo.Repository, gitRepo, ctx.Params(":sha"))
expectedGBR := &api.GitBlobResponse{
Content: "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK",
Encoding: "base64",
diff --git a/services/repository/files/delete.go b/services/repository/files/delete.go
index 781a762d0ff72..faa60bb3bae4f 100644
--- a/services/repository/files/delete.go
+++ b/services/repository/files/delete.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -104,7 +103,7 @@ func DeleteRepoFile(ctx context.Context, repo *repo_model.Repository, doer *user
} else {
lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID)
if err != nil {
- return nil, fmt.Errorf("DeleteRepoFile: Invalid last commit ID: %v", err)
+ return nil, fmt.Errorf("DeleteRepoFile: Invalid last commit ID: %w", err)
}
opts.LastCommitID = lastCommitID.String()
}
@@ -112,7 +111,7 @@ func DeleteRepoFile(ctx context.Context, repo *repo_model.Repository, doer *user
// Get the files in the index
filesInIndex, err := t.LsFiles(opts.TreePath)
if err != nil {
- return nil, fmt.Errorf("DeleteRepoFile: %v", err)
+ return nil, fmt.Errorf("DeleteRepoFile: %w", err)
}
// Find the file we want to delete in the index
diff --git a/services/repository/files/diff.go b/services/repository/files/diff.go
index dbe1bef52b7bc..373249b114541 100644
--- a/services/repository/files/diff.go
+++ b/services/repository/files/diff.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go
index c0a378dc4b0ad..621816e97d424 100644
--- a/services/repository/files/diff_test.go
+++ b/services/repository/files/diff_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -37,6 +36,7 @@ func TestGetDiffPreview(t *testing.T) {
{
Name: "README.md",
OldName: "README.md",
+ NameHash: "8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d",
Index: 1,
Addition: 2,
Deletion: 1,
@@ -123,7 +123,7 @@ func TestGetDiffPreview(t *testing.T) {
assert.NoError(t, err)
bs, err := json.Marshal(diff)
assert.NoError(t, err)
- assert.EqualValues(t, expectedBs, bs)
+ assert.EqualValues(t, string(expectedBs), string(bs))
})
t.Run("empty branch, same results", func(t *testing.T) {
diff --git a/services/repository/files/file.go b/services/repository/files/file.go
index c6c626c9ced7b..ddd64a5399d71 100644
--- a/services/repository/files/file.go
+++ b/services/repository/files/file.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go
index ee0582dfc2170..e1c7d5d7fb409 100644
--- a/services/repository/files/file_test.go
+++ b/services/repository/files/file_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -43,17 +42,18 @@ func getExpectedFileResponse() *api.FileResponse {
downloadURL := setting.AppURL + "user2/repo1/raw/branch/master/" + treePath
return &api.FileResponse{
Content: &api.ContentsResponse{
- Name: treePath,
- Path: treePath,
- SHA: sha,
- Type: "file",
- Size: 30,
- Encoding: &encoding,
- Content: &content,
- URL: &selfURL,
- HTMLURL: &htmlURL,
- GitURL: &gitURL,
- DownloadURL: &downloadURL,
+ Name: treePath,
+ Path: treePath,
+ SHA: sha,
+ LastCommitSHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ Type: "file",
+ Size: 30,
+ Encoding: &encoding,
+ Content: &content,
+ URL: &selfURL,
+ HTMLURL: &htmlURL,
+ GitURL: &gitURL,
+ DownloadURL: &downloadURL,
Links: &api.FileLinksResponse{
Self: &selfURL,
GitURL: &gitURL,
diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go
index 240cb4fe2c603..73ee0fa815e4c 100644
--- a/services/repository/files/patch.go
+++ b/services/repository/files/patch.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -10,6 +9,7 @@ import (
"strings"
"code.gitea.io/gitea/models"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
@@ -66,13 +66,16 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode
return err
}
} else {
- protectedBranch, err := models.GetProtectedBranchBy(repo.ID, opts.OldBranch)
+ protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, opts.OldBranch)
if err != nil {
return err
}
- if protectedBranch != nil && !protectedBranch.CanUserPush(doer.ID) {
- return models.ErrUserCannotCommit{
- UserName: doer.LowerName,
+ if protectedBranch != nil {
+ protectedBranch.Repo = repo
+ if !protectedBranch.CanUserPush(ctx, doer) {
+ return models.ErrUserCannotCommit{
+ UserName: doer.LowerName,
+ }
}
}
if protectedBranch != nil && protectedBranch.RequireSignedCommits {
@@ -124,7 +127,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
} else {
lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID)
if err != nil {
- return nil, fmt.Errorf("ApplyPatch: Invalid last commit ID: %v", err)
+ return nil, fmt.Errorf("ApplyPatch: Invalid last commit ID: %w", err)
}
opts.LastCommitID = lastCommitID.String()
if commit.ID.String() != opts.LastCommitID {
@@ -138,7 +141,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")
@@ -151,7 +154,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
Stderr: stderr,
Stdin: strings.NewReader(opts.Content),
}); err != nil {
- return nil, fmt.Errorf("Error: Stdout: %s\nStderr: %s\nErr: %v", stdout.String(), stderr.String(), err)
+ return nil, fmt.Errorf("Error: Stdout: %s\nStderr: %s\nErr: %w", stdout.String(), stderr.String(), err)
}
// Now write the tree
diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go
index 8ebf991382278..1f3375cdcc83c 100644
--- a/services/repository/files/temp_repo.go
+++ b/services/repository/files/temp_repo.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -19,6 +18,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/gitdiff"
@@ -34,7 +34,7 @@ type TemporaryUploadRepository struct {
// NewTemporaryUploadRepository creates a new temporary upload repository
func NewTemporaryUploadRepository(ctx context.Context, repo *repo_model.Repository) (*TemporaryUploadRepository, error) {
- basePath, err := models.CreateTemporaryPath("upload")
+ basePath, err := repo_module.CreateTemporaryPath("upload")
if err != nil {
return nil, err
}
@@ -45,14 +45,14 @@ func NewTemporaryUploadRepository(ctx context.Context, repo *repo_model.Reposito
// Close the repository cleaning up all files
func (t *TemporaryUploadRepository) Close() {
defer t.gitRepo.Close()
- if err := models.RemoveTemporaryPath(t.basePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(t.basePath); err != nil {
log.Error("Failed to remove temporary path %s: %v", t.basePath, err)
}
}
// 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{
@@ -66,7 +66,7 @@ func (t *TemporaryUploadRepository) Clone(branch string) error {
Name: t.repo.Name,
}
} else {
- return fmt.Errorf("Clone: %v %s", err, stderr)
+ return fmt.Errorf("Clone: %w %s", err, stderr)
}
}
gitRepo, err := git.OpenRepository(t.ctx, t.basePath)
@@ -93,7 +93,7 @@ func (t *TemporaryUploadRepository) Init() error {
// SetDefaultIndex sets the git index to our HEAD
func (t *TemporaryUploadRepository) SetDefaultIndex() error {
if _, _, err := git.NewCommand(t.ctx, "read-tree", "HEAD").RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil {
- return fmt.Errorf("SetDefaultIndex: %v", err)
+ return fmt.Errorf("SetDefaultIndex: %w", err)
}
return nil
}
@@ -103,21 +103,14 @@ 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,
Stderr: stdErr,
}); err != nil {
log.Error("Unable to run git ls-files for temporary repo: %s (%s) Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), t.basePath, err, stdOut.String(), stdErr.String())
- err = fmt.Errorf("Unable to run git ls-files for temporary repo of: %s Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String())
+ err = fmt.Errorf("Unable to run git ls-files for temporary repo of: %s Error: %w\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String())
return nil, err
}
@@ -150,7 +143,7 @@ func (t *TemporaryUploadRepository) RemoveFilesFromIndex(filenames ...string) er
Stderr: stdErr,
}); err != nil {
log.Error("Unable to update-index for temporary repo: %s (%s) Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), t.basePath, err, stdOut.String(), stdErr.String())
- return fmt.Errorf("Unable to update-index for temporary repo: %s Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String())
+ return fmt.Errorf("Unable to update-index for temporary repo: %s Error: %w\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String())
}
return nil
}
@@ -168,7 +161,7 @@ func (t *TemporaryUploadRepository) HashObject(content io.Reader) (string, error
Stderr: stdErr,
}); err != nil {
log.Error("Unable to hash-object to temporary repo: %s (%s) Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), t.basePath, err, stdOut.String(), stdErr.String())
- return "", fmt.Errorf("Unable to hash-object to temporary repo: %s Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String())
+ return "", fmt.Errorf("Unable to hash-object to temporary repo: %s Error: %w\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String())
}
return strings.TrimSpace(stdOut.String()), nil
@@ -176,7 +169,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{
@@ -185,7 +178,7 @@ func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPat
}
}
log.Error("Unable to add object to index: %s %s %s in temporary repo %s(%s) Error: %v", mode, objectHash, objectPath, t.repo.FullName(), t.basePath, err)
- return fmt.Errorf("Unable to add object to index at %s in temporary repo %s Error: %v", objectPath, t.repo.FullName(), err)
+ return fmt.Errorf("Unable to add object to index at %s in temporary repo %s Error: %w", objectPath, t.repo.FullName(), err)
}
return nil
}
@@ -195,7 +188,7 @@ func (t *TemporaryUploadRepository) WriteTree() (string, error) {
stdout, _, err := git.NewCommand(t.ctx, "write-tree").RunStdString(&git.RunOpts{Dir: t.basePath})
if err != nil {
log.Error("Unable to write tree in temporary repo: %s(%s): Error: %v", t.repo.FullName(), t.basePath, err)
- return "", fmt.Errorf("Unable to write-tree in temporary repo for: %s Error: %v", t.repo.FullName(), err)
+ return "", fmt.Errorf("Unable to write-tree in temporary repo for: %s Error: %w", t.repo.FullName(), err)
}
return strings.TrimSpace(stdout), nil
}
@@ -210,10 +203,10 @@ 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)
+ return "", fmt.Errorf("Unable to rev-parse %s in temporary repo for: %s Error: %w", ref, t.repo.FullName(), err)
}
return strings.TrimSpace(stdout), nil
}
@@ -228,11 +221,6 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co
authorSig := author.NewGitSig()
committerSig := committer.NewGitSig()
- err := git.LoadGitVersion()
- if err != nil {
- return "", fmt.Errorf("Unable to get git version: %v", err)
- }
-
// Because this may call hooks we should pass in the environment
env := append(os.Environ(),
"GIT_AUTHOR_NAME="+authorSig.Name,
@@ -245,41 +233,38 @@ 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)}
}
- // Determine if we should sign
- if git.CheckGitVersionAtLeast("1.7.9") == nil {
- var sign bool
- var keyID string
- var signer *git.Signature
- if parent != "" {
- sign, keyID, signer, _ = asymkey_service.SignCRUDAction(t.ctx, t.repo.RepoPath(), author, t.basePath, parent)
- } else {
- sign, keyID, signer, _ = asymkey_service.SignInitialCommit(t.ctx, t.repo.RepoPath(), author)
- }
- if sign {
- args = append(args, "-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
- _, _ = messageBytes.WriteString("\n")
- _, _ = messageBytes.WriteString("Co-authored-by: ")
- _, _ = messageBytes.WriteString(committerSig.String())
- _, _ = messageBytes.WriteString("\n")
- _, _ = messageBytes.WriteString("Co-committed-by: ")
- _, _ = messageBytes.WriteString(committerSig.String())
- _, _ = messageBytes.WriteString("\n")
- }
- committerSig = signer
+ var sign bool
+ var keyID string
+ var signer *git.Signature
+ if parent != "" {
+ sign, keyID, signer, _ = asymkey_service.SignCRUDAction(t.ctx, t.repo.RepoPath(), author, t.basePath, parent)
+ } else {
+ sign, keyID, signer, _ = asymkey_service.SignInitialCommit(t.ctx, t.repo.RepoPath(), author)
+ }
+ if sign {
+ 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
+ _, _ = messageBytes.WriteString("\n")
+ _, _ = messageBytes.WriteString("Co-authored-by: ")
+ _, _ = messageBytes.WriteString(committerSig.String())
+ _, _ = messageBytes.WriteString("\n")
+ _, _ = messageBytes.WriteString("Co-committed-by: ")
+ _, _ = messageBytes.WriteString(committerSig.String())
+ _, _ = messageBytes.WriteString("\n")
}
- } else if git.CheckGitVersionAtLeast("2.0.0") == nil {
- args = append(args, "--no-gpg-sign")
+ committerSig = signer
}
+ } else {
+ args = append(args, "--no-gpg-sign")
}
if signoff {
@@ -306,7 +291,7 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co
}); err != nil {
log.Error("Unable to commit-tree in temporary repo: %s (%s) Error: %v\nStdout: %s\nStderr: %s",
t.repo.FullName(), t.basePath, err, stdout, stderr)
- return "", fmt.Errorf("Unable to commit-tree in temporary repo: %s Error: %v\nStdout: %s\nStderr: %s",
+ return "", fmt.Errorf("Unable to commit-tree in temporary repo: %s Error: %w\nStdout: %s\nStderr: %s",
t.repo.FullName(), err, stdout, stderr)
}
return strings.TrimSpace(stdout.String()), nil
@@ -315,7 +300,7 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co
// Push the provided commitHash to the repository branch by the provided user
func (t *TemporaryUploadRepository) Push(doer *user_model.User, commitHash, branch string) error {
// Because calls hooks we need to pass in the environment
- env := models.PushingEnvironment(doer, t.repo)
+ env := repo_module.PushingEnvironment(doer, t.repo)
if err := git.Push(t.ctx, t.basePath, git.PushOptions{
Remote: t.repo.RepoPath(),
Branch: strings.TrimSpace(commitHash) + ":" + git.BranchPrefix + strings.TrimSpace(branch),
@@ -342,7 +327,7 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) {
stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil {
log.Error("Unable to open stdout pipe: %v", err)
- return nil, fmt.Errorf("Unable to open stdout pipe: %v", err)
+ return nil, fmt.Errorf("Unable to open stdout pipe: %w", err)
}
defer func() {
_ = stdoutReader.Close()
@@ -375,7 +360,7 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) {
}
log.Error("Unable to run diff-index pipeline in temporary repo %s (%s). Error: %v\nStderr: %s",
t.repo.FullName(), t.basePath, err, stderr)
- return nil, fmt.Errorf("Unable to run diff-index pipeline in temporary repo %s. Error: %v\nStderr: %s",
+ return nil, fmt.Errorf("Unable to run diff-index pipeline in temporary repo %s. Error: %w\nStderr: %s",
t.repo.FullName(), err, stderr)
}
diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go
index caad732887303..f4304ea6306ee 100644
--- a/services/repository/files/tree.go
+++ b/services/repository/files/tree.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -29,7 +28,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
tree.URL = repo.APIURL() + "/git/trees/" + url.PathEscape(tree.SHA)
var entries git.Entries
if recursive {
- entries, err = gitTree.ListEntriesRecursive()
+ entries, err = gitTree.ListEntriesRecursiveWithSize()
} else {
entries, err = gitTree.ListEntries()
}
@@ -50,7 +49,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
copy(treeURL[apiURLLen:], "/git/trees/")
// 40 is the size of the sha1 hash in hexadecimal format.
- copyPos := len(treeURL) - 40
+ copyPos := len(treeURL) - git.SHAFullLength
if perPage <= 0 || perPage > setting.API.DefaultGitTreesPerPage {
perPage = setting.API.DefaultGitTreesPerPage
diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go
index e900480d354dd..a500dbdb22417 100644
--- a/services/repository/files/tree_test.go
+++ b/services/repository/files/tree_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
diff --git a/services/repository/files/update.go b/services/repository/files/update.go
index 2cb40aac47ac6..58b7a5e082b58 100644
--- a/services/repository/files/update.go
+++ b/services/repository/files/update.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -13,6 +12,8 @@ import (
"time"
"code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/charset"
@@ -75,8 +76,8 @@ func detectEncodingAndBOM(entry *git.TreeEntry, repo *repo_model.Repository) (st
if setting.LFS.StartServer {
pointer, _ := lfs.ReadPointerFromBuffer(buf)
if pointer.IsValid() {
- meta, err := models.GetLFSMetaObjectByOid(repo.ID, pointer.Oid)
- if err != nil && err != models.ErrLFSObjectNotExist {
+ meta, err := git_model.GetLFSMetaObjectByOid(db.DefaultContext, repo.ID, pointer.Oid)
+ if err != nil && err != git_model.ErrLFSObjectNotExist {
// return default
return "UTF-8", false
}
@@ -225,7 +226,7 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
} else {
lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID)
if err != nil {
- return nil, fmt.Errorf("ConvertToSHA1: Invalid last commit ID: %v", err)
+ return nil, fmt.Errorf("ConvertToSHA1: Invalid last commit ID: %w", err)
}
opts.LastCommitID = lastCommitID.String()
@@ -320,7 +321,7 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
// Get the two paths (might be the same if not moving) from the index if they exist
filesInIndex, err := t.LsFiles(opts.TreePath, opts.FromTreePath)
if err != nil {
- return nil, fmt.Errorf("UpdateRepoFile: %v", err)
+ return nil, fmt.Errorf("UpdateRepoFile: %w", err)
}
// If is a new file (not updating) then the given path shouldn't exist
if opts.IsNewFile {
@@ -364,12 +365,12 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
}
// Reset the opts.Content to our adjusted content to ensure that LFS gets the correct content
opts.Content = content
- var lfsMetaObject *models.LFSMetaObject
+ var lfsMetaObject *git_model.LFSMetaObject
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,
})
@@ -383,7 +384,7 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
if err != nil {
return nil, err
}
- lfsMetaObject = &models.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID}
+ lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repo.ID}
content = pointer.StringContent()
}
}
@@ -423,7 +424,7 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
if lfsMetaObject != nil {
// We have an LFS object - create it
- lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject)
+ lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, lfsMetaObject)
if err != nil {
return nil, err
}
@@ -434,8 +435,8 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
}
if !exist {
if err := contentStore.Put(lfsMetaObject.Pointer, strings.NewReader(opts.Content)); err != nil {
- if _, err2 := models.RemoveLFSMetaObjectByOid(repo.ID, lfsMetaObject.Oid); err2 != nil {
- return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err)
+ if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, lfsMetaObject.Oid); err2 != nil {
+ return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err)
}
return nil, err
}
@@ -462,17 +463,18 @@ func CreateOrUpdateRepoFile(ctx context.Context, repo *repo_model.Repository, do
// VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch
func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName, treePath string) error {
- protectedBranch, err := models.GetProtectedBranchBy(repo.ID, branchName)
+ protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
if err != nil {
return err
}
if protectedBranch != nil {
+ protectedBranch.Repo = repo
isUnprotectedFile := false
glob := protectedBranch.GetUnprotectedFilePatterns()
if len(glob) != 0 {
isUnprotectedFile = protectedBranch.IsUnprotectedFile(glob, treePath)
}
- if !protectedBranch.CanUserPush(doer.ID) && !isUnprotectedFile {
+ if !protectedBranch.CanUserPush(ctx, doer) && !isUnprotectedFile {
return models.ErrUserCannotCommit{
UserName: doer.LowerName,
}
diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go
index ff0448fc87470..e7289dd60d23b 100644
--- a/services/repository/files/upload.go
+++ b/services/repository/files/upload.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package files
@@ -11,7 +10,8 @@ import (
"path"
"strings"
- "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
@@ -31,8 +31,8 @@ type UploadRepoFileOptions struct {
}
type uploadInfo struct {
- upload *models.Upload
- lfsMetaObject *models.LFSMetaObject
+ upload *repo_model.Upload
+ lfsMetaObject *git_model.LFSMetaObject
}
func cleanUpAfterFailure(infos *[]uploadInfo, t *TemporaryUploadRepository, original error) error {
@@ -41,8 +41,8 @@ func cleanUpAfterFailure(infos *[]uploadInfo, t *TemporaryUploadRepository, orig
continue
}
if !info.lfsMetaObject.Existing {
- if _, err := models.RemoveLFSMetaObjectByOid(t.repo.ID, info.lfsMetaObject.Oid); err != nil {
- original = fmt.Errorf("%v, %v", original, err)
+ if _, err := git_model.RemoveLFSMetaObjectByOid(db.DefaultContext, t.repo.ID, info.lfsMetaObject.Oid); err != nil {
+ original = fmt.Errorf("%w, %v", original, err) // We wrap the original error - as this is the underlying error that required the fallback
}
}
}
@@ -55,9 +55,9 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
return nil
}
- uploads, err := models.GetUploadsByUUIDs(opts.Files)
+ uploads, err := repo_model.GetUploadsByUUIDs(opts.Files)
if err != nil {
- return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %v", opts.Files, err)
+ return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err)
}
names := make([]string, len(uploads))
@@ -65,16 +65,16 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
for i, upload := range uploads {
// Check file is not lfs locked, will return nil if lock setting not enabled
filepath := path.Join(opts.TreePath, upload.Name)
- lfsLock, err := models.GetTreePathLock(repo.ID, filepath)
+ lfsLock, err := git_model.GetTreePathLock(ctx, repo.ID, filepath)
if err != nil {
return err
}
if lfsLock != nil && lfsLock.OwnerID != doer.ID {
- u, err := user_model.GetUserByID(lfsLock.OwnerID)
+ u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
if err != nil {
return err
}
- return models.ErrLFSFileLocked{RepoID: repo.ID, Path: filepath, UserName: u.Name}
+ return git_model.ErrLFSFileLocked{RepoID: repo.ID, Path: filepath, UserName: u.Name}
}
names[i] = upload.Name
@@ -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,
})
@@ -133,7 +133,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
if infos[i].lfsMetaObject == nil {
continue
}
- infos[i].lfsMetaObject, err = models.NewLFSMetaObject(infos[i].lfsMetaObject)
+ infos[i].lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, infos[i].lfsMetaObject)
if err != nil {
// OK Now we need to cleanup
return cleanUpAfterFailure(&infos, t, err)
@@ -156,7 +156,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
return err
}
- return models.DeleteUploads(uploads...)
+ return repo_model.DeleteUploads(uploads...)
}
func copyUploadedLFSFileIntoRepository(info *uploadInfo, filename2attribute2info map[string]map[string]string, t *TemporaryUploadRepository, treePath string) error {
@@ -175,7 +175,7 @@ func copyUploadedLFSFileIntoRepository(info *uploadInfo, filename2attribute2info
return err
}
- info.lfsMetaObject = &models.LFSMetaObject{Pointer: pointer, RepositoryID: t.repo.ID}
+ info.lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: t.repo.ID}
if objectHash, err = t.HashObject(strings.NewReader(pointer.StringContent())); err != nil {
return err
diff --git a/services/repository/fork.go b/services/repository/fork.go
index a2ef75bbd0420..ad534be887f1b 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
@@ -10,8 +9,8 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
@@ -22,6 +21,27 @@ import (
"code.gitea.io/gitea/modules/util"
)
+// ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error.
+type ErrForkAlreadyExist struct {
+ Uname string
+ RepoName string
+ ForkName string
+}
+
+// IsErrForkAlreadyExist checks if an error is an ErrForkAlreadyExist.
+func IsErrForkAlreadyExist(err error) bool {
+ _, ok := err.(ErrForkAlreadyExist)
+ return ok
+}
+
+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
@@ -31,12 +51,19 @@ type ForkRepoOptions struct {
// ForkRepository forks a repository
func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts ForkRepoOptions) (*repo_model.Repository, error) {
+ // Fork is prohibited, if user has reached maximum limit of repositories
+ if !owner.CanForkRepo() {
+ return nil, repo_model.ErrReachLimitOfRepo{
+ Limit: owner.MaxRepoCreation,
+ }
+ }
+
forkedRepo, err := repo_model.GetUserFork(ctx, opts.BaseRepo.ID, owner.ID)
if err != nil {
return nil, err
}
if forkedRepo != nil {
- return nil, models.ErrForkAlreadyExist{
+ return nil, ErrForkAlreadyExist{
Uname: owner.Name,
RepoName: opts.BaseRepo.FullName(),
ForkName: forkedRepo.FullName(),
@@ -91,17 +118,17 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
panic(panicErr)
}()
- err = db.WithTx(func(txCtx context.Context) error {
- if err = models.CreateRepository(txCtx, doer, owner, repo, false); err != nil {
+ err = db.WithTx(ctx, func(txCtx context.Context) error {
+ if err = repo_module.CreateRepositoryByExample(txCtx, doer, owner, repo, false); err != nil {
return err
}
- if err = models.IncrementRepoForkNum(txCtx, opts.BaseRepo.ID); err != nil {
+ if err = repo_model.IncrementRepoForkNum(txCtx, opts.BaseRepo.ID); err != nil {
return err
}
// copy lfs files failure should not be ignored
- if err = models.CopyLFS(txCtx, repo, opts.BaseRepo); err != nil {
+ if err = git_model.CopyLFS(txCtx, repo, opts.BaseRepo); err != nil {
return err
}
@@ -109,26 +136,26 @@ 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)
- return fmt.Errorf("git clone: %v", err)
+ return fmt.Errorf("git clone: %w", err)
}
- if err := models.CheckDaemonExportOK(txCtx, repo); err != nil {
- return fmt.Errorf("checkDaemonExportOK: %v", err)
+ if err := repo_module.CheckDaemonExportOK(txCtx, repo); err != nil {
+ return fmt.Errorf("checkDaemonExportOK: %w", err)
}
if stdout, _, err := git.NewCommand(txCtx, "update-server-info").
SetDescription(fmt.Sprintf("ForkRepository(git update-server-info): %s", repo.FullName())).
RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
log.Error("Fork Repository (git update-server-info) failed for %v:\nStdout: %s\nError: %v", repo, stdout, err)
- return fmt.Errorf("git update-server-info: %v", err)
+ return fmt.Errorf("git update-server-info: %w", err)
}
if err = repo_module.CreateDelegateHooks(repoPath); err != nil {
- return fmt.Errorf("createDelegateHooks: %v", err)
+ return fmt.Errorf("createDelegateHooks: %w", err)
}
return nil
})
@@ -139,7 +166,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
}
// even if below operations failed, it could be ignored. And they will be retried
- if err := models.UpdateRepoSize(ctx, repo); err != nil {
+ if err := repo_module.UpdateRepoSize(ctx, repo); err != nil {
log.Error("Failed to update size for repository: %v", err)
}
if err := repo_model.CopyLanguageStat(opts.BaseRepo, repo); err != nil {
@@ -156,15 +183,15 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
}
}
- notification.NotifyForkRepository(doer, opts.BaseRepo, repo)
+ notification.NotifyForkRepository(ctx, doer, opts.BaseRepo, repo)
return repo, nil
}
// ConvertForkToNormalRepository convert the provided repo from a forked repo to normal repo
func ConvertForkToNormalRepository(repo *repo_model.Repository) error {
- err := db.WithTx(func(ctx context.Context) error {
- repo, err := repo_model.GetRepositoryByIDCtx(ctx, repo.ID)
+ err := db.WithTx(db.DefaultContext, func(ctx context.Context) error {
+ repo, err := repo_model.GetRepositoryByID(ctx, repo.ID)
if err != nil {
return err
}
@@ -173,7 +200,7 @@ func ConvertForkToNormalRepository(repo *repo_model.Repository) error {
return nil
}
- if err := models.DecrementRepoForkNum(ctx, repo.ForkID); err != nil {
+ if err := repo_model.DecrementRepoForkNum(ctx, repo.ForkID); err != nil {
log.Error("Unable to decrement repo fork num for old root repo %d of repository %-v whilst converting from fork. Error: %v", repo.ForkID, repo, err)
return err
}
@@ -181,7 +208,7 @@ func ConvertForkToNormalRepository(repo *repo_model.Repository) error {
repo.IsFork = false
repo.ForkID = 0
- if err := models.UpdateRepositoryCtx(ctx, repo, false); err != nil {
+ if err := repo_module.UpdateRepository(ctx, repo, false); err != nil {
log.Error("Unable to update repository %-v whilst converting from fork. Error: %v", repo, err)
return err
}
diff --git a/services/repository/fork_test.go b/services/repository/fork_test.go
index 965887b5d14bc..452798b25b499 100644
--- a/services/repository/fork_test.go
+++ b/services/repository/fork_test.go
@@ -1,17 +1,16 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
import (
"testing"
- "code.gitea.io/gitea/models"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
@@ -20,8 +19,8 @@ func TestForkRepository(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// user 13 has already forked repo10
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13}).(*user_model.User)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}).(*repo_model.Repository)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
fork, err := ForkRepository(git.DefaultContext, user, user, ForkRepoOptions{
BaseRepo: repo,
@@ -30,5 +29,20 @@ func TestForkRepository(t *testing.T) {
})
assert.Nil(t, fork)
assert.Error(t, err)
- assert.True(t, models.IsErrForkAlreadyExist(err))
+ assert.True(t, IsErrForkAlreadyExist(err))
+
+ // user not reached maximum limit of repositories
+ assert.False(t, repo_model.IsErrReachLimitOfRepo(err))
+
+ // change AllowForkWithoutMaximumLimit to false for the test
+ setting.Repository.AllowForkWithoutMaximumLimit = false
+ // user has reached maximum limit of repositories
+ user.MaxRepoCreation = 0
+ fork2, err := ForkRepository(git.DefaultContext, user, user, ForkRepoOptions{
+ BaseRepo: repo,
+ Name: "test",
+ Description: "test",
+ })
+ assert.Nil(t, fork2)
+ assert.True(t, repo_model.IsErrReachLimitOfRepo(err))
}
diff --git a/services/repository/hooks.go b/services/repository/hooks.go
index 67931ffcb64b5..a8b6f7a622280 100644
--- a/services/repository/hooks.go
+++ b/services/repository/hooks.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
@@ -10,6 +9,7 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -24,10 +24,8 @@ func SyncRepositoryHooks(ctx context.Context) error {
if err := db.Iterate(
ctx,
- new(repo_model.Repository),
builder.Gt{"id": 0},
- func(idx int, bean interface{}) error {
- repo := bean.(*repo_model.Repository)
+ func(ctx context.Context, repo *repo_model.Repository) error {
select {
case <-ctx.Done():
return db.ErrCancelledf("before sync repository hooks for %s", repo.FullName())
@@ -35,11 +33,11 @@ func SyncRepositoryHooks(ctx context.Context) error {
}
if err := repo_module.CreateDelegateHooks(repo.RepoPath()); err != nil {
- return fmt.Errorf("SyncRepositoryHook: %v", err)
+ return fmt.Errorf("SyncRepositoryHook: %w", err)
}
if repo.HasWiki() {
if err := repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil {
- return fmt.Errorf("SyncRepositoryHook: %v", err)
+ return fmt.Errorf("SyncRepositoryHook: %w", err)
}
}
return nil
@@ -84,3 +82,29 @@ func GenerateGitHooks(ctx context.Context, templateRepo, generateRepo *repo_mode
}
return nil
}
+
+// GenerateWebhooks generates webhooks from a template repository
+func GenerateWebhooks(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error {
+ templateWebhooks, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{RepoID: templateRepo.ID})
+ if err != nil {
+ return err
+ }
+
+ ws := make([]*webhook.Webhook, 0, len(templateWebhooks))
+ for _, templateWebhook := range templateWebhooks {
+ ws = append(ws, &webhook.Webhook{
+ RepoID: generateRepo.ID,
+ URL: templateWebhook.URL,
+ HTTPMethod: templateWebhook.HTTPMethod,
+ ContentType: templateWebhook.ContentType,
+ Secret: templateWebhook.Secret,
+ HookEvent: templateWebhook.HookEvent,
+ IsActive: templateWebhook.IsActive,
+ Type: templateWebhook.Type,
+ OrgID: templateWebhook.OrgID,
+ Events: templateWebhook.Events,
+ Meta: templateWebhook.Meta,
+ })
+ }
+ return webhook.CreateWebhooks(ctx, ws)
+}
diff --git a/services/repository/lfs.go b/services/repository/lfs.go
new file mode 100644
index 0000000000000..aeb808a72f330
--- /dev/null
+++ b/services/repository/lfs.go
@@ -0,0 +1,140 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ git_model "code.gitea.io/gitea/models/git"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/lfs"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// GarbageCollectLFSMetaObjectsOptions provides options for GarbageCollectLFSMetaObjects function
+type GarbageCollectLFSMetaObjectsOptions struct {
+ Logger log.Logger
+ AutoFix bool
+ OlderThan time.Time
+ UpdatedLessRecentlyThan time.Time
+ NumberToCheckPerRepo int64
+ ProportionToCheckPerRepo float64
+}
+
+// GarbageCollectLFSMetaObjects garbage collects LFS objects for all repositories
+func GarbageCollectLFSMetaObjects(ctx context.Context, opts GarbageCollectLFSMetaObjectsOptions) error {
+ log.Trace("Doing: GarbageCollectLFSMetaObjects")
+ defer log.Trace("Finished: GarbageCollectLFSMetaObjects")
+
+ if !setting.LFS.StartServer {
+ if opts.Logger != nil {
+ opts.Logger.Info("LFS support is disabled")
+ }
+ return nil
+ }
+
+ return git_model.IterateRepositoryIDsWithLFSMetaObjects(ctx, func(ctx context.Context, repoID, count int64) error {
+ repo, err := repo_model.GetRepositoryByID(ctx, repoID)
+ if err != nil {
+ return err
+ }
+
+ if newMinimum := int64(float64(count) * opts.ProportionToCheckPerRepo); newMinimum > opts.NumberToCheckPerRepo && opts.NumberToCheckPerRepo != 0 {
+ opts.NumberToCheckPerRepo = newMinimum
+ }
+ return GarbageCollectLFSMetaObjectsForRepo(ctx, repo, opts)
+ })
+}
+
+// GarbageCollectLFSMetaObjectsForRepo garbage collects LFS objects for a specific repository
+func GarbageCollectLFSMetaObjectsForRepo(ctx context.Context, repo *repo_model.Repository, opts GarbageCollectLFSMetaObjectsOptions) error {
+ if opts.Logger != nil {
+ opts.Logger.Info("Checking %-v", repo)
+ }
+ total, orphaned, collected, deleted := int64(0), 0, 0, 0
+ if opts.Logger != nil {
+ defer func() {
+ if orphaned == 0 {
+ opts.Logger.Info("Found %d total LFSMetaObjects in %-v", total, repo)
+ } else if !opts.AutoFix {
+ opts.Logger.Info("Found %d/%d orphaned LFSMetaObjects in %-v", orphaned, total, repo)
+ } else {
+ opts.Logger.Info("Collected %d/%d orphaned/%d total LFSMetaObjects in %-v. %d removed from storage.", collected, orphaned, total, repo, deleted)
+ }
+ }()
+ }
+
+ gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
+ if err != nil {
+ log.Error("Unable to open git repository %-v: %v", repo, err)
+ return err
+ }
+ defer gitRepo.Close()
+
+ store := lfs.NewContentStore()
+ errStop := errors.New("STOPERR")
+
+ err = git_model.IterateLFSMetaObjectsForRepo(ctx, repo.ID, func(ctx context.Context, metaObject *git_model.LFSMetaObject, count int64) error {
+ if opts.NumberToCheckPerRepo > 0 && total > opts.NumberToCheckPerRepo {
+ return errStop
+ }
+ total++
+ pointerSha := git.ComputeBlobHash([]byte(metaObject.Pointer.StringContent()))
+
+ if gitRepo.IsObjectExist(pointerSha.String()) {
+ return git_model.MarkLFSMetaObject(ctx, metaObject.ID)
+ }
+ orphaned++
+
+ if !opts.AutoFix {
+ return nil
+ }
+ // Non-existent pointer file
+ _, err = git_model.RemoveLFSMetaObjectByOidFn(ctx, repo.ID, metaObject.Oid, func(count int64) error {
+ if count > 0 {
+ return nil
+ }
+
+ if err := store.Delete(metaObject.RelativePath()); err != nil {
+ log.Error("Unable to remove lfs metaobject %s from store: %v", metaObject.Oid, err)
+ }
+ deleted++
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("unable to remove meta-object %s in %s: %w", metaObject.Oid, repo.FullName(), err)
+ }
+ collected++
+
+ return nil
+ }, &git_model.IterateLFSMetaObjectsForRepoOptions{
+ // Only attempt to garbage collect lfs meta objects older than a week as the order of git lfs upload
+ // and git object upload is not necessarily guaranteed. It's possible to imagine a situation whereby
+ // an LFS object is uploaded but the git branch is not uploaded immediately, or there are some rapid
+ // changes in new branches that might lead to lfs objects becoming temporarily unassociated with git
+ // objects.
+ //
+ // It is likely that a week is potentially excessive but it should definitely be enough that any
+ // unassociated LFS object is genuinely unassociated.
+ OlderThan: opts.OlderThan,
+ UpdatedLessRecentlyThan: opts.UpdatedLessRecentlyThan,
+ OrderByUpdated: true,
+ LoopFunctionAlwaysUpdates: true,
+ })
+
+ if err == errStop {
+ if opts.Logger != nil {
+ opts.Logger.Info("Processing stopped at %d total LFSMetaObjects in %-v", total, repo)
+ }
+ return nil
+ } else if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/services/repository/main_test.go b/services/repository/main_test.go
index 42134fa7adabb..007790f2a9626 100644
--- a/services/repository/main_test.go
+++ b/services/repository/main_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
diff --git a/services/repository/push.go b/services/repository/push.go
index 4eb52c18c217f..dc8d564cb4e2f 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
@@ -11,8 +10,8 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
@@ -82,20 +81,20 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PushUpdates: %s/%s", optsList[0].RepoUserName, optsList[0].RepoName))
defer finished()
- repo, err := repo_model.GetRepositoryByOwnerAndName(optsList[0].RepoUserName, optsList[0].RepoName)
+ repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, optsList[0].RepoUserName, optsList[0].RepoName)
if err != nil {
- return fmt.Errorf("GetRepositoryByOwnerAndName failed: %v", err)
+ return fmt.Errorf("GetRepositoryByOwnerAndName failed: %w", err)
}
repoPath := repo.RepoPath()
gitRepo, err := git.OpenRepository(ctx, repoPath)
if err != nil {
- return fmt.Errorf("OpenRepository[%s]: %v", repoPath, err)
+ return fmt.Errorf("OpenRepository[%s]: %w", repoPath, err)
}
defer gitRepo.Close()
- if err = models.UpdateRepoSize(ctx, repo); err != nil {
+ if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
log.Error("Failed to update size for repository: %v", err)
}
@@ -110,14 +109,14 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
if opts.IsTag() { // If is tag reference
if pusher == nil || pusher.ID != opts.PusherID {
var err error
- if pusher, err = user_model.GetUserByID(opts.PusherID); err != nil {
+ if pusher, err = user_model.GetUserByID(ctx, opts.PusherID); err != nil {
return err
}
}
tagName := opts.TagName()
if opts.IsDelRef() {
notification.NotifyPushCommits(
- pusher, repo,
+ db.DefaultContext, pusher, repo,
&repo_module.PushUpdateOptions{
RefFullName: git.TagPrefix + tagName,
OldCommitID: opts.OldCommitID,
@@ -125,11 +124,11 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
}, repo_module.NewPushCommits())
delTags = append(delTags, tagName)
- notification.NotifyDeleteRef(pusher, repo, "tag", opts.RefFullName)
+ notification.NotifyDeleteRef(db.DefaultContext, pusher, repo, "tag", opts.RefFullName)
} else { // is new tag
newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
if err != nil {
- return fmt.Errorf("gitRepo.GetCommit: %v", err)
+ return fmt.Errorf("gitRepo.GetCommit: %w", err)
}
commits := repo_module.NewPushCommits()
@@ -137,7 +136,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
commits.CompareURL = repo.ComposeCompareURL(git.EmptySHA, opts.NewCommitID)
notification.NotifyPushCommits(
- pusher, repo,
+ db.DefaultContext, pusher, repo,
&repo_module.PushUpdateOptions{
RefFullName: git.TagPrefix + tagName,
OldCommitID: git.EmptySHA,
@@ -145,12 +144,12 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
}, commits)
addTags = append(addTags, tagName)
- notification.NotifyCreateRef(pusher, repo, "tag", opts.RefFullName, opts.NewCommitID)
+ notification.NotifyCreateRef(db.DefaultContext, pusher, repo, "tag", opts.RefFullName, opts.NewCommitID)
}
} else if opts.IsBranch() { // If is branch reference
if pusher == nil || pusher.ID != opts.PusherID {
var err error
- if pusher, err = user_model.GetUserByID(opts.PusherID); err != nil {
+ if pusher, err = user_model.GetUserByID(ctx, opts.PusherID); err != nil {
return err
}
}
@@ -162,7 +161,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
if err != nil {
- return fmt.Errorf("gitRepo.GetCommit: %v", err)
+ return fmt.Errorf("gitRepo.GetCommit: %w", err)
}
refName := opts.RefName()
@@ -181,20 +180,20 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
}
}
// Update the is empty and default_branch columns
- if err := repo_model.UpdateRepositoryCols(repo, "default_branch", "is_empty"); err != nil {
- return fmt.Errorf("UpdateRepositoryCols: %v", err)
+ if err := repo_model.UpdateRepositoryCols(db.DefaultContext, repo, "default_branch", "is_empty"); err != nil {
+ return fmt.Errorf("UpdateRepositoryCols: %w", err)
}
}
l, err = newCommit.CommitsBeforeLimit(10)
if err != nil {
- return fmt.Errorf("newCommit.CommitsBeforeLimit: %v", err)
+ return fmt.Errorf("newCommit.CommitsBeforeLimit: %w", err)
}
- notification.NotifyCreateRef(pusher, repo, "branch", opts.RefFullName, opts.NewCommitID)
+ notification.NotifyCreateRef(db.DefaultContext, pusher, repo, "branch", opts.RefFullName, opts.NewCommitID)
} else {
l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID)
if err != nil {
- return fmt.Errorf("newCommit.CommitsBeforeUntil: %v", err)
+ return fmt.Errorf("newCommit.CommitsBeforeUntil: %w", err)
}
isForce, err := repo_module.IsForcePush(ctx, opts)
@@ -219,10 +218,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,9 +245,13 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
commits.CompareURL = ""
}
- notification.NotifyPushCommits(pusher, repo, opts, commits)
+ if len(commits.Commits) > setting.UI.FeedMaxCommitNum {
+ commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
+ }
+
+ notification.NotifyPushCommits(db.DefaultContext, pusher, repo, opts, commits)
- if err = models.RemoveDeletedBranchByName(repo.ID, branch); err != nil {
+ if err = git_model.RemoveDeletedBranchByName(ctx, repo.ID, branch); err != nil {
log.Error("models.RemoveDeletedBranch %s/%s failed: %v", repo.ID, branch, err)
}
@@ -261,7 +260,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
log.Error("repo_module.CacheRef %s/%s failed: %v", repo.ID, branch, err)
}
} else {
- notification.NotifyDeleteRef(pusher, repo, "branch", opts.RefFullName)
+ notification.NotifyDeleteRef(db.DefaultContext, pusher, repo, "branch", opts.RefFullName)
if err = pull_service.CloseBranchPulls(pusher, repo.ID, branch); err != nil {
// close all related pulls
log.Error("close related pull request failed: %v", err)
@@ -269,7 +268,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
}
// Even if user delete a branch on a repository which he didn't watch, he will be watch that.
- if err = repo_model.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil {
+ if err = repo_model.WatchIfAuto(db.DefaultContext, opts.PusherID, repo.ID, true); err != nil {
log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err)
}
} else {
@@ -277,12 +276,12 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
}
}
if err := PushUpdateAddDeleteTags(repo, gitRepo, addTags, delTags); err != nil {
- return fmt.Errorf("PushUpdateAddDeleteTags: %v", err)
+ return fmt.Errorf("PushUpdateAddDeleteTags: %w", err)
}
// Change repository last updated time.
if err := repo_model.UpdateRepositoryUpdatedTime(repo.ID, time.Now()); err != nil {
- return fmt.Errorf("UpdateRepositoryUpdatedTime: %v", err)
+ return fmt.Errorf("UpdateRepositoryUpdatedTime: %w", err)
}
return nil
@@ -290,8 +289,8 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
// PushUpdateAddDeleteTags updates a number of added and delete tags
func PushUpdateAddDeleteTags(repo *repo_model.Repository, gitRepo *git.Repository, addTags, delTags []string) error {
- return db.WithTx(func(ctx context.Context) error {
- if err := models.PushUpdateDeleteTagsContext(ctx, repo, delTags); err != nil {
+ return db.WithTx(db.DefaultContext, func(ctx context.Context) error {
+ if err := repo_model.PushUpdateDeleteTagsContext(ctx, repo, delTags); err != nil {
return err
}
return pushUpdateAddTags(ctx, repo, gitRepo, addTags)
@@ -309,27 +308,27 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
lowerTags = append(lowerTags, strings.ToLower(tag))
}
- releases, err := models.GetReleasesByRepoIDAndNames(ctx, repo.ID, lowerTags)
+ releases, err := repo_model.GetReleasesByRepoIDAndNames(ctx, repo.ID, lowerTags)
if err != nil {
- return fmt.Errorf("GetReleasesByRepoIDAndNames: %v", err)
+ return fmt.Errorf("GetReleasesByRepoIDAndNames: %w", err)
}
- relMap := make(map[string]*models.Release)
+ relMap := make(map[string]*repo_model.Release)
for _, rel := range releases {
relMap[rel.LowerTagName] = rel
}
- newReleases := make([]*models.Release, 0, len(lowerTags)-len(relMap))
+ newReleases := make([]*repo_model.Release, 0, len(lowerTags)-len(relMap))
emailToUser := make(map[string]*user_model.User)
for i, lowerTag := range lowerTags {
tag, err := gitRepo.GetTag(tags[i])
if err != nil {
- return fmt.Errorf("GetTag: %v", err)
+ return fmt.Errorf("GetTag: %w", err)
}
commit, err := tag.Commit(gitRepo)
if err != nil {
- return fmt.Errorf("Commit: %v", err)
+ return fmt.Errorf("Commit: %w", err)
}
sig := tag.Tagger
@@ -348,7 +347,7 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
if !ok {
author, err = user_model.GetUserByEmailContext(ctx, sig.Email)
if err != nil && !user_model.IsErrUserNotExist(err) {
- return fmt.Errorf("GetUserByEmail: %v", err)
+ return fmt.Errorf("GetUserByEmail: %w", err)
}
if author != nil {
emailToUser[sig.Email] = author
@@ -359,13 +358,13 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
commitsCount, err := commit.CommitsCount()
if err != nil {
- return fmt.Errorf("CommitsCount: %v", err)
+ return fmt.Errorf("CommitsCount: %w", err)
}
rel, has := relMap[lowerTag]
if !has {
- rel = &models.Release{
+ rel = &repo_model.Release{
RepoID: repo.ID,
Title: "",
TagName: tags[i],
@@ -392,15 +391,15 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
if rel.IsTag && author != nil {
rel.PublisherID = author.ID
}
- if err = models.UpdateRelease(ctx, rel); err != nil {
- return fmt.Errorf("Update: %v", err)
+ if err = repo_model.UpdateRelease(ctx, rel); err != nil {
+ return fmt.Errorf("Update: %w", err)
}
}
}
if len(newReleases) > 0 {
- if err = models.InsertReleasesContext(ctx, newReleases); err != nil {
- return fmt.Errorf("Insert: %v", err)
+ if err = db.Insert(ctx, newReleases); err != nil {
+ return fmt.Errorf("Insert: %w", err)
}
}
diff --git a/services/repository/repository.go b/services/repository/repository.go
index 685a3c7601ac9..3c3e7e82c3f8f 100644
--- a/services/repository/repository.go
+++ b/services/repository/repository.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
@@ -9,26 +8,30 @@ import (
"fmt"
"code.gitea.io/gitea/models"
+ "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"
"code.gitea.io/gitea/modules/notification"
repo_module "code.gitea.io/gitea/modules/repository"
- cfg "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/setting"
pull_service "code.gitea.io/gitea/services/pull"
)
// CreateRepository creates a repository for the user/organization.
-func CreateRepository(doer, owner *user_model.User, opts models.CreateRepoOptions) (*repo_model.Repository, error) {
+func CreateRepository(doer, owner *user_model.User, opts repo_module.CreateRepoOptions) (*repo_model.Repository, error) {
repo, err := repo_module.CreateRepository(doer, owner, opts)
if err != nil {
// No need to rollback here we should do this in CreateRepository...
return nil, err
}
- notification.NotifyCreateRepository(doer, owner, repo)
+ notification.NotifyCreateRepository(db.DefaultContext, doer, owner, repo)
return repo, nil
}
@@ -41,7 +44,7 @@ func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_mod
if notify {
// If the repo itself has webhooks, we need to trigger them before deleting it...
- notification.NotifyDeleteRepository(doer, repo)
+ notification.NotifyDeleteRepository(ctx, doer, repo)
}
if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil {
@@ -55,7 +58,7 @@ func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_mod
func PushCreateRepo(authUser, owner *user_model.User, repoName string) (*repo_model.Repository, error) {
if !authUser.IsAdmin {
if owner.IsOrganization() {
- if ok, err := organization.CanCreateOrgRepo(owner.ID, authUser.ID); err != nil {
+ if ok, err := organization.CanCreateOrgRepo(db.DefaultContext, owner.ID, authUser.ID); err != nil {
return nil, err
} else if !ok {
return nil, fmt.Errorf("cannot push-create repository for org")
@@ -65,9 +68,9 @@ func PushCreateRepo(authUser, owner *user_model.User, repoName string) (*repo_mo
}
}
- repo, err := CreateRepository(authUser, owner, models.CreateRepoOptions{
+ repo, err := CreateRepository(authUser, owner, repo_module.CreateRepoOptions{
Name: repoName,
- IsPrivate: cfg.Repository.DefaultPushCreatePrivate,
+ IsPrivate: setting.Repository.DefaultPushCreatePrivate,
})
if err != nil {
return nil, err
@@ -76,8 +79,49 @@ func PushCreateRepo(authUser, owner *user_model.User, repoName string) (*repo_mo
return repo, nil
}
-// NewContext start repository service
-func NewContext() error {
+// Init start repository service
+func Init() error {
repo_module.LoadRepoConfig()
+ 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()
}
+
+// UpdateRepository updates a repository
+func UpdateRepository(repo *repo_model.Repository, visibilityChanged bool) (err error) {
+ ctx, committer, err := db.TxContext(db.DefaultContext)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err = repo_module.UpdateRepository(ctx, repo, visibilityChanged); err != nil {
+ return fmt.Errorf("updateRepository: %w", err)
+ }
+
+ return committer.Commit()
+}
+
+// LinkedRepository returns the linked repo if any
+func LinkedRepository(ctx context.Context, a *repo_model.Attachment) (*repo_model.Repository, unit.Type, error) {
+ if a.IssueID != 0 {
+ iss, err := issues_model.GetIssueByID(ctx, a.IssueID)
+ if err != nil {
+ return nil, unit.TypeIssues, err
+ }
+ repo, err := repo_model.GetRepositoryByID(ctx, iss.RepoID)
+ unitType := unit.TypeIssues
+ if iss.IsPull {
+ unitType = unit.TypePullRequests
+ }
+ return repo, unitType, err
+ } else if a.ReleaseID != 0 {
+ rel, err := repo_model.GetReleaseByID(ctx, a.ReleaseID)
+ if err != nil {
+ return nil, unit.TypeReleases, err
+ }
+ repo, err := repo_model.GetRepositoryByID(ctx, rel.RepoID)
+ return repo, unit.TypeReleases, err
+ }
+ return nil, -1, nil
+}
diff --git a/services/repository/repository_test.go b/services/repository/repository_test.go
new file mode 100644
index 0000000000000..892a11a23e9b6
--- /dev/null
+++ b/services/repository/repository_test.go
@@ -0,0 +1,42 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/models/unittest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLinkedRepository(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ testCases := []struct {
+ name string
+ attachID int64
+ expectedRepo *repo_model.Repository
+ expectedUnitType unit.Type
+ }{
+ {"LinkedIssue", 1, &repo_model.Repository{ID: 1}, unit.TypeIssues},
+ {"LinkedComment", 3, &repo_model.Repository{ID: 1}, unit.TypePullRequests},
+ {"LinkedRelease", 9, &repo_model.Repository{ID: 1}, unit.TypeReleases},
+ {"Notlinked", 10, nil, -1},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ attach, err := repo_model.GetAttachmentByID(db.DefaultContext, tc.attachID)
+ assert.NoError(t, err)
+ repo, unitType, err := LinkedRepository(db.DefaultContext, attach)
+ assert.NoError(t, err)
+ if tc.expectedRepo != nil {
+ assert.Equal(t, tc.expectedRepo.ID, repo.ID)
+ }
+ assert.Equal(t, tc.expectedUnitType, unitType)
+ })
+ }
+}
diff --git a/services/repository/review.go b/services/repository/review.go
new file mode 100644
index 0000000000000..d30d61ee06683
--- /dev/null
+++ b/services/repository/review.go
@@ -0,0 +1,23 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
+)
+
+// GetReviewerTeams get all teams can be requested to review
+func GetReviewerTeams(repo *repo_model.Repository) ([]*organization.Team, error) {
+ if err := repo.GetOwner(db.DefaultContext); err != nil {
+ return nil, err
+ }
+ if !repo.Owner.IsOrganization() {
+ return nil, nil
+ }
+
+ return organization.GetTeamsWithAccessToRepo(db.DefaultContext, repo.OwnerID, repo.ID, perm.AccessModeRead)
+}
diff --git a/services/repository/review_test.go b/services/repository/review_test.go
new file mode 100644
index 0000000000000..2bf4cdbf5cdf2
--- /dev/null
+++ b/services/repository/review_test.go
@@ -0,0 +1,27 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+ "testing"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRepoGetReviewerTeams(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
+ teams, err := GetReviewerTeams(repo2)
+ assert.NoError(t, err)
+ assert.Empty(t, teams)
+
+ repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+ teams, err = GetReviewerTeams(repo3)
+ assert.NoError(t, err)
+ assert.Len(t, teams, 2)
+}
diff --git a/services/repository/template.go b/services/repository/template.go
index 28fa1523a5952..13e0749869396 100644
--- a/services/repository/template.go
+++ b/services/repository/template.go
@@ -1,23 +1,45 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
import (
"context"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
repo_module "code.gitea.io/gitea/modules/repository"
)
+// GenerateIssueLabels generates issue labels from a template repository
+func GenerateIssueLabels(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error {
+ templateLabels, err := issues_model.GetLabelsByRepoID(ctx, templateRepo.ID, "", db.ListOptions{})
+ if err != nil {
+ return err
+ }
+ // Prevent insert being called with an empty slice which would result in
+ // err "no element on slice when insert".
+ if len(templateLabels) == 0 {
+ return nil
+ }
+
+ newLabels := make([]*issues_model.Label, 0, len(templateLabels))
+ for _, templateLabel := range templateLabels {
+ newLabels = append(newLabels, &issues_model.Label{
+ RepoID: generateRepo.ID,
+ Name: templateLabel.Name,
+ Description: templateLabel.Description,
+ Color: templateLabel.Color,
+ })
+ }
+ return db.Insert(ctx, newLabels)
+}
+
// GenerateRepository generates a repository from a template
-func GenerateRepository(doer, owner *user_model.User, templateRepo *repo_model.Repository, opts models.GenerateRepoOptions) (_ *repo_model.Repository, err error) {
+func GenerateRepository(doer, owner *user_model.User, templateRepo *repo_model.Repository, opts repo_module.GenerateRepoOptions) (_ *repo_model.Repository, err error) {
if !doer.IsAdmin && !owner.CanCreateRepo() {
return nil, repo_model.ErrReachLimitOfRepo{
Limit: owner.MaxRepoCreation,
@@ -25,7 +47,7 @@ func GenerateRepository(doer, owner *user_model.User, templateRepo *repo_model.R
}
var generateRepo *repo_model.Repository
- if err = db.WithTx(func(ctx context.Context) error {
+ if err = db.WithTx(db.DefaultContext, func(ctx context.Context) error {
generateRepo, err = repo_module.GenerateRepository(ctx, doer, owner, templateRepo, opts)
if err != nil {
return err
@@ -54,7 +76,7 @@ func GenerateRepository(doer, owner *user_model.User, templateRepo *repo_model.R
// Webhooks
if opts.Webhooks {
- if err = models.GenerateWebhooks(ctx, templateRepo, generateRepo); err != nil {
+ if err = GenerateWebhooks(ctx, templateRepo, generateRepo); err != nil {
return err
}
}
@@ -68,22 +90,17 @@ func GenerateRepository(doer, owner *user_model.User, templateRepo *repo_model.R
// Issue Labels
if opts.IssueLabels {
- if err = models.GenerateIssueLabels(ctx, templateRepo, generateRepo); err != nil {
+ if err = GenerateIssueLabels(ctx, templateRepo, generateRepo); err != nil {
return err
}
}
return nil
}); err != nil {
- if generateRepo != nil && generateRepo.ID > 0 {
- if errDelete := models.DeleteRepository(doer, owner.ID, generateRepo.ID); errDelete != nil {
- log.Error("Rollback deleteRepository: %v", errDelete)
- }
- }
return nil, err
}
- notification.NotifyCreateRepository(doer, owner, generateRepo)
+ notification.NotifyCreateRepository(db.DefaultContext, doer, owner, generateRepo)
return generateRepo, nil
}
diff --git a/services/repository/transfer.go b/services/repository/transfer.go
index 0abb03a88d930..f4afb7e2dec28 100644
--- a/services/repository/transfer.go
+++ b/services/repository/transfer.go
@@ -1,29 +1,32 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
import (
+ "context"
"fmt"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
+ repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/sync"
)
// repoWorkingPool represents a working pool to order the parallel changes to the same repository
+// TODO: use clustered lock (unique queue? or *abuse* cache)
var repoWorkingPool = sync.NewExclusivePool()
// TransferOwnership transfers all corresponding setting from old user to new one.
-func TransferOwnership(doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error {
- if err := repo.GetOwner(db.DefaultContext); err != nil {
+func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error {
+ if err := repo.GetOwner(ctx); err != nil {
return err
}
for _, team := range teams {
@@ -41,18 +44,18 @@ func TransferOwnership(doer, newOwner *user_model.User, repo *repo_model.Reposit
}
repoWorkingPool.CheckOut(fmt.Sprint(repo.ID))
- newRepo, err := repo_model.GetRepositoryByID(repo.ID)
+ newRepo, err := repo_model.GetRepositoryByID(ctx, repo.ID)
if err != nil {
return err
}
for _, team := range teams {
- if err := models.AddRepository(team, newRepo); err != nil {
+ if err := models.AddRepository(ctx, team, newRepo); err != nil {
return err
}
}
- notification.NotifyTransferRepository(doer, repo, oldOwner.Name)
+ notification.NotifyTransferRepository(ctx, doer, repo, oldOwner.Name)
return nil
}
@@ -75,56 +78,56 @@ func ChangeRepositoryName(doer *user_model.User, repo *repo_model.Repository, ne
repoWorkingPool.CheckOut(fmt.Sprint(repo.ID))
repo.Name = newRepoName
- notification.NotifyRenameRepository(doer, repo, oldRepoName)
+ notification.NotifyRenameRepository(db.DefaultContext, doer, repo, oldRepoName)
return nil
}
// StartRepositoryTransfer transfer a repo from one owner to a new one.
// it make repository into pending transfer state, if doer can not create repo for new owner.
-func StartRepositoryTransfer(doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error {
+func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error {
if err := models.TestRepositoryReadyForTransfer(repo.Status); err != nil {
return err
}
// Admin is always allowed to transfer || user transfer repo back to his account
if doer.IsAdmin || doer.ID == newOwner.ID {
- return TransferOwnership(doer, newOwner, repo, teams)
+ return TransferOwnership(ctx, doer, newOwner, repo, teams)
}
// If new owner is an org and user can create repos he can transfer directly too
if newOwner.IsOrganization() {
- allowed, err := organization.CanCreateOrgRepo(newOwner.ID, doer.ID)
+ allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID)
if err != nil {
return err
}
if allowed {
- return TransferOwnership(doer, newOwner, repo, teams)
+ return TransferOwnership(ctx, doer, newOwner, repo, teams)
}
}
// In case the new owner would not have sufficient access to the repo, give access rights for read
- hasAccess, err := models.HasAccess(newOwner.ID, repo)
+ hasAccess, err := access_model.HasAccess(ctx, newOwner.ID, repo)
if err != nil {
return err
}
if !hasAccess {
- if err := models.AddCollaborator(repo, newOwner); err != nil {
+ if err := repo_module.AddCollaborator(ctx, repo, newOwner); err != nil {
return err
}
- if err := models.ChangeCollaborationAccessMode(repo, newOwner.ID, perm.AccessModeRead); err != nil {
+ if err := repo_model.ChangeCollaborationAccessMode(ctx, repo, newOwner.ID, perm.AccessModeRead); err != nil {
return err
}
}
// Make repo as pending for transfer
repo.Status = repo_model.RepositoryPendingTransfer
- if err := models.CreatePendingRepositoryTransfer(doer, newOwner, repo.ID, teams); err != nil {
+ if err := models.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams); err != nil {
return err
}
// notify users who are able to accept / reject transfer
- notification.NotifyRepoPendingTransfer(doer, newOwner, repo)
+ notification.NotifyRepoPendingTransfer(ctx, doer, newOwner, repo)
return nil
}
diff --git a/services/repository/transfer_test.go b/services/repository/transfer_test.go
index 1081c76c7ed34..1299e66be2781 100644
--- a/services/repository/transfer_test.go
+++ b/services/repository/transfer_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package repository
@@ -8,8 +7,10 @@ import (
"sync"
"testing"
- "code.gitea.io/gitea/models"
+ activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
+ access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -33,12 +34,12 @@ func TestTransferOwnership(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository)
- repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
- assert.NoError(t, TransferOwnership(doer, doer, repo, nil))
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+ repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ assert.NoError(t, TransferOwnership(db.DefaultContext, doer, doer, repo, nil))
- transferredRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository)
+ transferredRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.EqualValues(t, 2, transferredRepo.OwnerID)
exist, err := util.IsExist(repo_model.RepoPath("user3", "repo3"))
@@ -47,8 +48,8 @@ func TestTransferOwnership(t *testing.T) {
exist, err = util.IsExist(repo_model.RepoPath("user2", "repo3"))
assert.NoError(t, err)
assert.True(t, exist)
- unittest.AssertExistsAndLoadBean(t, &models.Action{
- OpType: models.ActionTransferRepo,
+ unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
+ OpType: activities_model.ActionTransferRepo,
ActUserID: 2,
RepoID: 3,
Content: "user3/repo3",
@@ -60,18 +61,18 @@ func TestTransferOwnership(t *testing.T) {
func TestStartRepositoryTransferSetPermission(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
- recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}).(*user_model.User)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}).(*repo_model.Repository)
- repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+ recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+ repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
- hasAccess, err := models.HasAccess(recipient.ID, repo)
+ hasAccess, err := access_model.HasAccess(db.DefaultContext, recipient.ID, repo)
assert.NoError(t, err)
assert.False(t, hasAccess)
- assert.NoError(t, StartRepositoryTransfer(doer, recipient, repo, nil))
+ assert.NoError(t, StartRepositoryTransfer(db.DefaultContext, doer, recipient, repo, nil))
- hasAccess, err = models.HasAccess(recipient.ID, repo)
+ hasAccess, err = access_model.HasAccess(db.DefaultContext, recipient.ID, repo)
assert.NoError(t, err)
assert.True(t, hasAccess)
diff --git a/services/task/migrate.go b/services/task/migrate.go
index 6f3513452581b..03d083e596674 100644
--- a/services/task/migrate.go
+++ b/services/task/migrate.go
@@ -1,6 +1,5 @@
// Copyright 2019 Gitea. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package task
@@ -10,6 +9,7 @@ import (
"strings"
"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"
user_model "code.gitea.io/gitea/models/user"
@@ -40,7 +40,7 @@ func handleCreateError(owner *user_model.User, err error) error {
}
}
-func runMigrateTask(t *models.Task) (err error) {
+func runMigrateTask(t *admin_model.Task) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("PANIC whilst trying to do migrate task: %v", e)
@@ -48,9 +48,9 @@ func runMigrateTask(t *models.Task) (err error) {
}
if err == nil {
- err = models.FinishMigrateTask(t)
+ err = admin_model.FinishMigrateTask(t)
if err == nil {
- notification.NotifyMigrateRepository(t.Doer, t.Owner, t.Repo)
+ notification.NotifyMigrateRepository(db.DefaultContext, t.Doer, t.Owner, t.Repo)
return
}
@@ -110,7 +110,7 @@ func runMigrateTask(t *models.Task) (err error) {
}
t.Repo, err = migrations.MigrateRepository(ctx, t.Doer, t.Owner.Name, *opts, func(format string, args ...interface{}) {
- message := models.TranslatableMessage{
+ message := admin_model.TranslatableMessage{
Format: format,
Args: args,
}
@@ -132,12 +132,12 @@ func runMigrateTask(t *models.Task) (err error) {
err = util.SanitizeErrorCredentialURLs(err)
if strings.Contains(err.Error(), "Authentication failed") ||
strings.Contains(err.Error(), "could not read Username") {
- return fmt.Errorf("Authentication failed: %v", err.Error())
+ return fmt.Errorf("Authentication failed: %w", err)
} else if strings.Contains(err.Error(), "fatal:") {
- return fmt.Errorf("Migration failed: %v", err.Error())
+ return fmt.Errorf("Migration failed: %w", err)
}
// do not be tempted to coalesce this line with the return
err = handleCreateError(t.Owner, err)
- return
+ return err
}
diff --git a/services/task/task.go b/services/task/task.go
index 9deb0286c5be7..41bc07f2f6eb7 100644
--- a/services/task/task.go
+++ b/services/task/task.go
@@ -1,13 +1,12 @@
// Copyright 2019 Gitea. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package task
import (
"fmt"
- "code.gitea.io/gitea/models"
+ admin_model "code.gitea.io/gitea/models/admin"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful"
@@ -27,7 +26,7 @@ import (
var taskQueue queue.Queue
// Run a task
-func Run(t *models.Task) error {
+func Run(t *admin_model.Task) error {
switch t.Type {
case structs.TaskTypeMigrateRepo:
return runMigrateTask(t)
@@ -38,7 +37,7 @@ func Run(t *models.Task) error {
// Init will start the service to get all unfinished tasks and run them
func Init() error {
- taskQueue = queue.CreateQueue("task", handle, &models.Task{})
+ taskQueue = queue.CreateQueue("task", handle, &admin_model.Task{})
if taskQueue == nil {
return fmt.Errorf("Unable to create Task Queue")
@@ -51,7 +50,7 @@ func Init() error {
func handle(data ...queue.Data) []queue.Data {
for _, datum := range data {
- task := datum.(*models.Task)
+ task := datum.(*admin_model.Task)
if err := Run(task); err != nil {
log.Error("Run task failed: %v", err)
}
@@ -70,7 +69,7 @@ func MigrateRepository(doer, u *user_model.User, opts base.MigrateOptions) error
}
// CreateMigrateTask creates a migrate task
-func CreateMigrateTask(doer, u *user_model.User, opts base.MigrateOptions) (*models.Task, error) {
+func CreateMigrateTask(doer, u *user_model.User, opts base.MigrateOptions) (*admin_model.Task, error) {
// encrypt credentials for persistence
var err error
opts.CloneAddrEncrypted, err = secret.EncryptSecret(setting.SecretKey, opts.CloneAddr)
@@ -93,7 +92,7 @@ func CreateMigrateTask(doer, u *user_model.User, opts base.MigrateOptions) (*mod
return nil, err
}
- task := &models.Task{
+ task := &admin_model.Task{
DoerID: doer.ID,
OwnerID: u.ID,
Type: structs.TaskTypeMigrateRepo,
@@ -101,11 +100,11 @@ func CreateMigrateTask(doer, u *user_model.User, opts base.MigrateOptions) (*mod
PayloadContent: string(bs),
}
- if err := models.CreateTask(task); err != nil {
+ if err := admin_model.CreateTask(task); err != nil {
return nil, err
}
- repo, err := repo_module.CreateRepository(doer, u, models.CreateRepoOptions{
+ repo, err := repo_module.CreateRepository(doer, u, repo_module.CreateRepoOptions{
Name: opts.RepoName,
Description: opts.Description,
OriginalURL: opts.OriginalURL,
diff --git a/services/user/user.go b/services/user/user.go
index d41fc424939e4..c95eb67a851e9 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -1,52 +1,149 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
import (
"context"
- "crypto/md5"
"fmt"
"image/png"
"io"
"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"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/packages"
)
// DeleteUser completely and permanently deletes everything of a user,
// but issues/comments/pulls will be kept and shown as someone has been deleted,
// unless the user is younger than USER_DELETE_WITH_COMMENTS_MAX_DAYS.
-func DeleteUser(u *user_model.User) error {
+func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
if u.IsOrganization() {
return fmt.Errorf("%s is an organization not a user", u.Name)
}
- ctx, committer, err := db.TxContext()
+ if purge {
+ // Disable the user first
+ // NOTE: This is deliberately not within a transaction as it must disable the user immediately to prevent any further action by the user to be purged.
+ if err := user_model.UpdateUserCols(ctx, &user_model.User{
+ ID: u.ID,
+ IsActive: false,
+ IsRestricted: true,
+ IsAdmin: false,
+ ProhibitLogin: true,
+ Passwd: "",
+ Salt: "",
+ PasswdHashAlgo: "",
+ MaxRepoCreation: 0,
+ }, "is_active", "is_restricted", "is_admin", "prohibit_login", "max_repo_creation", "passwd", "salt", "passwd_hash_algo"); err != nil {
+ return fmt.Errorf("unable to disable user: %s[%d] prior to purge. UpdateUserCols: %w", u.Name, u.ID, err)
+ }
+
+ // Force any logged in sessions to log out
+ // FIXME: We also need to tell the session manager to log them out too.
+ eventsource.GetManager().SendMessage(u.ID, &eventsource.Event{
+ Name: "logout",
+ })
+
+ // Delete all repos belonging to this user
+ // Now this is not within a transaction because there are internal transactions within the DeleteRepository
+ // BUT: the db will still be consistent even if a number of repos have already been deleted.
+ // And in fact we want to capture any repositories that are being created in other transactions in the meantime
+ //
+ // An alternative option here would be write a DeleteAllRepositoriesForUserID function which would delete all of the repos
+ // but such a function would likely get out of date
+ for {
+ repos, _, err := repo_model.GetUserRepositories(&repo_model.SearchRepoOptions{
+ ListOptions: db.ListOptions{
+ PageSize: repo_model.RepositoryListDefaultPageSize,
+ Page: 1,
+ },
+ Private: true,
+ OwnerID: u.ID,
+ Actor: u,
+ })
+ if err != nil {
+ return fmt.Errorf("GetUserRepositories: %w", err)
+ }
+ if len(repos) == 0 {
+ break
+ }
+ for _, repo := range repos {
+ if err := models.DeleteRepository(u, u.ID, repo.ID); err != nil {
+ return fmt.Errorf("unable to delete repository %s for %s[%d]. Error: %w", repo.Name, u.Name, u.ID, err)
+ }
+ }
+ }
+
+ // Remove from Organizations and delete last owner organizations
+ // Now this is not within a transaction because there are internal transactions within the DeleteOrganization
+ // BUT: the db will still be consistent even if a number of organizations memberships and organizations have already been deleted
+ // And in fact we want to capture any organization additions that are being created in other transactions in the meantime
+ //
+ // An alternative option here would be write a function which would delete all organizations but it seems
+ // but such a function would likely get out of date
+ for {
+ orgs, err := organization.FindOrgs(organization.FindOrgOptions{
+ ListOptions: db.ListOptions{
+ PageSize: repo_model.RepositoryListDefaultPageSize,
+ Page: 1,
+ },
+ UserID: u.ID,
+ IncludePrivate: true,
+ })
+ if err != nil {
+ return fmt.Errorf("unable to find org list for %s[%d]. Error: %w", u.Name, u.ID, err)
+ }
+ if len(orgs) == 0 {
+ break
+ }
+ for _, org := range orgs {
+ if err := models.RemoveOrgUser(org.ID, u.ID); err != nil {
+ if organization.IsErrLastOrgOwner(err) {
+ err = organization.DeleteOrganization(ctx, org)
+ }
+ if err != nil {
+ return fmt.Errorf("unable to remove user %s[%d] from org %s[%d]. Error: %w", u.Name, u.ID, org.Name, org.ID, err)
+ }
+ }
+ }
+ }
+
+ // Delete Packages
+ if setting.Packages.Enabled {
+ if _, err := packages.RemoveAllPackages(ctx, u.ID); err != nil {
+ return err
+ }
+ }
+ }
+
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
// Note: A user owns any repository or belongs to any organization
- // cannot perform delete operation.
+ // cannot perform delete operation. This causes a race with the purge above
+ // however consistency requires that we ensure that this is the case
// Check ownership of repository.
- count, err := repo_model.GetRepositoryCount(ctx, u.ID)
+ count, err := repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{OwnerID: u.ID})
if err != nil {
- return fmt.Errorf("GetRepositoryCount: %v", err)
+ return fmt.Errorf("GetRepositoryCount: %w", err)
} else if count > 0 {
return models.ErrUserOwnRepos{UID: u.ID}
}
@@ -54,20 +151,20 @@ func DeleteUser(u *user_model.User) error {
// Check membership of organization.
count, err = organization.GetOrganizationCount(ctx, u)
if err != nil {
- return fmt.Errorf("GetOrganizationCount: %v", err)
+ return fmt.Errorf("GetOrganizationCount: %w", err)
} else if count > 0 {
return models.ErrUserHasOrgs{UID: u.ID}
}
// Check ownership of packages.
if ownsPackages, err := packages_model.HasOwnerPackages(ctx, u.ID); err != nil {
- return fmt.Errorf("HasOwnerPackages: %v", err)
+ return fmt.Errorf("HasOwnerPackages: %w", err)
} else if ownsPackages {
return models.ErrUserOwnPackages{UID: u.ID}
}
- if err := models.DeleteUser(ctx, u); err != nil {
- return fmt.Errorf("DeleteUser: %v", err)
+ if err := models.DeleteUser(ctx, u, purge); err != nil {
+ return fmt.Errorf("DeleteUser: %w", err)
}
if err := committer.Commit(); err != nil {
@@ -78,7 +175,7 @@ func DeleteUser(u *user_model.User) error {
if err = asymkey_model.RewriteAllPublicKeys(); err != nil {
return err
}
- if err = asymkey_model.RewriteAllPrincipalKeys(); err != nil {
+ if err = asymkey_model.RewriteAllPrincipalKeys(db.DefaultContext); err != nil {
return err
}
@@ -86,16 +183,16 @@ func DeleteUser(u *user_model.User) error {
// so just keep error logs of those operations.
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))
+ err = fmt.Errorf("Failed to RemoveAll %s: %w", path, err)
+ _ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
return err
}
if u.Avatar != "" {
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))
+ err = fmt.Errorf("Failed to remove %s: %w", avatarPath, err)
+ _ = system_model.CreateNotice(ctx, system_model.NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err))
return err
}
}
@@ -117,7 +214,7 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error {
return db.ErrCancelledf("Before delete inactive user %s", u.Name)
default:
}
- if err := DeleteUser(u); err != nil {
+ if err := DeleteUser(ctx, u, false); err != nil {
// Ignore users that were set inactive by admin.
if models.IsErrUserOwnRepos(err) || models.IsErrUserHasOrgs(err) || models.IsErrUserOwnPackages(err) {
continue
@@ -136,20 +233,16 @@ func UploadAvatar(u *user_model.User, data []byte) error {
return err
}
- ctx, committer, err := db.TxContext()
+ ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()
u.UseCustomAvatar = true
- // Different users can upload same image as avatar
- // If we prefix it with u.ID, it will be separated
- // Otherwise, if any of the users delete his avatar
- // Other users will lose their avatars too.
- u.Avatar = fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data)))))
+ u.Avatar = avatar.HashAvatar(u.ID, data)
if err = user_model.UpdateUserCols(ctx, u, "use_custom_avatar", "avatar"); err != nil {
- return fmt.Errorf("updateUser: %v", err)
+ return fmt.Errorf("updateUser: %w", err)
}
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
@@ -158,7 +251,7 @@ func UploadAvatar(u *user_model.User, data []byte) error {
}
return err
}); err != nil {
- return fmt.Errorf("Failed to create dir %s: %v", u.CustomAvatarRelativePath(), err)
+ return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err)
}
return committer.Commit()
@@ -170,14 +263,14 @@ func DeleteAvatar(u *user_model.User) error {
log.Trace("DeleteAvatar[%d]: %s", u.ID, aPath)
if len(u.Avatar) > 0 {
if err := storage.Avatars.Delete(aPath); err != nil {
- return fmt.Errorf("Failed to remove %s: %v", aPath, err)
+ return fmt.Errorf("Failed to remove %s: %w", aPath, err)
}
}
u.UseCustomAvatar = false
u.Avatar = ""
if _, err := db.GetEngine(db.DefaultContext).ID(u.ID).Cols("avatar, use_custom_avatar").Update(u); err != nil {
- return fmt.Errorf("UpdateUser: %v", err)
+ return fmt.Errorf("UpdateUser: %w", err)
}
return nil
}
diff --git a/services/user/user_test.go b/services/user/user_test.go
index cfa02b0033116..5e052a9df26fb 100644
--- a/services/user/user_test.go
+++ b/services/user/user_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package user
@@ -28,12 +27,12 @@ func TestMain(m *testing.M) {
func TestDeleteUser(t *testing.T) {
test := func(userID int64) {
assert.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}).(*user_model.User)
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
ownedRepos := make([]*repo_model.Repository, 0, 10)
assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&ownedRepos, &repo_model.Repository{OwnerID: userID}))
if len(ownedRepos) > 0 {
- err := DeleteUser(user)
+ err := DeleteUser(db.DefaultContext, user, false)
assert.Error(t, err)
assert.True(t, models.IsErrUserOwnRepos(err))
return
@@ -47,7 +46,7 @@ func TestDeleteUser(t *testing.T) {
return
}
}
- assert.NoError(t, DeleteUser(user))
+ assert.NoError(t, DeleteUser(db.DefaultContext, user, false))
unittest.AssertNotExistsBean(t, &user_model.User{ID: userID})
unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{})
}
@@ -56,8 +55,28 @@ func TestDeleteUser(t *testing.T) {
test(8)
test(11)
- org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
- assert.Error(t, DeleteUser(org))
+ org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+ assert.Error(t, DeleteUser(db.DefaultContext, org, false))
+}
+
+func TestPurgeUser(t *testing.T) {
+ test := func(userID int64) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
+
+ err := DeleteUser(db.DefaultContext, user, true)
+ assert.NoError(t, err)
+
+ unittest.AssertNotExistsBean(t, &user_model.User{ID: userID})
+ unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{})
+ }
+ test(2)
+ test(4)
+ test(8)
+ test(11)
+
+ org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+ assert.Error(t, DeleteUser(db.DefaultContext, org, false))
}
func TestCreateUser(t *testing.T) {
@@ -72,7 +91,7 @@ func TestCreateUser(t *testing.T) {
assert.NoError(t, user_model.CreateUser(user))
- assert.NoError(t, DeleteUser(user))
+ assert.NoError(t, DeleteUser(db.DefaultContext, user, false))
}
func TestCreateUser_Issue5882(t *testing.T) {
@@ -101,6 +120,6 @@ func TestCreateUser_Issue5882(t *testing.T) {
assert.Equal(t, !u.AllowCreateOrganization, v.disableOrgCreation)
- assert.NoError(t, DeleteUser(v.user))
+ assert.NoError(t, DeleteUser(db.DefaultContext, v.user, false))
}
}
diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go
index 7998be53c2831..effbe45e56910 100644
--- a/services/webhook/deliver.go
+++ b/services/webhook/deliver.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
@@ -15,7 +14,6 @@ import (
"io"
"net/http"
"net/url"
- "strconv"
"strings"
"sync"
"time"
@@ -26,7 +24,9 @@ import (
"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"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/gobwas/glob"
)
@@ -44,7 +44,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 task[%d] to webhook %s Panic: %v\nStacktrace: %s", t.ID, w.URL, err, log.Stack(2))
}()
t.IsDelivered = true
@@ -53,7 +53,7 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
switch w.HTTPMethod {
case "":
- log.Info("HTTP Method for webhook %d empty, setting to POST as default", t.ID)
+ log.Info("HTTP Method for webhook %s empty, setting to POST as default", w.URL)
fallthrough
case http.MethodPost:
switch w.ContentType {
@@ -79,27 +79,32 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
case http.MethodGet:
u, err := url.Parse(w.URL)
if err != nil {
- return err
+ return fmt.Errorf("unable to deliver webhook task[%d] as cannot parse webhook url %s: %w", t.ID, w.URL, err)
}
vals := u.Query()
vals["payload"] = []string{t.PayloadContent}
u.RawQuery = vals.Encode()
req, err = http.NewRequest("GET", u.String(), nil)
if err != nil {
- return err
+ return fmt.Errorf("unable to deliver webhook task[%d] as unable to create HTTP request for webhook url %s: %w", t.ID, w.URL, err)
}
case http.MethodPut:
switch w.Type {
- case webhook_model.MATRIX:
- req, err = getMatrixHookRequest(w, t)
+ case webhook_module.MATRIX:
+ txnID, err := getMatrixTxnID([]byte(t.PayloadContent))
if err != nil {
return err
}
+ url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
+ req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent))
+ if err != nil {
+ return fmt.Errorf("unable to deliver webhook task[%d] as cannot create matrix request for webhook url %s: %w", t.ID, w.URL, err)
+ }
default:
- return fmt.Errorf("invalid http method for webhook: [%d] %v", t.ID, w.HTTPMethod)
+ return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
}
default:
- return fmt.Errorf("invalid http method for webhook: [%d] %v", t.ID, w.HTTPMethod)
+ return fmt.Errorf("invalid http method for webhook task[%d] in webhook %s: %v", t.ID, w.URL, w.HTTPMethod)
}
var signatureSHA1 string
@@ -131,6 +136,16 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
req.Header["X-GitHub-Event"] = []string{event}
req.Header["X-GitHub-Event-Type"] = []string{eventType}
+ // Add Authorization Header
+ authorization, err := w.HeaderAuthorization()
+ if err != nil {
+ log.Error("Webhook could not get Authorization header [%d]: %v", w.ID, err)
+ return err
+ }
+ if authorization != "" {
+ req.Header["Authorization"] = []string{authorization}
+ }
+
// Record delivery information.
t.RequestInfo = &webhook_model.HookRequest{
URL: req.URL.String(),
@@ -145,6 +160,20 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
Headers: map[string]string{},
}
+ // OK We're now ready to attempt to deliver the task - we must double check that it
+ // has not been delivered in the meantime
+ updated, err := webhook_model.MarkTaskDelivered(ctx, t)
+ if err != nil {
+ log.Error("MarkTaskDelivered[%d]: %v", t.ID, err)
+ return fmt.Errorf("unable to mark task[%d] delivered in the db: %w", t.ID, err)
+ }
+ if !updated {
+ // This webhook task has already been attempted to be delivered or is in the process of being delivered
+ log.Trace("Webhook Task[%d] already delivered", t.ID)
+ return nil
+ }
+
+ // All code from this point will update the hook task
defer func() {
t.Delivered = time.Now().UnixNano()
if t.IsSucceed {
@@ -161,9 +190,9 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
// Update webhook last delivery status.
if t.IsSucceed {
- w.LastStatus = webhook_model.HookStatusSucceed
+ w.LastStatus = webhook_module.HookStatusSucceed
} else {
- w.LastStatus = webhook_model.HookStatusFail
+ w.LastStatus = webhook_module.HookStatusFail
}
if err = webhook_model.UpdateWebhookLastStatus(w); err != nil {
log.Error("UpdateWebhookLastStatus: %v", err)
@@ -176,13 +205,14 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
}
if !w.IsActive {
+ log.Trace("Webhook %s in Webhook Task[%d] is not active", w.URL, t.ID)
return nil
}
resp, err := webhookHTTPClient.Do(req.WithContext(ctx))
if err != nil {
t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err)
- return err
+ return fmt.Errorf("unable to deliver webhook task[%d] in %s due to error in http client: %w", t.ID, w.URL, err)
}
defer resp.Body.Close()
@@ -196,76 +226,12 @@ func Deliver(ctx context.Context, t *webhook_model.HookTask) error {
p, err := io.ReadAll(resp.Body)
if err != nil {
t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err)
- return err
+ return fmt.Errorf("unable to deliver webhook task[%d] in %s as unable to read response body: %w", t.ID, w.URL, err)
}
t.ResponseInfo.Body = string(p)
return nil
}
-// DeliverHooks checks and delivers undelivered hooks.
-// FIXME: graceful: This would likely benefit from either a worker pool with dummy queue
-// or a full queue. Then more hooks could be sent at same time.
-func DeliverHooks(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 = Deliver(ctx, t); err != nil {
- log.Error("deliver: %v", err)
- }
- }
-
- // Start listening on new hook requests.
- for {
- select {
- case <-ctx.Done():
- hookQueue.Close()
- return
- case repoIDStr := <-hookQueue.Queue():
- log.Trace("DeliverHooks [repo_id: %v]", repoIDStr)
- hookQueue.Remove(repoIDStr)
-
- repoID, err := strconv.ParseInt(repoIDStr, 10, 64)
- 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 {
- select {
- case <-ctx.Done():
- return
- default:
- }
- if err = Deliver(ctx, t); err != nil {
- log.Error("deliver: %v", err)
- }
- }
- }
- }
-}
-
var (
webhookHTTPClient *http.Client
once sync.Once
@@ -297,8 +263,8 @@ func webhookProxy() func(req *http.Request) (*url.URL, error) {
}
}
-// InitDeliverHooks starts the hooks delivery thread
-func InitDeliverHooks() {
+// Init starts the hooks delivery thread
+func Init() error {
timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second
allowedHostListValue := setting.Webhook.AllowedHostList
@@ -316,5 +282,43 @@ func InitDeliverHooks() {
},
}
- go graceful.GetManager().RunWithShutdownContext(DeliverHooks)
+ 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)
+
+ go graceful.GetManager().RunWithShutdownContext(populateWebhookSendingQueue)
+
+ return nil
+}
+
+func populateWebhookSendingQueue(ctx context.Context) {
+ ctx, _, finished := process.GetManager().AddContext(ctx, "Webhook: Populate sending queue")
+ defer finished()
+
+ lowerID := int64(0)
+ for {
+ taskIDs, err := webhook_model.FindUndeliveredHookTaskIDs(ctx, lowerID)
+ if err != nil {
+ log.Error("Unable to populate webhook queue as FindUndeliveredHookTaskIDs failed: %v", err)
+ return
+ }
+ if len(taskIDs) == 0 {
+ return
+ }
+ lowerID = taskIDs[len(taskIDs)-1]
+
+ for _, taskID := range taskIDs {
+ select {
+ case <-ctx.Done():
+ log.Warn("Shutdown before Webhook Sending queue finishing being populated")
+ return
+ default:
+ }
+ if err := enqueueHookTask(taskID); err != nil {
+ log.Error("Unable to push HookTask[%d] to the Webhook Sending queue: %v", taskID, err)
+ }
+ }
+ }
}
diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go
index 8d1d587c38fb7..ee63975ad37d1 100644
--- a/services/webhook/deliver_test.go
+++ b/services/webhook/deliver_test.go
@@ -1,15 +1,22 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
+ "context"
"net/http"
+ "net/http/httptest"
"net/url"
"testing"
+ "time"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
)
@@ -38,3 +45,45 @@ func TestWebhookProxy(t *testing.T) {
}
}
}
+
+func TestWebhookDeliverAuthorizationHeader(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ done := make(chan struct{}, 1)
+ s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, "/webhook", r.URL.Path)
+ assert.Equal(t, "Bearer s3cr3t-t0ken", r.Header.Get("Authorization"))
+ w.WriteHeader(200)
+ done <- struct{}{}
+ }))
+ t.Cleanup(s.Close)
+
+ hook := &webhook_model.Webhook{
+ RepoID: 3,
+ URL: s.URL + "/webhook",
+ ContentType: webhook_model.ContentTypeJSON,
+ IsActive: true,
+ Type: webhook_module.GITEA,
+ }
+ err := hook.SetHeaderAuthorization("Bearer s3cr3t-t0ken")
+ assert.NoError(t, err)
+ assert.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, hook))
+ db.GetEngine(db.DefaultContext).NoAutoTime().DB().Logger.ShowSQL(true)
+
+ hookTask := &webhook_model.HookTask{HookID: hook.ID, EventType: webhook_module.HookEventPush, Payloader: &api.PushPayload{}}
+
+ hookTask, err = webhook_model.CreateHookTask(db.DefaultContext, hookTask)
+ assert.NoError(t, err)
+ if !assert.NotNil(t, hookTask) {
+ return
+ }
+
+ assert.NoError(t, Deliver(context.Background(), hookTask))
+ select {
+ case <-done:
+ case <-time.After(5 * time.Second):
+ t.Fatal("waited to long for request to happen")
+ }
+
+ assert.True(t, hookTask.IsSucceed)
+}
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index 642cf6f2fda16..99ee6e4d192b8 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
@@ -9,13 +8,13 @@ import (
"net/url"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
- dingtalk "github.com/lunny/dingtalk_webhook"
+ dingtalk "gitea.com/lunny/dingtalk_webhook"
)
type (
@@ -67,14 +66,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)
@@ -107,6 +106,14 @@ func (d *DingtalkPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+attachmentText, "view issue", p.Issue.HTMLURL), nil
}
+// Wiki implements PayloadConvertor Wiki method
+func (d *DingtalkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+ text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
+ url := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
+
+ return createDingtalkPayload(text, text, "view wiki", url), nil
+}
+
// IssueComment implements PayloadConvertor IssueComment method
func (d *DingtalkPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)
@@ -122,7 +129,7 @@ func (d *DingtalkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader,
}
// Review implements PayloadConvertor Review method
-func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) {
+func (d *DingtalkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
var text, title string
switch p.Action {
case api.HookIssueReviewed:
@@ -183,6 +190,6 @@ func createDingtalkPayload(title, text, singleTitle, singleURL string) *Dingtalk
}
// GetDingtalkPayload converts a ding talk webhook into a DingtalkPayload
-func GetDingtalkPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) {
+func GetDingtalkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(DingtalkPayload), p, event)
}
diff --git a/services/webhook/dingtalk_test.go b/services/webhook/dingtalk_test.go
index b66b5e43a824d..e3122d2f36fca 100644
--- a/services/webhook/dingtalk_test.go
+++ b/services/webhook/dingtalk_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
@@ -8,8 +7,8 @@ import (
"net/url"
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -82,7 +81,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))
})
@@ -163,7 +162,7 @@ func TestDingTalkPayload(t *testing.T) {
p.Action = api.HookIssueReviewed
d := new(DingtalkPayload)
- pl, err := d.Review(p, webhook_model.HookEventPullRequestReviewApproved)
+ pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &DingtalkPayload{}, pl)
@@ -189,6 +188,44 @@ func TestDingTalkPayload(t *testing.T) {
assert.Equal(t, "http://localhost:3000/test/repo", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
})
+ t.Run("Wiki", func(t *testing.T) {
+ p := wikiTestPayload()
+
+ d := new(DingtalkPayload)
+ p.Action = api.HookWikiCreated
+ pl, err := d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &DingtalkPayload{}, pl)
+
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text)
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title)
+ assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+
+ p.Action = api.HookWikiEdited
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &DingtalkPayload{}, pl)
+
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Text)
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*DingtalkPayload).ActionCard.Title)
+ assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+
+ p.Action = api.HookWikiDeleted
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &DingtalkPayload{}, pl)
+
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Text)
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*DingtalkPayload).ActionCard.Title)
+ assert.Equal(t, "view wiki", pl.(*DingtalkPayload).ActionCard.SingleTitle)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", parseRealSingleURL(pl.(*DingtalkPayload).ActionCard.SingleURL))
+ })
+
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index ae5460b9a7a1b..014d409ce7326 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -1,12 +1,12 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
"errors"
"fmt"
+ "net/url"
"strconv"
"strings"
@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
)
type (
@@ -54,7 +55,7 @@ type (
Wait bool `json:"wait"`
Content string `json:"content"`
Username string `json:"username"`
- AvatarURL string `json:"avatar_url"`
+ AvatarURL string `json:"avatar_url,omitempty"`
TTS bool `json:"tts"`
Embeds []DiscordEmbed `json:"embeds"`
}
@@ -141,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 == "" {
@@ -190,7 +191,7 @@ func (d *DiscordPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader,
}
// Review implements PayloadConvertor Review method
-func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) {
+func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
var text, title string
var color int
switch p.Action {
@@ -204,11 +205,11 @@ func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_model.H
text = p.Review.Content
switch event {
- case webhook_model.HookEventPullRequestReviewApproved:
+ case webhook_module.HookEventPullRequestReviewApproved:
color = greenColor
- case webhook_model.HookEventPullRequestReviewRejected:
+ case webhook_module.HookEventPullRequestReviewRejected:
color = redColor
- case webhook_model.HookEventPullRequestComment:
+ case webhook_module.HookEventPullRequestComment:
color = greyColor
default:
color = yellowColor
@@ -235,6 +236,19 @@ func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er
return d.createPayload(p.Sender, title, "", url, color), nil
}
+// Wiki implements PayloadConvertor Wiki method
+func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+ text, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
+ htmlLink := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
+
+ var description string
+ if p.Action != api.HookWikiDeleted {
+ description = p.Comment
+ }
+
+ return d.createPayload(p.Sender, text, description, htmlLink, color), nil
+}
+
// Release implements PayloadConvertor Release method
func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
@@ -243,7 +257,7 @@ func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
}
// GetDiscordPayload converts a discord webhook into a DiscordPayload
-func GetDiscordPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) {
+func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
s := new(DiscordPayload)
discord := &DiscordMeta{}
@@ -256,14 +270,14 @@ func GetDiscordPayload(p api.Payloader, event webhook_model.HookEventType, meta
return convertPayloader(s, p, event)
}
-func parseHookPullRequestEventType(event webhook_model.HookEventType) (string, error) {
+func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
switch event {
- case webhook_model.HookEventPullRequestReviewApproved:
+ case webhook_module.HookEventPullRequestReviewApproved:
return "approved", nil
- case webhook_model.HookEventPullRequestReviewRejected:
+ case webhook_module.HookEventPullRequestReviewRejected:
return "rejected", nil
- case webhook_model.HookEventPullRequestComment:
+ case webhook_module.HookEventPullRequestComment:
return "comment", nil
default:
diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go
index 8fe20c01024c8..624d53446a981 100644
--- a/services/webhook/discord_test.go
+++ b/services/webhook/discord_test.go
@@ -1,15 +1,14 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -180,7 +179,7 @@ func TestDiscordPayload(t *testing.T) {
p.Action = api.HookIssueReviewed
d := new(DiscordPayload)
- pl, err := d.Review(p, webhook_model.HookEventPullRequestReviewApproved)
+ pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &DiscordPayload{}, pl)
@@ -212,6 +211,53 @@ func TestDiscordPayload(t *testing.T) {
assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
})
+ t.Run("Wiki", func(t *testing.T) {
+ p := wikiTestPayload()
+
+ d := new(DiscordPayload)
+ p.Action = api.HookWikiCreated
+ pl, err := d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &DiscordPayload{}, pl)
+
+ assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title)
+ assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+
+ p.Action = api.HookWikiEdited
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &DiscordPayload{}, pl)
+
+ assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*DiscordPayload).Embeds[0].Title)
+ assert.Equal(t, "Wiki change comment", pl.(*DiscordPayload).Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+
+ p.Action = api.HookWikiDeleted
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &DiscordPayload{}, pl)
+
+ assert.Len(t, pl.(*DiscordPayload).Embeds, 1)
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*DiscordPayload).Embeds[0].Title)
+ assert.Empty(t, pl.(*DiscordPayload).Embeds[0].Description)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*DiscordPayload).Embeds[0].URL)
+ assert.Equal(t, p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.Name)
+ assert.Equal(t, setting.AppURL+p.Sender.UserName, pl.(*DiscordPayload).Embeds[0].Author.URL)
+ assert.Equal(t, p.Sender.AvatarURL, pl.(*DiscordPayload).Embeds[0].Author.IconURL)
+ })
+
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 5b20c7dda7e5f..4fbf8f76a90ae 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
@@ -8,10 +7,10 @@ import (
"fmt"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
)
type (
@@ -118,7 +117,7 @@ func (f *FeishuPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, e
}
// Review implements PayloadConvertor Review method
-func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) {
+func (f *FeishuPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
action, err := parseHookPullRequestEventType(event)
if err != nil {
return nil, err
@@ -145,6 +144,13 @@ func (f *FeishuPayload) Repository(p *api.RepositoryPayload) (api.Payloader, err
return nil, nil
}
+// Wiki implements PayloadConvertor Wiki method
+func (f *FeishuPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+ text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
+
+ return newFeishuTextPayload(text), nil
+}
+
// Release implements PayloadConvertor Release method
func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
@@ -153,6 +159,6 @@ func (f *FeishuPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
}
// GetFeishuPayload converts a ding talk webhook into a FeishuPayload
-func GetFeishuPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) {
+func GetFeishuPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(FeishuPayload), p, event)
}
diff --git a/services/webhook/feishu_test.go b/services/webhook/feishu_test.go
index d862b015e799b..84549c1fa5764 100644
--- a/services/webhook/feishu_test.go
+++ b/services/webhook/feishu_test.go
@@ -1,14 +1,13 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -125,7 +124,7 @@ func TestFeishuPayload(t *testing.T) {
p.Action = api.HookIssueReviewed
d := new(FeishuPayload)
- pl, err := d.Review(p, webhook_model.HookEventPullRequestReviewApproved)
+ pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &FeishuPayload{}, pl)
@@ -145,6 +144,35 @@ func TestFeishuPayload(t *testing.T) {
assert.Equal(t, "[test/repo] Repository created", pl.(*FeishuPayload).Content.Text)
})
+ t.Run("Wiki", func(t *testing.T) {
+ p := wikiTestPayload()
+
+ d := new(FeishuPayload)
+ p.Action = api.HookWikiCreated
+ pl, err := d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &FeishuPayload{}, pl)
+
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text)
+
+ p.Action = api.HookWikiEdited
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &FeishuPayload{}, pl)
+
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1", pl.(*FeishuPayload).Content.Text)
+
+ p.Action = api.HookWikiDeleted
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &FeishuPayload{}, pl)
+
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted by user1", pl.(*FeishuPayload).Content.Text)
+ })
+
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
diff --git a/services/webhook/general.go b/services/webhook/general.go
index 5080cf98dc609..1f7d204d1f74c 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
@@ -10,9 +9,11 @@ import (
"net/url"
"strings"
+ webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
)
type linkFormatter = func(string, string) string
@@ -84,11 +85,13 @@ func getPullRequestPayloadInfo(p *api.PullRequestPayload, linkFormatter linkForm
issueTitle := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
titleLink := linkFormatter(p.PullRequest.URL, issueTitle)
var text string
+ var attachmentText string
color := yellowColor
switch p.Action {
case api.HookIssueOpened:
text = fmt.Sprintf("[%s] Pull request opened: %s", repoLink, titleLink)
+ attachmentText = p.PullRequest.Body
color = greenColor
case api.HookIssueClosed:
if p.PullRequest.HasMerged {
@@ -102,6 +105,7 @@ func getPullRequestPayloadInfo(p *api.PullRequestPayload, linkFormatter linkForm
text = fmt.Sprintf("[%s] Pull request re-opened: %s", repoLink, titleLink)
case api.HookIssueEdited:
text = fmt.Sprintf("[%s] Pull request edited: %s", repoLink, titleLink)
+ attachmentText = p.PullRequest.Body
case api.HookIssueAssigned:
list := make([]string, len(p.PullRequest.Assignees))
for i, user := range p.PullRequest.Assignees {
@@ -126,22 +130,18 @@ func getPullRequestPayloadInfo(p *api.PullRequestPayload, linkFormatter linkForm
text = fmt.Sprintf("[%s] Pull request milestone cleared: %s", repoLink, titleLink)
case api.HookIssueReviewed:
text = fmt.Sprintf("[%s] Pull request reviewed: %s", repoLink, titleLink)
+ attachmentText = p.Review.Content
}
if withSender {
text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName))
}
- var attachmentText string
- if p.Action == api.HookIssueOpened || p.Action == api.HookIssueEdited {
- attachmentText = p.PullRequest.Body
- }
-
return text, issueTitle, attachmentText, color
}
func getReleasePayloadInfo(p *api.ReleasePayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
repoLink := linkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
- refLink := linkFormatter(p.Repository.HTMLURL+"/src/"+util.PathEscapeSegments(p.Release.TagName), p.Release.TagName)
+ refLink := linkFormatter(p.Repository.HTMLURL+"/releases/tag/"+util.PathEscapeSegments(p.Release.TagName), p.Release.TagName)
switch p.Action {
case api.HookReleasePublished:
@@ -161,6 +161,35 @@ func getReleasePayloadInfo(p *api.ReleasePayload, linkFormatter linkFormatter, w
return text, color
}
+func getWikiPayloadInfo(p *api.WikiPayload, linkFormatter linkFormatter, withSender bool) (string, int, string) {
+ repoLink := linkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
+ pageLink := linkFormatter(p.Repository.HTMLURL+"/wiki/"+url.PathEscape(p.Page), p.Page)
+
+ var text string
+ color := greenColor
+
+ switch p.Action {
+ case api.HookWikiCreated:
+ text = fmt.Sprintf("[%s] New wiki page '%s'", repoLink, pageLink)
+ case api.HookWikiEdited:
+ text = fmt.Sprintf("[%s] Wiki page '%s' edited", repoLink, pageLink)
+ color = yellowColor
+ case api.HookWikiDeleted:
+ text = fmt.Sprintf("[%s] Wiki page '%s' deleted", repoLink, pageLink)
+ color = redColor
+ }
+
+ if p.Action != api.HookWikiDeleted && p.Comment != "" {
+ text += fmt.Sprintf(" (%s)", p.Comment)
+ }
+
+ if withSender {
+ text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+ }
+
+ return text, color, pageLink
+}
+
func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFormatter, withSender bool) (string, string, int) {
repoLink := linkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
issueTitle := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
@@ -196,3 +225,36 @@ func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFo
return text, issueTitle, color
}
+
+// ToHook convert models.Webhook to api.Hook
+// This function is not part of the convert package to prevent an import cycle
+func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
+ config := map[string]string{
+ "url": w.URL,
+ "content_type": w.ContentType.Name(),
+ }
+ if w.Type == webhook_module.SLACK {
+ s := GetSlackHook(w)
+ config["channel"] = s.Channel
+ config["username"] = s.Username
+ config["icon_url"] = s.IconURL
+ config["color"] = s.Color
+ }
+
+ authorizationHeader, err := w.HeaderAuthorization()
+ if err != nil {
+ return nil, err
+ }
+
+ return &api.Hook{
+ ID: w.ID,
+ Type: w.Type,
+ URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID),
+ Active: w.IsActive,
+ Config: config,
+ Events: w.EventsArray(),
+ AuthorizationHeader: authorizationHeader,
+ Updated: w.UpdatedUnix.AsTime(),
+ Created: w.CreatedUnix.AsTime(),
+ }, nil
+}
diff --git a/services/webhook/general_test.go b/services/webhook/general_test.go
index 4d73afe060a68..ba58ca4f90d6e 100644
--- a/services/webhook/general_test.go
+++ b/services/webhook/general_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
@@ -82,12 +81,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",
@@ -195,6 +195,22 @@ func pullRequestCommentTestPayload() *api.IssueCommentPayload {
}
}
+func wikiTestPayload() *api.WikiPayload {
+ return &api.WikiPayload{
+ Repository: &api.Repository{
+ HTMLURL: "http://localhost:3000/test/repo",
+ Name: "repo",
+ FullName: "test/repo",
+ },
+ Sender: &api.User{
+ UserName: "user1",
+ AvatarURL: "http://localhost:3000/user1/avatar",
+ },
+ Page: "index",
+ Comment: "Wiki change comment",
+ }
+}
+
func pullReleaseTestPayload() *api.ReleasePayload {
return &api.ReleasePayload{
Action: api.HookReleasePublished,
@@ -469,6 +485,44 @@ func TestGetPullRequestPayloadInfo(t *testing.T) {
}
}
+func TestGetWikiPayloadInfo(t *testing.T) {
+ p := wikiTestPayload()
+
+ cases := []struct {
+ action api.HookWikiAction
+ text string
+ color int
+ link string
+ }{
+ {
+ api.HookWikiCreated,
+ "[test/repo] New wiki page 'index' (Wiki change comment) by user1",
+ greenColor,
+ "index",
+ },
+ {
+ api.HookWikiEdited,
+ "[test/repo] Wiki page 'index' edited (Wiki change comment) by user1",
+ yellowColor,
+ "index",
+ },
+ {
+ api.HookWikiDeleted,
+ "[test/repo] Wiki page 'index' deleted by user1",
+ redColor,
+ "index",
+ },
+ }
+
+ for i, c := range cases {
+ p.Action = c.action
+ text, color, link := getWikiPayloadInfo(p, noneLinkFormatter, true)
+ assert.Equal(t, c.text, text, "case %d", i)
+ assert.Equal(t, c.color, color, "case %d", i)
+ assert.Equal(t, c.link, link, "case %d", i)
+ }
+}
+
func TestGetReleasePayloadInfo(t *testing.T) {
p := pullReleaseTestPayload()
diff --git a/services/webhook/main_test.go b/services/webhook/main_test.go
index 25b9df0af6688..6cf9410735c88 100644
--- a/services/webhook/main_test.go
+++ b/services/webhook/main_test.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
@@ -9,12 +8,20 @@ import (
"testing"
"code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/hostmatcher"
+ "code.gitea.io/gitea/modules/setting"
_ "code.gitea.io/gitea/models"
)
func TestMain(m *testing.M) {
+ setting.LoadForTest()
+ setting.NewQueueService()
+
+ // for tests, allow only loopback IPs
+ setting.Webhook.AllowedHostList = hostmatcher.MatchBuiltinLoopback
unittest.MainTest(m, &unittest.TestOptions{
GiteaRootPath: filepath.Join("..", ".."),
+ SetUp: Init,
})
}
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index a42ab2a93e063..cf2b503cdc2df 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -1,15 +1,14 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package webhook
import (
"crypto/sha1"
+ "encoding/hex"
"errors"
"fmt"
"html"
- "net/http"
"net/url"
"regexp"
"strings"
@@ -21,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
)
const matrixPayloadSizeLimit = 1024 * 64
@@ -29,7 +29,6 @@ const matrixPayloadSizeLimit = 1024 * 64
type MatrixMeta struct {
HomeserverURL string `json:"homeserver_url"`
Room string `json:"room_id"`
- AccessToken string `json:"access_token"`
MessageType int `json:"message_type"`
}
@@ -47,27 +46,10 @@ func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta {
return s
}
-// MatrixPayloadUnsafe contains the (unsafe) payload for a Matrix room
-type MatrixPayloadUnsafe struct {
- MatrixPayloadSafe
- AccessToken string `json:"access_token"`
-}
-
-var _ PayloadConvertor = &MatrixPayloadUnsafe{}
-
-// safePayload "converts" a unsafe payload to a safe payload
-func (m *MatrixPayloadUnsafe) safePayload() *MatrixPayloadSafe {
- return &MatrixPayloadSafe{
- Body: m.Body,
- MsgType: m.MsgType,
- Format: m.Format,
- FormattedBody: m.FormattedBody,
- Commits: m.Commits,
- }
-}
+var _ PayloadConvertor = &MatrixPayload{}
-// MatrixPayloadSafe contains (safe) payload for a Matrix room
-type MatrixPayloadSafe struct {
+// MatrixPayload contains payload for a Matrix room
+type MatrixPayload struct {
Body string `json:"body"`
MsgType string `json:"msgtype"`
Format string `json:"format"`
@@ -75,8 +57,8 @@ type MatrixPayloadSafe struct {
Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
}
-// JSONPayload Marshals the MatrixPayloadUnsafe to json
-func (m *MatrixPayloadUnsafe) JSONPayload() ([]byte, error) {
+// JSONPayload Marshals the MatrixPayload to json
+func (m *MatrixPayload) JSONPayload() ([]byte, error) {
data, err := json.MarshalIndent(m, "", " ")
if err != nil {
return []byte{}, err
@@ -103,61 +85,68 @@ func MatrixLinkToRef(repoURL, ref string) string {
}
// Create implements PayloadConvertor Create method
-func (m *MatrixPayloadUnsafe) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (m *MatrixPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
- return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil
+ return getMatrixPayload(text, nil, m.MsgType), nil
}
// Delete composes Matrix payload for delete a branch or tag.
-func (m *MatrixPayloadUnsafe) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (m *MatrixPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
refName := git.RefEndName(p.Ref)
repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
- return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil
+ return getMatrixPayload(text, nil, m.MsgType), nil
}
// Fork composes Matrix payload for forked by a repository.
-func (m *MatrixPayloadUnsafe) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (m *MatrixPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
- return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil
+ return getMatrixPayload(text, nil, m.MsgType), nil
}
// Issue implements PayloadConvertor Issue method
-func (m *MatrixPayloadUnsafe) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (m *MatrixPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true)
- return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil
+ return getMatrixPayload(text, nil, m.MsgType), nil
}
// IssueComment implements PayloadConvertor IssueComment method
-func (m *MatrixPayloadUnsafe) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (m *MatrixPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true)
- return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil
+ return getMatrixPayload(text, nil, m.MsgType), nil
+}
+
+// Wiki implements PayloadConvertor Wiki method
+func (m *MatrixPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+ text, _, _ := getWikiPayloadInfo(p, MatrixLinkFormatter, true)
+
+ return getMatrixPayload(text, nil, m.MsgType), nil
}
// Release implements PayloadConvertor Release method
-func (m *MatrixPayloadUnsafe) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (m *MatrixPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true)
- return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil
+ return getMatrixPayload(text, nil, m.MsgType), nil
}
// Push implements PayloadConvertor Push method
-func (m *MatrixPayloadUnsafe) Push(p *api.PushPayload) (api.Payloader, error) {
+func (m *MatrixPayload) 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)
@@ -174,21 +163,21 @@ func (m *MatrixPayloadUnsafe) Push(p *api.PushPayload) (api.Payloader, error) {
}
- return getMatrixPayloadUnsafe(text, p.Commits, m.AccessToken, m.MsgType), nil
+ return getMatrixPayload(text, p.Commits, m.MsgType), nil
}
// PullRequest implements PayloadConvertor PullRequest method
-func (m *MatrixPayloadUnsafe) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true)
- return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil
+ return getMatrixPayload(text, nil, m.MsgType), nil
}
// Review implements PayloadConvertor Review method
-func (m *MatrixPayloadUnsafe) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) {
+func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
- titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
+ titleLink := MatrixLinkFormatter(p.PullRequest.URL, title)
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string
@@ -199,14 +188,14 @@ func (m *MatrixPayloadUnsafe) Review(p *api.PullRequestPayload, event webhook_mo
return nil, err
}
- text = fmt.Sprintf("[%s] Pull request review %s: [%s](%s) by %s", repoLink, action, title, titleLink, senderLink)
+ text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink)
}
- return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil
+ return getMatrixPayload(text, nil, m.MsgType), nil
}
// Repository implements PayloadConvertor Repository method
-func (m *MatrixPayloadUnsafe) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
var text string
@@ -218,27 +207,25 @@ func (m *MatrixPayloadUnsafe) Repository(p *api.RepositoryPayload) (api.Payloade
text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
}
- return getMatrixPayloadUnsafe(text, nil, m.AccessToken, m.MsgType), nil
+ return getMatrixPayload(text, nil, m.MsgType), nil
}
-// GetMatrixPayload converts a Matrix webhook into a MatrixPayloadUnsafe
-func GetMatrixPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) {
- s := new(MatrixPayloadUnsafe)
+// GetMatrixPayload converts a Matrix webhook into a MatrixPayload
+func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
+ s := new(MatrixPayload)
matrix := &MatrixMeta{}
if err := json.Unmarshal([]byte(meta), &matrix); err != nil {
return s, errors.New("GetMatrixPayload meta json:" + err.Error())
}
- s.AccessToken = matrix.AccessToken
s.MsgType = messageTypeText[matrix.MessageType]
return convertPayloader(s, p, event)
}
-func getMatrixPayloadUnsafe(text string, commits []*api.PayloadCommit, accessToken, msgType string) *MatrixPayloadUnsafe {
- p := MatrixPayloadUnsafe{}
- p.AccessToken = accessToken
+func getMatrixPayload(text string, commits []*api.PayloadCommit, msgType string) *MatrixPayload {
+ p := MatrixPayload{}
p.FormattedBody = text
p.Body = getMessageBody(text)
p.Format = "org.matrix.custom.html"
@@ -255,52 +242,17 @@ func getMessageBody(htmlText string) string {
return htmlText
}
-// getMatrixHookRequest creates a new request which contains an Authorization header.
-// The access_token is removed from t.PayloadContent
-func getMatrixHookRequest(w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, error) {
- payloadunsafe := MatrixPayloadUnsafe{}
- if err := json.Unmarshal([]byte(t.PayloadContent), &payloadunsafe); err != nil {
- log.Error("Matrix Hook delivery failed: %v", err)
- return nil, err
- }
-
- payloadsafe := payloadunsafe.safePayload()
-
- var payload []byte
- var err error
- if payload, err = json.MarshalIndent(payloadsafe, "", " "); err != nil {
- return nil, err
- }
+// getMatrixTxnID computes the transaction ID to ensure idempotency
+func getMatrixTxnID(payload []byte) (string, error) {
if len(payload) >= matrixPayloadSizeLimit {
- return nil, fmt.Errorf("getMatrixHookRequest: payload size %d > %d", len(payload), matrixPayloadSizeLimit)
- }
- t.PayloadContent = string(payload)
-
- txnID, err := getMatrixTxnID(payload)
- if err != nil {
- return nil, fmt.Errorf("getMatrixHookRequest: unable to hash payload: %+v", err)
- }
-
- url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
-
- req, err := http.NewRequest(w.HTTPMethod, url, strings.NewReader(string(payload)))
- if err != nil {
- return nil, err
+ return "", fmt.Errorf("getMatrixTxnID: payload size %d > %d", len(payload), matrixPayloadSizeLimit)
}
- req.Header.Set("Content-Type", "application/json")
- req.Header.Add("Authorization", "Bearer "+payloadunsafe.AccessToken)
-
- return req, nil
-}
-
-// getMatrixTxnID creates a txnID based on the payload to ensure idempotency
-func getMatrixTxnID(payload []byte) (string, error) {
h := sha1.New()
_, err := h.Write(payload)
if err != nil {
return "", err
}
- return fmt.Sprintf("%x", h.Sum(nil)), nil
+ return hex.EncodeToString(h.Sum(nil)), nil
}
diff --git a/services/webhook/matrix_test.go b/services/webhook/matrix_test.go
index 3cc7c7518fc49..8c710942280fa 100644
--- a/services/webhook/matrix_test.go
+++ b/services/webhook/matrix_test.go
@@ -1,14 +1,13 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -18,243 +17,203 @@ func TestMatrixPayload(t *testing.T) {
t.Run("Create", func(t *testing.T) {
p := createTestPayload()
- d := new(MatrixPayloadUnsafe)
+ d := new(MatrixPayload)
pl, err := d.Create(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `[test/repo :test ] branch created by user1`, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):[test](http://localhost:3000/test/repo/src/branch/test)] branch created by user1", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo :test ] branch created by user1`, pl.(*MatrixPayload).FormattedBody)
})
t.Run("Delete", func(t *testing.T) {
p := deleteTestPayload()
- d := new(MatrixPayloadUnsafe)
+ d := new(MatrixPayload)
pl, err := d.Delete(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `[test/repo :test] branch deleted by user1`, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo):test] branch deleted by user1", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo :test] branch deleted by user1`, pl.(*MatrixPayload).FormattedBody)
})
t.Run("Fork", func(t *testing.T) {
p := forkTestPayload()
- d := new(MatrixPayloadUnsafe)
+ d := new(MatrixPayload)
pl, err := d.Fork(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `test/repo2 is forked to test/repo `, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, "[test/repo2](http://localhost:3000/test/repo2) is forked to [test/repo](http://localhost:3000/test/repo)", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `test/repo2 is forked to test/repo `, pl.(*MatrixPayload).FormattedBody)
})
t.Run("Push", func(t *testing.T) {
p := pushTestPayload()
- d := new(MatrixPayloadUnsafe)
+ d := new(MatrixPayload)
pl, err := d.Push(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `[test/repo ] user1 pushed 2 commits to test :2020558 : commit message - user12020558 : commit message - user1`, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] user1 pushed 2 commits to test :2020558 : commit message - user12020558 : commit message - user1`, pl.(*MatrixPayload).FormattedBody)
})
t.Run("Issue", func(t *testing.T) {
p := issueTestPayload()
- d := new(MatrixPayloadUnsafe)
+ d := new(MatrixPayload)
p.Action = api.HookIssueOpened
pl, err := d.Issue(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `[test/repo ] Issue opened: #2 crash by user1 `, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue opened: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] Issue opened: #2 crash by user1 `, pl.(*MatrixPayload).FormattedBody)
p.Action = api.HookIssueClosed
pl, err = d.Issue(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `[test/repo ] Issue closed: #2 crash by user1 `, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Issue closed: [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] Issue closed: #2 crash by user1 `, pl.(*MatrixPayload).FormattedBody)
})
t.Run("IssueComment", func(t *testing.T) {
p := issueCommentTestPayload()
- d := new(MatrixPayloadUnsafe)
+ d := new(MatrixPayload)
pl, err := d.IssueComment(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `[test/repo ] New comment on issue #2 crash by user1 `, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on issue [#2 crash](http://localhost:3000/test/repo/issues/2) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] New comment on issue #2 crash by user1 `, pl.(*MatrixPayload).FormattedBody)
})
t.Run("PullRequest", func(t *testing.T) {
p := pullRequestTestPayload()
- d := new(MatrixPayloadUnsafe)
+ d := new(MatrixPayload)
pl, err := d.PullRequest(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `[test/repo ] Pull request opened: #12 Fix bug by user1 `, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request opened: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] Pull request opened: #12 Fix bug by user1 `, pl.(*MatrixPayload).FormattedBody)
})
t.Run("PullRequestComment", func(t *testing.T) {
p := pullRequestCommentTestPayload()
- d := new(MatrixPayloadUnsafe)
+ d := new(MatrixPayload)
pl, err := d.IssueComment(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `[test/repo ] New comment on pull request #12 Fix bug by user1 `, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New comment on pull request [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] New comment on pull request #12 Fix bug by user1 `, pl.(*MatrixPayload).FormattedBody)
})
t.Run("Review", func(t *testing.T) {
p := pullRequestTestPayload()
p.Action = api.HookIssueReviewed
- d := new(MatrixPayloadUnsafe)
- pl, err := d.Review(p, webhook_model.HookEventPullRequestReviewApproved)
+ d := new(MatrixPayload)
+ pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `[test/repo ] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by user1 `, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Pull request review approved: [#12 Fix bug](http://localhost:3000/test/repo/pulls/12) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] Pull request review approved: #12 Fix bug by user1 `, pl.(*MatrixPayload).FormattedBody)
})
t.Run("Repository", func(t *testing.T) {
p := repositoryTestPayload()
- d := new(MatrixPayloadUnsafe)
+ d := new(MatrixPayload)
pl, err := d.Repository(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `[test/repo ] Repository created by user1 `, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, `[[test/repo](http://localhost:3000/test/repo)] Repository created by [user1](https://try.gitea.io/user1)`, pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] Repository created by user1 `, pl.(*MatrixPayload).FormattedBody)
+ })
+
+ t.Run("Wiki", func(t *testing.T) {
+ p := wikiTestPayload()
+
+ d := new(MatrixPayload)
+ p.Action = api.HookWikiCreated
+ pl, err := d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
+
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] New wiki page '[index](http://localhost:3000/test/repo/wiki/index)' (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] New wiki page 'index ' (Wiki change comment) by user1 `, pl.(*MatrixPayload).FormattedBody)
+
+ p.Action = api.HookWikiEdited
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
+
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' edited (Wiki change comment) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] Wiki page 'index ' edited (Wiki change comment) by user1 `, pl.(*MatrixPayload).FormattedBody)
+
+ p.Action = api.HookWikiDeleted
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
+
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Wiki page '[index](http://localhost:3000/test/repo/wiki/index)' deleted by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] Wiki page 'index ' deleted by user1 `, pl.(*MatrixPayload).FormattedBody)
})
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
- d := new(MatrixPayloadUnsafe)
+ d := new(MatrixPayload)
pl, err := d.Release(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
- assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/src/v1.0) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayloadUnsafe).Body)
- assert.Equal(t, `[test/repo ] Release created: v1.0 by user1 `, pl.(*MatrixPayloadUnsafe).FormattedBody)
+ assert.Equal(t, "[[test/repo](http://localhost:3000/test/repo)] Release created: [v1.0](http://localhost:3000/test/repo/releases/tag/v1.0) by [user1](https://try.gitea.io/user1)", pl.(*MatrixPayload).Body)
+ assert.Equal(t, `[test/repo ] Release created: v1.0 by user1 `, pl.(*MatrixPayload).FormattedBody)
})
}
func TestMatrixJSONPayload(t *testing.T) {
p := pushTestPayload()
- pl, err := new(MatrixPayloadUnsafe).Push(p)
+ pl, err := new(MatrixPayload).Push(p)
require.NoError(t, err)
require.NotNil(t, pl)
- require.IsType(t, &MatrixPayloadUnsafe{}, pl)
+ require.IsType(t, &MatrixPayload{}, pl)
json, err := pl.JSONPayload()
require.NoError(t, err)
assert.NotEmpty(t, json)
}
-func TestMatrixHookRequest(t *testing.T) {
- w := &webhook_model.Webhook{}
-
- h := &webhook_model.HookTask{
- PayloadContent: `{
- "body": "[[user1/test](http://localhost:3000/user1/test)] user1 pushed 1 commit to [master](http://localhost:3000/user1/test/src/branch/master):\n[5175ef2](http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee): Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n - user1",
- "msgtype": "m.notice",
- "format": "org.matrix.custom.html",
- "formatted_body": "[\u003ca href=\"http://localhost:3000/user1/test\"\u003euser1/test\u003c/a\u003e] user1 pushed 1 commit to \u003ca href=\"http://localhost:3000/user1/test/src/branch/master\"\u003emaster\u003c/a\u003e:\u003cbr\u003e\u003ca href=\"http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee\"\u003e5175ef2\u003c/a\u003e: Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n - user1",
- "io.gitea.commits": [
- {
- "id": "5175ef26201c58b035a3404b3fe02b4e8d436eee",
- "message": "Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n",
- "url": "http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee",
- "author": {
- "name": "user1",
- "email": "user@mail.com",
- "username": ""
- },
- "committer": {
- "name": "user1",
- "email": "user@mail.com",
- "username": ""
- },
- "verification": null,
- "timestamp": "0001-01-01T00:00:00Z",
- "added": null,
- "removed": null,
- "modified": null
- }
- ],
- "access_token": "dummy_access_token"
-}`,
- }
-
- wantPayloadContent := `{
- "body": "[[user1/test](http://localhost:3000/user1/test)] user1 pushed 1 commit to [master](http://localhost:3000/user1/test/src/branch/master):\n[5175ef2](http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee): Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n - user1",
- "msgtype": "m.notice",
- "format": "org.matrix.custom.html",
- "formatted_body": "[\u003ca href=\"http://localhost:3000/user1/test\"\u003euser1/test\u003c/a\u003e] user1 pushed 1 commit to \u003ca href=\"http://localhost:3000/user1/test/src/branch/master\"\u003emaster\u003c/a\u003e:\u003cbr\u003e\u003ca href=\"http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee\"\u003e5175ef2\u003c/a\u003e: Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n - user1",
- "io.gitea.commits": [
- {
- "id": "5175ef26201c58b035a3404b3fe02b4e8d436eee",
- "message": "Merge pull request 'Change readme.md' (#2) from add-matrix-webhook into master\n\nReviewed-on: http://localhost:3000/user1/test/pulls/2\n",
- "url": "http://localhost:3000/user1/test/commit/5175ef26201c58b035a3404b3fe02b4e8d436eee",
- "author": {
- "name": "user1",
- "email": "user@mail.com",
- "username": ""
- },
- "committer": {
- "name": "user1",
- "email": "user@mail.com",
- "username": ""
- },
- "verification": null,
- "timestamp": "0001-01-01T00:00:00Z",
- "added": null,
- "removed": null,
- "modified": null
- }
- ]
-}`
-
- req, err := getMatrixHookRequest(w, h)
- require.NoError(t, err)
- require.NotNil(t, req)
-
- assert.Equal(t, "Bearer dummy_access_token", req.Header.Get("Authorization"))
- assert.Equal(t, wantPayloadContent, h.PayloadContent)
-}
-
func Test_getTxnID(t *testing.T) {
type args struct {
payload []byte
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 59e2e93493982..03d92821b9eb3 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -1,18 +1,18 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
"fmt"
+ "net/url"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
)
type (
@@ -124,11 +124,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 == "" {
@@ -155,7 +155,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
}
@@ -205,7 +205,7 @@ func (m *MSTeamsPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader,
}
// Review implements PayloadConvertor Review method
-func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) {
+func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
var text, title string
var color int
switch p.Action {
@@ -219,11 +219,11 @@ func (m *MSTeamsPayload) Review(p *api.PullRequestPayload, event webhook_model.H
text = p.Review.Content
switch event {
- case webhook_model.HookEventPullRequestReviewApproved:
+ case webhook_module.HookEventPullRequestReviewApproved:
color = greenColor
- case webhook_model.HookEventPullRequestReviewRejected:
+ case webhook_module.HookEventPullRequestReviewRejected:
color = redColor
- case webhook_model.HookEventPullRequestComment:
+ case webhook_module.HookEventPullRequestComment:
color = greyColor
default:
color = yellowColor
@@ -266,6 +266,21 @@ func (m *MSTeamsPayload) Repository(p *api.RepositoryPayload) (api.Payloader, er
), nil
}
+// Wiki implements PayloadConvertor Wiki method
+func (m *MSTeamsPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+ title, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
+
+ return createMSTeamsPayload(
+ p.Repository,
+ p.Sender,
+ title,
+ "",
+ p.Repository.HTMLURL+"/wiki/"+url.PathEscape(p.Page),
+ color,
+ &MSTeamsFact{"Repository:", p.Repository.FullName},
+ ), nil
+}
+
// Release implements PayloadConvertor Release method
func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
title, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
@@ -282,7 +297,7 @@ func (m *MSTeamsPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
}
// GetMSTeamsPayload converts a MSTeams webhook into a MSTeamsPayload
-func GetMSTeamsPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) {
+func GetMSTeamsPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(MSTeamsPayload), p, event)
}
diff --git a/services/webhook/msteams_test.go b/services/webhook/msteams_test.go
index 3fdf47c1ae8cc..4f378713cc7c6 100644
--- a/services/webhook/msteams_test.go
+++ b/services/webhook/msteams_test.go
@@ -1,14 +1,13 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -278,7 +277,7 @@ func TestMSTeamsPayload(t *testing.T) {
p.Action = api.HookIssueReviewed
d := new(MSTeamsPayload)
- pl, err := d.Review(p, webhook_model.HookEventPullRequestReviewApproved)
+ pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &MSTeamsPayload{}, pl)
@@ -330,6 +329,80 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Equal(t, "http://localhost:3000/test/repo", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
})
+ t.Run("Wiki", func(t *testing.T) {
+ p := wikiTestPayload()
+
+ d := new(MSTeamsPayload)
+ p.Action = api.HookWikiCreated
+ pl, err := d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &MSTeamsPayload{}, pl)
+
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Title)
+ assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.(*MSTeamsPayload).Summary)
+ assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
+ assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
+ assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text)
+ assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
+ for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ if fact.Name == "Repository:" {
+ assert.Equal(t, p.Repository.FullName, fact.Value)
+ } else {
+ t.Fail()
+ }
+ }
+ assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
+ assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+
+ p.Action = api.HookWikiEdited
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &MSTeamsPayload{}, pl)
+
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Title)
+ assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.(*MSTeamsPayload).Summary)
+ assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
+ assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
+ assert.Equal(t, "", pl.(*MSTeamsPayload).Sections[0].Text)
+ assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
+ for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ if fact.Name == "Repository:" {
+ assert.Equal(t, p.Repository.FullName, fact.Value)
+ } else {
+ t.Fail()
+ }
+ }
+ assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
+ assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+
+ p.Action = api.HookWikiDeleted
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &MSTeamsPayload{}, pl)
+
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Title)
+ assert.Equal(t, "[test/repo] Wiki page 'index' deleted", pl.(*MSTeamsPayload).Summary)
+ assert.Len(t, pl.(*MSTeamsPayload).Sections, 1)
+ assert.Equal(t, "user1", pl.(*MSTeamsPayload).Sections[0].ActivitySubtitle)
+ assert.Empty(t, pl.(*MSTeamsPayload).Sections[0].Text)
+ assert.Len(t, pl.(*MSTeamsPayload).Sections[0].Facts, 2)
+ for _, fact := range pl.(*MSTeamsPayload).Sections[0].Facts {
+ if fact.Name == "Repository:" {
+ assert.Equal(t, p.Repository.FullName, fact.Value)
+ } else {
+ t.Fail()
+ }
+ }
+ assert.Len(t, pl.(*MSTeamsPayload).PotentialAction, 1)
+ assert.Len(t, pl.(*MSTeamsPayload).PotentialAction[0].Targets, 1)
+ assert.Equal(t, "http://localhost:3000/test/repo/wiki/index", pl.(*MSTeamsPayload).PotentialAction[0].Targets[0].URI)
+ })
+
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
new file mode 100644
index 0000000000000..ee80766032ff1
--- /dev/null
+++ b/services/webhook/notifier.go
@@ -0,0 +1,860 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package webhook
+
+import (
+ "context"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ packages_model "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/notification"
+ "code.gitea.io/gitea/modules/notification/base"
+ "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
+ "code.gitea.io/gitea/services/convert"
+)
+
+func init() {
+ notification.RegisterNotifier(&webhookNotifier{})
+}
+
+type webhookNotifier struct {
+ base.NullNotifier
+}
+
+var _ base.Notifier = &webhookNotifier{}
+
+// NewNotifier create a new webhookNotifier notifier
+func NewNotifier() base.Notifier {
+ return &webhookNotifier{}
+}
+
+func (m *webhookNotifier) NotifyIssueClearLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) {
+ if err := issue.LoadPoster(ctx); err != nil {
+ log.Error("LoadPoster: %v", err)
+ return
+ }
+
+ if err := issue.LoadRepo(ctx); err != nil {
+ log.Error("LoadRepo: %v", err)
+ return
+ }
+
+ mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo)
+ var err error
+ if issue.IsPull {
+ if err = issue.LoadPullRequest(ctx); err != nil {
+ log.Error("LoadPullRequest: %v", err)
+ return
+ }
+
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequestLabel, &api.PullRequestPayload{
+ Action: api.HookIssueLabelCleared,
+ Index: issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ })
+ } else {
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueLabel, &api.IssuePayload{
+ Action: api.HookIssueLabelCleared,
+ Index: issue.Index,
+ Issue: convert.ToAPIIssue(ctx, issue),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ })
+ }
+ if err != nil {
+ log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) {
+ oldMode, _ := access_model.AccessLevel(ctx, doer, oldRepo)
+ mode, _ := access_model.AccessLevel(ctx, doer, repo)
+
+ // forked webhook
+ if err := PrepareWebhooks(ctx, EventSource{Repository: oldRepo}, webhook_module.HookEventFork, &api.ForkPayload{
+ Forkee: convert.ToRepo(ctx, oldRepo, oldMode),
+ Repo: convert.ToRepo(ctx, repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks [repo_id: %d]: %v", oldRepo.ID, err)
+ }
+
+ u := repo.MustOwner(ctx)
+
+ // Add to hook queue for created repo after session commit.
+ if u.IsOrganization() {
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{
+ Action: api.HookRepoCreated,
+ Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner),
+ Organization: convert.ToUser(u, nil),
+ Sender: convert.ToUser(doer, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err)
+ }
+ }
+}
+
+func (m *webhookNotifier) NotifyCreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
+ // Add to hook queue for created repo after session commit.
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{
+ Action: api.HookRepoCreated,
+ Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner),
+ Organization: convert.ToUser(u, nil),
+ Sender: convert.ToUser(doer, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyDeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) {
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{
+ Action: api.HookRepoDeleted,
+ Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner),
+ Organization: convert.ToUser(repo.MustOwner(ctx), nil),
+ Sender: convert.ToUser(doer, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyMigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
+ // Add to hook queue for created repo after session commit.
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{
+ Action: api.HookRepoCreated,
+ Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner),
+ Organization: convert.ToUser(u, nil),
+ Sender: convert.ToUser(doer, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
+ if issue.IsPull {
+ mode, _ := access_model.AccessLevelUnit(ctx, doer, issue.Repo, unit.TypePullRequests)
+
+ if err := issue.LoadPullRequest(ctx); err != nil {
+ log.Error("LoadPullRequest failed: %v", err)
+ return
+ }
+ issue.PullRequest.Issue = issue
+ apiPullRequest := &api.PullRequestPayload{
+ Index: issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ }
+ if removed {
+ apiPullRequest.Action = api.HookIssueUnassigned
+ } else {
+ apiPullRequest.Action = api.HookIssueAssigned
+ }
+ // Assignee comment triggers a webhook
+ if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequestAssign, apiPullRequest); err != nil {
+ log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err)
+ return
+ }
+ } else {
+ mode, _ := access_model.AccessLevelUnit(ctx, doer, issue.Repo, unit.TypeIssues)
+ apiIssue := &api.IssuePayload{
+ Index: issue.Index,
+ Issue: convert.ToAPIIssue(ctx, issue),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ }
+ if removed {
+ apiIssue.Action = api.HookIssueUnassigned
+ } else {
+ apiIssue.Action = api.HookIssueAssigned
+ }
+ // Assignee comment triggers a webhook
+ if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueAssign, apiIssue); err != nil {
+ log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err)
+ return
+ }
+ }
+}
+
+func (m *webhookNotifier) NotifyIssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string) {
+ mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo)
+ var err error
+ if issue.IsPull {
+ if err = issue.LoadPullRequest(ctx); err != nil {
+ log.Error("LoadPullRequest failed: %v", err)
+ return
+ }
+ issue.PullRequest.Issue = issue
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{
+ Action: api.HookIssueEdited,
+ Index: issue.Index,
+ Changes: &api.ChangesPayload{
+ Title: &api.ChangesFromPayload{
+ From: oldTitle,
+ },
+ },
+ PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ })
+ } else {
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssues, &api.IssuePayload{
+ Action: api.HookIssueEdited,
+ Index: issue.Index,
+ Changes: &api.ChangesPayload{
+ Title: &api.ChangesFromPayload{
+ From: oldTitle,
+ },
+ },
+ Issue: convert.ToAPIIssue(ctx, issue),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ })
+ }
+
+ if err != nil {
+ log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyIssueChangeStatus(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) {
+ mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo)
+ var err error
+ if issue.IsPull {
+ if err = issue.LoadPullRequest(ctx); err != nil {
+ log.Error("LoadPullRequest: %v", err)
+ return
+ }
+ // Merge pull request calls issue.changeStatus so we need to handle separately.
+ apiPullRequest := &api.PullRequestPayload{
+ Index: issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ }
+ if isClosed {
+ apiPullRequest.Action = api.HookIssueClosed
+ } else {
+ apiPullRequest.Action = api.HookIssueReOpened
+ }
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, apiPullRequest)
+ } else {
+ apiIssue := &api.IssuePayload{
+ Index: issue.Index,
+ Issue: convert.ToAPIIssue(ctx, issue),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ }
+ if isClosed {
+ apiIssue.Action = api.HookIssueClosed
+ } else {
+ apiIssue.Action = api.HookIssueReOpened
+ }
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssues, apiIssue)
+ }
+ if err != nil {
+ log.Error("PrepareWebhooks [is_pull: %v, is_closed: %v]: %v", issue.IsPull, isClosed, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyNewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) {
+ if err := issue.LoadRepo(ctx); err != nil {
+ log.Error("issue.LoadRepo: %v", err)
+ return
+ }
+ if err := issue.LoadPoster(ctx); err != nil {
+ log.Error("issue.LoadPoster: %v", err)
+ return
+ }
+
+ mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo)
+ if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssues, &api.IssuePayload{
+ Action: api.HookIssueOpened,
+ Index: issue.Index,
+ Issue: convert.ToAPIIssue(ctx, issue),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(issue.Poster, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
+func (m *webhookNotifier) NotifyNewPullRequest(ctx context.Context, pull *issues_model.PullRequest, mentions []*user_model.User) {
+ if err := pull.LoadIssue(ctx); err != nil {
+ log.Error("pull.LoadIssue: %v", err)
+ return
+ }
+ if err := pull.Issue.LoadRepo(ctx); err != nil {
+ log.Error("pull.Issue.LoadRepo: %v", err)
+ return
+ }
+ if err := pull.Issue.LoadPoster(ctx); err != nil {
+ log.Error("pull.Issue.LoadPoster: %v", err)
+ return
+ }
+
+ mode, _ := access_model.AccessLevel(ctx, pull.Issue.Poster, pull.Issue.Repo)
+ if err := PrepareWebhooks(ctx, EventSource{Repository: pull.Issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{
+ Action: api.HookIssueOpened,
+ Index: pull.Issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, pull, nil),
+ Repository: convert.ToRepo(ctx, pull.Issue.Repo, mode),
+ Sender: convert.ToUser(pull.Issue.Poster, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
+func (m *webhookNotifier) NotifyIssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) {
+ if err := issue.LoadRepo(ctx); err != nil {
+ log.Error("LoadRepo: %v", err)
+ return
+ }
+
+ mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo)
+ var err error
+ if issue.IsPull {
+ issue.PullRequest.Issue = issue
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{
+ Action: api.HookIssueEdited,
+ Index: issue.Index,
+ Changes: &api.ChangesPayload{
+ Body: &api.ChangesFromPayload{
+ From: oldContent,
+ },
+ },
+ PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ })
+ } else {
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssues, &api.IssuePayload{
+ Action: api.HookIssueEdited,
+ Index: issue.Index,
+ Changes: &api.ChangesPayload{
+ Body: &api.ChangesFromPayload{
+ From: oldContent,
+ },
+ },
+ Issue: convert.ToAPIIssue(ctx, issue),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ })
+ }
+ if err != nil {
+ log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyUpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
+ if err := c.LoadPoster(ctx); err != nil {
+ log.Error("LoadPoster: %v", err)
+ return
+ }
+ if err := c.LoadIssue(ctx); err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+
+ if err := c.Issue.LoadAttributes(ctx); err != nil {
+ log.Error("LoadAttributes: %v", err)
+ return
+ }
+
+ var eventType webhook_module.HookEventType
+ if c.Issue.IsPull {
+ eventType = webhook_module.HookEventPullRequestComment
+ } else {
+ eventType = webhook_module.HookEventIssueComment
+ }
+
+ mode, _ := access_model.AccessLevel(ctx, doer, c.Issue.Repo)
+ if err := PrepareWebhooks(ctx, EventSource{Repository: c.Issue.Repo}, eventType, &api.IssueCommentPayload{
+ Action: api.HookIssueCommentEdited,
+ Issue: convert.ToAPIIssue(ctx, c.Issue),
+ Comment: convert.ToComment(c),
+ Changes: &api.ChangesPayload{
+ Body: &api.ChangesFromPayload{
+ From: oldContent,
+ },
+ },
+ Repository: convert.ToRepo(ctx, 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)
+ }
+}
+
+func (m *webhookNotifier) NotifyCreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
+ issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
+) {
+ var eventType webhook_module.HookEventType
+ if issue.IsPull {
+ eventType = webhook_module.HookEventPullRequestComment
+ } else {
+ eventType = webhook_module.HookEventIssueComment
+ }
+
+ mode, _ := access_model.AccessLevel(ctx, doer, repo)
+ if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, eventType, &api.IssueCommentPayload{
+ Action: api.HookIssueCommentCreated,
+ Issue: convert.ToAPIIssue(ctx, issue),
+ Comment: convert.ToComment(comment),
+ Repository: convert.ToRepo(ctx, repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ IsPull: issue.IsPull,
+ }); err != nil {
+ log.Error("PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyDeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) {
+ var err error
+
+ if err = comment.LoadPoster(ctx); err != nil {
+ log.Error("LoadPoster: %v", err)
+ return
+ }
+ if err = comment.LoadIssue(ctx); err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+
+ if err = comment.Issue.LoadAttributes(ctx); err != nil {
+ log.Error("LoadAttributes: %v", err)
+ return
+ }
+
+ var eventType webhook_module.HookEventType
+ if comment.Issue.IsPull {
+ eventType = webhook_module.HookEventPullRequestComment
+ } else {
+ eventType = webhook_module.HookEventIssueComment
+ }
+
+ mode, _ := access_model.AccessLevel(ctx, doer, comment.Issue.Repo)
+ if err := PrepareWebhooks(ctx, EventSource{Repository: comment.Issue.Repo}, eventType, &api.IssueCommentPayload{
+ Action: api.HookIssueCommentDeleted,
+ Issue: convert.ToAPIIssue(ctx, comment.Issue),
+ Comment: convert.ToComment(comment),
+ Repository: convert.ToRepo(ctx, 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(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page, comment string) {
+ // Add to hook queue for created wiki page.
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventWiki, &api.WikiPayload{
+ Action: api.HookWikiCreated,
+ Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner),
+ Sender: convert.ToUser(doer, nil),
+ Page: page,
+ Comment: comment,
+ }); err != nil {
+ log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyEditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page, comment string) {
+ // Add to hook queue for edit wiki page.
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventWiki, &api.WikiPayload{
+ Action: api.HookWikiEdited,
+ Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner),
+ Sender: convert.ToUser(doer, nil),
+ Page: page,
+ Comment: comment,
+ }); err != nil {
+ log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyDeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page string) {
+ // Add to hook queue for edit wiki page.
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventWiki, &api.WikiPayload{
+ Action: api.HookWikiDeleted,
+ Repository: convert.ToRepo(ctx, repo, perm.AccessModeOwner),
+ Sender: convert.ToUser(doer, nil),
+ Page: page,
+ }); err != nil {
+ log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyIssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue,
+ addedLabels, removedLabels []*issues_model.Label,
+) {
+ var err error
+
+ if err = issue.LoadRepo(ctx); err != nil {
+ log.Error("LoadRepo: %v", err)
+ return
+ }
+
+ if err = issue.LoadPoster(ctx); err != nil {
+ log.Error("LoadPoster: %v", err)
+ return
+ }
+
+ mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo)
+ if issue.IsPull {
+ if err = issue.LoadPullRequest(ctx); err != nil {
+ log.Error("loadPullRequest: %v", err)
+ return
+ }
+ if err = issue.PullRequest.LoadIssue(ctx); err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequestLabel, &api.PullRequestPayload{
+ Action: api.HookIssueLabelUpdated,
+ Index: issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+ Repository: convert.ToRepo(ctx, issue.Repo, perm.AccessModeNone),
+ Sender: convert.ToUser(doer, nil),
+ })
+ } else {
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueLabel, &api.IssuePayload{
+ Action: api.HookIssueLabelUpdated,
+ Index: issue.Index,
+ Issue: convert.ToAPIIssue(ctx, issue),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ })
+ }
+ if err != nil {
+ log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyIssueChangeMilestone(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) {
+ var hookAction api.HookIssueAction
+ var err error
+ if issue.MilestoneID > 0 {
+ hookAction = api.HookIssueMilestoned
+ } else {
+ hookAction = api.HookIssueDemilestoned
+ }
+
+ if err = issue.LoadAttributes(ctx); err != nil {
+ log.Error("issue.LoadAttributes failed: %v", err)
+ return
+ }
+
+ mode, _ := access_model.AccessLevel(ctx, doer, issue.Repo)
+ if issue.IsPull {
+ err = issue.PullRequest.LoadIssue(ctx)
+ if err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequestMilestone, &api.PullRequestPayload{
+ Action: hookAction,
+ Index: issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ })
+ } else {
+ err = PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssueMilestone, &api.IssuePayload{
+ Action: hookAction,
+ Index: issue.Index,
+ Issue: convert.ToAPIIssue(ctx, issue),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ })
+ }
+ if err != nil {
+ log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
+ apiPusher := convert.ToUser(pusher, nil)
+ apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo.RepoPath(), repo.HTMLURL())
+ if err != nil {
+ log.Error("commits.ToAPIPayloadCommits failed: %v", err)
+ return
+ }
+
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.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(ctx, repo, perm.AccessModeOwner),
+ Pusher: apiPusher,
+ Sender: apiPusher,
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
+func (m *webhookNotifier) NotifyAutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
+ // just redirect to the NotifyMergePullRequest
+ m.NotifyMergePullRequest(ctx, doer, pr)
+}
+
+func (*webhookNotifier) NotifyMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
+ // Reload pull request information.
+ if err := pr.LoadAttributes(ctx); err != nil {
+ log.Error("LoadAttributes: %v", err)
+ return
+ }
+
+ if err := pr.LoadIssue(ctx); err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+
+ if err := pr.Issue.LoadRepo(ctx); err != nil {
+ log.Error("pr.Issue.LoadRepo: %v", err)
+ return
+ }
+
+ mode, err := access_model.AccessLevel(ctx, doer, pr.Issue.Repo)
+ if err != nil {
+ log.Error("models.AccessLevel: %v", err)
+ return
+ }
+
+ // Merge pull request calls issue.changeStatus so we need to handle separately.
+ apiPullRequest := &api.PullRequestPayload{
+ Index: pr.Issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, pr, nil),
+ Repository: convert.ToRepo(ctx, pr.Issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ Action: api.HookIssueClosed,
+ }
+
+ if err := PrepareWebhooks(ctx, EventSource{Repository: pr.Issue.Repo}, webhook_module.HookEventPullRequest, apiPullRequest); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
+func (m *webhookNotifier) NotifyPullRequestChangeTargetBranch(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) {
+ if err := pr.LoadIssue(ctx); err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+
+ issue := pr.Issue
+
+ mode, _ := access_model.AccessLevel(ctx, issue.Poster, issue.Repo)
+ if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{
+ Action: api.HookIssueEdited,
+ Index: issue.Index,
+ Changes: &api.ChangesPayload{
+ Ref: &api.ChangesFromPayload{
+ From: oldBranch,
+ },
+ },
+ PullRequest: convert.ToAPIPullRequest(ctx, pr, nil),
+ Repository: convert.ToRepo(ctx, issue.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks [pr: %d]: %v", pr.ID, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyPullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
+ var reviewHookType webhook_module.HookEventType
+
+ switch review.Type {
+ case issues_model.ReviewTypeApprove:
+ reviewHookType = webhook_module.HookEventPullRequestReviewApproved
+ case issues_model.ReviewTypeComment:
+ reviewHookType = webhook_module.HookEventPullRequestComment
+ case issues_model.ReviewTypeReject:
+ reviewHookType = webhook_module.HookEventPullRequestReviewRejected
+ default:
+ // unsupported review webhook type here
+ log.Error("Unsupported review webhook type")
+ return
+ }
+
+ if err := pr.LoadIssue(ctx); err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+
+ mode, err := access_model.AccessLevel(ctx, review.Issue.Poster, review.Issue.Repo)
+ if err != nil {
+ log.Error("models.AccessLevel: %v", err)
+ return
+ }
+ if err := PrepareWebhooks(ctx, EventSource{Repository: review.Issue.Repo}, reviewHookType, &api.PullRequestPayload{
+ Action: api.HookIssueReviewed,
+ Index: review.Issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, pr, nil),
+ Repository: convert.ToRepo(ctx, review.Issue.Repo, mode),
+ Sender: convert.ToUser(review.Reviewer, nil),
+ Review: &api.ReviewPayload{
+ Type: string(reviewHookType),
+ Content: review.Content,
+ },
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
+func (m *webhookNotifier) NotifyCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refType, refFullName, refID string) {
+ apiPusher := convert.ToUser(pusher, nil)
+ apiRepo := convert.ToRepo(ctx, repo, perm.AccessModeNone)
+ refName := git.RefEndName(refFullName)
+
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventCreate, &api.CreatePayload{
+ Ref: refName,
+ Sha: refID,
+ RefType: refType,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
+func (m *webhookNotifier) NotifyPullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
+ if err := pr.LoadIssue(ctx); err != nil {
+ log.Error("LoadIssue: %v", err)
+ return
+ }
+ if err := pr.Issue.LoadAttributes(ctx); err != nil {
+ log.Error("LoadAttributes: %v", err)
+ return
+ }
+
+ if err := PrepareWebhooks(ctx, EventSource{Repository: pr.Issue.Repo}, webhook_module.HookEventPullRequestSync, &api.PullRequestPayload{
+ Action: api.HookIssueSynchronized,
+ Index: pr.Issue.Index,
+ PullRequest: convert.ToAPIPullRequest(ctx, pr, nil),
+ Repository: convert.ToRepo(ctx, pr.Issue.Repo, perm.AccessModeNone),
+ Sender: convert.ToUser(doer, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks [pull_id: %v]: %v", pr.ID, err)
+ }
+}
+
+func (m *webhookNotifier) NotifyDeleteRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refType, refFullName string) {
+ apiPusher := convert.ToUser(pusher, nil)
+ apiRepo := convert.ToRepo(ctx, repo, perm.AccessModeNone)
+ refName := git.RefEndName(refFullName)
+
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventDelete, &api.DeletePayload{
+ Ref: refName,
+ RefType: refType,
+ PusherType: api.PusherTypeUser,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ log.Error("PrepareWebhooks.(delete %s): %v", refType, err)
+ }
+}
+
+func sendReleaseHook(ctx context.Context, doer *user_model.User, rel *repo_model.Release, action api.HookReleaseAction) {
+ if err := rel.LoadAttributes(ctx); err != nil {
+ log.Error("LoadAttributes: %v", err)
+ return
+ }
+
+ mode, _ := access_model.AccessLevel(ctx, doer, rel.Repo)
+ if err := PrepareWebhooks(ctx, EventSource{Repository: rel.Repo}, webhook_module.HookEventRelease, &api.ReleasePayload{
+ Action: action,
+ Release: convert.ToRelease(rel),
+ Repository: convert.ToRepo(ctx, rel.Repo, mode),
+ Sender: convert.ToUser(doer, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
+func (m *webhookNotifier) NotifyNewRelease(ctx context.Context, rel *repo_model.Release) {
+ sendReleaseHook(ctx, rel.Publisher, rel, api.HookReleasePublished)
+}
+
+func (m *webhookNotifier) NotifyUpdateRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) {
+ sendReleaseHook(ctx, doer, rel, api.HookReleaseUpdated)
+}
+
+func (m *webhookNotifier) NotifyDeleteRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) {
+ sendReleaseHook(ctx, doer, rel, api.HookReleaseDeleted)
+}
+
+func (m *webhookNotifier) NotifySyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
+ apiPusher := convert.ToUser(pusher, nil)
+ apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo.RepoPath(), repo.HTMLURL())
+ if err != nil {
+ log.Error("commits.ToAPIPayloadCommits failed: %v", err)
+ return
+ }
+
+ if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.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(ctx, repo, perm.AccessModeOwner),
+ Pusher: apiPusher,
+ Sender: apiPusher,
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
+
+func (m *webhookNotifier) NotifySyncCreateRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refType, refFullName, refID string) {
+ m.NotifyCreateRef(ctx, pusher, repo, refType, refFullName, refID)
+}
+
+func (m *webhookNotifier) NotifySyncDeleteRef(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, refType, refFullName string) {
+ m.NotifyDeleteRef(ctx, pusher, repo, refType, refFullName)
+}
+
+func (m *webhookNotifier) NotifyPackageCreate(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) {
+ notifyPackage(ctx, doer, pd, api.HookPackageCreated)
+}
+
+func (m *webhookNotifier) NotifyPackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor) {
+ notifyPackage(ctx, doer, pd, api.HookPackageDeleted)
+}
+
+func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_model.PackageDescriptor, action api.HookPackageAction) {
+ source := EventSource{
+ Repository: pd.Repository,
+ Owner: pd.Owner,
+ }
+
+ apiPackage, err := convert.ToPackage(ctx, pd, sender)
+ if err != nil {
+ log.Error("Error converting package: %v", err)
+ return
+ }
+
+ if err := PrepareWebhooks(ctx, source, webhook_module.HookEventPackage, &api.PackagePayload{
+ Action: action,
+ Package: apiPackage,
+ Sender: convert.ToUser(sender, nil),
+ }); err != nil {
+ log.Error("PrepareWebhooks: %v", err)
+ }
+}
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index ace93b13ff056..e47e7d32850f4 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
@@ -11,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
)
type (
@@ -21,7 +21,7 @@ type (
} `json:"repository"`
}
- // PackagistMeta contains the meta data for the webhook
+ // PackagistMeta contains the metadata for the webhook
PackagistMeta struct {
Username string `json:"username"`
APIToken string `json:"api_token"`
@@ -50,57 +50,62 @@ func (f *PackagistPayload) JSONPayload() ([]byte, error) {
var _ PayloadConvertor = &PackagistPayload{}
// Create implements PayloadConvertor Create method
-func (f *PackagistPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
+func (f *PackagistPayload) Create(_ *api.CreatePayload) (api.Payloader, error) {
return nil, nil
}
// Delete implements PayloadConvertor Delete method
-func (f *PackagistPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
+func (f *PackagistPayload) Delete(_ *api.DeletePayload) (api.Payloader, error) {
return nil, nil
}
// Fork implements PayloadConvertor Fork method
-func (f *PackagistPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
+func (f *PackagistPayload) Fork(_ *api.ForkPayload) (api.Payloader, error) {
return nil, nil
}
// Push implements PayloadConvertor Push method
-func (f *PackagistPayload) Push(p *api.PushPayload) (api.Payloader, error) {
+func (f *PackagistPayload) Push(_ *api.PushPayload) (api.Payloader, error) {
return f, nil
}
// Issue implements PayloadConvertor Issue method
-func (f *PackagistPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
+func (f *PackagistPayload) Issue(_ *api.IssuePayload) (api.Payloader, error) {
return nil, nil
}
// IssueComment implements PayloadConvertor IssueComment method
-func (f *PackagistPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
+func (f *PackagistPayload) IssueComment(_ *api.IssueCommentPayload) (api.Payloader, error) {
return nil, nil
}
// PullRequest implements PayloadConvertor PullRequest method
-func (f *PackagistPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
+func (f *PackagistPayload) PullRequest(_ *api.PullRequestPayload) (api.Payloader, error) {
return nil, nil
}
// Review implements PayloadConvertor Review method
-func (f *PackagistPayload) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) {
+func (f *PackagistPayload) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (api.Payloader, error) {
return nil, nil
}
// Repository implements PayloadConvertor Repository method
-func (f *PackagistPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
+func (f *PackagistPayload) Repository(_ *api.RepositoryPayload) (api.Payloader, error) {
+ return nil, nil
+}
+
+// Wiki implements PayloadConvertor Wiki method
+func (f *PackagistPayload) Wiki(_ *api.WikiPayload) (api.Payloader, error) {
return nil, nil
}
// Release implements PayloadConvertor Release method
-func (f *PackagistPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
+func (f *PackagistPayload) Release(_ *api.ReleasePayload) (api.Payloader, error) {
return nil, nil
}
// GetPackagistPayload converts a packagist webhook into a PackagistPayload
-func GetPackagistPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) {
+func GetPackagistPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
s := new(PackagistPayload)
packagist := &PackagistMeta{}
diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go
index 08912924d260a..932b56fe9b9a1 100644
--- a/services/webhook/packagist_test.go
+++ b/services/webhook/packagist_test.go
@@ -1,14 +1,13 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -102,7 +101,7 @@ func TestPackagistPayload(t *testing.T) {
p.Action = api.HookIssueReviewed
d := new(PackagistPayload)
- pl, err := d.Review(p, webhook_model.HookEventPullRequestReviewApproved)
+ pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.Nil(t, pl)
})
@@ -116,6 +115,26 @@ func TestPackagistPayload(t *testing.T) {
require.Nil(t, pl)
})
+ t.Run("Wiki", func(t *testing.T) {
+ p := wikiTestPayload()
+
+ d := new(PackagistPayload)
+ p.Action = api.HookWikiCreated
+ pl, err := d.Wiki(p)
+ require.NoError(t, err)
+ require.Nil(t, pl)
+
+ p.Action = api.HookWikiEdited
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.Nil(t, pl)
+
+ p.Action = api.HookWikiDeleted
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.Nil(t, pl)
+ })
+
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index 0e09dd1b1e0db..9eff25628bce8 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -1,12 +1,11 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
- webhook_model "code.gitea.io/gitea/models/webhook"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
)
// PayloadConvertor defines the interface to convert system webhook payload to external payload
@@ -19,38 +18,41 @@ type PayloadConvertor interface {
IssueComment(*api.IssueCommentPayload) (api.Payloader, error)
Push(*api.PushPayload) (api.Payloader, error)
PullRequest(*api.PullRequestPayload) (api.Payloader, error)
- Review(*api.PullRequestPayload, webhook_model.HookEventType) (api.Payloader, error)
+ Review(*api.PullRequestPayload, webhook_module.HookEventType) (api.Payloader, error)
Repository(*api.RepositoryPayload) (api.Payloader, error)
Release(*api.ReleasePayload) (api.Payloader, error)
+ Wiki(*api.WikiPayload) (api.Payloader, error)
}
-func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_model.HookEventType) (api.Payloader, error) {
+func convertPayloader(s PayloadConvertor, p api.Payloader, event webhook_module.HookEventType) (api.Payloader, error) {
switch event {
- case webhook_model.HookEventCreate:
+ case webhook_module.HookEventCreate:
return s.Create(p.(*api.CreatePayload))
- case webhook_model.HookEventDelete:
+ case webhook_module.HookEventDelete:
return s.Delete(p.(*api.DeletePayload))
- case webhook_model.HookEventFork:
+ case webhook_module.HookEventFork:
return s.Fork(p.(*api.ForkPayload))
- case webhook_model.HookEventIssues, webhook_model.HookEventIssueAssign, webhook_model.HookEventIssueLabel, webhook_model.HookEventIssueMilestone:
+ case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone:
return s.Issue(p.(*api.IssuePayload))
- case webhook_model.HookEventIssueComment, webhook_model.HookEventPullRequestComment:
+ case webhook_module.HookEventIssueComment, webhook_module.HookEventPullRequestComment:
pl, ok := p.(*api.IssueCommentPayload)
if ok {
return s.IssueComment(pl)
}
return s.PullRequest(p.(*api.PullRequestPayload))
- case webhook_model.HookEventPush:
+ case webhook_module.HookEventPush:
return s.Push(p.(*api.PushPayload))
- case webhook_model.HookEventPullRequest, webhook_model.HookEventPullRequestAssign, webhook_model.HookEventPullRequestLabel,
- webhook_model.HookEventPullRequestMilestone, webhook_model.HookEventPullRequestSync:
+ case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel,
+ webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync:
return s.PullRequest(p.(*api.PullRequestPayload))
- case webhook_model.HookEventPullRequestReviewApproved, webhook_model.HookEventPullRequestReviewRejected, webhook_model.HookEventPullRequestReviewComment:
+ case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
return s.Review(p.(*api.PullRequestPayload), event)
- case webhook_model.HookEventRepository:
+ case webhook_module.HookEventRepository:
return s.Repository(p.(*api.RepositoryPayload))
- case webhook_model.HookEventRelease:
+ case webhook_module.HookEventRelease:
return s.Release(p.(*api.ReleasePayload))
+ case webhook_module.HookEventWiki:
+ return s.Wiki(p.(*api.WikiPayload))
}
return s, nil
}
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index 11e1d3c081c28..c2d4a7731e0a4 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -1,12 +1,12 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
+// SPDX-License-Identifier: MIT
package webhook
import (
"errors"
"fmt"
+ "regexp"
"strings"
webhook_model "code.gitea.io/gitea/models/webhook"
@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
)
// SlackMeta contains the slack metadata
@@ -156,6 +157,13 @@ func (s *SlackPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader,
}}), nil
}
+// Wiki implements PayloadConvertor Wiki method
+func (s *SlackPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+ text, _, _ := getWikiPayloadInfo(p, SlackLinkFormatter, true)
+
+ return s.createPayload(text, nil), nil
+}
+
// Release implements PayloadConvertor Release method
func (s *SlackPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, _ := getReleasePayloadInfo(p, SlackLinkFormatter, true)
@@ -171,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)
@@ -224,7 +232,7 @@ func (s *SlackPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, er
}
// Review implements PayloadConvertor Review method
-func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) {
+func (s *SlackPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
titleLink := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
@@ -271,7 +279,7 @@ func (s *SlackPayload) createPayload(text string, attachments []SlackAttachment)
}
// GetSlackPayload converts a slack webhook into a SlackPayload
-func GetSlackPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) {
+func GetSlackPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
s := new(SlackPayload)
slack := &SlackMeta{}
@@ -286,3 +294,13 @@ func GetSlackPayload(p api.Payloader, event webhook_model.HookEventType, meta st
return convertPayloader(s, p, event)
}
+
+var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
+
+// IsValidSlackChannel validates a channel name conforms to what slack expects:
+// https://api.slack.com/methods/conversations.rename#naming
+// Conversation names can only contain lowercase letters, numbers, hyphens, and underscores, and must be 80 characters or less.
+// Gitea accepts if it starts with a #.
+func IsValidSlackChannel(name string) bool {
+ return slackChannel.MatchString(name)
+}
diff --git a/services/webhook/slack_test.go b/services/webhook/slack_test.go
index 1fa777732868a..d9828f374f0dd 100644
--- a/services/webhook/slack_test.go
+++ b/services/webhook/slack_test.go
@@ -1,14 +1,13 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -125,7 +124,7 @@ func TestSlackPayload(t *testing.T) {
p.Action = api.HookIssueReviewed
d := new(SlackPayload)
- pl, err := d.Review(p, webhook_model.HookEventPullRequestReviewApproved)
+ pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &SlackPayload{}, pl)
@@ -145,6 +144,35 @@ func TestSlackPayload(t *testing.T) {
assert.Equal(t, "[] Repository created by ", pl.(*SlackPayload).Text)
})
+ t.Run("Wiki", func(t *testing.T) {
+ p := wikiTestPayload()
+
+ d := new(SlackPayload)
+ p.Action = api.HookWikiCreated
+ pl, err := d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &SlackPayload{}, pl)
+
+ assert.Equal(t, "[] New wiki page '' (Wiki change comment) by ", pl.(*SlackPayload).Text)
+
+ p.Action = api.HookWikiEdited
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &SlackPayload{}, pl)
+
+ assert.Equal(t, "[] Wiki page '' edited (Wiki change comment) by ", pl.(*SlackPayload).Text)
+
+ p.Action = api.HookWikiDeleted
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &SlackPayload{}, pl)
+
+ assert.Equal(t, "[] Wiki page '' deleted by ", pl.(*SlackPayload).Text)
+ })
+
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
@@ -154,7 +182,7 @@ func TestSlackPayload(t *testing.T) {
require.NotNil(t, pl)
require.IsType(t, &SlackPayload{}, pl)
- assert.Equal(t, "[] Release created: by ", pl.(*SlackPayload).Text)
+ assert.Equal(t, "[] Release created: by ", pl.(*SlackPayload).Text)
})
}
@@ -170,3 +198,22 @@ func TestSlackJSONPayload(t *testing.T) {
require.NoError(t, err)
assert.NotEmpty(t, json)
}
+
+func TestIsValidSlackChannel(t *testing.T) {
+ tt := []struct {
+ channelName string
+ expected bool
+ }{
+ {"gitea", true},
+ {"#gitea", true},
+ {" ", false},
+ {"#", false},
+ {" #", false},
+ {"gitea ", false},
+ {" gitea", false},
+ }
+
+ for _, v := range tt {
+ assert.Equal(t, v.expected, IsValidSlackChannel(v.channelName))
+ }
+}
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index 64211493ec62c..e5c731fc9291c 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
@@ -14,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
)
type (
@@ -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 == "" {
@@ -141,7 +141,7 @@ func (t *TelegramPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader,
}
// Review implements PayloadConvertor Review method
-func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) {
+func (t *TelegramPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
var text, attachmentText string
switch p.Action {
case api.HookIssueReviewed:
@@ -171,6 +171,13 @@ func (t *TelegramPayload) Repository(p *api.RepositoryPayload) (api.Payloader, e
return nil, nil
}
+// Wiki implements PayloadConvertor Wiki method
+func (t *TelegramPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+ text, _, _ := getWikiPayloadInfo(p, htmlLinkFormatter, true)
+
+ return createTelegramPayload(text), nil
+}
+
// Release implements PayloadConvertor Release method
func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, _ := getReleasePayloadInfo(p, htmlLinkFormatter, true)
@@ -179,7 +186,7 @@ func (t *TelegramPayload) Release(p *api.ReleasePayload) (api.Payloader, error)
}
// GetTelegramPayload converts a telegram webhook into a TelegramPayload
-func GetTelegramPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) {
+func GetTelegramPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(TelegramPayload), p, event)
}
diff --git a/services/webhook/telegram_test.go b/services/webhook/telegram_test.go
index 6a3682847cba9..b42b0ccda8847 100644
--- a/services/webhook/telegram_test.go
+++ b/services/webhook/telegram_test.go
@@ -1,14 +1,13 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -125,7 +124,7 @@ func TestTelegramPayload(t *testing.T) {
p.Action = api.HookIssueReviewed
d := new(TelegramPayload)
- pl, err := d.Review(p, webhook_model.HookEventPullRequestReviewApproved)
+ pl, err := d.Review(p, webhook_module.HookEventPullRequestReviewApproved)
require.NoError(t, err)
require.NotNil(t, pl)
require.IsType(t, &TelegramPayload{}, pl)
@@ -145,6 +144,35 @@ func TestTelegramPayload(t *testing.T) {
assert.Equal(t, `[test/repo ] Repository created`, pl.(*TelegramPayload).Message)
})
+ t.Run("Wiki", func(t *testing.T) {
+ p := wikiTestPayload()
+
+ d := new(TelegramPayload)
+ p.Action = api.HookWikiCreated
+ pl, err := d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &TelegramPayload{}, pl)
+
+ assert.Equal(t, `[test/repo ] New wiki page 'index ' (Wiki change comment) by user1 `, pl.(*TelegramPayload).Message)
+
+ p.Action = api.HookWikiEdited
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &TelegramPayload{}, pl)
+
+ assert.Equal(t, `[test/repo ] Wiki page 'index ' edited (Wiki change comment) by user1 `, pl.(*TelegramPayload).Message)
+
+ p.Action = api.HookWikiDeleted
+ pl, err = d.Wiki(p)
+ require.NoError(t, err)
+ require.NotNil(t, pl)
+ require.IsType(t, &TelegramPayload{}, pl)
+
+ assert.Equal(t, `[test/repo ] Wiki page 'index ' deleted by user1 `, pl.(*TelegramPayload).Message)
+ })
+
t.Run("Release", func(t *testing.T) {
p := pullReleaseTestPayload()
@@ -154,7 +182,7 @@ func TestTelegramPayload(t *testing.T) {
require.NotNil(t, pl)
require.IsType(t, &TelegramPayload{}, pl)
- assert.Equal(t, `[test/repo ] Release created: v1.0 by user1 `, pl.(*TelegramPayload).Message)
+ assert.Equal(t, `[test/repo ] Release created: v1.0 by user1 `, pl.(*TelegramPayload).Message)
})
}
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index a3efc7535fc3c..afd8e3c105cb1 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -1,86 +1,83 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
import (
+ "context"
"fmt"
- "strconv"
"strings"
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"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/sync"
"code.gitea.io/gitea/modules/util"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/gobwas/glob"
)
type webhook struct {
- name webhook_model.HookType
- payloadCreator func(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error)
+ name webhook_module.HookType
+ payloadCreator func(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error)
}
-var webhooks = map[webhook_model.HookType]*webhook{
- webhook_model.SLACK: {
- name: webhook_model.SLACK,
+var webhooks = map[webhook_module.HookType]*webhook{
+ webhook_module.SLACK: {
+ name: webhook_module.SLACK,
payloadCreator: GetSlackPayload,
},
- webhook_model.DISCORD: {
- name: webhook_model.DISCORD,
+ webhook_module.DISCORD: {
+ name: webhook_module.DISCORD,
payloadCreator: GetDiscordPayload,
},
- webhook_model.DINGTALK: {
- name: webhook_model.DINGTALK,
+ webhook_module.DINGTALK: {
+ name: webhook_module.DINGTALK,
payloadCreator: GetDingtalkPayload,
},
- webhook_model.TELEGRAM: {
- name: webhook_model.TELEGRAM,
+ webhook_module.TELEGRAM: {
+ name: webhook_module.TELEGRAM,
payloadCreator: GetTelegramPayload,
},
- webhook_model.MSTEAMS: {
- name: webhook_model.MSTEAMS,
+ webhook_module.MSTEAMS: {
+ name: webhook_module.MSTEAMS,
payloadCreator: GetMSTeamsPayload,
},
- webhook_model.FEISHU: {
- name: webhook_model.FEISHU,
+ webhook_module.FEISHU: {
+ name: webhook_module.FEISHU,
payloadCreator: GetFeishuPayload,
},
- webhook_model.MATRIX: {
- name: webhook_model.MATRIX,
+ webhook_module.MATRIX: {
+ name: webhook_module.MATRIX,
payloadCreator: GetMatrixPayload,
},
- webhook_model.WECHATWORK: {
- name: webhook_model.WECHATWORK,
+ webhook_module.WECHATWORK: {
+ name: webhook_module.WECHATWORK,
payloadCreator: GetWechatworkPayload,
},
- webhook_model.PACKAGIST: {
- name: webhook_model.PACKAGIST,
+ webhook_module.PACKAGIST: {
+ name: webhook_module.PACKAGIST,
payloadCreator: GetPackagistPayload,
},
}
-// RegisterWebhook registers a webhook
-func RegisterWebhook(name string, webhook *webhook) {
- webhooks[webhook_model.HookType(name)] = webhook
-}
-
// IsValidHookTaskType returns true if a webhook registered
func IsValidHookTaskType(name string) bool {
- if name == webhook_model.GITEA || name == webhook_model.GOGS {
+ if name == webhook_module.GITEA || name == webhook_module.GOGS {
return true
}
- _, ok := webhooks[webhook_model.HookType(name)]
+ _, ok := webhooks[name]
return ok
}
// hookQueue is a global queue of web hooks
-var hookQueue = sync.NewUniqueQueue(setting.Webhook.QueueLength)
+var hookQueue queue.UniqueQueue
// getPayloadBranch returns branch for hook event, if applicable.
func getPayloadBranch(p api.Payloader) string {
@@ -101,13 +98,42 @@ func getPayloadBranch(p api.Payloader) string {
return ""
}
-// 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
+// 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 {
+ ctx := graceful.GetManager().HammerContext()
+
+ for _, taskID := range data {
+ task, err := webhook_model.GetHookTaskByID(ctx, taskID.(int64))
+ if err != nil {
+ log.Error("GetHookTaskByID[%d] failed: %v", taskID.(int64), err)
+ continue
+ }
+
+ if task.IsDelivered {
+ // Already delivered in the meantime
+ log.Trace("Task[%d] has already been delivered", task.ID)
+ continue
+ }
+
+ if err := Deliver(ctx, task); err != nil {
+ log.Error("Unable to deliver webhook task[%d]: %v", task.ID, err)
+ }
}
- go hookQueue.Add(strconv.FormatInt(repo.ID, 10))
+ return nil
+}
+
+func enqueueHookTask(taskID int64) error {
+ err := hookQueue.Push(taskID)
+ if err != nil && err != queue.ErrAlreadyInQueue {
+ return err
+ }
return nil
}
@@ -126,7 +152,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_module.HookEventType, p api.Payloader) error {
// Skip sending if webhooks are disabled.
if setting.DisableWebhooks {
return nil
@@ -145,7 +172,7 @@ func prepareWebhook(w *webhook_model.Webhook, repo *repo_model.Repository, event
// Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.).
// Integration webhooks (e.g. drone) still receive the required data.
if pushEvent, ok := p.(*api.PushPayload); ok &&
- w.Type != webhook_model.GITEA && w.Type != webhook_model.GOGS &&
+ w.Type != webhook_module.GITEA && w.Type != webhook_module.GOGS &&
len(pushEvent.Commits) == 0 {
return nil
}
@@ -165,59 +192,59 @@ func prepareWebhook(w *webhook_model.Webhook, repo *repo_model.Repository, event
if ok {
payloader, err = webhook.payloadCreator(p, event, w.Meta)
if err != nil {
- return fmt.Errorf("create payload for %s[%s]: %v", w.Type, event, err)
+ return fmt.Errorf("create payload for %s[%s]: %w", w.Type, event, err)
}
} else {
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 {
- return fmt.Errorf("CreateHookTask: %v", err)
+ })
+ if err != nil {
+ return fmt.Errorf("CreateHookTask: %w", err)
}
- return nil
+
+ return enqueueHookTask(task.ID)
}
// 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(repo, event, p); err != nil {
- return err
- }
+func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_module.HookEventType, p api.Payloader) error {
+ owner := source.Owner
- go hookQueue.Add(strconv.FormatInt(repo.ID, 10))
- return nil
-}
+ var ws []*webhook_model.Webhook
-func prepareWebhooks(repo *repo_model.Repository, event webhook_model.HookEventType, p api.Payloader) error {
- ws, err := webhook_model.ListWebhooksByOpts(&webhook_model.ListWebhookOptions{
- RepoID: repo.ID,
- IsActive: util.OptionalBoolTrue,
- })
- if err != nil {
- return fmt.Errorf("GetActiveWebhooksByRepoID: %v", err)
+ 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("ListWebhooksByOpts: %w", err)
+ }
+ ws = append(ws, repoHooks...)
+
+ owner = source.Repository.MustOwner(ctx)
}
- // check if repo belongs to org and append additional webhooks
- if repo.MustOwner().IsOrganization() {
- // get hooks for org
- orgHooks, err := webhook_model.ListWebhooksByOpts(&webhook_model.ListWebhookOptions{
- OrgID: repo.OwnerID,
+ // 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("GetActiveWebhooksByOrgID: %v", err)
+ return fmt.Errorf("ListWebhooksByOpts: %w", err)
}
ws = append(ws, orgHooks...)
}
// Add any admin-defined system webhooks
- systemHooks, err := webhook_model.GetSystemWebhooks(util.OptionalBoolTrue)
+ systemHooks, err := webhook_model.GetSystemWebhooks(ctx, util.OptionalBoolTrue)
if err != nil {
- return fmt.Errorf("GetSystemWebhooks: %v", err)
+ return fmt.Errorf("GetSystemWebhooks: %w", err)
}
ws = append(ws, systemHooks...)
@@ -226,7 +253,7 @@ func prepareWebhooks(repo *repo_model.Repository, event webhook_model.HookEventT
}
for _, w := range ws {
- if err = prepareWebhook(w, repo, event, p); err != nil {
+ if err := PrepareWebhook(ctx, w, event, p); err != nil {
return err
}
}
@@ -234,13 +261,11 @@ func prepareWebhooks(repo *repo_model.Repository, event webhook_model.HookEventT
}
// 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
}
- go hookQueue.Add(strconv.FormatInt(t.RepoID, 10))
-
- return nil
+ return enqueueHookTask(task.ID)
}
diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go
index 85fc39770e21d..338b94360bbff 100644
--- a/services/webhook/webhook_test.go
+++ b/services/webhook/webhook_test.go
@@ -1,16 +1,17 @@
// 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.
+// SPDX-License-Identifier: MIT
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"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert"
)
@@ -30,14 +31,14 @@ func TestWebhook_GetSlackHook(t *testing.T) {
func TestPrepareWebhooks(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+ 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_module.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_module.HookEventPush, &api.PushPayload{Commits: []*api.PayloadCommit{{}}}))
for _, hookTask := range hookTasks {
unittest.AssertExistsAndLoadBean(t, hookTask)
}
@@ -46,15 +47,15 @@ func TestPrepareWebhooks(t *testing.T) {
func TestPrepareWebhooksBranchFilterMatch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
+ 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_module.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_module.HookEventPush, &api.PushPayload{Ref: "refs/heads/feature/7791", Commits: []*api.PayloadCommit{{}}}))
for _, hookTask := range hookTasks {
unittest.AssertExistsAndLoadBean(t, hookTask)
}
@@ -63,14 +64,14 @@ func TestPrepareWebhooksBranchFilterMatch(t *testing.T) {
func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
+ 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_module.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_module.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 de8b777066576..dc8810c4a6ae2 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -1,6 +1,5 @@
// 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.
+// SPDX-License-Identifier: MIT
package webhook
@@ -8,10 +7,10 @@ import (
"fmt"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
)
type (
@@ -93,7 +92,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")
@@ -136,10 +135,10 @@ func (f *WechatworkPayload) PullRequest(p *api.PullRequestPayload) (api.Payloade
}
// Review implements PayloadConvertor Review method
-func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_model.HookEventType) (api.Payloader, error) {
+func (f *WechatworkPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
var text, title string
switch p.Action {
- case api.HookIssueSynchronized:
+ case api.HookIssueReviewed:
action, err := parseHookPullRequestEventType(event)
if err != nil {
return nil, err
@@ -166,6 +165,13 @@ func (f *WechatworkPayload) Repository(p *api.RepositoryPayload) (api.Payloader,
return nil, nil
}
+// Wiki implements PayloadConvertor Wiki method
+func (f *WechatworkPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
+ text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
+
+ return newWechatworkMarkdownPayload(text), nil
+}
+
// Release implements PayloadConvertor Release method
func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
@@ -174,6 +180,6 @@ func (f *WechatworkPayload) Release(p *api.ReleasePayload) (api.Payloader, error
}
// GetWechatworkPayload GetWechatworkPayload converts a ding talk webhook into a WechatworkPayload
-func GetWechatworkPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) {
+func GetWechatworkPayload(p api.Payloader, event webhook_module.HookEventType, _ string) (api.Payloader, error) {
return convertPayloader(new(WechatworkPayload), p, event)
}
diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go
index 454f54983c107..e5cb2db02bdeb 100644
--- a/services/wiki/wiki.go
+++ b/services/wiki/wiki.go
@@ -1,7 +1,6 @@
// Copyright 2015 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.
+// SPDX-License-Identifier: MIT
package wiki
@@ -12,9 +11,8 @@ import (
"os"
"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"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
@@ -27,12 +25,18 @@ import (
var (
reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"}
- wikiWorkingPool = sync.NewExclusivePool()
+ // TODO: use clustered lock (unique queue? or *abuse* cache)
+ wikiWorkingPool = sync.NewExclusivePool()
+)
+
+const (
+ DefaultRemote = "origin"
+ DefaultBranch = "master"
)
func nameAllowed(name string) error {
- if util.IsStringInSlice(name, reservedWikiNames) {
- return models.ErrWikiReservedName{
+ if util.SliceContainsString(reservedWikiNames, name) {
+ return repo_model.ErrWikiReservedName{
Title: name,
}
}
@@ -58,7 +62,7 @@ func NameToFilename(name string) string {
// FilenameToName converts a wiki filename to its corresponding page name.
func FilenameToName(filename string) (string, error) {
if !strings.HasSuffix(filename, ".md") {
- return "", models.ErrWikiInvalidFileName{
+ return "", repo_model.ErrWikiInvalidFileName{
FileName: filename,
}
}
@@ -78,11 +82,11 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error {
}
if err := git.InitRepository(ctx, repo.WikiPath(), true); err != nil {
- return fmt.Errorf("InitRepository: %v", err)
+ return fmt.Errorf("InitRepository: %w", 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 {
- return fmt.Errorf("unable to set default wiki branch to master: %v", err)
+ return fmt.Errorf("createDelegateHooks: %w", err)
+ } 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: %w", err)
}
return nil
}
@@ -94,7 +98,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
@@ -118,7 +122,7 @@ func prepareWikiFileName(gitRepo *git.Repository, wikiName string) (bool, string
return foundEscaped, escaped, nil
}
-// updateWikiPage adds a new page to the repository wiki.
+// updateWikiPage adds a new page or edits an existing page in repository wiki.
func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName, content, message string, isNew bool) (err error) {
if err = nameAllowed(newWikiName); err != nil {
return err
@@ -127,17 +131,17 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID))
if err = InitWiki(ctx, repo); err != nil {
- return fmt.Errorf("InitWiki: %v", err)
+ return fmt.Errorf("InitWiki: %w", err)
}
- hasMasterBranch := git.IsBranchExist(ctx, repo.WikiPath(), "master")
+ hasMasterBranch := git.IsBranchExist(ctx, repo.WikiPath(), DefaultBranch)
- basePath, err := models.CreateTemporaryPath("update-wiki")
+ basePath, err := repo_module.CreateTemporaryPath("update-wiki")
if err != nil {
return err
}
defer func() {
- if err := models.RemoveTemporaryPath(basePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(basePath); err != nil {
log.Error("Merge: RemoveTemporaryPath: %s", err)
}
}()
@@ -148,25 +152,25 @@ 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 {
log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
- return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
+ return fmt.Errorf("Failed to clone repository: %s (%w)", repo.FullName(), err)
}
gitRepo, err := git.OpenRepository(ctx, basePath)
if err != nil {
log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
- return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
+ return fmt.Errorf("Failed to open new temporary repository in: %s %w", basePath, err)
}
defer gitRepo.Close()
if hasMasterBranch {
if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
- return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
+ return fmt.Errorf("Unable to read HEAD tree to index in: %s %w", basePath, err)
}
}
@@ -177,7 +181,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
if isNew {
if isWikiExist {
- return models.ErrWikiAlreadyExist{
+ return repo_model.ErrWikiAlreadyExist{
Title: newWikiPath,
}
}
@@ -246,9 +250,9 @@ 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"),
- Env: models.FullPushingEnvironment(
+ Remote: DefaultRemote,
+ Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch),
+ Env: repo_module.FullPushingEnvironment(
doer,
doer,
repo,
@@ -260,7 +264,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
return err
}
- return fmt.Errorf("Push: %v", err)
+ return fmt.Errorf("Push: %w", err)
}
return nil
@@ -283,15 +287,15 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID))
if err = InitWiki(ctx, repo); err != nil {
- return fmt.Errorf("InitWiki: %v", err)
+ return fmt.Errorf("InitWiki: %w", err)
}
- basePath, err := models.CreateTemporaryPath("update-wiki")
+ basePath, err := repo_module.CreateTemporaryPath("update-wiki")
if err != nil {
return err
}
defer func() {
- if err := models.RemoveTemporaryPath(basePath); err != nil {
+ if err := repo_module.RemoveTemporaryPath(basePath); err != nil {
log.Error("Merge: RemoveTemporaryPath: %s", err)
}
}()
@@ -299,22 +303,22 @@ 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)
+ return fmt.Errorf("Failed to clone repository: %s (%w)", repo.FullName(), err)
}
gitRepo, err := git.OpenRepository(ctx, basePath)
if err != nil {
log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
- return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
+ return fmt.Errorf("Failed to open new temporary repository in: %s %w", basePath, err)
}
defer gitRepo.Close()
if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
- return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
+ return fmt.Errorf("Unable to read HEAD tree to index in: %s %w", basePath, err)
}
found, wikiPath, err := prepareWikiFileName(gitRepo, wikiName)
@@ -360,14 +364,14 @@ 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"),
- Env: models.PushingEnvironment(doer, repo),
+ 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) {
return err
}
- return fmt.Errorf("Push: %v", err)
+ return fmt.Errorf("Push: %w", err)
}
return nil
@@ -379,6 +383,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 0c73074dbe27d..268d8848c5042 100644
--- a/services/wiki/wiki_test.go
+++ b/services/wiki/wiki_test.go
@@ -1,20 +1,16 @@
// 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.
+// SPDX-License-Identifier: MIT
package wiki
import (
- "os"
"path/filepath"
"testing"
- "code.gitea.io/gitea/models"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
@@ -90,11 +86,11 @@ func TestWikiFilenameToName(t *testing.T) {
} {
_, err := FilenameToName(badFilename)
assert.Error(t, err)
- assert.True(t, models.IsErrWikiInvalidFileName(err))
+ assert.True(t, repo_model.IsErrWikiInvalidFileName(err))
}
_, err := FilenameToName("badescaping%%.md")
assert.Error(t, err)
- assert.False(t, models.IsErrWikiInvalidFileName(err))
+ assert.False(t, repo_model.IsErrWikiInvalidFileName(err))
}
func TestWikiNameToFilenameToName(t *testing.T) {
@@ -116,11 +112,11 @@ func TestWikiNameToFilenameToName(t *testing.T) {
func TestRepository_InitWiki(t *testing.T) {
unittest.PrepareTestEnv(t)
// repo1 already has a wiki
- repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.NoError(t, InitWiki(git.DefaultContext, repo1))
// repo2 does not already have a wiki
- repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
+ repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
assert.NoError(t, InitWiki(git.DefaultContext, repo2))
assert.True(t, repo2.HasWiki())
}
@@ -129,8 +125,8 @@ func TestRepository_AddWikiPage(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const wikiContent = "This is the wiki content"
const commitMsg = "Commit message"
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
for _, wikiName := range []string{
"Another page",
"Here's a and a/slash",
@@ -143,7 +139,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)
@@ -157,7 +153,7 @@ func TestRepository_AddWikiPage(t *testing.T) {
// test for already-existing wiki name
err := AddWikiPage(git.DefaultContext, doer, repo, "Home", wikiContent, commitMsg)
assert.Error(t, err)
- assert.True(t, models.IsErrWikiAlreadyExist(err))
+ assert.True(t, repo_model.IsErrWikiAlreadyExist(err))
})
t.Run("check wiki reserved name", func(t *testing.T) {
@@ -165,7 +161,7 @@ func TestRepository_AddWikiPage(t *testing.T) {
// test for reserved wiki name
err := AddWikiPage(git.DefaultContext, doer, repo, "_edit", wikiContent, commitMsg)
assert.Error(t, err)
- assert.True(t, models.IsErrWikiReservedName(err))
+ assert.True(t, repo_model.IsErrWikiReservedName(err))
})
}
@@ -174,8 +170,8 @@ func TestRepository_EditWikiPage(t *testing.T) {
const newWikiContent = "This is the new content"
const commitMsg = "Commit message"
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
for _, newWikiName := range []string{
"Home", // same name as before
"New home",
@@ -187,7 +183,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)
@@ -204,15 +200,15 @@ func TestRepository_EditWikiPage(t *testing.T) {
func TestRepository_DeleteWikiPage(t *testing.T) {
unittest.PrepareTestEnv(t)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
assert.NoError(t, DeleteWikiPage(git.DefaultContext, doer, repo, "Home"))
// Now need to show that the page has been added:
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)
@@ -221,7 +217,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) {
func TestPrepareWikiFileName(t *testing.T) {
unittest.PrepareTestEnv(t)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath())
defer gitRepo.Close()
assert.NoError(t, err)
@@ -274,15 +270,9 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) {
unittest.PrepareTestEnv(t)
// Now create a temporaryDirectory
- tmpDir, err := os.MkdirTemp("", "empty-wiki")
- assert.NoError(t, err)
- defer func() {
- if _, err := os.Stat(tmpDir); !os.IsNotExist(err) {
- _ = util.RemoveAll(tmpDir)
- }
- }()
+ tmpDir := t.TempDir()
- err = git.InitRepository(git.DefaultContext, tmpDir, true)
+ err := git.InitRepository(git.DefaultContext, tmpDir, true)
assert.NoError(t, err)
gitRepo, err := git.OpenRepository(git.DefaultContext, tmpDir)
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 2defba7be329c..25494bde23985 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,5 +1,5 @@
name: gitea
-summary: Gitea - A painless self-hosted Git service
+summary: Gitea - A painless self-hosted Git service
description: |
The goal of this project is to make the easiest, fastest, and most painless
way of setting up a self-hosted Git service. With Go, this can be done with
@@ -39,13 +39,12 @@ apps:
command: usr/bin/sqlite3
parts:
-
gitea:
plugin: make
source: .
stage-packages: [ git, sqlite3, openssh-client ]
- build-packages: [ git, libpam0g-dev, libsqlite3-dev]
- build-snaps: [ go, node/14/stable ]
+ build-packages: [ git, libpam0g-dev, libsqlite3-dev, build-essential]
+ build-snaps: [ go, node/18/stable ]
build-environment:
- LDFLAGS: ""
override-pull: |
diff --git a/templates/admin/applications/list.tmpl b/templates/admin/applications/list.tmpl
new file mode 100644
index 0000000000000..6d627129df0e9
--- /dev/null
+++ b/templates/admin/applications/list.tmpl
@@ -0,0 +1,14 @@
+{{template "base/head" .}}
+
+ {{template "admin/navbar" .}}
+
+
+ {{template "base/alert" .}}
+
+ {{template "user/settings/applications_oauth2_list" .}}
+
+
+
+{{template "base/footer" .}}
diff --git a/templates/admin/applications/oauth2_edit.tmpl b/templates/admin/applications/oauth2_edit.tmpl
new file mode 100644
index 0000000000000..84d821eccacea
--- /dev/null
+++ b/templates/admin/applications/oauth2_edit.tmpl
@@ -0,0 +1,7 @@
+{{template "base/head" .}}
+
+ {{template "admin/navbar" .}}
+
+ {{template "user/settings/applications_oauth2_edit_form" .}}
+
+{{template "base/footer" .}}
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 6e491d6cf41ad..bf9d53152c2e7 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -4,7 +4,7 @@
{{template "base/alert" .}}
{{end}}
{{if .Source.IsLDAP}}
{{end}}
-
{{.i18n.Tr "admin.auths.update"}}
-
{{.i18n.Tr "admin.auths.delete"}}
+
{{.locale.Tr "admin.auths.update"}}
+
{{.locale.Tr "admin.auths.delete"}}
@@ -435,10 +435,10 @@
-
{{.i18n.Tr "admin.auths.delete_auth_desc"}}
+
{{.locale.Tr "admin.auths.delete_auth_desc"}}
{{template "base/delete_modal_actions" .}}
diff --git a/templates/admin/auth/list.tmpl b/templates/admin/auth/list.tmpl
index 71e5bfbda8d7a..c43287ee1ae0d 100644
--- a/templates/admin/auth/list.tmpl
+++ b/templates/admin/auth/list.tmpl
@@ -4,22 +4,22 @@
{{template "base/alert" .}}
-
+
ID
- {{.i18n.Tr "admin.auths.name"}}
- {{.i18n.Tr "admin.auths.type"}}
- {{.i18n.Tr "admin.auths.enabled"}}
- {{.i18n.Tr "admin.auths.updated"}}
- {{.i18n.Tr "admin.users.created"}}
- {{.i18n.Tr "admin.users.edit"}}
+ {{.locale.Tr "admin.auths.name"}}
+ {{.locale.Tr "admin.auths.type"}}
+ {{.locale.Tr "admin.auths.enabled"}}
+ {{.locale.Tr "admin.auths.updated"}}
+ {{.locale.Tr "admin.users.created"}}
+ {{.locale.Tr "admin.users.edit"}}
@@ -29,8 +29,8 @@
{{.Name}}
{{.TypeName}}
{{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.UpdatedUnix.FormatShort}}
- {{.CreatedUnix.FormatShort}}
+ {{.UpdatedUnix.FormatShort}}
+ {{.CreatedUnix.FormatShort}}
{{svg "octicon-pencil"}}
{{end}}
diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl
index 9882cde03bcd4..213c621b42f21 100644
--- a/templates/admin/auth/new.tmpl
+++ b/templates/admin/auth/new.tmpl
@@ -4,7 +4,7 @@
{{template "base/alert" .}}
GMail Settings:
Host: smtp.gmail.com, Port: 587, Enable TLS Encryption: true
-
{{.i18n.Tr "admin.auths.tips.oauth2.general"}}:
-
{{.i18n.Tr "admin.auths.tips.oauth2.general.tip"}}
+
{{.locale.Tr "admin.auths.tips.oauth2.general"}}:
+
{{.locale.Tr "admin.auths.tips.oauth2.general.tip"}}
-
+
Bitbucket
- {{.i18n.Tr "admin.auths.tip.bitbucket"}}
+ {{.locale.Tr "admin.auths.tip.bitbucket"}}
Dropbox
- {{.i18n.Tr "admin.auths.tip.dropbox"}}
+ {{.locale.Tr "admin.auths.tip.dropbox"}}
Facebook
- {{.i18n.Tr "admin.auths.tip.facebook"}}
+ {{.locale.Tr "admin.auths.tip.facebook"}}
GitHub
- {{.i18n.Tr "admin.auths.tip.github"}}
+ {{.locale.Tr "admin.auths.tip.github"}}
GitLab
- {{.i18n.Tr "admin.auths.tip.gitlab"}}
+ {{.locale.Tr "admin.auths.tip.gitlab"}}
Google
- {{.i18n.Tr "admin.auths.tip.google_plus"}}
+ {{.locale.Tr "admin.auths.tip.google_plus"}}
OpenID Connect
- {{.i18n.Tr "admin.auths.tip.openid_connect"}}
+ {{.locale.Tr "admin.auths.tip.openid_connect"}}
Twitter
- {{.i18n.Tr "admin.auths.tip.twitter"}}
+ {{.locale.Tr "admin.auths.tip.twitter"}}
Discord
- {{.i18n.Tr "admin.auths.tip.discord"}}
+ {{.locale.Tr "admin.auths.tip.discord"}}
Gitea
- {{.i18n.Tr "admin.auths.tip.gitea"}}
+ {{.locale.Tr "admin.auths.tip.gitea"}}
Nextcloud
- {{.i18n.Tr "admin.auths.tip.nextcloud"}}
+ {{.locale.Tr "admin.auths.tip.nextcloud"}}
Yandex
- {{.i18n.Tr "admin.auths.tip.yandex"}}
+ {{.locale.Tr "admin.auths.tip.yandex"}}
Mastodon
- {{.i18n.Tr "admin.auths.tip.mastodon"}}
+ {{.locale.Tr "admin.auths.tip.mastodon"}}
diff --git a/templates/admin/auth/source/ldap.tmpl b/templates/admin/auth/source/ldap.tmpl
index afdfbadd6518e..b44eb799b9ec7 100644
--- a/templates/admin/auth/source/ldap.tmpl
+++ b/templates/admin/auth/source/ldap.tmpl
@@ -1,6 +1,6 @@
-
{{.i18n.Tr "admin.auths.security_protocol"}}
+
{{.locale.Tr "admin.auths.security_protocol"}}
{{.CurrentSecurityProtocol}}
@@ -13,103 +13,103 @@
- {{.i18n.Tr "admin.auths.host"}}
+ {{.locale.Tr "admin.auths.host"}}
- {{.i18n.Tr "admin.auths.port"}}
+ {{.locale.Tr "admin.auths.port"}}
- {{.i18n.Tr "admin.auths.bind_dn"}}
+ {{.locale.Tr "admin.auths.bind_dn"}}
- {{.i18n.Tr "admin.auths.bind_password"}}
+ {{.locale.Tr "admin.auths.bind_password"}}
- {{.i18n.Tr "admin.auths.user_base"}}
+ {{.locale.Tr "admin.auths.user_base"}}
- {{.i18n.Tr "admin.auths.user_dn"}}
+ {{.locale.Tr "admin.auths.user_dn"}}
- {{.i18n.Tr "admin.auths.filter"}}
+ {{.locale.Tr "admin.auths.filter"}}
- {{.i18n.Tr "admin.auths.admin_filter"}}
+ {{.locale.Tr "admin.auths.admin_filter"}}
-
{{.i18n.Tr "admin.auths.restricted_filter"}}
+
{{.locale.Tr "admin.auths.restricted_filter"}}
-
{{.i18n.Tr "admin.auths.restricted_filter_helper"}}
+
{{.locale.Tr "admin.auths.restricted_filter_helper"}}
- {{.i18n.Tr "admin.auths.attribute_username"}}
-
+ {{.locale.Tr "admin.auths.attribute_username"}}
+
- {{.i18n.Tr "admin.auths.attribute_name"}}
+ {{.locale.Tr "admin.auths.attribute_name"}}
- {{.i18n.Tr "admin.auths.attribute_surname"}}
+ {{.locale.Tr "admin.auths.attribute_surname"}}
- {{.i18n.Tr "admin.auths.attribute_mail"}}
+ {{.locale.Tr "admin.auths.attribute_mail"}}
- {{.i18n.Tr "admin.auths.attribute_ssh_public_key"}}
+ {{.locale.Tr "admin.auths.attribute_ssh_public_key"}}
- {{.i18n.Tr "admin.auths.attribute_avatar"}}
+ {{.locale.Tr "admin.auths.attribute_avatar"}}
@@ -117,24 +117,24 @@
- {{.i18n.Tr "admin.auths.search_page_size"}}
+ {{.locale.Tr "admin.auths.search_page_size"}}
-
{{.i18n.Tr "admin.auths.skip_local_two_fa"}}
+
{{.locale.Tr "admin.auths.skip_local_two_fa"}}
-
{{.i18n.Tr "admin.auths.skip_local_two_fa_helper"}}
+
{{.locale.Tr "admin.auths.skip_local_two_fa_helper"}}
diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl
index 3991dc2170792..166373a324899 100644
--- a/templates/admin/auth/source/oauth.tmpl
+++ b/templates/admin/auth/source/oauth.tmpl
@@ -1,6 +1,6 @@
diff --git a/templates/admin/auth/source/smtp.tmpl b/templates/admin/auth/source/smtp.tmpl
index 8572d6dc56ebe..2d577412c1b62 100644
--- a/templates/admin/auth/source/smtp.tmpl
+++ b/templates/admin/auth/source/smtp.tmpl
@@ -1,6 +1,6 @@
-
{{.i18n.Tr "admin.auths.smtp_auth"}}
+
{{.locale.Tr "admin.auths.smtp_auth"}}
{{.smtp_auth}}
@@ -13,47 +13,47 @@
- {{.i18n.Tr "admin.auths.smtphost"}}
+ {{.locale.Tr "admin.auths.smtphost"}}
- {{.i18n.Tr "admin.auths.smtpport"}}
+ {{.locale.Tr "admin.auths.smtpport"}}
-
{{.i18n.Tr "admin.auths.force_smtps"}}
+
{{.locale.Tr "admin.auths.force_smtps"}}
-
{{.i18n.Tr "admin.auths.force_smtps_helper"}}
+
{{.locale.Tr "admin.auths.force_smtps_helper"}}
-
{{.i18n.Tr "admin.auths.helo_hostname"}}
+
{{.locale.Tr "admin.auths.helo_hostname"}}
-
{{.i18n.Tr "admin.auths.helo_hostname_helper"}}
+
{{.locale.Tr "admin.auths.helo_hostname_helper"}}
-
{{.i18n.Tr "admin.auths.allowed_domains"}}
+
{{.locale.Tr "admin.auths.allowed_domains"}}
-
{{.i18n.Tr "admin.auths.allowed_domains_helper"}}
+
{{.locale.Tr "admin.auths.allowed_domains_helper"}}
-
{{.i18n.Tr "admin.auths.skip_local_two_fa"}}
+
{{.locale.Tr "admin.auths.skip_local_two_fa"}}
-
{{.i18n.Tr "admin.auths.skip_local_two_fa_helper"}}
+
{{.locale.Tr "admin.auths.skip_local_two_fa_helper"}}
diff --git a/templates/admin/auth/source/sspi.tmpl b/templates/admin/auth/source/sspi.tmpl
index 91697ef9c5109..dee40d9fd5397 100644
--- a/templates/admin/auth/source/sspi.tmpl
+++ b/templates/admin/auth/source/sspi.tmpl
@@ -1,32 +1,32 @@
-
{{.i18n.Tr "admin.auths.sspi_auto_create_users"}}
+
{{.locale.Tr "admin.auths.sspi_auto_create_users"}}
-
{{.i18n.Tr "admin.auths.sspi_auto_create_users_helper"}}
+
{{.locale.Tr "admin.auths.sspi_auto_create_users_helper"}}
-
{{.i18n.Tr "admin.auths.sspi_auto_activate_users"}}
+
{{.locale.Tr "admin.auths.sspi_auto_activate_users"}}
-
{{.i18n.Tr "admin.auths.sspi_auto_activate_users_helper"}}
+
{{.locale.Tr "admin.auths.sspi_auto_activate_users_helper"}}
-
{{.i18n.Tr "admin.auths.sspi_strip_domain_names"}}
+
{{.locale.Tr "admin.auths.sspi_strip_domain_names"}}
-
{{.i18n.Tr "admin.auths.sspi_strip_domain_names_helper"}}
+
{{.locale.Tr "admin.auths.sspi_strip_domain_names_helper"}}
-
{{.i18n.Tr "admin.auths.sspi_separator_replacement"}}
+
{{.locale.Tr "admin.auths.sspi_separator_replacement"}}
-
{{.i18n.Tr "admin.auths.sspi_separator_replacement_helper"}}
+
{{.locale.Tr "admin.auths.sspi_separator_replacement_helper"}}
-
{{.i18n.Tr "admin.auths.sspi_default_language"}}
+
{{.locale.Tr "admin.auths.sspi_default_language"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
@@ -38,6 +38,6 @@
{{end}}
-
{{.i18n.Tr "admin.auths.sspi_default_language_helper"}}
+
{{.locale.Tr "admin.auths.sspi_default_language_helper"}}
diff --git a/templates/admin/base/search.tmpl b/templates/admin/base/search.tmpl
index 98fd3f4a0780f..ae1a4d2ac5628 100644
--- a/templates/admin/base/search.tmpl
+++ b/templates/admin/base/search.tmpl
@@ -2,22 +2,22 @@
- {{.i18n.Tr "repo.issues.filter_sort"}}
+ {{.locale.Tr "repo.issues.filter_sort"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 2a27baf53596b..982cfb2800815 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -4,53 +4,53 @@
{{template "base/alert" .}}
- {{.i18n.Tr "admin.config.app_name"}}
+ {{.locale.Tr "admin.config.app_name"}}
{{AppName}}
- {{.i18n.Tr "admin.config.app_ver"}}
+ {{.locale.Tr "admin.config.app_ver"}}
{{AppVer}}{{AppBuiltWith}}
- {{.i18n.Tr "admin.config.custom_conf"}}
+ {{.locale.Tr "admin.config.custom_conf"}}
{{.CustomConf}}
- {{.i18n.Tr "admin.config.app_url"}}
+ {{.locale.Tr "admin.config.app_url"}}
{{.AppUrl}}
- {{.i18n.Tr "admin.config.domain"}}
+ {{.locale.Tr "admin.config.domain"}}
{{.Domain}}
- {{.i18n.Tr "admin.config.offline_mode"}}
+ {{.locale.Tr "admin.config.offline_mode"}}
{{if .OfflineMode}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.disable_router_log"}}
+ {{.locale.Tr "admin.config.disable_router_log"}}
{{if .DisableRouterLog}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.run_user"}}
+ {{.locale.Tr "admin.config.run_user"}}
{{.RunUser}}
- {{.i18n.Tr "admin.config.run_mode"}}
+ {{.locale.Tr "admin.config.run_mode"}}
{{.RunMode}}
- {{.i18n.Tr "admin.config.git_version"}}
+ {{.locale.Tr "admin.config.git_version"}}
{{.GitVersion}}
- {{.i18n.Tr "admin.config.repo_root_path"}}
+ {{.locale.Tr "admin.config.repo_root_path"}}
{{.RepoRootPath}}
- {{.i18n.Tr "admin.config.static_file_root_path"}}
+ {{.locale.Tr "admin.config.static_file_root_path"}}
{{.StaticRootPath}}
- {{.i18n.Tr "admin.config.custom_file_root_path"}}
+ {{.locale.Tr "admin.config.custom_file_root_path"}}
{{.CustomRootPath}}
- {{.i18n.Tr "admin.config.log_file_root_path"}}
+ {{.locale.Tr "admin.config.log_file_root_path"}}
{{.LogRootPath}}
- {{.i18n.Tr "admin.config.script_type"}}
+ {{.locale.Tr "admin.config.script_type"}}
{{.ScriptType}}
- {{.i18n.Tr "admin.config.reverse_auth_user"}}
+ {{.locale.Tr "admin.config.reverse_auth_user"}}
{{.ReverseProxyAuthUser}}
- {{if .EnvVars }}
+ {{if .EnvVars}}
{{range .EnvVars}}
{{.Name}}
@@ -62,33 +62,33 @@
- {{.i18n.Tr "admin.config.ssh_enabled"}}
+ {{.locale.Tr "admin.config.ssh_enabled"}}
{{if not .SSH.Disabled}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{if not .SSH.Disabled}}
- {{.i18n.Tr "admin.config.ssh_start_builtin_server"}}
+ {{.locale.Tr "admin.config.ssh_start_builtin_server"}}
{{if .SSH.StartBuiltinServer}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.ssh_domain"}}
+ {{.locale.Tr "admin.config.ssh_domain"}}
{{.SSH.Domain}}
- {{.i18n.Tr "admin.config.ssh_port"}}
+ {{.locale.Tr "admin.config.ssh_port"}}
{{.SSH.Port}}
- {{.i18n.Tr "admin.config.ssh_listen_port"}}
+ {{.locale.Tr "admin.config.ssh_listen_port"}}
{{.SSH.ListenPort}}
{{if not .SSH.StartBuiltinServer}}
- {{.i18n.Tr "admin.config.ssh_root_path"}}
+ {{.locale.Tr "admin.config.ssh_root_path"}}
{{.SSH.RootPath}}
- {{.i18n.Tr "admin.config.ssh_key_test_path"}}
+ {{.locale.Tr "admin.config.ssh_key_test_path"}}
{{.SSH.KeyTestPath}}
- {{.i18n.Tr "admin.config.ssh_keygen_path"}}
+ {{.locale.Tr "admin.config.ssh_keygen_path"}}
{{.SSH.KeygenPath}}
- {{.i18n.Tr "admin.config.ssh_minimum_key_size_check"}}
+ {{.locale.Tr "admin.config.ssh_minimum_key_size_check"}}
{{if .SSH.MinimumKeySizeCheck}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{if .SSH.MinimumKeySizeCheck}}
- {{.i18n.Tr "admin.config.ssh_minimum_key_sizes"}}
+ {{.locale.Tr "admin.config.ssh_minimum_key_sizes"}}
{{.SSH.MinimumKeySizes}}
{{end}}
{{end}}
@@ -97,304 +97,319 @@
- {{.i18n.Tr "admin.config.lfs_enabled"}}
+ {{.locale.Tr "admin.config.lfs_enabled"}}
{{if .LFS.StartServer}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{if .LFS.StartServer}}
- {{.i18n.Tr "admin.config.lfs_content_path"}}
+ {{.locale.Tr "admin.config.lfs_content_path"}}
{{.LFS.Path}}
- {{.i18n.Tr "admin.config.lfs_http_auth_expiry"}}
+ {{.locale.Tr "admin.config.lfs_http_auth_expiry"}}
{{.LFS.HTTPAuthExpiry}}
{{end}}
- {{.i18n.Tr "admin.config.db_type"}}
+ {{.locale.Tr "admin.config.db_type"}}
{{.DbCfg.Type}}
{{if not (eq .DbCfg.Type "sqlite3")}}
- {{.i18n.Tr "admin.config.db_host"}}
+ {{.locale.Tr "admin.config.db_host"}}
{{if .DbCfg.Host}}{{.DbCfg.Host}}{{else}}-{{end}}
- {{.i18n.Tr "admin.config.db_name"}}
+ {{.locale.Tr "admin.config.db_name"}}
{{if .DbCfg.Name}}{{.DbCfg.Name}}{{else}}-{{end}}
- {{.i18n.Tr "admin.config.db_user"}}
+ {{.locale.Tr "admin.config.db_user"}}
{{if .DbCfg.User}}{{.DbCfg.User}}{{else}}-{{end}}
{{end}}
{{if eq .DbCfg.Type "postgres"}}
- {{.i18n.Tr "admin.config.db_schema"}}
+ {{.locale.Tr "admin.config.db_schema"}}
{{if .DbCfg.Schema}}{{.DbCfg.Schema}}{{else}}-{{end}}
- {{.i18n.Tr "admin.config.db_ssl_mode"}}
+ {{.locale.Tr "admin.config.db_ssl_mode"}}
{{if .DbCfg.SSLMode}}{{.DbCfg.SSLMode}}{{else}}-{{end}}
{{end}}
{{if eq .DbCfg.Type "sqlite3"}}
- {{.i18n.Tr "admin.config.db_path"}}
+ {{.locale.Tr "admin.config.db_path"}}
{{if .DbCfg.Path}}{{.DbCfg.Path}}{{else}}-{{end}}
{{end}}
- {{.i18n.Tr "admin.config.register_email_confirm"}}
+ {{.locale.Tr "admin.config.register_email_confirm"}}
{{if .Service.RegisterEmailConfirm}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.disable_register"}}
+ {{.locale.Tr "admin.config.disable_register"}}
{{if .Service.DisableRegistration}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.allow_only_internal_registration"}}
+ {{.locale.Tr "admin.config.allow_only_internal_registration"}}
{{if .Service.AllowOnlyInternalRegistration}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.allow_only_external_registration"}}
+ {{.locale.Tr "admin.config.allow_only_external_registration"}}
{{if .Service.AllowOnlyExternalRegistration}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.show_registration_button"}}
+ {{.locale.Tr "admin.config.show_registration_button"}}
{{if .Service.ShowRegistrationButton}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.enable_openid_signup"}}
+ {{.locale.Tr "admin.config.enable_openid_signup"}}
{{if .Service.EnableOpenIDSignUp}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.enable_openid_signin"}}
+ {{.locale.Tr "admin.config.enable_openid_signin"}}
{{if .Service.EnableOpenIDSignIn}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.require_sign_in_view"}}
+ {{.locale.Tr "admin.config.require_sign_in_view"}}
{{if .Service.RequireSignInView}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.mail_notify"}}
+ {{.locale.Tr "admin.config.mail_notify"}}
{{if .Service.EnableNotifyMail}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.disable_key_size_check"}}
+ {{.locale.Tr "admin.config.disable_key_size_check"}}
{{if .SSH.MinimumKeySizeCheck}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.enable_captcha"}}
+ {{.locale.Tr "admin.config.enable_captcha"}}
{{if .Service.EnableCaptcha}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.default_keep_email_private"}}
+ {{.locale.Tr "admin.config.default_keep_email_private"}}
{{if .Service.DefaultKeepEmailPrivate}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.default_allow_create_organization"}}
+ {{.locale.Tr "admin.config.default_allow_create_organization"}}
{{if .Service.DefaultAllowCreateOrganization}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.enable_timetracking"}}
+ {{.locale.Tr "admin.config.enable_timetracking"}}
{{if .Service.EnableTimetracking}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{if .Service.EnableTimetracking}}
- {{.i18n.Tr "admin.config.default_enable_timetracking"}}
+ {{.locale.Tr "admin.config.default_enable_timetracking"}}
{{if .Service.DefaultEnableTimetracking}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.default_allow_only_contributors_to_track_time"}}
+ {{.locale.Tr "admin.config.default_allow_only_contributors_to_track_time"}}
{{if .Service.DefaultAllowOnlyContributorsToTrackTime}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{end}}
- {{.i18n.Tr "admin.config.default_visibility_organization"}}
+ {{.locale.Tr "admin.config.default_visibility_organization"}}
{{.Service.DefaultOrgVisibility}}
- {{.i18n.Tr "admin.config.no_reply_address"}}
+ {{.locale.Tr "admin.config.no_reply_address"}}
{{if .Service.NoReplyAddress}}{{.Service.NoReplyAddress}}{{else}}-{{end}}
- {{.i18n.Tr "admin.config.default_enable_dependencies"}}
+ {{.locale.Tr "admin.config.default_enable_dependencies"}}
{{if .Service.DefaultEnableDependencies}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.active_code_lives"}}
- {{.Service.ActiveCodeLives}} {{.i18n.Tr "tool.raw_minutes"}}
- {{.i18n.Tr "admin.config.reset_password_code_lives"}}
- {{.Service.ResetPwdCodeLives}} {{.i18n.Tr "tool.raw_minutes"}}
+ {{.locale.Tr "admin.config.active_code_lives"}}
+ {{.Service.ActiveCodeLives}} {{.locale.Tr "tool.raw_minutes"}}
+ {{.locale.Tr "admin.config.reset_password_code_lives"}}
+ {{.Service.ResetPwdCodeLives}} {{.locale.Tr "tool.raw_minutes"}}
- {{.i18n.Tr "admin.config.queue_length"}}
+ {{.locale.Tr "admin.config.queue_length"}}
{{.Webhook.QueueLength}}
- {{.i18n.Tr "admin.config.deliver_timeout"}}
- {{.Webhook.DeliverTimeout}} {{.i18n.Tr "tool.raw_seconds"}}
- {{.i18n.Tr "admin.config.skip_tls_verify"}}
+ {{.locale.Tr "admin.config.deliver_timeout"}}
+ {{.Webhook.DeliverTimeout}} {{.locale.Tr "tool.raw_seconds"}}
+ {{.locale.Tr "admin.config.skip_tls_verify"}}
{{if .Webhook.SkipTLSVerify}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.mailer_enabled"}}
+ {{.locale.Tr "admin.config.mailer_enabled"}}
{{if .MailerEnabled}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{if .MailerEnabled}}
- {{.i18n.Tr "admin.config.mailer_name"}}
+ {{.locale.Tr "admin.config.mailer_name"}}
{{.Mailer.Name}}
- {{if eq .Mailer.MailerType "smtp"}}
- {{.i18n.Tr "admin.config.mailer_disable_helo"}}
- {{if .DisableHelo}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.mailer_host"}}
- {{.Mailer.Host}}
- {{else if eq .Mailer.MailerType "sendmail"}}
- {{.i18n.Tr "admin.config.mailer_use_sendmail"}}
+ {{if eq .Mailer.Protocol "sendmail"}}
+ {{.locale.Tr "admin.config.mailer_use_sendmail"}}
{{svg "octicon-check"}}
- {{.i18n.Tr "admin.config.mailer_sendmail_path"}}
+ {{.locale.Tr "admin.config.mailer_sendmail_path"}}
{{.Mailer.SendmailPath}}
- {{.i18n.Tr "admin.config.mailer_sendmail_args"}}
+ {{.locale.Tr "admin.config.mailer_sendmail_args"}}
{{.Mailer.SendmailArgs}}
- {{.i18n.Tr "admin.config.mailer_sendmail_timeout"}}
- {{.Mailer.SendmailTimeout}} {{.i18n.Tr "tool.raw_seconds"}}
+ {{.locale.Tr "admin.config.mailer_sendmail_timeout"}}
+ {{.Mailer.SendmailTimeout}} {{.locale.Tr "tool.raw_seconds"}}
+ {{else if eq .Mailer.Protocol "dummy"}}
+ {{.locale.Tr "admin.config.mailer_use_dummy"}}
+ {{svg "octicon-check"}}
+ {{else}}{{/* SMTP family */}}
+ {{.locale.Tr "admin.config.mailer_protocol"}}
+ {{.Mailer.Protocol}}
+ {{.locale.Tr "admin.config.mailer_enable_helo"}}
+ {{if .Mailer.EnableHelo}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
+ {{.locale.Tr "admin.config.mailer_smtp_addr"}}
+ {{.Mailer.SMTPAddr}}
+ {{.locale.Tr "admin.config.mailer_smtp_port"}}
+ {{.Mailer.SMTPPort}}
{{end}}
- {{.i18n.Tr "admin.config.mailer_user"}}
+ {{.locale.Tr "admin.config.mailer_user"}}
{{if .Mailer.User}}{{.Mailer.User}}{{else}}(empty){{end}}
{{end}}
- {{.i18n.Tr "admin.config.cache_adapter"}}
+ {{.locale.Tr "admin.config.cache_adapter"}}
{{.CacheAdapter}}
{{if eq .CacheAdapter "memory"}}
- {{.i18n.Tr "admin.config.cache_interval"}}
- {{.CacheInterval}} {{.i18n.Tr "tool.raw_seconds"}}
+ {{.locale.Tr "admin.config.cache_interval"}}
+ {{.CacheInterval}} {{.locale.Tr "tool.raw_seconds"}}
{{end}}
{{if .CacheConn}}
- {{.i18n.Tr "admin.config.cache_conn"}}
+ {{.locale.Tr "admin.config.cache_conn"}}
{{.CacheConn}}
- {{.i18n.Tr "admin.config.cache_item_ttl"}}
+ {{.locale.Tr "admin.config.cache_item_ttl"}}
{{.CacheItemTTL}}
{{end}}
- {{.i18n.Tr "admin.config.session_provider"}}
+ {{.locale.Tr "admin.config.session_provider"}}
{{.SessionConfig.Provider}}
- {{.i18n.Tr "admin.config.provider_config"}}
+ {{.locale.Tr "admin.config.provider_config"}}
{{if .SessionConfig.ProviderConfig}}{{.SessionConfig.ProviderConfig}}{{else}}-{{end}}
- {{.i18n.Tr "admin.config.cookie_name"}}
+ {{.locale.Tr "admin.config.cookie_name"}}
{{.SessionConfig.CookieName}}
- {{.i18n.Tr "admin.config.gc_interval_time"}}
- {{.SessionConfig.Gclifetime}} {{.i18n.Tr "tool.raw_seconds"}}
- {{.i18n.Tr "admin.config.session_life_time"}}
- {{.SessionConfig.Maxlifetime}} {{.i18n.Tr "tool.raw_seconds"}}
- {{.i18n.Tr "admin.config.https_only"}}
+ {{.locale.Tr "admin.config.gc_interval_time"}}
+ {{.SessionConfig.Gclifetime}} {{.locale.Tr "tool.raw_seconds"}}
+ {{.locale.Tr "admin.config.session_life_time"}}
+ {{.SessionConfig.Maxlifetime}} {{.locale.Tr "tool.raw_seconds"}}
+ {{.locale.Tr "admin.config.https_only"}}
{{if .SessionConfig.Secure}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.git_disable_diff_highlight"}}
+ {{.locale.Tr "admin.config.git_disable_diff_highlight"}}
{{if .Git.DisableDiffHighlight}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
- {{.i18n.Tr "admin.config.git_max_diff_lines"}}
+ {{.locale.Tr "admin.config.git_max_diff_lines"}}
{{.Git.MaxGitDiffLines}}
- {{.i18n.Tr "admin.config.git_max_diff_line_characters"}}
+ {{.locale.Tr "admin.config.git_max_diff_line_characters"}}
{{.Git.MaxGitDiffLineCharacters}}
- {{.i18n.Tr "admin.config.git_max_diff_files"}}
+ {{.locale.Tr "admin.config.git_max_diff_files"}}
{{.Git.MaxGitDiffFiles}}
- {{.i18n.Tr "admin.config.git_gc_args"}}
+ {{.locale.Tr "admin.config.git_gc_args"}}
{{.Git.GCArgs}}
- {{.i18n.Tr "admin.config.git_migrate_timeout"}}
- {{.Git.Timeout.Migrate}} {{.i18n.Tr "tool.raw_seconds"}}
- {{.i18n.Tr "admin.config.git_mirror_timeout"}}
- {{.Git.Timeout.Mirror}} {{.i18n.Tr "tool.raw_seconds"}}
- {{.i18n.Tr "admin.config.git_clone_timeout"}}
- {{.Git.Timeout.Clone}} {{.i18n.Tr "tool.raw_seconds"}}
- {{.i18n.Tr "admin.config.git_pull_timeout"}}
- {{.Git.Timeout.Pull}} {{.i18n.Tr "tool.raw_seconds"}}
- {{.i18n.Tr "admin.config.git_gc_timeout"}}
- {{.Git.Timeout.GC}} {{.i18n.Tr "tool.raw_seconds"}}
+ {{.locale.Tr "admin.config.git_migrate_timeout"}}
+ {{.Git.Timeout.Migrate}} {{.locale.Tr "tool.raw_seconds"}}
+ {{.locale.Tr "admin.config.git_mirror_timeout"}}
+ {{.Git.Timeout.Mirror}} {{.locale.Tr "tool.raw_seconds"}}
+ {{.locale.Tr "admin.config.git_clone_timeout"}}
+ {{.Git.Timeout.Clone}} {{.locale.Tr "tool.raw_seconds"}}
+ {{.locale.Tr "admin.config.git_pull_timeout"}}
+ {{.Git.Timeout.Pull}} {{.locale.Tr "tool.raw_seconds"}}
+ {{.locale.Tr "admin.config.git_gc_timeout"}}
+ {{.Git.Timeout.GC}} {{.locale.Tr "tool.raw_seconds"}}
{{range .Loggers.default.SubLogDescriptions}}
- {{$.i18n.Tr "admin.config.log_mode"}}
+ {{$.locale.Tr "admin.config.log_mode"}}
{{.Name}} ({{.Provider}})
- {{$.i18n.Tr "admin.config.log_config"}}
+ {{$.locale.Tr "admin.config.log_config"}}
{{.Config | JsonPrettyPrint}}
{{end}}
- {{$.i18n.Tr "admin.config.router_log_mode"}}
+ {{$.locale.Tr "admin.config.router_log_mode"}}
{{if .DisableRouterLog}}
- {{$.i18n.Tr "admin.config.disabled_logger"}}
+ {{$.locale.Tr "admin.config.disabled_logger"}}
{{else}}
{{if .Loggers.router.SubLogDescriptions}}
- {{$.i18n.Tr "admin.config.own_named_logger"}}
+ {{$.locale.Tr "admin.config.own_named_logger"}}
{{range .Loggers.router.SubLogDescriptions}}
- {{$.i18n.Tr "admin.config.log_mode"}}
+ {{$.locale.Tr "admin.config.log_mode"}}
{{.Name}} ({{.Provider}})
- {{$.i18n.Tr "admin.config.log_config"}}
+ {{$.locale.Tr "admin.config.log_config"}}
{{.Config | JsonPrettyPrint}}
{{end}}
{{else}}
- {{$.i18n.Tr "admin.config.routes_to_default_logger"}}
+ {{$.locale.Tr "admin.config.routes_to_default_logger"}}
{{end}}
{{end}}
- {{$.i18n.Tr "admin.config.access_log_mode"}}
+ {{$.locale.Tr "admin.config.access_log_mode"}}
{{if .EnableAccessLog}}
{{if .Loggers.access.SubLogDescriptions}}
- {{$.i18n.Tr "admin.config.own_named_logger"}}
+ {{$.locale.Tr "admin.config.own_named_logger"}}
{{range .Loggers.access.SubLogDescriptions}}
- {{$.i18n.Tr "admin.config.log_mode"}}
+ {{$.locale.Tr "admin.config.log_mode"}}
{{.Name}} ({{.Provider}})
- {{$.i18n.Tr "admin.config.log_config"}}
+ {{$.locale.Tr "admin.config.log_config"}}
{{.Config | JsonPrettyPrint}}
{{end}}
{{else}}
- {{$.i18n.Tr "admin.config.routes_to_default_logger"}}
+ {{$.locale.Tr "admin.config.routes_to_default_logger"}}
{{end}}
- {{$.i18n.Tr "admin.config.access_log_template"}}
+ {{$.locale.Tr "admin.config.access_log_template"}}
{{$.AccessLogTemplate}}
{{else}}
- {{$.i18n.Tr "admin.config.disabled_logger"}}
+ {{$.locale.Tr "admin.config.disabled_logger"}}
{{end}}
- {{$.i18n.Tr "admin.config.xorm_log_mode"}}
+ {{$.locale.Tr "admin.config.xorm_log_mode"}}
{{if .EnableXORMLog}}
{{if .Loggers.xorm.SubLogDescriptions}}
- {{$.i18n.Tr "admin.config.own_named_logger"}}
+ {{$.locale.Tr "admin.config.own_named_logger"}}
{{range .Loggers.xorm.SubLogDescriptions}}
- {{$.i18n.Tr "admin.config.log_mode"}}
+ {{$.locale.Tr "admin.config.log_mode"}}
{{.Name}} ({{.Provider}})
- {{$.i18n.Tr "admin.config.log_config"}}
+ {{$.locale.Tr "admin.config.log_config"}}
{{.Config | JsonPrettyPrint}}
{{end}}
{{else}}
- {{$.i18n.Tr "admin.config.routes_to_default_logger"}}
+ {{$.locale.Tr "admin.config.routes_to_default_logger"}}
{{end}}
- {{$.i18n.Tr "admin.config.xorm_log_sql"}}
+ {{$.locale.Tr "admin.config.xorm_log_sql"}}
{{if $.LogSQL}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
{{else}}
- {{$.i18n.Tr "admin.config.disabled_logger"}}
+ {{$.locale.Tr "admin.config.disabled_logger"}}
{{end}}
diff --git a/templates/admin/cron.tmpl b/templates/admin/cron.tmpl
index 30277177ed0c5..d34999e6ebdc4 100644
--- a/templates/admin/cron.tmpl
+++ b/templates/admin/cron.tmpl
@@ -1,30 +1,30 @@