@@ -8,6 +8,15 @@ delay = 1000 | |||
include_ext = ["go", "tmpl"] | |||
include_file = ["main.go"] | |||
include_dir = ["cmd", "models", "modules", "options", "routers", "services"] | |||
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"] | |||
exclude_dir = [ | |||
"models/fixtures", | |||
"models/migrations/fixtures", | |||
"modules/avatar/identicon/testdata", | |||
"modules/avatar/testdata", | |||
"modules/git/tests", | |||
"modules/migration/file_format_testdata", | |||
"routers/private/tests", | |||
"services/gitdiff/testdata", | |||
] | |||
exclude_regex = ["_test.go$", "_gen.go$"] | |||
stop_on_error = true |
@@ -4,7 +4,7 @@ | |||
"features": { | |||
// installs nodejs into container | |||
"ghcr.io/devcontainers/features/node:1": { | |||
"version":"20" | |||
"version": "20" | |||
}, | |||
"ghcr.io/devcontainers/features/git-lfs:1.1.0": {}, | |||
"ghcr.io/devcontainers-contrib/features/poetry:2": {}, | |||
@@ -24,7 +24,7 @@ | |||
"DavidAnson.vscode-markdownlint", | |||
"Vue.volar", | |||
"ms-azuretools.vscode-docker", | |||
"zixuanchen.vitest-explorer", | |||
"vitest.explorer", | |||
"qwtel.sqlite-viewer", | |||
"GitHub.vscode-pull-request-github" | |||
] |
@@ -14,7 +14,7 @@ _test | |||
# MS VSCode | |||
.vscode | |||
__debug_bin | |||
__debug_bin* | |||
# Architecture specific extensions/prefixes | |||
*.[568vq] | |||
@@ -62,7 +62,6 @@ cpu.out | |||
/data | |||
/indexers | |||
/log | |||
/public/img/avatar | |||
/tests/integration/gitea-integration-* | |||
/tests/integration/indexers-* | |||
/tests/e2e/gitea-e2e-* | |||
@@ -78,7 +77,7 @@ cpu.out | |||
/public/assets/js | |||
/public/assets/css | |||
/public/assets/fonts | |||
/public/assets/img/webpack | |||
/public/assets/img/avatar | |||
/vendor | |||
/web_src/fomantic/node_modules | |||
/web_src/fomantic/build/* |
@@ -3,6 +3,7 @@ reportUnusedDisableDirectives: true | |||
ignorePatterns: | |||
- /web_src/js/vendor | |||
- /web_src/fomantic | |||
parserOptions: | |||
sourceType: module | |||
@@ -42,10 +43,6 @@ overrides: | |||
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: | |||
i/no-unresolved: [0] | |||
i/no-extraneous-dependencies: [0] | |||
- files: ["*.config.*"] | |||
rules: | |||
i/no-unused-modules: [0] | |||
@@ -123,7 +120,7 @@ rules: | |||
"@stylistic/js/arrow-spacing": [2, {before: true, after: true}] | |||
"@stylistic/js/block-spacing": [0] | |||
"@stylistic/js/brace-style": [2, 1tbs, {allowSingleLine: true}] | |||
"@stylistic/js/comma-dangle": [2, only-multiline] | |||
"@stylistic/js/comma-dangle": [2, always-multiline] | |||
"@stylistic/js/comma-spacing": [2, {before: false, after: true}] | |||
"@stylistic/js/comma-style": [2, last] | |||
"@stylistic/js/computed-property-spacing": [2, never] | |||
@@ -171,7 +168,7 @@ rules: | |||
"@stylistic/js/semi-spacing": [2, {before: false, after: true}] | |||
"@stylistic/js/semi-style": [2, last] | |||
"@stylistic/js/space-before-blocks": [2, always] | |||
"@stylistic/js/space-before-function-paren": [0] | |||
"@stylistic/js/space-before-function-paren": [2, {anonymous: ignore, named: never, asyncArrow: always}] | |||
"@stylistic/js/space-in-parens": [2, never] | |||
"@stylistic/js/space-infix-ops": [2] | |||
"@stylistic/js/space-unary-ops": [2] | |||
@@ -283,14 +280,14 @@ rules: | |||
i/unambiguous: [0] | |||
init-declarations: [0] | |||
jquery/no-ajax-events: [2] | |||
jquery/no-ajax: [0] | |||
jquery/no-ajax: [2] | |||
jquery/no-animate: [2] | |||
jquery/no-attr: [0] | |||
jquery/no-attr: [2] | |||
jquery/no-bind: [2] | |||
jquery/no-class: [0] | |||
jquery/no-clone: [2] | |||
jquery/no-closest: [0] | |||
jquery/no-css: [0] | |||
jquery/no-css: [2] | |||
jquery/no-data: [0] | |||
jquery/no-deferred: [2] | |||
jquery/no-delegate: [2] | |||
@@ -307,7 +304,7 @@ rules: | |||
jquery/no-in-array: [2] | |||
jquery/no-is-array: [2] | |||
jquery/no-is-function: [2] | |||
jquery/no-is: [0] | |||
jquery/no-is: [2] | |||
jquery/no-load: [2] | |||
jquery/no-map: [2] | |||
jquery/no-merge: [2] | |||
@@ -315,7 +312,7 @@ rules: | |||
jquery/no-parent: [0] | |||
jquery/no-parents: [0] | |||
jquery/no-parse-html: [2] | |||
jquery/no-prop: [0] | |||
jquery/no-prop: [2] | |||
jquery/no-proxy: [2] | |||
jquery/no-ready: [2] | |||
jquery/no-serialize: [2] | |||
@@ -396,12 +393,12 @@ rules: | |||
no-irregular-whitespace: [2] | |||
no-iterator: [2] | |||
no-jquery/no-ajax-events: [2] | |||
no-jquery/no-ajax: [0] | |||
no-jquery/no-ajax: [2] | |||
no-jquery/no-and-self: [2] | |||
no-jquery/no-animate-toggle: [2] | |||
no-jquery/no-animate: [2] | |||
no-jquery/no-append-html: [0] | |||
no-jquery/no-attr: [0] | |||
no-jquery/no-append-html: [2] | |||
no-jquery/no-attr: [2] | |||
no-jquery/no-bind: [2] | |||
no-jquery/no-box-model: [2] | |||
no-jquery/no-browser: [2] | |||
@@ -413,7 +410,7 @@ rules: | |||
no-jquery/no-constructor-attributes: [2] | |||
no-jquery/no-contains: [2] | |||
no-jquery/no-context-prop: [2] | |||
no-jquery/no-css: [0] | |||
no-jquery/no-css: [2] | |||
no-jquery/no-data: [0] | |||
no-jquery/no-deferred: [2] | |||
no-jquery/no-delegate: [2] | |||
@@ -444,7 +441,7 @@ rules: | |||
no-jquery/no-is-numeric: [2] | |||
no-jquery/no-is-plain-object: [2] | |||
no-jquery/no-is-window: [2] | |||
no-jquery/no-is: [0] | |||
no-jquery/no-is: [2] | |||
no-jquery/no-jquery-constructor: [0] | |||
no-jquery/no-live: [2] | |||
no-jquery/no-load-shorthand: [2] | |||
@@ -466,7 +463,7 @@ rules: | |||
no-jquery/no-parse-html: [2] | |||
no-jquery/no-parse-json: [2] | |||
no-jquery/no-parse-xml: [2] | |||
no-jquery/no-prop: [0] | |||
no-jquery/no-prop: [2] | |||
no-jquery/no-proxy: [2] | |||
no-jquery/no-ready-shorthand: [2] | |||
no-jquery/no-ready: [2] | |||
@@ -487,7 +484,7 @@ rules: | |||
no-jquery/no-visibility: [2] | |||
no-jquery/no-when: [2] | |||
no-jquery/no-wrap: [2] | |||
no-jquery/variable-pattern: [0] | |||
no-jquery/variable-pattern: [2] | |||
no-label-var: [2] | |||
no-labels: [0] # handled by no-restricted-syntax | |||
no-lone-blocks: [2] |
@@ -1,36 +1,77 @@ | |||
modifies/docs: | |||
- "**/*.md" | |||
- "docs/**" | |||
modifies/frontend: | |||
- "web_src/**/*" | |||
- changed-files: | |||
- any-glob-to-any-file: | |||
- "**/*.md" | |||
- "docs/**" | |||
modifies/templates: | |||
- all: ["templates/**", "!templates/swagger/v1_json.tmpl"] | |||
- changed-files: | |||
- all-globs-to-any-file: | |||
- "templates/**" | |||
- "!templates/swagger/v1_json.tmpl" | |||
modifies/api: | |||
- "routers/api/**" | |||
- "templates/swagger/v1_json.tmpl" | |||
- changed-files: | |||
- any-glob-to-any-file: | |||
- "routers/api/**" | |||
- "templates/swagger/v1_json.tmpl" | |||
modifies/cli: | |||
- "cmd/**" | |||
- changed-files: | |||
- any-glob-to-any-file: | |||
- "cmd/**" | |||
modifies/translation: | |||
- "options/locale/*.ini" | |||
- changed-files: | |||
- any-glob-to-any-file: | |||
- "options/locale/*.ini" | |||
modifies/migrations: | |||
- "models/migrations/**/*" | |||
- changed-files: | |||
- any-glob-to-any-file: | |||
- "models/migrations/**" | |||
modifies/internal: | |||
- "Makefile" | |||
- "Dockerfile" | |||
- "Dockerfile.rootless" | |||
- "docker/**" | |||
- "webpack.config.js" | |||
- ".eslintrc.yaml" | |||
- ".golangci.yml" | |||
- ".markdownlint.yaml" | |||
- ".spectral.yaml" | |||
- ".stylelintrc.yaml" | |||
- ".yamllint.yaml" | |||
- ".github/**" | |||
- changed-files: | |||
- any-glob-to-any-file: | |||
- ".air.toml" | |||
- "Makefile" | |||
- "Dockerfile" | |||
- "Dockerfile.rootless" | |||
- ".dockerignore" | |||
- "docker/**" | |||
- ".editorconfig" | |||
- ".eslintrc.yaml" | |||
- ".golangci.yml" | |||
- ".gitpod.yml" | |||
- ".markdownlint.yaml" | |||
- ".spectral.yaml" | |||
- "stylelint.config.js" | |||
- ".yamllint.yaml" | |||
- ".github/**" | |||
- ".gitea/" | |||
- ".devcontainer/**" | |||
- "build.go" | |||
- "build/**" | |||
- "contrib/**" | |||
modifies/dependencies: | |||
- changed-files: | |||
- any-glob-to-any-file: | |||
- "package.json" | |||
- "package-lock.json" | |||
- "pyproject.toml" | |||
- "poetry.lock" | |||
- "go.mod" | |||
- "go.sum" | |||
modifies/go: | |||
- changed-files: | |||
- any-glob-to-any-file: | |||
- "**/*.go" | |||
modifies/js: | |||
- changed-files: | |||
- any-glob-to-any-file: | |||
- "**/*.js" | |||
- "**/*.vue" |
@@ -1,23 +0,0 @@ | |||
name: cron-lock | |||
on: | |||
schedule: | |||
- cron: "0 0 * * *" # every day at 00:00 UTC | |||
workflow_dispatch: | |||
permissions: | |||
issues: write | |||
pull-requests: write | |||
concurrency: | |||
group: lock | |||
jobs: | |||
action: | |||
runs-on: ubuntu-latest | |||
if: github.repository == 'go-gitea/gitea' | |||
steps: | |||
- uses: dessant/lock-threads@v5 | |||
with: | |||
issue-inactive-days: 10 | |||
pr-inactive-days: 7 |
@@ -11,14 +11,19 @@ jobs: | |||
if: github.repository == 'go-gitea/gitea' | |||
steps: | |||
- uses: actions/checkout@v4 | |||
- name: download from crowdin | |||
uses: docker://jonasfranz/crowdin | |||
- uses: crowdin/github-action@v1 | |||
with: | |||
upload_sources: true | |||
upload_translations: false | |||
download_sources: false | |||
download_translations: true | |||
push_translations: false | |||
push_sources: false | |||
create_pull_request: false | |||
config: crowdin.yml | |||
env: | |||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} | |||
CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }} | |||
PLUGIN_DOWNLOAD: true | |||
PLUGIN_EXPORT_DIR: options/locale/ | |||
PLUGIN_IGNORE_BRANCH: true | |||
PLUGIN_PROJECT_IDENTIFIER: gitea | |||
- name: update locales | |||
run: ./build/update-locales.sh | |||
- name: push translations to repo | |||
@@ -31,19 +36,3 @@ jobs: | |||
commit_message: "[skip ci] Updated translations via Crowdin" | |||
remote: "git@github.com:go-gitea/gitea.git" | |||
ssh_key: ${{ secrets.DEPLOY_KEY }} | |||
crowdin-push: | |||
runs-on: ubuntu-latest | |||
if: github.repository == 'go-gitea/gitea' | |||
steps: | |||
- uses: actions/checkout@v4 | |||
- name: push translations to crowdin | |||
uses: docker://jonasfranz/crowdin | |||
env: | |||
CROWDIN_KEY: ${{ secrets.CROWDIN_KEY }} | |||
PLUGIN_UPLOAD: true | |||
PLUGIN_EXPORT_DIR: options/locale/ | |||
PLUGIN_IGNORE_BRANCH: true | |||
PLUGIN_PROJECT_IDENTIFIER: gitea | |||
PLUGIN_FILES: | | |||
locale_en-US.ini: options/locale/locale_en-US.ini | |||
PLUGIN_BRANCH: main |
@@ -58,7 +58,7 @@ jobs: | |||
- "package-lock.json" | |||
- "Makefile" | |||
- ".eslintrc.yaml" | |||
- ".stylelintrc.yaml" | |||
- "stylelint.config.js" | |||
- ".npmrc" | |||
docs: | |||
@@ -73,6 +73,7 @@ jobs: | |||
- "Makefile" | |||
templates: | |||
- "tools/lint-templates-*.js" | |||
- "templates/**/*.tmpl" | |||
- "pyproject.toml" | |||
- "poetry.lock" |
@@ -35,8 +35,12 @@ jobs: | |||
- uses: actions/setup-python@v5 | |||
with: | |||
python-version: "3.12" | |||
- uses: actions/setup-node@v4 | |||
with: | |||
node-version: 20 | |||
- run: pip install poetry | |||
- run: make deps-py | |||
- run: make deps-frontend | |||
- run: make lint-templates | |||
lint-yaml: |
@@ -9,12 +9,12 @@ concurrency: | |||
cancel-in-progress: true | |||
jobs: | |||
label: | |||
labeler: | |||
runs-on: ubuntu-latest | |||
permissions: | |||
contents: read | |||
pull-requests: write | |||
steps: | |||
- uses: actions/labeler@v4 | |||
- uses: actions/labeler@v5 | |||
with: | |||
dot: true | |||
sync-labels: true |
@@ -58,7 +58,7 @@ cpu.out | |||
/data | |||
/indexers | |||
/log | |||
/public/img/avatar | |||
/public/assets/img/avatar | |||
/tests/integration/gitea-integration-* | |||
/tests/integration/indexers-* | |||
/tests/e2e/gitea-e2e-* | |||
@@ -77,7 +77,6 @@ cpu.out | |||
/public/assets/css | |||
/public/assets/fonts | |||
/public/assets/licenses.txt | |||
/public/assets/img/webpack | |||
/vendor | |||
/web_src/fomantic/node_modules | |||
/web_src/fomantic/build/* |
@@ -42,7 +42,7 @@ vscode: | |||
- DavidAnson.vscode-markdownlint | |||
- Vue.volar | |||
- ms-azuretools.vscode-docker | |||
- zixuanchen.vitest-explorer | |||
- vitest.explorer | |||
- qwtel.sqlite-viewer | |||
- GitHub.vscode-pull-request-github | |||
@@ -4,6 +4,8 @@ | |||
/modules/options/bindata.go | |||
/modules/public/bindata.go | |||
/modules/templates/bindata.go | |||
/vendor | |||
/options/gitignore | |||
/options/license | |||
/public/assets | |||
/vendor | |||
node_modules |
@@ -1,222 +0,0 @@ | |||
plugins: | |||
- stylelint-declaration-strict-value | |||
- stylelint-declaration-block-no-ignored-properties | |||
- "@stylistic/stylelint-plugin" | |||
ignoreFiles: | |||
- "**/*.go" | |||
overrides: | |||
- files: ["**/chroma/*", "**/codemirror/*", "**/standalone/*", "**/console.css", "font_i18n.css"] | |||
rules: | |||
scale-unlimited/declaration-strict-value: null | |||
- files: ["**/chroma/*", "**/codemirror/*"] | |||
rules: | |||
block-no-empty: null | |||
- files: ["**/*.vue"] | |||
customSyntax: postcss-html | |||
rules: | |||
"@stylistic/at-rule-name-case": null | |||
"@stylistic/at-rule-name-newline-after": null | |||
"@stylistic/at-rule-name-space-after": null | |||
"@stylistic/at-rule-semicolon-newline-after": null | |||
"@stylistic/at-rule-semicolon-space-before": null | |||
"@stylistic/block-closing-brace-empty-line-before": null | |||
"@stylistic/block-closing-brace-newline-after": null | |||
"@stylistic/block-closing-brace-newline-before": null | |||
"@stylistic/block-closing-brace-space-after": null | |||
"@stylistic/block-closing-brace-space-before": null | |||
"@stylistic/block-opening-brace-newline-after": null | |||
"@stylistic/block-opening-brace-newline-before": null | |||
"@stylistic/block-opening-brace-space-after": null | |||
"@stylistic/block-opening-brace-space-before": null | |||
"@stylistic/color-hex-case": lower | |||
"@stylistic/declaration-bang-space-after": never | |||
"@stylistic/declaration-bang-space-before": null | |||
"@stylistic/declaration-block-semicolon-newline-after": null | |||
"@stylistic/declaration-block-semicolon-newline-before": null | |||
"@stylistic/declaration-block-semicolon-space-after": null | |||
"@stylistic/declaration-block-semicolon-space-before": never | |||
"@stylistic/declaration-block-trailing-semicolon": null | |||
"@stylistic/declaration-colon-newline-after": null | |||
"@stylistic/declaration-colon-space-after": null | |||
"@stylistic/declaration-colon-space-before": never | |||
"@stylistic/function-comma-newline-after": null | |||
"@stylistic/function-comma-newline-before": null | |||
"@stylistic/function-comma-space-after": null | |||
"@stylistic/function-comma-space-before": null | |||
"@stylistic/function-max-empty-lines": 0 | |||
"@stylistic/function-parentheses-newline-inside": never-multi-line | |||
"@stylistic/function-parentheses-space-inside": null | |||
"@stylistic/function-whitespace-after": null | |||
"@stylistic/indentation": 2 | |||
"@stylistic/linebreaks": null | |||
"@stylistic/max-empty-lines": 1 | |||
"@stylistic/max-line-length": null | |||
"@stylistic/media-feature-colon-space-after": null | |||
"@stylistic/media-feature-colon-space-before": never | |||
"@stylistic/media-feature-name-case": null | |||
"@stylistic/media-feature-parentheses-space-inside": null | |||
"@stylistic/media-feature-range-operator-space-after": always | |||
"@stylistic/media-feature-range-operator-space-before": always | |||
"@stylistic/media-query-list-comma-newline-after": null | |||
"@stylistic/media-query-list-comma-newline-before": null | |||
"@stylistic/media-query-list-comma-space-after": null | |||
"@stylistic/media-query-list-comma-space-before": null | |||
"@stylistic/named-grid-areas-alignment": null | |||
"@stylistic/no-empty-first-line": null | |||
"@stylistic/no-eol-whitespace": true | |||
"@stylistic/no-extra-semicolons": true | |||
"@stylistic/no-missing-end-of-source-newline": null | |||
"@stylistic/number-leading-zero": null | |||
"@stylistic/number-no-trailing-zeros": null | |||
"@stylistic/property-case": lower | |||
"@stylistic/selector-attribute-brackets-space-inside": null | |||
"@stylistic/selector-attribute-operator-space-after": null | |||
"@stylistic/selector-attribute-operator-space-before": null | |||
"@stylistic/selector-combinator-space-after": null | |||
"@stylistic/selector-combinator-space-before": null | |||
"@stylistic/selector-descendant-combinator-no-non-space": null | |||
"@stylistic/selector-list-comma-newline-after": null | |||
"@stylistic/selector-list-comma-newline-before": null | |||
"@stylistic/selector-list-comma-space-after": always-single-line | |||
"@stylistic/selector-list-comma-space-before": never-single-line | |||
"@stylistic/selector-max-empty-lines": 0 | |||
"@stylistic/selector-pseudo-class-case": lower | |||
"@stylistic/selector-pseudo-class-parentheses-space-inside": never | |||
"@stylistic/selector-pseudo-element-case": lower | |||
"@stylistic/string-quotes": double | |||
"@stylistic/unicode-bom": null | |||
"@stylistic/unit-case": lower | |||
"@stylistic/value-list-comma-newline-after": null | |||
"@stylistic/value-list-comma-newline-before": null | |||
"@stylistic/value-list-comma-space-after": null | |||
"@stylistic/value-list-comma-space-before": null | |||
"@stylistic/value-list-max-empty-lines": 0 | |||
alpha-value-notation: null | |||
annotation-no-unknown: true | |||
at-rule-allowed-list: null | |||
at-rule-disallowed-list: null | |||
at-rule-empty-line-before: null | |||
at-rule-no-unknown: [true, {ignoreAtRules: [tailwind]}] | |||
at-rule-no-vendor-prefix: true | |||
at-rule-property-required-list: null | |||
block-no-empty: true | |||
color-function-notation: null | |||
color-hex-alpha: null | |||
color-hex-length: null | |||
color-named: null | |||
color-no-hex: null | |||
color-no-invalid-hex: true | |||
comment-empty-line-before: null | |||
comment-no-empty: true | |||
comment-pattern: null | |||
comment-whitespace-inside: null | |||
comment-word-disallowed-list: null | |||
custom-media-pattern: null | |||
custom-property-empty-line-before: null | |||
custom-property-no-missing-var-function: true | |||
custom-property-pattern: null | |||
declaration-block-no-duplicate-custom-properties: true | |||
declaration-block-no-duplicate-properties: [true, {ignore: [consecutive-duplicates-with-different-values]}] | |||
declaration-block-no-redundant-longhand-properties: null | |||
declaration-block-no-shorthand-property-overrides: null | |||
declaration-block-single-line-max-declarations: null | |||
declaration-empty-line-before: null | |||
declaration-no-important: null | |||
declaration-property-max-values: null | |||
declaration-property-unit-allowed-list: null | |||
declaration-property-unit-disallowed-list: {line-height: [em]} | |||
declaration-property-value-allowed-list: null | |||
declaration-property-value-disallowed-list: null | |||
declaration-property-value-no-unknown: true | |||
font-family-name-quotes: always-where-recommended | |||
font-family-no-duplicate-names: true | |||
font-family-no-missing-generic-family-keyword: true | |||
font-weight-notation: null | |||
function-allowed-list: null | |||
function-calc-no-unspaced-operator: true | |||
function-disallowed-list: null | |||
function-linear-gradient-no-nonstandard-direction: true | |||
function-name-case: lower | |||
function-no-unknown: null | |||
function-url-no-scheme-relative: null | |||
function-url-quotes: always | |||
function-url-scheme-allowed-list: null | |||
function-url-scheme-disallowed-list: null | |||
hue-degree-notation: null | |||
import-notation: string | |||
keyframe-block-no-duplicate-selectors: true | |||
keyframe-declaration-no-important: true | |||
keyframe-selector-notation: null | |||
keyframes-name-pattern: null | |||
length-zero-no-unit: [true, ignore: [custom-properties], ignoreFunctions: [var]] | |||
max-nesting-depth: null | |||
media-feature-name-allowed-list: null | |||
media-feature-name-disallowed-list: null | |||
media-feature-name-no-unknown: true | |||
media-feature-name-no-vendor-prefix: true | |||
media-feature-name-unit-allowed-list: null | |||
media-feature-name-value-allowed-list: null | |||
media-feature-name-value-no-unknown: true | |||
media-feature-range-notation: null | |||
media-query-no-invalid: true | |||
named-grid-areas-no-invalid: true | |||
no-descending-specificity: null | |||
no-duplicate-at-import-rules: true | |||
no-duplicate-selectors: true | |||
no-empty-source: true | |||
no-invalid-double-slash-comments: true | |||
no-invalid-position-at-import-rule: null | |||
no-irregular-whitespace: true | |||
no-unknown-animations: null | |||
no-unknown-custom-properties: null | |||
number-max-precision: null | |||
plugin/declaration-block-no-ignored-properties: true | |||
property-allowed-list: null | |||
property-disallowed-list: null | |||
property-no-unknown: true | |||
property-no-vendor-prefix: null | |||
rule-empty-line-before: null | |||
rule-selector-property-disallowed-list: null | |||
scale-unlimited/declaration-strict-value: [[/color$/, font-weight], {ignoreValues: /^(inherit|transparent|unset|initial|currentcolor|none)$/, ignoreFunctions: false, disableFix: true, expandShorthand: true}] | |||
selector-attribute-name-disallowed-list: null | |||
selector-attribute-operator-allowed-list: null | |||
selector-attribute-operator-disallowed-list: null | |||
selector-attribute-quotes: always | |||
selector-class-pattern: null | |||
selector-combinator-allowed-list: null | |||
selector-combinator-disallowed-list: null | |||
selector-disallowed-list: null | |||
selector-id-pattern: null | |||
selector-max-attribute: null | |||
selector-max-class: null | |||
selector-max-combinators: null | |||
selector-max-compound-selectors: null | |||
selector-max-id: null | |||
selector-max-pseudo-class: null | |||
selector-max-specificity: null | |||
selector-max-type: null | |||
selector-max-universal: null | |||
selector-nested-pattern: null | |||
selector-no-qualifying-type: null | |||
selector-no-vendor-prefix: true | |||
selector-not-notation: null | |||
selector-pseudo-class-allowed-list: null | |||
selector-pseudo-class-disallowed-list: null | |||
selector-pseudo-class-no-unknown: true | |||
selector-pseudo-element-allowed-list: null | |||
selector-pseudo-element-colon-notation: double | |||
selector-pseudo-element-disallowed-list: null | |||
selector-pseudo-element-no-unknown: true | |||
selector-type-case: lower | |||
selector-type-no-unknown: [true, {ignore: [custom-elements]}] | |||
shorthand-property-no-redundant-values: true | |||
string-no-newline: true | |||
time-min-milliseconds: null | |||
unit-allowed-list: null | |||
unit-disallowed-list: null | |||
unit-no-unknown: true | |||
value-keyword-case: null | |||
value-no-vendor-prefix: [true, {ignoreValues: [box, inline-box]}] |
@@ -60,3 +60,4 @@ Nanguan Lin <nanguanlin6@gmail.com> (@lng2020) | |||
kerwin612 <kerwin612@qq.com> (@kerwin612) | |||
Gary Wang <git@blumia.net> (@BLumia) | |||
Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis) | |||
Yu Liu <1240335630@qq.com> (@HEREYUA) |
@@ -31,7 +31,7 @@ GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0 | |||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56.1 | |||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 | |||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1 | |||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5 | |||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285 | |||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest | |||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 | |||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.0.3 | |||
@@ -42,9 +42,6 @@ DOCKER_TAG ?= latest | |||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG) | |||
ifeq ($(HAS_GO), yes) | |||
GOPATH ?= $(shell $(GO) env GOPATH) | |||
export PATH := $(GOPATH)/bin:$(PATH) | |||
CGO_EXTRA_CFLAGS := -DSQLITE_MAX_VARIABLE_NUMBER=32766 | |||
CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS) | |||
endif | |||
@@ -122,7 +119,7 @@ FOMANTIC_WORK_DIR := web_src/fomantic | |||
WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f) | |||
WEBPACK_CONFIGS := webpack.config.js tailwind.config.js | |||
WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css | |||
WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/img/webpack | |||
WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts | |||
BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go | |||
BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST)) | |||
@@ -147,6 +144,8 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN | |||
GO_DIRS := build cmd models modules routers services tests | |||
WEB_DIRS := web_src/js web_src/css | |||
ESLINT_FILES := web_src/js tools *.config.js tests/e2e | |||
STYLELINT_FILES := web_src/css web_src/js/components/*.vue | |||
SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github | |||
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini | |||
@@ -375,19 +374,19 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig | |||
.PHONY: lint-js | |||
lint-js: node_modules | |||
npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e | |||
npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES) | |||
.PHONY: lint-js-fix | |||
lint-js-fix: node_modules | |||
npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e --fix | |||
npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES) --fix | |||
.PHONY: lint-css | |||
lint-css: node_modules | |||
npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue | |||
npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) | |||
.PHONY: lint-css-fix | |||
lint-css-fix: node_modules | |||
npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue --fix | |||
npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix | |||
.PHONY: lint-swagger | |||
lint-swagger: node_modules | |||
@@ -435,7 +434,8 @@ lint-actions: | |||
$(GO) run $(ACTIONLINT_PACKAGE) | |||
.PHONY: lint-templates | |||
lint-templates: .venv | |||
lint-templates: .venv node_modules | |||
@node tools/lint-templates-svg.js | |||
@poetry run djlint $(shell find templates -type f -iname '*.tmpl') | |||
.PHONY: lint-yaml | |||
@@ -444,7 +444,7 @@ lint-yaml: .venv | |||
.PHONY: watch | |||
watch: | |||
@bash build/watch.sh | |||
@bash tools/watch.sh | |||
.PHONY: watch-frontend | |||
watch-frontend: node-check node_modules | |||
@@ -839,10 +839,6 @@ release-sources: | $(DIST_DIRS) | |||
release-docs: | $(DIST_DIRS) docs | |||
tar -czf $(DIST)/release/gitea-docs-$(VERSION).tar.gz -C ./docs . | |||
.PHONY: docs | |||
docs: | |||
cd docs; bash scripts/trans-copy.sh; | |||
.PHONY: deps | |||
deps: deps-frontend deps-backend deps-tools deps-py | |||
@@ -920,7 +916,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json | |||
.PHONY: svg | |||
svg: node-check | node_modules | |||
rm -rf $(SVG_DEST_DIR) | |||
node build/generate-svg.js | |||
node tools/generate-svg.js | |||
.PHONY: svg-check | |||
svg-check: svg | |||
@@ -963,8 +959,8 @@ generate-gitignore: | |||
.PHONY: generate-images | |||
generate-images: | node_modules | |||
npm install --no-save fabric@6.0.0-beta19 imagemin-zopfli@7 | |||
node build/generate-images.js $(TAGS) | |||
npm install --no-save fabric@6.0.0-beta20 imagemin-zopfli@7 | |||
node tools/generate-images.js $(TAGS) | |||
.PHONY: generate-manpage | |||
generate-manpage: |
@@ -1,55 +1,18 @@ | |||
<p align="center"> | |||
<a href="https://gitea.io/"> | |||
<img alt="Gitea" src="https://raw.githubusercontent.com/go-gitea/gitea/main/public/assets/img/gitea.svg" width="220"/> | |||
</a> | |||
</p> | |||
<h1 align="center">Gitea - Git with a cup of tea</h1> | |||
<p align="center"> | |||
<a href="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain" title="Release Nightly"> | |||
<img src="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main"> | |||
</a> | |||
<a href="https://discord.gg/Gitea" title="Join the Discord chat at https://discord.gg/Gitea"> | |||
<img src="https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2"> | |||
</a> | |||
<a href="https://app.codecov.io/gh/go-gitea/gitea" title="Codecov"> | |||
<img src="https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg"> | |||
</a> | |||
<a href="https://goreportcard.com/report/code.gitea.io/gitea" title="Go Report Card"> | |||
<img src="https://goreportcard.com/badge/code.gitea.io/gitea"> | |||
</a> | |||
<a href="https://pkg.go.dev/code.gitea.io/gitea" title="GoDoc"> | |||
<img src="https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg"> | |||
</a> | |||
<a href="https://github.com/go-gitea/gitea/releases/latest" title="GitHub release"> | |||
<img src="https://img.shields.io/github/release/go-gitea/gitea.svg"> | |||
</a> | |||
<a href="https://www.codetriage.com/go-gitea/gitea" title="Help Contribute to Open Source"> | |||
<img src="https://www.codetriage.com/go-gitea/gitea/badges/users.svg"> | |||
</a> | |||
<a href="https://opencollective.com/gitea" title="Become a backer/sponsor of gitea"> | |||
<img src="https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen"> | |||
</a> | |||
<a href="https://opensource.org/licenses/MIT" title="License: MIT"> | |||
<img src="https://img.shields.io/badge/License-MIT-blue.svg"> | |||
</a> | |||
<a href="https://gitpod.io/#https://github.com/go-gitea/gitea"> | |||
<img | |||
src="https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod" | |||
alt="Contribute with Gitpod" | |||
/> | |||
</a> | |||
<a href="https://crowdin.com/project/gitea" title="Crowdin"> | |||
<img src="https://badges.crowdin.net/gitea/localized.svg"> | |||
</a> | |||
<a href="https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main" title="TODOs"> | |||
<img src="https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main"> | |||
</a> | |||
</p> | |||
<p align="center"> | |||
<a href="README_ZH.md">View this document in Chinese</a> | |||
</p> | |||
# Gitea | |||
[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly") | |||
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") | |||
[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card") | |||
[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc") | |||
[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release") | |||
[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") | |||
[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") | |||
[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") | |||
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/go-gitea/gitea) | |||
[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin") | |||
[![](https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main)](https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main "TODOs") | |||
[View this document in Chinese](./README_ZH.md) | |||
## Purpose | |||
@@ -1,55 +1,18 @@ | |||
<p align="center"> | |||
<a href="https://gitea.io/"> | |||
<img alt="Gitea" src="https://raw.githubusercontent.com/go-gitea/gitea/main/public/assets/img/gitea.svg" width="220"/> | |||
</a> | |||
</p> | |||
<h1 align="center">Gitea - Git with a cup of tea</h1> | |||
<p align="center"> | |||
<a href="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain" title="Release Nightly"> | |||
<img src="https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main"> | |||
</a> | |||
<a href="https://discord.gg/Gitea" title="Join the Discord chat at https://discord.gg/Gitea"> | |||
<img src="https://img.shields.io/discord/322538954119184384.svg"> | |||
</a> | |||
<a href="https://app.codecov.io/gh/go-gitea/gitea" title="Codecov"> | |||
<img src="https://codecov.io/gh/go-gitea/gitea/branch/main/graph/badge.svg"> | |||
</a> | |||
<a href="https://goreportcard.com/report/code.gitea.io/gitea" title="Go Report Card"> | |||
<img src="https://goreportcard.com/badge/code.gitea.io/gitea"> | |||
</a> | |||
<a href="https://pkg.go.dev/code.gitea.io/gitea" title="GoDoc"> | |||
<img src="https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg"> | |||
</a> | |||
<a href="https://github.com/go-gitea/gitea/releases/latest" title="GitHub release"> | |||
<img src="https://img.shields.io/github/release/go-gitea/gitea.svg"> | |||
</a> | |||
<a href="https://www.codetriage.com/go-gitea/gitea" title="Help Contribute to Open Source"> | |||
<img src="https://www.codetriage.com/go-gitea/gitea/badges/users.svg"> | |||
</a> | |||
<a href="https://opencollective.com/gitea" title="Become a backer/sponsor of gitea"> | |||
<img src="https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen"> | |||
</a> | |||
<a href="https://opensource.org/licenses/MIT" title="License: MIT"> | |||
<img src="https://img.shields.io/badge/License-MIT-blue.svg"> | |||
</a> | |||
<a href="https://gitpod.io/#https://github.com/go-gitea/gitea"> | |||
<img | |||
src="https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod" | |||
alt="Contribute with Gitpod" | |||
/> | |||
</a> | |||
<a href="https://crowdin.com/project/gitea" title="Crowdin"> | |||
<img src="https://badges.crowdin.net/gitea/localized.svg"> | |||
</a> | |||
<a href="https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main" title="TODOs"> | |||
<img src="https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main"> | |||
</a> | |||
</p> | |||
<p align="center"> | |||
<a href="README.md">View this document in English</a> | |||
</p> | |||
# Gitea | |||
[![](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml/badge.svg?branch=main)](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly") | |||
[![](https://img.shields.io/discord/322538954119184384.svg?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea") | |||
[![](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card") | |||
[![](https://pkg.go.dev/badge/code.gitea.io/gitea?status.svg)](https://pkg.go.dev/code.gitea.io/gitea "GoDoc") | |||
[![](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest "GitHub release") | |||
[![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") | |||
[![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") | |||
[![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") | |||
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/go-gitea/gitea) | |||
[![](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea "Crowdin") | |||
[![](https://badgen.net/https/api.tickgit.com/badgen/github.com/go-gitea/gitea/main)](https://www.tickgit.com/browse?repo=github.com/go-gitea/gitea&branch=main "TODOs") | |||
[View this document in English](./README.md) | |||
## 目标 | |||
@@ -0,0 +1,12 @@ | |||
project_id_env: CROWDIN_PROJECT_ID | |||
api_token_env: CROWDIN_KEY | |||
base_path: "." | |||
base_url: "https://api.crowdin.com" | |||
preserve_hierarchy: true | |||
files: | |||
- source: "/options/locale/locale_en-US.ini" | |||
translation: "/options/locale/locale_%locale%.ini" | |||
type: "ini" | |||
skip_untranslated_strings: true | |||
export_only_approved: true | |||
update_option: "update_as_unapproved" |
@@ -441,7 +441,7 @@ INTERNAL_TOKEN = | |||
;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 | |||
;LOGIN_REMEMBER_DAYS = 31 | |||
;; | |||
;; Name of the cookie used to store the current username. | |||
;COOKIE_USERNAME = gitea_awesome | |||
@@ -1485,6 +1485,11 @@ LEVEL = Info | |||
;; - manage_ssh_keys: a user cannot configure ssh keys | |||
;; - manage_gpg_keys: a user cannot configure gpg keys | |||
;USER_DISABLED_FEATURES = | |||
;; Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior. | |||
;; - deletion: a user cannot delete their own account | |||
;; - manage_ssh_keys: a user cannot configure ssh keys | |||
;; - manage_gpg_keys: a user cannot configure gpg keys | |||
;;EXTERNAL_USER_DISABLE_FEATURES = | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||
@@ -2608,7 +2613,7 @@ LEVEL = Info | |||
;ENDLESS_TASK_TIMEOUT = 3h | |||
;; Timeout to cancel the jobs which have waiting status, but haven't been picked by a runner for a long time | |||
;ABANDONED_JOB_TIMEOUT = 24h | |||
;; Strings committers can place inside a commit message to skip executing the corresponding actions workflow | |||
;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow | |||
;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip] | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
@@ -37,7 +37,7 @@ gitea embedded list [--include-vendored] [patterns...] | |||
- 列出所有模板文件,无论在哪个虚拟目录下:`**.tmpl` | |||
- 列出所有邮件模板文件:`templates/mail/**.tmpl` | |||
- 列出 `public/img` 目录下的所有文件:`public/img/**` | |||
列出 `public/assets/img` 目录下的所有文件:`public/assets/img/**` | |||
不要忘记为模式使用引号,因为空格、`*` 和其他字符可能对命令行解释器有特殊含义。 | |||
@@ -49,8 +49,8 @@ gitea embedded list [--include-vendored] [patterns...] | |||
```sh | |||
$ gitea embedded list '**openid**' | |||
public/img/auth/openid_connect.svg | |||
public/img/openid-16x16.png | |||
public/assets/img/auth/openid_connect.svg | |||
public/assets/img/openid-16x16.png | |||
templates/user/auth/finalize_openid.tmpl | |||
templates/user/auth/signin_openid.tmpl | |||
templates/user/auth/signup_openid_connect.tmpl |
@@ -522,13 +522,17 @@ And the following unique queues: | |||
- `deletion`: User cannot delete their own account. | |||
- `manage_ssh_keys`: User cannot configure ssh keys. | |||
- `manage_gpg_keys`: User cannot configure gpg keys. | |||
- `EXTERNAL_USER_DISABLE_FEATURES`: **_empty_**: Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior. | |||
- `deletion`: User cannot delete their own account. | |||
- `manage_ssh_keys`: User cannot configure ssh keys. | |||
- `manage_gpg_keys`: User cannot configure gpg keys. | |||
## Security (`security`) | |||
- `INSTALL_LOCK`: **false**: Controls access to the installation page. When set to "true", the installation page is not accessible. | |||
- `SECRET_KEY`: **\<random at every install\>**: 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`: **_empty_**: 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. | |||
- `LOGIN_REMEMBER_DAYS`: **31**: How long to remember that a user is logged in before requiring relogin (in days). | |||
- `COOKIE_REMEMBER_NAME`: **gitea\_incredible**: Name of cookie used to store authentication | |||
information. | |||
- `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**: Header name for reverse proxy | |||
@@ -590,7 +594,7 @@ And the following unique queues: | |||
## OpenID (`openid`) | |||
- `ENABLE_OPENID_SIGNIN`: **false**: Allow authentication in via OpenID. | |||
- `ENABLE_OPENID_SIGNIN`: **true**: Allow authentication in via OpenID. | |||
- `ENABLE_OPENID_SIGNUP`: **! DISABLE\_REGISTRATION**: Allow registering via OpenID. | |||
- `WHITELISTED_URIS`: **_empty_**: If non-empty, list of POSIX regex patterns matching | |||
OpenID URI's to permit. | |||
@@ -1406,7 +1410,7 @@ PROXY_HOSTS = *.github.com | |||
- `ZOMBIE_TASK_TIMEOUT`: **10m**: Timeout to stop the task which have running status, but haven't been updated for a long time | |||
- `ENDLESS_TASK_TIMEOUT`: **3h**: Timeout to stop the tasks which have running status and continuous updates, but don't end for a long time | |||
- `ABANDONED_JOB_TIMEOUT`: **24h**: Timeout to cancel the jobs which have waiting status, but haven't been picked by a runner for a long time | |||
- `SKIP_WORKFLOW_STRINGS`: **[skip ci],[ci skip],[no ci],[skip actions],[actions skip]**: Strings committers can place inside a commit message to skip executing the corresponding actions workflow | |||
- `SKIP_WORKFLOW_STRINGS`: **[skip ci],[ci skip],[no ci],[skip actions],[actions skip]**: Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow | |||
`DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path. | |||
For example, `uses: actions/checkout@v4` means `https://github.com/actions/checkout@v4` since the value of `DEFAULT_ACTIONS_URL` is `github`. |
@@ -507,7 +507,7 @@ Gitea 创建以下非唯一队列: | |||
- `INSTALL_LOCK`: **false**:控制是否能够访问安装向导页面,设置为 `true` 则禁止访问安装向导页面。 | |||
- `SECRET_KEY`: **\<每次安装时随机生成\>**:全局服务器安全密钥。这个密钥非常重要,如果丢失将无法解密加密的数据(例如 2FA)。 | |||
- `SECRET_KEY_URI`: **_empty_**:与定义 `SECRET_KEY` 不同,此选项可用于使用存储在文件中的密钥(示例值:`file:/etc/gitea/secret_key`)。它不应该像 `SECRET_KEY` 一样容易丢失。 | |||
- `LOGIN_REMEMBER_DAYS`: **7**:Cookie 保存时间,单位为天。 | |||
- `LOGIN_REMEMBER_DAYS`: **31**:在要求重新登录之前,记住用户的登录状态多长时间(以天为单位)。 | |||
- `COOKIE_REMEMBER_NAME`: **gitea\_incredible**:保存自动登录信息的 Cookie 名称。 | |||
- `REVERSE_PROXY_AUTHENTICATION_USER`: **X-WEBAUTH-USER**:反向代理认证的 HTTP 头部名称,用于提供用户信息。 | |||
- `REVERSE_PROXY_AUTHENTICATION_EMAIL`: **X-WEBAUTH-EMAIL**:反向代理认证的 HTTP 头部名称,用于提供邮箱信息。 | |||
@@ -562,7 +562,7 @@ Gitea 创建以下非唯一队列: | |||
## OpenID (`openid`) | |||
- `ENABLE_OPENID_SIGNIN`: **false**:允许通过OpenID进行身份验证。 | |||
- `ENABLE_OPENID_SIGNIN`: **true**:允许通过OpenID进行身份验证。 | |||
- `ENABLE_OPENID_SIGNUP`: **! DISABLE\_REGISTRATION**:允许通过OpenID进行注册。 | |||
- `WHITELISTED_URIS`: **_empty_**:如果非空,是一组匹配OpenID URI的POSIX正则表达式模式,用于允许访问。 | |||
- `BLACKLISTED_URIS`: **_empty_**:如果非空,是一组匹配OpenID URI的POSIX正则表达式模式,用于阻止访问。 |
@@ -163,7 +163,7 @@ clients don't even support HTML, so they show the text version included in the g | |||
If the template fails to render, it will be noticed only at the moment the mail is sent. | |||
A default subject is used if the subject template fails, and whatever was rendered successfully | |||
from the the _mail body_ is used, disregarding the rest. | |||
from the _mail body_ is used, disregarding the rest. | |||
Please check [Gitea's logs](administration/logging-config.md) for error messages in case of trouble. | |||
@@ -17,6 +17,12 @@ menu: | |||
# Repository indexer | |||
## Builtin repository code search without indexer | |||
Users could do repository-level code search without setting up a repository indexer. | |||
The builtin code search is based on the `git grep` command, which is fast and efficient for small repositories. | |||
Better code search support could be achieved by setting up the repository indexer. | |||
## Setting up the repository indexer | |||
Gitea can search through the files of the repositories by enabling this function in your [`app.ini`](administration/config-cheat-sheet.md): |
@@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h | |||
9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided. | |||
10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event. | |||
11. Custom event names are recommended to use `ce-` prefix. | |||
12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-df`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). | |||
12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-word-break`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`). | |||
13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided. | |||
### Accessibility / ARIA | |||
@@ -118,7 +118,7 @@ However, there are still some special cases, so the current guideline is: | |||
### Show/Hide Elements | |||
* Vue components are recommended to use `v-if` and `v-show` to show/hide elements. | |||
* Go template code should use Gitea's `.gt-hidden` and `showElem()/hideElem()/toggleElem()`, see more details in `.gt-hidden`'s comment. | |||
* Go template code should use `.tw-hidden` and `showElem()/hideElem()/toggleElem()`, see more details in `.tw-hidden`'s comment. | |||
### Styles and Attributes in Go HTML Template | |||
@@ -47,13 +47,13 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。 | |||
9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。 | |||
10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。 | |||
11. 推荐使用自定义事件名称前缀`ce-`。 | |||
12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-df`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。 | |||
12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-word-break`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。 | |||
13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。 | |||
### 可访问性 / ARIA | |||
在历史上,Gitea大量使用了可访问性不友好的框架 Fomantic UI。 | |||
Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`aria.md`), | |||
Gitea 使用一些补丁使 Fomantic UI 更具可访问性(参见 `aria.md`), | |||
但仍然存在许多问题需要大量的工作和时间来修复。 | |||
### 框架使用 | |||
@@ -117,7 +117,7 @@ Gitea使用一些补丁使Fomantic UI更具可访问性(参见`aria.js`和`ari | |||
### 显示/隐藏元素 | |||
* 推荐在Vue组件中使用`v-if`和`v-show`来显示/隐藏元素。 | |||
* Go 模板代码应使用 Gitea 的 `.gt-hidden` 和 `showElem()/hideElem()/toggleElem()` 来显示/隐藏元素,请参阅`.gt-hidden`的注释以获取更多详细信息。 | |||
* Go 模板代码应使用 `.tw-hidden` 和 `showElem()/hideElem()/toggleElem()` 来显示/隐藏元素,请参阅`.tw-hidden`的注释以获取更多详细信息。 | |||
### Go HTML 模板中的样式和属性 | |||
@@ -214,7 +214,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200 | |||
### Building and adding SVGs | |||
SVG icons are built using the `make svg` target which compiles the icon sources defined in `build/generate-svg.js` into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory. | |||
SVG icons are built using the `make svg` target which compiles the icon sources into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory. | |||
### Building the Logo | |||
@@ -333,14 +333,9 @@ Documentation for the website is found in `docs/`. If you change this you | |||
can test your changes to ensure that they pass continuous integration using: | |||
```bash | |||
# from the docs directory within Gitea | |||
make trans-copy clean build | |||
make lint-md | |||
``` | |||
You will require a copy of [Hugo](https://gohugo.io/) to run this task. Please | |||
note: this may generate a number of untracked Git objects, which will need to | |||
be cleaned up. | |||
## Visual Studio Code | |||
A `launch.json` and `tasks.json` are provided within `contrib/ide/vscode` for |
@@ -201,7 +201,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200 | |||
### 构建和添加 SVGs | |||
SVG 图标是使用 `make svg` 目标构建的,该目标将 `build/generate-svg.js` 中定义的图标源编译到输出目录 `public/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。 | |||
SVG 图标是使用 `make svg` 命令构建的,该命令将图标资源编译到输出目录 `public/assets/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。 | |||
### 构建 Logo | |||
@@ -307,13 +307,9 @@ TAGS="bindata sqlite sqlite_unlock_notify" make build test-sqlite | |||
该网站的文档位于 `docs/` 中。如果你改变了文档内容,你可以使用以下测试方法进行持续集成: | |||
```bash | |||
# 来自 Gitea 中的 docs 目录 | |||
make trans-copy clean build | |||
make lint-md | |||
``` | |||
运行此任务依赖于 [Hugo](https://gohugo.io/)。请注意:这可能会生成一些未跟踪的 Git 对象, | |||
需要被清理干净。 | |||
## Visual Studio Code | |||
`contrib/ide/vscode` 中为 Visual Studio Code 提供了 `launch.json` 和 `tasks.json`。查看 |
@@ -87,6 +87,9 @@ _Symbols used in table:_ | |||
| Git Blame | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | |||
| Visual comparison of image changes | ✓ | ✘ | ✓ | ? | ? | ? | ✘ | ✘ | | |||
- Gitea has builtin repository-level code search | |||
- Better code search support could be achieved by [using a repository indexer](administration/repo-indexer.md) | |||
## Issue Tracker | |||
| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | RhodeCode EE | |
@@ -1,34 +0,0 @@ | |||
#!/usr/bin/env bash | |||
set -e | |||
# | |||
# This script is used to copy the en-US content to our available locales as a | |||
# fallback to always show all pages when displaying a specific locale that is | |||
# missing some documents to be translated. | |||
# | |||
# Just execute the script without any argument and you will get the missing | |||
# files copied into the content folder. We are calling this script within the CI | |||
# server simply by `make trans-copy`. | |||
# | |||
declare -a LOCALES=( | |||
"fr-fr" | |||
"nl-nl" | |||
"pt-br" | |||
"zh-cn" | |||
"zh-tw" | |||
) | |||
ROOT=$(realpath $(dirname $0)/..) | |||
for SOURCE in $(find ${ROOT}/content -type f -iname *.en-us.md); do | |||
for LOCALE in "${LOCALES[@]}"; do | |||
DEST="${SOURCE%.en-us.md}.${LOCALE}.md" | |||
if [[ ! -f ${DEST} ]]; then | |||
cp ${SOURCE} ${DEST} | |||
sed -i.bak "s/en\-us/${LOCALE}/g" ${DEST} | |||
rm ${DEST}.bak | |||
fi | |||
done | |||
done |
@@ -1,27 +1,27 @@ | |||
module code.gitea.io/gitea | |||
go 1.21 | |||
go 1.22 | |||
require ( | |||
code.gitea.io/actions-proto-go v0.3.1 | |||
code.gitea.io/actions-proto-go v0.4.0 | |||
code.gitea.io/gitea-vet v0.2.3 | |||
code.gitea.io/sdk/gitea v0.17.1 | |||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 | |||
gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 | |||
connectrpc.com/connect v1.15.0 | |||
gitea.com/go-chi/binding v0.0.0-20240316035258-17450c5f3028 | |||
gitea.com/go-chi/cache v0.2.0 | |||
gitea.com/go-chi/captcha v0.0.0-20230415143339-2c0754df4384 | |||
gitea.com/go-chi/session v0.0.0-20230613035928-39541325faa3 | |||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 | |||
gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96 | |||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 | |||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 | |||
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 | |||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 | |||
github.com/NYTimes/gziphandler v1.1.1 | |||
github.com/PuerkitoBio/goquery v1.8.1 | |||
github.com/alecthomas/chroma/v2 v2.12.0 | |||
github.com/PuerkitoBio/goquery v1.9.1 | |||
github.com/alecthomas/chroma/v2 v2.13.0 | |||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb | |||
github.com/blevesearch/bleve/v2 v2.3.10 | |||
github.com/bufbuild/connect-go v1.10.0 | |||
github.com/buildkite/terminal-to-html/v3 v3.10.1 | |||
github.com/buildkite/terminal-to-html/v3 v3.11.0 | |||
github.com/caddyserver/certmagic v0.20.0 | |||
github.com/chi-middleware/proxy v1.1.1 | |||
github.com/denisenkom/go-mssqldb v0.12.3 | |||
@@ -30,33 +30,33 @@ require ( | |||
github.com/djherbis/nio/v3 v3.0.1 | |||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 | |||
github.com/dustin/go-humanize v1.0.1 | |||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.0 | |||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.1 | |||
github.com/emersion/go-imap v1.2.1 | |||
github.com/emirpasic/gods v1.18.1 | |||
github.com/ethantkoenig/rupture v1.0.1 | |||
github.com/felixge/fgprof v0.9.3 | |||
github.com/felixge/fgprof v0.9.4 | |||
github.com/fsnotify/fsnotify v1.7.0 | |||
github.com/gliderlabs/ssh v0.3.6 | |||
github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 | |||
github.com/go-ap/activitypub v0.0.0-20240316125321-b61fd6a83225 | |||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 | |||
github.com/go-chi/chi/v5 v5.0.11 | |||
github.com/go-chi/chi/v5 v5.0.12 | |||
github.com/go-chi/cors v1.2.1 | |||
github.com/go-co-op/gocron v1.37.0 | |||
github.com/go-enry/go-enry/v2 v2.8.6 | |||
github.com/go-enry/go-enry/v2 v2.8.7 | |||
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e | |||
github.com/go-git/go-billy/v5 v5.5.0 | |||
github.com/go-git/go-git/v5 v5.11.0 | |||
github.com/go-ldap/ldap/v3 v3.4.6 | |||
github.com/go-sql-driver/mysql v1.7.1 | |||
github.com/go-sql-driver/mysql v1.8.0 | |||
github.com/go-swagger/go-swagger v0.30.5 | |||
github.com/go-testfixtures/testfixtures/v3 v3.10.0 | |||
github.com/go-webauthn/webauthn v0.10.0 | |||
github.com/go-webauthn/webauthn v0.10.2 | |||
github.com/gobwas/glob v0.2.3 | |||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f | |||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 | |||
github.com/golang-jwt/jwt/v5 v5.2.0 | |||
github.com/golang-jwt/jwt/v5 v5.2.1 | |||
github.com/google/go-github/v57 v57.0.0 | |||
github.com/google/pprof v0.0.0-20240117000934-35fc243c5815 | |||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 | |||
github.com/google/uuid v1.6.0 | |||
github.com/gorilla/feeds v1.1.2 | |||
github.com/gorilla/sessions v1.2.2 | |||
@@ -64,55 +64,55 @@ require ( | |||
github.com/hashicorp/golang-lru/v2 v2.0.7 | |||
github.com/huandu/xstrings v1.4.0 | |||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 | |||
github.com/jhillyerd/enmime v1.1.0 | |||
github.com/jhillyerd/enmime v1.2.0 | |||
github.com/json-iterator/go v1.1.12 | |||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 | |||
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 | |||
github.com/klauspost/compress v1.17.4 | |||
github.com/klauspost/cpuid/v2 v2.2.6 | |||
github.com/klauspost/compress v1.17.7 | |||
github.com/klauspost/cpuid/v2 v2.2.7 | |||
github.com/lib/pq v1.10.9 | |||
github.com/markbates/goth v1.78.0 | |||
github.com/markbates/goth v1.79.0 | |||
github.com/mattn/go-isatty v0.0.20 | |||
github.com/mattn/go-sqlite3 v1.14.22 | |||
github.com/meilisearch/meilisearch-go v0.26.1 | |||
github.com/meilisearch/meilisearch-go v0.26.2 | |||
github.com/mholt/archiver/v3 v3.5.1 | |||
github.com/microcosm-cc/bluemonday v1.0.26 | |||
github.com/minio/minio-go/v7 v7.0.66 | |||
github.com/minio/minio-go/v7 v7.0.69 | |||
github.com/msteinert/pam v1.2.0 | |||
github.com/nektos/act v0.2.52 | |||
github.com/niklasfasching/go-org v1.7.0 | |||
github.com/olivere/elastic/v7 v7.0.32 | |||
github.com/opencontainers/go-digest v1.0.0 | |||
github.com/opencontainers/image-spec v1.1.0-rc6 | |||
github.com/opencontainers/image-spec v1.1.0 | |||
github.com/pkg/errors v0.9.1 | |||
github.com/pquerna/otp v1.4.0 | |||
github.com/prometheus/client_golang v1.18.0 | |||
github.com/prometheus/client_golang v1.19.0 | |||
github.com/quasoft/websspi v1.1.2 | |||
github.com/redis/go-redis/v9 v9.4.0 | |||
github.com/redis/go-redis/v9 v9.5.1 | |||
github.com/robfig/cron/v3 v3.0.1 | |||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 | |||
github.com/sassoftware/go-rpmutils v0.2.1-0.20240124161140-277b154961dd | |||
github.com/sassoftware/go-rpmutils v0.3.0 | |||
github.com/sergi/go-diff v1.3.1 | |||
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 | |||
github.com/stretchr/testify v1.8.4 | |||
github.com/stretchr/testify v1.9.0 | |||
github.com/syndtr/goleveldb v1.0.0 | |||
github.com/tstranex/u2f v1.0.0 | |||
github.com/ulikunitz/xz v0.5.11 | |||
github.com/urfave/cli/v2 v2.27.1 | |||
github.com/xanzy/go-gitlab v0.96.0 | |||
github.com/xanzy/go-gitlab v0.100.0 | |||
github.com/xeipuuv/gojsonschema v1.2.0 | |||
github.com/yohcop/openid-go v1.0.1 | |||
github.com/yuin/goldmark v1.6.0 | |||
github.com/yuin/goldmark v1.7.0 | |||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc | |||
github.com/yuin/goldmark-meta v1.1.0 | |||
golang.org/x/crypto v0.18.0 | |||
golang.org/x/crypto v0.21.0 | |||
golang.org/x/image v0.15.0 | |||
golang.org/x/net v0.20.0 | |||
golang.org/x/oauth2 v0.16.0 | |||
golang.org/x/sys v0.16.0 | |||
golang.org/x/net v0.22.0 | |||
golang.org/x/oauth2 v0.18.0 | |||
golang.org/x/sys v0.18.0 | |||
golang.org/x/text v0.14.0 | |||
golang.org/x/tools v0.17.0 | |||
google.golang.org/grpc v1.60.1 | |||
golang.org/x/tools v0.19.0 | |||
google.golang.org/grpc v1.62.1 | |||
google.golang.org/protobuf v1.33.0 | |||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df | |||
gopkg.in/ini.v1 v1.67.0 | |||
@@ -120,23 +120,24 @@ require ( | |||
mvdan.cc/xurls/v2 v2.5.0 | |||
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 | |||
xorm.io/builder v0.3.13 | |||
xorm.io/xorm v1.3.7 | |||
xorm.io/xorm v1.3.8 | |||
) | |||
require ( | |||
cloud.google.com/go/compute v1.23.3 // indirect | |||
cloud.google.com/go/compute v1.25.1 // indirect | |||
cloud.google.com/go/compute/metadata v0.2.3 // indirect | |||
dario.cat/mergo v1.0.0 // indirect | |||
filippo.io/edwards25519 v1.1.0 // indirect | |||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect | |||
github.com/ClickHouse/ch-go v0.61.1 // indirect | |||
github.com/ClickHouse/clickhouse-go/v2 v2.18.0 // indirect | |||
github.com/ClickHouse/ch-go v0.61.5 // indirect | |||
github.com/ClickHouse/clickhouse-go/v2 v2.22.0 // indirect | |||
github.com/DataDog/zstd v1.5.5 // indirect | |||
github.com/Masterminds/goutils v1.1.1 // indirect | |||
github.com/Masterminds/semver/v3 v3.2.1 // indirect | |||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect | |||
github.com/Microsoft/go-winio v0.6.1 // indirect | |||
github.com/ProtonMail/go-crypto v1.0.0 // indirect | |||
github.com/RoaringBitmap/roaring v1.7.0 // indirect | |||
github.com/RoaringBitmap/roaring v1.9.0 // indirect | |||
github.com/andybalholm/brotli v1.1.0 // indirect | |||
github.com/andybalholm/cascadia v1.3.2 // indirect | |||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect | |||
@@ -144,12 +145,12 @@ require ( | |||
github.com/aymerick/douceur v0.2.0 // indirect | |||
github.com/beorn7/perks v1.0.1 // indirect | |||
github.com/bits-and-blooms/bitset v1.13.0 // indirect | |||
github.com/blevesearch/bleve_index_api v1.1.5 // indirect | |||
github.com/blevesearch/geo v0.1.19 // indirect | |||
github.com/blevesearch/bleve_index_api v1.1.6 // indirect | |||
github.com/blevesearch/geo v0.1.20 // indirect | |||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect | |||
github.com/blevesearch/gtreap v0.1.1 // indirect | |||
github.com/blevesearch/mmap-go v1.0.4 // indirect | |||
github.com/blevesearch/scorch_segment_api/v2 v2.2.6 // indirect | |||
github.com/blevesearch/scorch_segment_api/v2 v2.2.8 // indirect | |||
github.com/blevesearch/segment v0.9.1 // indirect | |||
github.com/blevesearch/snowballstem v0.9.0 // indirect | |||
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect | |||
@@ -165,43 +166,43 @@ require ( | |||
github.com/cespare/xxhash/v2 v2.2.0 // indirect | |||
github.com/cloudflare/circl v1.3.7 // indirect | |||
github.com/couchbase/go-couchbase v0.1.1 // indirect | |||
github.com/couchbase/gomemcached v0.3.0 // indirect | |||
github.com/couchbase/gomemcached v0.3.1 // indirect | |||
github.com/couchbase/goutils v0.1.2 // indirect | |||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect | |||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect | |||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect | |||
github.com/davidmz/go-pageant v1.0.2 // indirect | |||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | |||
github.com/dlclark/regexp2 v1.10.0 // indirect | |||
github.com/dlclark/regexp2 v1.11.0 // indirect | |||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect | |||
github.com/fatih/color v1.16.0 // indirect | |||
github.com/felixge/httpsnoop v1.0.4 // indirect | |||
github.com/fxamacker/cbor/v2 v2.5.0 // indirect | |||
github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect | |||
github.com/fxamacker/cbor/v2 v2.6.0 // indirect | |||
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 // indirect | |||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect | |||
github.com/go-enry/go-oniguruma v1.2.1 // indirect | |||
github.com/go-faster/city v1.0.1 // indirect | |||
github.com/go-faster/errors v0.7.1 // indirect | |||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect | |||
github.com/go-openapi/analysis v0.22.2 // indirect | |||
github.com/go-openapi/errors v0.21.0 // indirect | |||
github.com/go-openapi/inflect v0.19.0 // indirect | |||
github.com/go-openapi/jsonpointer v0.20.2 // indirect | |||
github.com/go-openapi/jsonreference v0.20.4 // indirect | |||
github.com/go-openapi/loads v0.21.5 // indirect | |||
github.com/go-openapi/runtime v0.26.2 // indirect | |||
github.com/go-openapi/spec v0.20.14 // indirect | |||
github.com/go-openapi/strfmt v0.22.0 // indirect | |||
github.com/go-openapi/swag v0.22.7 // indirect | |||
github.com/go-openapi/validate v0.22.6 // indirect | |||
github.com/go-webauthn/x v0.1.6 // indirect | |||
github.com/go-openapi/analysis v0.23.0 // indirect | |||
github.com/go-openapi/errors v0.22.0 // indirect | |||
github.com/go-openapi/inflect v0.21.0 // indirect | |||
github.com/go-openapi/jsonpointer v0.21.0 // indirect | |||
github.com/go-openapi/jsonreference v0.21.0 // indirect | |||
github.com/go-openapi/loads v0.22.0 // indirect | |||
github.com/go-openapi/runtime v0.28.0 // indirect | |||
github.com/go-openapi/spec v0.21.0 // indirect | |||
github.com/go-openapi/strfmt v0.23.0 // indirect | |||
github.com/go-openapi/swag v0.23.0 // indirect | |||
github.com/go-openapi/validate v0.24.0 // indirect | |||
github.com/go-webauthn/x v0.1.9 // indirect | |||
github.com/goccy/go-json v0.10.2 // indirect | |||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect | |||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect | |||
github.com/golang-sql/sqlexp v0.1.0 // indirect | |||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect | |||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect | |||
github.com/golang/protobuf v1.5.3 // indirect | |||
github.com/golang/protobuf v1.5.4 // indirect | |||
github.com/golang/snappy v0.0.4 // indirect | |||
github.com/google/go-querystring v1.1.0 // indirect | |||
github.com/google/go-tpm v0.9.0 // indirect | |||
@@ -246,11 +247,11 @@ require ( | |||
github.com/pierrec/lz4/v4 v4.1.21 // indirect | |||
github.com/pjbgf/sha1cd v0.3.0 // indirect | |||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | |||
github.com/prometheus/client_model v0.5.0 // indirect | |||
github.com/prometheus/common v0.46.0 // indirect | |||
github.com/prometheus/procfs v0.12.0 // indirect | |||
github.com/rhysd/actionlint v1.6.26 // indirect | |||
github.com/rivo/uniseg v0.4.4 // indirect | |||
github.com/prometheus/client_model v0.6.0 // indirect | |||
github.com/prometheus/common v0.50.0 // indirect | |||
github.com/prometheus/procfs v0.13.0 // indirect | |||
github.com/rhysd/actionlint v1.6.27 // indirect | |||
github.com/rivo/uniseg v0.4.7 // indirect | |||
github.com/rogpeppe/go-internal v1.12.0 // indirect | |||
github.com/rs/xid v1.5.0 // indirect | |||
github.com/russross/blackfriday/v2 v2.1.0 // indirect | |||
@@ -260,7 +261,7 @@ require ( | |||
github.com/shopspring/decimal v1.3.1 // indirect | |||
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect | |||
github.com/sirupsen/logrus v1.9.3 // indirect | |||
github.com/skeema/knownhosts v1.2.1 // indirect | |||
github.com/skeema/knownhosts v1.2.2 // indirect | |||
github.com/sourcegraph/conc v0.3.0 // indirect | |||
github.com/spf13/afero v1.11.0 // indirect | |||
github.com/spf13/cast v1.6.0 // indirect | |||
@@ -271,28 +272,28 @@ require ( | |||
github.com/toqueteos/webbrowser v1.2.0 // indirect | |||
github.com/unknwon/com v1.0.1 // indirect | |||
github.com/valyala/bytebufferpool v1.0.0 // indirect | |||
github.com/valyala/fasthttp v1.51.0 // indirect | |||
github.com/valyala/fasthttp v1.52.0 // indirect | |||
github.com/valyala/fastjson v1.6.4 // indirect | |||
github.com/x448/float16 v0.8.4 // indirect | |||
github.com/xanzy/ssh-agent v0.3.3 // indirect | |||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect | |||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect | |||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect | |||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect | |||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect | |||
github.com/zeebo/blake3 v0.2.3 // indirect | |||
go.etcd.io/bbolt v1.3.8 // indirect | |||
go.mongodb.org/mongo-driver v1.13.1 // indirect | |||
go.opentelemetry.io/otel v1.22.0 // indirect | |||
go.opentelemetry.io/otel/trace v1.22.0 // indirect | |||
go.etcd.io/bbolt v1.3.9 // indirect | |||
go.mongodb.org/mongo-driver v1.14.0 // indirect | |||
go.opentelemetry.io/otel v1.24.0 // indirect | |||
go.opentelemetry.io/otel/trace v1.24.0 // indirect | |||
go.uber.org/atomic v1.11.0 // indirect | |||
go.uber.org/multierr v1.11.0 // indirect | |||
go.uber.org/zap v1.26.0 // indirect | |||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect | |||
golang.org/x/mod v0.14.0 // indirect | |||
go.uber.org/zap v1.27.0 // indirect | |||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect | |||
golang.org/x/mod v0.16.0 // indirect | |||
golang.org/x/sync v0.6.0 // indirect | |||
golang.org/x/time v0.5.0 // indirect | |||
google.golang.org/appengine v1.6.8 // indirect | |||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect | |||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect | |||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect | |||
gopkg.in/warnings.v0 v0.1.2 // indirect | |||
gopkg.in/yaml.v2 v2.4.0 // indirect |
@@ -170,15 +170,16 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err | |||
return err | |||
} | |||
// CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow. | |||
func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { | |||
// Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'. | |||
// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event. | |||
// It's useful when a new run is triggered, and all previous runs needn't be continued anymore. | |||
func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { | |||
// Find all runs in the specified repository, reference, and workflow with non-final status | |||
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{ | |||
RepoID: repoID, | |||
Ref: ref, | |||
WorkflowID: workflowID, | |||
TriggerEvent: event, | |||
Status: []Status{StatusRunning, StatusWaiting}, | |||
Status: []Status{StatusRunning, StatusWaiting, StatusBlocked}, | |||
}) | |||
if err != nil { | |||
return err |
@@ -127,14 +127,14 @@ func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) er | |||
return fmt.Errorf("DeleteCronTaskByRepo: %v", err) | |||
} | |||
// cancel running cron jobs of this repository and delete old schedules | |||
if err := CancelRunningJobs( | |||
if err := CancelPreviousJobs( | |||
ctx, | |||
repo.ID, | |||
repo.DefaultBranch, | |||
"", | |||
webhook_module.HookEventSchedule, | |||
); err != nil { | |||
return fmt.Errorf("CancelRunningJobs: %v", err) | |||
return fmt.Errorf("CancelPreviousJobs: %v", err) | |||
} | |||
return nil | |||
} |
@@ -6,13 +6,11 @@ package actions | |||
import ( | |||
"context" | |||
"errors" | |||
"fmt" | |||
"strings" | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/timeutil" | |||
"code.gitea.io/gitea/modules/util" | |||
"xorm.io/builder" | |||
) | |||
@@ -55,24 +53,24 @@ type FindVariablesOpts struct { | |||
db.ListOptions | |||
OwnerID int64 | |||
RepoID int64 | |||
Name string | |||
} | |||
func (opts FindVariablesOpts) ToConds() builder.Cond { | |||
cond := builder.NewCond() | |||
// Since we now support instance-level variables, | |||
// there is no need to check for null values for `owner_id` and `repo_id` | |||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) | |||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) | |||
if opts.Name != "" { | |||
cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)}) | |||
} | |||
return cond | |||
} | |||
func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) { | |||
var variable ActionVariable | |||
has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable) | |||
if err != nil { | |||
return nil, err | |||
} else if !has { | |||
return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist) | |||
} | |||
return &variable, nil | |||
func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) { | |||
return db.Find[ActionVariable](ctx, opts) | |||
} | |||
func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) { | |||
@@ -84,6 +82,13 @@ func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) | |||
return count != 0, err | |||
} | |||
func DeleteVariable(ctx context.Context, id int64) error { | |||
if _, err := db.DeleteByID[ActionVariable](ctx, id); err != nil { | |||
return err | |||
} | |||
return nil | |||
} | |||
func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) { | |||
variables := map[string]string{} | |||
@@ -148,6 +148,7 @@ type Action struct { | |||
Repo *repo_model.Repository `xorm:"-"` | |||
CommentID int64 `xorm:"INDEX"` | |||
Comment *issues_model.Comment `xorm:"-"` | |||
Issue *issues_model.Issue `xorm:"-"` // get the issue id from content | |||
IsDeleted bool `xorm:"NOT NULL DEFAULT false"` | |||
RefName string | |||
IsPrivate bool `xorm:"NOT NULL DEFAULT false"` | |||
@@ -290,11 +291,6 @@ func (a *Action) GetRepoAbsoluteLink(ctx context.Context) string { | |||
return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx)) | |||
} | |||
// GetCommentHTMLURL returns link to action comment. | |||
func (a *Action) GetCommentHTMLURL(ctx context.Context) string { | |||
return a.getCommentHTMLURL(ctx) | |||
} | |||
func (a *Action) loadComment(ctx context.Context) (err error) { | |||
if a.CommentID == 0 || a.Comment != nil { | |||
return nil | |||
@@ -303,7 +299,8 @@ func (a *Action) loadComment(ctx context.Context) (err error) { | |||
return err | |||
} | |||
func (a *Action) getCommentHTMLURL(ctx context.Context) string { | |||
// GetCommentHTMLURL returns link to action comment. | |||
func (a *Action) GetCommentHTMLURL(ctx context.Context) string { | |||
if a == nil { | |||
return "#" | |||
} | |||
@@ -311,34 +308,19 @@ func (a *Action) getCommentHTMLURL(ctx context.Context) string { | |||
if a.Comment != nil { | |||
return a.Comment.HTMLURL(ctx) | |||
} | |||
if len(a.GetIssueInfos()) == 0 { | |||
return "#" | |||
} | |||
// Return link to issue | |||
issueIDString := a.GetIssueInfos()[0] | |||
issueID, err := strconv.ParseInt(issueIDString, 10, 64) | |||
if err != nil { | |||
return "#" | |||
} | |||
issue, err := issues_model.GetIssueByID(ctx, issueID) | |||
if err != nil { | |||
if err := a.LoadIssue(ctx); err != nil || a.Issue == nil { | |||
return "#" | |||
} | |||
if err = issue.LoadRepo(ctx); err != nil { | |||
if err := a.Issue.LoadRepo(ctx); err != nil { | |||
return "#" | |||
} | |||
return issue.HTMLURL() | |||
return a.Issue.HTMLURL() | |||
} | |||
// GetCommentLink returns link to action comment. | |||
func (a *Action) GetCommentLink(ctx context.Context) string { | |||
return a.getCommentLink(ctx) | |||
} | |||
func (a *Action) getCommentLink(ctx context.Context) string { | |||
if a == nil { | |||
return "#" | |||
} | |||
@@ -346,26 +328,15 @@ func (a *Action) getCommentLink(ctx context.Context) string { | |||
if a.Comment != nil { | |||
return a.Comment.Link(ctx) | |||
} | |||
if len(a.GetIssueInfos()) == 0 { | |||
return "#" | |||
} | |||
// Return link to issue | |||
issueIDString := a.GetIssueInfos()[0] | |||
issueID, err := strconv.ParseInt(issueIDString, 10, 64) | |||
if err != nil { | |||
return "#" | |||
} | |||
issue, err := issues_model.GetIssueByID(ctx, issueID) | |||
if err != nil { | |||
if err := a.LoadIssue(ctx); err != nil || a.Issue == nil { | |||
return "#" | |||
} | |||
if err = issue.LoadRepo(ctx); err != nil { | |||
if err := a.Issue.LoadRepo(ctx); err != nil { | |||
return "#" | |||
} | |||
return issue.Link() | |||
return a.Issue.Link() | |||
} | |||
// GetBranch returns the action's repository branch. | |||
@@ -393,6 +364,10 @@ func (a *Action) GetCreate() time.Time { | |||
return a.CreatedUnix.AsTime() | |||
} | |||
func (a *Action) IsIssueEvent() bool { | |||
return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request") | |||
} | |||
// GetIssueInfos returns a list of associated information with the action. | |||
func (a *Action) GetIssueInfos() []string { | |||
// make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length | |||
@@ -403,27 +378,52 @@ func (a *Action) GetIssueInfos() []string { | |||
return ret | |||
} | |||
func (a *Action) getIssueIndex() int64 { | |||
infos := a.GetIssueInfos() | |||
if len(infos) == 0 { | |||
return 0 | |||
} | |||
index, _ := strconv.ParseInt(infos[0], 10, 64) | |||
return index | |||
} | |||
func (a *Action) LoadIssue(ctx context.Context) error { | |||
if a.Issue != nil { | |||
return nil | |||
} | |||
if index := a.getIssueIndex(); index > 0 { | |||
issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) | |||
if err != nil { | |||
return err | |||
} | |||
a.Issue = issue | |||
a.Issue.Repo = a.Repo | |||
} | |||
return nil | |||
} | |||
// GetIssueTitle returns the title of first issue associated with the action. | |||
func (a *Action) GetIssueTitle(ctx context.Context) string { | |||
index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64) | |||
issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) | |||
if err != nil { | |||
log.Error("GetIssueByIndex: %v", err) | |||
return "500 when get issue" | |||
if err := a.LoadIssue(ctx); err != nil { | |||
log.Error("LoadIssue: %v", err) | |||
return "<500 when get issue>" | |||
} | |||
if a.Issue == nil { | |||
return "<Issue not found>" | |||
} | |||
return issue.Title | |||
return a.Issue.Title | |||
} | |||
// GetIssueContent returns the content of first issue associated with | |||
// this action. | |||
// GetIssueContent returns the content of first issue associated with this action. | |||
func (a *Action) GetIssueContent(ctx context.Context) string { | |||
index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64) | |||
issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index) | |||
if err != nil { | |||
log.Error("GetIssueByIndex: %v", err) | |||
return "500 when get issue" | |||
if err := a.LoadIssue(ctx); err != nil { | |||
log.Error("LoadIssue: %v", err) | |||
return "<500 when get issue>" | |||
} | |||
if a.Issue == nil { | |||
return "<Content not found>" | |||
} | |||
return issue.Content | |||
return a.Issue.Content | |||
} | |||
// GetFeedsOptions options for retrieving feeds | |||
@@ -463,7 +463,7 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err | |||
return nil, 0, fmt.Errorf("FindAndCount: %w", err) | |||
} | |||
if err := ActionList(actions).loadAttributes(ctx); err != nil { | |||
if err := ActionList(actions).LoadAttributes(ctx); err != nil { | |||
return nil, 0, fmt.Errorf("LoadAttributes: %w", err) | |||
} | |||
@@ -6,11 +6,16 @@ package activities | |||
import ( | |||
"context" | |||
"fmt" | |||
"strconv" | |||
"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/container" | |||
"code.gitea.io/gitea/modules/util" | |||
"xorm.io/builder" | |||
) | |||
// ActionList defines a list of actions | |||
@@ -24,7 +29,7 @@ func (actions ActionList) getUserIDs() []int64 { | |||
return userIDs.Values() | |||
} | |||
func (actions ActionList) loadUsers(ctx context.Context) (map[int64]*user_model.User, error) { | |||
func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_model.User, error) { | |||
if len(actions) == 0 { | |||
return nil, nil | |||
} | |||
@@ -52,7 +57,7 @@ func (actions ActionList) getRepoIDs() []int64 { | |||
return repoIDs.Values() | |||
} | |||
func (actions ActionList) loadRepositories(ctx context.Context) error { | |||
func (actions ActionList) LoadRepositories(ctx context.Context) error { | |||
if len(actions) == 0 { | |||
return nil | |||
} | |||
@@ -63,11 +68,11 @@ func (actions ActionList) loadRepositories(ctx context.Context) error { | |||
if err != nil { | |||
return fmt.Errorf("find repository: %w", err) | |||
} | |||
for _, action := range actions { | |||
action.Repo = repoMaps[action.RepoID] | |||
} | |||
return nil | |||
repos := repo_model.RepositoryList(util.ValuesOfMap(repoMaps)) | |||
return repos.LoadUnits(ctx) | |||
} | |||
func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]*user_model.User) (err error) { | |||
@@ -75,37 +80,124 @@ func (actions ActionList) loadRepoOwner(ctx context.Context, userMap map[int64]* | |||
userMap = make(map[int64]*user_model.User) | |||
} | |||
userSet := make(container.Set[int64], len(actions)) | |||
for _, action := range actions { | |||
if action.Repo == nil { | |||
continue | |||
} | |||
repoOwner, ok := userMap[action.Repo.OwnerID] | |||
if !ok { | |||
repoOwner, err = user_model.GetUserByID(ctx, action.Repo.OwnerID) | |||
if err != nil { | |||
if user_model.IsErrUserNotExist(err) { | |||
continue | |||
} | |||
return err | |||
} | |||
userMap[repoOwner.ID] = repoOwner | |||
if _, ok := userMap[action.Repo.OwnerID]; !ok { | |||
userSet.Add(action.Repo.OwnerID) | |||
} | |||
} | |||
if err := db.GetEngine(ctx). | |||
In("id", userSet.Values()). | |||
Find(&userMap); err != nil { | |||
return fmt.Errorf("find user: %w", err) | |||
} | |||
for _, action := range actions { | |||
if action.Repo != nil { | |||
action.Repo.Owner = userMap[action.Repo.OwnerID] | |||
} | |||
action.Repo.Owner = repoOwner | |||
} | |||
return nil | |||
} | |||
// loadAttributes loads all attributes | |||
func (actions ActionList) loadAttributes(ctx context.Context) error { | |||
userMap, err := actions.loadUsers(ctx) | |||
// LoadAttributes loads all attributes | |||
func (actions ActionList) LoadAttributes(ctx context.Context) error { | |||
// the load sequence cannot be changed because of the dependencies | |||
userMap, err := actions.LoadActUsers(ctx) | |||
if err != nil { | |||
return err | |||
} | |||
if err := actions.loadRepositories(ctx); err != nil { | |||
if err := actions.LoadRepositories(ctx); err != nil { | |||
return err | |||
} | |||
if err := actions.loadRepoOwner(ctx, userMap); err != nil { | |||
return err | |||
} | |||
if err := actions.LoadIssues(ctx); err != nil { | |||
return err | |||
} | |||
return actions.LoadComments(ctx) | |||
} | |||
func (actions ActionList) LoadComments(ctx context.Context) error { | |||
if len(actions) == 0 { | |||
return nil | |||
} | |||
commentIDs := make([]int64, 0, len(actions)) | |||
for _, action := range actions { | |||
if action.CommentID > 0 { | |||
commentIDs = append(commentIDs, action.CommentID) | |||
} | |||
} | |||
return actions.loadRepoOwner(ctx, userMap) | |||
commentsMap := make(map[int64]*issues_model.Comment, len(commentIDs)) | |||
if err := db.GetEngine(ctx).In("id", commentIDs).Find(&commentsMap); err != nil { | |||
return fmt.Errorf("find comment: %w", err) | |||
} | |||
for _, action := range actions { | |||
if action.CommentID > 0 { | |||
action.Comment = commentsMap[action.CommentID] | |||
if action.Comment != nil { | |||
action.Comment.Issue = action.Issue | |||
} | |||
} | |||
} | |||
return nil | |||
} | |||
func (actions ActionList) LoadIssues(ctx context.Context) error { | |||
if len(actions) == 0 { | |||
return nil | |||
} | |||
conditions := builder.NewCond() | |||
issueNum := 0 | |||
for _, action := range actions { | |||
if action.IsIssueEvent() { | |||
infos := action.GetIssueInfos() | |||
if len(infos) == 0 { | |||
continue | |||
} | |||
index, _ := strconv.ParseInt(infos[0], 10, 64) | |||
if index > 0 { | |||
conditions = conditions.Or(builder.Eq{ | |||
"repo_id": action.RepoID, | |||
"`index`": index, | |||
}) | |||
issueNum++ | |||
} | |||
} | |||
} | |||
if !conditions.IsValid() { | |||
return nil | |||
} | |||
issuesMap := make(map[string]*issues_model.Issue, issueNum) | |||
issues := make([]*issues_model.Issue, 0, issueNum) | |||
if err := db.GetEngine(ctx).Where(conditions).Find(&issues); err != nil { | |||
return fmt.Errorf("find issue: %w", err) | |||
} | |||
for _, issue := range issues { | |||
issuesMap[fmt.Sprintf("%d-%d", issue.RepoID, issue.Index)] = issue | |||
} | |||
for _, action := range actions { | |||
if !action.IsIssueEvent() { | |||
continue | |||
} | |||
if index := action.getIssueIndex(); index > 0 { | |||
if issue, ok := issuesMap[fmt.Sprintf("%d-%d", action.RepoID, index)]; ok { | |||
action.Issue = issue | |||
action.Issue.Repo = action.Repo | |||
} | |||
} | |||
} | |||
return nil | |||
} |
@@ -12,12 +12,8 @@ import ( | |||
"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/container" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/timeutil" | |||
@@ -79,53 +75,6 @@ func init() { | |||
db.RegisterModel(new(Notification)) | |||
} | |||
// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored. | |||
type FindNotificationOptions struct { | |||
db.ListOptions | |||
UserID int64 | |||
RepoID int64 | |||
IssueID int64 | |||
Status []NotificationStatus | |||
Source []NotificationSource | |||
UpdatedAfterUnix int64 | |||
UpdatedBeforeUnix int64 | |||
} | |||
// ToCond will convert each condition into a xorm-Cond | |||
func (opts FindNotificationOptions) ToConds() builder.Cond { | |||
cond := builder.NewCond() | |||
if opts.UserID != 0 { | |||
cond = cond.And(builder.Eq{"notification.user_id": opts.UserID}) | |||
} | |||
if opts.RepoID != 0 { | |||
cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID}) | |||
} | |||
if opts.IssueID != 0 { | |||
cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID}) | |||
} | |||
if len(opts.Status) > 0 { | |||
if len(opts.Status) == 1 { | |||
cond = cond.And(builder.Eq{"notification.status": opts.Status[0]}) | |||
} else { | |||
cond = cond.And(builder.In("notification.status", opts.Status)) | |||
} | |||
} | |||
if len(opts.Source) > 0 { | |||
cond = cond.And(builder.In("notification.source", opts.Source)) | |||
} | |||
if opts.UpdatedAfterUnix != 0 { | |||
cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix}) | |||
} | |||
if opts.UpdatedBeforeUnix != 0 { | |||
cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix}) | |||
} | |||
return cond | |||
} | |||
func (opts FindNotificationOptions) ToOrders() string { | |||
return "notification.updated_unix DESC" | |||
} | |||
// CreateRepoTransferNotification creates notification for the user a repository was transferred to | |||
func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error { | |||
return db.WithTx(ctx, func(ctx context.Context) error { | |||
@@ -159,109 +108,6 @@ func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_mo | |||
}) | |||
} | |||
// CreateOrUpdateIssueNotifications creates an issue notification | |||
// for each watcher, or updates it if already exists | |||
// receiverID > 0 just send to receiver, else send to all watcher | |||
func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { | |||
ctx, committer, err := db.TxContext(ctx) | |||
if err != nil { | |||
return err | |||
} | |||
defer committer.Close() | |||
if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil { | |||
return err | |||
} | |||
return committer.Commit() | |||
} | |||
func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { | |||
// init | |||
var toNotify container.Set[int64] | |||
notifications, err := db.Find[Notification](ctx, FindNotificationOptions{ | |||
IssueID: issueID, | |||
}) | |||
if err != nil { | |||
return err | |||
} | |||
issue, err := issues_model.GetIssueByID(ctx, issueID) | |||
if err != nil { | |||
return err | |||
} | |||
if receiverID > 0 { | |||
toNotify = make(container.Set[int64], 1) | |||
toNotify.Add(receiverID) | |||
} else { | |||
toNotify = make(container.Set[int64], 32) | |||
issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true) | |||
if err != nil { | |||
return err | |||
} | |||
toNotify.AddMultiple(issueWatches...) | |||
if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) { | |||
repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID) | |||
if err != nil { | |||
return err | |||
} | |||
toNotify.AddMultiple(repoWatches...) | |||
} | |||
issueParticipants, err := issue.GetParticipantIDsByIssue(ctx) | |||
if err != nil { | |||
return err | |||
} | |||
toNotify.AddMultiple(issueParticipants...) | |||
// dont notify user who cause notification | |||
delete(toNotify, notificationAuthorID) | |||
// explicit unwatch on issue | |||
issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false) | |||
if err != nil { | |||
return err | |||
} | |||
for _, id := range issueUnWatches { | |||
toNotify.Remove(id) | |||
} | |||
} | |||
err = issue.LoadRepo(ctx) | |||
if err != nil { | |||
return err | |||
} | |||
// notify | |||
for userID := range toNotify { | |||
issue.Repo.Units = nil | |||
user, err := user_model.GetUserByID(ctx, userID) | |||
if err != nil { | |||
if user_model.IsErrUserNotExist(err) { | |||
continue | |||
} | |||
return err | |||
} | |||
if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) { | |||
continue | |||
} | |||
if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) { | |||
continue | |||
} | |||
if notificationExists(notifications, issue.ID, userID) { | |||
if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil { | |||
return err | |||
} | |||
continue | |||
} | |||
if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
} | |||
func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error { | |||
notification := &Notification{ | |||
UserID: userID, | |||
@@ -449,309 +295,6 @@ func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.Tim | |||
return res, db.GetEngine(ctx).SQL(sql, since, until, NotificationStatusUnread).Find(&res) | |||
} | |||
// NotificationList contains a list of notifications | |||
type NotificationList []*Notification | |||
// LoadAttributes load Repo Issue User and Comment if not loaded | |||
func (nl NotificationList) LoadAttributes(ctx context.Context) error { | |||
if _, _, err := nl.LoadRepos(ctx); err != nil { | |||
return err | |||
} | |||
if _, err := nl.LoadIssues(ctx); err != nil { | |||
return err | |||
} | |||
if _, err := nl.LoadUsers(ctx); err != nil { | |||
return err | |||
} | |||
if _, err := nl.LoadComments(ctx); err != nil { | |||
return err | |||
} | |||
return nil | |||
} | |||
func (nl NotificationList) getPendingRepoIDs() []int64 { | |||
ids := make(container.Set[int64], len(nl)) | |||
for _, notification := range nl { | |||
if notification.Repository != nil { | |||
continue | |||
} | |||
ids.Add(notification.RepoID) | |||
} | |||
return ids.Values() | |||
} | |||
// LoadRepos loads repositories from database | |||
func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) { | |||
if len(nl) == 0 { | |||
return repo_model.RepositoryList{}, []int{}, nil | |||
} | |||
repoIDs := nl.getPendingRepoIDs() | |||
repos := make(map[int64]*repo_model.Repository, len(repoIDs)) | |||
left := len(repoIDs) | |||
for left > 0 { | |||
limit := db.DefaultMaxInSize | |||
if left < limit { | |||
limit = left | |||
} | |||
rows, err := db.GetEngine(ctx). | |||
In("id", repoIDs[:limit]). | |||
Rows(new(repo_model.Repository)) | |||
if err != nil { | |||
return nil, nil, err | |||
} | |||
for rows.Next() { | |||
var repo repo_model.Repository | |||
err = rows.Scan(&repo) | |||
if err != nil { | |||
rows.Close() | |||
return nil, nil, err | |||
} | |||
repos[repo.ID] = &repo | |||
} | |||
_ = rows.Close() | |||
left -= limit | |||
repoIDs = repoIDs[limit:] | |||
} | |||
failed := []int{} | |||
reposList := make(repo_model.RepositoryList, 0, len(repoIDs)) | |||
for i, notification := range nl { | |||
if notification.Repository == nil { | |||
notification.Repository = repos[notification.RepoID] | |||
} | |||
if notification.Repository == nil { | |||
log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID) | |||
failed = append(failed, i) | |||
continue | |||
} | |||
var found bool | |||
for _, r := range reposList { | |||
if r.ID == notification.RepoID { | |||
found = true | |||
break | |||
} | |||
} | |||
if !found { | |||
reposList = append(reposList, notification.Repository) | |||
} | |||
} | |||
return reposList, failed, nil | |||
} | |||
func (nl NotificationList) getPendingIssueIDs() []int64 { | |||
ids := make(container.Set[int64], len(nl)) | |||
for _, notification := range nl { | |||
if notification.Issue != nil { | |||
continue | |||
} | |||
ids.Add(notification.IssueID) | |||
} | |||
return ids.Values() | |||
} | |||
// LoadIssues loads issues from database | |||
func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) { | |||
if len(nl) == 0 { | |||
return []int{}, nil | |||
} | |||
issueIDs := nl.getPendingIssueIDs() | |||
issues := make(map[int64]*issues_model.Issue, len(issueIDs)) | |||
left := len(issueIDs) | |||
for left > 0 { | |||
limit := db.DefaultMaxInSize | |||
if left < limit { | |||
limit = left | |||
} | |||
rows, err := db.GetEngine(ctx). | |||
In("id", issueIDs[:limit]). | |||
Rows(new(issues_model.Issue)) | |||
if err != nil { | |||
return nil, err | |||
} | |||
for rows.Next() { | |||
var issue issues_model.Issue | |||
err = rows.Scan(&issue) | |||
if err != nil { | |||
rows.Close() | |||
return nil, err | |||
} | |||
issues[issue.ID] = &issue | |||
} | |||
_ = rows.Close() | |||
left -= limit | |||
issueIDs = issueIDs[limit:] | |||
} | |||
failures := []int{} | |||
for i, notification := range nl { | |||
if notification.Issue == nil { | |||
notification.Issue = issues[notification.IssueID] | |||
if notification.Issue == nil { | |||
if notification.IssueID != 0 { | |||
log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID) | |||
failures = append(failures, i) | |||
} | |||
continue | |||
} | |||
notification.Issue.Repo = notification.Repository | |||
} | |||
} | |||
return failures, nil | |||
} | |||
// Without returns the notification list without the failures | |||
func (nl NotificationList) Without(failures []int) NotificationList { | |||
if len(failures) == 0 { | |||
return nl | |||
} | |||
remaining := make([]*Notification, 0, len(nl)) | |||
last := -1 | |||
var i int | |||
for _, i = range failures { | |||
remaining = append(remaining, nl[last+1:i]...) | |||
last = i | |||
} | |||
if len(nl) > i { | |||
remaining = append(remaining, nl[i+1:]...) | |||
} | |||
return remaining | |||
} | |||
func (nl NotificationList) getPendingCommentIDs() []int64 { | |||
ids := make(container.Set[int64], len(nl)) | |||
for _, notification := range nl { | |||
if notification.CommentID == 0 || notification.Comment != nil { | |||
continue | |||
} | |||
ids.Add(notification.CommentID) | |||
} | |||
return ids.Values() | |||
} | |||
func (nl NotificationList) getUserIDs() []int64 { | |||
ids := make(container.Set[int64], len(nl)) | |||
for _, notification := range nl { | |||
if notification.UserID == 0 || notification.User != nil { | |||
continue | |||
} | |||
ids.Add(notification.UserID) | |||
} | |||
return ids.Values() | |||
} | |||
// LoadUsers loads users from database | |||
func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) { | |||
if len(nl) == 0 { | |||
return []int{}, nil | |||
} | |||
userIDs := nl.getUserIDs() | |||
users := make(map[int64]*user_model.User, len(userIDs)) | |||
left := len(userIDs) | |||
for left > 0 { | |||
limit := db.DefaultMaxInSize | |||
if left < limit { | |||
limit = left | |||
} | |||
rows, err := db.GetEngine(ctx). | |||
In("id", userIDs[:limit]). | |||
Rows(new(user_model.User)) | |||
if err != nil { | |||
return nil, err | |||
} | |||
for rows.Next() { | |||
var user user_model.User | |||
err = rows.Scan(&user) | |||
if err != nil { | |||
rows.Close() | |||
return nil, err | |||
} | |||
users[user.ID] = &user | |||
} | |||
_ = rows.Close() | |||
left -= limit | |||
userIDs = userIDs[limit:] | |||
} | |||
failures := []int{} | |||
for i, notification := range nl { | |||
if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil { | |||
notification.User = users[notification.UserID] | |||
if notification.User == nil { | |||
log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID) | |||
failures = append(failures, i) | |||
continue | |||
} | |||
} | |||
} | |||
return failures, nil | |||
} | |||
// LoadComments loads comments from database | |||
func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { | |||
if len(nl) == 0 { | |||
return []int{}, nil | |||
} | |||
commentIDs := nl.getPendingCommentIDs() | |||
comments := make(map[int64]*issues_model.Comment, len(commentIDs)) | |||
left := len(commentIDs) | |||
for left > 0 { | |||
limit := db.DefaultMaxInSize | |||
if left < limit { | |||
limit = left | |||
} | |||
rows, err := db.GetEngine(ctx). | |||
In("id", commentIDs[:limit]). | |||
Rows(new(issues_model.Comment)) | |||
if err != nil { | |||
return nil, err | |||
} | |||
for rows.Next() { | |||
var comment issues_model.Comment | |||
err = rows.Scan(&comment) | |||
if err != nil { | |||
rows.Close() | |||
return nil, err | |||
} | |||
comments[comment.ID] = &comment | |||
} | |||
_ = rows.Close() | |||
left -= limit | |||
commentIDs = commentIDs[limit:] | |||
} | |||
failures := []int{} | |||
for i, notification := range nl { | |||
if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil { | |||
notification.Comment = comments[notification.CommentID] | |||
if notification.Comment == nil { | |||
log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID) | |||
failures = append(failures, i) | |||
continue | |||
} | |||
notification.Comment.Issue = notification.Issue | |||
} | |||
} | |||
return failures, nil | |||
} | |||
// SetIssueReadBy sets issue to be read by given user. | |||
func SetIssueReadBy(ctx context.Context, issueID, userID int64) error { | |||
if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil { |
@@ -0,0 +1,501 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package activities | |||
import ( | |||
"context" | |||
"code.gitea.io/gitea/models/db" | |||
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/util" | |||
"xorm.io/builder" | |||
) | |||
// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored. | |||
type FindNotificationOptions struct { | |||
db.ListOptions | |||
UserID int64 | |||
RepoID int64 | |||
IssueID int64 | |||
Status []NotificationStatus | |||
Source []NotificationSource | |||
UpdatedAfterUnix int64 | |||
UpdatedBeforeUnix int64 | |||
} | |||
// ToCond will convert each condition into a xorm-Cond | |||
func (opts FindNotificationOptions) ToConds() builder.Cond { | |||
cond := builder.NewCond() | |||
if opts.UserID != 0 { | |||
cond = cond.And(builder.Eq{"notification.user_id": opts.UserID}) | |||
} | |||
if opts.RepoID != 0 { | |||
cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID}) | |||
} | |||
if opts.IssueID != 0 { | |||
cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID}) | |||
} | |||
if len(opts.Status) > 0 { | |||
if len(opts.Status) == 1 { | |||
cond = cond.And(builder.Eq{"notification.status": opts.Status[0]}) | |||
} else { | |||
cond = cond.And(builder.In("notification.status", opts.Status)) | |||
} | |||
} | |||
if len(opts.Source) > 0 { | |||
cond = cond.And(builder.In("notification.source", opts.Source)) | |||
} | |||
if opts.UpdatedAfterUnix != 0 { | |||
cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix}) | |||
} | |||
if opts.UpdatedBeforeUnix != 0 { | |||
cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix}) | |||
} | |||
return cond | |||
} | |||
func (opts FindNotificationOptions) ToOrders() string { | |||
return "notification.updated_unix DESC" | |||
} | |||
// CreateOrUpdateIssueNotifications creates an issue notification | |||
// for each watcher, or updates it if already exists | |||
// receiverID > 0 just send to receiver, else send to all watcher | |||
func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { | |||
ctx, committer, err := db.TxContext(ctx) | |||
if err != nil { | |||
return err | |||
} | |||
defer committer.Close() | |||
if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil { | |||
return err | |||
} | |||
return committer.Commit() | |||
} | |||
func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { | |||
// init | |||
var toNotify container.Set[int64] | |||
notifications, err := db.Find[Notification](ctx, FindNotificationOptions{ | |||
IssueID: issueID, | |||
}) | |||
if err != nil { | |||
return err | |||
} | |||
issue, err := issues_model.GetIssueByID(ctx, issueID) | |||
if err != nil { | |||
return err | |||
} | |||
if receiverID > 0 { | |||
toNotify = make(container.Set[int64], 1) | |||
toNotify.Add(receiverID) | |||
} else { | |||
toNotify = make(container.Set[int64], 32) | |||
issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true) | |||
if err != nil { | |||
return err | |||
} | |||
toNotify.AddMultiple(issueWatches...) | |||
if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) { | |||
repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID) | |||
if err != nil { | |||
return err | |||
} | |||
toNotify.AddMultiple(repoWatches...) | |||
} | |||
issueParticipants, err := issue.GetParticipantIDsByIssue(ctx) | |||
if err != nil { | |||
return err | |||
} | |||
toNotify.AddMultiple(issueParticipants...) | |||
// dont notify user who cause notification | |||
delete(toNotify, notificationAuthorID) | |||
// explicit unwatch on issue | |||
issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false) | |||
if err != nil { | |||
return err | |||
} | |||
for _, id := range issueUnWatches { | |||
toNotify.Remove(id) | |||
} | |||
} | |||
err = issue.LoadRepo(ctx) | |||
if err != nil { | |||
return err | |||
} | |||
// notify | |||
for userID := range toNotify { | |||
issue.Repo.Units = nil | |||
user, err := user_model.GetUserByID(ctx, userID) | |||
if err != nil { | |||
if user_model.IsErrUserNotExist(err) { | |||
continue | |||
} | |||
return err | |||
} | |||
if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) { | |||
continue | |||
} | |||
if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) { | |||
continue | |||
} | |||
if notificationExists(notifications, issue.ID, userID) { | |||
if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil { | |||
return err | |||
} | |||
continue | |||
} | |||
if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
} | |||
// NotificationList contains a list of notifications | |||
type NotificationList []*Notification | |||
// LoadAttributes load Repo Issue User and Comment if not loaded | |||
func (nl NotificationList) LoadAttributes(ctx context.Context) error { | |||
if _, _, err := nl.LoadRepos(ctx); err != nil { | |||
return err | |||
} | |||
if _, err := nl.LoadIssues(ctx); err != nil { | |||
return err | |||
} | |||
if _, err := nl.LoadUsers(ctx); err != nil { | |||
return err | |||
} | |||
if _, err := nl.LoadComments(ctx); err != nil { | |||
return err | |||
} | |||
return nil | |||
} | |||
func (nl NotificationList) getPendingRepoIDs() []int64 { | |||
ids := make(container.Set[int64], len(nl)) | |||
for _, notification := range nl { | |||
if notification.Repository != nil { | |||
continue | |||
} | |||
ids.Add(notification.RepoID) | |||
} | |||
return ids.Values() | |||
} | |||
// LoadRepos loads repositories from database | |||
func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) { | |||
if len(nl) == 0 { | |||
return repo_model.RepositoryList{}, []int{}, nil | |||
} | |||
repoIDs := nl.getPendingRepoIDs() | |||
repos := make(map[int64]*repo_model.Repository, len(repoIDs)) | |||
left := len(repoIDs) | |||
for left > 0 { | |||
limit := db.DefaultMaxInSize | |||
if left < limit { | |||
limit = left | |||
} | |||
rows, err := db.GetEngine(ctx). | |||
In("id", repoIDs[:limit]). | |||
Rows(new(repo_model.Repository)) | |||
if err != nil { | |||
return nil, nil, err | |||
} | |||
for rows.Next() { | |||
var repo repo_model.Repository | |||
err = rows.Scan(&repo) | |||
if err != nil { | |||
rows.Close() | |||
return nil, nil, err | |||
} | |||
repos[repo.ID] = &repo | |||
} | |||
_ = rows.Close() | |||
left -= limit | |||
repoIDs = repoIDs[limit:] | |||
} | |||
failed := []int{} | |||
reposList := make(repo_model.RepositoryList, 0, len(repoIDs)) | |||
for i, notification := range nl { | |||
if notification.Repository == nil { | |||
notification.Repository = repos[notification.RepoID] | |||
} | |||
if notification.Repository == nil { | |||
log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID) | |||
failed = append(failed, i) | |||
continue | |||
} | |||
var found bool | |||
for _, r := range reposList { | |||
if r.ID == notification.RepoID { | |||
found = true | |||
break | |||
} | |||
} | |||
if !found { | |||
reposList = append(reposList, notification.Repository) | |||
} | |||
} | |||
return reposList, failed, nil | |||
} | |||
func (nl NotificationList) getPendingIssueIDs() []int64 { | |||
ids := make(container.Set[int64], len(nl)) | |||
for _, notification := range nl { | |||
if notification.Issue != nil { | |||
continue | |||
} | |||
ids.Add(notification.IssueID) | |||
} | |||
return ids.Values() | |||
} | |||
// LoadIssues loads issues from database | |||
func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) { | |||
if len(nl) == 0 { | |||
return []int{}, nil | |||
} | |||
issueIDs := nl.getPendingIssueIDs() | |||
issues := make(map[int64]*issues_model.Issue, len(issueIDs)) | |||
left := len(issueIDs) | |||
for left > 0 { | |||
limit := db.DefaultMaxInSize | |||
if left < limit { | |||
limit = left | |||
} | |||
rows, err := db.GetEngine(ctx). | |||
In("id", issueIDs[:limit]). | |||
Rows(new(issues_model.Issue)) | |||
if err != nil { | |||
return nil, err | |||
} | |||
for rows.Next() { | |||
var issue issues_model.Issue | |||
err = rows.Scan(&issue) | |||
if err != nil { | |||
rows.Close() | |||
return nil, err | |||
} | |||
issues[issue.ID] = &issue | |||
} | |||
_ = rows.Close() | |||
left -= limit | |||
issueIDs = issueIDs[limit:] | |||
} | |||
failures := []int{} | |||
for i, notification := range nl { | |||
if notification.Issue == nil { | |||
notification.Issue = issues[notification.IssueID] | |||
if notification.Issue == nil { | |||
if notification.IssueID != 0 { | |||
log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID) | |||
failures = append(failures, i) | |||
} | |||
continue | |||
} | |||
notification.Issue.Repo = notification.Repository | |||
} | |||
} | |||
return failures, nil | |||
} | |||
// Without returns the notification list without the failures | |||
func (nl NotificationList) Without(failures []int) NotificationList { | |||
if len(failures) == 0 { | |||
return nl | |||
} | |||
remaining := make([]*Notification, 0, len(nl)) | |||
last := -1 | |||
var i int | |||
for _, i = range failures { | |||
remaining = append(remaining, nl[last+1:i]...) | |||
last = i | |||
} | |||
if len(nl) > i { | |||
remaining = append(remaining, nl[i+1:]...) | |||
} | |||
return remaining | |||
} | |||
func (nl NotificationList) getPendingCommentIDs() []int64 { | |||
ids := make(container.Set[int64], len(nl)) | |||
for _, notification := range nl { | |||
if notification.CommentID == 0 || notification.Comment != nil { | |||
continue | |||
} | |||
ids.Add(notification.CommentID) | |||
} | |||
return ids.Values() | |||
} | |||
func (nl NotificationList) getUserIDs() []int64 { | |||
ids := make(container.Set[int64], len(nl)) | |||
for _, notification := range nl { | |||
if notification.UserID == 0 || notification.User != nil { | |||
continue | |||
} | |||
ids.Add(notification.UserID) | |||
} | |||
return ids.Values() | |||
} | |||
// LoadUsers loads users from database | |||
func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) { | |||
if len(nl) == 0 { | |||
return []int{}, nil | |||
} | |||
userIDs := nl.getUserIDs() | |||
users := make(map[int64]*user_model.User, len(userIDs)) | |||
left := len(userIDs) | |||
for left > 0 { | |||
limit := db.DefaultMaxInSize | |||
if left < limit { | |||
limit = left | |||
} | |||
rows, err := db.GetEngine(ctx). | |||
In("id", userIDs[:limit]). | |||
Rows(new(user_model.User)) | |||
if err != nil { | |||
return nil, err | |||
} | |||
for rows.Next() { | |||
var user user_model.User | |||
err = rows.Scan(&user) | |||
if err != nil { | |||
rows.Close() | |||
return nil, err | |||
} | |||
users[user.ID] = &user | |||
} | |||
_ = rows.Close() | |||
left -= limit | |||
userIDs = userIDs[limit:] | |||
} | |||
failures := []int{} | |||
for i, notification := range nl { | |||
if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil { | |||
notification.User = users[notification.UserID] | |||
if notification.User == nil { | |||
log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID) | |||
failures = append(failures, i) | |||
continue | |||
} | |||
} | |||
} | |||
return failures, nil | |||
} | |||
// LoadComments loads comments from database | |||
func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { | |||
if len(nl) == 0 { | |||
return []int{}, nil | |||
} | |||
commentIDs := nl.getPendingCommentIDs() | |||
comments := make(map[int64]*issues_model.Comment, len(commentIDs)) | |||
left := len(commentIDs) | |||
for left > 0 { | |||
limit := db.DefaultMaxInSize | |||
if left < limit { | |||
limit = left | |||
} | |||
rows, err := db.GetEngine(ctx). | |||
In("id", commentIDs[:limit]). | |||
Rows(new(issues_model.Comment)) | |||
if err != nil { | |||
return nil, err | |||
} | |||
for rows.Next() { | |||
var comment issues_model.Comment | |||
err = rows.Scan(&comment) | |||
if err != nil { | |||
rows.Close() | |||
return nil, err | |||
} | |||
comments[comment.ID] = &comment | |||
} | |||
_ = rows.Close() | |||
left -= limit | |||
commentIDs = commentIDs[limit:] | |||
} | |||
failures := []int{} | |||
for i, notification := range nl { | |||
if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil { | |||
notification.Comment = comments[notification.CommentID] | |||
if notification.Comment == nil { | |||
log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID) | |||
failures = append(failures, i) | |||
continue | |||
} | |||
notification.Comment.Issue = notification.Issue | |||
} | |||
} | |||
return failures, nil | |||
} | |||
// LoadIssuePullRequests loads all issues' pull requests if possible | |||
func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error { | |||
issues := make(map[int64]*issues_model.Issue, len(nl)) | |||
for _, notification := range nl { | |||
if notification.Issue != nil && notification.Issue.IsPull && notification.Issue.PullRequest == nil { | |||
issues[notification.Issue.ID] = notification.Issue | |||
} | |||
} | |||
if len(issues) == 0 { | |||
return nil | |||
} | |||
pulls, err := issues_model.GetPullRequestByIssueIDs(ctx, util.KeysOfMap(issues)) | |||
if err != nil { | |||
return err | |||
} | |||
for _, pull := range pulls { | |||
if issue := issues[pull.IssueID]; issue != nil { | |||
issue.PullRequest = pull | |||
issue.PullRequest.Issue = issue | |||
} | |||
} | |||
return nil | |||
} |
@@ -9,6 +9,7 @@ import ( | |||
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" | |||
access_model "code.gitea.io/gitea/models/perm/access" | |||
@@ -29,7 +30,8 @@ type Statistic struct { | |||
Mirror, Release, AuthSource, Webhook, | |||
Milestone, Label, HookTask, | |||
Team, UpdateTask, Project, | |||
ProjectBoard, Attachment int64 | |||
ProjectBoard, Attachment, | |||
Branches, Tags, CommitStatus int64 | |||
IssueByLabel []IssueByLabelCount | |||
IssueByRepository []IssueByRepositoryCount | |||
} | |||
@@ -58,6 +60,9 @@ func GetStatistic(ctx context.Context) (stats Statistic) { | |||
stats.Counter.Watch, _ = e.Count(new(repo_model.Watch)) | |||
stats.Counter.Star, _ = e.Count(new(repo_model.Star)) | |||
stats.Counter.Access, _ = e.Count(new(access_model.Access)) | |||
stats.Counter.Branches, _ = e.Count(new(git_model.Branch)) | |||
stats.Counter.Tags, _ = e.Where("is_draft=?", false).Count(new(repo_model.Release)) | |||
stats.Counter.CommitStatus, _ = e.Count(new(git_model.CommitStatus)) | |||
type IssueCount struct { | |||
Count int64 |
@@ -46,6 +46,10 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st | |||
return "", ErrGPGKeyNotExist{} | |||
} | |||
if err := key.LoadSubKeys(ctx); err != nil { | |||
return "", err | |||
} | |||
sig, err := extractSignature(signature) | |||
if err != nil { | |||
return "", ErrGPGInvalidTokenSignature{ |
@@ -139,6 +139,8 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error { | |||
if err != nil { | |||
return err | |||
} | |||
defer f.Close() | |||
scanner := bufio.NewScanner(f) | |||
for scanner.Scan() { | |||
line := scanner.Text() | |||
@@ -148,11 +150,12 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error { | |||
} | |||
_, err = t.WriteString(line + "\n") | |||
if err != nil { | |||
f.Close() | |||
return err | |||
} | |||
} | |||
f.Close() | |||
if err = scanner.Err(); err != nil { | |||
return fmt.Errorf("RegeneratePublicKeys scan: %w", err) | |||
} | |||
} | |||
return nil | |||
} |
@@ -24,7 +24,7 @@ import ( | |||
const ( | |||
// DefaultAvatarClass is the default class of a rendered avatar | |||
DefaultAvatarClass = "ui avatar gt-vm" | |||
DefaultAvatarClass = "ui avatar tw-align-middle" | |||
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar | |||
DefaultAvatarPixelSize = 28 | |||
) |
@@ -120,6 +120,16 @@ func (c *halfCommitter) Close() error { | |||
// TxContext represents a transaction Context, | |||
// it will reuse the existing transaction in the parent context or create a new one. | |||
// Some tips to use: | |||
// | |||
// 1 It's always recommended to use `WithTx` in new code instead of `TxContext`, since `WithTx` will handle the transaction automatically. | |||
// 2. To maintain the old code which uses `TxContext`: | |||
// a. Always call `Close()` before returning regardless of whether `Commit()` has been called. | |||
// b. Always call `Commit()` before returning if there are no errors, even if the code did not change any data. | |||
// c. Remember the `Committer` will be a halfCommitter when a transaction is being reused. | |||
// So calling `Commit()` will do nothing, but calling `Close()` without calling `Commit()` will rollback the transaction. | |||
// And all operations submitted by the caller stack will be rollbacked as well, not only the operations in the current function. | |||
// d. It doesn't mean rollback is forbidden, but always do it only when there is an error, and you do want to rollback. | |||
func TxContext(parentCtx context.Context) (*Context, Committer, error) { | |||
if sess, ok := inTransaction(parentCtx); ok { | |||
return newContext(parentCtx, sess, true), &halfCommitter{committer: sess}, nil |
@@ -75,3 +75,11 @@ | |||
content: "comment in private pository" | |||
created_unix: 946684811 | |||
updated_unix: 946684811 | |||
- | |||
id: 9 | |||
type: 22 # review | |||
poster_id: 2 | |||
issue_id: 2 # in repo_id 1 | |||
review_id: 20 | |||
created_unix: 946684810 |
@@ -45,3 +45,27 @@ | |||
type: 2 | |||
created_unix: 1688973000 | |||
updated_unix: 1688973000 | |||
- | |||
id: 5 | |||
title: project without default column | |||
owner_id: 2 | |||
repo_id: 0 | |||
is_closed: false | |||
creator_id: 2 | |||
board_type: 1 | |||
type: 2 | |||
created_unix: 1688973000 | |||
updated_unix: 1688973000 | |||
- | |||
id: 6 | |||
title: project with multiple default columns | |||
owner_id: 2 | |||
repo_id: 0 | |||
is_closed: false | |||
creator_id: 2 | |||
board_type: 1 | |||
type: 2 | |||
created_unix: 1688973000 | |||
updated_unix: 1688973000 |
@@ -3,6 +3,7 @@ | |||
project_id: 1 | |||
title: To Do | |||
creator_id: 2 | |||
default: true | |||
created_unix: 1588117528 | |||
updated_unix: 1588117528 | |||
@@ -29,3 +30,48 @@ | |||
creator_id: 2 | |||
created_unix: 1588117528 | |||
updated_unix: 1588117528 | |||
- | |||
id: 5 | |||
project_id: 2 | |||
title: Backlog | |||
creator_id: 2 | |||
default: true | |||
created_unix: 1588117528 | |||
updated_unix: 1588117528 | |||
- | |||
id: 6 | |||
project_id: 4 | |||
title: Backlog | |||
creator_id: 2 | |||
default: true | |||
created_unix: 1588117528 | |||
updated_unix: 1588117528 | |||
- | |||
id: 7 | |||
project_id: 5 | |||
title: Done | |||
creator_id: 2 | |||
default: false | |||
created_unix: 1588117528 | |||
updated_unix: 1588117528 | |||
- | |||
id: 8 | |||
project_id: 6 | |||
title: Backlog | |||
creator_id: 2 | |||
default: true | |||
created_unix: 1588117528 | |||
updated_unix: 1588117528 | |||
- | |||
id: 9 | |||
project_id: 6 | |||
title: Uncategorized | |||
creator_id: 2 | |||
default: true | |||
created_unix: 1588117528 | |||
updated_unix: 1588117528 |
@@ -170,3 +170,12 @@ | |||
content: "review request for user15" | |||
updated_unix: 946684835 | |||
created_unix: 946684835 | |||
- | |||
id: 20 | |||
type: 22 | |||
reviewer_id: 1 | |||
issue_id: 2 | |||
content: "Review Comment" | |||
updated_unix: 946684810 | |||
created_unix: 946684810 |
@@ -25,6 +25,7 @@ import ( | |||
"code.gitea.io/gitea/modules/translation" | |||
"xorm.io/builder" | |||
"xorm.io/xorm" | |||
) | |||
// CommitStatus holds a single Status of a single Commit | |||
@@ -269,44 +270,48 @@ type CommitStatusIndex struct { | |||
// GetLatestCommitStatus returns all statuses with a unique context for a given commit. | |||
func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOptions db.ListOptions) ([]*CommitStatus, int64, error) { | |||
ids := make([]int64, 0, 10) | |||
sess := db.GetEngine(ctx).Table(&CommitStatus{}). | |||
Where("repo_id = ?", repoID).And("sha = ?", sha). | |||
Select("max( id ) as id"). | |||
GroupBy("context_hash").OrderBy("max( id ) desc") | |||
getBase := func() *xorm.Session { | |||
return db.GetEngine(ctx).Table(&CommitStatus{}). | |||
Where("repo_id = ?", repoID).And("sha = ?", sha) | |||
} | |||
indices := make([]int64, 0, 10) | |||
sess := getBase().Select("max( `index` ) as `index`"). | |||
GroupBy("context_hash").OrderBy("max( `index` ) desc") | |||
if !listOptions.IsListAll() { | |||
sess = db.SetSessionPagination(sess, &listOptions) | |||
} | |||
count, err := sess.FindAndCount(&ids) | |||
count, err := sess.FindAndCount(&indices) | |||
if err != nil { | |||
return nil, count, err | |||
} | |||
statuses := make([]*CommitStatus, 0, len(ids)) | |||
if len(ids) == 0 { | |||
statuses := make([]*CommitStatus, 0, len(indices)) | |||
if len(indices) == 0 { | |||
return statuses, count, nil | |||
} | |||
return statuses, count, db.GetEngine(ctx).In("id", ids).Find(&statuses) | |||
return statuses, count, getBase().And(builder.In("`index`", indices)).Find(&statuses) | |||
} | |||
// GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs | |||
func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHAs map[int64]string, listOptions db.ListOptions) (map[int64][]*CommitStatus, error) { | |||
type result struct { | |||
ID int64 | |||
Index int64 | |||
RepoID int64 | |||
} | |||
results := make([]result, 0, len(repoIDsToLatestCommitSHAs)) | |||
sess := db.GetEngine(ctx).Table(&CommitStatus{}) | |||
getBase := func() *xorm.Session { | |||
return db.GetEngine(ctx).Table(&CommitStatus{}) | |||
} | |||
// Create a disjunction of conditions for each repoID and SHA pair | |||
conds := make([]builder.Cond, 0, len(repoIDsToLatestCommitSHAs)) | |||
for repoID, sha := range repoIDsToLatestCommitSHAs { | |||
conds = append(conds, builder.Eq{"repo_id": repoID, "sha": sha}) | |||
} | |||
sess = sess.Where(builder.Or(conds...)). | |||
Select("max( id ) as id, repo_id"). | |||
GroupBy("context_hash, repo_id").OrderBy("max( id ) desc") | |||
sess := getBase().Where(builder.Or(conds...)). | |||
Select("max( `index` ) as `index`, repo_id"). | |||
GroupBy("context_hash, repo_id").OrderBy("max( `index` ) desc") | |||
if !listOptions.IsListAll() { | |||
sess = db.SetSessionPagination(sess, &listOptions) | |||
@@ -317,15 +322,21 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHA | |||
return nil, err | |||
} | |||
ids := make([]int64, 0, len(results)) | |||
repoStatuses := make(map[int64][]*CommitStatus) | |||
for _, result := range results { | |||
ids = append(ids, result.ID) | |||
} | |||
statuses := make([]*CommitStatus, 0, len(ids)) | |||
if len(ids) > 0 { | |||
err = db.GetEngine(ctx).In("id", ids).Find(&statuses) | |||
if len(results) > 0 { | |||
statuses := make([]*CommitStatus, 0, len(results)) | |||
conds = make([]builder.Cond, 0, len(results)) | |||
for _, result := range results { | |||
cond := builder.Eq{ | |||
"`index`": result.Index, | |||
"repo_id": result.RepoID, | |||
"sha": repoIDsToLatestCommitSHAs[result.RepoID], | |||
} | |||
conds = append(conds, cond) | |||
} | |||
err = getBase().Where(builder.Or(conds...)).Find(&statuses) | |||
if err != nil { | |||
return nil, err | |||
} | |||
@@ -342,42 +353,43 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHA | |||
// GetLatestCommitStatusForRepoCommitIDs returns all statuses with a unique context for a given list of repo-sha pairs | |||
func GetLatestCommitStatusForRepoCommitIDs(ctx context.Context, repoID int64, commitIDs []string) (map[string][]*CommitStatus, error) { | |||
type result struct { | |||
ID int64 | |||
Sha string | |||
Index int64 | |||
SHA string | |||
} | |||
getBase := func() *xorm.Session { | |||
return db.GetEngine(ctx).Table(&CommitStatus{}).Where("repo_id = ?", repoID) | |||
} | |||
results := make([]result, 0, len(commitIDs)) | |||
sess := db.GetEngine(ctx).Table(&CommitStatus{}) | |||
// Create a disjunction of conditions for each repoID and SHA pair | |||
conds := make([]builder.Cond, 0, len(commitIDs)) | |||
for _, sha := range commitIDs { | |||
conds = append(conds, builder.Eq{"sha": sha}) | |||
} | |||
sess = sess.Where(builder.Eq{"repo_id": repoID}.And(builder.Or(conds...))). | |||
Select("max( id ) as id, sha"). | |||
GroupBy("context_hash, sha").OrderBy("max( id ) desc") | |||
sess := getBase().And(builder.Or(conds...)). | |||
Select("max( `index` ) as `index`, sha"). | |||
GroupBy("context_hash, sha").OrderBy("max( `index` ) desc") | |||
err := sess.Find(&results) | |||
if err != nil { | |||
return nil, err | |||
} | |||
ids := make([]int64, 0, len(results)) | |||
repoStatuses := make(map[string][]*CommitStatus) | |||
for _, result := range results { | |||
ids = append(ids, result.ID) | |||
} | |||
statuses := make([]*CommitStatus, 0, len(ids)) | |||
if len(ids) > 0 { | |||
err = db.GetEngine(ctx).In("id", ids).Find(&statuses) | |||
if len(results) > 0 { | |||
statuses := make([]*CommitStatus, 0, len(results)) | |||
conds = make([]builder.Cond, 0, len(results)) | |||
for _, result := range results { | |||
conds = append(conds, builder.Eq{"`index`": result.Index, "sha": result.SHA}) | |||
} | |||
err = getBase().And(builder.Or(conds...)).Find(&statuses) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Group the statuses by repo ID | |||
// Group the statuses by commit | |||
for _, status := range statuses { | |||
repoStatuses[status.SHA] = append(repoStatuses[status.SHA], status) | |||
} | |||
@@ -388,22 +400,36 @@ func GetLatestCommitStatusForRepoCommitIDs(ctx context.Context, repoID int64, co | |||
// FindRepoRecentCommitStatusContexts returns repository's recent commit status contexts | |||
func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, before time.Duration) ([]string, error) { | |||
type result struct { | |||
Index int64 | |||
SHA string | |||
} | |||
getBase := func() *xorm.Session { | |||
return db.GetEngine(ctx).Table(&CommitStatus{}).Where("repo_id = ?", repoID) | |||
} | |||
start := timeutil.TimeStampNow().AddDuration(-before) | |||
ids := make([]int64, 0, 10) | |||
if err := db.GetEngine(ctx).Table("commit_status"). | |||
Where("repo_id = ?", repoID). | |||
And("updated_unix >= ?", start). | |||
Select("max( id ) as id"). | |||
GroupBy("context_hash").OrderBy("max( id ) desc"). | |||
Find(&ids); err != nil { | |||
results := make([]result, 0, 10) | |||
sess := getBase().And("updated_unix >= ?", start). | |||
Select("max( `index` ) as `index`, sha"). | |||
GroupBy("context_hash, sha").OrderBy("max( `index` ) desc") | |||
err := sess.Find(&results) | |||
if err != nil { | |||
return nil, err | |||
} | |||
contexts := make([]string, 0, len(ids)) | |||
if len(ids) == 0 { | |||
contexts := make([]string, 0, len(results)) | |||
if len(results) == 0 { | |||
return contexts, nil | |||
} | |||
return contexts, db.GetEngine(ctx).Select("context").Table("commit_status").In("id", ids).Find(&contexts) | |||
conds := make([]builder.Cond, 0, len(results)) | |||
for _, result := range results { | |||
conds = append(conds, builder.Eq{"`index`": result.Index, "sha": result.SHA}) | |||
} | |||
return contexts, getBase().And(builder.Or(conds...)).Select("context").Find(&contexts) | |||
} | |||
// NewCommitStatusOptions holds options for creating a CommitStatus |
@@ -673,7 +673,8 @@ func (c *Comment) LoadTime(ctx context.Context) error { | |||
return err | |||
} | |||
func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository) (err error) { | |||
// LoadReactions loads comment reactions | |||
func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository) (err error) { | |||
if c.Reactions != nil { | |||
return nil | |||
} | |||
@@ -691,11 +692,6 @@ func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository | |||
return nil | |||
} | |||
// LoadReactions loads comment reactions | |||
func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository) error { | |||
return c.loadReactions(ctx, repo) | |||
} | |||
func (c *Comment) loadReview(ctx context.Context) (err error) { | |||
if c.ReviewID == 0 { | |||
return nil |
@@ -74,6 +74,10 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu | |||
return nil, err | |||
} | |||
if err := comments.LoadAttachments(ctx); err != nil { | |||
return nil, err | |||
} | |||
// Find all reviews by ReviewID | |||
reviews := make(map[int64]*Review) | |||
ids := make([]int64, 0, len(comments)) | |||
@@ -122,7 +126,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu | |||
} | |||
// FetchCodeCommentsByLine fetches the code comments for a given treePath and line number | |||
func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) ([]*Comment, error) { | |||
func FetchCodeCommentsByLine(ctx context.Context, issue *Issue, currentUser *user_model.User, treePath string, line int64, showOutdatedComments bool) (CommentList, error) { | |||
opts := FindCommentsOptions{ | |||
Type: CommentTypeCode, | |||
IssueID: issue.ID, |
@@ -19,7 +19,9 @@ type CommentList []*Comment | |||
func (comments CommentList) getPosterIDs() []int64 { | |||
posterIDs := make(container.Set[int64], len(comments)) | |||
for _, comment := range comments { | |||
posterIDs.Add(comment.PosterID) | |||
if comment.PosterID > 0 { | |||
posterIDs.Add(comment.PosterID) | |||
} | |||
} | |||
return posterIDs.Values() | |||
} | |||
@@ -41,18 +43,12 @@ func (comments CommentList) LoadPosters(ctx context.Context) error { | |||
return nil | |||
} | |||
func (comments CommentList) getCommentIDs() []int64 { | |||
ids := make([]int64, 0, len(comments)) | |||
for _, comment := range comments { | |||
ids = append(ids, comment.ID) | |||
} | |||
return ids | |||
} | |||
func (comments CommentList) getLabelIDs() []int64 { | |||
ids := make(container.Set[int64], len(comments)) | |||
for _, comment := range comments { | |||
ids.Add(comment.LabelID) | |||
if comment.LabelID > 0 { | |||
ids.Add(comment.LabelID) | |||
} | |||
} | |||
return ids.Values() | |||
} | |||
@@ -100,7 +96,9 @@ func (comments CommentList) loadLabels(ctx context.Context) error { | |||
func (comments CommentList) getMilestoneIDs() []int64 { | |||
ids := make(container.Set[int64], len(comments)) | |||
for _, comment := range comments { | |||
ids.Add(comment.MilestoneID) | |||
if comment.MilestoneID > 0 { | |||
ids.Add(comment.MilestoneID) | |||
} | |||
} | |||
return ids.Values() | |||
} | |||
@@ -141,7 +139,9 @@ func (comments CommentList) loadMilestones(ctx context.Context) error { | |||
func (comments CommentList) getOldMilestoneIDs() []int64 { | |||
ids := make(container.Set[int64], len(comments)) | |||
for _, comment := range comments { | |||
ids.Add(comment.OldMilestoneID) | |||
if comment.OldMilestoneID > 0 { | |||
ids.Add(comment.OldMilestoneID) | |||
} | |||
} | |||
return ids.Values() | |||
} | |||
@@ -182,7 +182,9 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error { | |||
func (comments CommentList) getAssigneeIDs() []int64 { | |||
ids := make(container.Set[int64], len(comments)) | |||
for _, comment := range comments { | |||
ids.Add(comment.AssigneeID) | |||
if comment.AssigneeID > 0 { | |||
ids.Add(comment.AssigneeID) | |||
} | |||
} | |||
return ids.Values() | |||
} | |||
@@ -314,7 +316,9 @@ func (comments CommentList) getDependentIssueIDs() []int64 { | |||
if comment.DependentIssue != nil { | |||
continue | |||
} | |||
ids.Add(comment.DependentIssueID) | |||
if comment.DependentIssueID > 0 { | |||
ids.Add(comment.DependentIssueID) | |||
} | |||
} | |||
return ids.Values() | |||
} | |||
@@ -369,6 +373,41 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error { | |||
return nil | |||
} | |||
// getAttachmentCommentIDs only return the comment ids which possibly has attachments | |||
func (comments CommentList) getAttachmentCommentIDs() []int64 { | |||
ids := make(container.Set[int64], len(comments)) | |||
for _, comment := range comments { | |||
if comment.Type == CommentTypeComment || | |||
comment.Type == CommentTypeReview || | |||
comment.Type == CommentTypeCode { | |||
ids.Add(comment.ID) | |||
} | |||
} | |||
return ids.Values() | |||
} | |||
// LoadAttachmentsByIssue loads attachments by issue id | |||
func (comments CommentList) LoadAttachmentsByIssue(ctx context.Context) error { | |||
if len(comments) == 0 { | |||
return nil | |||
} | |||
attachments := make([]*repo_model.Attachment, 0, len(comments)/2) | |||
if err := db.GetEngine(ctx).Where("issue_id=? AND comment_id>0", comments[0].IssueID).Find(&attachments); err != nil { | |||
return err | |||
} | |||
commentAttachmentsMap := make(map[int64][]*repo_model.Attachment, len(comments)) | |||
for _, attach := range attachments { | |||
commentAttachmentsMap[attach.CommentID] = append(commentAttachmentsMap[attach.CommentID], attach) | |||
} | |||
for _, comment := range comments { | |||
comment.Attachments = commentAttachmentsMap[comment.ID] | |||
} | |||
return nil | |||
} | |||
// LoadAttachments loads attachments | |||
func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { | |||
if len(comments) == 0 { | |||
@@ -376,16 +415,15 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { | |||
} | |||
attachments := make(map[int64][]*repo_model.Attachment, len(comments)) | |||
commentsIDs := comments.getCommentIDs() | |||
commentsIDs := comments.getAttachmentCommentIDs() | |||
left := len(commentsIDs) | |||
for left > 0 { | |||
limit := db.DefaultMaxInSize | |||
if left < limit { | |||
limit = left | |||
} | |||
rows, err := db.GetEngine(ctx).Table("attachment"). | |||
Join("INNER", "comment", "comment.id = attachment.comment_id"). | |||
In("comment.id", commentsIDs[:limit]). | |||
rows, err := db.GetEngine(ctx). | |||
In("comment_id", commentsIDs[:limit]). | |||
Rows(new(repo_model.Attachment)) | |||
if err != nil { | |||
return err | |||
@@ -415,7 +453,9 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { | |||
func (comments CommentList) getReviewIDs() []int64 { | |||
ids := make(container.Set[int64], len(comments)) | |||
for _, comment := range comments { | |||
ids.Add(comment.ReviewID) | |||
if comment.ReviewID > 0 { | |||
ids.Add(comment.ReviewID) | |||
} | |||
} | |||
return ids.Values() | |||
} |
@@ -193,20 +193,6 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool { | |||
return issue.Repo.IsTimetrackerEnabled(ctx) | |||
} | |||
// GetPullRequest returns the issue pull request | |||
func (issue *Issue) GetPullRequest(ctx context.Context) (pr *PullRequest, err error) { | |||
if !issue.IsPull { | |||
return nil, fmt.Errorf("Issue is not a pull request") | |||
} | |||
pr, err = GetPullRequestByIssueID(ctx, issue.ID) | |||
if err != nil { | |||
return nil, err | |||
} | |||
pr.Issue = issue | |||
return pr, err | |||
} | |||
// LoadPoster loads poster | |||
func (issue *Issue) LoadPoster(ctx context.Context) (err error) { | |||
if issue.Poster == nil && issue.PosterID != 0 { |
@@ -370,6 +370,9 @@ func (issues IssueList) LoadPullRequests(ctx context.Context) error { | |||
for _, issue := range issues { | |||
issue.PullRequest = pullRequestMaps[issue.ID] | |||
if issue.PullRequest != nil { | |||
issue.PullRequest.Issue = issue | |||
} | |||
} | |||
return nil | |||
} | |||
@@ -388,9 +391,8 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) { | |||
if left < limit { | |||
limit = left | |||
} | |||
rows, err := db.GetEngine(ctx).Table("attachment"). | |||
Join("INNER", "issue", "issue.id = attachment.issue_id"). | |||
In("issue.id", issuesIDs[:limit]). | |||
rows, err := db.GetEngine(ctx). | |||
In("issue_id", issuesIDs[:limit]). | |||
Rows(new(repo_model.Attachment)) | |||
if err != nil { | |||
return err | |||
@@ -476,6 +478,16 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { | |||
} | |||
trackedTimes := make(map[int64]int64, len(issues)) | |||
reposMap := make(map[int64]*repo_model.Repository, len(issues)) | |||
for _, issue := range issues { | |||
reposMap[issue.RepoID] = issue.Repo | |||
} | |||
repos := repo_model.RepositoryListOfMap(reposMap) | |||
if err := repos.LoadUnits(ctx); err != nil { | |||
return err | |||
} | |||
ids := make([]int64, 0, len(issues)) | |||
for _, issue := range issues { | |||
if issue.Repo.IsTimetrackerEnabled(ctx) { | |||
@@ -600,11 +612,32 @@ func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*Rev | |||
return approvalCountMap, nil | |||
} | |||
func (issues IssueList) LoadIsRead(ctx context.Context, userID int64) error { | |||
issueIDs := issues.getIssueIDs() | |||
issueUsers := make([]*IssueUser, 0, len(issueIDs)) | |||
if err := db.GetEngine(ctx).Where("uid =?", userID). | |||
In("issue_id"). | |||
Find(&issueUsers); err != nil { | |||
return err | |||
} | |||
for _, issueUser := range issueUsers { | |||
for _, issue := range issues { | |||
if issue.ID == issueUser.IssueID { | |||
issue.IsRead = issueUser.IsRead | |||
} | |||
} | |||
} | |||
return nil | |||
} | |||
func (issues IssueList) BlockingDependenciesMap(ctx context.Context) (issueDepsMap map[int64][]*DependencyInfo, err error) { | |||
var issueDeps []*DependencyInfo | |||
err = db.GetEngine(ctx). | |||
Table("issue"). | |||
Join("INNER", "repository", "repository.id = issue.repo_id"). | |||
Join("INNER", "issue_dependency", "issue_dependency.issue_id = issue.id"). | |||
Where(builder.In("issue_dependency.dependency_id", issues.getIssueIDs())). | |||
// sort by repo id then index | |||
@@ -630,6 +663,7 @@ func (issues IssueList) BlockedByDependenciesMap(ctx context.Context) (issueDeps | |||
err = db.GetEngine(ctx). | |||
Table("issue"). | |||
Join("INNER", "repository", "repository.id = issue.repo_id"). | |||
Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id"). | |||
Where(builder.In("issue_dependency.issue_id", issues.getIssueIDs())). | |||
// sort by repo id then index |
@@ -49,18 +49,13 @@ func (issue *Issue) ProjectBoardID(ctx context.Context) int64 { | |||
// LoadIssuesFromBoard load issues assigned to this board | |||
func LoadIssuesFromBoard(ctx context.Context, b *project_model.Board) (IssueList, error) { | |||
issueList := make(IssueList, 0, 10) | |||
if b.ID > 0 { | |||
issues, err := Issues(ctx, &IssuesOptions{ | |||
ProjectBoardID: b.ID, | |||
ProjectID: b.ProjectID, | |||
SortType: "project-column-sorting", | |||
}) | |||
if err != nil { | |||
return nil, err | |||
} | |||
issueList = issues | |||
issueList, err := Issues(ctx, &IssuesOptions{ | |||
ProjectBoardID: b.ID, | |||
ProjectID: b.ProjectID, | |||
SortType: "project-column-sorting", | |||
}) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if b.Default { |
@@ -21,7 +21,7 @@ import ( | |||
// IssuesOptions represents options of an issue. | |||
type IssuesOptions struct { //nolint | |||
db.Paginator | |||
Paginator *db.ListOptions | |||
RepoIDs []int64 // overwrites RepoCond if the length is not 0 | |||
AllPublic bool // include also all public repositories | |||
RepoCond builder.Cond | |||
@@ -104,23 +104,11 @@ func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { | |||
return sess | |||
} | |||
// Warning: Do not use GetSkipTake() for *db.ListOptions | |||
// Its implementation could reset the page size with setting.API.MaxResponseItems | |||
if listOptions, ok := opts.Paginator.(*db.ListOptions); ok { | |||
if listOptions.Page >= 0 && listOptions.PageSize > 0 { | |||
var start int | |||
if listOptions.Page == 0 { | |||
start = 0 | |||
} else { | |||
start = (listOptions.Page - 1) * listOptions.PageSize | |||
} | |||
sess.Limit(listOptions.PageSize, start) | |||
} | |||
return sess | |||
start := 0 | |||
if opts.Paginator.Page > 1 { | |||
start = (opts.Paginator.Page - 1) * opts.Paginator.PageSize | |||
} | |||
start, limit := opts.Paginator.GetSkipTake() | |||
sess.Limit(limit, start) | |||
sess.Limit(opts.Paginator.PageSize, start) | |||
return sess | |||
} | |||
@@ -393,7 +381,7 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) | |||
func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session { | |||
// Query for pull requests where you are a reviewer or commenter, excluding | |||
// any pull requests already returned by the the review requested filter. | |||
// any pull requests already returned by the review requested filter. | |||
notPoster := builder.Neq{"issue.poster_id": reviewedID} | |||
reviewed := builder.In("issue.id", builder. | |||
Select("issue_id"). |
@@ -68,13 +68,17 @@ func CountIssuesByRepo(ctx context.Context, opts *IssuesOptions) (map[int64]int6 | |||
} | |||
// CountIssues number return of issues by given conditions. | |||
func CountIssues(ctx context.Context, opts *IssuesOptions) (int64, error) { | |||
func CountIssues(ctx context.Context, opts *IssuesOptions, otherConds ...builder.Cond) (int64, error) { | |||
sess := db.GetEngine(ctx). | |||
Select("COUNT(issue.id) AS count"). | |||
Table("issue"). | |||
Join("INNER", "repository", "`issue`.repo_id = `repository`.id") | |||
applyConditions(sess, opts) | |||
for _, cond := range otherConds { | |||
sess.And(cond) | |||
} | |||
return sess.Count() | |||
} | |||
@@ -116,12 +116,17 @@ func (l *Label) CalOpenIssues() { | |||
func (l *Label) SetArchived(isArchived bool) { | |||
if !isArchived { | |||
l.ArchivedUnix = timeutil.TimeStamp(0) | |||
} else if isArchived && l.ArchivedUnix.IsZero() { | |||
} else if isArchived && !l.IsArchived() { | |||
// Only change the date when it is newly archived. | |||
l.ArchivedUnix = timeutil.TimeStampNow() | |||
} | |||
} | |||
// IsArchived returns true if label is an archived | |||
func (l *Label) IsArchived() bool { | |||
return !l.ArchivedUnix.IsZero() | |||
} | |||
// CalOpenOrgIssues calculates the open issues of a label for a specific repo | |||
func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { | |||
counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{ | |||
@@ -166,11 +171,6 @@ func (l *Label) BelongsToOrg() bool { | |||
return l.OrgID > 0 | |||
} | |||
// IsArchived returns true if label is an archived | |||
func (l *Label) IsArchived() bool { | |||
return l.ArchivedUnix > 0 | |||
} | |||
// BelongsToRepo returns true if label is a repository label | |||
func (l *Label) BelongsToRepo() bool { | |||
return l.RepoID > 0 |
@@ -19,7 +19,6 @@ import ( | |||
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/gitrepo" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/timeutil" | |||
@@ -884,77 +883,6 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr * | |||
return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 | |||
} | |||
func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullRequest) error { | |||
files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} | |||
if pr.IsWorkInProgress(ctx) { | |||
return nil | |||
} | |||
if err := pr.LoadBaseRepo(ctx); err != nil { | |||
return err | |||
} | |||
repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) | |||
if err != nil { | |||
return err | |||
} | |||
defer repo.Close() | |||
commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch) | |||
if err != nil { | |||
return err | |||
} | |||
var data string | |||
for _, file := range files { | |||
if blob, err := commit.GetBlobByPath(file); err == nil { | |||
data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize) | |||
if err == nil { | |||
break | |||
} | |||
} | |||
} | |||
rules, _ := GetCodeOwnersFromContent(ctx, data) | |||
changedFiles, err := repo.GetFilesChangedBetween(git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) | |||
if err != nil { | |||
return err | |||
} | |||
uniqUsers := make(map[int64]*user_model.User) | |||
uniqTeams := make(map[string]*org_model.Team) | |||
for _, rule := range rules { | |||
for _, f := range changedFiles { | |||
if (rule.Rule.MatchString(f) && !rule.Negative) || (!rule.Rule.MatchString(f) && rule.Negative) { | |||
for _, u := range rule.Users { | |||
uniqUsers[u.ID] = u | |||
} | |||
for _, t := range rule.Teams { | |||
uniqTeams[fmt.Sprintf("%d/%d", t.OrgID, t.ID)] = t | |||
} | |||
} | |||
} | |||
} | |||
for _, u := range uniqUsers { | |||
if u.ID != pull.Poster.ID { | |||
if _, err := AddReviewRequest(ctx, pull, u, pull.Poster); err != nil { | |||
log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err) | |||
return err | |||
} | |||
} | |||
} | |||
for _, t := range uniqTeams { | |||
if _, err := AddTeamReviewRequest(ctx, pull, t, pull.Poster); err != nil { | |||
log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err) | |||
return err | |||
} | |||
} | |||
return nil | |||
} | |||
// GetCodeOwnersFromContent returns the code owners configuration | |||
// Return empty slice if files missing | |||
// Return warning messages on parsing errors |
@@ -11,7 +11,6 @@ import ( | |||
access_model "code.gitea.io/gitea/models/perm/access" | |||
"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/log" | |||
"code.gitea.io/gitea/modules/util" | |||
@@ -23,7 +22,7 @@ type PullRequestsOptions struct { | |||
db.ListOptions | |||
State string | |||
SortType string | |||
Labels []string | |||
Labels []int64 | |||
MilestoneID int64 | |||
} | |||
@@ -36,11 +35,9 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR | |||
sess.And("issue.is_closed=?", opts.State == "closed") | |||
} | |||
if labelIDs, err := base.StringsToInt64s(opts.Labels); err != nil { | |||
return nil, err | |||
} else if len(labelIDs) > 0 { | |||
if len(opts.Labels) > 0 { | |||
sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id"). | |||
In("issue_label.label_id", labelIDs) | |||
In("issue_label.label_id", opts.Labels) | |||
} | |||
if opts.MilestoneID > 0 { | |||
@@ -212,3 +209,12 @@ func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bo | |||
Limit(1). | |||
Get(new(Issue)) | |||
} | |||
// GetPullRequestByIssueIDs returns all pull requests by issue ids | |||
func GetPullRequestByIssueIDs(ctx context.Context, issueIDs []int64) (PullRequestList, error) { | |||
prs := make([]*PullRequest, 0, len(issueIDs)) | |||
return prs, db.GetEngine(ctx). | |||
Where("issue_id > 0"). | |||
In("issue_id", issueIDs). | |||
Find(&prs) | |||
} |
@@ -66,7 +66,6 @@ func TestPullRequestsNewest(t *testing.T) { | |||
}, | |||
State: "open", | |||
SortType: "newest", | |||
Labels: []string{}, | |||
}) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, 3, count) | |||
@@ -113,7 +112,6 @@ func TestPullRequestsOldest(t *testing.T) { | |||
}, | |||
State: "open", | |||
SortType: "oldest", | |||
Labels: []string{}, | |||
}) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, 3, count) |
@@ -66,6 +66,23 @@ func (err ErrNotValidReviewRequest) Unwrap() error { | |||
return util.ErrInvalidArgument | |||
} | |||
// ErrReviewRequestOnClosedPR represents an error when an user tries to request a re-review on a closed or merged PR. | |||
type ErrReviewRequestOnClosedPR struct{} | |||
// IsErrReviewRequestOnClosedPR checks if an error is an ErrReviewRequestOnClosedPR. | |||
func IsErrReviewRequestOnClosedPR(err error) bool { | |||
_, ok := err.(ErrReviewRequestOnClosedPR) | |||
return ok | |||
} | |||
func (err ErrReviewRequestOnClosedPR) Error() string { | |||
return "cannot request a re-review on a closed or merged PR" | |||
} | |||
func (err ErrReviewRequestOnClosedPR) Unwrap() error { | |||
return util.ErrPermissionDenied | |||
} | |||
// ReviewType defines the sort of feedback a review gives | |||
type ReviewType int | |||
@@ -239,11 +256,11 @@ type CreateReviewOptions struct { | |||
// IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals) | |||
func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.User) (bool, error) { | |||
pr, err := GetPullRequestByIssueID(ctx, issue.ID) | |||
if err != nil { | |||
if err := issue.LoadPullRequest(ctx); err != nil { | |||
return false, err | |||
} | |||
pr := issue.PullRequest | |||
rule, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) | |||
if err != nil { | |||
return false, err | |||
@@ -271,11 +288,10 @@ func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model. | |||
// IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals) | |||
func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organization.Team) (bool, error) { | |||
pr, err := GetPullRequestByIssueID(ctx, issue.ID) | |||
if err != nil { | |||
if err := issue.LoadPullRequest(ctx); err != nil { | |||
return false, err | |||
} | |||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) | |||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, issue.PullRequest.BaseRepoID, issue.PullRequest.BaseBranch) | |||
if err != nil { | |||
return false, err | |||
} | |||
@@ -619,9 +635,24 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo | |||
return nil, err | |||
} | |||
// skip it when reviewer hase been request to review | |||
if review != nil && review.Type == ReviewTypeRequest { | |||
return nil, nil | |||
if review != nil { | |||
// skip it when reviewer hase been request to review | |||
if review.Type == ReviewTypeRequest { | |||
return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction. | |||
} | |||
if issue.IsClosed { | |||
return nil, ErrReviewRequestOnClosedPR{} | |||
} | |||
if issue.IsPull { | |||
if err := issue.LoadPullRequest(ctx); err != nil { | |||
return nil, err | |||
} | |||
if issue.PullRequest.HasMerged { | |||
return nil, ErrReviewRequestOnClosedPR{} | |||
} | |||
} | |||
} | |||
// if the reviewer is an official reviewer, |
@@ -288,3 +288,33 @@ func TestDeleteDismissedReview(t *testing.T) { | |||
assert.NoError(t, issues_model.DeleteReview(db.DefaultContext, review)) | |||
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID}) | |||
} | |||
func TestAddReviewRequest(t *testing.T) { | |||
assert.NoError(t, unittest.PrepareTestDatabase()) | |||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) | |||
assert.NoError(t, pull.LoadIssue(db.DefaultContext)) | |||
issue := pull.Issue | |||
assert.NoError(t, issue.LoadRepo(db.DefaultContext)) | |||
reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) | |||
_, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ | |||
Issue: issue, | |||
Reviewer: reviewer, | |||
Type: issues_model.ReviewTypeReject, | |||
}) | |||
assert.NoError(t, err) | |||
pull.HasMerged = false | |||
assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged")) | |||
issue.IsClosed = true | |||
_, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{}) | |||
assert.Error(t, err) | |||
assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err)) | |||
pull.HasMerged = true | |||
assert.NoError(t, pull.UpdateCols(db.DefaultContext, "has_merged")) | |||
issue.IsClosed = false | |||
_, err = issues_model.AddReviewRequest(db.DefaultContext, issue, reviewer, &user_model.User{}) | |||
assert.Error(t, err) | |||
assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err)) | |||
} |
@@ -0,0 +1,23 @@ | |||
- | |||
id: 1 | |||
title: project without default column | |||
owner_id: 2 | |||
repo_id: 0 | |||
is_closed: false | |||
creator_id: 2 | |||
board_type: 1 | |||
type: 2 | |||
created_unix: 1688973000 | |||
updated_unix: 1688973000 | |||
- | |||
id: 2 | |||
title: project with multiple default columns | |||
owner_id: 2 | |||
repo_id: 0 | |||
is_closed: false | |||
creator_id: 2 | |||
board_type: 1 | |||
type: 2 | |||
created_unix: 1688973000 | |||
updated_unix: 1688973000 |
@@ -0,0 +1,26 @@ | |||
- | |||
id: 1 | |||
project_id: 1 | |||
title: Done | |||
creator_id: 2 | |||
default: false | |||
created_unix: 1588117528 | |||
updated_unix: 1588117528 | |||
- | |||
id: 2 | |||
project_id: 2 | |||
title: Backlog | |||
creator_id: 2 | |||
default: true | |||
created_unix: 1588117528 | |||
updated_unix: 1588117528 | |||
- | |||
id: 3 | |||
project_id: 2 | |||
title: Uncategorized | |||
creator_id: 2 | |||
default: true | |||
created_unix: 1588117528 | |||
updated_unix: 1588117528 |
@@ -567,7 +567,11 @@ var migrations = []Migration{ | |||
// v290 -> v291 | |||
NewMigration("Add PayloadVersion to HookTask", v1_22.AddPayloadVersionToHookTaskTable), | |||
// v291 -> v292 | |||
NewMigration("Add indecies to IssueDependency", v1_22.AddIndeciesToIssueDepencencies), | |||
NewMigration("Add Index to attachment.comment_id", v1_22.AddCommentIDIndexofAttachment), | |||
// v292 -> v293 | |||
NewMigration("Ensure every project has exactly one default column - No Op", noopMigration), | |||
// v293 -> v294 | |||
NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency), | |||
} | |||
// GetCurrentDBVersion returns the current db version |
@@ -3,15 +3,12 @@ | |||
package v1_22 //nolint | |||
import ( | |||
"xorm.io/xorm" | |||
) | |||
import "xorm.io/xorm" | |||
func AddIndeciesToIssueDepencencies(x *xorm.Engine) error { | |||
type IssueDependency struct { | |||
IssueID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL index"` | |||
DependencyID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL index"` | |||
func AddCommentIDIndexofAttachment(x *xorm.Engine) error { | |||
type Attachment struct { | |||
CommentID int64 `xorm:"INDEX"` | |||
} | |||
return x.Sync(&IssueDependency{}) | |||
return x.Sync(&Attachment{}) | |||
} |
@@ -0,0 +1,9 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package v1_22 //nolint | |||
// NOTE: noop the original migration has bug which some projects will be skip, so | |||
// these projects will have no default board. | |||
// So that this migration will be skipped and go to v293.go | |||
// This file is a placeholder so that readers can know what happened |
@@ -0,0 +1,108 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package v1_22 //nolint | |||
import ( | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/timeutil" | |||
"xorm.io/xorm" | |||
) | |||
// CheckProjectColumnsConsistency ensures there is exactly one default board per project present | |||
func CheckProjectColumnsConsistency(x *xorm.Engine) error { | |||
sess := x.NewSession() | |||
defer sess.Close() | |||
limit := setting.Database.IterateBufferSize | |||
if limit <= 0 { | |||
limit = 50 | |||
} | |||
type Project struct { | |||
ID int64 | |||
CreatorID int64 | |||
BoardID int64 | |||
} | |||
type ProjectBoard struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
Title string | |||
Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific board will be assigned to this board | |||
Sorting int8 `xorm:"NOT NULL DEFAULT 0"` | |||
Color string `xorm:"VARCHAR(7)"` | |||
ProjectID int64 `xorm:"INDEX NOT NULL"` | |||
CreatorID int64 `xorm:"NOT NULL"` | |||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||
} | |||
for { | |||
if err := sess.Begin(); err != nil { | |||
return err | |||
} | |||
// all these projects without defaults will be fixed in the same loop, so | |||
// we just need to always get projects without defaults until no such project | |||
var projects []*Project | |||
if err := sess.Select("project.id as id, project.creator_id, project_board.id as board_id"). | |||
Join("LEFT", "project_board", "project_board.project_id = project.id AND project_board.`default`=?", true). | |||
Where("project_board.id is NULL OR project_board.id = 0"). | |||
Limit(limit). | |||
Find(&projects); err != nil { | |||
return err | |||
} | |||
for _, p := range projects { | |||
if _, err := sess.Insert(ProjectBoard{ | |||
ProjectID: p.ID, | |||
Default: true, | |||
Title: "Uncategorized", | |||
CreatorID: p.CreatorID, | |||
}); err != nil { | |||
return err | |||
} | |||
} | |||
if err := sess.Commit(); err != nil { | |||
return err | |||
} | |||
if len(projects) == 0 { | |||
break | |||
} | |||
} | |||
sess.Close() | |||
return removeDuplicatedBoardDefault(x) | |||
} | |||
func removeDuplicatedBoardDefault(x *xorm.Engine) error { | |||
type ProjectInfo struct { | |||
ProjectID int64 | |||
DefaultNum int | |||
} | |||
var projects []ProjectInfo | |||
if err := x.Select("project_id, count(*) AS default_num"). | |||
Table("project_board"). | |||
Where("`default` = ?", true). | |||
GroupBy("project_id"). | |||
Having("count(*) > 1"). | |||
Find(&projects); err != nil { | |||
return err | |||
} | |||
for _, project := range projects { | |||
if _, err := x.Where("project_id=?", project.ProjectID). | |||
Table("project_board"). | |||
Limit(project.DefaultNum - 1). | |||
Update(map[string]bool{ | |||
"`default`": false, | |||
}); err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
} |
@@ -0,0 +1,44 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package v1_22 //nolint | |||
import ( | |||
"testing" | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/models/migrations/base" | |||
"code.gitea.io/gitea/models/project" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func Test_CheckProjectColumnsConsistency(t *testing.T) { | |||
// Prepare and load the testing database | |||
x, deferable := base.PrepareTestEnv(t, 0, new(project.Project), new(project.Board)) | |||
defer deferable() | |||
if x == nil || t.Failed() { | |||
return | |||
} | |||
assert.NoError(t, CheckProjectColumnsConsistency(x)) | |||
// check if default board was added | |||
var defaultBoard project.Board | |||
has, err := x.Where("project_id=? AND `default` = ?", 1, true).Get(&defaultBoard) | |||
assert.NoError(t, err) | |||
assert.True(t, has) | |||
assert.Equal(t, int64(1), defaultBoard.ProjectID) | |||
assert.True(t, defaultBoard.Default) | |||
// check if multiple defaults, previous were removed and last will be kept | |||
expectDefaultBoard, err := project.GetBoard(db.DefaultContext, 2) | |||
assert.NoError(t, err) | |||
assert.Equal(t, int64(2), expectDefaultBoard.ProjectID) | |||
assert.False(t, expectDefaultBoard.Default) | |||
expectNonDefaultBoard, err := project.GetBoard(db.DefaultContext, 3) | |||
assert.NoError(t, err) | |||
assert.Equal(t, int64(2), expectNonDefaultBoard.ProjectID) | |||
assert.True(t, expectNonDefaultBoard.Default) | |||
} |
@@ -319,8 +319,9 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode | |||
// Add initial creator to organization and owner team. | |||
if err = db.Insert(ctx, &OrgUser{ | |||
UID: owner.ID, | |||
OrgID: org.ID, | |||
UID: owner.ID, | |||
OrgID: org.ID, | |||
IsPublic: setting.Service.DefaultOrgMemberVisible, | |||
}); err != nil { | |||
return fmt.Errorf("insert org-user relation: %w", err) | |||
} |
@@ -123,6 +123,17 @@ func createBoardsForProjectsType(ctx context.Context, project *Project) error { | |||
return nil | |||
} | |||
board := Board{ | |||
CreatedUnix: timeutil.TimeStampNow(), | |||
CreatorID: project.CreatorID, | |||
Title: "Backlog", | |||
ProjectID: project.ID, | |||
Default: true, | |||
} | |||
if err := db.Insert(ctx, board); err != nil { | |||
return err | |||
} | |||
if len(items) == 0 { | |||
return nil | |||
} | |||
@@ -176,6 +187,10 @@ func deleteBoardByID(ctx context.Context, boardID int64) error { | |||
return err | |||
} | |||
if board.Default { | |||
return fmt.Errorf("deleteBoardByID: cannot delete default board") | |||
} | |||
if err = board.removeIssues(ctx); err != nil { | |||
return err | |||
} | |||
@@ -194,7 +209,6 @@ func deleteBoardByProjectID(ctx context.Context, projectID int64) error { | |||
// GetBoard fetches the current board of a project | |||
func GetBoard(ctx context.Context, boardID int64) (*Board, error) { | |||
board := new(Board) | |||
has, err := db.GetEngine(ctx).ID(boardID).Get(board) | |||
if err != nil { | |||
return nil, err | |||
@@ -228,7 +242,6 @@ func UpdateBoard(ctx context.Context, board *Board) error { | |||
} | |||
// GetBoards fetches all boards related to a project | |||
// if no default board set, first board is a temporary "Uncategorized" board | |||
func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { | |||
boards := make([]*Board, 0, 5) | |||
@@ -244,53 +257,64 @@ func (p *Project) GetBoards(ctx context.Context) (BoardList, error) { | |||
return append([]*Board{defaultB}, boards...), nil | |||
} | |||
// getDefaultBoard return default board and create a dummy if none exist | |||
// getDefaultBoard return default board and ensure only one exists | |||
func (p *Project) getDefaultBoard(ctx context.Context) (*Board, error) { | |||
var board Board | |||
exist, err := db.GetEngine(ctx).Where("project_id=? AND `default`=?", p.ID, true).Get(&board) | |||
has, err := db.GetEngine(ctx). | |||
Where("project_id=? AND `default` = ?", p.ID, true). | |||
Desc("id").Get(&board) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if exist { | |||
if has { | |||
return &board, nil | |||
} | |||
// represents a board for issues not assigned to one | |||
return &Board{ | |||
// create a default board if none is found | |||
board = Board{ | |||
ProjectID: p.ID, | |||
Title: "Uncategorized", | |||
Default: true, | |||
}, nil | |||
Title: "Uncategorized", | |||
CreatorID: p.CreatorID, | |||
} | |||
if _, err := db.GetEngine(ctx).Insert(&board); err != nil { | |||
return nil, err | |||
} | |||
return &board, nil | |||
} | |||
// SetDefaultBoard represents a board for issues not assigned to one | |||
// if boardID is 0 unset default | |||
func SetDefaultBoard(ctx context.Context, projectID, boardID int64) error { | |||
_, err := db.GetEngine(ctx).Where(builder.Eq{ | |||
"project_id": projectID, | |||
"`default`": true, | |||
}).Cols("`default`").Update(&Board{Default: false}) | |||
if err != nil { | |||
return err | |||
} | |||
return db.WithTx(ctx, func(ctx context.Context) error { | |||
if _, err := GetBoard(ctx, boardID); err != nil { | |||
return err | |||
} | |||
if boardID > 0 { | |||
_, err = db.GetEngine(ctx).ID(boardID).Where(builder.Eq{"project_id": projectID}). | |||
Cols("`default`").Update(&Board{Default: true}) | |||
} | |||
if _, err := db.GetEngine(ctx).Where(builder.Eq{ | |||
"project_id": projectID, | |||
"`default`": true, | |||
}).Cols("`default`").Update(&Board{Default: false}); err != nil { | |||
return err | |||
} | |||
return err | |||
_, err := db.GetEngine(ctx).ID(boardID). | |||
Where(builder.Eq{"project_id": projectID}). | |||
Cols("`default`").Update(&Board{Default: true}) | |||
return err | |||
}) | |||
} | |||
// UpdateBoardSorting update project board sorting | |||
func UpdateBoardSorting(ctx context.Context, bs BoardList) error { | |||
for i := range bs { | |||
_, err := db.GetEngine(ctx).ID(bs[i].ID).Cols( | |||
"sorting", | |||
).Update(bs[i]) | |||
if err != nil { | |||
return err | |||
return db.WithTx(ctx, func(ctx context.Context) error { | |||
for i := range bs { | |||
if _, err := db.GetEngine(ctx).ID(bs[i].ID).Cols( | |||
"sorting", | |||
).Update(bs[i]); err != nil { | |||
return err | |||
} | |||
} | |||
} | |||
return nil | |||
return nil | |||
}) | |||
} |
@@ -0,0 +1,44 @@ | |||
// Copyright 2020 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package project | |||
import ( | |||
"testing" | |||
"code.gitea.io/gitea/models/db" | |||
"code.gitea.io/gitea/models/unittest" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestGetDefaultBoard(t *testing.T) { | |||
assert.NoError(t, unittest.PrepareTestDatabase()) | |||
projectWithoutDefault, err := GetProjectByID(db.DefaultContext, 5) | |||
assert.NoError(t, err) | |||
// check if default board was added | |||
board, err := projectWithoutDefault.getDefaultBoard(db.DefaultContext) | |||
assert.NoError(t, err) | |||
assert.Equal(t, int64(5), board.ProjectID) | |||
assert.Equal(t, "Uncategorized", board.Title) | |||
projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6) | |||
assert.NoError(t, err) | |||
// check if multiple defaults were removed | |||
board, err = projectWithMultipleDefaults.getDefaultBoard(db.DefaultContext) | |||
assert.NoError(t, err) | |||
assert.Equal(t, int64(6), board.ProjectID) | |||
assert.Equal(t, int64(9), board.ID) | |||
// set 8 as default board | |||
assert.NoError(t, SetDefaultBoard(db.DefaultContext, board.ProjectID, 8)) | |||
// then 9 will become a non-default board | |||
board, err = GetBoard(db.DefaultContext, 9) | |||
assert.NoError(t, err) | |||
assert.Equal(t, int64(6), board.ProjectID) | |||
assert.False(t, board.Default) | |||
} |
@@ -92,19 +92,19 @@ func TestProjectsSort(t *testing.T) { | |||
}{ | |||
{ | |||
sortType: "default", | |||
wants: []int64{1, 3, 2, 4}, | |||
wants: []int64{1, 3, 2, 6, 5, 4}, | |||
}, | |||
{ | |||
sortType: "oldest", | |||
wants: []int64{4, 2, 3, 1}, | |||
wants: []int64{4, 5, 6, 2, 3, 1}, | |||
}, | |||
{ | |||
sortType: "recentupdate", | |||
wants: []int64{1, 3, 2, 4}, | |||
wants: []int64{1, 3, 2, 6, 5, 4}, | |||
}, | |||
{ | |||
sortType: "leastupdate", | |||
wants: []int64{4, 2, 3, 1}, | |||
wants: []int64{4, 5, 6, 2, 3, 1}, | |||
}, | |||
} | |||
@@ -113,8 +113,8 @@ func TestProjectsSort(t *testing.T) { | |||
OrderBy: GetSearchOrderByBySortType(tt.sortType), | |||
}) | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, int64(4), count) | |||
if assert.Len(t, projects, 4) { | |||
assert.EqualValues(t, int64(6), count) | |||
if assert.Len(t, projects, 6) { | |||
for i := range projects { | |||
assert.EqualValues(t, tt.wants[i], projects[i].ID) | |||
} |
@@ -24,7 +24,7 @@ type Attachment struct { | |||
IssueID int64 `xorm:"INDEX"` // maybe zero when creating | |||
ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating | |||
UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added | |||
CommentID int64 | |||
CommentID int64 `xorm:"INDEX"` | |||
Name string | |||
DownloadCount int64 `xorm:"DEFAULT 0"` | |||
Size int64 `xorm:"DEFAULT 0"` |
@@ -531,6 +531,9 @@ func (repo *Repository) GetBaseRepo(ctx context.Context) (err error) { | |||
return nil | |||
} | |||
if repo.BaseRepo != nil { | |||
return nil | |||
} | |||
repo.BaseRepo, err = GetRepositoryByID(ctx, repo.ForkID) | |||
return err | |||
} |
@@ -63,6 +63,41 @@ func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList { | |||
return RepositoryList(ValuesRepository(repoMap)) | |||
} | |||
func (repos RepositoryList) LoadUnits(ctx context.Context) error { | |||
if len(repos) == 0 { | |||
return nil | |||
} | |||
// Load units. | |||
units := make([]*RepoUnit, 0, len(repos)*6) | |||
if err := db.GetEngine(ctx). | |||
In("repo_id", repos.IDs()). | |||
Find(&units); err != nil { | |||
return fmt.Errorf("find units: %w", err) | |||
} | |||
unitsMap := make(map[int64][]*RepoUnit, len(repos)) | |||
for _, unit := range units { | |||
if !unit.Type.UnitGlobalDisabled() { | |||
unitsMap[unit.RepoID] = append(unitsMap[unit.RepoID], unit) | |||
} | |||
} | |||
for _, repo := range repos { | |||
repo.Units = unitsMap[repo.ID] | |||
} | |||
return nil | |||
} | |||
func (repos RepositoryList) IDs() []int64 { | |||
repoIDs := make([]int64, len(repos)) | |||
for i := range repos { | |||
repoIDs[i] = repos[i].ID | |||
} | |||
return repoIDs | |||
} | |||
// LoadAttributes loads the attributes for the given RepositoryList | |||
func (repos RepositoryList) LoadAttributes(ctx context.Context) error { | |||
if len(repos) == 0 { |
@@ -178,7 +178,7 @@ type FindTopicOptions struct { | |||
Keyword string | |||
} | |||
func (opts *FindTopicOptions) toConds() builder.Cond { | |||
func (opts *FindTopicOptions) ToConds() builder.Cond { | |||
cond := builder.NewCond() | |||
if opts.RepoID > 0 { | |||
cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID}) | |||
@@ -191,29 +191,24 @@ func (opts *FindTopicOptions) toConds() builder.Cond { | |||
return cond | |||
} | |||
// FindTopics retrieves the topics via FindTopicOptions | |||
func FindTopics(ctx context.Context, opts *FindTopicOptions) ([]*Topic, int64, error) { | |||
sess := db.GetEngine(ctx).Select("topic.*").Where(opts.toConds()) | |||
func (opts *FindTopicOptions) ToOrders() string { | |||
orderBy := "topic.repo_count DESC" | |||
if opts.RepoID > 0 { | |||
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") | |||
orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result | |||
} | |||
if opts.PageSize != 0 && opts.Page != 0 { | |||
sess = db.SetSessionPagination(sess, opts) | |||
} | |||
topics := make([]*Topic, 0, 10) | |||
total, err := sess.OrderBy(orderBy).FindAndCount(&topics) | |||
return topics, total, err | |||
return orderBy | |||
} | |||
// CountTopics counts the number of topics matching the FindTopicOptions | |||
func CountTopics(ctx context.Context, opts *FindTopicOptions) (int64, error) { | |||
sess := db.GetEngine(ctx).Where(opts.toConds()) | |||
if opts.RepoID > 0 { | |||
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") | |||
func (opts *FindTopicOptions) ToJoins() []db.JoinFunc { | |||
if opts.RepoID <= 0 { | |||
return nil | |||
} | |||
return []db.JoinFunc{ | |||
func(e db.Engine) error { | |||
e.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") | |||
return nil | |||
}, | |||
} | |||
return sess.Count(new(Topic)) | |||
} | |||
// GetRepoTopicByName retrieves topic from name for a repo if it exist | |||
@@ -283,7 +278,7 @@ func DeleteTopic(ctx context.Context, repoID int64, topicName string) (*Topic, e | |||
// SaveTopics save topics to a repository | |||
func SaveTopics(ctx context.Context, repoID int64, topicNames ...string) error { | |||
topics, _, err := FindTopics(ctx, &FindTopicOptions{ | |||
topics, err := db.Find[Topic](ctx, &FindTopicOptions{ | |||
RepoID: repoID, | |||
}) | |||
if err != nil { |
@@ -19,18 +19,18 @@ func TestAddTopic(t *testing.T) { | |||
assert.NoError(t, unittest.PrepareTestDatabase()) | |||
topics, _, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{}) | |||
topics, err := db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{}) | |||
assert.NoError(t, err) | |||
assert.Len(t, topics, totalNrOfTopics) | |||
topics, total, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{ | |||
topics, total, err := db.FindAndCount[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{ | |||
ListOptions: db.ListOptions{Page: 1, PageSize: 2}, | |||
}) | |||
assert.NoError(t, err) | |||
assert.Len(t, topics, 2) | |||
assert.EqualValues(t, 6, total) | |||
topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{ | |||
topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{ | |||
RepoID: 1, | |||
}) | |||
assert.NoError(t, err) | |||
@@ -38,11 +38,11 @@ func TestAddTopic(t *testing.T) { | |||
assert.NoError(t, repo_model.SaveTopics(db.DefaultContext, 2, "golang")) | |||
repo2NrOfTopics := 1 | |||
topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{}) | |||
topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{}) | |||
assert.NoError(t, err) | |||
assert.Len(t, topics, totalNrOfTopics) | |||
topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{ | |||
topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{ | |||
RepoID: 2, | |||
}) | |||
assert.NoError(t, err) | |||
@@ -55,11 +55,11 @@ func TestAddTopic(t *testing.T) { | |||
assert.NoError(t, err) | |||
assert.EqualValues(t, 1, topic.RepoCount) | |||
topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{}) | |||
topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{}) | |||
assert.NoError(t, err) | |||
assert.Len(t, topics, totalNrOfTopics) | |||
topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{ | |||
topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{ | |||
RepoID: 2, | |||
}) | |||
assert.NoError(t, err) |
@@ -256,14 +256,6 @@ func IsEmailUsed(ctx context.Context, email string) (bool, error) { | |||
return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{}) | |||
} | |||
// DeleteInactiveEmailAddresses deletes inactive email addresses | |||
func DeleteInactiveEmailAddresses(ctx context.Context) error { | |||
_, err := db.GetEngine(ctx). | |||
Where("is_activated = ?", false). | |||
Delete(new(EmailAddress)) | |||
return err | |||
} | |||
// ActivateEmail activates the email address to given user. | |||
func ActivateEmail(ctx context.Context, email *EmailAddress) error { | |||
ctx, committer, err := db.TxContext(ctx) | |||
@@ -434,7 +426,7 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail | |||
cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()}) | |||
} | |||
count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid"). | |||
count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.id = email_address.uid"). | |||
Where(cond).Count(new(EmailAddress)) | |||
if err != nil { | |||
return nil, 0, fmt.Errorf("Count: %w", err) | |||
@@ -450,7 +442,7 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail | |||
emails := make([]*SearchEmailResult, 0, opts.PageSize) | |||
err = db.GetEngine(ctx).Table("email_address"). | |||
Select("email_address.*, `user`.name, `user`.full_name"). | |||
Join("INNER", "`user`", "`user`.ID = email_address.uid"). | |||
Join("INNER", "`user`", "`user`.id = email_address.uid"). | |||
Where(cond). | |||
OrderBy(orderby). | |||
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize). | |||
@@ -539,17 +531,17 @@ func validateEmailBasic(email string) error { | |||
// validateEmailDomain checks whether the email domain is allowed or blocked | |||
func validateEmailDomain(email string) error { | |||
// if there is no allow list, then check email against block list | |||
if len(setting.Service.EmailDomainAllowList) == 0 && | |||
validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) { | |||
if !IsEmailDomainAllowed(email) { | |||
return ErrEmailInvalid{email} | |||
} | |||
// if there is an allow list, then check email against allow list | |||
if len(setting.Service.EmailDomainAllowList) > 0 && | |||
!validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) { | |||
return ErrEmailInvalid{email} | |||
return nil | |||
} | |||
func IsEmailDomainAllowed(email string) bool { | |||
if len(setting.Service.EmailDomainAllowList) == 0 { | |||
return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) | |||
} | |||
return nil | |||
return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) | |||
} |
@@ -425,7 +425,7 @@ func (u *User) GetDisplayName() string { | |||
return u.Name | |||
} | |||
// GetCompleteName returns the the full name and username in the form of | |||
// GetCompleteName returns the full name and username in the form of | |||
// "Full Name (username)" if full name is not empty, otherwise it returns | |||
// "username". | |||
func (u *User) GetCompleteName() string { | |||
@@ -1232,3 +1232,21 @@ func GetOrderByName() string { | |||
} | |||
return "name" | |||
} | |||
// IsFeatureDisabledWithLoginType checks if a user feature is disabled, taking into account the login type of the | |||
// user if applicable | |||
func IsFeatureDisabledWithLoginType(user *User, feature string) bool { | |||
// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType | |||
return (user != nil && user.LoginType > auth.Plain && setting.Admin.ExternalUserDisableFeatures.Contains(feature)) || | |||
setting.Admin.UserDisabledFeatures.Contains(feature) | |||
} | |||
// DisabledFeaturesWithLoginType returns the set of user features disabled, taking into account the login type | |||
// of the user if applicable | |||
func DisabledFeaturesWithLoginType(user *User) *container.Set[string] { | |||
// NOTE: in the long run it may be better to check the ExternalLoginUser table rather than user.LoginType | |||
if user != nil && user.LoginType > auth.Plain { | |||
return &setting.Admin.ExternalUserDisableFeatures | |||
} | |||
return &setting.Admin.UserDisabledFeatures | |||
} |
@@ -16,6 +16,7 @@ import ( | |||
"code.gitea.io/gitea/models/unittest" | |||
user_model "code.gitea.io/gitea/models/user" | |||
"code.gitea.io/gitea/modules/auth/password/hash" | |||
"code.gitea.io/gitea/modules/container" | |||
"code.gitea.io/gitea/modules/optional" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/structs" | |||
@@ -526,3 +527,37 @@ func Test_NormalizeUserFromEmail(t *testing.T) { | |||
} | |||
} | |||
} | |||
func TestDisabledUserFeatures(t *testing.T) { | |||
assert.NoError(t, unittest.PrepareTestDatabase()) | |||
testValues := container.SetOf(setting.UserFeatureDeletion, | |||
setting.UserFeatureManageSSHKeys, | |||
setting.UserFeatureManageGPGKeys) | |||
oldSetting := setting.Admin.ExternalUserDisableFeatures | |||
defer func() { | |||
setting.Admin.ExternalUserDisableFeatures = oldSetting | |||
}() | |||
setting.Admin.ExternalUserDisableFeatures = testValues | |||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) | |||
assert.Len(t, setting.Admin.UserDisabledFeatures.Values(), 0) | |||
// no features should be disabled with a plain login type | |||
assert.LessOrEqual(t, user.LoginType, auth.Plain) | |||
assert.Len(t, user_model.DisabledFeaturesWithLoginType(user).Values(), 0) | |||
for _, f := range testValues.Values() { | |||
assert.False(t, user_model.IsFeatureDisabledWithLoginType(user, f)) | |||
} | |||
// check disabled features with external login type | |||
user.LoginType = auth.OAuth2 | |||
// all features should be disabled | |||
assert.NotEmpty(t, user_model.DisabledFeaturesWithLoginType(user).Values()) | |||
for _, f := range testValues.Values() { | |||
assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f)) | |||
} | |||
} |
@@ -100,7 +100,7 @@ func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limi | |||
} | |||
if err := scanner.Err(); err != nil { | |||
return nil, fmt.Errorf("scan: %w", err) | |||
return nil, fmt.Errorf("ReadLogs scan: %w", err) | |||
} | |||
return rows, nil |
@@ -41,6 +41,12 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep { | |||
} | |||
logIndex += preStep.LogLength | |||
// lastHasRunStep is the last step that has run. | |||
// For example, | |||
// 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step1. | |||
// 2. preStep(Success) -> step1(Success) -> step2(Success) -> step3(Success) -> postStep(Success): lastHasRunStep is step3. | |||
// 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3 -> postStep(Waiting): lastHasRunStep is step2. | |||
// So its Stopped is the Started of postStep when there are no more steps to run. | |||
var lastHasRunStep *actions_model.ActionTaskStep | |||
for _, step := range task.Steps { | |||
if step.Status.HasRun() { | |||
@@ -56,11 +62,15 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep { | |||
Name: postStepName, | |||
Status: actions_model.StatusWaiting, | |||
} | |||
if task.Status.IsDone() { | |||
// If the lastHasRunStep is the last step, or it has failed, postStep has started. | |||
if lastHasRunStep.Status.IsFailure() || lastHasRunStep == task.Steps[len(task.Steps)-1] { | |||
postStep.LogIndex = logIndex | |||
postStep.LogLength = task.LogLength - postStep.LogIndex | |||
postStep.Status = task.Status | |||
postStep.Started = lastHasRunStep.Stopped | |||
postStep.Status = actions_model.StatusRunning | |||
} | |||
if task.Status.IsDone() { | |||
postStep.Status = task.Status | |||
postStep.Stopped = task.Stopped | |||
} | |||
ret := make([]*actions_model.ActionTaskStep, 0, len(task.Steps)+2) |
@@ -103,6 +103,40 @@ func TestFullSteps(t *testing.T) { | |||
{Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 100, LogLength: 0, Started: 10100, Stopped: 10100}, | |||
}, | |||
}, | |||
{ | |||
name: "all steps finished but task is running", | |||
task: &actions_model.ActionTask{ | |||
Steps: []*actions_model.ActionTaskStep{ | |||
{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090}, | |||
}, | |||
Status: actions_model.StatusRunning, | |||
Started: 10000, | |||
Stopped: 0, | |||
LogLength: 100, | |||
}, | |||
want: []*actions_model.ActionTaskStep{ | |||
{Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010}, | |||
{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090}, | |||
{Name: postStepName, Status: actions_model.StatusRunning, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 0}, | |||
}, | |||
}, | |||
{ | |||
name: "skipped task", | |||
task: &actions_model.ActionTask{ | |||
Steps: []*actions_model.ActionTaskStep{ | |||
{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, | |||
}, | |||
Status: actions_model.StatusSkipped, | |||
Started: 0, | |||
Stopped: 0, | |||
LogLength: 0, | |||
}, | |||
want: []*actions_model.ActionTaskStep{ | |||
{Name: preStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, | |||
{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, | |||
{Name: postStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, | |||
}, | |||
}, | |||
} | |||
for _, tt := range tests { | |||
t.Run(tt.name, func(t *testing.T) { |
@@ -150,13 +150,16 @@ func TruncateString(str string, limit int) string { | |||
// StringsToInt64s converts a slice of string to a slice of int64. | |||
func StringsToInt64s(strs []string) ([]int64, error) { | |||
ints := make([]int64, len(strs)) | |||
for i := range strs { | |||
n, err := strconv.ParseInt(strs[i], 10, 64) | |||
if strs == nil { | |||
return nil, nil | |||
} | |||
ints := make([]int64, 0, len(strs)) | |||
for _, s := range strs { | |||
n, err := strconv.ParseInt(s, 10, 64) | |||
if err != nil { | |||
return ints, err | |||
return nil, err | |||
} | |||
ints[i] = n | |||
ints = append(ints, n) | |||
} | |||
return ints, nil | |||
} |
@@ -138,12 +138,13 @@ func TestStringsToInt64s(t *testing.T) { | |||
assert.NoError(t, err) | |||
assert.Equal(t, expected, result) | |||
} | |||
testSuccess(nil, nil) | |||
testSuccess([]string{}, []int64{}) | |||
testSuccess([]string{"-1234"}, []int64{-1234}) | |||
testSuccess([]string{"1", "4", "16", "64", "256"}, | |||
[]int64{1, 4, 16, 64, 256}) | |||
testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256}) | |||
_, err := StringsToInt64s([]string{"-1", "a", "$"}) | |||
ints, err := StringsToInt64s([]string{"-1", "a"}) | |||
assert.Len(t, ints, 0) | |||
assert.Error(t, err) | |||
} | |||
@@ -118,11 +118,12 @@ func TestReadingBlameOutputSha256(t *testing.T) { | |||
}, | |||
} | |||
objectFormat, err := repo.GetObjectFormat() | |||
assert.NoError(t, err) | |||
for _, c := range cases { | |||
commit, err := repo.GetCommit(c.CommitID) | |||
assert.NoError(t, err) | |||
blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass) | |||
blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass) | |||
assert.NoError(t, err) | |||
assert.NotNil(t, blameReader) | |||
defer blameReader.Close() |
@@ -118,11 +118,13 @@ func TestReadingBlameOutput(t *testing.T) { | |||
}, | |||
} | |||
objectFormat, err := repo.GetObjectFormat() | |||
assert.NoError(t, err) | |||
for _, c := range cases { | |||
commit, err := repo.GetCommit(c.CommitID) | |||
assert.NoError(t, err) | |||
blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass) | |||
blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass) | |||
assert.NoError(t, err) | |||
assert.NotNil(t, blameReader) | |||
defer blameReader.Close() |
@@ -367,7 +367,6 @@ type RunStdError interface { | |||
error | |||
Unwrap() error | |||
Stderr() string | |||
IsExitCode(code int) bool | |||
} | |||
type runStdError struct { | |||
@@ -392,9 +391,9 @@ func (r *runStdError) Stderr() string { | |||
return r.stderr | |||
} | |||
func (r *runStdError) IsExitCode(code int) bool { | |||
func IsErrorExitCode(err error, code int) bool { | |||
var exitError *exec.ExitError | |||
if errors.As(r.err, &exitError) { | |||
if errors.As(err, &exitError) { | |||
return exitError.ExitCode() == code | |||
} | |||
return false |
@@ -9,6 +9,7 @@ import ( | |||
"bytes" | |||
"context" | |||
"errors" | |||
"fmt" | |||
"io" | |||
"os/exec" | |||
"strconv" | |||
@@ -311,7 +312,7 @@ func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error) | |||
return c.repo.GetFilesChangedBetween(pastCommit, c.ID.String()) | |||
} | |||
// FileChangedSinceCommit Returns true if the file given has changed since the the past commit | |||
// FileChangedSinceCommit Returns true if the file given has changed since the past commit | |||
// YOU MUST ENSURE THAT pastCommit is a valid commit ID. | |||
func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) { | |||
return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String()) | |||
@@ -396,6 +397,9 @@ func (c *Commit) GetSubModules() (*ObjectCache, error) { | |||
} | |||
} | |||
} | |||
if err = scanner.Err(); err != nil { | |||
return nil, fmt.Errorf("GetSubModules scan: %w", err) | |||
} | |||
return c.submoduleCache, nil | |||
} |
@@ -47,6 +47,12 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature { | |||
return nil | |||
} | |||
if c.Encoding != "" && c.Encoding != "UTF-8" { | |||
if _, err = fmt.Fprintf(&w, "\nencoding %s\n", c.Encoding); err != nil { | |||
return nil | |||
} | |||
} | |||
if _, err = fmt.Fprintf(&w, "\n\n%s", c.Message); err != nil { | |||
return nil | |||
} |
@@ -84,6 +84,8 @@ readLoop: | |||
commit.Committer = &Signature{} | |||
commit.Committer.Decode(data) | |||
_, _ = payloadSB.Write(line) | |||
case "encoding": | |||
_, _ = payloadSB.Write(line) | |||
case "gpgsig": | |||
fallthrough | |||
case "gpgsig-sha256": // FIXME: no intertop, so only 1 exists at present. |
@@ -140,10 +140,13 @@ func TestHasPreviousCommitSha256(t *testing.T) { | |||
commit, err := repo.GetCommit("f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc") | |||
assert.NoError(t, err) | |||
objectFormat, err := repo.GetObjectFormat() | |||
assert.NoError(t, err) | |||
parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c") | |||
notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236") | |||
assert.Equal(t, repo.objectFormat, parentSHA.Type()) | |||
assert.Equal(t, repo.objectFormat.Name(), "sha256") | |||
assert.Equal(t, objectFormat, parentSHA.Type()) | |||
assert.Equal(t, objectFormat.Name(), "sha256") | |||
haz, err := commit.HasPreviousCommit(parentSHA) | |||
assert.NoError(t, err) |
@@ -125,6 +125,73 @@ empty commit`, commitFromReader.Signature.Payload) | |||
assert.EqualValues(t, commitFromReader, commitFromReader2) | |||
} | |||
func TestCommitWithEncodingFromReader(t *testing.T) { | |||
commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074 | |||
tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5 | |||
parent 47b24e7ab977ed31c5a39989d570847d6d0052af | |||
author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 | |||
committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 | |||
encoding ISO-8859-1 | |||
gpgsig -----BEGIN PGP SIGNATURE----- | |||
iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow | |||
Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR | |||
gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq | |||
zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr | |||
frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j | |||
FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd | |||
G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn | |||
SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F | |||
yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz | |||
jw4YcO5u | |||
=r3UU | |||
-----END PGP SIGNATURE----- | |||
ISO-8859-1` | |||
sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2} | |||
gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare")) | |||
assert.NoError(t, err) | |||
assert.NotNil(t, gitRepo) | |||
defer gitRepo.Close() | |||
commitFromReader, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString)) | |||
assert.NoError(t, err) | |||
if !assert.NotNil(t, commitFromReader) { | |||
return | |||
} | |||
assert.EqualValues(t, sha, commitFromReader.ID) | |||
assert.EqualValues(t, `-----BEGIN PGP SIGNATURE----- | |||
iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow | |||
Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR | |||
gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq | |||
zOfZraLOEWRH4tZcS+u2yFLu3ez2Wqh1xW5LNy7xqEedMXEFD1HwSJ0+pjacNkzr | |||
frp6Asyt7xRI6YmgFJZJoRsS3Ktr6rtKeRL2IErSQQyorOqj6gKrglhrhfG/114j | |||
FKB1v4or0WZ1DE8iP2SJZ3n+/K1IuWAINh7MVdb7PndfBPEa+IL+ucNk5uzEE8Jd | |||
G8smGxXUeFEt2cP1dj2W8EgAxuA9sTnH9dqI5aRqy5ifDjuya7Emm8sdOUvtGdmn | |||
SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F | |||
yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz | |||
jw4YcO5u | |||
=r3UU | |||
-----END PGP SIGNATURE----- | |||
`, commitFromReader.Signature.Signature) | |||
assert.EqualValues(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5 | |||
parent 47b24e7ab977ed31c5a39989d570847d6d0052af | |||
author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 | |||
committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 | |||
encoding ISO-8859-1 | |||
ISO-8859-1`, commitFromReader.Signature.Payload) | |||
assert.EqualValues(t, "KN4CK3R <admin@oldschoolhack.me>", commitFromReader.Author.String()) | |||
commitFromReader2, err := CommitFromReader(gitRepo, sha, strings.NewReader(commitString+"\n\n")) | |||
assert.NoError(t, err) | |||
commitFromReader.CommitMessage += "\n\n" | |||
commitFromReader.Signature.Payload += "\n\n" | |||
assert.EqualValues(t, commitFromReader, commitFromReader2) | |||
} | |||
func TestHasPreviousCommit(t *testing.T) { | |||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") | |||
@@ -341,7 +341,7 @@ func checkGitVersionCompatibility(gitVer *version.Version) error { | |||
func configSet(key, value string) error { | |||
stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) | |||
if err != nil && !err.IsExitCode(1) { | |||
if err != nil && !IsErrorExitCode(err, 1) { | |||
return fmt.Errorf("failed to get git config %s, err: %w", key, err) | |||
} | |||
@@ -364,7 +364,7 @@ func configSetNonExist(key, value string) error { | |||
// already exist | |||
return nil | |||
} | |||
if err.IsExitCode(1) { | |||
if IsErrorExitCode(err, 1) { | |||
// not exist, set new config | |||
_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) | |||
if err != nil { | |||
@@ -382,7 +382,7 @@ func configAddNonExist(key, value string) error { | |||
// already exist | |||
return nil | |||
} | |||
if err.IsExitCode(1) { | |||
if IsErrorExitCode(err, 1) { | |||
// not exist, add new config | |||
_, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil) | |||
if err != nil { | |||
@@ -403,7 +403,7 @@ func configUnsetAll(key, value string) error { | |||
} | |||
return nil | |||
} | |||
if err.IsExitCode(1) { | |||
if IsErrorExitCode(err, 1) { | |||
// not exist | |||
return nil | |||
} |
@@ -0,0 +1,118 @@ | |||
// Copyright 2024 The Gitea Authors. All rights reserved. | |||
// SPDX-License-Identifier: MIT | |||
package git | |||
import ( | |||
"bufio" | |||
"bytes" | |||
"context" | |||
"errors" | |||
"fmt" | |||
"os" | |||
"strconv" | |||
"strings" | |||
"code.gitea.io/gitea/modules/util" | |||
) | |||
type GrepResult struct { | |||
Filename string | |||
LineNumbers []int | |||
LineCodes []string | |||
} | |||
type GrepOptions struct { | |||
RefName string | |||
MaxResultLimit int | |||
ContextLineNumber int | |||
IsFuzzy bool | |||
} | |||
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { | |||
stdoutReader, stdoutWriter, err := os.Pipe() | |||
if err != nil { | |||
return nil, fmt.Errorf("unable to create os pipe to grep: %w", err) | |||
} | |||
defer func() { | |||
_ = stdoutReader.Close() | |||
_ = stdoutWriter.Close() | |||
}() | |||
/* | |||
The output is like this ( "^@" means \x00): | |||
HEAD:.air.toml | |||
6^@bin = "gitea" | |||
HEAD:.changelog.yml | |||
2^@repo: go-gitea/gitea | |||
*/ | |||
var results []*GrepResult | |||
cmd := NewCommand(ctx, "grep", "--null", "--break", "--heading", "--fixed-strings", "--line-number", "--ignore-case", "--full-name") | |||
cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber)) | |||
if opts.IsFuzzy { | |||
words := strings.Fields(search) | |||
for _, word := range words { | |||
cmd.AddOptionValues("-e", strings.TrimLeft(word, "-")) | |||
} | |||
} else { | |||
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) | |||
} | |||
cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD")) | |||
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) | |||
stderr := bytes.Buffer{} | |||
err = cmd.Run(&RunOpts{ | |||
Dir: repo.Path, | |||
Stdout: stdoutWriter, | |||
Stderr: &stderr, | |||
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { | |||
_ = stdoutWriter.Close() | |||
defer stdoutReader.Close() | |||
isInBlock := false | |||
scanner := bufio.NewScanner(stdoutReader) | |||
var res *GrepResult | |||
for scanner.Scan() { | |||
line := scanner.Text() | |||
if !isInBlock { | |||
if _ /* ref */, filename, ok := strings.Cut(line, ":"); ok { | |||
isInBlock = true | |||
res = &GrepResult{Filename: filename} | |||
results = append(results, res) | |||
} | |||
continue | |||
} | |||
if line == "" { | |||
if len(results) >= opts.MaxResultLimit { | |||
cancel() | |||
break | |||
} | |||
isInBlock = false | |||
continue | |||
} | |||
if line == "--" { | |||
continue | |||
} | |||
if lineNum, lineCode, ok := strings.Cut(line, "\x00"); ok { | |||
lineNumInt, _ := strconv.Atoi(lineNum) | |||
res.LineNumbers = append(res.LineNumbers, lineNumInt) | |||
res.LineCodes = append(res.LineCodes, lineCode) | |||
} | |||
} | |||
return scanner.Err() | |||
}, | |||
}) | |||
// git grep exits by cancel (killed), usually it is caused by the limit of results | |||
if IsErrorExitCode(err, -1) && stderr.Len() == 0 { | |||
return results, nil | |||
} | |||
// git grep exits with 1 if no results are found | |||
if IsErrorExitCode(err, 1) && stderr.Len() == 0 { | |||
return nil, nil | |||
} | |||
if err != nil && !errors.Is(err, context.Canceled) { | |||
return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String()) | |||
} | |||
return results, nil | |||
} |