diff --git a/.air.toml b/.air.toml index de97bd8b29..d506c19426 100644 --- a/.air.toml +++ b/.air.toml @@ -2,9 +2,10 @@ root = "." tmp_dir = ".air" [build] +pre_cmd = ["killall -9 gitea 2>/dev/null || true"] # kill off potential zombie processes from previous runs cmd = "make --no-print-directory backend" bin = "gitea" -delay = 1000 +delay = 2000 include_ext = ["go", "tmpl"] include_file = ["main.go"] include_dir = ["cmd", "models", "modules", "options", "routers", "services"] @@ -15,8 +16,11 @@ exclude_dir = [ "modules/avatar/testdata", "modules/git/tests", "modules/migration/file_format_testdata", + "modules/markup/tests/repo/repo1_filepreview", "routers/private/tests", "services/gitdiff/testdata", + "services/migrations/testdata", + "services/webhook/sourcehut/testdata", ] exclude_regex = ["_test.go$", "_gen.go$"] stop_on_error = true diff --git a/.deadcode-out b/.deadcode-out index 940551da04..62458dd6b6 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -22,7 +22,6 @@ package "code.gitea.io/gitea/models/actions" func (ScheduleList).GetRepoIDs func (ScheduleList).LoadTriggerUser func (ScheduleList).LoadRepos - func GetVariableByID package "code.gitea.io/gitea/models/asymkey" func (ErrGPGKeyAccessDenied).Error @@ -66,6 +65,7 @@ package "code.gitea.io/gitea/models/migrations/base" func MainTest package "code.gitea.io/gitea/models/organization" + func GetTeamNamesByID func UpdateTeamUnits func (SearchMembersOptions).ToConds func UsersInTeamsCount @@ -131,6 +131,7 @@ package "code.gitea.io/gitea/models/user" func GetUserAllSettings func DeleteUserSetting func GetUserEmailsByNames + func GetUserNamesByIDs package "code.gitea.io/gitea/modules/activitypub" func CurrentTime diff --git a/.dockerignore b/.dockerignore index 4c14a94620..a1611a1ca5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -77,7 +77,6 @@ cpu.out /public/assets/css /public/assets/fonts /public/assets/img/avatar -/public/assets/img/webpack /vendor /web_src/fomantic/node_modules /web_src/fomantic/build/* @@ -95,6 +94,9 @@ cpu.out /.air /.go-licenses +# Files and folders that were previously generated +/public/assets/img/webpack + # Snapcraft snap/.snapcraft/ parts/ diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 99ce2e97d6..91019cde84 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -3,6 +3,7 @@ reportUnusedDisableDirectives: true ignorePatterns: - /web_src/js/vendor + - /web_src/fomantic parserOptions: sourceType: module @@ -309,7 +310,7 @@ rules: jquery/no-merge: [2] jquery/no-param: [2] jquery/no-parent: [0] - jquery/no-parents: [0] + jquery/no-parents: [2] jquery/no-parse-html: [2] jquery/no-prop: [2] jquery/no-proxy: [2] @@ -318,8 +319,8 @@ rules: jquery/no-show: [2] jquery/no-size: [2] jquery/no-sizzle: [0] - jquery/no-slide: [0] - jquery/no-submit: [0] + jquery/no-slide: [2] + jquery/no-submit: [2] jquery/no-text: [0] jquery/no-toggle: [2] jquery/no-trigger: [0] @@ -457,7 +458,7 @@ rules: no-jquery/no-other-utils: [2] no-jquery/no-param: [2] no-jquery/no-parent: [0] - no-jquery/no-parents: [0] + no-jquery/no-parents: [2] no-jquery/no-parse-html-literal: [0] no-jquery/no-parse-html: [2] no-jquery/no-parse-json: [2] @@ -536,7 +537,7 @@ rules: no-underscore-dangle: [0] no-unexpected-multiline: [2] no-unmodified-loop-condition: [2] - no-unneeded-ternary: [0] + no-unneeded-ternary: [2] no-unreachable-loop: [2] no-unreachable: [2] no-unsafe-finally: [2] @@ -715,12 +716,14 @@ rules: unicorn/import-style: [0] unicorn/new-for-builtins: [2] unicorn/no-abusive-eslint-disable: [0] + unicorn/no-anonymous-default-export: [0] unicorn/no-array-callback-reference: [0] unicorn/no-array-for-each: [2] unicorn/no-array-method-this-argument: [2] unicorn/no-array-push-push: [2] unicorn/no-array-reduce: [2] unicorn/no-await-expression-member: [0] + unicorn/no-await-in-promise-methods: [2] unicorn/no-console-spaces: [0] unicorn/no-document-cookie: [2] unicorn/no-empty-file: [2] @@ -737,6 +740,7 @@ rules: unicorn/no-null: [0] unicorn/no-object-as-default-parameter: [0] unicorn/no-process-exit: [0] + unicorn/no-single-promise-in-promise-methods: [2] unicorn/no-static-only-class: [2] unicorn/no-thenable: [2] unicorn/no-this-assignment: [2] diff --git a/.forgejo/testdata/build-release/Dockerfile b/.forgejo/testdata/build-release/Dockerfile index 8dccad281c..f798448261 100644 --- a/.forgejo/testdata/build-release/Dockerfile +++ b/.forgejo/testdata/build-release/Dockerfile @@ -1,4 +1,6 @@ FROM code.forgejo.org/oci/alpine:3.19 ARG RELEASE_VERSION=unkown +LABEL maintainer="contact@forgejo.org" \ + org.opencontainers.image.version="${RELEASE_VERSION}" RUN mkdir -p /app/gitea RUN ( echo '#!/bin/sh' ; echo "echo forgejo v$RELEASE_VERSION" ) > /app/gitea/gitea ; chmod +x /app/gitea/gitea diff --git a/.forgejo/workflows/backport.yml b/.forgejo/workflows/backport.yml index da064feff3..6181dcf352 100644 --- a/.forgejo/workflows/backport.yml +++ b/.forgejo/workflows/backport.yml @@ -45,39 +45,13 @@ jobs: cat <<'EOF' ${{ toJSON(github) }} EOF - - name: Fetch labels - id: fetch-labels - shell: bash - run: | - set -x - echo "Labels retrieved below" - export DEBIAN_FRONTEND=noninteractive - apt-get update -qq - apt-get -q install -qq -y jq - filtered_labels=$(echo "$LABELS" | jq -c 'map(select(.name | startswith("backport/v")))') - echo "FILTERED_LABELS=${filtered_labels}" >> $GITHUB_ENV - env: - LABELS: ${{ toJSON(github.event.pull_request.labels) }} - - name: Extract targets - id: extract-targets - shell: bash - run: | - set -x - targets="$(echo $FILTERED_LABELS | jq -c '[.[] | .name | sub("backport/"; "")]')" - echo "targets=$(echo $targets)" >> $GITHUB_OUTPUT - - - name: Printing info - shell: bash - run: | - echo "targets: ${{ steps.extract-targets.outputs.targets }}" - echo "target-branch: ${{ fromJSON(steps.extract-targets.outputs.targets)[0] }}" - echo "pull-request: ${{ github.event.pull_request.url }}" - - - uses: https://code.forgejo.org/forgejo/git-backporting@b2554a678d5ea2814f0df3abad2ac4fcdee2d81f + - uses: https://code.forgejo.org/actions/git-backporting@v4.8.0 with: - target-branch: ${{ fromJSON(steps.extract-targets.outputs.targets)[0] }}/forgejo + target-branch-pattern: "^backport/(?(v.*))$" strategy: ort strategy-option: find-renames cherry-pick-options: -x auth: ${{ secrets.BACKPORT_TOKEN }} pull-request: ${{ github.event.pull_request.url }} + auto-no-squash: true + enable-err-notification: true diff --git a/.forgejo/workflows/build-release.yml b/.forgejo/workflows/build-release.yml index f8cd69850c..eb4297c7ef 100644 --- a/.forgejo/workflows/build-release.yml +++ b/.forgejo/workflows/build-release.yml @@ -159,7 +159,7 @@ jobs: - name: build container & release if: ${{ secrets.TOKEN != '' }} - uses: https://code.forgejo.org/forgejo/forgejo-build-publish/build@v3 + uses: https://code.forgejo.org/forgejo/forgejo-build-publish/build@v5.1.1 with: forgejo: "${{ env.GITHUB_SERVER_URL }}" owner: "${{ env.GITHUB_REPOSITORY_OWNER }}" @@ -173,11 +173,12 @@ jobs: binary-name: forgejo binary-path: /app/gitea/gitea override: "${{ steps.release-info.outputs.override }}" + verify-labels: "maintainer=contact@forgejo.org,org.opencontainers.image.version=${{ steps.release-info.outputs.version }}" verbose: ${{ vars.VERBOSE || secrets.VERBOSE || 'false' }} - name: build rootless container if: ${{ secrets.TOKEN != '' }} - uses: https://code.forgejo.org/forgejo/forgejo-build-publish/build@v3 + uses: https://code.forgejo.org/forgejo/forgejo-build-publish/build@v5.1.1 with: forgejo: "${{ env.GITHUB_SERVER_URL }}" owner: "${{ env.GITHUB_REPOSITORY_OWNER }}" @@ -190,6 +191,7 @@ jobs: suffix: -rootless dockerfile: Dockerfile.rootless override: "${{ steps.release-info.outputs.override }}" + verify-labels: "maintainer=contact@forgejo.org,org.opencontainers.image.version=${{ steps.release-info.outputs.version }}" verbose: ${{ vars.VERBOSE || secrets.VERBOSE || 'false' }} - name: end-to-end tests diff --git a/.forgejo/workflows/publish-release.yml b/.forgejo/workflows/publish-release.yml index 674c1c2704..b89e8d1d7b 100644 --- a/.forgejo/workflows/publish-release.yml +++ b/.forgejo/workflows/publish-release.yml @@ -42,7 +42,7 @@ jobs: - uses: actions/checkout@v3 - name: copy & sign - uses: https://code.forgejo.org/forgejo/forgejo-build-publish/publish@v4 + uses: https://code.forgejo.org/forgejo/forgejo-build-publish/publish@v5 with: from-forgejo: ${{ vars.FORGEJO }} to-forgejo: ${{ vars.FORGEJO }} diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml index a9687db81a..6ce7141004 100644 --- a/.forgejo/workflows/renovate.yml +++ b/.forgejo/workflows/renovate.yml @@ -1,10 +1,7 @@ # -# The 2am run will rebase what needs rebasing & trigger the CI -# The 4am run will merge one of them -# -# These times are chosen to minimize the likelyhood that another PR -# is merged at the same time. This would not be necessary if automerge -# worked but as of 30 March 2024 it does not. +# Runs every 2 hours, but Renovate is limited to create new PR before 4am. +# See renovate.json for more settings. +# Automerge is enabled for Renovate PR's but need to be approved before. # name: renovate @@ -13,7 +10,7 @@ on: branches: - 'renovate/**' # self-test updates schedule: - - cron: '0 2,4 * * *' + - cron: '0 0/2 * * *' env: RENOVATE_DRY_RUN: ${{ (github.event_name != 'schedule' && github.ref_name != github.event.repository.default_branch) && 'full' || '' }} @@ -25,10 +22,11 @@ jobs: runs-on: docker container: - image: ghcr.io/visualon/renovate:37.278.0 + image: ghcr.io/visualon/renovate:37.330.1 steps: - - uses: https://code.forgejo.org/actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + - name: Load renovate repo cache + uses: https://code.forgejo.org/actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: | .tmp/cache/renovate/repository @@ -36,7 +34,8 @@ jobs: restore-keys: | repo-cache- - - run: renovate + - name: Run renovate + run: renovate env: GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }} LOG_LEVEL: debug @@ -53,7 +52,7 @@ jobs: GIT_COMMITTER_EMAIL: 'forgejo-renovate-action@forgejo.org' - name: Save renovate repo cache - if: always() && env.RENOVATE_DRY_RUN == 'true' + if: always() && env.RENOVATE_DRY_RUN != 'full' uses: https://code.forgejo.org/actions/cache/save@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: | diff --git a/.forgejo/workflows/testing.yml b/.forgejo/workflows/testing.yml index 125cb798dc..cd955c01af 100644 --- a/.forgejo/workflows/testing.yml +++ b/.forgejo/workflows/testing.yml @@ -46,7 +46,7 @@ jobs: image: 'docker.io/node:20-bookworm' services: minio: - image: bitnami/minio:2024.2.26 + image: bitnami/minio:2024.3.30 options: >- --hostname gitea.minio env: @@ -136,10 +136,12 @@ jobs: image: 'docker.io/node:20-bookworm' services: minio: - image: bitnami/minio:2024.2.26 + image: bitnami/minio:2024.3.30 env: MINIO_ROOT_USER: 123456 MINIO_ROOT_PASSWORD: 12345678 + ldap: + image: docker.io/gitea/test-openldap:latest pgsql: image: 'docker.io/postgres:15' env: @@ -176,6 +178,7 @@ jobs: TAGS: bindata RACE_ENABLED: true USE_REPO_TEST_DIR: 1 + TEST_LDAP: 1 test-sqlite: if: ${{ !startsWith(vars.ROLE, 'forgejo-') }} runs-on: docker diff --git a/.gitattributes b/.gitattributes index 51131c7d83..4e748c071a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ * text=auto eol=lf *.tmpl linguist-language=go-html-template +*.pb.go linguist-generated /assets/*.json linguist-generated /public/assets/img/svg/*.svg linguist-generated /templates/swagger/v1_json.tmpl linguist-generated diff --git a/.gitea/issue_template/bug-report.yaml b/.gitea/issue_template/bug-report.yaml index 6edbca886f..6fab61fcdc 100644 --- a/.gitea/issue_template/bug-report.yaml +++ b/.gitea/issue_template/bug-report.yaml @@ -87,4 +87,3 @@ body: - SQLite - PostgreSQL - MySQL - - MSSQL diff --git a/.gitignore b/.gitignore index b883e079d1..ebbed981e1 100644 --- a/.gitignore +++ b/.gitignore @@ -83,7 +83,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/* @@ -102,6 +101,9 @@ cpu.out /.go-licenses /.cur-deadcode-out +# Files and folders that were previously generated +/public/assets/img/webpack + # Snapcraft /gitea_a*.txt snap/.snapcraft/ diff --git a/.gitmodules b/.gitmodules index f5796e6e31..e69de29bb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "manual-testing"] - path = manual-testing - url = https://codeberg.org/forgejo/forgejo-manual-testing diff --git a/.golangci.yml b/.golangci.yml index 6d52f99401..6d835909fc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,13 +1,14 @@ linters: + enable-all: false + disable-all: true + fast: false enable: - bidichk - # - deadcode # deprecated - https://github.com/golangci/golangci-lint/issues/1841 - depguard - dupl - errcheck - forbidigo - gocritic - # - gocyclo # The cyclomatic complexety of a lot of functions is too high, we should refactor those another time. - gofmt - gofumpt - gosimple @@ -17,16 +18,11 @@ linters: - nolintlint - revive - staticcheck - # - structcheck # deprecated - https://github.com/golangci/golangci-lint/issues/1841 - stylecheck - typecheck - unconvert - unused - # - varcheck # deprecated - https://github.com/golangci/golangci-lint/issues/1841 - wastedassign - enable-all: false - disable-all: true - fast: false run: timeout: 10m @@ -35,6 +31,9 @@ run: - public - web_src +output: + sort-results: true + linters-settings: stylecheck: checks: ["all", "-ST1005", "-ST1003"] @@ -51,27 +50,37 @@ linters-settings: errorCode: 1 warningCode: 1 rules: + - name: atomic + - name: bare-return - name: blank-imports + - name: constant-logical-expr - name: context-as-argument - name: context-keys-type - name: dot-imports + - name: duplicated-imports + - name: empty-lines + - name: error-naming - name: error-return - name: error-strings - - name: error-naming + - name: errorf - name: exported + - name: identical-branches - name: if-return - name: increment-decrement - - name: var-naming - - name: var-declaration + - name: indent-error-flow + - name: modifies-value-receiver - name: package-comments - name: range - name: receiver-naming + - name: redefines-builtin-id + - name: string-of-int + - name: superfluous-else - name: time-naming + - name: unconditional-recursion - name: unexported-return - - name: indent-error-flow - - name: errorf - - name: duplicated-imports - - name: modifies-value-receiver + - name: unreachable-code + - name: var-declaration + - name: var-naming gofumpt: extra-rules: true depguard: @@ -96,8 +105,12 @@ linters-settings: issues: max-issues-per-linter: 0 max-same-issues: 0 + exclude-dirs: [node_modules, public, web_src] + exclude-case-sensitive: true exclude-rules: - # Exclude some linters from running on tests files. + - path: models/db/sql_postgres_with_schema.go + linters: + - nolintlint - path: _test\.go linters: - gocyclo @@ -115,19 +128,19 @@ issues: - path: cmd linters: - forbidigo - - linters: + - text: "webhook" + linters: - dupl - text: "webhook" - - linters: + - text: "`ID' should not be capitalized" + linters: - gocritic - text: "`ID' should not be capitalized" - - linters: + - text: "swagger" + linters: - unused - deadcode - text: "swagger" - - linters: + - text: "argument x is overwritten before first use" + linters: - staticcheck - text: "argument x is overwritten before first use" - text: "commentFormatting: put a space between `//` and comment text" linters: - gocritic diff --git a/.ignore b/.ignore index 5c945ab981..5b96dabd38 100644 --- a/.ignore +++ b/.ignore @@ -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 diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml deleted file mode 100644 index 60cce7dbf7..0000000000 --- a/.stylelintrc.yaml +++ /dev/null @@ -1,223 +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": always - "@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: true - 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: [true, ignoreAtRules: [tailwind]] - 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-anb-no-unmatchable: 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]}] diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index ae87638f1c..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,8355 +0,0 @@ -# Changelog - -This changelog goes through all the changes that have been made in each release -without substantial changes to our git log; to see the highlights of what has -been added to each release, please refer to the [blog](https://blog.gitea.com). - -## [1.21.0](https://github.com/go-gitea/gitea/releases/tag/v1.21.0) - 2023-11-14 - -* BREAKING - * Restrict certificate type for builtin SSH server (#26789) - * Refactor to use urfave/cli/v2 (#25959) - * Move public asset files to the proper directory (#25907) - * Remove commit status running and warning to align GitHub (#25839) (partially reverted: Restore warning commit status (#27504) (#27529)) - * Remove "CHARSET" config option for MySQL, always use "utf8mb4" (#25413) - * Set SSH_AUTHORIZED_KEYS_BACKUP to false (#25412) -* FEATURES - * User details page (#26713) - * Chore(actions): support cron schedule task (#26655) - * Support rebuilding issue indexer manually (#26546) - * Allow to archive labels (#26478) - * Add disable workflow feature (#26413) - * Support `.git-blame-ignore-revs` file (#26395) - * Pre-register OAuth2 applications for git credential helpers (#26291) - * Add `Retry` button when creating a mirror-repo fails (#26228) - * Artifacts retention and auto clean up (#26131) - * Serve pre-defined files in "public", add "security.txt", add CORS header for ".well-known" (#25974) - * Implement auto-cancellation of concurrent jobs if the event is push (#25716) - * Newly pushed branches hints on repository home page (#25715) - * Display branch commit status (#25608) - * Add direct serving of package content (#25543) - * Add commits dropdown in PR files view and allow commit by commit review (#25528) - * Allow package cleanup from admin page (#25307) - * Batch delete issue and improve tippy opts (#25253) - * Show branches and tags that contain a commit (#25180) - * Add actor and status dropdowns to run list (#25118) - * Allow Organisations to have a E-Mail (#25082) - * Add codeowners feature (#24910) - * Actions Artifacts support uploading multiple files and directories (#24874) - * Support configuration variables on Gitea Actions (#24724) - * Support downloading raw task logs (#24451) -* API - * Unify two factor check (#27915) (#27929) - * Fix package webhook (#27839) (#27855) - * Fix/upload artifact error windows (#27802) (#27840) - * Fix bad method call when deleting user secrets via API (#27829) (#27831) - * Do not force creation of _cargo-index repo on publish (#27266) (#27765) - * Delete repos of org when purge delete user (#27273) (#27728) - * Fix org team endpoint (#27721) (#27727) - * Api: GetPullRequestCommits: return file list (#27483) (#27539) - * Don't let API add 2 exclusive labels from same scope (#27433) (#27460) - * Redefine the meaning of column is_active to make Actions Registration Token generation easier (#27143) (#27304) - * Fix PushEvent NullPointerException jenkinsci/github-plugin (#27203) (#27251) - * Fix organization field being null in POST /orgs/{orgid}/teams (#27150) (#27163) - * Allow empty Conan files (#27092) - * Fix token endpoints ignore specified account (#27080) - * Reduce usage of `db.DefaultContext` (#27073) (#27083) (#27089) (#27103) (#27262) (#27265) (#27347) (#26076) - * Make SSPI auth mockable (#27036) - * Extract auth middleware from service (#27028) - * Add `RemoteAddress` to mirrors (#26952) - * Feat(API): add routes and functions for managing user's secrets (#26909) - * Feat(API): add secret deletion functionality for repository (#26808) - * Feat(API): add route and implementation for creating/updating repository secret (#26766) - * Add Upload URL to release API (#26663) - * Feat(API): update and delete secret for managing organization secrets (#26660) - * Feat: implement organization secret creation API (#26566) - * Add API route to list org secrets (#26485) - * Set commit id when ref used explicitly (#26447) - * PATCH branch-protection updates check list even when checks are disabled (#26351) - * Add file status for API "Get a single commit from a repository" (#16205) (#25831) - * Add API for changing Avatars (#25369) -* BUGFIXES - * Fix viewing wiki commit on empty repo (#28040) (#28044) - * Enable system users for comment.LoadPoster (#28014) (#28032) - * Fixed duplicate attachments on dump on windows (#28019) (#28031) - * Fix wrong xorm Delete usage(backport for 1.21) (#28002) - * Add word-break to repo description in home page (#27924) (#27957) - * Fix rendering assignee changed comments without assignee (#27927) (#27952) - * Add word break to release title (#27942) (#27947) - * Fix JS NPE when viewing specific range of PR commits (#27912) (#27923) - * Show correct commit sha when viewing single commit diff (#27916) (#27921) - * Fix 500 when deleting a dismissed review (#27903) (#27910) - * Fix DownloadFunc when migrating releases (#27887) (#27890) - * Fix http protocol auth (#27875) (#27876) - * Refactor postgres connection string building (#27723) (#27869) - * Close all hashed buffers (#27787) (#27790) - * Fix label render containing invalid HTML (#27752) (#27762) - * Fix duplicate project board when hitting `enter` key (#27746) (#27751) - * Fix `link-action` redirect network error (#27734) (#27749) - * Fix sticky diff header background (#27697) (#27712) - * Always delete existing scheduled action tasks (#27662) (#27688) - * Support allowed hosts for webhook to work with proxy (#27655) (#27675) - * Fix poster is not loaded in get default merge message (#27657) (#27666) - * Improve dropdown button alignment and fix hover bug (#27632) (#27637) - * Improve retrying index issues (#27554) (#27634) - * Fix 404 when deleting Docker package with an internal version (#27615) (#27630) - * Backport manually for a tmpl issue in v1.21 (#27612) - * Don't show Link to TOTP if not set up (#27585) (#27588) - * Fix data-race bug when accessing task.LastRun (#27584) (#27586) - * Fix attachment download bug (#27486) (#27571) - * Respect SSH.KeygenPath option when calculating ssh key fingerprints (#27536) (#27551) - * Improve dropdown's behavior when there is a search input in menu (#27526) (#27534) - * Fix panic in storageHandler (#27446) (#27479) - * When comparing with an non-exist repository, return 404 but 500 (#27437) (#27442) - * Fix pr template (#27436) (#27440) - * Fix git 2.11 error when checking IsEmpty (#27393) (#27397) - * Allow get release download files and lfs files with oauth2 token format (#26430) (#27379) - * Fix missing ctx for GetRepoLink in dashboard (#27372) (#27375) - * Absolute positioned checkboxes overlay floated elements (#26870) (#27366) - * Introduce fixes and more rigorous tests for 'Show on a map' feature (#26803) (#27365) - * Fix repo count in org action settings (#27245) (#27353) - * Add logs for data broken of comment review (#27326) (#27345) - * Fix the approval count of PR when there is no protection branch rule (#27272) (#27343) - * Fix Bug in Issue Config when only contact links are set (#26521) (#27334) - * Improve issue history dialog and make poster can delete their own history (#27323) (#27327) - * Fix orphan check for deleted branch (#27310) (#27321) - * Fix protected branch icon location (#26576) (#27317) - * Fix yaml test (#27297) (#27303) - * Fix some animation bugs (#27287) (#27294) - * Fix incorrect change from #27231 (#27275) (#27282) - * Add missing public user visibility in user details page (#27246) (#27250) - * Fix EOL handling in web editor (#27141) (#27234) - * Fix issues on action runners page (#27226) (#27233) - * Quote table `release` in sql queries (#27205) (#27218) - * Fix release URL in webhooks (#27182) (#27185) - * Fix review request number and add more tests (#27104) (#27168) - * Fix the variable regexp pattern on web page (#27161) (#27164) - * Fix: treat tab "overview" as "repositories" in user profiles without readme (#27124) - * Fix NPE when editing OAuth2 applications (#27078) - * Fix the incorrect route path in the user edit page. (#27007) - * Fix the secret regexp pattern on web page (#26910) - * Allow users with write permissions for issues to add attachments with API (#26837) - * Make "link-action" backend code respond correct JSON content (#26680) - * Use line-height: normal by default (#26635) - * Fix NPM packages name validation (#26595) - * Rewrite the DiffFileTreeItem and fix misalignment (#26565) - * Return empty when searching issues with no repos (#26545) - * Explain SearchOptions and fix ToSearchOptions (#26542) - * Add missing triggers to update issue indexer (#26539) - * Handle base64 decoding correctly to avoid panic (#26483) - * Avoiding accessing undefined mentionValues (#26461) - * Fix incorrect redirection in new issue using references (#26440) - * Fix the bug when getting files changed for `pull_request_target` event (#26320) - * Remove IsWarning in tmpl (#26120) - * Fix loading `LFS_JWT_SECRET` from wrong section (#26109) - * Fixing redirection issue for logged-in users (#26105) - * Improve "gitea doctor" sub-command and fix "help" commands (#26072) - * Fix the truncate and alignment problem for some admin tables (#26042) - * Update minimum password length requirements (#25946) - * Do not "guess" the file encoding/BOM when using API to upload files (#25828) - * Restructure issue list template, styles (#25750) - * Fix `ref` for workflows triggered by `pull_request_target` (#25743) - * Fix issues indexer document mapping (#25619) - * Use JSON response for "user/logout" (#25522) - * Fix migrate page layout on mobile (#25507) - * Link to existing PR when trying to open a new PR on the same branches (#25494) - * Do not publish docker release images on `-dev` tags (#25471) - * Support `pull_request_target` event (#25229) - * Modify the content format of the Feishu webhook (#25106) -* ENHANCEMENTS - * Render email addresses as such if followed by punctuation (#27987) (#27992) - * Show error toast when file size exceeds the limits (#27985) (#27986) - * Fix citation error when the file size is larger than 1024 bytes (#27958) (#27965) - * Remove action runners on user deletion (#27902) (#27908) - * Remove set tabindex on view issue (#27892) (#27896) - * Reduce margin/padding on flex-list items and divider (#27872) (#27874) - * Change katex limits (#27823) (#27868) - * Clean up template locale usage (#27856) (#27857) - * Add dedicated class for empty placeholders (#27788) (#27792) - * Add gap between diff boxes (#27776) (#27781) - * Fix incorrect "tab" parameter for repo search sub-template (#27755) (#27764) - * Enable followCursor for language stats bar (#27713) (#27739) - * Improve diff tree spacing (#27714) (#27719) - * Feed UI Improvements (#27356) (#27717) - * Improve feed icons and feed merge text color (#27498) (#27716) - * [FIX] resolve confusing colors in languages stats by insert a gap (#27704) (#27715) - * Add doctor dbconsistency fix to delete repos with no owner (#27290) (#27693) - * Fix required checkboxes in issue forms (#27592) (#27692) - * Hide archived labels by default from the suggestions when assigning labels for an issue (#27451) (#27661) - * Cleanup repo details icons/labels (#27644) (#27654) - * Keep filter when showing unfiltered results on explore page (#27192) (#27589) - * Show manual cron run's last time (#27544) (#27577) - * Revert "Fix pr template (#27436)" (#27567) - * Increase queue length (#27555) (#27562) - * Avoid run change title process when the title is same (#27467) (#27558) - * Remove max-width and add hide text overflow (#27359) (#27550) - * Add hover background to wiki list page (#27507) (#27521) - * Fix mermaid flowchart margin issue (#27503) (#27516) - * Refactor system setting (#27000) (#27452) - * Fix missing `ctx` in new_form.tmpl (#27434) (#27438) - * Add Index to `action.user_id` (#27403) (#27425) - * Don't use subselect in `DeleteIssuesByRepoID` (#27332) (#27408) - * Add support for HEAD ref in /src/branch and /src/commit routes (#27384) (#27407) - * Make Actions tasks/jobs timeouts configurable by the user (#27400) (#27402) - * Hide archived labels when filtering by labels on the issue list (#27115) (#27381) - * Highlight user details link (#26998) (#27376) - * Add protected branch name description (#27257) (#27351) - * Improve tree not found page (#26570) (#27346) - * Add Index to `comment.dependent_issue_id` (#27325) (#27340) - * Improve branch list UI (#27319) (#27324) - * Fix divider in subscription page (#27298) (#27301) - * Add missed return to actions view fetch (#27289) (#27293) - * Backport ctx locale refactoring manually (#27231) (#27259) (#27260) - * Disable `Test Delivery` and `Replay` webhook buttons when webhook is inactive (#27211) (#27253) - * Use mask-based fade-out effect for `.new-menu` (#27181) (#27243) - * Cleanup locale function usage (#27227) (#27240) - * Fix z-index on markdown completion (#27237) (#27239) - * Fix Fomantic UI dropdown icon bug when there is a search input in menu (#27225) (#27228) - * Allow copying issue comment link on archived repos and when not logged in (#27193) (#27210) - * Fix: text decorator on issue sidebar menu label (#27206) (#27209) - * Fix dropdown icon position (#27175) (#27177) - * Add index to `issue_user.issue_id` (#27154) (#27158) - * Increase auth provider icon size on login page (#27122) - * Remove a `gt-float-right` and some unnecessary helpers (#27110) - * Change green buttons to primary color (#27099) - * Use db.WithTx for AddTeamMember to avoid ctx abuse (#27095) - * Use `print` instead of `printf` (#27093) - * Remove the useless function `GetUserIssueStats` and move relevant tests to `indexer_test.go` (#27067) - * Search branches (#27055) - * Display all user types and org types on admin management UI (#27050) - * Ui correction in mobile view nav bar left aligned items. (#27046) - * Chroma color tweaks (#26978) - * Move some functions to service layer (#26969) - * Improve "language stats" UI (#26968) - * Replace `util.SliceXxx` with `slices.Xxx` (#26958) - * Refactor dashboard/feed.tmpl (#26956) - * Move repository deletion to service layer (#26948) - * Fix the missing repo count (#26942) - * Improve hint when uploading a too large avatar (#26935) - * Extract common code to new template (#26933) - * Move createrepository from module to service layer (#26927) - * Move notification interface to services layer (#26915) - * Move feed notification service layer (#26908) - * Move ui notification to service layer (#26907) - * Move indexer notification to service layer (#26906) - * Move mail notification logic to service layer (#26905) - * Extract common code to new template (#26903) - * Show queue's active worker number (#26896) - * Fix media description render for orgmode (#26895) - * Remove CSS `has` selector and improve various styles (#26891) - * Relocate the `RSS user feed` button (#26882) - * Refactor "shortsha" (#26877) - * Refactor `og:description` to limit the max length (#26876) - * Move web/api context related testing function into a separate package (#26859) - * Redable error on S3 storage connection failure (#26856) - * Improve opengraph previews (#26851) - * Add more descriptive error on forgot password page (#26848) - * Show always repo count in header (#26842) - * Remove "TODO" tasks from CSS file (#26835) - * Render code blocks in repo description (#26830) - * Minor dashboard tweaks, fix flex-list margins (#26829) - * Remove polluted `.ui.right` (#26825) - * Display archived labels specially when listing labels (#26820) - * Remove polluted ".ui.left" style (#26809) - * Make it posible to customize nav text color via css var (#26807) - * Refactor lfs requests (#26783) - * Improve flex list item padding (#26779) - * Remove fomantic `text` module (#26777) - * Remove fomantic `item` module (#26775) - * Remove redundant nil check in `WalkGitLog` (#26773) - * Reduce some allocations in type conversion (#26772) - * Refactor some CSS styles and simplify code (#26771) - * Unify `border-radius` behavior (#26770) - * Improve modal dialog UI (#26764) - * Allow "latest" to be used in release vTag when downloading file (#26748) - * Adding hint `Archived` to archive label. (#26741) - * Move `modules/mirror` to `services` (#26737) - * Add "dir=auto" for input/textarea elements by default (#26735) - * Add auth-required to config.json for Cargo http registry (#26729) - * Simplify helper CSS classes and avoid abuse (#26728) - * Make web context initialize correctly for different cases (#26726) - * Focus editor on "Write" tab click (#26714) - * Remove incorrect CSS helper classes (#26712) - * Fix review bar misalignment (#26711) - * Add reverseproxy auth for API back with default disabled (#26703) - * Add default label in branch select list (#26697) - * Improve Image Diff UI (#26696) - * Fixed text overflow in dropdown menu (#26694) - * [Refactor] getIssueStatsChunk to move inner function into own one (#26671) - * Remove fomantic loader module (#26670) - * Add `member`, `collaborator`, `contributor`, and `first-time contributor` roles and tooltips (#26658) - * Improve some flex layouts (#26649) - * Improve the branch selector tab UI (#26631) - * Improve show role (#26621) - * Remove avatarHTML from template helpers (#26598) - * Allow text selection in actions step header (#26588) - * Improve translation of milestone filters (#26569) - * Add optimistic lock to ActionRun table (#26563) - * Update team invitation email link (#26550) - * Differentiate better between user settings and admin settings (#26538) - * Check disabled workflow when rerun jobs (#26535) - * Improve deadline icon location in milestone list page (#26532) - * Improve repo sub menu (#26531) - * Fix the display of org level badges (#26504) - * Rename `Sync2` -> `Sync` (#26479) - * Fix stderr usages (#26477) - * Remove fomantic transition module (#26469) - * Refactor tests (#26464) - * Refactor project templates (#26448) - * Fall back to esbuild for css minify (#26445) - * Always show usernames in reaction tooltips (#26444) - * Use correct pull request commit link instead of a generic commit link (#26434) - * Refactor "editorconfig" (#26391) - * Make `user-content-* ` consistent with github (#26388) - * Remove unnecessary template helper repoAvatar (#26387) - * Remove unnecessary template helper DisableGravatar (#26386) - * Use template context function for avatar rendering (#26385) - * Rename code_langauge.go to code_language.go (#26377) - * Use more `IssueList` instead of `[]*Issue` (#26369) - * Do not highlight `#number` in documents (#26365) - * Fix display problems of members and teams unit (#26363) - * Fix 404 error when remove self from an organization (#26362) - * Improve CLI and messages (#26341) - * Refactor backend SVG package and add tests (#26335) - * Add link to job details and tooltip to commit status in repo list in dashboard (#26326) - * Use yellow if an approved review is stale (#26312) - * Remove commit load branches and tags in wiki repo (#26304) - * Add highlight to selected repos in milestone dashboard (#26300) - * Delete `issue_service.CreateComment` (#26298) - * Do not show Profile README when repository is private (#26295) - * Tweak actions menu (#26278) - * Start using template context function (#26254) - * Use calendar icon for `Joined on...` in profiles (#26215) - * Add 'Show on a map' button to Location in profile, fix layout (#26214) - * Render plaintext task list items for markdown files (#26186) - * Add tooltip to describe LFS table column and color `delete LFS file` button red (#26181) - * Release attachments duplicated check (#26176) - * De-emphasize issue sidebar buttons (#26171) - * Fixing the align of commit stats in commit_page template. (#26161) - * Allow editing push mirrors after creation (#26151) - * Move web JSON functions to web context and simplify code (#26132) - * Refactor improve NoBetterThan (#26126) - * Improve clickable area in repo action view page (#26115) - * Add context parameter to some database functions (#26055) - * Docusaurus-ify (#26051) - * Improve text for empty issue/pr description (#26047) - * Categorize admin settings sidebar panel (#26030) - * Remove redundant "RouteMethods" method (#26024) - * Refactor and enhance issue indexer to support both searching, filtering and paging (#26012) - * Add a link to OpenID Issuer URL in WebFinger response (#26000) - * Fix UI for release tag page / wiki page / subscription page (#25948) - * Support copy protected branch from template repository (#25889) - * Improve display of Labels/Projects/Assignees sort options (#25886) - * Fix margin on the new/edit project page. (#25885) - * Show image size on view page (#25884) - * Remove ref name in PR commits page (#25876) - * Allow the use of alternative net.Listener implementations by downstreams (#25855) - * Refactor "Content" for file uploading (#25851) - * Add error info if no user can fork the repo (#25820) - * Show edit title button on commits tab of PR, too (#25791) - * Introduce `flex-list` & `flex-item` elements for Gitea UI (#25790) - * Don't stack PR tab menu on small screens (#25789) - * Repository Archived text title center align (#25767) - * Make route middleware/handler mockable (#25766) - * Move issue filters to shared template (#25729) - * Use frontend fetch for branch dropdown component (#25719) - * Add open/closed field support for issue index (#25708) - * Some less naked returns (#25682) - * Fix inconsistent user profile layout across tabs (#25625) - * Get latest commit statuses from database instead of git data on dashboard for repositories (#25605) - * Adding branch-name copy to clipboard branches screen. (#25596) - * Update emoji set to Unicode 15 (#25595) - * Move some files under repo/setting (#25585) - * Add custom ansi colors and CSS variables for them (#25546) - * Add log line anchor for action logs (#25532) - * Use flex instead of float for sort button and search input (#25519) - * Update octicons and use `octicon-file-directory-symlink` (#25453) - * Add toasts to UI (#25449) - * Fine tune project board label colors and modal content background (#25419) - * Import additional secrets via file uri (#25408) - * Switch to ansi_up for ansi rendering in actions (#25401) - * Store and use seconds for timeline time comments (#25392) - * Support displaying diff stats in PR tab bar (#25387) - * Use fetch form action for lock/unlock/pin/unpin on sidebar (#25380) - * Refactor: TotalTimes return seconds (#25370) - * Navbar styling rework (#25343) - * Introduce shared template for search inputs (#25338) - * Only show 'Manage Account Links' when necessary (#25311) - * Improve 'Privacy' section in profile settings (#25309) - * Substitute variables in path names of template repos too (#25294) - * Fix tags line no margin see #25255 (#25280) - * Use fetch to send requests to create issues/comments (#25258) - * Change form actions to fetch for submit review box (#25219) - * Improve AJAX link and modal confirm dialog (#25210) - * Reduce unnecessary DB queries for Actions tasks (#25199) - * Disable `Create column` button while the column name is empty (#25192) - * Refactor indexer (#25174) - * Adjust style for action run list (align icons, adjust padding) (#25170) - * Remove duplicated functions when deleting a branch (#25128) - * Make confusable character warning less jarring (#25069) - * Highlight viewed files differently in the PR filetree (#24956) - * Support changing labels of Actions runner without re-registration (#24806) - * Fix duplicate Reviewed-by trailers (#24796) - * Resolve issue with sort icons on admin/users and admin/runners (#24360) - * Split lfs size from repository size (#22900) - * Sync branches into databases (#22743) - * Disable run user change in installation page (#22499) - * Add merge files files to GetCommitFileStatus (#20515) - * Show OpenID Connect and OAuth on signup page (#20242) -* SECURITY - * Dont leak private users via extensions (#28023) (#28029) - * Expanded minimum RSA Keylength to 3072 (#26604) -* TESTING - * Add user secrets API integration tests (#27832) (#27852) - * Add tests for db indexer in indexer_test.go (#27087) - * Speed up TestEventSourceManagerRun (#26262) - * Add unit test for user renaming (#26261) - * Add some Wiki unit tests (#26260) - * Improve unit test for caching (#26185) - * Add unit test for `HashAvatar` (#25662) -* TRANSLATION - * Backport translations to v1.21 (#27899) - * Fix issues in translation file (#27699) (#27737) - * Add locale for deleted head branch (#26296) - * Improve multiple strings in en-US locale (#26213) - * Fix broken translations for package documantion (#25742) - * Correct translation wrong format (#25643) -* BUILD - * Dockerfile small refactor (#27757) (#27826) - * Fix build errors on BSD (in BSDMakefile) (#27594) (#27608) - * Fully replace drone with actions (#27556) (#27575) - * Enable markdownlint `no-duplicate-header` (#27500) (#27506) - * Enable production source maps for index.js, fix CSS sourcemaps (#27291) (#27295) - * Update snap package (#27021) - * Bump go to 1.21 (#26608) - * Bump xgo to go-1.21.x and node to 20 in release-version (#26589) - * Add template linting via djlint (#25212) -* DOCS - * Change default size of issue/pr attachments and repo file (#27946) (#28017) - * Remove `known issue` section in Gitea Actions Doc (#27930) (#27938) - * Remove outdated paragraphs when comparing Gitea Actions to GitHub Actions (#27119) - * Update brew installation documentation since gitea moved to brew core package (#27070) - * Actions are no longer experimental, so enable them by default (#27054) - * Add a documentation note for Windows Service (#26938) - * Add sparse url in cargo package guide (#26937) - * Update nginx recommendations (#26924) - * Update backup instructions to align with archive structure (#26902) - * Expanding documentation in queue.go (#26889) - * Update info regarding internet connection for build (#26776) - * Docs: template variables (#26547) - * Update index doc (#26455) - * Update zh-cn documentation (#26406) - * Fix typos and grammer problems for actions documentation (#26328) - * Update documentation for 1.21 actions (#26317) - * Doc update swagger doc for POST /orgs/{org}/teams (#26155) - * Doc sync authentication.md to zh-cn (#26117) - * Doc guide the user to create the appropriate level runner (#26091) - * Make organization redirect warning more clear (#26077) - * Update blog links (#25843) - * Fix default value for LocalURL (#25426) - * Update `from-source.zh-cn.md` & `from-source.en-us.md` - Cross Compile Using Zig (#25194) -* MISC - * Replace deprecated `elliptic.Marshal` (#26800) - * Add elapsed time on debug for slow git commands (#25642) - -## [1.20.5](https://github.com/go-gitea/gitea/releases/tag/v1.20.5) - 2023-10-03 - -* ENHANCEMENTS - * Fix z-index on markdown completion (#27237) (#27242 & #27238) - * Use secure cookie for HTTPS sites (#26999) (#27013) -* BUGFIXES - * Fix git 2.11 error when checking IsEmpty (#27393) (#27396) - * Allow get release download files and lfs files with oauth2 token format (#26430) (#27378) - * Fix orphan check for deleted branch (#27310) (#27320) - * Quote table `release` in sql queries (#27205) (#27219) - * Fix release URL in webhooks (#27182) (#27184) - * Fix successful return value for `SyncAndGetUserSpecificDiff` (#27152) (#27156) - * fix pagination for followers and following (#27127) (#27138) - * Fix issue templates when blank isses are disabled (#27061) (#27082) - * Fix context cache bug & enable context cache for dashabord commits' authors(#26991) (#27017) - * Fix INI parsing for value with trailing slash (#26995) (#27001) - * Fix PushEvent NullPointerException jenkinsci/github-plugin (#27203) (#27249) - * Fix organization field being null in POST /orgs/{orgid}/teams (#27150) (#27167 & #27162) - * Fix bug of review request number (#27406) (#27104) -* TESTING - * services/wiki: Close() after error handling (#27129) (#27137) -* DOCS - * Improve actions docs related to `pull_request` event (#27126) (#27145) -* MISC - * Add logs for data broken of comment review (#27326) (#27344) - * Load reviewer before sending notification (#27063) (#27064) - -## [1.20.4](https://github.com/go-gitea/gitea/releases/tag/v1.20.4) - 2023-09-08 - -* SECURITY - * Check blocklist for emails when adding them to account (#26812) (#26831) -* ENHANCEMENTS - * Add `branch_filter` to hooks API endpoints (#26599) (#26632) - * Fix incorrect "tabindex" attributes (#26733) (#26734) - * Use line-height: normal by default (#26635) (#26708) - * Fix unable to display individual-level project (#26198) (#26636) -* BUGFIXES - * Fix wrong review requested number (#26784) (#26880) - * Avoid double-unescaping of form value (#26853) (#26863) - * Redirect from `{repo}/issues/new` to `{repo}/issues/new/choose` when blank issues are disabled (#26813) (#26847) - * Sync tags when adopting repos (#26816) (#26834) - * Fix verifyCommits error when push a new branch (#26664) (#26810) - * Include the GITHUB_TOKEN/GITEA_TOKEN secret for fork pull requests (#26759) (#26806) - * Fix some slice append usages (#26778) (#26798) - * Add fix incorrect can_create_org_repo for org owner team (#26683) (#26791) - * Fix bug for ctx usage (#26763) - * Make issue template field template access correct template data (#26698) (#26709) - * Use correct minio error (#26634) (#26639) - * Ignore the trailing slashes when comparing oauth2 redirect_uri (#26597) (#26618) - * Set errwriter for urfave/cli v1 (#26616) - * Fix reopen logic for agit flow pull request (#26399) (#26613) - * Fix context filter has no effect in dashboard (#26695) (#26811) - * Fix being unable to use a repo that prohibits accepting PRs as a PR source. (#26785) (#26790) - * Fix Page Not Found error (#26768) - -## [1.20.3](https://github.com/go-gitea/gitea/releases/tag/v1.20.3) - 2023-08-20 - -* BREAKING - * Fix the wrong derive path (#26271) (#26318) -* SECURITY - * Fix API leaking Usermail if not logged in (#25097) (#26350) -* FEATURES - * Add ThreadID parameter for Telegram webhooks (#25996) (#26480) -* ENHANCEMENTS - * Add minimum polyfill to support "relative-time-element" in PaleMoon (#26575) (#26578) - * Fix dark theme highlight for "NameNamespace" (#26519) (#26527) - * Detect ogg mime-type as audio or video (#26494) (#26505) - * Use `object-fit: contain` for oauth2 custom icons (#26493) (#26498) - * Move dropzone progress bar to bottom to show filename when uploading (#26492) (#26497) - * Remove last newline from config file (#26468) (#26471) - * Minio: add missing region on client initialization (#26412) (#26438) - * Add pull request review request webhook event (#26401) (#26407) - * Fix text truncate (#26354) (#26384) - * Fix incorrect color of selected assignees when create issue (#26324) (#26372) - * Display human-readable text instead of cryptic filemodes (#26352) (#26358) - * Hide `last indexed SHA` when a repo could not be indexed yet (#26340) (#26345) - * Fix the topic validation rule and suport dots (#26286) (#26303) - * Fix due date rendering the wrong date in issue (#26268) (#26274) - * Don't autosize textarea in diff view (#26233) (#26244) - * Fix commit compare style (#26209) (#26226) - * Warn instead of reporting an error when a webhook cannot be found (#26039) (#26211) -* BUGFIXES - * Use "input" event instead of "keyup" event for migration form (#26602) (#26605) - * Do not use deprecated log config options by default (#26592) (#26600) - * Fix "issueReposQueryPattern does not match query" (#26556) (#26564) - * Sync repo's IsEmpty status correctly (#26517) (#26560) - * Fix project filter bugs (#26490) (#26558) - * Use `hidden` over `clip` for text truncation (#26520) (#26522) - * Set "type=button" for editor's toolbar buttons (#26510) (#26518) - * Fix NuGet search endpoints (#25613) (#26499) - * Fix storage path logic especially for relative paths (#26441) (#26481) - * Close stdout correctly for "git blame" (#26470) (#26473) - * Check first if minio bucket exists before trying to create it (#26420) (#26465) - * Avoiding accessing undefined tributeValues #26461 (#26462) - * Call git.InitSimple for runRepoSyncReleases (#26396) (#26450) - * Add transaction when creating pull request created dirty data (#26259) (#26437) - * Fix wrong middleware sequence (#26428) (#26436) - * Fix admin queue page title and fix CI failures (#26409) (#26421) - * Introduce ctx.PathParamRaw to avoid incorrect unescaping (#26392) (#26405) - * Bypass MariaDB performance bug of the "IN" sub-query, fix incorrect IssueIndex (#26279) (#26368) - * Fix incorrect CLI exit code and duplicate error message (#26346) (#26347) - * Prevent newline errors with Debian packages (#26332) (#26342) - * Fix bug with sqlite load read (#26305) (#26339) - * Make git batch operations use parent context timeout instead of default timeout (#26325) (#26330) - * Support getting changed files when commit ID is `EmptySHA` (#26290) (#26316) - * Clarify the logger's MODE config option (#26267) (#26281) - * Use shared template for webhook icons (#26242) (#26246) - * Fix pull request check list is limited (#26179) (#26245) - * Fix attachment clipboard copy on insecure origin (#26224) (#26231) - * Fix access check for org-level project (#26182) (#26223) -* MISC - * Improve profile readme rendering (#25988) (#26453) - * [docs] Add missing backtick in quickstart.zh-cn.md (#26349) (#26357) - * Upgrade x/net to 0.13.0 (#26301) - -## [1.20.2](https://github.com/go-gitea/gitea/releases/tag/v1.20.2) - 2023-07-29 - -* ENHANCEMENTS - * Calculate MAX_WORKERS default value by CPU number (#26177) (#26183) - * Display deprecated warning in admin panel pages as well as in the log file (#26094) (#26154) -* BUGFIXES - * Fix allowed user types setting problem (#26200) (#26206) - * Fix handling of plenty Nuget package versions (#26075) (#26173) - * Fix UI regression of asciinema player (#26159) (#26162) - * Fix LFS object list style (#26133) (#26147) - * Fix allowed user types setting problem (#26200) (#26206) - * Prevent primary key update on migration (#26192) (#26199) - * Fix bug when pushing to a pull request which enabled dismiss approval automatically (#25882) (#26158) - * Fix bugs in LFS meta garbage collection (#26122) (#26157) - * Update xorm version (#26128) (#26150) - * Remove "misc" scope check from public API endpoints (#26134) (#26149) - * Fix CLI allowing creation of access tokens with existing name (#26071) (#26144) - * Fix incorrect router logger (#26137) (#26143) - * Improve commit graph alignment and truncating (#26112) (#26127) - * Avoid writing config file if not installed (#26107) (#26113) - * Fix escape problems in the branch selector (#25875) (#26103) - * Fix handling of Debian files with trailing slash (#26087) (#26098) - * Fix Missing 404 swagger response docs for /admin/users/{username} (#26086) (#26089) - * Use stderr as fallback if the log file can't be opened (#26074) (#26083) - * Increase table cell horizontal padding (#26140) (#26142) - * Fix wrong workflow status when rerun a job in an already finished workflow (#26119) (#26124) - * Fix duplicated url prefix on issue context menu (#26066) (#26067) - -## [1.20.1](https://github.com/go-gitea/gitea/releases/tag/v1.20.1) - 2023-07-22 - -* SECURITY - * Disallow dangerous URL schemes (#25960) (#25964) -* ENHANCEMENTS - * Show the mismatched ROOT_URL warning on the sign-in page if OAuth2 is enabled (#25947) (#25972) - * Make pending commit status yellow again (#25935) (#25968) -* BUGFIXES - * Fix version in rpm repodata/primary.xml.gz (#26009) (#26048) - * Fix env config parsing for "GITEA____APP_NAME" (#26001) (#26013) - * ParseScope with owner/repo always sets owner to zero (#25987) (#25989) - * Fix SSPI auth panic (#25955) (#25969) - * Avoid creating directories when loading config (#25944) (#25957) - * Make environment-to-ini work with INSTALL_LOCK=true (#25926) (#25937) - * Ignore `runs-on` with expressions when warning no matched runners (#25917) (#25933) - * Avoid opening/closing PRs which are already merged (#25883) (#25903) -* DOCS - * RPM Registry: Show zypper commands for SUSE based distros as well (#25981) (#26020) - * Correctly refer to dev tags as nightly in the docker docs (#26004) (#26019) - * Update path related documents (#25417) (#25982) -* MISC - * Adding remaining enum for migration repo model type. (#26021) (#26034) - * Fix the route for pull-request's authors (#26016) (#26018) - * Fix commit status color on dashboard repolist (#25993) (#25998) - * Avoid hard-coding height in language dropdown menu (#25986) (#25997) - * Add shutting down notice (#25920) (#25922) - * Fix incorrect milestone count when provide a keyword (#25880) (#25904) - -## [1.20.0](https://github.com/go-gitea/gitea/releases/tag/v1.20.0) - 2023-07-16 - -* BREAKING - * Fix WORK_DIR for docker (root) image (#25738) (#25811) - * Restrict `[actions].DEFAULT_ACTIONS_URL` to only `github` or `self` (#25581) (#25604) - * Refactor path & config system (#25330) (#25416) - * Fix all possible setting error related storages and added some tests (#23911) (#25244) - * Use a separate admin page to show global stats, remove `actions` stat (#25062) - * Remove the service worker (#25010) - * Remove meta tags `theme-color` and `default-theme` (#24960) - * Use `[git.config]` for reflog cleaning up (#24958) - * Allow all URL schemes in Markdown links by default (#24805) - * Redesign Scoped Access Tokens (#24767) - * Fix team members API endpoint pagination (#24754) - * Rewrite logger system (#24726) - * Increase default LFS auth timeout from 20m to 24h (#24628) - * Rewrite queue (#24505) - * Remove unused setting `time.FORMAT` (#24430) - * Refactor `setting.Other` and remove unused `SHOW_FOOTER_BRANDING` (#24270) - * Correct the access log format (#24085) - * Reserve ".png" suffix for user/org names (#23992) - * Prefer native parser for SSH public key parsing (#23798) - * Editor preview support for external renderers (#23333) - * Add Gitea Profile Readmes (#23260) - * Refactor `ctx` in templates (#23105) -* SECURITY - * Test if container blob is accessible before mounting (#22759) (#25784) - * Set type="password" on all auth_token fields (#22175) -* FEATURES - * Add button on diff header to copy file name, misc diff header tweaks (#24986) - * API endpoint for changing/creating/deleting multiple files (#24887) - * Support changing git config through `app.ini`, use `diff.algorithm=histogram` by default (#24860) - * Add up and down arrows to selected lookup repositories (#24727) - * Add Go package registry (#24687) - * Add status indicator on main home screen for each repo (#24638) - * Support for status check pattern (#24633) - * Implement Cargo HTTP index (#24452) - * Add Debian package registry (#24426) - * Add the ability to pin Issues (#24406) - * Add follow organization and fix the logic of following page (#24345) - * Allow `webp` images as avatars (#24248) - * Support upload `outputs` and use `needs` context on Actions (#24230) - * Allow adding new files to an empty repo (#24164) - * Make wiki title supports dashes and improve wiki name related features (#24143) - * Add monospace toggle button to textarea (#24034) - * Use auto-updating, natively hoverable, localized time elements (#23988) - * Add ntlm authentication support for mail (#23811) - * Add CLI command to register runner tokens (#23762) - * Add Alpine package registry (#23714) - * Expand/Collapse all changed files (#23639) - * Add unset default project column (#23531) - * Add activity feeds API (#23494) - * Add RPM registry (#23380) - * Add meilisearch support (#23136) - * Add API for License templates (#23009) - * Add admin API email endpoints (#22792) - * Add user rename endpoint to admin api (#22789) - * Add API for gitignore templates (#22783) - * Implement actions artifacts (#22738) - * Add RSS Feeds for branches and files (#22719) - * Display when a repo was archived (#22664) - * Add Swift package registry (#22404) - * Add CRAN package registry (#22331) - * Add user webhooks (#21563) - * Implement systemd-notify protocol (#21151) - * Implement Issue Config (#20956) - * Add API to manage issue dependencies (#17935) -* API - * Use correct response code in push mirror creation response in v1_json.tmpl (#25476) (#25571) - * Fix `Permission` in API returned repository struct (#25388) (#25441) - * Add API for Label templates (#24602) - * Filters for GetAllCommits (#24568) - * Add ability to specify '--not' from GetAllCommits (#24409) - * Support uploading file to empty repo by API (#24357) - * Add absent repounits to create/edit repo API (#23500) - * Add login name and source id for admin user searching API (#23376) - * Create a branch directly from commit on the create branch API (#22956) -* ENHANCEMENTS - * Make `add line comment` buttons focusable (#25894) (#25896) - * Always pass 6-digit hex color to monaco (#25780) (#25782) - * Clarify "text-align" CSS helpers, fix clone button padding (#25763) (#25764) - * Hide `add file` button for pull mirrors (#25748) (#25751) - * Allow/fix review (approve/reject) of empty PRs (#25690) (#25732) - * Fix tags header and pretty format numbers (#25624) (#25694) - * Actions list enhancements (#25601) (#25678) - * Fix show more for image on diff page (#25672) (#25673) - * Prevent SVG shrinking (#25652) (#25669) - * Fix UI misalignment on user setting page (#25629) (#25656) - * Use css on labels (#25626) (#25636) - * Read-only checkboxes don't appear and don't entirely act the way one might expect (#25573) (#25602) - * Redirect to package after version deletion (#25594) (#25599) - * Reduce table padding globally (#25568) (#25577) - * Change `Regenerate Secret` button display (#25534) (#25541) - * Fix rerun icon on action view component (#25531) (#25536) - * Move some regexp out of functions (#25430) (#25445) - * Diff page enhancements (#25398) (#25437) - * Various UI fixes (#25264) (#25431) - * Fix label list divider (#25312) (#25372) - * Fix UI on mobile view (#25315) (#25340) - * When viewing a file, hide the add button (#25320) (#25339) - * Show if File is Executable (#25287) (#25300) - * Fix edit OAuth application width (#25262) (#25263) - * Use flex to align SVG and text (#25163) (#25260) - * Revert overflow: overlay (revert #21850) (#25231) (#25239) - * Use inline SVG for built-in OAuth providers (#25171) (#25234) - * Change access token UI to select dropdowns (#25109) (#25230) - * Remove hacky patch for "safari emoji glitch fix" (#25208) (#25211) - * Minor arc-green color tweaks (#25175) (#25205) - * Button and color enhancements (#24989) (#25176) - * Fix mobile navbar and misc cleanups (#25134) (#25169) - * Modify OAuth login ui and fix display name, iconurl related logic (#25030) (#25161) - * Improve notification icon and navbar (#25111) (#25124) - * Add details summary for vertical menus in settings to allow toggling (#25098) - * Don't display `select all issues` checkbox when no issues are available (#25086) - * Use RepositoryList instead of []*Repository (#25074) - * Add ability to set multiple redirect URIs in OAuth application UI (#25072) - * Use git command instead of the ini package to remove the `origin` remote (#25066) - * Remove cancel button from branch protection form (#25063) - * Show file tree by default (#25052) - * Add Progressbar to Milestone Page (#25050) - * Minor UI improvements: logo alignment, auth map editor, auth name display (#25043) - * Allow for PKCE flow without client secret + add docs (#25033) - * Refactor INI package (first step) (#25024) - * Various style fixes (#25008) - * Fix delete user account modal (#25004) - * Refactor diffFileInfo / DiffTreeStore (#24998) - * Add user level action runners (#24995) - * Rename NotifyPullReviewRequest to NotifyPullRequestReviewRequest (#24988) - * Add step start time to `ViewStepLog` (#24980) - * Add dark mode to API Docs (#24971) - * Display file mode for new file and file mode changes (#24966) - * Make the 500 page load themes (#24953) - * Show `bot` label next to username when rendering autor link if the user is a bot (#24943) - * Repo list improvements, fix bold helper classes (#24935) - * Improve queue and logger context (#24924) - * Improve RunMode / dev mode (#24886) - * Improve some Forms (#24878) - * Add show timestamp/seconds and fullscreen options to action page (#24876) - * Fix double border and adjust width for user profile page (#24870) - * Improve Actions CSS (#24864) - * Fix `@font-face` overrides (#24855) - * Remove `In your repositories` link in milestones dashboard (#24853) - * Fix missing yes/no in delete time log modal (#24851) - * Show new pull request button also on subdirectories and files (#24842) - * Make environment-to-ini support loading key value from file (#24832) - * Support wildcard in email domain allow/block list (#24831) - * Use `CommentList` instead of `[]*Comment` (#24828) - * Add RTL rendering support to Markdown (#24816) - * Rework notifications list (#24812) - * Mute repo names in dashboard repo list (#24811) - * Fix max width and margin of comment box on conversation page (#24809) - * Some refactors for issues stats (#24793) - * Rework label colors (#24790) - * Fix OAuth login loading state (#24788) - * Remove duplicated issues options and some more refactors (#24787) - * Decouple the different contexts from each other (#24786) - * Remove background on user dashboard filter bar (#24779) - * Improve and fix bugs surrounding reactions (#24760) - * Make the color of zero-contribution-squares in the activity heatmap more subtle (#24758) - * Fix WEBP image copying (#24743) - * Rework OAuth login buttons, swap github logo to monocolor (#24740) - * Consolidate the two review boxes into one (#24738) - * Unification of registration fields order (#24737) - * Refactor Pull Mirror and fix out-of-sync bugs (#24732) - * Improvements for action detail page (#24718) - * Fix flash of unstyled content in action view page (#24712) - * Don't filter action runs based on state (#24711) - * Optimize actions list by removing an unnecessary `git` call (#24710) - * Support no label/assignee filter and batch clearing labels/assignees (#24707) - * Add icon support for safari (#24697) - * Use standard HTTP library to serve files (#24693) - * Improve button-ghost, remove tertiary button (#24692) - * Only hide tooltip tippy instances (#24688) - * Support migrating storage for actions log via command line (#24679) - * Remove highlight in repo list (#24675) - * Add markdown preview to Submit Review Textarea (#24672) - * Update pin and add pin-slash (#24669) - * Improve empty notifications display (#24668) - * Support SSH for go get (#24664) - * Improve avatar uploading / resizing / compressing, remove Fomantic card module (#24653) - * Only show one tippy at a time (#24648) - * Notification list enhancements, fix striped tables on dark theme (#24639) - * Improve queue & process & stacktrace (#24636) - * Use the type RefName for all the needed places and fix pull mirror sync bugs (#24634) - * Remove fluid on compare diff page (#24627) - * Add a tooltip to the job rerun button (#24617) - * Attach a tooltip to the action status icon (#24614) - * Make the actions control button look like an actual button (#24611) - * Remove unnecessary code (#24610) - * Make repo migration cancelable and fix various bugs (#24605) - * Improve updating Actions tasks (#24600) - * Attach a tooltip to the action control button (#24595) - * Make repository response support HTTP range request (#24592) - * Improve Gitea's web context, decouple "issue template" code into service package (#24590) - * Modify luminance calculation and extract related functions into single files (#24586) - * Simplify template helper functions (#24570) - * Split "modules/context.go" to separate files (#24569) - * Add org visibility label to non-organization's dashboard (#24558) - * Update LDAP filters to include both username and email address (#24547) - * Review fixes and enhancements (#24526) - * Display warning when user try to rename default branch (#24512) - * Fix color for transfer related buttons when having no permission to act (#24510) - * Rework button coloring, add focus and active colors (#24507) - * New webhook trigger for receiving Pull Request review requests (#24481) - * Add goto issue id function (#24479) - * Fix incorrect webhook time and use relative-time to display it (#24477) - * RSS icon fixes (#24476) - * Replace `N/A` with `-` everywhere (#24474) - * Pass 'not' to commit count (#24473) - * Enhance stylelint rule config, remove dead CSS (#24472) - * Remove `font-awesome` and fomantic `icon` module (#24471) - * Improve "new-menu" (#24465) - * Remove fomantic breadcrumb module (#24463) - * Improve template system and panic recovery (#24461) - * Make Issue/PR/projects more compact, misc CSS tweaks (#24459) - * Replace remaining fontawesome dropdown icons with SVG (#24455) - * Remove all direct references to font-awesome (#24448) - * Move links out of translation (#24446) - * Add `ui-monospace` and `SF Mono` to `--fonts-monospace` (#24442) - * Hide 'Mirror Settings' when unneeded, improve hints (#24433) - * Add "Updated" column for admin repositories list (#24429) - * Improve issue list filter (#24425) - * Rework header bar on issue, pull requests and milestone (#24420) - * Improve template helper (#24417) - * Make repo size style matches others (commits/branches/tags) (#24408) - * Support markdown editor for issue template (#24400) - * Improve commit date in commit graph (#24399) - * Start cleaning the messy ".ui.left / .ui.right", improve label list page, fix stackable menu (#24393) - * Merge setting.InitXXX into one function with options (#24389) - * Move `Rename branch` from repo settings page to the page of branches list (#24380) - * Improve protected branch setting page (#24379) - * Display 'Unknown' when runner.version is empty (#24378) - * Display owner of a runner as a tooltip instead of static text (#24377) - * Fix incorrect last online time in runner_edit.tmpl (#24376) - * Fix unclear `IsRepositoryExist` logic (#24374) - * Add custom helm repo name generated from url (#24363) - * Replace placeholders in licenses (#24354) - * Add rerun workflow button and refactor to use SVG octicons (#24350) - * Fix runner button height (#24338) - * Restore bold on repolist (#24337) - * Improve RSS (#24335) - * Refactor "route" related code, fix Safari cookie bug (#24330) - * Alert error message if open dependencies are included in the issues that try to batch close (#24329) - * Add missed column title in runner management page (#24328) - * Automatically select the org when click create repo from org dashboard (#24325) - * Modify width of ui container, fine tune css for settings pages and org header (#24315) - * Fix config list overflow and layout (#24312) - * Improve some modal action buttons (#24289) - * Move code from module to service (#24287) - * Sort users and orgs on explore by recency by default (#24279) - * Allow using localized absolute date times within phrases with place holders and localize issue due date events (#24275) - * Show workflow config error on file view also (#24267) - * Improve template helper functions: string/slice (#24266) - * Use more specific test methods (#24265) - * Add `DumpVar` helper function to help debugging templates (#24262) - * Limit avatar upload to valid image files (#24258) - * Improve emoji and mention matching (#24255) - * Change to vertical navbar layout for secondary navbar for repo/user/admin settings (#24246) - * Refactor config provider (#24245) - * Improve test logger (#24235) - * Default show closed actions list if all actions was closed (#24234) - * Add missing badges in user profile for /projects and /packages (#24232) - * Add repository counter badge to repository tab (#24205) - * Move secrets and runners settings to actions settings (#24200) - * Require at least one unit to be enabled (#24189) - * Use same action status svg icons on actions list as on action page (#24178) - * Use secondary pointing menu for tabs on user/organization home page (#24162) - * Improve Wiki TOC (#24137) - * Refactor locale number (#24134) - * Localize activity heatmap (except tooltip) (#24131) - * Fix duplicate modals when clicking on "remove all" repository button (#24129) - * Add runner check in repo action page (#24124) - * Support triggering workflows by wiki related events (#24119) - * Refactor cookie (#24107) - * Remove untranslatable `on_date` key (#24106) - * Refactor delete_modal_actions template and use it for project column related actions (#24097) - * Improve git log for debugging (#24095) - * Add option to search for users is active join a team (#24093) - * Add PDF rendering via PDFObject (#24086) - * Refactor web route (#24080) - * Make HTML template functions support context (#24056) - * Refactor rename user and rename organization (#24052) - * Localize milestone related time strings (#24051) - * Expand selected file when clicking file tree (#24041) - * Add popup to hashed comments/pull requests/issues in file editing/adding preview tab (#24040) - * Add placeholder and aria attributes to release and wiki edit page (#24031) - * Add new user types `reserved`, `bot`, and `remote` (#24026) - * Allow adding SSH keys even if SSH server is disabled (#24025) - * Use a general approach to access custom/static/builtin assets (#24022) - * Update github.com/google/go-github to v52 (#24004) - * Replace tribute with text-expander-element for textarea (#23985) - * Group template helper functions, remove `Printf`, improve template error messages (#23982) - * Drop "unrolled/render" package (#23965) - * Add job.duration in web ui (#23963) - * Tweak pull request branch delete ui (#23951) - * Merge template functions "dict/Dict/mergeinto" (#23932) - * Use a general Eval function for expressions in templates. (#23927) - * Clean template/helper.go (#23922) - * Actions: Use default branch as ref when a branch/tag delete occurs (#23910) - * Add tooltips for MD editor buttons and add `muted` class for buttons (#23896) - * Improve markdown editor: width, height, preferred (#23895) - * Make Release Download URLs predictable (#23891) - * Remove fomantic ".link" selector and styles (#23888) - * Added close/open button to details page of milestone (#23877) - * Introduce GitHub markdown editor, keep EasyMDE as fallback (#23876) - * Introduce GiteaLocaleNumber custom element to handle number localization on pages. (#23861) - * Make first section on home page full width (#23854) - * Use different SVG for pending and running actions (#23836) - * Display image size for multiarch container images (#23821) - * Improve action log display with control chars (#23820) - * Fix dropdown direction behavior (#23806) - * Fix incorrect/Improve error handle in edit user page (#23805) - * Use clippie module to copy to clipboard (#23801) - * Make minio package support legacy MD5 checksum (#23768) - * Add ONLY_SHOW_RELEVANT_REPOS back, fix explore page bug, make code more strict (#23766) - * Refactor docs (#23752) - * Fix markup background, improve wiki rendering (#23750) - * Make label templates have consistent behavior and priority (#23749) - * Improve LoadUnitConfig to handle invalid or duplicate units (#23736) - * Append `(comment)` when a link points at a comment rather than the whole issue (#23734) - * Clean some legacy files and move some build files (#23699) - * Refactor repo commit list (#23690) - * Refactor internal API for git commands, use meaningful messages instead of "Internal Server Error" (#23687) - * Add aria attributes to interactive time tooltips. (#23661) - * Fix long project name display in issue list and in related dropdown (#23653) - * Use data-tooltip-content for tippy tooltip (#23649) - * Fix new issue/pull request btn margin when it is next to sort (#23647) - * Fine tune more downdrop settings, use SVG for labels, improve Repo Topic Edit form (#23626) - * Allow new file and edit file preview if it has editable extension (#23624) - * Replace a few fontawesome icons with svg (#23602) - * `Publish Review` buttons should indicate why they are disabled (#23598) - * Convert issue list checkboxes to native (#23596) - * Set opaque background on markup and images (#23578) - * Use a general approach to show tooltip, fix temporary tooltip bug (#23574) - * Improve `` to make it output `svg` node and optimize performance (#23570) - * Enable color for consistency checks diffs (#23563) - * Fix dropdown icon misalignment when using fomantic icon (#23558) - * Decouple the issue-template code from comment_tab.tmpl (#23556) - * Remove `id="comment-form"` dead code, fix tag (#23555) - * Diff improvements (#23553) - * Sort Python package descriptors by version to mimic PyPI format (#23550) - * Use a general approch to improve a11y for all checkboxes and dropdowns. (#23542) - * Fix long name ui issues and label ui issue (#23541) - * Return `repository` in npm package metadata endpoint (#23539) - * Use `project.IconName` instead of repeated unreadable `if-else` chains (#23538) - * Remove stars in dashboard repo list (#23530) - * Update mini-css-extract-plugin, remove postcss (#23520) - * Change `Close` to either `Close issue` or `Close pull request` (#23506) - * Fix theme-auto loading (#23504) - * Fix tags sort by creation time (descending) on branch/tag dropdowns (#23491) - * Display the version of runner in the runner list (#23490) - * Replace Less with CSS (#23481) - * Fix `.locale.Tr` function not found in delete modal (#23468) - * Allow both fullname and username search when `DEFAULT_SHOW_FULL_NAME` is true (#23463) - * Add project type descriptions in issue badge and improve project icons (#23437) - * Use context for `RepositoryList.LoadAttributes` (#23435) - * Refactor branch/tag selector to Vue SFC (#23421) - * Keep (add if not existing) xmlns attribute for generated SVG images (#23410) - * Refactor dashboard repo list to Vue SFC (#23405) - * Add workflow error notification in ui (#23404) - * Refactor branch/tag selector dropdown (first step) (#23394) - * Reduce duplicate and useless code in options (#23369) - * Convert `
` to ` - + {{if .EasyMDE}} + + {{end}}
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 06887c2914..1c0dfcc551 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -21,7 +21,7 @@ {{end}} {{range .Labels}} - {{RenderLabel $.Context .}} + {{RenderLabel $.Context ctx.Locale .}} {{end}} diff --git a/templates/shared/label_filter.tmpl b/templates/shared/label_filter.tmpl new file mode 100644 index 0000000000..9daeb3f100 --- /dev/null +++ b/templates/shared/label_filter.tmpl @@ -0,0 +1,50 @@ + + diff --git a/templates/shared/search/code/results.tmpl b/templates/shared/search/code/results.tmpl index 022192a150..a98a662654 100644 --- a/templates/shared/search/code/results.tmpl +++ b/templates/shared/search/code/results.tmpl @@ -24,10 +24,10 @@ {{else}} {{.Filename}} {{end}} - {{ctx.Locale.Tr "repo.diff.view_file"}} + {{ctx.Locale.Tr "repo.diff.view_file"}}
- {{template "shared/searchfile" dict "RepoLink" $repo.Link "CodeIndexerDisabled" $.CodeIndexerDisabled "SearchResult" .}} + {{template "shared/searchfile" dict "RepoLink" $repo.Link "SearchResult" .}}
{{template "shared/searchbottom" dict "root" $ "result" .}} diff --git a/templates/shared/search/code/search.tmpl b/templates/shared/search/code/search.tmpl index 4a9d905b9d..37a23dc3d6 100644 --- a/templates/shared/search/code/search.tmpl +++ b/templates/shared/search/code/search.tmpl @@ -1,9 +1,5 @@
- {{if not $.CodeIndexerDisabled}} - {{template "shared/search/combo_fuzzy" dict "Value" .Keyword "Disabled" .CodeIndexerUnavailable "IsFuzzy" .IsFuzzy "Placeholder" (ctx.Locale.Tr "search.code_kind")}} - {{else}} - {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.code_kind")}} - {{end}} + {{template "shared/search/combo_fuzzy" dict "Value" .Keyword "Disabled" .CodeIndexerUnavailable "IsFuzzy" .IsFuzzy "Placeholder" (ctx.Locale.Tr "search.code_kind")}}
@@ -12,8 +8,8 @@

{{ctx.Locale.Tr "search.code_search_unavailable"}}

{{else}} - {{if not .CodeIndexerEnabled}} -
+ {{if .CodeIndexerDisabled}} +

{{ctx.Locale.Tr "search.code_search_by_git_grep"}}

{{end}} diff --git a/templates/shared/searchfile.tmpl b/templates/shared/searchfile.tmpl index f2c1369555..280584e4d1 100644 --- a/templates/shared/searchfile.tmpl +++ b/templates/shared/searchfile.tmpl @@ -4,7 +4,7 @@ {{range .SearchResult.Lines}} - {{.Num}} + {{.Num}} {{.FormattedContent}} diff --git a/templates/shared/user/org_profile_avatar.tmpl b/templates/shared/user/org_profile_avatar.tmpl index 2ff1e40ca8..d67f133abf 100644 --- a/templates/shared/user/org_profile_avatar.tmpl +++ b/templates/shared/user/org_profile_avatar.tmpl @@ -4,7 +4,7 @@
{{ctx.AvatarUtils.Avatar . 100}} - {{.DisplayName}} + {{.DisplayName}} {{if .Visibility.IsLimited}}
{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}
{{end}} {{if .Visibility.IsPrivate}}
{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}
{{end}} diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index bc7785629e..95277e2f78 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -13,13 +13,13 @@
{{if .ContextUser.FullName}}{{.ContextUser.FullName}}{{end}} - {{.ContextUser.Name}} {{if .IsAdmin}} + {{.ContextUser.Name}}{{if .ContextUser.Pronouns}} · {{.ContextUser.Pronouns}}{{end}} {{if .IsAdmin}} {{svg "octicon-gear" 18}} {{end}}
- {{svg "octicon-person" 18 "tw-mr-1"}}{{.NumFollowers}} {{ctx.Locale.Tr "user.followers"}} · {{.NumFollowing}} {{ctx.Locale.Tr "user.following"}} + {{svg "octicon-people" 18 "tw-mr-1"}}{{ctx.Locale.TrN .NumFollowers "user.followers_one" "user.followers_few" .NumFollowers}} · {{ctx.Locale.TrN .NumFollowing "user.following_one" "user.following_few" .NumFollowing}} {{if .EnableFeed}} {{svg "octicon-rss" 18}} {{end}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 0be0506510..25a66c29ec 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1741,6 +1741,232 @@ } } }, + "/orgs/{org}/actions/variables": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get an org-level variables list", + "operationId": "getOrgVariablesList", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/VariableList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/orgs/{org}/actions/variables/{variablename}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get an org-level variable", + "operationId": "getOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionVariable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Update an org-level variable", + "operationId": "updateOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UpdateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when updating an org-level variable" + }, + "204": { + "description": "response when updating an org-level variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Create an org-level variable", + "operationId": "createOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when creating an org-level variable" + }, + "204": { + "description": "response when creating an org-level variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Delete an org-level variable", + "operationId": "deleteOrgVariable", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionVariable" + }, + "201": { + "description": "response when deleting a variable" + }, + "204": { + "description": "response when deleting a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/orgs/{org}/activities/feeds": { "get": { "produces": [ @@ -3485,6 +3711,54 @@ } } }, + "/repos/{owner}/{repo}/actions/secrets": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "List an repo's actions secrets", + "operationId": "repoListActionsSecrets", + "parameters": [ + { + "type": "string", + "description": "owner of the repository", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/SecretList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/secrets/{secretname}": { "put": { "consumes": [ @@ -3591,6 +3865,261 @@ } } }, + "/repos/{owner}/{repo}/actions/variables": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get repo-level variables list", + "operationId": "getRepoVariablesList", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/VariableList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/repos/{owner}/{repo}/actions/variables/{variablename}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a repo-level variable", + "operationId": "getRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionVariable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Update a repo-level variable", + "operationId": "updateRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UpdateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when updating a repo-level variable" + }, + "204": { + "description": "response when updating a repo-level variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Create a repo-level variable", + "operationId": "createRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when creating a repo-level variable" + }, + "204": { + "description": "response when creating a repo-level variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Delete a repo-level variable", + "operationId": "deleteRepoVariable", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionVariable" + }, + "201": { + "description": "response when deleting a variable" + }, + "204": { + "description": "response when deleting a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/activities/feeds": { "get": { "produces": [ @@ -4729,6 +5258,49 @@ } } }, + "/repos/{owner}/{repo}/compare/{basehead}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get commit comparison information", + "operationId": "repoCompareDiff", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "compare two branches or commits", + "name": "basehead", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Compare" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/contents": { "get": { "produces": [ @@ -15541,6 +16113,194 @@ } } }, + "/user/actions/variables": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get the user-level list of variables which is created by current doer", + "operationId": "getUserVariablesList", + "parameters": [ + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/VariableList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/user/actions/variables/{variablename}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get a user-level variable which is created by current doer", + "operationId": "getUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/ActionVariable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Update a user-level variable which is created by current doer", + "operationId": "updateUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UpdateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when updating a variable" + }, + "204": { + "description": "response when updating a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Create a user-level variable", + "operationId": "createUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateVariableOption" + } + } + ], + "responses": { + "201": { + "description": "response when creating a variable" + }, + "204": { + "description": "response when creating a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Delete a user-level variable which is created by current doer", + "operationId": "deleteUserVariable", + "parameters": [ + { + "type": "string", + "description": "name of the variable", + "name": "variablename", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "response when deleting a variable" + }, + "204": { + "description": "response when deleting a variable" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/user/applications/oauth2": { "get": { "produces": [ @@ -17659,6 +18419,35 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ActionVariable": { + "description": "ActionVariable return value of the query API", + "type": "object", + "properties": { + "data": { + "description": "the value of the variable", + "type": "string", + "x-go-name": "Data" + }, + "name": { + "description": "the name of the variable", + "type": "string", + "x-go-name": "Name" + }, + "owner_id": { + "description": "the owner to which the variable belongs", + "type": "integer", + "format": "int64", + "x-go-name": "OwnerID" + }, + "repo_id": { + "description": "the repository to which the variable belongs", + "type": "integer", + "format": "int64", + "x-go-name": "RepoID" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Activity": { "type": "object", "properties": { @@ -17772,6 +18561,9 @@ "description": "AnnotatedTag represents an annotated tag", "type": "object", "properties": { + "archive_download_count": { + "$ref": "#/definitions/TagArchiveDownloadCount" + }, "message": { "type": "string", "x-go-name": "Message" @@ -18482,6 +19274,25 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "Compare": { + "type": "object", + "title": "Compare represents a comparison between two commits.", + "properties": { + "commits": { + "type": "array", + "items": { + "$ref": "#/definitions/Commit" + }, + "x-go-name": "Commits" + }, + "total_commits": { + "type": "integer", + "format": "int64", + "x-go-name": "TotalCommits" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "ContentsResponse": { "description": "ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content", "type": "object", @@ -19291,6 +20102,10 @@ "type": "boolean", "x-go-name": "IsDraft" }, + "hide_archive_links": { + "type": "boolean", + "x-go-name": "HideArchiveLinks" + }, "name": { "type": "string", "x-go-name": "Title" @@ -19556,6 +20371,21 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "CreateVariableOption": { + "description": "CreateVariableOption the option when creating variable", + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "description": "Value of the variable to create", + "type": "string", + "x-go-name": "Value" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "CreateWikiPageOptions": { "description": "CreateWikiPageOptions form for creating wiki", "type": "object", @@ -20181,6 +21011,10 @@ "type": "boolean", "x-go-name": "IsDraft" }, + "hide_archive_links": { + "type": "boolean", + "x-go-name": "HideArchiveLinks" + }, "name": { "type": "string", "x-go-name": "Title" @@ -20285,6 +21119,11 @@ "external_wiki": { "$ref": "#/definitions/ExternalWiki" }, + "globally_editable_wiki": { + "description": "set the globally editable state of the wiki", + "type": "boolean", + "x-go-name": "GloballyEditableWiki" + }, "has_actions": { "description": "either `true` to enable actions unit, or `false` to disable them.", "type": "boolean", @@ -20434,10 +21273,6 @@ "EditUserOption": { "description": "EditUserOption edit user options", "type": "object", - "required": [ - "source_id", - "login_name" - ], "properties": { "active": { "type": "boolean", @@ -20497,6 +21332,10 @@ "type": "boolean", "x-go-name": "ProhibitLogin" }, + "pronouns": { + "type": "string", + "x-go-name": "Pronouns" + }, "restricted": { "type": "boolean", "x-go-name": "Restricted" @@ -22917,6 +23756,9 @@ "description": "Release represents a repository release", "type": "object", "properties": { + "archive_download_count": { + "$ref": "#/definitions/TagArchiveDownloadCount" + }, "assets": { "type": "array", "items": { @@ -22940,6 +23782,10 @@ "type": "boolean", "x-go-name": "IsDraft" }, + "hide_archive_links": { + "type": "boolean", + "x-go-name": "HideArchiveLinks" + }, "html_url": { "type": "string", "x-go-name": "HTMLURL" @@ -23192,6 +24038,10 @@ "type": "string", "x-go-name": "FullName" }, + "globally_editable_wiki": { + "type": "boolean", + "x-go-name": "GloballyEditableWiki" + }, "has_actions": { "type": "boolean", "x-go-name": "HasActions" @@ -23511,6 +24361,9 @@ "description": "Tag represents a repository tag", "type": "object", "properties": { + "archive_download_count": { + "$ref": "#/definitions/TagArchiveDownloadCount" + }, "commit": { "$ref": "#/definitions/CommitMeta" }, @@ -23537,6 +24390,23 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "TagArchiveDownloadCount": { + "description": "TagArchiveDownloadCount counts how many times a archive was downloaded", + "type": "object", + "properties": { + "tar_gz": { + "type": "integer", + "format": "int64", + "x-go-name": "TarGz" + }, + "zip": { + "type": "integer", + "format": "int64", + "x-go-name": "Zip" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Team": { "description": "Team represents a team in an organization", "type": "object", @@ -23928,6 +24798,26 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UpdateVariableOption": { + "description": "UpdateVariableOption the option when updating variable", + "type": "object", + "required": [ + "value" + ], + "properties": { + "name": { + "description": "New name for the variable. If the field is empty, the variable name won't be updated.", + "type": "string", + "x-go-name": "Name" + }, + "value": { + "description": "Value of the variable to update", + "type": "string", + "x-go-name": "Value" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "User": { "description": "User represents a user", "type": "object", @@ -24015,11 +24905,22 @@ "type": "boolean", "x-go-name": "ProhibitLogin" }, + "pronouns": { + "description": "the user's pronouns", + "type": "string", + "x-go-name": "Pronouns" + }, "restricted": { "description": "Is user restricted", "type": "boolean", "x-go-name": "Restricted" }, + "source_id": { + "description": "The ID of the user's Authentication Source", + "type": "integer", + "format": "int64", + "x-go-name": "SourceID" + }, "starred_repos_count": { "type": "integer", "format": "int64", @@ -24090,6 +24991,10 @@ "type": "string", "x-go-name": "Location" }, + "pronouns": { + "type": "string", + "x-go-name": "Pronouns" + }, "theme": { "type": "string", "x-go-name": "Theme" @@ -24138,6 +25043,10 @@ "type": "string", "x-go-name": "Location" }, + "pronouns": { + "type": "string", + "x-go-name": "Pronouns" + }, "theme": { "type": "string", "x-go-name": "Theme" @@ -24299,6 +25208,12 @@ } } }, + "ActionVariable": { + "description": "ActionVariable", + "schema": { + "$ref": "#/definitions/ActionVariable" + } + }, "ActivityFeedsList": { "description": "ActivityFeedsList", "schema": { @@ -24486,6 +25401,12 @@ } } }, + "Compare": { + "description": "", + "schema": { + "$ref": "#/definitions/Compare" + } + }, "ContentsListResponse": { "description": "ContentsListResponse", "schema": { @@ -25191,6 +26112,15 @@ } } }, + "VariableList": { + "description": "VariableList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/ActionVariable" + } + } + }, "WatchInfo": { "description": "WatchInfo", "schema": { @@ -25266,7 +26196,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/CreateOrUpdateSecretOption" + "$ref": "#/definitions/UpdateVariableOption" } }, "redirect": { diff --git a/templates/user/dashboard/dashboard.tmpl b/templates/user/dashboard/dashboard.tmpl index d4553ea61b..5dc46dc0a5 100644 --- a/templates/user/dashboard/dashboard.tmpl +++ b/templates/user/dashboard/dashboard.tmpl @@ -1,15 +1,13 @@ {{template "base/head" .}}
{{template "user/dashboard/navbar" .}} -
- {{template "base/alert" .}} -
-
- {{template "user/heatmap" .}} - {{template "user/dashboard/feeds" .}} -
- {{template "user/dashboard/repolist" .}} +
+
+ {{template "base/alert" .}} + {{template "user/heatmap" .}} + {{template "user/dashboard/feeds" .}}
+ {{template "user/dashboard/repolist" .}}
{{template "base/footer" .}} diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 89f23163f7..b7cc54091c 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -37,11 +37,11 @@
diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl index 34f9b67f8e..2781f710ed 100644 --- a/templates/user/dashboard/repolist.tmpl +++ b/templates/user/dashboard/repolist.tmpl @@ -52,4 +52,4 @@ data.organizationId = {{.ContextUser.ID}}; window.config.pageData.dashboardRepoList = data; -
+
diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl index 04e79ba749..99da119901 100644 --- a/templates/user/notification/notification_div.tmpl +++ b/templates/user/notification/notification_div.tmpl @@ -2,7 +2,7 @@
{{$notificationUnreadCount := call .NotificationUnreadCount}}
-
diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl index a5a965ca52..0a3ae99b40 100644 --- a/templates/user/notification/notification_subscriptions.tmpl +++ b/templates/user/notification/notification_subscriptions.tmpl @@ -1,15 +1,20 @@ {{template "base/head" .}}
- - {{template "repo/settings/webhook/settings" .}} + {{template "webhook/shared-settings" .}} diff --git a/templates/webhook/new/packagist.tmpl b/templates/webhook/new/packagist.tmpl index 5cf6ed0012..04240bbf93 100644 --- a/templates/webhook/new/packagist.tmpl +++ b/templates/webhook/new/packagist.tmpl @@ -13,5 +13,5 @@
- {{template "repo/settings/webhook/settings" .}} + {{template "webhook/shared-settings" .}} diff --git a/templates/webhook/new/slack.tmpl b/templates/webhook/new/slack.tmpl index d24e659097..cfaeb41f20 100644 --- a/templates/webhook/new/slack.tmpl +++ b/templates/webhook/new/slack.tmpl @@ -22,5 +22,5 @@
- {{template "repo/settings/webhook/settings" .}} + {{template "webhook/shared-settings" .}} diff --git a/templates/webhook/new/sourcehut_builds.tmpl b/templates/webhook/new/sourcehut_builds.tmpl new file mode 100644 index 0000000000..3bcbe1bf6e --- /dev/null +++ b/templates/webhook/new/sourcehut_builds.tmpl @@ -0,0 +1,39 @@ +

{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://sourcehut.org/" (ctx.Locale.Tr "repo.settings.web_hook_name_sourcehut_builds")}}

+
+ {{.CsrfTokenHtml}} +
+ + +
+
+ + +
+
+ + +
+
+
+ + + {{ctx.Locale.Tr "repo.settings.sourcehut_builds.secrets_helper"}} +
+
+ +
+ + + {{ctx.Locale.Tr "repo.settings.sourcehut_builds.access_token_helper" "https://meta.sr.ht/oauth2/personal-token?grants=builds.sr.ht/JOBS:RW" "https://meta.sr.ht/oauth2/personal-token?grants=builds.sr.ht/JOBS:RW+builds.sr.ht/SECRETS:RO"}} +
+ {{template "webhook/shared-settings" .}} +
diff --git a/templates/webhook/new/telegram.tmpl b/templates/webhook/new/telegram.tmpl index ea677ca4c3..3627dff36a 100644 --- a/templates/webhook/new/telegram.tmpl +++ b/templates/webhook/new/telegram.tmpl @@ -13,5 +13,5 @@
- {{template "repo/settings/webhook/settings" .}} + {{template "webhook/shared-settings" .}} diff --git a/templates/webhook/new/wechatwork.tmpl b/templates/webhook/new/wechatwork.tmpl index 15050d71bd..ae9d36a7a6 100644 --- a/templates/webhook/new/wechatwork.tmpl +++ b/templates/webhook/new/wechatwork.tmpl @@ -5,5 +5,5 @@
- {{template "repo/settings/webhook/settings" .}} + {{template "webhook/shared-settings" .}} diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/webhook/shared-settings.tmpl similarity index 94% rename from templates/repo/settings/webhook/settings.tmpl rename to templates/webhook/shared-settings.tmpl index 0a39643260..80caad7279 100644 --- a/templates/repo/settings/webhook/settings.tmpl +++ b/templates/webhook/shared-settings.tmpl @@ -258,14 +258,15 @@ {{ctx.Locale.Tr "repo.settings.branch_filter_desc"}}
- -
- - - {{if ne .HookType "matrix"}}{{/* Matrix doesn't make the authorization optional but it is implied by the help string, should be changed.*/}} +{{$skipAuthorizationHeader := or (eq .HookType "sourcehut_builds") (eq .HookType "matrix")}} +{{if not $skipAuthorizationHeader}} + +
+ + {{ctx.Locale.Tr "repo.settings.authorization_header_desc" ("Bearer token123456, Basic YWxhZGRpbjpvcGVuc2VzYW1l" | SafeHTML)}} - {{end}} -
+
+{{end}}
diff --git a/tests/e2e/README.md b/tests/e2e/README.md index e5fd1ca6c0..a0a9fc9ec9 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -7,7 +7,6 @@ They can be run with make commands for the appropriate backends, namely: make test-sqlite make test-pgsql make test-mysql -make test-mssql ``` Make sure to perform a clean front-end build before running tests: @@ -53,16 +52,6 @@ Start tests based on the database container TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-e2e-pgsql ``` -## Run mssql e2e tests -Setup a mssql database inside docker -``` -docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest #(just ctrl-c to stop db and clean the container) -``` -Start tests based on the database container -``` -TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql -``` - ## Running individual tests Example command to run `example.test.e2e.js` test file: @@ -75,10 +64,10 @@ For SQLite: make test-e2e-sqlite#example ``` -For other databases(replace `mssql` to `mysql` or `pgsql`): +For PostgreSQL databases(replace `mysql` to `pgsql`): ``` -TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql#example +TEST_MYSQL_HOST=localhost:1433 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=sa TEST_MYSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mysql#example ``` ## Visual testing diff --git a/tests/e2e/debugserver_test.go b/tests/e2e/debugserver_test.go index f0f54665e1..b1496b22a1 100644 --- a/tests/e2e/debugserver_test.go +++ b/tests/e2e/debugserver_test.go @@ -10,6 +10,7 @@ package e2e import ( + "fmt" "net/url" "os" "os/signal" @@ -24,7 +25,7 @@ func TestDebugserver(t *testing.T) { signal.Notify(done, syscall.SIGINT, syscall.SIGTERM) onGiteaRun(t, func(*testing.T, *url.URL) { - println(setting.AppURL) + fmt.Println(setting.AppURL) <-done }) } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 304dac0f45..07f0bf52a3 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -60,8 +60,7 @@ func TestMain(m *testing.M) { exitVal := m.Run() if err := testlogger.WriterCloser.Reset(); err != nil { - fmt.Printf("testlogger.WriterCloser.Reset: %v\n", err) - os.Exit(1) + fmt.Printf("testlogger.WriterCloser.Reset: error ignored: %v\n", err) } if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil { fmt.Printf("util.RemoveAll: %v\n", err) @@ -92,6 +91,9 @@ func TestE2e(t *testing.T) { if _, set := os.LookupEnv("ACCEPT_VISUAL"); set { runArgs = append(runArgs, "--update-snapshots") } + if project := os.Getenv("PLAYWRIGHT_PROJECT"); project != "" { + runArgs = append(runArgs, "--project="+project) + } // Create new test for each input file for _, path := range paths { @@ -104,18 +106,20 @@ func TestE2e(t *testing.T) { cmd := exec.Command(runArgs[0], runArgs...) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, fmt.Sprintf("GITEA_URL=%s", setting.AppURL)) + var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr + err := cmd.Run() if err != nil { // Currently colored output is conflicting. Using Printf until that is resolved. fmt.Printf("%v", stdout.String()) fmt.Printf("%v", stderr.String()) log.Fatal("Playwright Failed: %s", err) - } else { - fmt.Printf("%v", stdout.String()) } + + fmt.Printf("%v", stdout.String()) }) }) } diff --git a/tests/e2e/right-settings-button.test.e2e.js b/tests/e2e/right-settings-button.test.e2e.js new file mode 100644 index 0000000000..698aa03192 --- /dev/null +++ b/tests/e2e/right-settings-button.test.e2e.js @@ -0,0 +1,128 @@ +// @ts-check +import {test, expect} from '@playwright/test'; +import {login_user, load_logged_in_context} from './utils_e2e.js'; + +test.beforeAll(async ({browser}, workerInfo) => { + await login_user(browser, workerInfo, 'user2'); +}); + +test.describe('desktop viewport', () => { + test.use({viewport: {width: 1920, height: 300}}); + + test('Settings button on right of repo header', async ({browser}, workerInfo) => { + const context = await load_logged_in_context(browser, workerInfo, 'user2'); + const page = await context.newPage(); + + await page.goto('/user2/repo1'); + + const settingsBtn = page.locator('.overflow-menu-items>#settings-btn'); + await expect(settingsBtn).toBeVisible(); + await expect(settingsBtn).toHaveClass(/right/); + + await expect(page.locator('.overflow-menu-button')).toHaveCount(0); + }); + + test('Settings button on right of repo header also when add more button is shown', async ({browser}, workerInfo) => { + await login_user(browser, workerInfo, 'user12'); + const context = await load_logged_in_context(browser, workerInfo, 'user12'); + const page = await context.newPage(); + + await page.goto('/user12/repo10'); + + const settingsBtn = page.locator('.overflow-menu-items>#settings-btn'); + await expect(settingsBtn).toBeVisible(); + await expect(settingsBtn).toHaveClass(/right/); + + await expect(page.locator('.overflow-menu-button')).toHaveCount(0); + }); + + test('Settings button on right of org header', async ({browser}, workerInfo) => { + const context = await load_logged_in_context(browser, workerInfo, 'user2'); + const page = await context.newPage(); + + await page.goto('/org3'); + + const settingsBtn = page.locator('.overflow-menu-items>#settings-btn'); + await expect(settingsBtn).toBeVisible(); + await expect(settingsBtn).toHaveClass(/right/); + + await expect(page.locator('.overflow-menu-button')).toHaveCount(0); + }); + + test('User overview overflow menu should not be influenced', async ({page}) => { + await page.goto('/user2'); + + await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0); + + await expect(page.locator('.overflow-menu-button')).toHaveCount(0); + }); +}); + +test.describe('small viewport', () => { + test.use({viewport: {width: 800, height: 300}}); + + test('Settings button in overflow menu of repo header', async ({browser}, workerInfo) => { + const context = await load_logged_in_context(browser, workerInfo, 'user2'); + const page = await context.newPage(); + + await page.goto('/user2/repo1'); + + await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0); + + await expect(page.locator('.overflow-menu-button')).toBeVisible(); + + await page.click('.overflow-menu-button'); + await expect(page.locator('.tippy-target>#settings-btn')).toBeVisible(); + + // Verify that we have no duplicated items + const shownItems = await page.locator('.overflow-menu-items>a').all(); + expect(shownItems).not.toHaveLength(0); + const overflowItems = await page.locator('.tippy-target>a').all(); + expect(overflowItems).not.toHaveLength(0); + + const items = shownItems.concat(overflowItems); + expect(Array.from(new Set(items))).toHaveLength(items.length); + }); + + test('Settings button in overflow menu of org header', async ({browser}, workerInfo) => { + const context = await load_logged_in_context(browser, workerInfo, 'user2'); + const page = await context.newPage(); + + await page.goto('/org3'); + + await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0); + + await expect(page.locator('.overflow-menu-button')).toBeVisible(); + + await page.click('.overflow-menu-button'); + await expect(page.locator('.tippy-target>#settings-btn')).toBeVisible(); + + // Verify that we have no duplicated items + const shownItems = await page.locator('.overflow-menu-items>a').all(); + expect(shownItems).not.toHaveLength(0); + const overflowItems = await page.locator('.tippy-target>a').all(); + expect(overflowItems).not.toHaveLength(0); + + const items = shownItems.concat(overflowItems); + expect(Array.from(new Set(items))).toHaveLength(items.length); + }); + + test('User overview overflow menu should not be influenced', async ({page}) => { + await page.goto('/user2'); + + await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0); + + await expect(page.locator('.overflow-menu-button')).toBeVisible(); + await page.click('.overflow-menu-button'); + await expect(page.locator('.tippy-target>#settings-btn')).toHaveCount(0); + + // Verify that we have no duplicated items + const shownItems = await page.locator('.overflow-menu-items>a').all(); + expect(shownItems).not.toHaveLength(0); + const overflowItems = await page.locator('.tippy-target>a').all(); + expect(overflowItems).not.toHaveLength(0); + + const items = shownItems.concat(overflowItems); + expect(Array.from(new Set(items))).toHaveLength(items.length); + }); +}); diff --git a/tests/integration/README.md b/tests/integration/README.md index f6f74ca21f..d659d51b20 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -6,7 +6,6 @@ appropriate backends, namely: make test-sqlite make test-pgsql make test-mysql -make test-mssql ``` Make sure to perform a clean build before running tests: @@ -63,16 +62,6 @@ Start tests based on the database container TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-pgsql ``` -## Run mssql integration tests -Setup a mssql database inside docker -``` -docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest #(just ctrl-c to stop db and clean the container) -``` -Start tests based on the database container -``` -TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-mssql -``` - ## Running individual tests Example command to run GPG test: @@ -83,10 +72,10 @@ For SQLite: make test-sqlite#GPG ``` -For other databases(replace `mssql` to `mysql`, or `pgsql`): +For other databases (replace `mysql` to `pgsql`): ``` -TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-mssql#GPG +TEST_MYSQL_HOST=localhost:1433 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=sa TEST_MYSQL_PASSWORD=MwantsaSecurePassword1 make test-mysql#GPG ``` ## Setting timeouts for declaring long-tests and long-flushes diff --git a/tests/integration/README_ZH.md b/tests/integration/README_ZH.md index 6aea4ab212..d0debf2fd6 100644 --- a/tests/integration/README_ZH.md +++ b/tests/integration/README_ZH.md @@ -59,16 +59,6 @@ docker run -e "POSTGRES_DB=test" -p 5432:5432 --rm --name pgsql postgres:14 #(ju TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-pgsql ``` -## Run mssql integration tests -åŒä¸Šï¼Œé¦–先在 docker 容器里部署一个 mssql æ•°æ®åº“ -``` -docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest #(just ctrl-c to stop db and clean the container) -``` -之åŽä¾¿å¯ä»¥åŸºäºŽè¿™ä¸ªæ•°æ®åº“进行集æˆæµ‹è¯• -``` -TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-mssql -``` - ## 如何进行自定义的集æˆæµ‹è¯• 下é¢çš„示例展示了怎样在集æˆæµ‹è¯•ä¸­åªè¿›è¡Œ GPG 测试: @@ -79,9 +69,9 @@ sqlite æ•°æ®åº“: make test-sqlite#GPG ``` -其它数æ®åº“(把 MSSQL 替æ¢ä¸º MYSQL, PGSQL): +其它数æ®åº“ (用 PGSQL å–代 MYSQL): ``` -TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-mssql#GPG +TEST_MYSQL_HOST=localhost:1433 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=sa TEST_MYSQL_PASSWORD=MwantsaSecurePassword1 make test-mysql#GPG ``` diff --git a/tests/integration/actions_commit_status_test.go b/tests/integration/actions_commit_status_test.go new file mode 100644 index 0000000000..3d191a283f --- /dev/null +++ b/tests/integration/actions_commit_status_test.go @@ -0,0 +1,48 @@ +// Copyright 20124 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/url" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/actions" + "code.gitea.io/gitea/services/automerge" + + "github.com/stretchr/testify/assert" +) + +func TestActionsAutomerge(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + assert.True(t, setting.Actions.Enabled, "Actions should be enabled") + + ctx := db.DefaultContext + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) + job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 292}) + + assert.False(t, pr.HasMerged, "PR should not be merged") + assert.Equal(t, issues_model.PullRequestStatusMergeable, pr.Status, "PR should be mergable") + + scheduled, err := automerge.ScheduleAutoMerge(ctx, user, pr, repo_model.MergeStyleMerge, "Dummy") + + assert.NoError(t, err, "PR should be scheduled for automerge") + assert.True(t, scheduled, "PR should be scheduled for automerge") + + actions.CreateCommitStatus(ctx, job) + + pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) + + assert.True(t, pr.HasMerged, "PR should be merged") + }, + ) +} diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go index e8954f5b20..0e97e606f9 100644 --- a/tests/integration/api_admin_test.go +++ b/tests/integration/api_admin_test.go @@ -196,19 +196,13 @@ func TestAPIEditUser(t *testing.T) { urlStr := fmt.Sprintf("/api/v1/admin/users/%s", "user2") req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{ - // required - "login_name": "user2", - "source_id": "0", - // to change "full_name": "Full Name User 2", }).AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) empty := "" req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ - LoginName: "user2", - SourceID: 0, - Email: &empty, + Email: &empty, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusBadRequest) @@ -220,10 +214,6 @@ func TestAPIEditUser(t *testing.T) { assert.False(t, user2.IsRestricted) bTrue := true req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ - // required - LoginName: "user2", - SourceID: 0, - // to change Restricted: &bTrue, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) @@ -231,6 +221,45 @@ func TestAPIEditUser(t *testing.T) { assert.True(t, user2.IsRestricted) } +func TestAPIEditUserWithLoginName(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + adminUsername := "user1" + token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin) + urlStr := fmt.Sprintf("/api/v1/admin/users/%s", "user2") + + loginName := "user2" + loginSource := int64(0) + + t.Run("login_name only", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ + LoginName: &loginName, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusUnprocessableEntity) + }) + + t.Run("source_id only", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ + SourceID: &loginSource, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusUnprocessableEntity) + }) + + t.Run("login_name & source_id", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ + LoginName: &loginName, + SourceID: &loginSource, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + }) +} + func TestAPICreateRepoForUser(t *testing.T) { defer tests.PrepareTestEnv(t)() adminUsername := "user1" @@ -375,18 +404,14 @@ func TestAPIEditUser_NotAllowedEmailDomain(t *testing.T) { newEmail := "user2@example1.com" req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ - LoginName: "user2", - SourceID: 0, - Email: &newEmail, + Email: &newEmail, }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, "the domain of user email user2@example1.com conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", resp.Header().Get("X-Gitea-Warning")) originalEmail := "user2@example.com" req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ - LoginName: "user2", - SourceID: 0, - Email: &originalEmail, + Email: &originalEmail, }).AddTokenAuth(token) MakeRequest(t, req, http.StatusOK) } diff --git a/tests/integration/api_comment_attachment_test.go b/tests/integration/api_comment_attachment_test.go index b6f3d3bc81..d4368d51fe 100644 --- a/tests/integration/api_comment_attachment_test.go +++ b/tests/integration/api_comment_attachment_test.go @@ -1,6 +1,5 @@ // Copyright 2021 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT package integration diff --git a/tests/integration/api_fork_test.go b/tests/integration/api_fork_test.go index 87d2a10152..b80b4c6645 100644 --- a/tests/integration/api_fork_test.go +++ b/tests/integration/api_fork_test.go @@ -5,10 +5,14 @@ package integration import ( + "fmt" "net/http" "net/url" "testing" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" @@ -16,6 +20,65 @@ import ( "code.gitea.io/gitea/tests" ) +func TestAPIForkAsAdminIgnoringLimits(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Repository.AllowForkWithoutMaximumLimit, false)() + defer test.MockVariableValue(&setting.Repository.MaxCreationLimit, 0)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) + userSession := loginUser(t, user.Name) + userToken := getTokenForLoggedInUser(t, userSession, auth_model.AccessTokenScopeWriteRepository) + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + adminSession := loginUser(t, adminUser.Name) + adminToken := getTokenForLoggedInUser(t, adminSession, + auth_model.AccessTokenScopeWriteRepository, + auth_model.AccessTokenScopeWriteOrganization) + + originForkURL := "/api/v1/repos/user12/repo10/forks" + orgName := "fork-org" + + // Create an organization + req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{ + UserName: orgName, + }).AddTokenAuth(adminToken) + MakeRequest(t, req, http.StatusCreated) + + // Create a team + teamToCreate := &api.CreateTeamOption{ + Name: "testers", + IncludesAllRepositories: true, + Permission: "write", + Units: []string{"repo.code", "repo.issues"}, + } + + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &teamToCreate).AddTokenAuth(adminToken) + resp := MakeRequest(t, req, http.StatusCreated) + var team api.Team + DecodeJSON(t, resp, &team) + + // Add user2 to the team + req = NewRequestf(t, "PUT", "/api/v1/teams/%d/members/user2", team.ID).AddTokenAuth(adminToken) + MakeRequest(t, req, http.StatusNoContent) + + t.Run("forking as regular user", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "POST", originForkURL, &api.CreateForkOption{ + Organization: &orgName, + }).AddTokenAuth(userToken) + MakeRequest(t, req, http.StatusConflict) + }) + + t.Run("forking as an instance admin", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "POST", originForkURL, &api.CreateForkOption{ + Organization: &orgName, + }).AddTokenAuth(adminToken) + MakeRequest(t, req, http.StatusAccepted) + }) +} + func TestCreateForkNoLogin(t *testing.T) { defer tests.PrepareTestEnv(t)() req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{}) diff --git a/tests/integration/api_health_test.go b/tests/integration/api_health_test.go new file mode 100644 index 0000000000..5657f4fd06 --- /dev/null +++ b/tests/integration/api_health_test.go @@ -0,0 +1,25 @@ +package integration + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers/web/healthcheck" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestApiHeatlhCheck(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/api/healthz") + resp := MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Header().Values("Cache-Control"), "no-store") + + var status healthcheck.Response + DecodeJSON(t, resp, &status) + assert.Equal(t, healthcheck.Pass, status.Status) + assert.Equal(t, setting.AppName, status.Description) +} diff --git a/tests/integration/api_issue_attachment_test.go b/tests/integration/api_issue_attachment_test.go index 375fe9ced8..b6a0cca6d5 100644 --- a/tests/integration/api_issue_attachment_test.go +++ b/tests/integration/api_issue_attachment_test.go @@ -1,6 +1,5 @@ // Copyright 2021 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT package integration diff --git a/tests/integration/api_notification_test.go b/tests/integration/api_notification_test.go index 528890ca22..abb9852eef 100644 --- a/tests/integration/api_notification_test.go +++ b/tests/integration/api_notification_test.go @@ -111,7 +111,7 @@ func TestAPINotification(t *testing.T) { MakeRequest(t, NewRequest(t, "GET", "/api/v1/notifications/new"), http.StatusUnauthorized) - new := struct { + newStruct := struct { New int64 `json:"new"` }{} @@ -119,8 +119,8 @@ func TestAPINotification(t *testing.T) { req = NewRequest(t, "GET", "/api/v1/notifications/new"). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) - DecodeJSON(t, resp, &new) - assert.True(t, new.New > 0) + DecodeJSON(t, resp, &newStruct) + assert.True(t, newStruct.New > 0) // -- mark notifications as read -- req = NewRequest(t, "GET", "/api/v1/notifications?status-types=unread"). @@ -153,8 +153,8 @@ func TestAPINotification(t *testing.T) { req = NewRequest(t, "GET", "/api/v1/notifications/new"). AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) - DecodeJSON(t, resp, &new) - assert.True(t, new.New == 0) + DecodeJSON(t, resp, &newStruct) + assert.True(t, newStruct.New == 0) } func TestAPINotificationPUT(t *testing.T) { diff --git a/tests/integration/api_packages_alpine_test.go b/tests/integration/api_packages_alpine_test.go index ea9735236b..f70d3c23af 100644 --- a/tests/integration/api_packages_alpine_test.go +++ b/tests/integration/api_packages_alpine_test.go @@ -480,7 +480,6 @@ aiIK5QoSDwAAAAAAAAAAAAAAAP/IK49O1e8AKAAA` req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/x86_64/%s-%s.apk", rootURL, branch, repository, pkg, packageVersion)). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNoContent) - } // Deleting the last file of an architecture should remove that index req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/x86_64/APKINDEX.tar.gz", rootURL, branch, repository)) diff --git a/tests/integration/api_packages_cargo_test.go b/tests/integration/api_packages_cargo_test.go index 869d90066a..55cce50c7b 100644 --- a/tests/integration/api_packages_cargo_test.go +++ b/tests/integration/api_packages_cargo_test.go @@ -1,6 +1,5 @@ // Copyright 2021 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT package integration diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go index eb67693010..991f37fe74 100644 --- a/tests/integration/api_packages_nuget_test.go +++ b/tests/integration/api_packages_nuget_test.go @@ -112,6 +112,20 @@ func TestPackageNuGet(t *testing.T) { return &buf } + nuspec := ` + + + ` + packageName + ` + ` + packageVersion + ` + ` + packageAuthors + ` + ` + packageDescription + ` + + + + + + + ` content, _ := io.ReadAll(createPackage(packageName, packageVersion)) url := fmt.Sprintf("/api/packages/%s/nuget", user.Name) @@ -224,7 +238,7 @@ func TestPackageNuGet(t *testing.T) { pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet) assert.NoError(t, err) - assert.Len(t, pvs, 1) + assert.Len(t, pvs, 1, "Should have one version") pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0]) assert.NoError(t, err) @@ -235,7 +249,7 @@ func TestPackageNuGet(t *testing.T) { pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) assert.NoError(t, err) - assert.Len(t, pfs, 1) + assert.Len(t, pfs, 2, "Should have 2 files: nuget and nuspec") assert.Equal(t, fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion), pfs[0].Name) assert.True(t, pfs[0].IsLead) @@ -302,16 +316,27 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) assert.NoError(t, err) - assert.Len(t, pfs, 3) + assert.Len(t, pfs, 4, "Should have 4 files: nupkg, snupkg, nuspec and pdb") for _, pf := range pfs { switch pf.Name { case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion): + assert.True(t, pf.IsLead) + + pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID) + assert.NoError(t, err) + assert.Equal(t, int64(414), pb.Size) case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion): assert.False(t, pf.IsLead) pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID) assert.NoError(t, err) assert.Equal(t, int64(616), pb.Size) + case fmt.Sprintf("%s.nuspec", packageName): + assert.False(t, pf.IsLead) + + pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID) + assert.NoError(t, err) + assert.Equal(t, int64(453), pb.Size) case symbolFilename: assert.False(t, pf.IsLead) @@ -357,15 +382,6 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) AddBasicAuth(user.Name) resp = MakeRequest(t, req, http.StatusOK) - nuspec := `` + "\n" + - `` + - `` + packageName + `` + packageVersion + `` + packageAuthors + `` + packageDescription + `` + - `` + - // https://github.com/golang/go/issues/21399 go can't generate self-closing tags - `` + - `` + - `` - assert.Equal(t, nuspec, resp.Body.String()) checkDownloadCount(1) diff --git a/tests/integration/api_packages_pypi_test.go b/tests/integration/api_packages_pypi_test.go index a090b31e20..e973f6a52a 100644 --- a/tests/integration/api_packages_pypi_test.go +++ b/tests/integration/api_packages_pypi_test.go @@ -164,7 +164,7 @@ func TestPackagePyPI(t *testing.T) { nodes := htmlDoc.doc.Find("a").Nodes assert.Len(t, nodes, 2) - hrefMatcher := regexp.MustCompile(fmt.Sprintf(`%s/files/%s/%s/test\..+#sha256-%s`, root, regexp.QuoteMeta(packageName), regexp.QuoteMeta(packageVersion), hashSHA256)) + hrefMatcher := regexp.MustCompile(fmt.Sprintf(`%s/files/%s/%s/test\..+#sha256=%s`, root, regexp.QuoteMeta(packageName), regexp.QuoteMeta(packageVersion), hashSHA256)) for _, a := range nodes { for _, att := range a.Attr { diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go index 49aa4c4e1b..0a392d0a15 100644 --- a/tests/integration/api_releases_test.go +++ b/tests/integration/api_releases_test.go @@ -133,14 +133,18 @@ func TestAPICreateAndUpdateRelease(t *testing.T) { assert.Equal(t, newRelease.TagName, release.TagName) assert.Equal(t, newRelease.Title, release.Title) assert.Equal(t, newRelease.Note, release.Note) + assert.False(t, newRelease.HideArchiveLinks) + + hideArchiveLinks := true req = NewRequestWithJSON(t, "PATCH", urlStr, &api.EditReleaseOption{ - TagName: release.TagName, - Title: release.Title, - Note: "updated", - IsDraft: &release.IsDraft, - IsPrerelease: &release.IsPrerelease, - Target: release.Target, + TagName: release.TagName, + Title: release.Title, + Note: "updated", + IsDraft: &release.IsDraft, + IsPrerelease: &release.IsPrerelease, + Target: release.Target, + HideArchiveLinks: &hideArchiveLinks, }).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) @@ -152,6 +156,7 @@ func TestAPICreateAndUpdateRelease(t *testing.T) { } unittest.AssertExistsAndLoadBean(t, rel) assert.EqualValues(t, rel.Note, newRelease.Note) + assert.True(t, newRelease.HideArchiveLinks) } func TestAPICreateReleaseToDefaultBranch(t *testing.T) { @@ -319,3 +324,39 @@ func TestAPIUploadAssetRelease(t *testing.T) { assert.EqualValues(t, 104, attachment.Size) }) } + +func TestAPIGetReleaseArchiveDownloadCount(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + session := loginUser(t, owner.LowerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + name := "ReleaseDownloadCount" + + createNewReleaseUsingAPI(t, session, token, owner, repo, name, "", name, "test") + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, name) + + req := NewRequest(t, "GET", urlStr) + resp := MakeRequest(t, req, http.StatusOK) + + var release *api.Release + DecodeJSON(t, resp, &release) + + // Check if everything defaults to 0 + assert.Equal(t, int64(0), release.ArchiveDownloadCount.TarGz) + assert.Equal(t, int64(0), release.ArchiveDownloadCount.Zip) + + // Download the tarball to increase the count + MakeRequest(t, NewRequest(t, "GET", release.TarURL), http.StatusOK) + + // Check if the count has increased + resp = MakeRequest(t, req, http.StatusOK) + + DecodeJSON(t, resp, &release) + + assert.Equal(t, int64(1), release.ArchiveDownloadCount.TarGz) + assert.Equal(t, int64(0), release.ArchiveDownloadCount.Zip) +} diff --git a/tests/integration/api_repo_compare_test.go b/tests/integration/api_repo_compare_test.go new file mode 100644 index 0000000000..f3188eb49f --- /dev/null +++ b/tests/integration/api_repo_compare_test.go @@ -0,0 +1,38 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPICompareBranches(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + // Login as User2. + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + repoName := "repo20" + + req := NewRequestf(t, "GET", "/api/v1/repos/user2/%s/compare/add-csv...remove-files-b", repoName). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + var apiResp *api.Compare + DecodeJSON(t, resp, &apiResp) + + assert.Equal(t, 2, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 2) +} diff --git a/tests/integration/api_repo_secrets_test.go b/tests/integration/api_repo_secrets_test.go index feb9bae2b2..c3074d9ece 100644 --- a/tests/integration/api_repo_secrets_test.go +++ b/tests/integration/api_repo_secrets_test.go @@ -24,6 +24,12 @@ func TestAPIRepoSecrets(t *testing.T) { session := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + t.Run("List", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/secrets", repo.FullName())). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + }) + t.Run("Create", func(t *testing.T) { cases := []struct { Name string @@ -31,7 +37,7 @@ func TestAPIRepoSecrets(t *testing.T) { }{ { Name: "", - ExpectedStatus: http.StatusNotFound, + ExpectedStatus: http.StatusMethodNotAllowed, }, { Name: "-", diff --git a/tests/integration/api_repo_tags_test.go b/tests/integration/api_repo_tags_test.go index c6eeb404c0..10a82e11a8 100644 --- a/tests/integration/api_repo_tags_test.go +++ b/tests/integration/api_repo_tags_test.go @@ -85,3 +85,39 @@ func createNewTagUsingAPI(t *testing.T, session *TestSession, token, ownerName, DecodeJSON(t, resp, &respObj) return &respObj } + +func TestAPIGetTagArchiveDownloadCount(t *testing.T) { + defer tests.PrepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + // Login as User2. + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + repoName := "repo1" + tagName := "TagDownloadCount" + + createNewTagUsingAPI(t, session, token, user.Name, repoName, tagName, "", "") + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/tags/%s?token=%s", user.Name, repoName, tagName, token) + + req := NewRequest(t, "GET", urlStr) + resp := MakeRequest(t, req, http.StatusOK) + + var tagInfo *api.Tag + DecodeJSON(t, resp, &tagInfo) + + // Check if everything defaults to 0 + assert.Equal(t, int64(0), tagInfo.ArchiveDownloadCount.TarGz) + assert.Equal(t, int64(0), tagInfo.ArchiveDownloadCount.Zip) + + // Download the tarball to increase the count + MakeRequest(t, NewRequest(t, "GET", tagInfo.TarballURL), http.StatusOK) + + // Check if the count has increased + resp = MakeRequest(t, req, http.StatusOK) + + DecodeJSON(t, resp, &tagInfo) + + assert.Equal(t, int64(1), tagInfo.ArchiveDownloadCount.TarGz) + assert.Equal(t, int64(0), tagInfo.ArchiveDownloadCount.Zip) +} diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 8ae2622976..2fb89cfa6e 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -701,3 +701,14 @@ func TestAPIRepoGetAssignees(t *testing.T) { DecodeJSON(t, resp, &assignees) assert.Len(t, assignees, 1) } + +func TestAPIViewRepoObjectFormat(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + var repo api.Repository + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1") + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &repo) + assert.EqualValues(t, "sha1", repo.ObjectFormatName) +} diff --git a/tests/integration/api_repo_variables_test.go b/tests/integration/api_repo_variables_test.go new file mode 100644 index 0000000000..7847962b07 --- /dev/null +++ b/tests/integration/api_repo_variables_test.go @@ -0,0 +1,149 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" +) + +func TestAPIRepoVariables(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + t.Run("CreateRepoVariable", func(t *testing.T) { + cases := []struct { + Name string + ExpectedStatus int + }{ + { + Name: "-", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "_", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "TEST_VAR", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "test_var", + ExpectedStatus: http.StatusConflict, + }, + { + Name: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "123var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "var@test", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "github_var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "gitea_var", + ExpectedStatus: http.StatusBadRequest, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.CreateVariableOption{ + Value: "value", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("UpdateRepoVariable", func(t *testing.T) { + variableName := "test_update_var" + url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName) + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + cases := []struct { + Name string + UpdateName string + ExpectedStatus int + }{ + { + Name: "not_found_var", + ExpectedStatus: http.StatusNotFound, + }, + { + Name: variableName, + UpdateName: "1invalid", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "invalid@name", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: variableName, + ExpectedStatus: http.StatusNotFound, + }, + { + Name: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.UpdateVariableOption{ + Name: c.UpdateName, + Value: "updated_val", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("DeleteRepoVariable", func(t *testing.T) { + variableName := "test_delete_var" + url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName) + + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + }) +} diff --git a/tests/integration/api_user_search_test.go b/tests/integration/api_user_search_test.go index f776b35325..0e01b504cc 100644 --- a/tests/integration/api_user_search_test.go +++ b/tests/integration/api_user_search_test.go @@ -10,7 +10,9 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -57,6 +59,25 @@ func TestAPIUserSearchNotLoggedIn(t *testing.T) { } } +func TestAPIUserSearchPaged(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.API.DefaultPagingNum, 5)() + + req := NewRequest(t, "GET", "/api/v1/users/search?limit=1") + resp := MakeRequest(t, req, http.StatusOK) + + var limitedResults SearchResults + DecodeJSON(t, resp, &limitedResults) + assert.Len(t, limitedResults.Data, 1) + + req = NewRequest(t, "GET", "/api/v1/users/search") + resp = MakeRequest(t, req, http.StatusOK) + + var results SearchResults + DecodeJSON(t, resp, &results) + assert.Len(t, results.Data, 5) +} + func TestAPIUserSearchSystemUsers(t *testing.T) { defer tests.PrepareTestEnv(t)() for _, systemUser := range []*user_model.User{ diff --git a/tests/integration/api_user_variables_test.go b/tests/integration/api_user_variables_test.go new file mode 100644 index 0000000000..dd5501f0b9 --- /dev/null +++ b/tests/integration/api_user_variables_test.go @@ -0,0 +1,144 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" +) + +func TestAPIUserVariables(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser) + + t.Run("CreateRepoVariable", func(t *testing.T) { + cases := []struct { + Name string + ExpectedStatus int + }{ + { + Name: "-", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "_", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "TEST_VAR", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: "test_var", + ExpectedStatus: http.StatusConflict, + }, + { + Name: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "123var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "var@test", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "github_var", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "gitea_var", + ExpectedStatus: http.StatusBadRequest, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.CreateVariableOption{ + Value: "value", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("UpdateRepoVariable", func(t *testing.T) { + variableName := "test_update_var" + url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName) + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + cases := []struct { + Name string + UpdateName string + ExpectedStatus int + }{ + { + Name: "not_found_var", + ExpectedStatus: http.StatusNotFound, + }, + { + Name: variableName, + UpdateName: "1invalid", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "invalid@name", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "ci", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: variableName, + UpdateName: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + { + Name: variableName, + ExpectedStatus: http.StatusNotFound, + }, + { + Name: "updated_var_name", + ExpectedStatus: http.StatusNoContent, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.UpdateVariableOption{ + Name: c.UpdateName, + Value: "updated_val", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("DeleteRepoVariable", func(t *testing.T) { + variableName := "test_delete_var" + url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName) + + req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ + Value: "initial_val", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + }) +} diff --git a/tests/integration/api_wiki_test.go b/tests/integration/api_wiki_test.go index c61b4a061b..400cf068b4 100644 --- a/tests/integration/api_wiki_test.go +++ b/tests/integration/api_wiki_test.go @@ -15,6 +15,8 @@ import ( repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" api "code.gitea.io/gitea/modules/structs" repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" @@ -286,6 +288,63 @@ func TestAPIEditOtherWikiPage(t *testing.T) { testCreateWiki(http.StatusCreated) } +func TestAPISetWikiGlobalEditability(t *testing.T) { + defer tests.PrepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) + session := loginUser(t, user.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // Create a new repository for testing purposes + repo, _, f := CreateDeclarativeRepo(t, user, "", []unit_model.Type{ + unit_model.TypeCode, + unit_model.TypeWiki, + }, nil, nil) + defer f() + urlStr := fmt.Sprintf("/api/v1/repos/%s", repo.FullName()) + + assertGlobalEditability := func(t *testing.T, editability bool) { + t.Helper() + + req := NewRequest(t, "GET", urlStr) + resp := MakeRequest(t, req, http.StatusOK) + + var opts api.Repository + DecodeJSON(t, resp, &opts) + + assert.Equal(t, opts.GloballyEditableWiki, editability) + } + + t.Run("api includes GloballyEditableWiki", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assertGlobalEditability(t, false) + }) + + t.Run("api can turn on GloballyEditableWiki", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + globallyEditable := true + req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditRepoOption{ + GloballyEditableWiki: &globallyEditable, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + + assertGlobalEditability(t, true) + }) + + t.Run("disabling the wiki disables GloballyEditableWiki", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + hasWiki := false + req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditRepoOption{ + HasWiki: &hasWiki, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + + assertGlobalEditability(t, false) + }) +} + func TestAPIListPageRevisions(t *testing.T) { defer tests.PrepareTestEnv(t)() username := "user2" @@ -324,3 +383,28 @@ func TestAPIListPageRevisions(t *testing.T) { assert.Equal(t, dummyrevisions, revisions) } + +func TestAPIWikiNonMasterBranch(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + repo, _, f := CreateDeclarativeRepoWithOptions(t, user, DeclarativeRepoOptions{ + WikiBranch: optional.Some("main"), + }) + defer f() + + uris := []string{ + "revisions/Home", + "pages", + "page/Home", + } + baseURL := fmt.Sprintf("/api/v1/repos/%s/wiki", repo.FullName()) + for _, uri := range uris { + t.Run(uri, func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestf(t, "GET", "%s/%s", baseURL, uri) + MakeRequest(t, req, http.StatusOK) + }) + } +} diff --git a/tests/integration/archived_labels_display_test.go b/tests/integration/archived_labels_display_test.go new file mode 100644 index 0000000000..c9748f81d6 --- /dev/null +++ b/tests/integration/archived_labels_display_test.go @@ -0,0 +1,71 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "net/url" + "strings" + "testing" + + "github.com/PuerkitoBio/goquery" + "github.com/stretchr/testify/assert" +) + +func TestArchivedLabelVisualProperties(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user2") + + // Create labels + session.MakeRequest(t, NewRequestWithValues(t, "POST", "user2/repo1/labels/new", map[string]string{ + "_csrf": GetCSRF(t, session, "user2/repo1/labels"), + "title": "active_label", + "description": "", + "color": "#aa00aa", + }), http.StatusSeeOther) + session.MakeRequest(t, NewRequestWithValues(t, "POST", "user2/repo1/labels/new", map[string]string{ + "_csrf": GetCSRF(t, session, "user2/repo1/labels"), + "title": "archived_label", + "description": "", + "color": "#00aa00", + }), http.StatusSeeOther) + + // Get ID of label to archive it + var id string + doc := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "user2/repo1/labels"), http.StatusOK).Body) + doc.Find(".issue-label-list .item").Each(func(i int, s *goquery.Selection) { + label := s.Find(".label-title .label") + if label.Text() == "archived_label" { + href, _ := s.Find(".label-issues a.open-issues").Attr("href") + hrefParts := strings.Split(href, "=") + id = hrefParts[len(hrefParts)-1] + } + }) + + // Make label archived + session.MakeRequest(t, NewRequestWithValues(t, "POST", "user2/repo1/labels/edit", map[string]string{ + "_csrf": GetCSRF(t, session, "user2/repo1/labels"), + "id": id, + "title": "archived_label", + "is_archived": "on", + "description": "", + "color": "#00aa00", + }), http.StatusSeeOther) + + // Test label properties + doc = NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "user2/repo1/labels"), http.StatusOK).Body) + doc.Find(".issue-label-list .item").Each(func(i int, s *goquery.Selection) { + label := s.Find(".label-title .label") + style, _ := label.Attr("style") + + if label.Text() == "active_label" { + assert.False(t, label.HasClass("archived-label")) + assert.Contains(t, style, "background-color: #aa00aaff") + } else if label.Text() == "archived_label" { + assert.True(t, label.HasClass("archived-label")) + assert.Contains(t, style, "background-color: #00aa007f") + } + }) + }) +} diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index 3a5fdb97a6..06677287c0 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -112,13 +112,17 @@ func getLDAPServerPort() string { return port } -func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval string) map[string]string { +func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, mailKeyAttribute, defaultDomainName, groupFilter, groupTeamMap, groupTeamMapRemoval string) map[string]string { // Modify user filter to test group filter explicitly userFilter := "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))" if groupFilter != "" { userFilter = "(&(objectClass=inetOrgPerson)(uid=%s))" } + if len(mailKeyAttribute) == 0 { + mailKeyAttribute = "mail" + } + return map[string]string{ "_csrf": csrf, "type": "2", @@ -134,8 +138,9 @@ func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap "attribute_username": "uid", "attribute_name": "givenName", "attribute_surname": "sn", - "attribute_mail": "mail", + "attribute_mail": mailKeyAttribute, "attribute_ssh_public_key": sshKeyAttribute, + "default_domain_name": defaultDomainName, "is_sync_enabled": "on", "is_active": "on", "groups_enabled": "on", @@ -148,7 +153,7 @@ func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap } } -func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupMapParams ...string) { +func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, mailKeyAttribute, defaultDomainName, groupFilter string, groupMapParams ...string) { groupTeamMapRemoval := "off" groupTeamMap := "" if len(groupMapParams) == 2 { @@ -157,7 +162,7 @@ func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupM } session := loginUser(t, "user1") csrf := GetCSRF(t, session, "/admin/auths/new") - req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval)) + req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, mailKeyAttribute, defaultDomainName, groupFilter, groupTeamMap, groupTeamMapRemoval)) session.MakeRequest(t, req, http.StatusSeeOther) } @@ -167,7 +172,7 @@ func TestLDAPUserSignin(t *testing.T) { return } defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "") + addAuthSourceLDAP(t, "", "", "", "") u := gitLDAPUsers[0] @@ -184,7 +189,7 @@ func TestLDAPUserSignin(t *testing.T) { func TestLDAPAuthChange(t *testing.T) { defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "") + addAuthSourceLDAP(t, "", "", "", "") session := loginUser(t, "user1") req := NewRequest(t, "GET", "/admin/auths") @@ -205,7 +210,7 @@ func TestLDAPAuthChange(t *testing.T) { binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value") assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn) - req = NewRequestWithValues(t, "POST", href, buildAuthSourceLDAPPayload(csrf, "", "", "", "off")) + req = NewRequestWithValues(t, "POST", href, buildAuthSourceLDAPPayload(csrf, "", "", "", "", "", "off")) session.MakeRequest(t, req, http.StatusSeeOther) req = NewRequest(t, "GET", href) @@ -215,6 +220,21 @@ func TestLDAPAuthChange(t *testing.T) { assert.Equal(t, host, getLDAPServerHost()) binddn, _ = doc.Find(`input[name="bind_dn"]`).Attr("value") assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn) + domainname, _ := doc.Find(`input[name="default_domain_name"]`).Attr("value") + assert.Equal(t, "", domainname) + + req = NewRequestWithValues(t, "POST", href, buildAuthSourceLDAPPayload(csrf, "", "", "test.org", "", "", "off")) + session.MakeRequest(t, req, http.StatusSeeOther) + + req = NewRequest(t, "GET", href) + resp = session.MakeRequest(t, req, http.StatusOK) + doc = NewHTMLParser(t, resp.Body) + host, _ = doc.Find(`input[name="host"]`).Attr("value") + assert.Equal(t, host, getLDAPServerHost()) + binddn, _ = doc.Find(`input[name="bind_dn"]`).Attr("value") + assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn) + domainname, _ = doc.Find(`input[name="default_domain_name"]`).Attr("value") + assert.Equal(t, "test.org", domainname) } func TestLDAPUserSync(t *testing.T) { @@ -223,7 +243,7 @@ func TestLDAPUserSync(t *testing.T) { return } defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "") + addAuthSourceLDAP(t, "", "", "", "") auth.SyncExternalUsers(context.Background(), true) // Check if users exists @@ -252,7 +272,7 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) { session := loginUser(t, "user1") csrf := GetCSRF(t, session, "/admin/auths/new") - payload := buildAuthSourceLDAPPayload(csrf, "", "", "", "") + payload := buildAuthSourceLDAPPayload(csrf, "", "", "", "", "", "") payload["attribute_username"] = "" req := NewRequestWithValues(t, "POST", "/admin/auths/new", payload) session.MakeRequest(t, req, http.StatusSeeOther) @@ -300,7 +320,7 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) { return } defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "(cn=git)") + addAuthSourceLDAP(t, "", "", "", "(cn=git)") // Assert a user not a member of the LDAP group "cn=git" cannot login // This test may look like TestLDAPUserSigninFailed but it is not. @@ -359,7 +379,7 @@ func TestLDAPUserSigninFailed(t *testing.T) { return } defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "") + addAuthSourceLDAP(t, "", "", "", "") u := otherLDAPUsers[0] testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect")) @@ -371,7 +391,7 @@ func TestLDAPUserSSHKeySync(t *testing.T) { return } defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "sshPublicKey", "") + addAuthSourceLDAP(t, "sshPublicKey", "", "", "") auth.SyncExternalUsers(context.Background(), true) @@ -404,7 +424,7 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.T) { return } defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "", "on", `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`) + addAuthSourceLDAP(t, "", "", "", "", "on", `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`) org, err := organization.GetOrgByName(db.DefaultContext, "org26") assert.NoError(t, err) team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11") @@ -449,7 +469,7 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) { return } defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`) + addAuthSourceLDAP(t, "", "", "", "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`) org, err := organization.GetOrgByName(db.DefaultContext, "org26") assert.NoError(t, err) team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11") @@ -487,6 +507,58 @@ func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) { session := loginUser(t, "user1") csrf := GetCSRF(t, session, "/admin/auths/new") - req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, "", "", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "off")) + req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, "", "", "", "", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "off")) session.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok } + +func TestLDAPUserSyncInvalidMail(t *testing.T) { + if skipLDAPTests() { + t.Skip() + return + } + defer tests.PrepareTestEnv(t)() + addAuthSourceLDAP(t, "", "nonexisting", "", "") + auth.SyncExternalUsers(context.Background(), true) + + // Check if users exists + for _, gitLDAPUser := range gitLDAPUsers { + dbUser, err := user_model.GetUserByName(db.DefaultContext, gitLDAPUser.UserName) + assert.NoError(t, err) + assert.Equal(t, gitLDAPUser.UserName, dbUser.Name) + assert.Equal(t, gitLDAPUser.UserName+"@localhost.local", dbUser.Email) + assert.Equal(t, gitLDAPUser.IsAdmin, dbUser.IsAdmin) + assert.Equal(t, gitLDAPUser.IsRestricted, dbUser.IsRestricted) + } + + // Check if no users exist + for _, otherLDAPUser := range otherLDAPUsers { + _, err := user_model.GetUserByName(db.DefaultContext, otherLDAPUser.UserName) + assert.True(t, user_model.IsErrUserNotExist(err)) + } +} + +func TestLDAPUserSyncInvalidMailDefaultDomain(t *testing.T) { + if skipLDAPTests() { + t.Skip() + return + } + defer tests.PrepareTestEnv(t)() + addAuthSourceLDAP(t, "", "nonexisting", "test.org", "") + auth.SyncExternalUsers(context.Background(), true) + + // Check if users exists + for _, gitLDAPUser := range gitLDAPUsers { + dbUser, err := user_model.GetUserByName(db.DefaultContext, gitLDAPUser.UserName) + assert.NoError(t, err) + assert.Equal(t, gitLDAPUser.UserName, dbUser.Name) + assert.Equal(t, gitLDAPUser.UserName+"@test.org", dbUser.Email) + assert.Equal(t, gitLDAPUser.IsAdmin, dbUser.IsAdmin) + assert.Equal(t, gitLDAPUser.IsRestricted, dbUser.IsRestricted) + } + + // Check if no users exist + for _, otherLDAPUser := range otherLDAPUsers { + _, err := user_model.GetUserByName(db.DefaultContext, otherLDAPUser.UserName) + assert.True(t, user_model.IsErrUserNotExist(err)) + } +} diff --git a/tests/integration/benchmarks_test.go b/tests/integration/benchmarks_test.go index 7a882fe836..62da761d2d 100644 --- a/tests/integration/benchmarks_test.go +++ b/tests/integration/benchmarks_test.go @@ -4,7 +4,7 @@ package integration import ( - "math/rand" + "math/rand/v2" "net/http" "net/url" "testing" @@ -18,7 +18,7 @@ import ( func StringWithCharset(length int, charset string) string { b := make([]byte, length) for i := range b { - b[i] = charset[rand.Intn(len(charset))] + b[i] = charset[rand.IntN(len(charset))] } return string(b) } @@ -37,7 +37,7 @@ func BenchmarkRepoBranchCommit(b *testing.B) { b.ResetTimer() b.Run("CreateBranch", func(b *testing.B) { b.StopTimer() - branchName := StringWithCharset(5+rand.Intn(10), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + branchName := StringWithCharset(5+rand.IntN(10), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") b.StartTimer() for i := 0; i < b.N; i++ { b.Run("new_"+branchName, func(b *testing.B) { diff --git a/tests/integration/cmd_admin_test.go b/tests/integration/cmd_admin_test.go new file mode 100644 index 0000000000..6a85460450 --- /dev/null +++ b/tests/integration/cmd_admin_test.go @@ -0,0 +1,146 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/url" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func Test_Cmd_AdminUser(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + for _, testCase := range []struct { + name string + options []string + mustChangePassword bool + }{ + { + name: "default", + options: []string{}, + mustChangePassword: true, + }, + { + name: "--must-change-password=false", + options: []string{"--must-change-password=false"}, + mustChangePassword: false, + }, + { + name: "--must-change-password=true", + options: []string{"--must-change-password=true"}, + mustChangePassword: true, + }, + { + name: "--must-change-password", + options: []string{"--must-change-password"}, + mustChangePassword: true, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + name := "testuser" + + options := []string{"user", "create", "--username", name, "--password", "password", "--email", name + "@example.com"} + options = append(options, testCase.options...) + output, err := runMainApp("admin", options...) + assert.NoError(t, err) + assert.Contains(t, output, "has been successfully created") + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: name}) + assert.Equal(t, testCase.mustChangePassword, user.MustChangePassword) + + options = []string{"user", "change-password", "--username", name, "--password", "password"} + options = append(options, testCase.options...) + output, err = runMainApp("admin", options...) + assert.NoError(t, err) + assert.Contains(t, output, "has been successfully updated") + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: name}) + assert.Equal(t, testCase.mustChangePassword, user.MustChangePassword) + + _, err = runMainApp("admin", "user", "delete", "--username", name) + assert.NoError(t, err) + unittest.AssertNotExistsBean(t, &user_model.User{Name: name}) + }) + } + }) +} + +func Test_Cmd_AdminFirstUser(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + for _, testCase := range []struct { + name string + options []string + mustChangePassword bool + isAdmin bool + }{ + { + name: "default", + options: []string{}, + mustChangePassword: false, + isAdmin: false, + }, + { + name: "--must-change-password=false", + options: []string{"--must-change-password=false"}, + mustChangePassword: false, + isAdmin: false, + }, + { + name: "--must-change-password=true", + options: []string{"--must-change-password=true"}, + mustChangePassword: true, + isAdmin: false, + }, + { + name: "--must-change-password", + options: []string{"--must-change-password"}, + mustChangePassword: true, + isAdmin: false, + }, + { + name: "--admin default", + options: []string{"--admin"}, + mustChangePassword: false, + isAdmin: true, + }, + { + name: "--admin --must-change-password=false", + options: []string{"--admin", "--must-change-password=false"}, + mustChangePassword: false, + isAdmin: true, + }, + { + name: "--admin --must-change-password=true", + options: []string{"--admin", "--must-change-password=true"}, + mustChangePassword: true, + isAdmin: true, + }, + { + name: "--admin --must-change-password", + options: []string{"--admin", "--must-change-password"}, + mustChangePassword: true, + isAdmin: true, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + db.GetEngine(db.DefaultContext).Exec("DELETE FROM `user`") + db.GetEngine(db.DefaultContext).Exec("DELETE FROM `email_address`") + assert.Equal(t, int64(0), user_model.CountUsers(db.DefaultContext, nil)) + name := "testuser" + + options := []string{"user", "create", "--username", name, "--password", "password", "--email", name + "@example.com"} + options = append(options, testCase.options...) + output, err := runMainApp("admin", options...) + assert.NoError(t, err) + assert.Contains(t, output, "has been successfully created") + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: name}) + assert.Equal(t, testCase.mustChangePassword, user.MustChangePassword) + assert.Equal(t, testCase.isAdmin, user.IsAdmin) + }) + } + }) +} diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go index 5d5529c36e..0929e8938e 100644 --- a/tests/integration/compare_test.go +++ b/tests/integration/compare_test.go @@ -14,6 +14,8 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/gitrepo" repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" @@ -182,3 +184,32 @@ func TestCompareWithPRsDisabled(t *testing.T) { }) }) } + +func TestCompareCrossRepo(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1-copy") + testCreateBranch(t, session, "user1", "repo1-copy", "branch/master", "recent-push", http.StatusSeeOther) + testEditFile(t, session, "user1", "repo1-copy", "recent-push", "README.md", "Hello recently!\n") + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1-copy"}) + + gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo) + assert.NoError(t, err) + defer gitRepo.Close() + + lastCommit, err := gitRepo.GetBranchCommitID("recent-push") + assert.NoError(t, err) + assert.NotEmpty(t, lastCommit) + + t.Run("view file button links to correct file in fork", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2/repo1/compare/master...user1/repo1-copy:recent-push") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, "a[href='/user1/repo1-copy/src/commit/"+lastCommit+"/README.md']", true) + htmlDoc.AssertElement(t, "a[href='/user1/repo1/src/commit/"+lastCommit+"/README.md']", false) + }) + }) +} diff --git a/tests/integration/db_collation_test.go b/tests/integration/db_collation_test.go index eee26d1ed1..4d822c45a6 100644 --- a/tests/integration/db_collation_test.go +++ b/tests/integration/db_collation_test.go @@ -39,7 +39,7 @@ func TestDatabaseCollationSelfCheckUI(t *testing.T) { htmlDoc.AssertElement(t, "a.item[href*='/admin/self_check']", exists) } - if setting.Database.Type.IsMySQL() || setting.Database.Type.IsMSSQL() { + if setting.Database.Type.IsMySQL() { assertSelfCheckExists(true) } else { assertSelfCheckExists(false) @@ -61,10 +61,9 @@ func TestDatabaseCollation(t *testing.T) { assert.EqualValues(t, 2, cnt) _, _ = x.Exec("DROP TABLE IF EXISTS test_collation_tbl") - // by default, SQLite3 and PostgreSQL are using case-sensitive collations, but MySQL and MSSQL are not - // the following tests are only for MySQL and MSSQL - if !setting.Database.Type.IsMySQL() && !setting.Database.Type.IsMSSQL() { - t.Skip("only MySQL and MSSQL requires the case-sensitive collation check at the moment") + // by default, SQLite3 and PostgreSQL are using case-sensitive collations, but MySQL is not. + if !setting.Database.Type.IsMySQL() { + t.Skip("only MySQL requires the case-sensitive collation check at the moment") return } @@ -86,20 +85,11 @@ func TestDatabaseCollation(t *testing.T) { assert.True(t, r.CollationEquals("abc", "abc")) assert.True(t, r.CollationEquals("abc", "utf8mb4_abc")) assert.False(t, r.CollationEquals("utf8mb4_general_ci", "utf8mb4_unicode_ci")) - } else if setting.Database.Type.IsMSSQL() { - assert.True(t, r.IsCollationCaseSensitive("Latin1_General_CS_AS")) - assert.False(t, r.IsCollationCaseSensitive("Latin1_General_CI_AS")) - assert.True(t, r.CollationEquals("abc", "abc")) - assert.False(t, r.CollationEquals("Latin1_General_CS_AS", "SQL_Latin1_General_CP1_CS_AS")) } else { assert.Fail(t, "unexpected database type") } }) - if setting.Database.Type.IsMSSQL() { - return // skip table converting tests because MSSQL doesn't have a simple solution at the moment - } - t.Run("Convert tables to utf8mb4_bin", func(t *testing.T) { defer tests.PrintCurrentTest(t)() diff --git a/tests/integration/doctor_packages_nuget_test.go b/tests/integration/doctor_packages_nuget_test.go new file mode 100644 index 0000000000..29e4f6055f --- /dev/null +++ b/tests/integration/doctor_packages_nuget_test.go @@ -0,0 +1,121 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "archive/zip" + "bytes" + "fmt" + "io" + "strings" + "testing" + + "code.gitea.io/gitea/models/db" + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + packages_module "code.gitea.io/gitea/modules/packages" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + doctor "code.gitea.io/gitea/services/doctor" + packages_service "code.gitea.io/gitea/services/packages" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestDoctorPackagesNuget(t *testing.T) { + defer tests.PrepareTestEnv(t, 1)() + // use local storage for tests because minio is too flaky + defer test.MockVariableValue(&setting.Packages.Storage.Type, setting.LocalStorageType)() + + logger := log.GetLogger("doctor") + + ctx := db.DefaultContext + + packageName := "test.package" + packageVersion := "1.0.3" + packageAuthors := "KN4CK3R" + packageDescription := "Gitea Test Package" + + createPackage := func(id, version string) io.Reader { + var buf bytes.Buffer + archive := zip.NewWriter(&buf) + w, _ := archive.Create("package.nuspec") + w.Write([]byte(` + + + ` + id + ` + ` + version + ` + ` + packageAuthors + ` + ` + packageDescription + ` + + + + + + + `)) + archive.Close() + return &buf + } + + pkg := createPackage(packageName, packageVersion) + + pkgBuf, err := packages_module.CreateHashedBufferFromReader(pkg) + assert.NoError(t, err, "Error creating hashed buffer from nupkg") + defer pkgBuf.Close() + + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + assert.NoError(t, err, "Error getting user by ID 2") + + t.Run("PackagesNugetNuspecCheck", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + pi := &packages_service.PackageInfo{ + Owner: doer, + PackageType: packages_model.TypeNuGet, + Name: packageName, + Version: packageVersion, + } + _, _, err := packages_service.CreatePackageAndAddFile( + ctx, + &packages_service.PackageCreationInfo{ + PackageInfo: *pi, + SemverCompatible: true, + Creator: doer, + Metadata: nil, + }, + &packages_service.PackageFileCreationInfo{ + PackageFileInfo: packages_service.PackageFileInfo{ + Filename: strings.ToLower(fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion)), + }, + Creator: doer, + Data: pkgBuf, + IsLead: true, + }, + ) + assert.NoError(t, err, "Error creating package and adding file") + + assert.NoError(t, doctor.PackagesNugetNuspecCheck(ctx, logger, true), "Doctor check failed") + + s, _, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + ctx, + &packages_service.PackageInfo{ + Owner: doer, + PackageType: packages_model.TypeNuGet, + Name: packageName, + Version: packageVersion, + }, + &packages_service.PackageFileInfo{ + Filename: strings.ToLower(fmt.Sprintf("%s.nuspec", packageName)), + }, + ) + + assert.NoError(t, err, "Error getting nuspec file stream by package name and version") + defer s.Close() + + assert.Equal(t, fmt.Sprintf("%s.nuspec", packageName), pf.Name, "Not a nuspec") + }) +} diff --git a/tests/integration/easymde_test.go b/tests/integration/easymde_test.go new file mode 100644 index 0000000000..c8203d36be --- /dev/null +++ b/tests/integration/easymde_test.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" +) + +func TestEasyMDESwitch(t *testing.T) { + session := loginUser(t, "user2") + testEasyMDESwitch(t, session, "user2/glob/issues/1", false) + testEasyMDESwitch(t, session, "user2/glob/issues/new", false) + testEasyMDESwitch(t, session, "user2/glob/wiki?action=_new", true) + testEasyMDESwitch(t, session, "user2/glob/releases/new", true) +} + +func testEasyMDESwitch(t *testing.T, session *TestSession, url string, expected bool) { + t.Helper() + req := NewRequest(t, "GET", url) + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + doc.AssertElement(t, ".combo-markdown-editor button.markdown-switch-easymde", expected) +} diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index f2f312b8a4..ae0aea237b 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -187,6 +187,19 @@ func TestEditFileToNewBranch(t *testing.T) { }) } +func TestEditorAddTranslation(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user2/repo1/_new/master") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + placeholder, ok := htmlDoc.Find("input[name='commit_summary']").Attr("placeholder") + assert.True(t, ok) + assert.EqualValues(t, `Add ""`, placeholder) +} + func TestCommitMail(t *testing.T) { onGiteaRun(t, func(t *testing.T, _ *url.URL) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) @@ -391,7 +404,7 @@ func TestCommitMail(t *testing.T) { t.Run("Upload", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - // Upload two seperate times, so we have two different 'uploads' that can + // Upload two separate times, so we have two different 'uploads' that can // be used indepently of each other. uploadFile := func(t *testing.T, name, content string) string { t.Helper() diff --git a/tests/integration/explore_code_test.go b/tests/integration/explore_code_test.go new file mode 100644 index 0000000000..cf3939b093 --- /dev/null +++ b/tests/integration/explore_code_test.go @@ -0,0 +1,25 @@ +package integration + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestExploreCodeSearchIndexer(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Indexer.RepoIndexerEnabled, true)() + + req := NewRequest(t, "GET", "/explore/code") + resp := MakeRequest(t, req, http.StatusOK) + + doc := NewHTMLParser(t, resp.Body) + msg := doc.Find(".explore").Find(".ui.container").Find(".ui.message[data-test-tag=grep]") + + assert.EqualValues(t, 0, len(msg.Nodes)) +} diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go index 0a35724807..838ee0ff79 100644 --- a/tests/integration/git_push_test.go +++ b/tests/integration/git_push_test.go @@ -7,12 +7,17 @@ import ( "fmt" "net/url" "testing" + "time" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/test" repo_service "code.gitea.io/gitea/services/repository" "github.com/stretchr/testify/assert" @@ -146,3 +151,90 @@ func runTestGitPush(t *testing.T, u *url.URL, gitOperation func(t *testing.T, gi require.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, repo.ID)) } + +func TestOptionsGitPush(t *testing.T) { + onGiteaRun(t, testOptionsGitPush) +} + +func testOptionsGitPush(t *testing.T, u *url.URL) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{ + Name: "repo-to-push", + Description: "test git push", + AutoInit: false, + DefaultBranch: "main", + IsPrivate: false, + }) + require.NoError(t, err) + require.NotEmpty(t, repo) + + gitPath := t.TempDir() + + doGitInitTestRepository(gitPath)(t) + + u.Path = repo.FullName() + ".git" + u.User = url.UserPassword(user.LowerName, userPassword) + doGitAddRemote(gitPath, "origin", u)(t) + + t.Run("Unknown push options are rejected", func(t *testing.T) { + logChecker, cleanup := test.NewLogChecker(log.DEFAULT, log.TRACE) + logChecker.Filter("unknown option").StopMark("Git push options validation") + defer cleanup() + branchName := "branch0" + doGitCreateBranch(gitPath, branchName)(t) + doGitPushTestRepositoryFail(gitPath, "origin", branchName, "-o", "repo.template=false", "-o", "uknownoption=randomvalue")(t) + logFiltered, logStopped := logChecker.Check(5 * time.Second) + assert.True(t, logStopped) + assert.True(t, logFiltered[0]) + }) + + t.Run("Owner sets private & template to true via push options", func(t *testing.T) { + branchName := "branch1" + doGitCreateBranch(gitPath, branchName)(t) + doGitPushTestRepository(gitPath, "origin", branchName, "-o", "repo.private=true", "-o", "repo.template=true")(t) + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, user.Name, "repo-to-push") + require.NoError(t, err) + require.True(t, repo.IsPrivate) + require.True(t, repo.IsTemplate) + }) + + t.Run("Owner sets private & template to false via push options", func(t *testing.T) { + branchName := "branch2" + doGitCreateBranch(gitPath, branchName)(t) + doGitPushTestRepository(gitPath, "origin", branchName, "-o", "repo.private=false", "-o", "repo.template=false")(t) + repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, user.Name, "repo-to-push") + require.NoError(t, err) + require.False(t, repo.IsPrivate) + require.False(t, repo.IsTemplate) + }) + + // create a collaborator with write access + collaborator := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + u.User = url.UserPassword(collaborator.LowerName, userPassword) + doGitAddRemote(gitPath, "collaborator", u)(t) + repo_module.AddCollaborator(db.DefaultContext, repo, collaborator) + + t.Run("Collaborator with write access is allowed to push", func(t *testing.T) { + branchName := "branch3" + doGitCreateBranch(gitPath, branchName)(t) + doGitPushTestRepository(gitPath, "collaborator", branchName)(t) + }) + + t.Run("Collaborator with write access fails to change private & template via push options", func(t *testing.T) { + logChecker, cleanup := test.NewLogChecker(log.DEFAULT, log.TRACE) + logChecker.Filter("permission denied for changing repo settings").StopMark("Git push options validation") + defer cleanup() + branchName := "branch4" + doGitCreateBranch(gitPath, branchName)(t) + doGitPushTestRepositoryFail(gitPath, "collaborator", branchName, "-o", "repo.private=true", "-o", "repo.template=true")(t) + repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, user.Name, "repo-to-push") + require.NoError(t, err) + require.False(t, repo.IsPrivate) + require.False(t, repo.IsTemplate) + logFiltered, logStopped := logChecker.Check(5 * time.Second) + assert.True(t, logStopped) + assert.True(t, logFiltered[0]) + }) + + require.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, repo.ID)) +} diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index 782390002c..6ee3be2df2 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -5,9 +5,9 @@ package integration import ( "bytes" + "crypto/rand" "encoding/hex" "fmt" - "math/rand" "net/http" "net/url" "os" @@ -1025,7 +1025,7 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headB t.Run("Succeeds", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - _, _, gitErr := git.NewCommand(git.DefaultContext, "push", "origin", "-o", "force-push=true").AddDynamicArguments("HEAD:refs/for/master/" + headBranch + "-force-push").RunStdString(&git.RunOpts{Dir: dstPath}) + _, _, gitErr := git.NewCommand(git.DefaultContext, "push", "origin", "-o", "force-push").AddDynamicArguments("HEAD:refs/for/master/" + headBranch + "-force-push").RunStdString(&git.RunOpts{Dir: dstPath}) assert.NoError(t, gitErr) currentHeadCommitID, err := upstreamGitRepo.GetRefCommitID(pr.GetGitRefName()) @@ -1076,6 +1076,7 @@ func TestDataAsync_Issue29101(t *testing.T) { gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo) assert.NoError(t, err) + defer gitRepo.Close() commit, err := gitRepo.GetCommit(sha) assert.NoError(t, err) diff --git a/tests/integration/gpg_git_test.go b/tests/integration/gpg_git_test.go index 00890cfb38..3ba4a5882c 100644 --- a/tests/integration/gpg_git_test.go +++ b/tests/integration/gpg_git_test.go @@ -19,9 +19,9 @@ import ( "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/stretchr/testify/assert" - "golang.org/x/crypto/openpgp" - "golang.org/x/crypto/openpgp/armor" ) func TestGPGGit(t *testing.T) { diff --git a/tests/integration/incoming_email_test.go b/tests/integration/incoming_email_test.go index 1284833864..543e620dbf 100644 --- a/tests/integration/incoming_email_test.go +++ b/tests/integration/incoming_email_test.go @@ -76,14 +76,11 @@ func TestIncomingEmail(t *testing.T) { t.Run("Handler", func(t *testing.T) { t.Run("Reply", func(t *testing.T) { - t.Run("Comment", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() + checkReply := func(t *testing.T, payload []byte, issue *issues_model.Issue, commentType issues_model.CommentType) { + t.Helper() handler := &incoming.ReplyHandler{} - payload, err := incoming_payload.CreateReferencePayload(issue) - assert.NoError(t, err) - assert.Error(t, handler.Handle(db.DefaultContext, &incoming.MailContent{}, nil, payload)) assert.NoError(t, handler.Handle(db.DefaultContext, &incoming.MailContent{}, user, payload)) @@ -101,7 +98,7 @@ func TestIncomingEmail(t *testing.T) { comments, err := issues_model.FindComments(db.DefaultContext, &issues_model.FindCommentsOptions{ IssueID: issue.ID, - Type: issues_model.CommentTypeComment, + Type: commentType, }) assert.NoError(t, err) assert.NotEmpty(t, comments) @@ -113,6 +110,14 @@ func TestIncomingEmail(t *testing.T) { attachment := comment.Attachments[0] assert.Equal(t, content.Attachments[0].Name, attachment.Name) assert.EqualValues(t, 4, attachment.Size) + } + t.Run("Issue", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + payload, err := incoming_payload.CreateReferencePayload(issue) + assert.NoError(t, err) + + checkReply(t, payload, issue, issues_model.CommentTypeComment) }) t.Run("CodeComment", func(t *testing.T) { @@ -121,33 +126,22 @@ func TestIncomingEmail(t *testing.T) { comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 6}) issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) - handler := &incoming.ReplyHandler{} - content := &incoming.MailContent{ - Content: "code reply by mail", - Attachments: []*incoming.Attachment{ - { - Name: "attachment.txt", - Content: []byte("test"), - }, - }, - } + payload, err := incoming_payload.CreateReferencePayload(comment) + assert.NoError(t, err) + + checkReply(t, payload, issue, issues_model.CommentTypeCode) + }) + + t.Run("Comment", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2}) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}) payload, err := incoming_payload.CreateReferencePayload(comment) assert.NoError(t, err) - assert.NoError(t, handler.Handle(db.DefaultContext, content, user, payload)) - - comments, err := issues_model.FindComments(db.DefaultContext, &issues_model.FindCommentsOptions{ - IssueID: issue.ID, - Type: issues_model.CommentTypeCode, - }) - assert.NoError(t, err) - assert.NotEmpty(t, comments) - comment = comments[len(comments)-1] - assert.Equal(t, user.ID, comment.PosterID) - assert.Equal(t, content.Content, comment.Content) - assert.NoError(t, comment.LoadAttachments(db.DefaultContext)) - assert.Empty(t, comment.Attachments) + checkReply(t, payload, issue, issues_model.CommentTypeComment) }) }) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index e147d6a21b..f6daf0d146 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -36,22 +36,26 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/testlogger" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers" + "code.gitea.io/gitea/services/auth/source/remote" gitea_context "code.gitea.io/gitea/services/context" repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" user_service "code.gitea.io/gitea/services/user" + wiki_service "code.gitea.io/gitea/services/wiki" "code.gitea.io/gitea/tests" "github.com/PuerkitoBio/goquery" gouuid "github.com/google/uuid" "github.com/markbates/goth" "github.com/markbates/goth/gothic" - goth_gitlab "github.com/markbates/goth/providers/gitlab" + goth_gitlab "github.com/markbates/goth/providers/github" + goth_github "github.com/markbates/goth/providers/gitlab" "github.com/santhosh-tekuri/jsonschema/v5" "github.com/stretchr/testify/assert" ) @@ -184,8 +188,7 @@ func TestMain(m *testing.M) { exitCode := m.Run() if err := testlogger.WriterCloser.Reset(); err != nil { - fmt.Printf("testlogger.WriterCloser.Reset: %v\n", err) - os.Exit(1) + fmt.Printf("testlogger.WriterCloser.Reset: error ignored: %v\n", err) } if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil { @@ -337,6 +340,36 @@ func authSourcePayloadGitLabCustom(name string) map[string]string { return payload } +func authSourcePayloadGitHub(name string) map[string]string { + payload := authSourcePayloadOAuth2(name) + payload["oauth2_provider"] = "github" + return payload +} + +func authSourcePayloadGitHubCustom(name string) map[string]string { + payload := authSourcePayloadGitHub(name) + payload["oauth2_use_custom_url"] = "on" + payload["oauth2_auth_url"] = goth_github.AuthURL + payload["oauth2_token_url"] = goth_github.TokenURL + payload["oauth2_profile_url"] = goth_github.ProfileURL + return payload +} + +func createRemoteAuthSource(t *testing.T, name, url, matchingSource string) *auth.Source { + assert.NoError(t, auth.CreateSource(context.Background(), &auth.Source{ + Type: auth.Remote, + Name: name, + IsActive: true, + Cfg: &remote.Source{ + URL: url, + MatchingSource: matchingSource, + }, + })) + source, err := auth.GetSourceByName(context.Background(), name) + assert.NoError(t, err) + return source +} + func createUser(ctx context.Context, t testing.TB, user *user_model.User) func() { user.MustChangePassword = false user.LowerName = strings.ToLower(user.Name) @@ -622,7 +655,7 @@ func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile schema, err := jsonschema.Compile(schemaFilePath) assert.NoError(t, err) - var data interface{} + var data any err = json.Unmarshal(resp.Body.Bytes(), &data) assert.NoError(t, err) @@ -653,19 +686,39 @@ func GetHTMLTitle(t testing.TB, session *TestSession, urlStr string) string { return doc.Find("head title").Text() } -func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, enabledUnits, disabledUnits []unit_model.Type, files []*files_service.ChangeRepoFile) (*repo_model.Repository, string, func()) { +type DeclarativeRepoOptions struct { + Name optional.Option[string] + EnabledUnits optional.Option[[]unit_model.Type] + DisabledUnits optional.Option[[]unit_model.Type] + Files optional.Option[[]*files_service.ChangeRepoFile] + WikiBranch optional.Option[string] + AutoInit optional.Option[bool] +} + +func CreateDeclarativeRepoWithOptions(t *testing.T, owner *user_model.User, opts DeclarativeRepoOptions) (*repo_model.Repository, string, func()) { t.Helper() - repoName := name - if repoName == "" { + // Not using opts.Name.ValueOrDefault() here to avoid unnecessarily + // generating an UUID when a name is specified. + var repoName string + if opts.Name.Has() { + repoName = opts.Name.Value() + } else { repoName = gouuid.NewString() } - // Create a new repository + var autoInit bool + if opts.AutoInit.Has() { + autoInit = opts.AutoInit.Value() + } else { + autoInit = true + } + + // Create the repository repo, err := repo_service.CreateRepository(db.DefaultContext, owner, owner, repo_service.CreateRepoOptions{ Name: repoName, Description: "Temporary Repo", - AutoInit: true, + AutoInit: autoInit, Gitignores: "", License: "WTFPL", Readme: "Default", @@ -674,21 +727,32 @@ func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, en assert.NoError(t, err) assert.NotEmpty(t, repo) - if enabledUnits != nil || disabledUnits != nil { - units := make([]repo_model.RepoUnit, len(enabledUnits)) - for i, unitType := range enabledUnits { - units[i] = repo_model.RepoUnit{ + // Populate `enabledUnits` if we have any enabled. + var enabledUnits []repo_model.RepoUnit + if opts.EnabledUnits.Has() { + units := opts.EnabledUnits.Value() + enabledUnits = make([]repo_model.RepoUnit, len(units)) + + for i, unitType := range units { + enabledUnits[i] = repo_model.RepoUnit{ RepoID: repo.ID, Type: unitType, } } + } - err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, units, disabledUnits) + // Adjust the repo units according to our parameters. + if opts.EnabledUnits.Has() || opts.DisabledUnits.Has() { + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, enabledUnits, opts.DisabledUnits.ValueOrDefault(nil)) assert.NoError(t, err) } + // Add files, if any. var sha string - if len(files) > 0 { + if opts.Files.Has() { + assert.True(t, autoInit, "Files cannot be specified if AutoInit is disabled") + files := opts.Files.Value() + resp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, owner, &files_service.ChangeRepoFilesOptions{ Files: files, Message: "add files", @@ -713,7 +777,46 @@ func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, en sha = resp.Commit.SHA } + // If there's a Wiki branch specified, create a wiki, and a default wiki page. + if opts.WikiBranch.Has() { + // Set the wiki branch in the database first + repo.WikiBranch = opts.WikiBranch.Value() + err := repo_model.UpdateRepositoryCols(db.DefaultContext, repo, "wiki_branch") + assert.NoError(t, err) + + // Initialize the wiki + err = wiki_service.InitWiki(db.DefaultContext, repo) + assert.NoError(t, err) + + // Add a new wiki page + err = wiki_service.AddWikiPage(db.DefaultContext, owner, repo, "Home", "Welcome to the wiki!", "Add a Home page") + assert.NoError(t, err) + } + + // Return the repo, the top commit, and a defer-able function to delete the + // repo. return repo, sha, func() { repo_service.DeleteRepository(db.DefaultContext, owner, repo, false) } } + +func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, enabledUnits, disabledUnits []unit_model.Type, files []*files_service.ChangeRepoFile) (*repo_model.Repository, string, func()) { + t.Helper() + + var opts DeclarativeRepoOptions + + if name != "" { + opts.Name = optional.Some(name) + } + if enabledUnits != nil { + opts.EnabledUnits = optional.Some(enabledUnits) + } + if disabledUnits != nil { + opts.DisabledUnits = optional.Some(disabledUnits) + } + if files != nil { + opts.Files = optional.Some(files) + } + + return CreateDeclarativeRepoWithOptions(t, owner, opts) +} diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 38c3a091b5..b63127be82 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -15,16 +15,20 @@ import ( "testing" "time" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/indexer/issues" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" + files_service "code.gitea.io/gitea/services/repository/files" "code.gitea.io/gitea/tests" "github.com/PuerkitoBio/goquery" @@ -191,6 +195,93 @@ func TestNewIssue(t *testing.T) { testNewIssue(t, session, "user2", "repo1", "Title", "Description") } +func TestIssueDependencies(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) + + repo, _, f := CreateDeclarativeRepoWithOptions(t, owner, DeclarativeRepoOptions{}) + defer f() + + createIssue := func(t *testing.T, title string) api.Issue { + t.Helper() + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name) + req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{ + Body: "", + Title: title, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusCreated) + + var apiIssue api.Issue + DecodeJSON(t, resp, &apiIssue) + + return apiIssue + } + addDependency := func(t *testing.T, issue, dependency api.Issue) { + t.Helper() + + urlStr := fmt.Sprintf("/%s/%s/issues/%d/dependency/add", owner.Name, repo.Name, issue.Index) + req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetCSRF(t, session, fmt.Sprintf("/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)), + "newDependency": fmt.Sprintf("%d", dependency.Index), + }) + session.MakeRequest(t, req, http.StatusSeeOther) + } + removeDependency := func(t *testing.T, issue, dependency api.Issue) { + t.Helper() + + urlStr := fmt.Sprintf("/%s/%s/issues/%d/dependency/delete", owner.Name, repo.Name, issue.Index) + req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetCSRF(t, session, fmt.Sprintf("/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)), + "removeDependencyID": fmt.Sprintf("%d", dependency.Index), + "dependencyType": "blockedBy", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + } + + assertHasDependency := func(t *testing.T, issueID, dependencyID int64, hasDependency bool) { + t.Helper() + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", owner.Name, repo.Name, issueID) + req := NewRequest(t, "GET", urlStr) + resp := MakeRequest(t, req, http.StatusOK) + + var issues []api.Issue + DecodeJSON(t, resp, &issues) + + if hasDependency { + assert.NotEmpty(t, issues) + assert.EqualValues(t, issues[0].Index, dependencyID) + } else { + assert.Empty(t, issues) + } + } + + t.Run("Add dependency", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + issue1 := createIssue(t, "issue #1") + issue2 := createIssue(t, "issue #2") + addDependency(t, issue1, issue2) + + assertHasDependency(t, issue1.Index, issue2.Index, true) + }) + + t.Run("Remove dependency", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + issue1 := createIssue(t, "issue #1") + issue2 := createIssue(t, "issue #2") + addDependency(t, issue1, issue2) + removeDependency(t, issue1, issue2) + + assertHasDependency(t, issue1.Index, issue2.Index, false) + }) +} + func TestIssueCommentClose(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") @@ -305,6 +396,16 @@ func TestIssueCommentUpdate(t *testing.T) { comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID}) assert.Equal(t, modifiedContent, comment.Content) + + // make the comment empty + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{ + "_csrf": GetCSRF(t, session, issueURL), + "content": "", + }) + session.MakeRequest(t, req, http.StatusOK) + + comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: commentID}) + assert.Equal(t, "", comment.Content) } func TestIssueReaction(t *testing.T) { @@ -815,3 +916,72 @@ func TestIssueFilterNoFollow(t *testing.T) { assert.Equal(t, "nofollow", rel) }) } + +func TestIssueForm(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + repo, _, f := CreateDeclarativeRepo(t, user2, "", + []unit_model.Type{unit_model.TypeCode, unit_model.TypeIssues}, nil, + []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: ".forgejo/issue_template/test.yaml", + ContentReader: strings.NewReader(`name: Test +about: Hello World +body: + - type: checkboxes + id: test + attributes: + label: Test + options: + - label: This is a label +`), + }, + }, + ) + defer f() + + t.Run("Choose list", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", repo.Link()+"/issues/new/choose") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + htmlDoc.AssertElement(t, "a[href$='/issues/new?template=.forgejo%2fissue_template%2ftest.yaml']", true) + }) + + t.Run("Issue template", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", repo.Link()+"/issues/new?template=.forgejo%2fissue_template%2ftest.yaml") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + htmlDoc.AssertElement(t, "#new-issue .field .ui.checkbox input[name='form-field-test-0']", true) + checkboxLabel := htmlDoc.Find("#new-issue .field .ui.checkbox label").Text() + assert.Contains(t, checkboxLabel, "This is a label") + }) + }) +} + +func TestIssueUnsubscription(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + repo, _, f := CreateDeclarativeRepoWithOptions(t, user, DeclarativeRepoOptions{ + AutoInit: optional.Some(false), + }) + defer f() + session := loginUser(t, user.Name) + + issueURL := testNewIssue(t, session, user.Name, repo.Name, "Issue title", "Description") + req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/watch", issueURL), map[string]string{ + "_csrf": GetCSRF(t, session, issueURL), + "watch": "0", + }) + session.MakeRequest(t, req, http.StatusOK) + }) +} diff --git a/tests/integration/lfs_view_test.go b/tests/integration/lfs_view_test.go index 1775fa629f..9dfcb3e698 100644 --- a/tests/integration/lfs_view_test.go +++ b/tests/integration/lfs_view_test.go @@ -80,4 +80,26 @@ func TestLFSRender(t *testing.T) { content := doc.Find("div.file-view").Text() assert.Contains(t, content, "Testing READMEs in LFS") }) + + t.Run("/settings/lfs/pointers", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // visit /user2/lfs/settings/lfs/pointer + req := NewRequest(t, "GET", "/user2/lfs/settings/lfs/pointers") + resp := session.MakeRequest(t, req, http.StatusOK) + + // follow the first link to /user2/lfs/settings/lfs/find?oid=.... + filesTable := NewHTMLParser(t, resp.Body).doc.Find("#lfs-files-table") + assert.Contains(t, filesTable.Text(), "Find commits") + lfsFind := filesTable.Find(`.primary.button[href^="/user2"]`) + assert.Greater(t, lfsFind.Length(), 0) + lfsFindPath, exists := lfsFind.First().Attr("href") + assert.True(t, exists) + + assert.Contains(t, lfsFindPath, "oid=") + req = NewRequest(t, "GET", lfsFindPath) + resp = session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body).doc + assert.Equal(t, 1, doc.Find(`.sha.label[href="/user2/lfs/commit/73cf03db6ece34e12bf91e8853dc58f678f2f82d"]`).Length(), "could not find link to commit") + }) } diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index 5f102f8d62..e50f5c1356 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -1,6 +1,5 @@ // Copyright 2022 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. +// SPDX-License-Identifier: MIT package integration diff --git a/tests/integration/migration-test/gitea-v1.6.4.mssql.sql.gz b/tests/integration/migration-test/gitea-v1.6.4.mssql.sql.gz deleted file mode 100644 index 1b676feda1..0000000000 Binary files a/tests/integration/migration-test/gitea-v1.6.4.mssql.sql.gz and /dev/null differ diff --git a/tests/integration/migration-test/gitea-v1.7.0.mssql.sql.gz b/tests/integration/migration-test/gitea-v1.7.0.mssql.sql.gz deleted file mode 100644 index bd869cfa58..0000000000 Binary files a/tests/integration/migration-test/gitea-v1.7.0.mssql.sql.gz and /dev/null differ diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go index d1aa3f3eb8..e0e5620cd2 100644 --- a/tests/integration/migration-test/migration_test.go +++ b/tests/integration/migration-test/migration_test.go @@ -14,7 +14,6 @@ import ( "path/filepath" "regexp" "sort" - "strings" "testing" "code.gitea.io/gitea/models/db" @@ -258,31 +257,6 @@ func restoreOldDB(t *testing.T, version string) bool { _, err = db.Exec(data) assert.NoError(t, err) db.Close() - - case setting.Database.Type.IsMSSQL(): - host, port := setting.ParseMSSQLHostPort(setting.Database.Host) - db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, "master", setting.Database.User, setting.Database.Passwd)) - assert.NoError(t, err) - defer db.Close() - - _, err = db.Exec("DROP DATABASE IF EXISTS [gitea]") - assert.NoError(t, err) - - statements := strings.Split(data, "\nGO\n") - for _, statement := range statements { - if len(statement) > 5 && statement[:5] == "USE [" { - dbname := statement[5 : len(statement)-1] - db.Close() - db, err = sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, dbname, setting.Database.User, setting.Database.Passwd)) - assert.NoError(t, err) - defer db.Close() - } - _, err = db.Exec(statement) - assert.NoError(t, err, "Failure whilst running: %s\nError: %v", statement, err) - } - db.Close() } return true } diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index 1da1c6f9c0..46beddb5f3 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -6,9 +6,12 @@ package integration import ( "bytes" "context" + "crypto/sha256" + "encoding/base64" "fmt" "io" "net/http" + "net/url" "testing" auth_model "code.gitea.io/gitea/models/auth" @@ -470,6 +473,57 @@ func TestSignInOAuthCallbackSignIn(t *testing.T) { assert.Greater(t, userAfterLogin.LastLoginUnix, userGitLab.LastLoginUnix) } +func TestSignInOAuthCallbackPKCE(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // Setup authentication source + gitlabName := "gitlab" + gitlab := addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName)) + // Create a user as if it had been previously been created by the authentication source. + userGitLabUserID := "5678" + userGitLab := &user_model.User{ + Name: "gitlabuser", + Email: "gitlabuser@example.com", + Passwd: "gitlabuserpassword", + Type: user_model.UserTypeIndividual, + LoginType: auth_model.OAuth2, + LoginSource: gitlab.ID, + LoginName: userGitLabUserID, + } + defer createUser(context.Background(), t, userGitLab)() + + // initial redirection (to generate the code_challenge) + session := emptyTestSession(t) + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s", gitlabName)) + resp := session.MakeRequest(t, req, http.StatusTemporaryRedirect) + dest, err := url.Parse(resp.Header().Get("Location")) + assert.NoError(t, err) + assert.Equal(t, "S256", dest.Query().Get("code_challenge_method")) + codeChallenge := dest.Query().Get("code_challenge") + assert.NotEmpty(t, codeChallenge) + + // callback (to check the initial code_challenge) + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + codeVerifier := req.URL.Query().Get("code_verifier") + assert.NotEmpty(t, codeVerifier) + assert.Greater(t, len(codeVerifier), 40, codeVerifier) + + sha2 := sha256.New() + io.WriteString(sha2, codeVerifier) + assert.Equal(t, codeChallenge, base64.RawURLEncoding.EncodeToString(sha2.Sum(nil))) + + return goth.User{ + Provider: gitlabName, + UserID: userGitLabUserID, + Email: userGitLab.Email, + }, nil + })() + req = NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName)) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", test.RedirectURL(resp)) + unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userGitLab.ID}) +} + func TestSignInOAuthCallbackRedirectToEscaping(t *testing.T) { defer tests.PrepareTestEnv(t)() diff --git a/tests/integration/org_test.go b/tests/integration/org_test.go index 94c4e19727..a1e448be8a 100644 --- a/tests/integration/org_test.go +++ b/tests/integration/org_test.go @@ -222,3 +222,28 @@ func TestTeamSearch(t *testing.T) { req.Header.Add("X-Csrf-Token", csrf) session.MakeRequest(t, req, http.StatusNotFound) } + +func TestOrgDashboardLabels(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization}) + session := loginUser(t, user.Name) + + req := NewRequestf(t, "GET", "/org/%s/issues?labels=3,4", org.Name) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + labelFilterHref, ok := htmlDoc.Find(".list-header-sort a").Attr("href") + assert.True(t, ok) + assert.Contains(t, labelFilterHref, "labels=3%2c4") + + // Exclude label + req = NewRequestf(t, "GET", "/org/%s/issues?labels=3,-4", org.Name) + resp = session.MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + + labelFilterHref, ok = htmlDoc.Find(".list-header-sort a").Attr("href") + assert.True(t, ok) + assert.Contains(t, labelFilterHref, "labels=3%2c-4") +} diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index e613538d05..3bd4f57410 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -83,6 +83,7 @@ func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum str return resp } +// returns the hook tasks, order by ID desc. func retrieveHookTasks(t *testing.T, hookID int64, activateWebhook bool) []*webhook.HookTask { t.Helper() if activateWebhook { diff --git a/tests/integration/pull_request_task_test.go b/tests/integration/pull_request_task_test.go new file mode 100644 index 0000000000..4366d97c39 --- /dev/null +++ b/tests/integration/pull_request_task_test.go @@ -0,0 +1,109 @@ +// Copyright 2024 The Forgejo Authors +// SPDX-License-Identifier: MIT + +package integration + +import ( + "context" + "testing" + "time" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/timeutil" + pull_service "code.gitea.io/gitea/services/pull" + repo_service "code.gitea.io/gitea/services/repository" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPullRequestSynchronized(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // unmerged pull request of user2/repo1 from branch2 to master + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) + // tip of tests/gitea-repositories-meta/user2/repo1 branch2 + pull.HeadCommitID = "985f0301dba5e7b34be866819cd15ad3d8f508ee" + pull.LoadIssue(db.DefaultContext) + pull.Issue.Created = timeutil.TimeStampNanoNow() + issues_model.UpdateIssueCols(db.DefaultContext, pull.Issue, "created") + + require.Equal(t, pull.HeadRepoID, pull.BaseRepoID) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pull.HeadRepoID}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + for _, testCase := range []struct { + name string + timeNano int64 + expected bool + }{ + { + name: "AddTestPullRequestTask process PR", + timeNano: int64(pull.Issue.Created), + expected: true, + }, + { + name: "AddTestPullRequestTask skip PR", + timeNano: 0, + expected: false, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + logChecker, cleanup := test.NewLogChecker(log.DEFAULT, log.TRACE) + logChecker.Filter("Updating PR").StopMark("TestPullRequest ") + defer cleanup() + + opt := &repo_module.PushUpdateOptions{ + PusherID: owner.ID, + PusherName: owner.Name, + RepoUserName: owner.Name, + RepoName: repo.Name, + RefFullName: git.RefName("refs/heads/branch2"), + OldCommitID: pull.HeadCommitID, + NewCommitID: pull.HeadCommitID, + TimeNano: testCase.timeNano, + } + require.NoError(t, repo_service.PushUpdate(opt)) + logFiltered, logStopped := logChecker.Check(5 * time.Second) + assert.True(t, logStopped) + assert.Equal(t, testCase.expected, logFiltered[0]) + }) + } + + for _, testCase := range []struct { + name string + olderThan int64 + expected bool + }{ + { + name: "TestPullRequest process PR", + olderThan: int64(pull.Issue.Created), + expected: true, + }, + { + name: "TestPullRequest skip PR", + olderThan: int64(pull.Issue.Created) - 1, + expected: false, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + logChecker, cleanup := test.NewLogChecker(log.DEFAULT, log.TRACE) + logChecker.Filter("Updating PR").StopMark("TestPullRequest ") + defer cleanup() + + pull_service.TestPullRequest(context.Background(), owner, repo.ID, testCase.olderThan, "branch2", true, pull.HeadCommitID, pull.HeadCommitID) + logFiltered, logStopped := logChecker.Check(5 * time.Second) + assert.True(t, logStopped) + assert.Equal(t, testCase.expected, logFiltered[0]) + }) + } +} diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go index 63ec3f9f35..bcdb352612 100644 --- a/tests/integration/pull_review_test.go +++ b/tests/integration/pull_review_test.go @@ -6,13 +6,16 @@ package integration import ( "context" "net/http" + "net/http/httptest" "net/url" + "path" "strconv" "strings" "testing" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -399,3 +402,82 @@ func TestPullView_CodeOwner(t *testing.T) { }) }) } + +func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + user1Session := loginUser(t, "user1") + user2Session := loginUser(t, "user2") + + // Have user1 create a fork of repo1. + testRepoFork(t, user1Session, "user2", "repo1", "user1", "repo1") + + t.Run("Submit approve/reject review on merged PR", func(t *testing.T) { + // Create a merged PR (made by user1) in the upstream repo1. + testEditFile(t, user1Session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n") + resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "master", "This is a pull title") + elem := strings.Split(test.RedirectURL(resp), "/") + assert.EqualValues(t, "pulls", elem[3]) + testPullMerge(t, user1Session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false) + + // Grab the CSRF token. + req := NewRequest(t, "GET", path.Join(elem[1], elem[2], "pulls", elem[4])) + resp = user2Session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Submit an approve review on the PR. + testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity) + + // Submit a reject review on the PR. + testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity) + }) + + t.Run("Submit approve/reject review on closed PR", func(t *testing.T) { + // Created a closed PR (made by user1) in the upstream repo1. + testEditFileToNewBranch(t, user1Session, "user1", "repo1", "master", "a-test-branch", "README.md", "Hello, World (Editied...again)\n") + resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "a-test-branch", "This is a pull title") + elem := strings.Split(test.RedirectURL(resp), "/") + assert.EqualValues(t, "pulls", elem[3]) + testIssueClose(t, user1Session, elem[1], elem[2], elem[4]) + + // Grab the CSRF token. + req := NewRequest(t, "GET", path.Join(elem[1], elem[2], "pulls", elem[4])) + resp = user2Session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Submit an approve review on the PR. + testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity) + + // Submit a reject review on the PR. + testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity) + }) + }) +} + +func testSubmitReview(t *testing.T, session *TestSession, csrf, owner, repo, pullNumber, reviewType string, expectedSubmitStatus int) *httptest.ResponseRecorder { + options := map[string]string{ + "_csrf": csrf, + "commit_id": "", + "content": "test", + "type": reviewType, + } + + submitURL := path.Join(owner, repo, "pulls", pullNumber, "files", "reviews", "submit") + req := NewRequestWithValues(t, "POST", submitURL, options) + return session.MakeRequest(t, req, expectedSubmitStatus) +} + +func testIssueClose(t *testing.T, session *TestSession, owner, repo, issueNumber string) *httptest.ResponseRecorder { + req := NewRequest(t, "GET", path.Join(owner, repo, "pulls", issueNumber)) + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + closeURL := path.Join(owner, repo, "issues", issueNumber, "comments") + + options := map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "status": "close", + } + + req = NewRequestWithValues(t, "POST", closeURL, options) + return session.MakeRequest(t, req, http.StatusOK) +} diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go index 26c99e6445..80eea34513 100644 --- a/tests/integration/pull_status_test.go +++ b/tests/integration/pull_status_test.go @@ -12,6 +12,9 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" @@ -68,7 +71,6 @@ func TestPullCreate_CommitStatus(t *testing.T) { // Update commit status, and check if icon is updated as well for _, status := range statusList { - // Call API to add status for commit t.Run("CreateStatus", doAPICreateCommitStatus(testCtx, commitID, api.CreateStatusOption{ State: status, @@ -90,6 +92,10 @@ func TestPullCreate_CommitStatus(t *testing.T) { assert.True(t, ok) assert.Contains(t, cls, statesIcons[status]) } + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"}) + css := unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusSummary{RepoID: repo1.ID, SHA: commitID}) + assert.EqualValues(t, api.CommitStatusWarning, css.State) }) } diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go index acc8cb68c2..ad3c7bf325 100644 --- a/tests/integration/release_test.go +++ b/tests/integration/release_test.go @@ -9,15 +9,19 @@ import ( "testing" "time" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/tests" "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title string, preRelease, draft bool) { @@ -307,3 +311,34 @@ func TestDownloadReleaseAttachment(t *testing.T) { session := loginUser(t, "user2") session.MakeRequest(t, req, http.StatusOK) } + +func TestReleaseHideArchiveLinksUI(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{TagName: "v2.0"}) + + require.NoError(t, release.LoadAttributes(db.DefaultContext)) + + session := loginUser(t, release.Repo.OwnerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + zipURL := fmt.Sprintf("%s/archive/%s.zip", release.Repo.Link(), release.TagName) + tarGzURL := fmt.Sprintf("%s/archive/%s.tar.gz", release.Repo.Link(), release.TagName) + + resp := session.MakeRequest(t, NewRequest(t, "GET", release.HTMLURL()), http.StatusOK) + body := resp.Body.String() + assert.Contains(t, body, zipURL) + assert.Contains(t, body, tarGzURL) + + hideArchiveLinks := true + + req := NewRequestWithJSON(t, "PATCH", release.APIURL(), &api.EditReleaseOption{ + HideArchiveLinks: &hideArchiveLinks, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + + resp = session.MakeRequest(t, NewRequest(t, "GET", release.HTMLURL()), http.StatusOK) + body = resp.Body.String() + assert.NotContains(t, body, zipURL) + assert.NotContains(t, body, tarGzURL) +} diff --git a/tests/integration/remote_test.go b/tests/integration/remote_test.go new file mode 100644 index 0000000000..d905f88a81 --- /dev/null +++ b/tests/integration/remote_test.go @@ -0,0 +1,205 @@ +// Copyright Earl Warren +// SPDX-License-Identifier: MIT + +package integration + +import ( + "context" + "fmt" + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/test" + remote_service "code.gitea.io/gitea/services/remote" + "code.gitea.io/gitea/tests" + + "github.com/markbates/goth" + "github.com/stretchr/testify/assert" +) + +func TestRemote_MaybePromoteUserSuccess(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // + // OAuth2 authentication source GitLab + // + gitlabName := "gitlab" + _ = addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName)) + // + // Remote authentication source matching the GitLab authentication source + // + remoteName := "remote" + remote := createRemoteAuthSource(t, remoteName, "http://mygitlab.eu", gitlabName) + + // + // Create a user as if it had previously been created by the remote + // authentication source. + // + gitlabUserID := "5678" + gitlabEmail := "gitlabuser@example.com" + userBeforeSignIn := &user_model.User{ + Name: "gitlabuser", + Type: user_model.UserTypeRemoteUser, + LoginType: auth_model.Remote, + LoginSource: remote.ID, + LoginName: gitlabUserID, + } + defer createUser(context.Background(), t, userBeforeSignIn)() + + // + // A request for user information sent to Goth will return a + // goth.User exactly matching the user created above. + // + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{ + Provider: gitlabName, + UserID: gitlabUserID, + Email: gitlabEmail, + }, nil + })() + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName)) + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", test.RedirectURL(resp)) + userAfterSignIn := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userBeforeSignIn.ID}) + + // both are about the same user + assert.Equal(t, userAfterSignIn.ID, userBeforeSignIn.ID) + // the login time was updated, proof the login succeeded + assert.Greater(t, userAfterSignIn.LastLoginUnix, userBeforeSignIn.LastLoginUnix) + // the login type was promoted from Remote to OAuth2 + assert.Equal(t, userBeforeSignIn.LoginType, auth_model.Remote) + assert.Equal(t, userAfterSignIn.LoginType, auth_model.OAuth2) + // the OAuth2 email was used to set the missing user email + assert.Equal(t, userBeforeSignIn.Email, "") + assert.Equal(t, userAfterSignIn.Email, gitlabEmail) +} + +func TestRemote_MaybePromoteUserFail(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + ctx := context.Background() + // + // OAuth2 authentication source GitLab + // + gitlabName := "gitlab" + gitlabSource := addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName)) + // + // Remote authentication source matching the GitLab authentication source + // + remoteName := "remote" + remoteSource := createRemoteAuthSource(t, remoteName, "http://mygitlab.eu", gitlabName) + + { + promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, &auth_model.Source{}, "", "") + assert.NoError(t, err) + assert.False(t, promoted) + assert.Equal(t, remote_service.ReasonNotAuth2, reason) + } + + { + remoteSource.Type = auth_model.OAuth2 + promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, remoteSource, "", "") + assert.NoError(t, err) + assert.False(t, promoted) + assert.Equal(t, remote_service.ReasonBadAuth2, reason) + remoteSource.Type = auth_model.Remote + } + + { + promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, gitlabSource, "unknownloginname", "") + assert.NoError(t, err) + assert.False(t, promoted) + assert.Equal(t, remote_service.ReasonLoginNameNotExists, reason) + } + + { + remoteUserID := "844" + remoteUser := &user_model.User{ + Name: "withmailuser", + Type: user_model.UserTypeRemoteUser, + LoginType: auth_model.Remote, + LoginSource: remoteSource.ID, + LoginName: remoteUserID, + Email: "some@example.com", + } + defer createUser(context.Background(), t, remoteUser)() + promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, gitlabSource, remoteUserID, "") + assert.NoError(t, err) + assert.False(t, promoted) + assert.Equal(t, remote_service.ReasonEmailIsSet, reason) + } + + { + remoteUserID := "7464" + nonexistentloginsource := int64(4344) + remoteUser := &user_model.User{ + Name: "badsourceuser", + Type: user_model.UserTypeRemoteUser, + LoginType: auth_model.Remote, + LoginSource: nonexistentloginsource, + LoginName: remoteUserID, + } + defer createUser(context.Background(), t, remoteUser)() + promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, gitlabSource, remoteUserID, "") + assert.NoError(t, err) + assert.False(t, promoted) + assert.Equal(t, remote_service.ReasonNoSource, reason) + } + + { + remoteUserID := "33335678" + remoteUser := &user_model.User{ + Name: "badremoteuser", + Type: user_model.UserTypeRemoteUser, + LoginType: auth_model.Remote, + LoginSource: gitlabSource.ID, + LoginName: remoteUserID, + } + defer createUser(context.Background(), t, remoteUser)() + promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, gitlabSource, remoteUserID, "") + assert.NoError(t, err) + assert.False(t, promoted) + assert.Equal(t, remote_service.ReasonSourceWrongType, reason) + } + + { + unrelatedName := "unrelated" + unrelatedSource := addAuthSource(t, authSourcePayloadGitHubCustom(unrelatedName)) + assert.NotNil(t, unrelatedSource) + + remoteUserID := "488484" + remoteEmail := "4848484@example.com" + remoteUser := &user_model.User{ + Name: "unrelateduser", + Type: user_model.UserTypeRemoteUser, + LoginType: auth_model.Remote, + LoginSource: remoteSource.ID, + LoginName: remoteUserID, + } + defer createUser(context.Background(), t, remoteUser)() + promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, unrelatedSource, remoteUserID, remoteEmail) + assert.NoError(t, err) + assert.False(t, promoted) + assert.Equal(t, remote_service.ReasonNoMatch, reason) + } + + { + remoteUserID := "5678" + remoteEmail := "gitlabuser@example.com" + remoteUser := &user_model.User{ + Name: "remoteuser", + Type: user_model.UserTypeRemoteUser, + LoginType: auth_model.Remote, + LoginSource: remoteSource.ID, + LoginName: remoteUserID, + } + defer createUser(context.Background(), t, remoteUser)() + promoted, reason, err := remote_service.MaybePromoteRemoteUser(ctx, gitlabSource, remoteUserID, remoteEmail) + assert.NoError(t, err) + assert.True(t, promoted) + assert.Equal(t, remote_service.ReasonPromoted, reason) + } +} diff --git a/tests/integration/rename_branch_test.go b/tests/integration/rename_branch_test.go index b037178f90..27be77f71a 100644 --- a/tests/integration/rename_branch_test.go +++ b/tests/integration/rename_branch_test.go @@ -5,6 +5,7 @@ package integration import ( "net/http" + "net/url" "testing" git_model "code.gitea.io/gitea/models/git" @@ -17,6 +18,10 @@ import ( ) func TestRenameBranch(t *testing.T) { + onGiteaRun(t, testRenameBranch) +} + +func testRenameBranch(t *testing.T, u *url.URL) { defer tests.PrepareTestEnv(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) @@ -49,6 +54,96 @@ func TestRenameBranch(t *testing.T) { assert.Equal(t, "main", repo1.DefaultBranch) }) + t.Run("Database syncronization", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{ + "_csrf": GetCSRF(t, session, "/user2/repo1/settings/branches"), + "from": "master", + "to": "main", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // check new branch link + req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/main/README.md", nil) + session.MakeRequest(t, req, http.StatusOK) + + // check old branch link + req = NewRequestWithValues(t, "GET", "/user2/repo1/src/branch/master/README.md", nil) + resp := session.MakeRequest(t, req, http.StatusSeeOther) + location := resp.Header().Get("Location") + assert.Equal(t, "/user2/repo1/src/branch/main/README.md", location) + + // check db + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + assert.Equal(t, "main", repo1.DefaultBranch) + + // create branch1 + csrf := GetCSRF(t, session, "/user2/repo1/src/branch/main") + + req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/main", map[string]string{ + "_csrf": csrf, + "new_branch_name": "branch1", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + branch1 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"}) + assert.Equal(t, "branch1", branch1.Name) + + // create branch2 + req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/main", map[string]string{ + "_csrf": csrf, + "new_branch_name": "branch2", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + branch2 := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"}) + assert.Equal(t, "branch2", branch2.Name) + + // rename branch2 to branch1 + req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{ + "_csrf": GetCSRF(t, session, "/user2/repo1/settings/branches"), + "from": "branch2", + "to": "branch1", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + flashCookie := session.GetCookie(gitea_context.CookieNameFlash) + assert.NotNil(t, flashCookie) + assert.Contains(t, flashCookie.Value, "error") + + branch2 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"}) + assert.Equal(t, "branch2", branch2.Name) + branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"}) + assert.Equal(t, "branch1", branch1.Name) + + // delete branch1 + req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete", map[string]string{ + "_csrf": GetCSRF(t, session, "/user2/repo1/settings/branches"), + "name": "branch1", + }) + session.MakeRequest(t, req, http.StatusOK) + branch2 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"}) + assert.Equal(t, "branch2", branch2.Name) + branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"}) + assert.True(t, branch1.IsDeleted) // virtual deletion + + // rename branch2 to branch1 again + req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/rename_branch", map[string]string{ + "_csrf": GetCSRF(t, session, "/user2/repo1/settings/branches"), + "from": "branch2", + "to": "branch1", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + flashCookie = session.GetCookie(gitea_context.CookieNameFlash) + assert.NotNil(t, flashCookie) + assert.Contains(t, flashCookie.Value, "success") + + unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"}) + branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"}) + assert.Equal(t, "branch1", branch1.Name) + }) + t.Run("Protected branch", func(t *testing.T) { defer tests.PrintCurrentTest(t)() diff --git a/tests/integration/repo_activity_test.go b/tests/integration/repo_activity_test.go index 792554db4b..0b1e9939a1 100644 --- a/tests/integration/repo_activity_test.go +++ b/tests/integration/repo_activity_test.go @@ -4,13 +4,20 @@ package integration import ( + "fmt" "net/http" "net/url" "strings" "testing" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/test" + repo_service "code.gitea.io/gitea/services/repository" + "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" ) @@ -63,3 +70,122 @@ func TestRepoActivity(t *testing.T) { assert.Len(t, list.Nodes, 3) }) } + +func TestRepoActivityAllUnitsDisabled(t *testing.T) { + defer tests.PrepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"}) + session := loginUser(t, user.Name) + + unit_model.LoadUnitConfig() + + // Create a repo, with no unit enabled. + repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{ + Name: "empty-repo", + AutoInit: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + enabledUnits := make([]repo_model.RepoUnit, 0) + disabledUnits := []unit_model.Type{unit_model.TypeCode, unit_model.TypeIssues, unit_model.TypePullRequests, unit_model.TypeReleases} + err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, enabledUnits, disabledUnits) + assert.NoError(t, err) + + req := NewRequest(t, "GET", fmt.Sprintf("%s/activity", repo.Link())) + session.MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/contributors", repo.Link())) + session.MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/code-frequency", repo.Link())) + session.MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/recent-commits", repo.Link())) + session.MakeRequest(t, req, http.StatusNotFound) +} + +func TestRepoActivityOnlyCodeUnitWithEmptyRepo(t *testing.T) { + defer tests.PrepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"}) + session := loginUser(t, user.Name) + + unit_model.LoadUnitConfig() + + // Create a empty repo, with only code unit enabled. + repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{ + Name: "empty-repo", + AutoInit: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + enabledUnits := make([]repo_model.RepoUnit, 1) + enabledUnits[0] = repo_model.RepoUnit{RepoID: repo.ID, Type: unit_model.TypeCode} + disabledUnits := []unit_model.Type{unit_model.TypeIssues, unit_model.TypePullRequests, unit_model.TypeReleases} + err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, enabledUnits, disabledUnits) + assert.NoError(t, err) + + req := NewRequest(t, "GET", fmt.Sprintf("%s/activity", repo.Link())) + session.MakeRequest(t, req, http.StatusOK) + + // Git repo empty so no activity for contributors etc + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/contributors", repo.Link())) + session.MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/code-frequency", repo.Link())) + session.MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/recent-commits", repo.Link())) + session.MakeRequest(t, req, http.StatusNotFound) +} + +func TestRepoActivityOnlyCodeUnitWithNonEmptyRepo(t *testing.T) { + defer tests.PrepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"}) + session := loginUser(t, user.Name) + + unit_model.LoadUnitConfig() + + // Create a repo, with only code unit enabled. + repo, _, f := CreateDeclarativeRepo(t, user, "", []unit_model.Type{unit_model.TypeCode}, nil, nil) + defer f() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/activity", repo.Link())) + session.MakeRequest(t, req, http.StatusOK) + + // Git repo not empty so activity for contributors etc + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/contributors", repo.Link())) + session.MakeRequest(t, req, http.StatusOK) + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/code-frequency", repo.Link())) + session.MakeRequest(t, req, http.StatusOK) + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/recent-commits", repo.Link())) + session.MakeRequest(t, req, http.StatusOK) +} + +func TestRepoActivityOnlyIssuesUnit(t *testing.T) { + defer tests.PrepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"}) + session := loginUser(t, user.Name) + + unit_model.LoadUnitConfig() + + // Create a empty repo, with only code unit enabled. + repo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{ + Name: "empty-repo", + AutoInit: false, + }) + assert.NoError(t, err) + assert.NotEmpty(t, repo) + + enabledUnits := make([]repo_model.RepoUnit, 1) + enabledUnits[0] = repo_model.RepoUnit{RepoID: repo.ID, Type: unit_model.TypeIssues} + disabledUnits := []unit_model.Type{unit_model.TypeCode, unit_model.TypePullRequests, unit_model.TypeReleases} + err = repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, enabledUnits, disabledUnits) + assert.NoError(t, err) + + req := NewRequest(t, "GET", fmt.Sprintf("%s/activity", repo.Link())) + session.MakeRequest(t, req, http.StatusOK) + + // Git repo empty so no activity for contributors etc + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/contributors", repo.Link())) + session.MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/code-frequency", repo.Link())) + session.MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "GET", fmt.Sprintf("%s/activity/recent-commits", repo.Link())) + session.MakeRequest(t, req, http.StatusNotFound) +} diff --git a/tests/integration/repo_mergecommit_revert_test.go b/tests/integration/repo_mergecommit_revert_test.go index 7041861f11..eb75d45c15 100644 --- a/tests/integration/repo_mergecommit_revert_test.go +++ b/tests/integration/repo_mergecommit_revert_test.go @@ -1,3 +1,6 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package integration import ( diff --git a/tests/integration/repo_search_test.go b/tests/integration/repo_search_test.go index 56cc45d901..e058851071 100644 --- a/tests/integration/repo_search_test.go +++ b/tests/integration/repo_search_test.go @@ -11,6 +11,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/PuerkitoBio/goquery" @@ -18,7 +19,7 @@ import ( ) func resultFilenames(t testing.TB, doc *HTMLDoc) []string { - filenameSelections := doc.doc.Find(".repository.search").Find(".repo-search-result").Find(".header").Find("span.file") + filenameSelections := doc.Find(".repository.search").Find(".repo-search-result").Find(".header").Find("span.file") result := make([]string, filenameSelections.Length()) filenameSelections.Each(func(i int, selection *goquery.Selection) { result[i] = selection.Text() @@ -26,36 +27,67 @@ func resultFilenames(t testing.TB, doc *HTMLDoc) []string { return result } -func TestSearchRepo(t *testing.T) { +func TestSearchRepoIndexer(t *testing.T) { + testSearchRepo(t, true) +} + +func TestSearchRepoNoIndexer(t *testing.T) { + testSearchRepo(t, false) +} + +func testSearchRepo(t *testing.T, indexer bool) { defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Indexer.RepoIndexerEnabled, indexer)() repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1") assert.NoError(t, err) - code_indexer.UpdateRepoIndexer(repo) + if indexer { + code_indexer.UpdateRepoIndexer(repo) + } - testSearch(t, "/user2/repo1/search?q=Description&page=1", []string{"README.md"}) + testSearch(t, "/user2/repo1/search?q=Description&page=1", []string{"README.md"}, indexer) - setting.Indexer.IncludePatterns = setting.IndexerGlobFromString("**.txt") - setting.Indexer.ExcludePatterns = setting.IndexerGlobFromString("**/y/**") + defer test.MockVariableValue(&setting.Indexer.IncludePatterns, setting.IndexerGlobFromString("**.txt"))() + defer test.MockVariableValue(&setting.Indexer.ExcludePatterns, setting.IndexerGlobFromString("**/y/**"))() repo, err = repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "glob") assert.NoError(t, err) - code_indexer.UpdateRepoIndexer(repo) + if indexer { + code_indexer.UpdateRepoIndexer(repo) + } - testSearch(t, "/user2/glob/search?q=loren&page=1", []string{"a.txt"}) - testSearch(t, "/user2/glob/search?q=loren&page=1&t=match", []string{"a.txt"}) - testSearch(t, "/user2/glob/search?q=file3&page=1", []string{"x/b.txt", "a.txt"}) - testSearch(t, "/user2/glob/search?q=file3&page=1&t=match", []string{"x/b.txt", "a.txt"}) - testSearch(t, "/user2/glob/search?q=file4&page=1&t=match", []string{"x/b.txt", "a.txt"}) - testSearch(t, "/user2/glob/search?q=file5&page=1&t=match", []string{"x/b.txt", "a.txt"}) + testSearch(t, "/user2/glob/search?q=loren&page=1", []string{"a.txt"}, indexer) + testSearch(t, "/user2/glob/search?q=loren&page=1&fuzzy=false", []string{"a.txt"}, indexer) + + if indexer { + // fuzzy search: matches both file3 (x/b.txt) and file1 (a.txt) + // when indexer is enabled + testSearch(t, "/user2/glob/search?q=file3&page=1", []string{"x/b.txt", "a.txt"}, indexer) + testSearch(t, "/user2/glob/search?q=file4&page=1", []string{"x/b.txt", "a.txt"}, indexer) + testSearch(t, "/user2/glob/search?q=file5&page=1", []string{"x/b.txt", "a.txt"}, indexer) + } else { + // fuzzy search: OR of all the keywords + // when indexer is disabled + testSearch(t, "/user2/glob/search?q=file3+file1&page=1", []string{"a.txt", "x/b.txt"}, indexer) + testSearch(t, "/user2/glob/search?q=file4&page=1", []string{}, indexer) + testSearch(t, "/user2/glob/search?q=file5&page=1", []string{}, indexer) + } + + testSearch(t, "/user2/glob/search?q=file3&page=1&fuzzy=false", []string{"x/b.txt"}, indexer) + testSearch(t, "/user2/glob/search?q=file4&page=1&fuzzy=false", []string{}, indexer) + testSearch(t, "/user2/glob/search?q=file5&page=1&fuzzy=false", []string{}, indexer) } -func testSearch(t *testing.T, url string, expected []string) { +func testSearch(t *testing.T, url string, expected []string, indexer bool) { req := NewRequest(t, "GET", url) resp := MakeRequest(t, req, http.StatusOK) - filenames := resultFilenames(t, NewHTMLParser(t, resp.Body)) + doc := NewHTMLParser(t, resp.Body) + msg := doc.Find(".repository").Find(".ui.container").Find(".ui.message[data-test-tag=grep]") + assert.EqualValues(t, indexer, len(msg.Nodes) == 0) + + filenames := resultFilenames(t, doc) assert.EqualValues(t, expected, filenames) } diff --git a/tests/integration/repo_settings_test.go b/tests/integration/repo_settings_test.go index 771aa9ad8e..de86cba77a 100644 --- a/tests/integration/repo_settings_test.go +++ b/tests/integration/repo_settings_test.go @@ -14,9 +14,11 @@ import ( unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" gitea_context "code.gitea.io/gitea/services/context" repo_service "code.gitea.io/gitea/services/repository" + user_service "code.gitea.io/gitea/services/user" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -32,6 +34,97 @@ func TestRepoSettingsUnits(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } +func TestRepoAddMoreUnitsHighlighting(t *testing.T) { + defer tests.PrepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) + session := loginUser(t, user.Name) + + // Make sure there are no disabled repos in the settings! + setting.Repository.DisabledRepoUnits = []string{} + unit_model.LoadUnitConfig() + + // Create a known-good repo, with some units disabled. + repo, _, f := CreateDeclarativeRepo(t, user, "", []unit_model.Type{ + unit_model.TypeCode, + unit_model.TypePullRequests, + unit_model.TypeProjects, + unit_model.TypeActions, + unit_model.TypeIssues, + unit_model.TypeWiki, + }, []unit_model.Type{unit_model.TypePackages}, nil) + defer f() + + setUserHints := func(t *testing.T, hints bool) func() { + saved := user.EnableRepoUnitHints + + assert.NoError(t, user_service.UpdateUser(db.DefaultContext, user, &user_service.UpdateOptions{ + EnableRepoUnitHints: optional.Some(hints), + })) + + return func() { + assert.NoError(t, user_service.UpdateUser(db.DefaultContext, user, &user_service.UpdateOptions{ + EnableRepoUnitHints: optional.Some(saved), + })) + } + } + + assertHighlight := func(t *testing.T, page, uri string, highlighted bool) { + t.Helper() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/settings%s", repo.Link(), page)) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + htmlDoc.AssertElement(t, fmt.Sprintf(".overflow-menu-items a[href='%s'].active", fmt.Sprintf("%s/settings%s", repo.Link(), uri)), highlighted) + } + + t.Run("hints enabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer setUserHints(t, true)() + + t.Run("settings", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Visiting the /settings page, "Settings" is highlighted + assertHighlight(t, "", "", true) + // ...but "Add more" isn't. + assertHighlight(t, "", "/units", false) + }) + + t.Run("units", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Visiting the /settings/units page, "Add more" is highlighted + assertHighlight(t, "/units", "/units", true) + // ...but "Settings" isn't. + assertHighlight(t, "/units", "", false) + }) + }) + + t.Run("hints disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer setUserHints(t, false)() + + t.Run("settings", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Visiting the /settings page, "Settings" is highlighted + assertHighlight(t, "", "", true) + // ...but "Add more" isn't (it doesn't exist). + assertHighlight(t, "", "/units", false) + }) + + t.Run("units", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Visiting the /settings/units page, "Settings" is highlighted + assertHighlight(t, "/units", "", true) + // ...but "Add more" isn't (it doesn't exist) + assertHighlight(t, "/units", "/units", false) + }) + }) +} + func TestRepoAddMoreUnits(t *testing.T) { defer tests.PrepareTestEnv(t)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 15da511758..b98fce0f4a 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -7,7 +7,6 @@ import ( "net/http" "net/http/httptest" "net/url" - "strings" "testing" gitea_context "code.gitea.io/gitea/services/context" @@ -37,30 +36,58 @@ func TestNewWebHookLink(t *testing.T) { for _, url := range tests { resp := session.MakeRequest(t, NewRequest(t, "GET", url), http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - menus := htmlDoc.doc.Find(".ui.top.attached.header .ui.dropdown .menu a") - menus.Each(func(i int, menu *goquery.Selection) { - url, exist := menu.Attr("href") - assert.True(t, exist) - assert.True(t, strings.HasPrefix(url, baseurl)) - }) - assert.Equal(t, webhooksLen, htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(), "not all webhooks are listed in the 'new' dropdown") + assert.Equal(t, + webhooksLen, + htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(), + "not all webhooks are listed in the 'new' dropdown") + csrfToken = htmlDoc.GetCSRF() } // ensure that the "failure" pages has the full dropdown as well resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", baseurl+"/gitea/new", map[string]string{"_csrf": csrfToken}), http.StatusUnprocessableEntity) htmlDoc := NewHTMLParser(t, resp.Body) - assert.Equal(t, webhooksLen, htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(), "not all webhooks are listed in the 'new' dropdown on failure") + assert.Equal(t, + webhooksLen, + htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(), + "not all webhooks are listed in the 'new' dropdown on failure") resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", baseurl+"/1", map[string]string{"_csrf": csrfToken}), http.StatusUnprocessableEntity) htmlDoc = NewHTMLParser(t, resp.Body) - assert.Equal(t, webhooksLen, htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(), "not all webhooks are listed in the 'new' dropdown on failure") + assert.Equal(t, + webhooksLen, + htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(), + "not all webhooks are listed in the 'new' dropdown on failure") + + adminSession := loginUser(t, "user1") + t.Run("org3", func(t *testing.T) { + baseurl := "/org/org3/settings/hooks" + resp := adminSession.MakeRequest(t, NewRequest(t, "GET", baseurl), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Equal(t, + webhooksLen, + htmlDoc.Find(`a[href^="`+baseurl+`/"][href$="/new"]`).Length(), + "not all webhooks are listed in the 'new' dropdown") + }) + t.Run("admin", func(t *testing.T) { + baseurl := "/admin/hooks" + resp := adminSession.MakeRequest(t, NewRequest(t, "GET", baseurl), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Equal(t, + webhooksLen, + htmlDoc.Find(`a[href^="/admin/default-hooks/"][href$="/new"]`).Length(), + "not all webhooks are listed in the 'new' dropdown for default-hooks") + assert.Equal(t, + webhooksLen, + htmlDoc.Find(`a[href^="/admin/system-hooks/"][href$="/new"]`).Length(), + "not all webhooks are listed in the 'new' dropdown for system-hooks") + }) } func TestWebhookForms(t *testing.T) { defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user2") + session := loginUser(t, "user1") t.Run("forgejo/required", testWebhookForms("forgejo", session, map[string]string{ "payload_url": "https://forgejo.example.com", @@ -200,19 +227,19 @@ func TestWebhookForms(t *testing.T) { })) t.Run("matrix/required", testWebhookForms("matrix", session, map[string]string{ - "homeserver_url": "https://matrix.example.com", - "room_id": "123", - "authorization_header": "Bearer 123456", + "homeserver_url": "https://matrix.example.com", + "access_token": "123456", + "room_id": "123", }, map[string]string{ - "authorization_header": "", + "access_token": "", })) t.Run("matrix/optional", testWebhookForms("matrix", session, map[string]string{ "homeserver_url": "https://matrix.example.com", + "access_token": "123456", "room_id": "123", "message_type": "1", // m.text - "branch_filter": "matrix/*", - "authorization_header": "Bearer 123456", + "branch_filter": "matrix/*", })) t.Run("wechatwork/required", testWebhookForms("wechatwork", session, map[string]string{ @@ -238,113 +265,176 @@ func TestWebhookForms(t *testing.T) { "branch_filter": "packagist/*", "authorization_header": "Bearer 123456", })) + + t.Run("sourcehut_builds/required", testWebhookForms("sourcehut_builds", session, map[string]string{ + "payload_url": "https://sourcehut_builds.example.com", + "manifest_path": ".build.yml", + "visibility": "PRIVATE", + "access_token": "123456", + }, map[string]string{ + "access_token": "", + }, map[string]string{ + "manifest_path": "", + }, map[string]string{ + "manifest_path": "/absolute", + }, map[string]string{ + "visibility": "", + }, map[string]string{ + "visibility": "INVALID", + })) + t.Run("sourcehut_builds/optional", testWebhookForms("sourcehut_builds", session, map[string]string{ + "payload_url": "https://sourcehut_builds.example.com", + "manifest_path": ".build.yml", + "visibility": "PRIVATE", + "secrets": "on", + "access_token": "123456", + + "branch_filter": "srht/*", + })) } func assertInput(t testing.TB, form *goquery.Selection, name string) string { t.Helper() input := form.Find(`input[name="` + name + `"]`) if input.Length() != 1 { - t.Log(form.Html()) + form.Find("input").Each(func(i int, s *goquery.Selection) { + t.Logf("found ", s.AttrOr("name", "")) + }) t.Errorf("field found %d times, expected once", name, input.Length()) } - return input.AttrOr("value", "") + switch input.AttrOr("type", "") { + case "checkbox": + if _, checked := input.Attr("checked"); checked { + return "on" + } + return "" + default: + return input.AttrOr("value", "") + } } func testWebhookForms(name string, session *TestSession, validFields map[string]string, invalidPatches ...map[string]string) func(t *testing.T) { return func(t *testing.T) { - // new webhook form - resp := session.MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/settings/hooks/"+name+"/new"), http.StatusOK) - htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="/user2/repo1/settings/hooks/"]`) + t.Run("repo1", func(t *testing.T) { + testWebhookFormsShared(t, "/user2/repo1/settings/hooks", name, session, validFields, invalidPatches...) + }) + t.Run("org3", func(t *testing.T) { + testWebhookFormsShared(t, "/org/org3/settings/hooks", name, session, validFields, invalidPatches...) + }) + t.Run("system", func(t *testing.T) { + testWebhookFormsShared(t, "/admin/system-hooks", name, session, validFields, invalidPatches...) + }) + t.Run("default", func(t *testing.T) { + testWebhookFormsShared(t, "/admin/default-hooks", name, session, validFields, invalidPatches...) + }) + } +} - // fill the form - payload := map[string]string{ - "_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""), - "events": "send_everything", - } - for k, v := range validFields { - assertInput(t, htmlForm, k) - payload[k] = v - } - if t.Failed() { - t.FailNow() // prevent further execution if the form could not be filled properly - } +func testWebhookFormsShared(t *testing.T, endpoint, name string, session *TestSession, validFields map[string]string, invalidPatches ...map[string]string) { + // new webhook form + resp := session.MakeRequest(t, NewRequest(t, "GET", endpoint+"/"+name+"/new"), http.StatusOK) + htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`) - // create the webhook (this redirects back to the hook list) - resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user2/repo1/settings/hooks/"+name+"/new", payload), http.StatusSeeOther) - assertHasFlashMessages(t, resp, "success") + // fill the form + payload := map[string]string{ + "_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""), + "events": "send_everything", + } + for k, v := range validFields { + assertInput(t, htmlForm, k) + payload[k] = v + } + if t.Failed() { + t.FailNow() // prevent further execution if the form could not be filled properly + } - // find last created hook in the hook list - // (a bit hacky, but the list should be sorted) - resp = session.MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/settings/hooks"), http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - editFormURL := htmlDoc.Find(`a[href^="/user2/repo1/settings/hooks/"]`).Last().AttrOr("href", "") - assert.NotEmpty(t, editFormURL) + // create the webhook (this redirects back to the hook list) + resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", endpoint+"/"+name+"/new", payload), http.StatusSeeOther) + assertHasFlashMessages(t, resp, "success") + listEndpoint := resp.Header().Get("Location") + updateEndpoint := endpoint + "/" + if endpoint == "/admin/system-hooks" || endpoint == "/admin/default-hooks" { + updateEndpoint = "/admin/hooks/" + } - // edit webhook form - resp = session.MakeRequest(t, NewRequest(t, "GET", editFormURL), http.StatusOK) - htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="/user2/repo1/settings/hooks/"]`) - editPostURL := htmlForm.AttrOr("action", "") - assert.NotEmpty(t, editPostURL) + // find last created hook in the hook list + // (a bit hacky, but the list should be sorted) + resp = session.MakeRequest(t, NewRequest(t, "GET", listEndpoint), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + selector := `a[href^="` + updateEndpoint + `"]` + if endpoint == "/admin/system-hooks" { + // system-hooks and default-hooks are listed on the same page + // add a specifier to select the latest system-hooks + // (the default-hooks are at the end, so no further specifier needed) + selector = `.admin-setting-content > div:first-of-type ` + selector + } + editFormURL := htmlDoc.Find(selector).Last().AttrOr("href", "") + assert.NotEmpty(t, editFormURL) - // fill the form - payload = map[string]string{ - "_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""), - "events": "push_only", - } - for k, v := range validFields { - assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v) - payload[k] = v - } + // edit webhook form + resp = session.MakeRequest(t, NewRequest(t, "GET", editFormURL), http.StatusOK) + htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="` + updateEndpoint + `"]`) + editPostURL := htmlForm.AttrOr("action", "") + assert.NotEmpty(t, editPostURL) - // update the webhook - resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", editPostURL, payload), http.StatusSeeOther) - assertHasFlashMessages(t, resp, "success") + // fill the form + payload = map[string]string{ + "_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""), + "events": "push_only", + } + for k, v := range validFields { + assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v) + payload[k] = v + } - // check the updated webhook - resp = session.MakeRequest(t, NewRequest(t, "GET", editFormURL), http.StatusOK) - htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="/user2/repo1/settings/hooks/"]`) - for k, v := range validFields { - assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v) - } + // update the webhook + resp = session.MakeRequest(t, NewRequestWithValues(t, "POST", editPostURL, payload), http.StatusSeeOther) + assertHasFlashMessages(t, resp, "success") - if len(invalidPatches) > 0 { - // check that invalid fields are rejected - resp := session.MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/settings/hooks/"+name+"/new"), http.StatusOK) - htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="/user2/repo1/settings/hooks/"]`) + // check the updated webhook + resp = session.MakeRequest(t, NewRequest(t, "GET", editFormURL), http.StatusOK) + htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="` + updateEndpoint + `"]`) + for k, v := range validFields { + assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v) + } - for _, invalidPatch := range invalidPatches { - t.Run("invalid", func(t *testing.T) { - // fill the form - payload := map[string]string{ - "_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""), - "events": "send_everything", - } - for k, v := range validFields { + if len(invalidPatches) > 0 { + // check that invalid fields are rejected + resp := session.MakeRequest(t, NewRequest(t, "GET", endpoint+"/"+name+"/new"), http.StatusOK) + htmlForm := NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`) + + for _, invalidPatch := range invalidPatches { + t.Run("invalid", func(t *testing.T) { + // fill the form + payload := map[string]string{ + "_csrf": htmlForm.Find(`input[name="_csrf"]`).AttrOr("value", ""), + "events": "send_everything", + } + for k, v := range validFields { + payload[k] = v + } + for k, v := range invalidPatch { + if v == "" { + delete(payload, k) + } else { payload[k] = v } - for k, v := range invalidPatch { - if v == "" { - delete(payload, k) - } else { - payload[k] = v - } - } + } - resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user2/repo1/settings/hooks/"+name+"/new", payload), http.StatusUnprocessableEntity) - // check that the invalid form is pre-filled - htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="/user2/repo1/settings/hooks/"]`) - for k, v := range payload { - if k == "_csrf" || k == "events" || v == "" { - // the 'events' is a radio input, which is buggy below - continue - } - assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v) + resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", endpoint+"/"+name+"/new", payload), http.StatusUnprocessableEntity) + // check that the invalid form is pre-filled + htmlForm = NewHTMLParser(t, resp.Body).Find(`form[action^="` + endpoint + `/"]`) + for k, v := range payload { + if k == "_csrf" || k == "events" || v == "" { + // the 'events' is a radio input, which is buggy below + continue } - if t.Failed() { - t.Log(invalidPatch) - } - }) - } + assert.Equal(t, v, assertInput(t, htmlForm, k), "input %q did not contain value %q", k, v) + } + if t.Failed() { + t.Log(invalidPatch) + } + }) } } } diff --git a/tests/integration/size_translations_test.go b/tests/integration/size_translations_test.go index 78cd16795d..1ee5f7b36f 100644 --- a/tests/integration/size_translations_test.go +++ b/tests/integration/size_translations_test.go @@ -89,7 +89,7 @@ func TestDataSizeTranslation(t *testing.T) { fullSize = noDigits.ReplaceAllString(fullSize, "") assert.Equal(t, "git: КиБ; lfs: Б", fullSize) - // Check if file sizes are correclty translated + // Check if file sizes are correctly translated testFileSizeTranslated(t, session, path.Join(testUser, testRepoName, "src/branch/main/137byteFile.txt"), "137 Б") testFileSizeTranslated(t, session, path.Join(testUser, testRepoName, "src/branch/main/1.5kibFile.txt"), "1,5 КиБ") testFileSizeTranslated(t, session, path.Join(testUser, testRepoName, "src/branch/main/1.25mibFile.txt"), "1,3 МиБ") diff --git a/tests/integration/user_count_test.go b/tests/integration/user_count_test.go new file mode 100644 index 0000000000..c0837d57fd --- /dev/null +++ b/tests/integration/user_count_test.go @@ -0,0 +1,167 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + project_model "code.gitea.io/gitea/models/project" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/tests" + + "github.com/PuerkitoBio/goquery" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type userCountTest struct { + doer *user_model.User + user *user_model.User + session *TestSession + repoCount int64 + projectCount int64 + memberCount int64 + teamCount int64 +} + +func (countTest *userCountTest) Init(t *testing.T, doerID, userID int64) { + countTest.doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doerID}) + countTest.user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) + countTest.session = loginUser(t, countTest.doer.Name) + + var err error + + countTest.repoCount, err = repo_model.CountRepository(db.DefaultContext, &repo_model.SearchRepoOptions{ + Actor: countTest.doer, + OwnerID: countTest.user.ID, + Private: true, + Collaborate: optional.Some(false), + }) + require.NoError(t, err) + + var projectType project_model.Type + if countTest.user.IsOrganization() { + projectType = project_model.TypeOrganization + } else { + projectType = project_model.TypeIndividual + } + countTest.projectCount, err = db.Count[project_model.Project](db.DefaultContext, project_model.SearchOptions{ + OwnerID: countTest.user.ID, + IsClosed: optional.Some(false), + Type: projectType, + }) + require.NoError(t, err) + + if !countTest.user.IsOrganization() { + return + } + + org := (*organization.Organization)(countTest.user) + + isMember, err := org.IsOrgMember(db.DefaultContext, countTest.doer.ID) + require.NoError(t, err) + + countTest.memberCount, err = organization.CountOrgMembers(db.DefaultContext, &organization.FindOrgMembersOpts{ + OrgID: org.ID, + PublicOnly: !isMember, + }) + require.NoError(t, err) + + teams, err := org.LoadTeams(db.DefaultContext) + require.NoError(t, err) + + countTest.teamCount = int64(len(teams)) +} + +func (countTest *userCountTest) getCount(doc *goquery.Document, name string) (int64, error) { + selection := doc.Find(fmt.Sprintf("[test-name=\"%s\"]", name)) + + if selection.Length() != 1 { + return 0, fmt.Errorf("%s was not found", name) + } + + return strconv.ParseInt(selection.Text(), 10, 64) +} + +func (countTest *userCountTest) TestPage(t *testing.T, page string, orgLink bool) { + t.Run(page, func(t *testing.T) { + var userLink string + + if orgLink { + userLink = countTest.user.OrganisationLink() + } else { + userLink = countTest.user.HomeLink() + } + + req := NewRequestf(t, "GET", "%s/%s", userLink, page) + resp := countTest.session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + repoCount, err := countTest.getCount(htmlDoc.doc, "repository-count") + require.NoError(t, err) + assert.Equal(t, countTest.repoCount, repoCount) + + projectCount, err := countTest.getCount(htmlDoc.doc, "project-count") + require.NoError(t, err) + assert.Equal(t, countTest.projectCount, projectCount) + + if !countTest.user.IsOrganization() { + return + } + + memberCount, err := countTest.getCount(htmlDoc.doc, "member-count") + require.NoError(t, err) + assert.Equal(t, countTest.memberCount, memberCount) + + teamCount, err := countTest.getCount(htmlDoc.doc, "team-count") + require.NoError(t, err) + assert.Equal(t, countTest.teamCount, teamCount) + }) +} + +func TestFrontendHeaderCountUser(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + countTest := new(userCountTest) + countTest.Init(t, 2, 2) + + countTest.TestPage(t, "", false) + countTest.TestPage(t, "?tab=repositories", false) + countTest.TestPage(t, "-/projects", false) + countTest.TestPage(t, "-/packages", false) + countTest.TestPage(t, "?tab=activity", false) + countTest.TestPage(t, "?tab=stars", false) +} + +func TestFrontendHeaderCountOrg(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + countTest := new(userCountTest) + countTest.Init(t, 15, 17) + + countTest.TestPage(t, "", false) + countTest.TestPage(t, "-/projects", false) + countTest.TestPage(t, "-/packages", false) + countTest.TestPage(t, "members", true) + countTest.TestPage(t, "teams", true) + + countTest.TestPage(t, "settings", true) + countTest.TestPage(t, "settings/hooks", true) + countTest.TestPage(t, "settings/labels", true) + countTest.TestPage(t, "settings/applications", true) + countTest.TestPage(t, "settings/packages", true) + countTest.TestPage(t, "settings/actions/runners", true) + countTest.TestPage(t, "settings/actions/secrets", true) + countTest.TestPage(t, "settings/actions/variables", true) + countTest.TestPage(t, "settings/blocked_users", true) + countTest.TestPage(t, "settings/delete", true) +} diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index b4b99a1917..02cc9b51cc 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -1,4 +1,5 @@ // Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package integration @@ -6,6 +7,7 @@ package integration import ( "fmt" "net/http" + "strings" "testing" auth_model "code.gitea.io/gitea/models/auth" @@ -428,3 +430,180 @@ func TestUserHints(t *testing.T) { }) }) } + +func TestUserPronouns(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser) + + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + adminSession := loginUser(t, adminUser.Name) + adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeWriteAdmin) + + t.Run("API", func(t *testing.T) { + t.Run("user", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + // We check the raw JSON, because we want to test the response, not + // what it decodes into. Contents doesn't matter, we're testing the + // presence only. + assert.Contains(t, resp.Body.String(), `"pronouns":`) + }) + + t.Run("users/{username}", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/users/user2") + resp := MakeRequest(t, req, http.StatusOK) + + // We check the raw JSON, because we want to test the response, not + // what it decodes into. Contents doesn't matter, we're testing the + // presence only. + assert.Contains(t, resp.Body.String(), `"pronouns":`) + }) + + t.Run("user/settings", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Set pronouns first + pronouns := "they/them" + req := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", &api.UserSettingsOptions{ + Pronouns: &pronouns, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + // Verify the response + var user *api.UserSettings + DecodeJSON(t, resp, &user) + assert.Equal(t, pronouns, user.Pronouns) + + // Verify retrieving the settings again + req = NewRequest(t, "GET", "/api/v1/user/settings").AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + + DecodeJSON(t, resp, &user) + assert.Equal(t, pronouns, user.Pronouns) + }) + + t.Run("admin/users/{username}", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Set the pronouns for user2 + pronouns := "she/her" + req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/users/user2", &api.EditUserOption{ + Pronouns: &pronouns, + }).AddTokenAuth(adminToken) + resp := MakeRequest(t, req, http.StatusOK) + + // Verify the API response + var user *api.User + DecodeJSON(t, resp, &user) + assert.Equal(t, pronouns, user.Pronouns) + + // Verify via user2 too + req = NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &user) + assert.Equal(t, pronouns, user.Pronouns) + }) + }) + + t.Run("UI", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Set the pronouns to a known state via the API + pronouns := "she/her" + req := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", &api.UserSettingsOptions{ + Pronouns: &pronouns, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusOK) + + t.Run("profile view", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user2") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + userNameAndPronouns := strings.TrimSpace(htmlDoc.Find(".profile-avatar-name .username").Text()) + assert.Contains(t, userNameAndPronouns, pronouns) + }) + + t.Run("settings", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user/settings") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Check that the field is present + pronounField, has := htmlDoc.Find(`input[name="pronouns"]`).Attr("value") + assert.True(t, has) + assert.Equal(t, pronouns, pronounField) + + // Check that updating the field works + newPronouns := "they/them" + req = NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ + "_csrf": GetCSRF(t, session, "/user/settings"), + "pronouns": newPronouns, + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) + assert.Equal(t, newPronouns, user2.Pronouns) + }) + + t.Run("admin settings", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) + + req := NewRequestf(t, "GET", "/admin/users/%d/edit", user2.ID) + resp := adminSession.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Check that the pronouns field is present + pronounField, has := htmlDoc.Find(`input[name="pronouns"]`).Attr("value") + assert.True(t, has) + assert.NotEmpty(t, pronounField) + + // Check that updating the field works + newPronouns := "it/its" + editURI := fmt.Sprintf("/admin/users/%d/edit", user2.ID) + req = NewRequestWithValues(t, "POST", editURI, map[string]string{ + "_csrf": GetCSRF(t, adminSession, editURI), + "login_type": "0-0", + "login_name": user2.LoginName, + "email": user2.Email, + "pronouns": newPronouns, + }) + adminSession.MakeRequest(t, req, http.StatusSeeOther) + + user2New := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) + assert.Equal(t, newPronouns, user2New.Pronouns) + }) + }) + + t.Run("unspecified", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Set the pronouns to Unspecified (an empty string) via the API + pronouns := "" + req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/users/user2", &api.EditUserOption{ + Pronouns: &pronouns, + }).AddTokenAuth(adminToken) + MakeRequest(t, req, http.StatusOK) + + // Verify that the profile page does not display any pronouns, nor the separator + req = NewRequest(t, "GET", "/user2") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + userName := strings.TrimSpace(htmlDoc.Find(".profile-avatar-name .username").Text()) + assert.EqualValues(t, userName, "user2") + }) +} diff --git a/tests/integration/view_test.go b/tests/integration/view_test.go index cd63304a91..e77cc382e9 100644 --- a/tests/integration/view_test.go +++ b/tests/integration/view_test.go @@ -4,6 +4,7 @@ package integration import ( + "fmt" "net/http" "net/url" "strings" @@ -129,3 +130,58 @@ func TestAmbiguousCharacterDetection(t *testing.T) { }) }) } + +func TestInHistoryButton(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.Name) + repo, commitID, f := CreateDeclarativeRepo(t, user2, "", + []unit_model.Type{unit_model.TypeCode, unit_model.TypeWiki}, nil, + []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: "test.sh", + ContentReader: strings.NewReader("Hello there!"), + }, + }, + ) + defer f() + + req := NewRequestWithValues(t, "POST", repo.Link()+"/wiki?action=new", map[string]string{ + "_csrf": GetCSRF(t, session, repo.Link()+"/wiki?action=new"), + "title": "Normal", + "content": "Hello world!", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + t.Run("Wiki revision", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", repo.Link()+"/wiki/Normal?action=_revision") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + htmlDoc.AssertElement(t, fmt.Sprintf(".commit-list a[href^='/%s/src/commit/']", repo.FullName()), false) + }) + + t.Run("Commit list", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", repo.Link()+"/commits/branch/main") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + htmlDoc.AssertElement(t, fmt.Sprintf(".commit-list a[href='/%s/src/commit/%s']", repo.FullName(), commitID), true) + }) + + t.Run("File history", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", repo.Link()+"/commits/branch/main/test.sh") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + htmlDoc.AssertElement(t, fmt.Sprintf(".commit-list a[href='/%s/src/commit/%s/test.sh']", repo.FullName(), commitID), true) + }) + }) +} diff --git a/tests/integration/webfinger_test.go b/tests/integration/webfinger_test.go index 55fb211779..825cffed7a 100644 --- a/tests/integration/webfinger_test.go +++ b/tests/integration/webfinger_test.go @@ -66,4 +66,19 @@ func TestWebfinger(t *testing.T) { req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=mailto:%s", user.Email)) MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=https://%s/%s/", appURL.Host, user.Name)) + session.MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=https://%s/%s", appURL.Host, user.Name)) + session.MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=http://%s/%s/foo", appURL.Host, user.Name)) + session.MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=http://%s", appURL.Host)) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=http://%s/%s/foo", "example.com", user.Name)) + MakeRequest(t, req, http.StatusBadRequest) } diff --git a/tests/integration/webhook_test.go b/tests/integration/webhook_test.go new file mode 100644 index 0000000000..ec85d12b07 --- /dev/null +++ b/tests/integration/webhook_test.go @@ -0,0 +1,188 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "net/url" + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/json" + webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/release" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestWebhookPayloadRef(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + w := unittest.AssertExistsAndLoadBean(t, &webhook_model.Webhook{ID: 1}) + w.HookEvent = &webhook_module.HookEvent{ + SendEverything: true, + } + assert.NoError(t, w.UpdateEvent()) + assert.NoError(t, webhook_model.UpdateWebhook(db.DefaultContext, w)) + + hookTasks := retrieveHookTasks(t, w.ID, true) + hookTasksLenBefore := len(hookTasks) + + session := loginUser(t, "user2") + // create new branch + csrf := GetCSRF(t, session, "user2/repo1") + req := NewRequestWithValues(t, "POST", "user2/repo1/branches/_new/branch/master", + map[string]string{ + "_csrf": csrf, + "new_branch_name": "arbre", + "create_tag": "false", + }, + ) + session.MakeRequest(t, req, http.StatusSeeOther) + // delete the created branch + req = NewRequestWithValues(t, "POST", "user2/repo1/branches/delete?name=arbre", + map[string]string{ + "_csrf": csrf, + }, + ) + session.MakeRequest(t, req, http.StatusOK) + + // check the newly created hooktasks + hookTasks = retrieveHookTasks(t, w.ID, false) + expected := map[webhook_module.HookEventType]bool{ + webhook_module.HookEventCreate: true, + webhook_module.HookEventPush: true, // the branch creation also creates a push event + webhook_module.HookEventDelete: true, + } + for _, hookTask := range hookTasks[:len(hookTasks)-hookTasksLenBefore] { + if !expected[hookTask.EventType] { + t.Errorf("unexpected (or duplicated) event %q", hookTask.EventType) + } + + var payload struct { + Ref string `json:"ref"` + } + assert.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payload)) + assert.Equal(t, "refs/heads/arbre", payload.Ref, "unexpected ref for %q event", hookTask.EventType) + delete(expected, hookTask.EventType) + } + assert.Empty(t, expected) + }) +} + +func TestWebhookReleaseEvents(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + w := unittest.AssertExistsAndLoadBean(t, &webhook_model.Webhook{ + ID: 1, + RepoID: repo.ID, + }) + w.HookEvent = &webhook_module.HookEvent{ + SendEverything: true, + } + assert.NoError(t, w.UpdateEvent()) + assert.NoError(t, webhook_model.UpdateWebhook(db.DefaultContext, w)) + + hookTasks := retrieveHookTasks(t, w.ID, true) + + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo) + assert.NoError(t, err) + defer gitRepo.Close() + + t.Run("CreateRelease", func(t *testing.T) { + assert.NoError(t, release.CreateRelease(gitRepo, &repo_model.Release{ + RepoID: repo.ID, + Repo: repo, + PublisherID: user.ID, + Publisher: user, + TagName: "v1.1.1", + Target: "master", + Title: "v1.1.1 is released", + Note: "v1.1.1 is released", + IsDraft: false, + IsPrerelease: false, + IsTag: false, + }, nil, "")) + + // check the newly created hooktasks + hookTasksLenBefore := len(hookTasks) + hookTasks = retrieveHookTasks(t, w.ID, false) + + checkHookTasks(t, map[webhook_module.HookEventType]string{ + webhook_module.HookEventRelease: "published", + webhook_module.HookEventCreate: "", // a tag was created as well + webhook_module.HookEventPush: "", // the tag creation also means a push event + }, hookTasks[:len(hookTasks)-hookTasksLenBefore]) + + t.Run("UpdateRelease", func(t *testing.T) { + rel := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{RepoID: repo.ID, TagName: "v1.1.1"}) + assert.NoError(t, release.UpdateRelease(db.DefaultContext, user, gitRepo, rel, nil, nil, nil, false)) + + // check the newly created hooktasks + hookTasksLenBefore := len(hookTasks) + hookTasks = retrieveHookTasks(t, w.ID, false) + + checkHookTasks(t, map[webhook_module.HookEventType]string{ + webhook_module.HookEventRelease: "updated", + }, hookTasks[:len(hookTasks)-hookTasksLenBefore]) + }) + }) + + t.Run("CreateNewTag", func(t *testing.T) { + assert.NoError(t, release.CreateNewTag(db.DefaultContext, + user, + repo, + "master", + "v1.1.2", + "v1.1.2 is tagged", + )) + + // check the newly created hooktasks + hookTasksLenBefore := len(hookTasks) + hookTasks = retrieveHookTasks(t, w.ID, false) + + checkHookTasks(t, map[webhook_module.HookEventType]string{ + webhook_module.HookEventCreate: "", // tag was created as well + webhook_module.HookEventPush: "", // the tag creation also means a push event + }, hookTasks[:len(hookTasks)-hookTasksLenBefore]) + + t.Run("UpdateRelease", func(t *testing.T) { + rel := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{RepoID: repo.ID, TagName: "v1.1.2"}) + assert.NoError(t, release.UpdateRelease(db.DefaultContext, user, gitRepo, rel, nil, nil, nil, true)) + + // check the newly created hooktasks + hookTasksLenBefore := len(hookTasks) + hookTasks = retrieveHookTasks(t, w.ID, false) + + checkHookTasks(t, map[webhook_module.HookEventType]string{ + webhook_module.HookEventRelease: "published", + }, hookTasks[:len(hookTasks)-hookTasksLenBefore]) + }) + }) +} + +func checkHookTasks(t *testing.T, expectedActions map[webhook_module.HookEventType]string, hookTasks []*webhook_model.HookTask) { + t.Helper() + for _, hookTask := range hookTasks { + expectedAction, ok := expectedActions[hookTask.EventType] + if !ok { + t.Errorf("unexpected (or duplicated) event %q", hookTask.EventType) + } + var payload struct { + Action string `json:"action"` + } + assert.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payload)) + assert.Equal(t, expectedAction, payload.Action, "unexpected action for %q event", hookTask.EventType) + delete(expectedActions, hookTask.EventType) + } + assert.Empty(t, expectedActions) +} diff --git a/tests/mssql.ini.tmpl b/tests/mssql.ini.tmpl deleted file mode 100644 index 9346f75874..0000000000 --- a/tests/mssql.ini.tmpl +++ /dev/null @@ -1,107 +0,0 @@ -APP_NAME = Gitea: Git with a cup of tea -RUN_MODE = prod - -[database] -DB_TYPE = mssql -HOST = {{TEST_MSSQL_HOST}} -NAME = {{TEST_MSSQL_DBNAME}} -USER = {{TEST_MSSQL_USERNAME}} -PASSWD = {{TEST_MSSQL_PASSWORD}} -SSL_MODE = disable - -[indexer] -REPO_INDEXER_ENABLED = true -REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/indexers/repos.bleve - -[queue.issue_indexer] -TYPE = level -DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/indexers/issues.queue - -[queue] -TYPE = immediate - -[repository] -ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/gitea-repositories - -[repository.local] -LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/tmp/local-repo - -[repository.upload] -TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/tmp/uploads - -[repository.signing] -SIGNING_KEY = none - -[server] -SSH_DOMAIN = localhost -HTTP_PORT = 3003 -ROOT_URL = http://localhost:3003/ -DISABLE_SSH = false -SSH_LISTEN_HOST = localhost -SSH_PORT = 2201 -START_SSH_SERVER = true -LFS_START_SERVER = true -OFFLINE_MODE = false -LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w -APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data -BUILTIN_SSH_SERVER_USER = git -SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE= - -[attachment] -PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/attachments - -[mailer] -ENABLED = true -PROTOCOL = dummy -FROM = mssql-{{TEST_TYPE}}-test@gitea.io - -[service] -REGISTER_EMAIL_CONFIRM = false -REGISTER_MANUAL_CONFIRM = false -DISABLE_REGISTRATION = false -ENABLE_CAPTCHA = false -REQUIRE_SIGNIN_VIEW = false -DEFAULT_KEEP_EMAIL_PRIVATE = false -DEFAULT_ALLOW_CREATE_ORGANIZATION = true -NO_REPLY_ADDRESS = noreply.example.org -ENABLE_NOTIFY_MAIL = true - -[picture] -DISABLE_GRAVATAR = false -ENABLE_FEDERATED_AVATAR = false -AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/avatars -REPOSITORY_AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/repo-avatars - -[session] -PROVIDER = file -PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/sessions - -[log] -MODE = {{TEST_LOGGER}} -ROOT_PATH = {{REPO_TEST_DIR}}mssql-log -ENABLE_SSH_LOG = true -logger.xorm.MODE = file - -[log.test] -LEVEL = Info -COLORIZE = true - -[log.file] -LEVEL = Debug - -[security] -PASSWORD_HASH_ALGO = argon2 -DISABLE_GIT_HOOKS = false -INSTALL_LOCK = true -SECRET_KEY = 9pCviYTWSb -INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ -DISABLE_QUERY_AUTH_TOKEN = true - -[lfs] -PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/lfs - -[packages] -ENABLED = true - -[actions] -ENABLED = true diff --git a/tests/test_utils.go b/tests/test_utils.go index 85d7462d73..a607194bef 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -11,7 +11,9 @@ import ( "os" "path" "path/filepath" + "runtime" "testing" + "time" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" @@ -20,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/process" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" @@ -162,18 +165,6 @@ func InitTest(requireGitea bool) { log.Fatal("db.Exec: CREATE SCHEMA: %v", err) } } - - case setting.Database.Type.IsMSSQL(): - host, port := setting.ParseMSSQLHostPort(setting.Database.Host) - db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, "master", setting.Database.User, setting.Database.Passwd)) - if err != nil { - log.Fatal("sql.Open: %v", err) - } - if _, err := db.Exec(fmt.Sprintf("If(db_id(N'%s') IS NULL) BEGIN CREATE DATABASE %s; END;", setting.Database.Name, setting.Database.Name)); err != nil { - log.Fatal("db.Exec: %v", err) - } - defer db.Close() } routers.InitWebInstalled(graceful.GetManager().HammerContext()) @@ -193,6 +184,36 @@ func PrepareAttachmentsStorage(t testing.TB) { })) } +// cancelProcesses cancels all processes of the [process.Manager]. +// Returns immediately if delay is 0, otherwise wait until all processes are done +// and fails the test if it takes longer that the given delay. +func cancelProcesses(t testing.TB, delay time.Duration) { + processManager := process.GetManager() + processes, _ := processManager.Processes(true, true) + for _, p := range processes { + processManager.Cancel(p.PID) + t.Logf("PrepareTestEnv:Process %q cancelled", p.Description) + } + if delay == 0 || len(processes) == 0 { + return + } + + start := time.Now() + processes, _ = processManager.Processes(true, true) + for len(processes) > 0 { + if time.Since(start) > delay { + t.Errorf("ERROR PrepareTestEnv: could not cancel all processes within %s", delay) + for _, p := range processes { + t.Logf("PrepareTestEnv:Remaining Process: %q", p.Description) + } + return + } + runtime.Gosched() // let the context cancellation propagate + processes, _ = processManager.Processes(true, true) + } + t.Logf("PrepareTestEnv: all processes cancelled within %s", time.Since(start)) +} + func PrepareTestEnv(t testing.TB, skip ...int) func() { t.Helper() ourSkip := 1 @@ -201,6 +222,11 @@ func PrepareTestEnv(t testing.TB, skip ...int) func() { } deferFn := PrintCurrentTest(t, ourSkip) + // kill all background processes to prevent them from interfering with the fixture loading + // see https://codeberg.org/forgejo/forgejo/issues/2962 + cancelProcesses(t, 30*time.Second) + t.Cleanup(func() { cancelProcesses(t, 0) }) // cancel remaining processes in a non-blocking way + // load database fixtures assert.NoError(t, unittest.LoadFixtures()) diff --git a/web_src/css/actions.css b/web_src/css/actions.css index e7b9a3855a..0ab09f537a 100644 --- a/web_src/css/actions.css +++ b/web_src/css/actions.css @@ -44,9 +44,10 @@ } .run-list-item-right { - flex: 0 0 15%; + width: 130px; display: flex; flex-direction: column; + flex-shrink: 0; gap: 3px; color: var(--color-text-light); } @@ -57,3 +58,26 @@ gap: .25rem; align-items: center; } + +.run-list .flex-item-trailing { + flex-wrap: nowrap; + width: 280px; + flex: 0 0 280px; +} + +.run-list-ref { + display: inline-block !important; +} + +@media (max-width: 767.98px) { + .run-list .flex-item-trailing { + flex-direction: column; + align-items: flex-end; + width: auto; + flex-basis: auto; + } + .run-list-item-right, + .run-list-ref { + max-width: 110px; + } +} diff --git a/web_src/css/base.css b/web_src/css/base.css index 05ab0255e8..3d91586934 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -24,6 +24,21 @@ --repo-header-issue-min-height: 41px; --min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */ --tab-size: 4; + --checkbox-size: 15px; /* height and width of checkbox and radio inputs */ + --page-spacing: 16px; /* space between page elements */ + --page-margin-x: 32px; /* minimum space on left and right side of page */ +} + +@media (min-width: 768px) and (max-width: 1200px) { + :root { + --page-margin-x: 16px; + } +} + +@media (max-width: 767.98px) { + :root { + --page-margin-x: 8px; + } } :root * { @@ -44,7 +59,7 @@ html, body { } body { - line-height: 1.4285rem; + line-height: 20px; font-family: var(--fonts-regular); color: var(--color-text); background-color: var(--color-body); @@ -231,6 +246,18 @@ h1.error-code { user-select: none; } +.top-right-buttons { + gap: 0.5rem; +} + +.top-right-buttons .ui.button { + margin-right: 0; +} + +.ui.partial.secondary.menu { + margin-bottom: 0; +} + a { color: var(--color-primary); cursor: pointer; @@ -316,61 +343,6 @@ a.label, background-color: var(--color-label-bg); } -/* fix Fomantic's line-height cutting off "g" on Windows Chrome with Segoe UI */ -.ui.input > input { - line-height: var(--line-height-default); - text-align: start; /* Override fomantic's `text-align: left` to make RTL work via HTML `dir="auto"` */ -} - -/* fix Fomantic's line-height causing vertical scrollbars to appear */ -ul.ui.list li, -ol.ui.list li, -.ui.list > .item, -.ui.list .list > .item { - line-height: var(--line-height-default); -} - -.ui.input.focus > input, -.ui.input > input:focus { - border-color: var(--color-primary); -} - -.ui.action.input .ui.ui.button { - border-color: var(--color-input-border); - padding-top: 0; /* the ".action.input" is "flex + stretch", so let the buttons layout themselves */ - padding-bottom: 0; -} - -/* currently used for search bar dropdowns in repo search and explore code */ -.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection { - min-width: 10em; -} -.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus) { - border-right: none; -} -.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(.active):hover { - border-color: var(--color-input-border); -} -.ui.action.input:not([class*="left action"]) .ui.dropdown.selection.upward.visible { - border-bottom-left-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} -.ui.action.input:not([class*="left action"]) > input, -.ui.action.input:not([class*="left action"]) > input:hover { - border-right: none; -} -.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection, -.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover, -.ui.action.input:not([class*="left action"]) > input:focus + .button, -.ui.action.input:not([class*="left action"]) > input:focus + .button:hover, -.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button, -.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover { - border-left-color: var(--color-primary); -} -.ui.action.input:not([class*="left action"]) > input:focus { - border-right-color: var(--color-primary); -} - .ui.menu { display: flex; } @@ -481,6 +453,7 @@ ol.ui.list li, .ui.selection.dropdown .menu > .item { border-color: var(--color-secondary); + white-space: nowrap; } .ui.selection.visible.dropdown > .text:not(.default) { @@ -514,21 +487,6 @@ ol.ui.list li, color: var(--color-text-light-2); } -.ui.list .list > .item .header, -.ui.list > .item .header { - color: var(--color-text-dark); -} - -.ui.list .list > .item > .content, -.ui.list > .item > .content { - color: var(--color-text); -} - -.ui.list .list > .item .description, -.ui.list > .item .description { - color: var(--color-text); -} - /* replace item margin on secondary menu items with gap and remove both the negative margins on the menu as well as margin on the items */ .ui.secondary.menu { @@ -647,10 +605,6 @@ img.ui.avatar, aspect-ratio: 1; } -.ui.divided.list > .item { - border-color: var(--color-secondary); -} - .ui.error.message .header, .ui.warning.message .header { color: inherit; @@ -667,11 +621,14 @@ img.ui.avatar, margin-bottom: 14px; } -/* add padding to all content when there is no .secondary.nav. this uses padding instead of - margin because with the negative margin on .ui.grid we would have to set margin-top: 0, - but that does not work universally for all pages */ +/* add margin to all pages when there is no .secondary.nav */ .page-content > :first-child:not(.secondary-nav) { - padding-top: 14px; + margin-top: var(--page-spacing); +} +/* if .ui.grid is the first child the first grid-column has 'padding-top: 1rem' which we need + to compensate here */ +.page-content > :first-child.ui.grid { + margin-top: calc(var(--page-spacing) - 1rem); } .ui.pagination.menu .active.item { @@ -900,14 +857,6 @@ input:-webkit-autofill:active, text-align: right !important; } -.ui .text.normal { - font-weight: var(--font-weight-normal); -} - -.ui .text.italic { - font-style: italic; -} - .ui .text.truncate { overflow-x: hidden; text-overflow: ellipsis; @@ -915,14 +864,6 @@ input:-webkit-autofill:active, display: inline-block; } -.ui .text.thin { - font-weight: var(--font-weight-normal); -} - -.ui .text.middle { - vertical-align: middle; -} - .ui .message.flash-message { text-align: center; } @@ -1127,6 +1068,11 @@ input:-webkit-autofill:active, margin: auto 0.5em auto 0; } +.attention-title { + align-items: center; + display: flex; +} + blockquote.attention-note { border-left-color: var(--color-blue-dark-1); } @@ -1222,6 +1168,10 @@ overflow-menu .ui.label { color: var(--color-label-text); } +.ui.menu .active.item > .label { + background: var(--color-label-bg-alt, var(--color-label-bg)); +} + .lines-blame-btn { padding: 0 0 0 5px; display: flex; @@ -1320,6 +1270,7 @@ overflow-menu .ui.label { white-space: pre-wrap; word-break: break-all; overflow-wrap: anywhere; + line-height: inherit; /* needed for inline code preview in markup */ } .blame .code-inner { @@ -1448,11 +1399,6 @@ table th[data-sortt-desc] .svg { vertical-align: -0.15em; } -/* for the jquery.minicolors plugin */ -.minicolors-panel { - background: var(--color-secondary-dark-1) !important; -} - .ui.tabular.menu { border-color: var(--color-secondary); } @@ -1601,6 +1547,7 @@ table th[data-sortt-desc] .svg { align-items: center; gap: .25rem; vertical-align: middle; + min-width: 0; } .ui.ui.button { @@ -1616,19 +1563,10 @@ table th[data-sortt-desc] .svg { align-items: stretch; } -.ui.ui.icon.input .icon { - display: flex; - align-items: center; - justify-content: center; -} - -.ui.icon.input > i.icon { - transition: none; -} - .flex-items-block > .item, .flex-text-block { display: flex; align-items: center; gap: .25rem; + min-width: 0; } diff --git a/web_src/css/dashboard.css b/web_src/css/dashboard.css index 4bb9fa38bf..2ee2399d73 100644 --- a/web_src/css/dashboard.css +++ b/web_src/css/dashboard.css @@ -7,7 +7,6 @@ .dashboard.feeds .context.user.menu .ui.header, .dashboard.issues .context.user.menu .ui.header { font-size: 1rem; - text-transform: none; } .dashboard.feeds .filter.menu, diff --git a/web_src/css/features/colorpicker.css b/web_src/css/features/colorpicker.css new file mode 100644 index 0000000000..b7436783df --- /dev/null +++ b/web_src/css/features/colorpicker.css @@ -0,0 +1,47 @@ +.js-color-picker-input { + display: flex; + position: relative; +} + +.js-color-picker-input input { + padding-top: 8px !important; + padding-bottom: 8px !important; + padding-left: 32px !important; +} + +.js-color-picker-input .preview-square { + position: absolute; + aspect-ratio: 1; + height: 16px; + left: 10px; + top: 50%; + transform: translateY(-50%); + border-radius: 2px; + background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */ + background-position: 0 0, 4px 4px; + background-size: 8px 8px; +} + +.js-color-picker-input .preview-square::after { + content: ""; + position: absolute; + width: 100%; + height: 100%; + border-radius: inherit; + background-color: currentcolor; +} + +hex-color-picker { + width: 180px; + height: 120px; +} + +hex-color-picker::part(hue-pointer), +hex-color-picker::part(saturation-pointer) { + width: 22px; + height: 22px; +} + +hex-color-picker::part(hue) { + flex-basis: 16px; +} diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 30df994c38..e23c146748 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -22,34 +22,27 @@ cursor: default; } +.project-column .issue-card { + color: var(--color-text); +} + .project-column-header { display: flex; align-items: center; justify-content: space-between; } -.project-column-header.dark-label { - color: var(--color-project-board-dark-label) !important; -} - -.project-column-header.dark-label .project-column-title { - color: var(--color-project-board-dark-label) !important; -} - -.project-column-header.light-label { - color: var(--color-project-board-light-label) !important; -} - -.project-column-header.light-label .project-column-title { - color: var(--color-project-board-light-label) !important; -} - .project-column-title { background: none !important; line-height: 1.25 !important; cursor: inherit; } +.project-column-title, +.project-column-issue-count { + color: inherit !important; +} + .project-column > .cards { flex: 1; display: flex; @@ -64,6 +57,8 @@ .project-column > .divider { margin: 5px 0; + border-color: currentcolor; + opacity: .5; } .project-column:first-child { @@ -102,26 +97,3 @@ .card-ghost * { opacity: 0; } - -.color-field .minicolors.minicolors-theme-default { - display: block; -} - -.color-field .minicolors.minicolors-theme-default .minicolors-input { - height: 38px; - padding-left: 2rem; -} - -.color-field .minicolors.minicolors-theme-default .minicolors-swatch { - top: 10px; -} - -.edit-project-column-modal .color.picker.column, -.new-project-column-modal .color.picker.column { - display: flex; -} - -.edit-project-column-modal .color.picker.column .minicolors, -.new-project-column-modal .color.picker.column .minicolors { - flex: 1; -} diff --git a/web_src/css/form.css b/web_src/css/form.css index ca65b677d7..c757234e3b 100644 --- a/web_src/css/form.css +++ b/web_src/css/form.css @@ -32,10 +32,7 @@ textarea, .ui.form input[type="text"], .ui.form input[type="time"], .ui.form input[type="url"], -.ui.selection.dropdown, -.ui.checkbox label::before, -.ui.checkbox input:checked ~ label::before, -.ui.checkbox input:not([type="radio"]):indeterminate ~ label::before { +.ui.selection.dropdown { background: var(--color-input-background); border-color: var(--color-input-border); color: var(--color-input-text); @@ -63,12 +60,7 @@ textarea:hover, .ui.form input[type="text"]:hover, .ui.form input[type="time"]:hover, .ui.form input[type="url"]:hover, -.ui.selection.dropdown:hover, -.ui.checkbox label:hover::before, -.ui.checkbox label:active::before, -.ui.radio.checkbox label::after, -.ui.radio.checkbox input:focus ~ label::before, -.ui.radio.checkbox input:checked ~ label::before { +.ui.selection.dropdown:hover { background: var(--color-input-background); border-color: var(--color-input-border-hover); color: var(--color-input-text); @@ -91,11 +83,7 @@ textarea:focus, .ui.form input[type="text"]:focus, .ui.form input[type="time"]:focus, .ui.form input[type="url"]:focus, -.ui.selection.dropdown:focus, -.ui.checkbox input:focus ~ label::before, -.ui.checkbox input:not([type="radio"]):indeterminate:focus ~ label::before, -.ui.checkbox input:checked:focus ~ label::before, -.ui.radio.checkbox input:focus:checked ~ label::before { +.ui.selection.dropdown:focus { background: var(--color-input-background); border-color: var(--color-primary); color: var(--color-input-text); @@ -106,65 +94,28 @@ textarea:focus, .ui.form .inline.fields .field > label, .ui.form .inline.fields .field > p, .ui.form .inline.field > label, -.ui.form .inline.field > p, -.ui.checkbox label, -.ui.checkbox + label, -.ui.checkbox label:hover, -.ui.checkbox + label:hover, -.ui.checkbox input:focus ~ label, -.ui.checkbox input:active ~ label { +.ui.form .inline.field > p { color: var(--color-text); } .ui.form .required.fields:not(.grouped) > .field > label::after, .ui.form .required.fields.grouped > label::after, .ui.form .required.field > label::after, -.ui.form .required.fields:not(.grouped) > .field > .checkbox::after, -.ui.form .required.field > .checkbox::after, .ui.form label.required::after { color: var(--color-red); } -.ui.input, -.ui.checkbox input:focus ~ label::after, -.ui.checkbox input:checked ~ label::after, -.ui.checkbox label:active::after, -.ui.checkbox input:not([type="radio"]):indeterminate ~ label::after, -.ui.checkbox input:not([type="radio"]):indeterminate:focus ~ label::after, -.ui.checkbox input:checked:focus ~ label::after, -.ui.disabled.checkbox label, -.ui.checkbox input[disabled] ~ label { +.ui.input { color: var(--color-input-text); } -.ui.radio.checkbox input:focus ~ label::after, -.ui.radio.checkbox input:checked ~ label::after, -.ui.radio.checkbox input:focus:checked ~ label::after { - background: var(--color-input-text); -} - -.ui.toggle.checkbox label::before { - background: var(--color-input-toggle-background); -} - -.ui.toggle.checkbox label, -.ui.toggle.checkbox input:checked ~ label, -.ui.toggle.checkbox input:focus:checked ~ label { - color: var(--color-text) !important; -} - -.ui.toggle.checkbox input:checked ~ label::before, -.ui.toggle.checkbox input:focus:checked ~ label::before { - background: var(--color-primary) !important; -} - /* match */ .ui.form select { padding: 0.67857143em 1em; } .form .help { - color: var(--color-secondary-dark-5); + color: var(--color-secondary-dark-8); padding-bottom: 0.6em; display: inline-block; } @@ -298,21 +249,6 @@ textarea:focus, .user.signup form .optional .title { margin-left: 250px !important; } - .user.activate form .inline.field > input, - .user.forgot.password form .inline.field > input, - .user.reset.password form .inline.field > input, - .user.link-account form .inline.field > input, - .user.signin form .inline.field > input, - .user.signup form .inline.field > input, - .user.activate form .inline.field > textarea, - .user.forgot.password form .inline.field > textarea, - .user.reset.password form .inline.field > textarea, - .user.link-account form .inline.field > textarea, - .user.signin form .inline.field > textarea, - .user.signup form .inline.field > textarea, - .oauth-login-link { - width: 50%; - } } @media (max-width: 767.98px) { @@ -359,14 +295,7 @@ textarea:focus, .user.reset.password form .inline.field > label, .user.link-account form .inline.field > label, .user.signin form .inline.field > label, - .user.signup form .inline.field > label, - .user.activate form input, - .user.forgot.password form input, - .user.reset.password form input, - .user.link-account form input, - .user.signin form input, - .user.signup form input, - .oauth-login-link { + .user.signup form .inline.field > label { width: 100% !important; } } @@ -484,9 +413,9 @@ textarea:focus, .repository.new.repo form label, .repository.new.migrate form label, .repository.new.fork form label, - .repository.new.repo form input, - .repository.new.migrate form input, - .repository.new.fork form input, + .repository.new.repo form .inline.field > input, + .repository.new.migrate form .inline.field > input, + .repository.new.fork form .inline.field > input, .repository.new.fork form .field a, .repository.new.repo form .selection.dropdown, .repository.new.migrate form .selection.dropdown, diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css index 13962f19d7..118c058b19 100644 --- a/web_src/css/helpers.css +++ b/web_src/css/helpers.css @@ -63,3 +63,20 @@ only use: display: none !important; } } + +.tab-size-1 { tab-size: 1 !important; } +.tab-size-2 { tab-size: 2 !important; } +.tab-size-3 { tab-size: 3 !important; } +.tab-size-4 { tab-size: 4 !important; } +.tab-size-5 { tab-size: 5 !important; } +.tab-size-6 { tab-size: 6 !important; } +.tab-size-7 { tab-size: 7 !important; } +.tab-size-8 { tab-size: 8 !important; } +.tab-size-9 { tab-size: 9 !important; } +.tab-size-10 { tab-size: 10 !important; } +.tab-size-11 { tab-size: 11 !important; } +.tab-size-12 { tab-size: 12 !important; } +.tab-size-13 { tab-size: 13 !important; } +.tab-size-14 { tab-size: 14 !important; } +.tab-size-15 { tab-size: 15 !important; } +.tab-size-16 { tab-size: 16 !important; } diff --git a/web_src/css/index.css b/web_src/css/index.css index 224d3d23ab..49ceb2c8ce 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -6,12 +6,15 @@ @import "./modules/container.css"; @import "./modules/divider.css"; @import "./modules/header.css"; +@import "./modules/input.css"; @import "./modules/label.css"; +@import "./modules/list.css"; @import "./modules/segment.css"; @import "./modules/grid.css"; @import "./modules/message.css"; @import "./modules/table.css"; @import "./modules/card.css"; +@import "./modules/checkbox.css"; @import "./modules/modal.css"; @import "./modules/select.css"; diff --git a/web_src/css/install.css b/web_src/css/install.css index 4ac294e902..ee2395e6c5 100644 --- a/web_src/css/install.css +++ b/web_src/css/install.css @@ -18,7 +18,8 @@ width: auto; } -.page-content.install form.ui.form input { +.page-content.install form.ui.form input:not([type="checkbox"],[type="radio"]), +.page-content.install form.ui.form .ui.selection.dropdown { width: 60%; } diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 430b4802d6..419868a461 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -438,7 +438,7 @@ margin: 0; font-size: 85%; white-space: break-spaces; - background-color: var(--color-markup-code-block); + background-color: var(--color-markup-code-inline); border-radius: var(--border-radius); } @@ -509,7 +509,7 @@ line-height: 10px; color: var(--color-text-light); vertical-align: middle; - background-color: var(--color-markup-code-block); + background-color: var(--color-markup-code-inline); border: 1px solid var(--color-secondary); border-radius: var(--border-radius); box-shadow: inset 0 -1px 0 var(--color-secondary); diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css index 788a4ed6ed..361618c449 100644 --- a/web_src/css/modules/animations.css +++ b/web_src/css/modules/animations.css @@ -6,7 +6,6 @@ .is-loading { pointer-events: none !important; position: relative !important; - overflow: hidden !important; } .is-loading > * { @@ -35,10 +34,14 @@ border-radius: var(--border-radius-circle); } -.is-loading.small-loading-icon::after { +.is-loading.loading-icon-2px::after { border-width: 2px; } +.is-loading.loading-icon-3px::after { + border-width: 3px; +} + /* for single form button, the loading state should be on the button, but not go semi-transparent, just replace the text on the button with the loader. */ form.single-button-form.is-loading > * { opacity: 1; @@ -63,7 +66,7 @@ form.single-button-form.is-loading .button { background: transparent; } -/* TODO: not needed, use "is-loading small-loading-icon" instead */ +/* TODO: not needed, use "is-loading loading-icon-2px" instead */ code.language-math.is-loading::after { padding: 0; border-width: 2px; diff --git a/web_src/css/modules/checkbox.css b/web_src/css/modules/checkbox.css new file mode 100644 index 0000000000..8d73573bfa --- /dev/null +++ b/web_src/css/modules/checkbox.css @@ -0,0 +1,121 @@ +/* based on Fomantic UI checkbox module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +input[type="checkbox"], +input[type="radio"] { + width: var(--checkbox-size); + height: var(--checkbox-size); +} + +.ui.checkbox { + position: relative; + display: inline-block; + vertical-align: baseline; + min-height: var(--checkbox-size); + line-height: var(--checkbox-size); + min-width: var(--checkbox-size); + padding: 1px; +} + +.ui.checkbox input[type="checkbox"], +.ui.checkbox input[type="radio"] { + position: absolute; + top: 1px; + left: 0; + width: var(--checkbox-size); + height: var(--checkbox-size); +} + +.ui.checkbox input[type="checkbox"]:enabled, +.ui.checkbox input[type="radio"]:enabled, +.ui.checkbox label:enabled { + cursor: pointer; +} + +.ui.checkbox label { + cursor: auto; + position: relative; + display: block; + user-select: none; +} + +.ui.checkbox label, +.ui.radio.checkbox label { + margin-left: 1.85714em; +} + +.ui.checkbox + label { + vertical-align: middle; +} + +.ui.disabled.checkbox label, +.ui.checkbox input[disabled] ~ label { + cursor: default !important; + opacity: 0.5; + pointer-events: none; +} + +.ui.radio.checkbox { + min-height: var(--checkbox-size); +} + +/* "switch" styled checkbox */ + +.ui.toggle.checkbox { + min-height: 1.5rem; +} +.ui.toggle.checkbox input { + width: 3.5rem; + height: 21px; + opacity: 0; + z-index: 3; +} +.ui.toggle.checkbox label { + min-height: 1.5rem; + padding-left: 4.5rem; + padding-top: 0.15em; +} +.ui.toggle.checkbox label::before { + display: block; + position: absolute; + content: ""; + z-index: 1; + top: 0; + width: 49px; + height: 21px; + border-radius: 500rem; + left: 0; +} +.ui.toggle.checkbox label::after { + background: var(--color-white); + box-shadow: 1px 1px 4px 1px var(--color-shadow); + position: absolute; + content: ""; + opacity: 1; + z-index: 2; + width: 18px; + height: 18px; + top: 1.5px; + left: 1.5px; + border-radius: 500rem; + transition: background 0.3s ease, left 0.3s ease; +} +.ui.toggle.checkbox input ~ label::after { + left: 1.5px; +} +.ui.toggle.checkbox input:checked ~ label::after { + left: 29px; +} +.ui.toggle.checkbox input:focus ~ label::before, +.ui.toggle.checkbox label::before { + background: var(--color-input-toggle-background); +} +.ui.toggle.checkbox label, +.ui.toggle.checkbox input:checked ~ label, +.ui.toggle.checkbox input:focus:checked ~ label { + color: var(--color-text) !important; +} +.ui.toggle.checkbox input:checked ~ label::before, +.ui.toggle.checkbox input:focus:checked ~ label::before { + background: var(--color-primary) !important; +} diff --git a/web_src/css/modules/container.css b/web_src/css/modules/container.css index dc854f89d0..f394d6c06d 100644 --- a/web_src/css/modules/container.css +++ b/web_src/css/modules/container.css @@ -49,30 +49,11 @@ /* overwrite width of containers inside the main page content div (div with class "page-content") */ .page-content .ui.ui.ui.container:not(.fluid) { width: 1280px; - max-width: calc(100% - 64px); + max-width: calc(100% - calc(2 * var(--page-margin-x))); margin-left: auto; margin-right: auto; } .ui.container.fluid.padded { - padding: 0 32px; -} - -/* enable fluid page widths for medium size viewports */ -@media (min-width: 768px) and (max-width: 1200px) { - .page-content .ui.ui.ui.container:not(.fluid) { - max-width: calc(100% - 32px); - } - .ui.container.fluid.padded { - padding: 0 16px; - } -} - -@media (max-width: 767.98px) { - .page-content .ui.ui.ui.container:not(.fluid) { - max-width: calc(100% - 16px); - } - .ui.container.fluid.padded { - padding: 0 8px; - } + padding: 0 var(--page-margin-x); } diff --git a/web_src/css/modules/divider.css b/web_src/css/modules/divider.css index 48560bd3d9..acc8408f37 100644 --- a/web_src/css/modules/divider.css +++ b/web_src/css/modules/divider.css @@ -2,12 +2,16 @@ margin: 10px 0; height: 0; font-weight: var(--font-weight-medium); - text-transform: uppercase; color: var(--color-text); font-size: 1rem; width: 100%; } +h4.divider { + margin-top: 1.25rem; + margin-bottom: 1.25rem; +} + .divider:not(.divider-text) { border-top: 1px solid var(--color-secondary); } diff --git a/web_src/css/modules/flexcontainer.css b/web_src/css/modules/flexcontainer.css index 0b559f1e7d..5d4e12cc12 100644 --- a/web_src/css/modules/flexcontainer.css +++ b/web_src/css/modules/flexcontainer.css @@ -2,13 +2,20 @@ .flex-container { display: flex !important; - gap: 16px; + gap: var(--page-spacing); + margin-top: var(--page-spacing); } +/* small options menu on the left, used in settings/admin pages */ .flex-container-nav { width: 240px; } +/* wide sidebar on the right, used in frontpage */ +.flex-container-sidebar { + width: 35%; +} + .flex-container-main { flex: 1; min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */ @@ -18,7 +25,9 @@ .flex-container { flex-direction: column; } - .flex-container-nav { + .flex-container-nav, + .flex-container-sidebar { + order: -1; width: auto; } } diff --git a/web_src/css/modules/header.css b/web_src/css/modules/header.css index 091d536cfc..9cec5fcbe6 100644 --- a/web_src/css/modules/header.css +++ b/web_src/css/modules/header.css @@ -9,7 +9,6 @@ font-family: var(--fonts-regular); font-weight: var(--font-weight-medium); line-height: 1.28571429; - text-transform: none; } .ui.header:first-child { @@ -135,6 +134,12 @@ h4.ui.header .sub.header { font-weight: var(--font-weight-normal); } +/* open dropdown menus to the left in right-attached headers */ +.ui.attached.header > .ui.right .ui.dropdown .menu { + right: 0; + left: auto; +} + /* if a .top.attached.header is followed by a .segment, add some margin */ .ui.segments + .ui.top.attached.header, .ui.attached.segment + .ui.top.attached.header { diff --git a/web_src/css/modules/input.css b/web_src/css/modules/input.css new file mode 100644 index 0000000000..18b785ac82 --- /dev/null +++ b/web_src/css/modules/input.css @@ -0,0 +1,197 @@ +/* based on Fomantic UI input module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +.ui.input { + position: relative; + font-weight: var(--font-weight-normal); + display: inline-flex; + color: var(--color-input-text); +} +.ui.input > input { + margin: 0; + max-width: 100%; + flex: 1 0 auto; + outline: none; + font-family: var(--fonts-regular); + padding: 0.67857143em 1em; + border: 1px solid var(--color-input-border); + color: var(--color-input-text); + border-radius: 0.28571429rem; + line-height: var(--line-height-default); + text-align: start; +} + +.ui.disabled.input, +.ui.input:not(.disabled) input[disabled] { + opacity: var(--opacity-disabled); +} +.ui.disabled.input > input, +.ui.input:not(.disabled) input[disabled] { + pointer-events: none; +} + +.ui.input.focus > input, +.ui.input > input:focus { + border-color: var(--color-primary); +} + +.ui.input.error > input { + background: var(--color-error-bg); + border-color: var(--color-error-border); + color: var(--color-error-text); +} + +.ui.icon.input > i.icon { + display: flex; + align-items: center; + justify-content: center; + cursor: default; + position: absolute; + text-align: center; + top: 0; + right: 0; + margin: 0; + height: 100%; + width: 2.67142857em; + opacity: 0.5; + border-radius: 0 0.28571429rem 0.28571429rem 0; + pointer-events: none; + padding: 4px; +} + +.ui.icon.input > i.icon.is-loading { + position: absolute !important; + height: 28px; + top: 4px; +} + +.ui.icon.input > i.icon.is-loading > * { + visibility: hidden; +} + +.ui.ui.ui.ui.icon.input > textarea, +.ui.ui.ui.ui.icon.input > input { + padding-right: 2.67142857em; +} +.ui.icon.input > i.link.icon { + cursor: pointer; +} +.ui.icon.input > i.circular.icon { + top: 0.35em; + right: 0.5em; +} + +.ui[class*="left icon"].input > i.icon { + right: auto; + left: 1px; + border-radius: 0.28571429rem 0 0 0.28571429rem; +} +.ui[class*="left icon"].input > i.circular.icon { + right: auto; + left: 0.5em; +} +.ui.ui.ui.ui[class*="left icon"].input > textarea, +.ui.ui.ui.ui[class*="left icon"].input > input { + padding-left: 2.67142857em; + padding-right: 1em; +} + +.ui.icon.input > textarea:focus ~ .icon, +.ui.icon.input > input:focus ~ .icon { + opacity: 1; +} + +.ui.icon.input > textarea ~ i.icon { + height: 3em; +} + +.ui.form .field.error > .ui.action.input > .ui.button, +.ui.action.input.error > .ui.button { + border-top: 1px solid var(--color-error-border); + border-bottom: 1px solid var(--color-error-border); +} + +.ui.action.input > .button, +.ui.action.input > .buttons { + display: flex; + align-items: center; + flex: 0 0 auto; +} +.ui.action.input > .button, +.ui.action.input > .buttons > .button { + padding-top: 0.78571429em; + padding-bottom: 0.78571429em; + margin: 0; +} + +.ui.action.input:not([class*="left action"]) > input { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right-color: transparent; +} + +.ui.action.input > .dropdown:first-child, +.ui.action.input > .button:first-child, +.ui.action.input > .buttons:first-child > .button { + border-radius: 0.28571429rem 0 0 0.28571429rem; +} +.ui.action.input > .dropdown:not(:first-child), +.ui.action.input > .button:not(:first-child), +.ui.action.input > .buttons:not(:first-child) > .button { + border-radius: 0; +} +.ui.action.input > .dropdown:last-child, +.ui.action.input > .button:last-child, +.ui.action.input > .buttons:last-child > .button { + border-radius: 0 0.28571429rem 0.28571429rem 0; +} + +.ui.fluid.input { + display: flex; +} +.ui.fluid.input > input { + width: 0 !important; +} + +.ui.tiny.input { + font-size: 0.85714286em; +} +.ui.small.input { + font-size: 0.92857143em; +} + +.ui.action.input .ui.ui.button { + border-color: var(--color-input-border); + padding-top: 0; /* the ".action.input" is "flex + stretch", so let the buttons layout themselves */ + padding-bottom: 0; +} + +/* currently used for search bar dropdowns in repo search and explore code */ +.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection { + min-width: 10em; +} +.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(:focus) { + border-right: none; +} +.ui.action.input:not([class*="left action"]) > .ui.dropdown.selection:not(.active):hover { + border-color: var(--color-input-border); +} +.ui.action.input:not([class*="left action"]) .ui.dropdown.selection.upward.visible { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} +.ui.action.input:not([class*="left action"]) > input, +.ui.action.input:not([class*="left action"]) > input:hover { + border-right: none; +} +.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection, +.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover, +.ui.action.input:not([class*="left action"]) > input:focus + .button, +.ui.action.input:not([class*="left action"]) > input:focus + .button:hover, +.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button, +.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover { + border-left-color: var(--color-primary); +} +.ui.action.input:not([class*="left action"]) > input:focus { + border-right-color: var(--color-primary); +} diff --git a/web_src/css/modules/label.css b/web_src/css/modules/label.css index 0512c5fddb..2032b2c84b 100644 --- a/web_src/css/modules/label.css +++ b/web_src/css/modules/label.css @@ -5,12 +5,12 @@ display: inline-flex; align-items: center; gap: .25rem; + min-width: 0; vertical-align: middle; line-height: 1; background: var(--color-label-bg); color: var(--color-label-text); padding: 0.3em 0.5em; - text-transform: none; font-size: 0.85714286rem; font-weight: var(--font-weight-medium); border: 0 solid transparent; diff --git a/web_src/css/modules/list.css b/web_src/css/modules/list.css new file mode 100644 index 0000000000..32c71e802b --- /dev/null +++ b/web_src/css/modules/list.css @@ -0,0 +1,193 @@ +/* based on Fomantic UI list module, with just the parts extracted that we use. If you find any + unused rules here after refactoring, please remove them. */ + +.ui.list { + list-style-type: none; + margin: 1em 0; + padding: 0; + font-size: 1em; +} + +.ui.list:first-child { + margin-top: 0; + padding-top: 0; +} + +.ui.list:last-child { + margin-bottom: 0; + padding-bottom: 0; +} + +.ui.list > .item, +.ui.list .list > .item { + display: list-item; + table-layout: fixed; + list-style-type: none; + list-style-position: outside; +} + +.ui.list > .list > .item::after, +.ui.list > .item::after { + content: ""; + display: block; + height: 0; + clear: both; + visibility: hidden; +} + +.ui.list .list:not(.icon) { + clear: both; + margin: 0; + padding: 0.75em 0 0.25em 0.5em; +} + +.ui.list .list > .item { + padding: 0.14285714em 0; +} + +.ui.list .list > .item > i.icon, +.ui.list > .item > i.icon { + display: table-cell; + min-width: 1.55em; + padding-top: 0; + transition: color 0.1s ease; + padding-right: 0.28571429em; + vertical-align: top; +} +.ui.list .list > .item > i.icon:only-child, +.ui.list > .item > i.icon:only-child { + display: inline-block; + min-width: auto; + vertical-align: top; +} + +.ui.list .list > .item > .image, +.ui.list > .item > .image { + display: table-cell; + background-color: transparent; + vertical-align: top; +} +.ui.list .list > .item > .image:not(:only-child):not(img), +.ui.list > .item > .image:not(:only-child):not(img) { + padding-right: 0.5em; +} +.ui.list .list > .item > .image img, +.ui.list > .item > .image img { + vertical-align: top; +} +.ui.list .list > .item > img.image, +.ui.list .list > .item > .image:only-child, +.ui.list > .item > img.image, +.ui.list > .item > .image:only-child { + display: inline-block; +} + +.ui.list .list > .item > .content, +.ui.list > .item > .content { + color: var(--color-text); +} +.ui.list .list > .item > .image + .content, +.ui.list .list > .item > i.icon + .content, +.ui.list > .item > .image + .content, +.ui.list > .item > i.icon + .content { + display: table-cell; + width: 100%; + padding: 0 0 0 0.5em; + vertical-align: top; +} +.ui.list .list > .item > img.image + .content, +.ui.list > .item > img.image + .content { + display: inline-block; + width: auto; +} +.ui.list .list > .item > .content > .list, +.ui.list > .item > .content > .list { + margin-left: 0; + padding-left: 0; +} + +.ui.list .list > .item .header, +.ui.list > .item .header { + display: block; + margin: 0; + font-family: var(--fonts-regular); + font-weight: var(--font-weight-medium); + color: var(--color-text-dark); +} + +.ui.list .list > .item .description, +.ui.list > .item .description { + display: block; + color: var(--color-text); +} + +.ui.list > .item a, +.ui.list .list > .item a { + cursor: pointer; +} + +.ui.list .list > .item [class*="right floated"], +.ui.list > .item [class*="right floated"] { + float: right; + margin: 0 0 0 1em; +} + +.ui.menu .ui.list > .item, +.ui.menu .ui.list .list > .item { + display: list-item; + table-layout: fixed; + background-color: transparent; + list-style-type: none; + list-style-position: outside; + padding: 0.21428571em 0; +} +.ui.menu .ui.list .list > .item::before, +.ui.menu .ui.list > .item::before { + border: none; + background: none; +} +.ui.menu .ui.list .list > .item:first-child, +.ui.menu .ui.list > .item:first-child { + padding-top: 0; +} +.ui.menu .ui.list .list > .item:last-child, +.ui.menu .ui.list > .item:last-child { + padding-bottom: 0; +} + +.ui.list .list > .disabled.item, +.ui.list > .disabled.item { + pointer-events: none; + opacity: var(--opacity-disabled); +} + +.ui.list .list > a.item:hover > .icons, +.ui.list > a.item:hover > .icons, +.ui.list .list > a.item:hover > i.icon, +.ui.list > a.item:hover > i.icon { + color: var(--color-text-dark); +} + +.ui.divided.list > .item { + border-top: 1px solid var(--color-secondary); +} +.ui.divided.list .list > .item { + border-top: none; +} +.ui.divided.list .item .list > .item { + border-top: none; +} +.ui.divided.list .list > .item:first-child, +.ui.divided.list > .item:first-child { + border-top: none; +} +.ui.divided.list .list > .item:first-child { + border-top-width: 1px; +} + +.ui.relaxed.list > .item:not(:first-child) { + padding-top: 0.42857143em; +} +.ui.relaxed.list > .item:not(:last-child) { + padding-bottom: 0.42857143em; +} diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css index 6906eb49a2..02d470f5dc 100644 --- a/web_src/css/modules/navbar.css +++ b/web_src/css/modules/navbar.css @@ -140,3 +140,8 @@ .secondary-nav { background: var(--color-secondary-nav-bg) !important; /* important because of .ui.secondary.menu */ } + +.issue-navbar { + display: flex; + justify-content: space-between; +} diff --git a/web_src/css/modules/segment.css b/web_src/css/modules/segment.css index 963b2209b0..04543f0986 100644 --- a/web_src/css/modules/segment.css +++ b/web_src/css/modules/segment.css @@ -72,6 +72,9 @@ .ui.segments:not(.horizontal) > .segment:only-child { border-radius: 0.214285717rem; } +.ui.segments:not(.horizontal) > .segment:has(~ .tw-hidden) { /* workaround issue with :last-child ignoring hidden elements */ + border-radius: 0.28571429rem; +} .ui.segments > .ui.segments { border-top: 1px solid var(--color-secondary); @@ -150,6 +153,11 @@ border-top: none; } +.ui.attached.segment:has(+ .ui[class*="top attached"].header), +.ui.attached.segment:last-child { + border-radius: 0 0 0.28571429rem 0.28571429rem; +} + .ui[class*="top attached"].segment { bottom: 0; margin-bottom: 0; diff --git a/web_src/css/modules/tippy.css b/web_src/css/modules/tippy.css index 76d36b4293..6ac7c37d93 100644 --- a/web_src/css/modules/tippy.css +++ b/web_src/css/modules/tippy.css @@ -29,6 +29,17 @@ z-index: 1; } +/* bare theme, no styling at all, except box-shadow */ +.tippy-box[data-theme="bare"] { + border: none; + box-shadow: 0 6px 18px var(--color-shadow); +} + +.tippy-box[data-theme="bare"] .tippy-content { + padding: 0; + background: transparent; +} + /* tooltip theme for text tooltips */ .tippy-box[data-theme="tooltip"] { diff --git a/web_src/css/org.css b/web_src/css/org.css index a1ef8e08ed..00d50fce41 100644 --- a/web_src/css/org.css +++ b/web_src/css/org.css @@ -89,10 +89,6 @@ text-align: center; } -.organization.options input { - min-width: 300px; -} - .page-content.organization .org-avatar { margin-right: 15px; } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 03e4f1d74b..bf6bfd464b 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -177,12 +177,44 @@ } } -.repository.file.list .repo-path { - word-break: break-word; +.commit-summary { + flex: 1; + overflow-wrap: anywhere; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } -.repository.file.list #repo-files-table { - table-layout: fixed; +.commit-header .commit-summary, +td .commit-summary { + white-space: normal; +} + +.latest-commit { + display: flex; + flex: 1; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 767.98px) { + .latest-commit .sha { + display: none; + } + .latest-commit .commit-summary { + margin-left: 8px; + } +} + +.repo-path { + display: flex; + overflow-wrap: anywhere; +} + +/* this is what limits the commit table width to a value that works on all viewport sizes */ +#repo-files-table th:first-of-type { + max-width: calc(calc(min(100vw, 1280px)) - 145px - calc(2 * var(--page-margin-x))); } .repository.file.list #repo-files-table thead th { @@ -262,7 +294,6 @@ } .repository.file.list #repo-files-table td.age { - width: 120px; color: var(--color-text-light-1); } @@ -371,7 +402,7 @@ .pdf-content { width: 100%; - height: 600px; + height: 100vh; border: none !important; display: flex; align-items: center; @@ -404,7 +435,6 @@ padding: 0 !important; } -.non-diff-file-content .attached.segment, .non-diff-file-content .pdfobject { border-radius: 0 0 var(--border-radius) var(--border-radius); } @@ -564,26 +594,18 @@ align-items: center; } -.repository.view.issue .issue-title-buttons, -.repository.view.issue .edit-buttons { +.repository.view.issue .top-right-buttons { display: flex; } -.issue-title-buttons { - gap: 0.5rem; -} - @media (max-width: 767.98px) { .repository.view.issue .issue-title { flex-direction: column; } - .repository.view.issue .issue-title-buttons, - .repository.view.issue .edit-buttons { + .repository.view.issue .top-right-buttons { width: 100%; - justify-content: space-between; - } - .repository.view.issue .edit-buttons { margin-top: .5rem; + justify-content: space-between; } .comment.form .issue-content-left .avatar { display: none; @@ -597,6 +619,10 @@ .comment.form .content .form::after { display: none; } + + .repository.view.issue .issue-title.edit-active h1 { + padding-right: 0; + } } .repository.view.issue .issue-title { @@ -614,7 +640,7 @@ font-size: 32px; line-height: 40px; margin: 0; - padding-right: 0.25rem; + padding-right: 0.5rem; min-height: var(--repo-header-issue-min-height); } @@ -809,55 +835,53 @@ margin-right: 0.25em; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit { - line-height: 34px; /* this must be same as .badge height, to avoid overflow */ - clear: both; /* reset the "float right shabox", in the future, use flexbox instead */ +.singular-commit { + display: flex; + align-items: center; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit > img.avatar, -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit > .avatar img { - position: relative; - top: -2px; +.singular-commit .badge { + height: 30px !important; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label { +.singular-commit .shabox .sha.label { margin: 0; border: 1px solid var(--color-light-border); } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isWarning { +.singular-commit .shabox .sha.label.isSigned.isWarning { border: 1px solid var(--color-red-badge); background: var(--color-red-badge-bg); } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isWarning:hover { +.singular-commit .shabox .sha.label.isSigned.isWarning:hover { background: var(--color-red-badge-hover-bg) !important; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerified { +.singular-commit .shabox .sha.label.isSigned.isVerified { border: 1px solid var(--color-green-badge); background: var(--color-green-badge-bg); } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerified:hover { +.singular-commit .shabox .sha.label.isSigned.isVerified:hover { background: var(--color-green-badge-hover-bg) !important; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted { +.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted { border: 1px solid var(--color-yellow-badge); background: var(--color-yellow-badge-bg); } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover { +.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover { background: var(--color-yellow-badge-hover-bg) !important; } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched { +.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched { border: 1px solid var(--color-orange-badge); background: var(--color-orange-badge-bg); } -.repository.view.issue .comment-list .timeline-item.commits-list .singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover { +.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover { background: var(--color-orange-badge-hover-bg) !important; } @@ -1052,6 +1076,12 @@ margin-left: 15px; } +.repository.view.issue .comment-list .event .detail .text { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + .repository.view.issue .comment-list .event .segments { box-shadow: none; } @@ -1223,10 +1253,6 @@ margin: 0; } -.repository #commits-table td.message { - text-overflow: unset; -} - .repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) { background-color: var(--color-light) !important; } @@ -2153,6 +2179,20 @@ display: inline-block !important; } +.commit-header-buttons { + display: flex; + gap: 4px; + align-items: flex-start; + white-space: nowrap; +} + +@media (max-width: 767.98px) { + .commit-header-buttons { + flex-direction: column; + align-items: stretch; + } +} + .settings.webhooks .list > .item:not(:first-child), .settings.githooks .list > .item:not(:first-child), .settings.actions .list > .item:not(:first-child) { @@ -2299,107 +2339,11 @@ padding-top: 15px; } -.edit-label.modal .form .color.picker.column, -.new-label.modal .form .color.picker.column { - display: flex; -} - -.edit-label.modal .form .color.picker.column .minicolors, -.new-label.modal .form .color.picker.column .minicolors { - flex: 1; -} - -.edit-label.modal .form .minicolors-swatch.minicolors-sprite, -.new-label.modal .form .minicolors-swatch.minicolors-sprite { - top: 10px; - left: 10px; - width: 15px; - height: 15px; -} - -.tab-size-1 { - tab-size: 1 !important; - -moz-tab-size: 1 !important; -} - -.tab-size-2 { - tab-size: 2 !important; - -moz-tab-size: 2 !important; -} - -.tab-size-3 { - tab-size: 3 !important; - -moz-tab-size: 3 !important; -} - -.tab-size-4 { - tab-size: 4 !important; - -moz-tab-size: 4 !important; -} - -.tab-size-5 { - tab-size: 5 !important; - -moz-tab-size: 5 !important; -} - -.tab-size-6 { - tab-size: 6 !important; - -moz-tab-size: 6 !important; -} - -.tab-size-7 { - tab-size: 7 !important; - -moz-tab-size: 7 !important; -} - -.tab-size-8 { - tab-size: 8 !important; - -moz-tab-size: 8 !important; -} - -.tab-size-9 { - tab-size: 9 !important; - -moz-tab-size: 9 !important; -} - -.tab-size-10 { - tab-size: 10 !important; - -moz-tab-size: 10 !important; -} - -.tab-size-11 { - tab-size: 11 !important; - -moz-tab-size: 11 !important; -} - -.tab-size-12 { - tab-size: 12 !important; - -moz-tab-size: 12 !important; -} - -.tab-size-13 { - tab-size: 13 !important; - -moz-tab-size: 13 !important; -} - -.tab-size-14 { - tab-size: 14 !important; - -moz-tab-size: 14 !important; -} - -.tab-size-15 { - tab-size: 15 !important; - -moz-tab-size: 15 !important; -} - -.tab-size-16 { - tab-size: 16 !important; - -moz-tab-size: 16 !important; -} - .stats-table { display: table; width: 100%; + margin: 6px 0; + border-spacing: 2px; } .stats-table .table-cell { @@ -2407,11 +2351,34 @@ } .stats-table .table-cell.tiny { - height: 0.5em; + height: 8px; +} + +.stats-table .table-cell:first-child { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.stats-table .table-cell:last-child { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.labels-list { + display: inline-flex; + flex-wrap: wrap; + gap: 2.5px; +} + +.labels-list a { + display: flex; + text-decoration: none; } .labels-list .label { - margin: 2px 0; + padding: 0 6px; + margin: 0 !important; + min-height: 20px; display: inline-flex !important; line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */ } @@ -2435,6 +2402,10 @@ margin-left: 0; } +.archived-label { + filter: grayscale(0.25) saturate(0.75); +} + .repo-button-row { margin: 10px 0; display: flex; @@ -2465,7 +2436,7 @@ tbody.commit-list { .author-wrapper { overflow: hidden; text-overflow: ellipsis; - max-width: calc(100% - 50px); + max-width: 100%; display: inline-block; vertical-align: middle; } @@ -2473,6 +2444,7 @@ tbody.commit-list { .author-wrapper { max-width: 180px; align-self: center; + white-space: nowrap; } /* in the commit list, messages can wrap so we can use inline */ @@ -2490,10 +2462,6 @@ tbody.commit-list { tr.commit-list { width: 100%; } - th .message-wrapper { - display: block; - max-width: calc(100vw - 70px); - } .author-wrapper { max-width: 80px; } @@ -2503,27 +2471,18 @@ tbody.commit-list { tr.commit-list { width: 723px; } - th .message-wrapper { - max-width: 120px; - } } @media (min-width: 992px) and (max-width: 1200px) { tr.commit-list { width: 933px; } - th .message-wrapper { - max-width: 350px; - } } @media (min-width: 1201px) { tr.commit-list { width: 1127px; } - th .message-wrapper { - max-width: 525px; - } } .commit-list .commit-status-link { @@ -2569,6 +2528,7 @@ tbody.commit-list { #repo-topics .repo-topic { font-weight: var(--font-weight-normal); cursor: pointer; + margin: 0; } #new-dependency-drop-list.ui.selection.dropdown { @@ -2733,7 +2693,7 @@ tbody.commit-list { display: inline-block; background-color: var(--color-red); height: 12px; - width: 40px; + width: 44px; } .diff-stats-bar .diff-stats-add-bar { @@ -2850,7 +2810,7 @@ tbody.commit-list { .repository.file.list #repo-files-table .entry td.message, .repository.file.list #repo-files-table .commit-list td.message, .repository.file.list #repo-files-table .entry span.commit-summary, - .repository.file.list #repo-files-table .commit-list span.commit-summary { + .repository.file.list #repo-files-table .commit-list tr span.commit-summary { display: none !important; } .repository.view.issue .comment-list .timeline, @@ -2986,6 +2946,7 @@ tbody.commit-list { display: flex; align-items: center; justify-content: flex-end; + gap: 8px; } @media (max-width: 767.98px) { diff --git a/web_src/css/repo/issue-card.css b/web_src/css/repo/issue-card.css index b9368df4f6..609b1b3dbd 100644 --- a/web_src/css/repo/issue-card.css +++ b/web_src/css/repo/issue-card.css @@ -1,6 +1,7 @@ .issue-card { display: flex; flex-direction: column; + gap: 4px; align-items: start; border-radius: var(--border-radius); padding: 8px 10px; @@ -17,7 +18,6 @@ .issue-card-title { flex: 1; font-size: 14px; - margin-left: 4px; } .issue-card.sortable-chosen .issue-card-title { diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index e46ffeb4f0..37090f71b4 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -9,6 +9,7 @@ .issue-list-toolbar-left { display: flex; + align-items: center; } .issue-list-toolbar-right .filter.menu { @@ -68,23 +69,6 @@ } } -#issue-list .flex-item-title .labels-list { - display: flex; - flex-wrap: wrap; - gap: 0.25em; -} - -#issue-list .flex-item-title .labels-list a { - display: flex; - text-decoration: none; -} - -#issue-list .flex-item-title .labels-list .label { - padding: 0 6px; - margin: 0; - min-height: 20px; -} - #issue-list .flex-item-body .branches { display: inline-flex; } diff --git a/web_src/css/repo/release-tag.css b/web_src/css/repo/release-tag.css index a146eda6a9..e2b11dc77e 100644 --- a/web_src/css/repo/release-tag.css +++ b/web_src/css/repo/release-tag.css @@ -33,20 +33,32 @@ .repository.releases #release-list > li .detail .download .list { padding-left: 0; - border: 1px solid var(--color-secondary); - border-radius: var(--border-radius); - background: var(--color-light); } .repository.releases #release-list > li .detail .download .list li { + background: var(--color-light); + border: 1px solid var(--color-secondary); + border-top: none; display: flex; justify-content: space-between; padding: 8px; - border-bottom: 1px solid var(--color-secondary); } -.repository.releases #release-list > li .detail .download .list li:last-child { - border-bottom: none; +.repository.releases #release-list > li .detail .download .list :is(li:first-child, .start-gap + hr + li) { + border-top: 1px solid var(--color-secondary); + border-top-left-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); +} + +.repository.releases #release-list > li .detail .download .list :is(li:last-child, .start-gap) { + border-bottom: 1px solid var(--color-secondary); + border-bottom-left-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); +} + +.repository.releases #release-list > li .detail .download .list hr { + height: 8px; + margin: 0; } .repository.releases #release-list > li .detail .dot { diff --git a/web_src/css/themes/theme-forgejo-dark.css b/web_src/css/themes/theme-forgejo-dark.css index 0533344a8c..95d35ddec0 100644 --- a/web_src/css/themes/theme-forgejo-dark.css +++ b/web_src/css/themes/theme-forgejo-dark.css @@ -141,6 +141,7 @@ /* other colors */ --color-gold: #b1983b; --color-white: #ffffff; + --color-pure-black: #000000; --color-diff-removed-word-bg: #783030; --color-diff-added-word-bg: #255c39; --color-diff-removed-row-bg: #432121; @@ -157,10 +158,10 @@ --color-error-text: #fef2f2; --color-success-border: #1f6e3c; --color-success-bg: #1d462c; - --color-success-text: #f0fdf4; + --color-success-text: #aef0c2; --color-warning-border: #a67a1d; --color-warning-bg: #644821; - --color-warning-text: #fefce8; + --color-warning-text: #fff388; --color-info-border: #2e50b0; --color-info-bg: #2a396b; --color-info-text: var(--steel-100); @@ -205,15 +206,15 @@ --color-menu: var(--steel-700); --color-card: var(--steel-700); --color-markup-table-row: #ffffff06; - --color-markup-code-block: var(--steel-850); + --color-markup-code-block: var(--steel-800); + --color-markup-code-inline: var(--steel-850); --color-button: var(--steel-600); --color-code-bg: var(--steel-750); - --color-code-sidebar-bg: var(--steel-600); --color-shadow: #00000060; --color-secondary-bg: var(--steel-700); --color-text-focus: #fff; --color-expand-button: #3c404d; - --color-placeholder-text: var(--steel-450); + --color-placeholder-text: var(--color-text-light-3); --color-editor-line-highlight: var(--steel-700); --color-project-board-bg: var(--color-secondary-light-3); --color-project-board-dark-label: var(--color-text-light-3); @@ -226,12 +227,14 @@ --color-nav-bg: var(--steel-900); --color-nav-hover-bg: var(--steel-600); --color-label-text: #fff; - --color-label-bg: #cacaca5b; - --color-label-hover-bg: #cacacaa0; - --color-label-active-bg: #cacacaff; + --color-label-bg: var(--steel-600); + --color-label-hover-bg: var(--steel-550); + --color-label-active-bg: var(--steel-500); + --color-label-bg-alt: var(--steel-550); --color-accent: var(--color-primary-light-1); --color-small-accent: var(--color-primary-light-5); - --color-active-line: var(--color-primary-alpha-20); + --color-highlight-fg: var(--color-primary-light-4); + --color-highlight-bg: var(--color-primary-alpha-20); --color-overlay-backdrop: #080808c0; accent-color: var(--color-accent); color-scheme: dark; @@ -269,9 +272,6 @@ i.grey.icon.icon.icon.icon { border-radius: 0.28571429rem !important; overflow: hidden; } -.ui.secondary.vertical.menu > .item { - border-radius: 0 !important; -} .ui.basic.primary.button.item { background-color: var(--color-active) !important; color: var(--color-text) !important; @@ -305,5 +305,27 @@ i.grey.icon.icon.icon.icon { } ::selection { background: var(--steel-100) !important; - color: var(--color-white) !important; + color: var(--color-pure-black) !important; +} +strong.attention-important, svg.attention-important { + color: var(--color-violet-light); +} +strong.attention-note, svg.attention-note { + color: var(--color-blue-light); +} +strong.attention-caution, svg.attention-caution { + color: var(--color-red-light); +} +.ui.basic.red.button { + background-color: var(--color-red); + color: var(--color-white); +} +.ui.basic.red.button:hover, +.ui.basic.red.button:focus { + background-color: var(--color-red-dark-1); + color: var(--color-white); +} +.ui.basic.red.button:active { + background-color: var(--color-red-dark-2); + color: var(--color-white); } diff --git a/web_src/css/themes/theme-forgejo-light.css b/web_src/css/themes/theme-forgejo-light.css index e26ba2552c..ea988e67fa 100644 --- a/web_src/css/themes/theme-forgejo-light.css +++ b/web_src/css/themes/theme-forgejo-light.css @@ -224,14 +224,14 @@ --color-card: var(--zinc-50); --color-markup-table-row: #ffffff06; --color-markup-code-block: var(--zinc-150); + --color-markup-code-inline: var(--zinc-200); --color-button: var(--zinc-150); --color-code-bg: var(--zinc-50); - --color-code-sidebar-bg: var(--zinc-100); --color-shadow: #00000060; --color-secondary-bg: var(--zinc-100); --color-text-focus: #fff; --color-expand-button: var(--zinc-200); - --color-placeholder-text: var(--zinc-400); + --color-placeholder-text: var(--color-text-light-3); --color-editor-line-highlight: var(--zinc-100); --color-project-board-bg: var(--color-secondary-light-2); --color-project-board-dark-label: var(--color-text-light-3); @@ -246,9 +246,11 @@ --color-label-bg: #cacaca5b; --color-label-hover-bg: #cacacaa0; --color-label-active-bg: #cacacaff; + --color-label-bg-alt: #cacacaff; --color-accent: var(--color-primary-light-1); --color-small-accent: var(--color-primary-light-5); - --color-active-line: var(--color-primary-light-6); + --color-highlight-fg: var(--color-primary-light-4); + --color-highlight-bg: var(--color-primary-light-6); --color-overlay-backdrop: #080808c0; accent-color: var(--color-accent); color-scheme: light; @@ -260,9 +262,6 @@ border-radius: 0.28571429rem !important; overflow: hidden; } -.ui.secondary.vertical.menu > .item { - border-radius: 0 !important; -} .ui.basic.primary.button.item { background-color: var(--color-active) !important; color: var(--color-text) !important; diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index 626590ca54..c74f334c2d 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -65,7 +65,7 @@ --color-console-fg-subtle: #bec4c8; --color-console-bg: #171b1e; --color-console-border: #2e353b; - --color-console-hover-bg: #e8e8ff16; + --color-console-hover-bg: #292d31; --color-console-active-bg: #2e353b; --color-console-menu-bg: #252b30; --color-console-menu-border: #424b51; @@ -204,8 +204,9 @@ --color-active: #e8e8ff24; --color-menu: #151a1e; --color-card: #151a1e; - --color-markup-table-row: #e8e8ff06; - --color-markup-code-block: #e8e8ff16; + --color-markup-table-row: #e8e8ff0f; + --color-markup-code-block: #e8e8ff12; + --color-markup-code-inline: #e8e8ff28; --color-button: #151a1e; --color-code-bg: #14171a; --color-shadow: #00001758; @@ -214,8 +215,6 @@ --color-placeholder-text: var(--color-text-light-3); --color-editor-line-highlight: var(--color-primary-light-5); --color-project-board-bg: var(--color-secondary-light-2); - --color-project-board-dark-label: #0e1011; - --color-project-board-light-label: #dde0e2; --color-caret: var(--color-text); /* should ideally be --color-text-dark, see #15651 */ --color-reaction-bg: #e8e8ff12; --color-reaction-hover-bg: var(--color-primary-light-4); diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index f6913fbe22..01dd8ba4f7 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -63,12 +63,12 @@ /* console colors - used for actions console and console files */ --color-console-fg: #f8f8f9; --color-console-fg-subtle: #bec4c8; - --color-console-bg: #181b1d; - --color-console-border: #313538; - --color-console-hover-bg: #ffffff16; - --color-console-active-bg: #313538; - --color-console-menu-bg: #272b2e; - --color-console-menu-border: #464a4d; + --color-console-bg: #171b1e; + --color-console-border: #2e353b; + --color-console-hover-bg: #292d31; + --color-console-active-bg: #2e353b; + --color-console-menu-bg: #252b30; + --color-console-menu-border: #424b51; /* named colors */ --color-red: #db2828; --color-orange: #f2711c; @@ -204,8 +204,9 @@ --color-active: #00001714; --color-menu: #f8f9fb; --color-card: #f8f9fb; - --color-markup-table-row: #00001708; - --color-markup-code-block: #00001710; + --color-markup-table-row: #0030600a; + --color-markup-code-block: #00306010; + --color-markup-code-inline: #00306012; --color-button: #f8f9fb; --color-code-bg: #fafdff; --color-shadow: #00001726; @@ -214,8 +215,6 @@ --color-placeholder-text: var(--color-text-light-3); --color-editor-line-highlight: var(--color-primary-light-6); --color-project-board-bg: var(--color-secondary-light-4); - --color-project-board-dark-label: #0e1114; - --color-project-board-light-label: #eaeef2; --color-caret: var(--color-text-dark); --color-reaction-bg: #0000170a; --color-reaction-hover-bg: var(--color-primary-light-5); diff --git a/web_src/css/user.css b/web_src/css/user.css index 33ffa1eabc..e96598768b 100644 --- a/web_src/css/user.css +++ b/web_src/css/user.css @@ -157,3 +157,7 @@ .notifications-item:hover .notifications-updated { display: none; } + +#pronouns-dropdown, #pronouns-custom { + width: 140px; +} \ No newline at end of file diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css index 21c41a6161..49c00c4dad 100644 --- a/web_src/fomantic/build/semantic.css +++ b/web_src/fomantic/build/semantic.css @@ -2323,715 +2323,6 @@ Theme Overrides *******************************/ -/******************************* - Site Overrides -*******************************/ -/*! - * # Fomantic-UI - Checkbox - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Checkbox -*******************************/ - -/*-------------- - Content ----------------*/ - -.ui.checkbox { - position: relative; - display: inline-block; - backface-visibility: hidden; - outline: none; - vertical-align: baseline; - font-style: normal; - min-height: 17px; - font-size: 1em; - line-height: 17px; - min-width: 17px; -} - -/* HTML Checkbox */ - -.ui.checkbox input[type="checkbox"], -.ui.checkbox input[type="radio"] { - cursor: pointer; - position: absolute; - top: 0; - left: 0; - opacity: 0 !important; - outline: none; - z-index: 3; - width: 17px; - height: 17px; -} - -.ui.checkbox label { - cursor: auto; - position: relative; - display: block; - padding-left: 1.85714em; - outline: none; - font-size: 1em; -} - -.ui.checkbox label:before { - position: absolute; - top: 0; - left: 0; - width: 17px; - height: 17px; - content: ''; - background: #FFFFFF; - border-radius: 0.21428571rem; - transition: border 0.1s ease, opacity 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease; - border: 1px solid #D4D4D5; -} - -/*-------------- - Checkmark ----------------*/ - -.ui.checkbox label:after { - position: absolute; - font-size: 14px; - top: 0; - left: 0; - width: 17px; - height: 17px; - text-align: center; - opacity: 0; - color: rgba(0, 0, 0, 0.87); - transition: border 0.1s ease, opacity 0.1s ease, transform 0.1s ease, box-shadow 0.1s ease; -} - -/*-------------- - Label ----------------*/ - -/* Inside */ - -.ui.checkbox label, -.ui.checkbox + label { - color: rgba(0, 0, 0, 0.87); - transition: color 0.1s ease; -} - -/* Outside */ - -.ui.checkbox + label { - vertical-align: middle; -} - -/******************************* - States -*******************************/ - -/*-------------- - Hover ----------------*/ - -.ui.checkbox label:hover::before { - background: #FFFFFF; - border-color: rgba(34, 36, 38, 0.35); -} - -.ui.checkbox label:hover, -.ui.checkbox + label:hover { - color: rgba(0, 0, 0, 0.8); -} - -/*-------------- - Down ----------------*/ - -.ui.checkbox label:active::before { - background: #F9FAFB; - border-color: rgba(34, 36, 38, 0.35); -} - -.ui.checkbox label:active::after { - color: rgba(0, 0, 0, 0.95); -} - -.ui.checkbox input:active ~ label { - color: rgba(0, 0, 0, 0.95); -} - -/*-------------- - Focus ----------------*/ - -.ui.checkbox input:focus ~ label:before { - background: #FFFFFF; - border-color: #96C8DA; -} - -.ui.checkbox input:focus ~ label:after { - color: rgba(0, 0, 0, 0.95); -} - -.ui.checkbox input:focus ~ label { - color: rgba(0, 0, 0, 0.95); -} - -/*-------------- - Active ----------------*/ - -.ui.checkbox input:checked ~ label:before { - background: #FFFFFF; - border-color: rgba(34, 36, 38, 0.35); -} - -.ui.checkbox input:checked ~ label:after { - opacity: 1; - color: rgba(0, 0, 0, 0.95); -} - -/*-------------- - Indeterminate - ---------------*/ - -.ui.checkbox input:not([type=radio]):indeterminate ~ label:before { - background: #FFFFFF; - border-color: rgba(34, 36, 38, 0.35); -} - -.ui.checkbox input:not([type=radio]):indeterminate ~ label:after { - opacity: 1; - color: rgba(0, 0, 0, 0.95); -} - -.ui.indeterminate.toggle.checkbox input:not([type=radio]):indeterminate ~ label:before { - background: rgba(0, 0, 0, 0.15); -} - -.ui.indeterminate.toggle.checkbox input:not([type=radio]) ~ label:after { - left: 1.075rem; -} - -/*-------------- - Active Focus ----------------*/ - -.ui.checkbox input:not([type=radio]):indeterminate:focus ~ label:before, -.ui.checkbox input:checked:focus ~ label:before { - background: #FFFFFF; - border-color: #96C8DA; -} - -.ui.checkbox input:not([type=radio]):indeterminate:focus ~ label:after, -.ui.checkbox input:checked:focus ~ label:after { - color: rgba(0, 0, 0, 0.95); -} - -/*-------------- - Read-Only ----------------*/ - -.ui.read-only.checkbox, -.ui.read-only.checkbox label { - cursor: default; -} - -/*-------------- - Disabled - ---------------*/ - -.ui.disabled.checkbox label, -.ui.checkbox input[disabled] ~ label { - cursor: default !important; - opacity: 0.5; - color: #000000; - pointer-events: none; -} - -/*-------------- - Hidden ----------------*/ - -/* Initialized checkbox moves input below element - to prevent manually triggering */ - -.ui.checkbox input.hidden { - z-index: -1; -} - -/* Selectable Label */ - -.ui.checkbox input.hidden + label { - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} - -/******************************* - Types -*******************************/ - -/*-------------- - Radio - ---------------*/ - -.ui.radio.checkbox { - min-height: 15px; -} - -.ui.radio.checkbox label { - padding-left: 1.85714em; -} - -/* Box */ - -.ui.radio.checkbox label:before { - content: ''; - transform: none; - width: 15px; - height: 15px; - border-radius: 500rem; - top: 1px; - left: 0; -} - -/* Bullet */ - -.ui.radio.checkbox label:after { - border: none; - content: '' !important; - line-height: 15px; - top: 1px; - left: 0; - width: 15px; - height: 15px; - border-radius: 500rem; - transform: scale(0.46666667); - background-color: rgba(0, 0, 0, 0.87); -} - -/* Focus */ - -.ui.radio.checkbox input:focus ~ label:before { - background-color: #FFFFFF; -} - -.ui.radio.checkbox input:focus ~ label:after { - background-color: rgba(0, 0, 0, 0.95); -} - -/* Indeterminate */ - -.ui.radio.checkbox input:indeterminate ~ label:after { - opacity: 0; -} - -/* Active */ - -.ui.radio.checkbox input:checked ~ label:before { - background-color: #FFFFFF; -} - -.ui.radio.checkbox input:checked ~ label:after { - background-color: rgba(0, 0, 0, 0.95); -} - -/* Active Focus */ - -.ui.radio.checkbox input:focus:checked ~ label:before { - background-color: #FFFFFF; -} - -.ui.radio.checkbox input:focus:checked ~ label:after { - background-color: rgba(0, 0, 0, 0.95); -} - -/*-------------- - Slider - ---------------*/ - -.ui.slider.checkbox { - min-height: 1.25rem; -} - -/* Input */ - -.ui.slider.checkbox input { - width: 3.5rem; - height: 1.25rem; -} - -/* Label */ - -.ui.slider.checkbox label { - padding-left: 4.5rem; - line-height: 1rem; - color: rgba(0, 0, 0, 0.4); -} - -/* Line */ - -.ui.slider.checkbox label:before { - display: block; - position: absolute; - content: ''; - transform: none; - border: none !important; - left: 0; - z-index: 1; - top: 0.4rem; - background-color: rgba(0, 0, 0, 0.05); - width: 3.5rem; - height: 0.21428571rem; - border-radius: 500rem; - transition: background 0.3s ease; -} - -/* Handle */ - -.ui.slider.checkbox label:after { - background: #FFFFFF linear-gradient(transparent, rgba(0, 0, 0, 0.05)); - position: absolute; - content: '' !important; - opacity: 1; - z-index: 2; - border: none; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset; - width: 1.5rem; - height: 1.5rem; - top: -0.25rem; - left: 0; - transform: none; - border-radius: 500rem; - transition: left 0.3s ease; -} - -/* Focus */ - -.ui.slider.checkbox input:focus ~ label:before { - background-color: rgba(0, 0, 0, 0.15); - border: none; -} - -/* Hover */ - -.ui.slider.checkbox label:hover { - color: rgba(0, 0, 0, 0.8); -} - -.ui.slider.checkbox label:hover::before { - background: rgba(0, 0, 0, 0.15); -} - -/* Active */ - -.ui.slider.checkbox input:checked ~ label { - color: rgba(0, 0, 0, 0.95) !important; -} - -.ui.slider.checkbox input:checked ~ label:before { - background-color: #545454 !important; -} - -.ui.slider.checkbox input:checked ~ label:after { - left: 2rem; -} - -/* Active Focus */ - -.ui.slider.checkbox input:focus:checked ~ label { - color: rgba(0, 0, 0, 0.95) !important; -} - -.ui.slider.checkbox input:focus:checked ~ label:before { - background-color: #000000 !important; -} - -/*-------------- - Toggle - ---------------*/ - -.ui.toggle.checkbox { - min-height: 1.5rem; -} - -/* Input */ - -.ui.toggle.checkbox input { - width: 3.5rem; - height: 1.5rem; -} - -/* Label */ - -.ui.toggle.checkbox label { - min-height: 1.5rem; - padding-left: 4.5rem; - color: rgba(0, 0, 0, 0.87); -} - -.ui.toggle.checkbox label { - padding-top: 0.15em; -} - -/* Switch */ - -.ui.toggle.checkbox label:before { - display: block; - position: absolute; - content: ''; - z-index: 1; - transform: none; - border: none; - top: 0; - background: rgba(0, 0, 0, 0.05); - box-shadow: none; - width: 3.5rem; - height: 1.5rem; - border-radius: 500rem; -} - -/* Handle */ - -.ui.toggle.checkbox label:after { - background: #FFFFFF linear-gradient(transparent, rgba(0, 0, 0, 0.05)); - position: absolute; - content: '' !important; - opacity: 1; - z-index: 2; - border: none; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset; - width: 1.5rem; - height: 1.5rem; - top: 0; - left: 0; - border-radius: 500rem; - transition: background 0.3s ease, left 0.3s ease; -} - -.ui.toggle.checkbox input ~ label:after { - left: -0.05rem; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset; -} - -/* Focus */ - -.ui.toggle.checkbox input:focus ~ label:before { - background-color: rgba(0, 0, 0, 0.15); - border: none; -} - -/* Hover */ - -.ui.toggle.checkbox label:hover::before { - background-color: rgba(0, 0, 0, 0.15); - border: none; -} - -/* Active */ - -.ui.toggle.checkbox input:checked ~ label { - color: rgba(0, 0, 0, 0.95) !important; -} - -.ui.toggle.checkbox input:checked ~ label:before { - background-color: #2185D0 !important; -} - -.ui.toggle.checkbox input:checked ~ label:after { - left: 2.15rem; - box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15), 0 0 0 1px rgba(34, 36, 38, 0.15) inset; -} - -/* Active Focus */ - -.ui.toggle.checkbox input:focus:checked ~ label { - color: rgba(0, 0, 0, 0.95) !important; -} - -.ui.toggle.checkbox input:focus:checked ~ label:before { - background-color: #0d71bb !important; -} - -/******************************* - Variations -*******************************/ - -/*-------------- - Fitted - ---------------*/ - -.ui.fitted.checkbox label { - padding-left: 0 !important; -} - -.ui.fitted.toggle.checkbox { - width: 3.5rem; -} - -.ui.fitted.slider.checkbox { - width: 3.5rem; -} - -/*-------------------- - Size ----------------------*/ - -.ui.mini.checkbox { - font-size: 0.78571429em; -} - -.ui.tiny.checkbox { - font-size: 0.85714286em; -} - -.ui.small.checkbox { - font-size: 0.92857143em; -} - -.ui.large.checkbox { - font-size: 1.14285714em; -} - -.ui.large.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.large.checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.large.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before, -.ui.large.checkbox:not(.slider):not(.toggle):not(.radio) label:before { - transform: scale(1.14285714); - transform-origin: left; -} - -.ui.large.form .checkbox.radio label:before, -.ui.large.checkbox.radio label:before { - transform: scale(1.14285714); - transform-origin: left; -} - -.ui.large.form .checkbox.radio label:after, -.ui.large.checkbox.radio label:after { - transform: scale(0.57142857); - transform-origin: left; - left: 0.33571429em; -} - -.ui.big.checkbox { - font-size: 1.28571429em; -} - -.ui.big.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.big.checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.big.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before, -.ui.big.checkbox:not(.slider):not(.toggle):not(.radio) label:before { - transform: scale(1.28571429); - transform-origin: left; -} - -.ui.big.form .checkbox.radio label:before, -.ui.big.checkbox.radio label:before { - transform: scale(1.28571429); - transform-origin: left; -} - -.ui.big.form .checkbox.radio label:after, -.ui.big.checkbox.radio label:after { - transform: scale(0.64285714); - transform-origin: left; - left: 0.37142857em; -} - -.ui.huge.checkbox { - font-size: 1.42857143em; -} - -.ui.huge.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.huge.checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.huge.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before, -.ui.huge.checkbox:not(.slider):not(.toggle):not(.radio) label:before { - transform: scale(1.42857143); - transform-origin: left; -} - -.ui.huge.form .checkbox.radio label:before, -.ui.huge.checkbox.radio label:before { - transform: scale(1.42857143); - transform-origin: left; -} - -.ui.huge.form .checkbox.radio label:after, -.ui.huge.checkbox.radio label:after { - transform: scale(0.71428571); - transform-origin: left; - left: 0.40714286em; -} - -.ui.massive.checkbox { - font-size: 1.71428571em; -} - -.ui.massive.form .checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.massive.checkbox:not(.slider):not(.toggle):not(.radio) label:after, -.ui.massive.form .checkbox:not(.slider):not(.toggle):not(.radio) label:before, -.ui.massive.checkbox:not(.slider):not(.toggle):not(.radio) label:before { - transform: scale(1.71428571); - transform-origin: left; -} - -.ui.massive.form .checkbox.radio label:before, -.ui.massive.checkbox.radio label:before { - transform: scale(1.71428571); - transform-origin: left; -} - -.ui.massive.form .checkbox.radio label:after, -.ui.massive.checkbox.radio label:after { - transform: scale(0.85714286); - transform-origin: left; - left: 0.47857143em; -} - -/******************************* - Theme Overrides -*******************************/ - -@font-face { - font-family: 'Checkbox'; - src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBD8AAAC8AAAAYGNtYXAYVtCJAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5Zn4huwUAAAF4AAABYGhlYWQGPe1ZAAAC2AAAADZoaGVhB30DyAAAAxAAAAAkaG10eBBKAEUAAAM0AAAAHGxvY2EAmgESAAADUAAAABBtYXhwAAkALwAAA2AAAAAgbmFtZSC8IugAAAOAAAABknBvc3QAAwAAAAAFFAAAACAAAwMTAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADoAgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6AL//f//AAAAAAAg6AD//f//AAH/4xgEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAEUAUQO7AvgAGgAAARQHAQYjIicBJjU0PwE2MzIfAQE2MzIfARYVA7sQ/hQQFhcQ/uMQEE4QFxcQqAF2EBcXEE4QAnMWEP4UEBABHRAXFhBOEBCoAXcQEE4QFwAAAAABAAABbgMlAkkAFAAAARUUBwYjISInJj0BNDc2MyEyFxYVAyUQEBf9SRcQEBAQFwK3FxAQAhJtFxAQEBAXbRcQEBAQFwAAAAABAAAASQMlA24ALAAAARUUBwYrARUUBwYrASInJj0BIyInJj0BNDc2OwE1NDc2OwEyFxYdATMyFxYVAyUQEBfuEBAXbhYQEO4XEBAQEBfuEBAWbhcQEO4XEBACEm0XEBDuFxAQEBAX7hAQF20XEBDuFxAQEBAX7hAQFwAAAQAAAAIAAHRSzT9fDzz1AAsEAAAAAADRsdR3AAAAANGx1HcAAAAAA7sDbgAAAAgAAgAAAAAAAAABAAADwP/AAAAEAAAAAAADuwABAAAAAAAAAAAAAAAAAAAABwQAAAAAAAAAAAAAAAIAAAAEAABFAyUAAAMlAAAAAAAAAAoAFAAeAE4AcgCwAAEAAAAHAC0AAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAIAAAAAQAAAAAAAgAHAGkAAQAAAAAAAwAIADkAAQAAAAAABAAIAH4AAQAAAAAABQALABgAAQAAAAAABgAIAFEAAQAAAAAACgAaAJYAAwABBAkAAQAQAAgAAwABBAkAAgAOAHAAAwABBAkAAwAQAEEAAwABBAkABAAQAIYAAwABBAkABQAWACMAAwABBAkABgAQAFkAAwABBAkACgA0ALBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhWZXJzaW9uIDIuMABWAGUAcgBzAGkAbwBuACAAMgAuADBDaGVja2JveABDAGgAZQBjAGsAYgBvAHhDaGVja2JveABDAGgAZQBjAGsAYgBvAHhSZWd1bGFyAFIAZQBnAHUAbABhAHJDaGVja2JveABDAGgAZQBjAGsAYgBvAHhGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('truetype'); -} - -/* Checkmark */ - -.ui.checkbox label:after, -.ui.checkbox .box:after { - font-family: 'Checkbox'; -} - -/* Checked */ - -.ui.checkbox input:checked ~ .box:after, -.ui.checkbox input:checked ~ label:after { - content: '\e800'; -} - -/* Indeterminate */ - -.ui.checkbox input:indeterminate ~ .box:after, -.ui.checkbox input:indeterminate ~ label:after { - font-size: 12px; - content: '\e801'; -} - -/* UTF Reference -.check:before { content: '\e800'; } -.dash:before { content: '\e801'; } -.plus:before { content: '\e802'; } -*/ - /******************************* Site Overrides *******************************/ @@ -7186,1727 +6477,6 @@ select.ui.dropdown { /******************************* Site Overrides *******************************/ -/*! - * # Fomantic-UI - Input - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - Standard -*******************************/ - -/*-------------------- - Inputs ----------------------*/ - -.ui.input { - position: relative; - font-weight: normal; - font-style: normal; - display: inline-flex; - color: rgba(0, 0, 0, 0.87); -} - -.ui.input > input { - margin: 0; - max-width: 100%; - flex: 1 0 auto; - outline: none; - -webkit-tap-highlight-color: rgba(255, 255, 255, 0); - text-align: left; - line-height: 1.21428571em; - font-family: var(--fonts-regular); - padding: 0.67857143em 1em; - background: #FFFFFF; - border: 1px solid rgba(34, 36, 38, 0.15); - color: rgba(0, 0, 0, 0.87); - border-radius: 0.28571429rem; - transition: box-shadow 0.1s ease, border-color 0.1s ease; - box-shadow: none; -} - -/*-------------------- - Placeholder ----------------------*/ - -/* browsers require these rules separate */ - -.ui.input > input::-webkit-input-placeholder { - color: rgba(191, 191, 191, 0.87); -} - -.ui.input > input::-moz-placeholder { - color: rgba(191, 191, 191, 0.87); -} - -.ui.input > input:-ms-input-placeholder { - color: rgba(191, 191, 191, 0.87); -} - -/******************************* - States -*******************************/ - -/*-------------------- - Disabled - ---------------------*/ - -.ui.disabled.input, -.ui.input:not(.disabled) input[disabled] { - opacity: var(--opacity-disabled); -} - -.ui.disabled.input > input, -.ui.input:not(.disabled) input[disabled] { - pointer-events: none; -} - -/*-------------------- - Active ----------------------*/ - -.ui.input > input:active, -.ui.input.down input { - border-color: rgba(0, 0, 0, 0.3); - background: #FAFAFA; - color: rgba(0, 0, 0, 0.87); - box-shadow: none; -} - -/*-------------------- - Loading - ---------------------*/ - -.ui.loading.loading.input > i.icon:before { - position: absolute; - content: ''; - top: 50%; - left: 50%; - margin: -0.64285714em 0 0 -0.64285714em; - width: 1.28571429em; - height: 1.28571429em; - border-radius: 500rem; - border: 0.2em solid rgba(0, 0, 0, 0.1); -} - -.ui.loading.loading.input > i.icon:after { - position: absolute; - content: ''; - top: 50%; - left: 50%; - margin: -0.64285714em 0 0 -0.64285714em; - width: 1.28571429em; - height: 1.28571429em; - animation: loader 0.6s infinite linear; - border: 0.2em solid #767676; - border-radius: 500rem; - box-shadow: 0 0 0 1px transparent; -} - -/*-------------------- - Focus ----------------------*/ - -.ui.input.focus > input, -.ui.input > input:focus { - border-color: #85B7D9; - background: #FFFFFF; - color: rgba(0, 0, 0, 0.8); - box-shadow: none; -} - -.ui.input.focus > input::-webkit-input-placeholder, -.ui.input > input:focus::-webkit-input-placeholder { - color: rgba(115, 115, 115, 0.87); -} - -.ui.input.focus > input::-moz-placeholder, -.ui.input > input:focus::-moz-placeholder { - color: rgba(115, 115, 115, 0.87); -} - -.ui.input.focus > input:-ms-input-placeholder, -.ui.input > input:focus:-ms-input-placeholder { - color: rgba(115, 115, 115, 0.87); -} - -/*-------------------- - States - ---------------------*/ - -.ui.input.error > input { - background-color: #FFF6F6; - border-color: #E0B4B4; - color: #9F3A38; - box-shadow: none; -} - -/* Placeholder */ - -.ui.input.error > input::-webkit-input-placeholder { - color: #e7bdbc; -} - -.ui.input.error > input::-moz-placeholder { - color: #e7bdbc; -} - -.ui.input.error > input:-ms-input-placeholder { - color: #e7bdbc !important; -} - -/* Focused Placeholder */ - -.ui.input.error > input:focus::-webkit-input-placeholder { - color: #da9796; -} - -.ui.input.error > input:focus::-moz-placeholder { - color: #da9796; -} - -.ui.input.error > input:focus:-ms-input-placeholder { - color: #da9796 !important; -} - -.ui.input.info > input { - background-color: #F8FFFF; - border-color: #A9D5DE; - color: #276F86; - box-shadow: none; -} - -/* Placeholder */ - -.ui.input.info > input::-webkit-input-placeholder { - color: #98cfe1; -} - -.ui.input.info > input::-moz-placeholder { - color: #98cfe1; -} - -.ui.input.info > input:-ms-input-placeholder { - color: #98cfe1 !important; -} - -/* Focused Placeholder */ - -.ui.input.info > input:focus::-webkit-input-placeholder { - color: #70bdd6; -} - -.ui.input.info > input:focus::-moz-placeholder { - color: #70bdd6; -} - -.ui.input.info > input:focus:-ms-input-placeholder { - color: #70bdd6 !important; -} - -.ui.input.success > input { - background-color: #FCFFF5; - border-color: #A3C293; - color: #2C662D; - box-shadow: none; -} - -/* Placeholder */ - -.ui.input.success > input::-webkit-input-placeholder { - color: #8fcf90; -} - -.ui.input.success > input::-moz-placeholder { - color: #8fcf90; -} - -.ui.input.success > input:-ms-input-placeholder { - color: #8fcf90 !important; -} - -/* Focused Placeholder */ - -.ui.input.success > input:focus::-webkit-input-placeholder { - color: #6cbf6d; -} - -.ui.input.success > input:focus::-moz-placeholder { - color: #6cbf6d; -} - -.ui.input.success > input:focus:-ms-input-placeholder { - color: #6cbf6d !important; -} - -.ui.input.warning > input { - background-color: #FFFAF3; - border-color: #C9BA9B; - color: #573A08; - box-shadow: none; -} - -/* Placeholder */ - -.ui.input.warning > input::-webkit-input-placeholder { - color: #edad3e; -} - -.ui.input.warning > input::-moz-placeholder { - color: #edad3e; -} - -.ui.input.warning > input:-ms-input-placeholder { - color: #edad3e !important; -} - -/* Focused Placeholder */ - -.ui.input.warning > input:focus::-webkit-input-placeholder { - color: #e39715; -} - -.ui.input.warning > input:focus::-moz-placeholder { - color: #e39715; -} - -.ui.input.warning > input:focus:-ms-input-placeholder { - color: #e39715 !important; -} - -/******************************* - Variations -*******************************/ - -/*-------------------- - Transparent - ---------------------*/ - -.ui.transparent.input > textarea, -.ui.transparent.input > input { - border-color: transparent !important; - background-color: transparent !important; - padding: 0; - box-shadow: none !important; - border-radius: 0 !important; -} - -.field .ui.transparent.input > textarea { - padding: 0.67857143em 1em; -} - -/* Transparent Icon */ - -:not(.field) > .ui.transparent.icon.input > i.icon { - width: 1.1em; -} - -:not(.field) > .ui.ui.ui.transparent.icon.input > input { - padding-left: 0; - padding-right: 2em; -} - -:not(.field) > .ui.ui.ui.transparent[class*="left icon"].input > input { - padding-left: 2em; - padding-right: 0; -} - -/*-------------------- - Icon - ---------------------*/ - -.ui.icon.input > i.icon { - cursor: default; - position: absolute; - line-height: 1; - text-align: center; - top: 0; - right: 0; - margin: 0; - height: 100%; - width: 2.67142857em; - opacity: 0.5; - border-radius: 0 0.28571429rem 0.28571429rem 0; - transition: opacity 0.3s ease; -} - -.ui.icon.input > i.icon:not(.link) { - pointer-events: none; -} - -.ui.ui.ui.ui.icon.input > textarea, -.ui.ui.ui.ui.icon.input > input { - padding-right: 2.67142857em; -} - -.ui.icon.input > i.icon:before, -.ui.icon.input > i.icon:after { - left: 0; - position: absolute; - text-align: center; - top: 50%; - width: 100%; - margin-top: -0.5em; -} - -.ui.icon.input > i.link.icon { - cursor: pointer; -} - -.ui.icon.input > i.circular.icon { - top: 0.35em; - right: 0.5em; -} - -/* Left Icon Input */ - -.ui[class*="left icon"].input > i.icon { - right: auto; - left: 1px; - border-radius: 0.28571429rem 0 0 0.28571429rem; -} - -.ui[class*="left icon"].input > i.circular.icon { - right: auto; - left: 0.5em; -} - -.ui.ui.ui.ui[class*="left icon"].input > textarea, -.ui.ui.ui.ui[class*="left icon"].input > input { - padding-left: 2.67142857em; - padding-right: 1em; -} - -/* Focus */ - -.ui.icon.input > textarea:focus ~ i.icon, -.ui.icon.input > input:focus ~ i.icon { - opacity: 1; -} - -/*-------------------- - Labeled - ---------------------*/ - -/* Adjacent Label */ - -.ui.labeled.input > .label { - flex: 0 0 auto; - margin: 0; - font-size: 1em; -} - -.ui.labeled.input > .label:not(.corner) { - padding-top: 0.78571429em; - padding-bottom: 0.78571429em; -} - -/* Regular Label on Left */ - -.ui.labeled.input:not([class*="corner labeled"]) .label:first-child { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -.ui.labeled.input:not([class*="corner labeled"]) .label:first-child + input { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left-color: transparent; -} - -.ui.labeled.input:not([class*="corner labeled"]) .label:first-child + input:focus { - border-left-color: #85B7D9; -} - -/* Regular Label on Right */ - -.ui[class*="right labeled"].input > input { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; - border-right-color: transparent !important; -} - -.ui[class*="right labeled"].input > input + .label { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.ui[class*="right labeled"].input > input:focus { - border-right-color: #85B7D9 !important; -} - -/* Corner Label */ - -.ui.labeled.input .corner.label { - top: 1px; - right: 1px; - font-size: 0.64285714em; - border-radius: 0 0.28571429rem 0 0; -} - -/* Spacing with corner label */ - -.ui[class*="corner labeled"]:not([class*="left corner labeled"]).labeled.input > textarea, -.ui[class*="corner labeled"]:not([class*="left corner labeled"]).labeled.input > input { - padding-right: 2.5em !important; -} - -.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > textarea, -.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > input { - padding-right: 3.25em !important; -} - -.ui[class*="corner labeled"].icon.input:not([class*="left corner labeled"]) > i.icon { - margin-right: 1.25em; -} - -/* Left Labeled */ - -.ui[class*="left corner labeled"].labeled.input > textarea, -.ui[class*="left corner labeled"].labeled.input > input { - padding-left: 2.5em !important; -} - -.ui[class*="left corner labeled"].icon.input > textarea, -.ui[class*="left corner labeled"].icon.input > input { - padding-left: 3.25em !important; -} - -.ui[class*="left corner labeled"].icon.input > i.icon { - margin-left: 1.25em; -} - -.ui.icon.input > textarea ~ i.icon { - height: 3em; -} - -:not(.field) > .ui.transparent.icon.input > textarea ~ i.icon { - height: 1.3em; -} - -/* Corner Label Position */ - -.ui.input > .ui.corner.label { - top: 1px; - right: 1px; -} - -.ui.input > .ui.left.corner.label { - right: auto; - left: 1px; -} - -/* Labeled and action input states */ - -.ui.form .field.error > .ui.action.input > .ui.button, -.ui.form .field.error > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label, -.ui.action.input.error > .ui.button, -.ui.labeled.input.error:not([class*="corner labeled"]) > .ui.label { - border-top: 1px solid #E0B4B4; - border-bottom: 1px solid #E0B4B4; -} - -.ui.form .field.error > .ui[class*="left action"].input > .ui.button, -.ui.form .field.error > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label, -.ui[class*="left action"].input.error > .ui.button, -.ui.labeled.input.error:not(.right):not([class*="corner labeled"]) > .ui.label { - border-left: 1px solid #E0B4B4; -} - -.ui.form .field.error > .ui.action.input:not([class*="left action"]) > input + .ui.button, -.ui.form .field.error > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label, -.ui.action.input.error:not([class*="left action"]) > input + .ui.button, -.ui.right.labeled.input.error:not([class*="corner labeled"]) > input + .ui.label { - border-right: 1px solid #E0B4B4; -} - -.ui.form .field.error > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child, -.ui.right.labeled.input.error:not([class*="corner labeled"]) > .ui.label:first-child { - border-left: 1px solid #E0B4B4; -} - -.ui.form .field.info > .ui.action.input > .ui.button, -.ui.form .field.info > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label, -.ui.action.input.info > .ui.button, -.ui.labeled.input.info:not([class*="corner labeled"]) > .ui.label { - border-top: 1px solid #A9D5DE; - border-bottom: 1px solid #A9D5DE; -} - -.ui.form .field.info > .ui[class*="left action"].input > .ui.button, -.ui.form .field.info > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label, -.ui[class*="left action"].input.info > .ui.button, -.ui.labeled.input.info:not(.right):not([class*="corner labeled"]) > .ui.label { - border-left: 1px solid #A9D5DE; -} - -.ui.form .field.info > .ui.action.input:not([class*="left action"]) > input + .ui.button, -.ui.form .field.info > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label, -.ui.action.input.info:not([class*="left action"]) > input + .ui.button, -.ui.right.labeled.input.info:not([class*="corner labeled"]) > input + .ui.label { - border-right: 1px solid #A9D5DE; -} - -.ui.form .field.info > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child, -.ui.right.labeled.input.info:not([class*="corner labeled"]) > .ui.label:first-child { - border-left: 1px solid #A9D5DE; -} - -.ui.form .field.success > .ui.action.input > .ui.button, -.ui.form .field.success > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label, -.ui.action.input.success > .ui.button, -.ui.labeled.input.success:not([class*="corner labeled"]) > .ui.label { - border-top: 1px solid #A3C293; - border-bottom: 1px solid #A3C293; -} - -.ui.form .field.success > .ui[class*="left action"].input > .ui.button, -.ui.form .field.success > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label, -.ui[class*="left action"].input.success > .ui.button, -.ui.labeled.input.success:not(.right):not([class*="corner labeled"]) > .ui.label { - border-left: 1px solid #A3C293; -} - -.ui.form .field.success > .ui.action.input:not([class*="left action"]) > input + .ui.button, -.ui.form .field.success > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label, -.ui.action.input.success:not([class*="left action"]) > input + .ui.button, -.ui.right.labeled.input.success:not([class*="corner labeled"]) > input + .ui.label { - border-right: 1px solid #A3C293; -} - -.ui.form .field.success > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child, -.ui.right.labeled.input.success:not([class*="corner labeled"]) > .ui.label:first-child { - border-left: 1px solid #A3C293; -} - -.ui.form .field.warning > .ui.action.input > .ui.button, -.ui.form .field.warning > .ui.labeled.input:not([class*="corner labeled"]) > .ui.label, -.ui.action.input.warning > .ui.button, -.ui.labeled.input.warning:not([class*="corner labeled"]) > .ui.label { - border-top: 1px solid #C9BA9B; - border-bottom: 1px solid #C9BA9B; -} - -.ui.form .field.warning > .ui[class*="left action"].input > .ui.button, -.ui.form .field.warning > .ui.labeled.input:not(.right):not([class*="corner labeled"]) > .ui.label, -.ui[class*="left action"].input.warning > .ui.button, -.ui.labeled.input.warning:not(.right):not([class*="corner labeled"]) > .ui.label { - border-left: 1px solid #C9BA9B; -} - -.ui.form .field.warning > .ui.action.input:not([class*="left action"]) > input + .ui.button, -.ui.form .field.warning > .ui.right.labeled.input:not([class*="corner labeled"]) > input + .ui.label, -.ui.action.input.warning:not([class*="left action"]) > input + .ui.button, -.ui.right.labeled.input.warning:not([class*="corner labeled"]) > input + .ui.label { - border-right: 1px solid #C9BA9B; -} - -.ui.form .field.warning > .ui.right.labeled.input:not([class*="corner labeled"]) > .ui.label:first-child, -.ui.right.labeled.input.warning:not([class*="corner labeled"]) > .ui.label:first-child { - border-left: 1px solid #C9BA9B; -} - -/*-------------------- - Action - ---------------------*/ - -.ui.action.input > .button, -.ui.action.input > .buttons { - display: flex; - align-items: center; - flex: 0 0 auto; -} - -.ui.action.input > .button, -.ui.action.input > .buttons > .button { - padding-top: 0.78571429em; - padding-bottom: 0.78571429em; - margin: 0; -} - -/* Input when ui Left*/ - -.ui[class*="left action"].input > input { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left-color: transparent; -} - -/* Input when ui Right*/ - -.ui.action.input:not([class*="left action"]) > input { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-right-color: transparent; -} - -/* Button and Dropdown */ - -.ui.action.input > .dropdown:first-child, -.ui.action.input > .button:first-child, -.ui.action.input > .buttons:first-child > .button { - border-radius: 0.28571429rem 0 0 0.28571429rem; -} - -.ui.action.input > .dropdown:not(:first-child), -.ui.action.input > .button:not(:first-child), -.ui.action.input > .buttons:not(:first-child) > .button { - border-radius: 0; -} - -.ui.action.input > .dropdown:last-child, -.ui.action.input > .button:last-child, -.ui.action.input > .buttons:last-child > .button { - border-radius: 0 0.28571429rem 0.28571429rem 0; -} - -/* Input Focus */ - -.ui.action.input:not([class*="left action"]) > input:focus { - border-right-color: #85B7D9; -} - -.ui.ui[class*="left action"].input > input:focus { - border-left-color: #85B7D9; -} - -/*-------------------- - Fluid - ---------------------*/ - -.ui.fluid.input { - display: flex; -} - -.ui.fluid.input > input { - width: 0 !important; -} - -/*-------------------- - Size ----------------------*/ - -.ui.input { - font-size: 1em; -} - -.ui.mini.input { - font-size: 0.78571429em; -} - -.ui.tiny.input { - font-size: 0.85714286em; -} - -.ui.small.input { - font-size: 0.92857143em; -} - -.ui.large.input { - font-size: 1.14285714em; -} - -.ui.big.input { - font-size: 1.28571429em; -} - -.ui.huge.input { - font-size: 1.42857143em; -} - -.ui.massive.input { - font-size: 1.71428571em; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - Site Overrides -*******************************/ -/*! - * # Fomantic-UI - List - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -/******************************* - List -*******************************/ - -ul.ui.list, -ol.ui.list, -.ui.list { - list-style-type: none; - margin: 1em 0; - padding: 0 0; -} - -ul.ui.list:first-child, -ol.ui.list:first-child, -.ui.list:first-child { - margin-top: 0; - padding-top: 0; -} - -ul.ui.list:last-child, -ol.ui.list:last-child, -.ui.list:last-child { - margin-bottom: 0; - padding-bottom: 0; -} - -/******************************* - Content -*******************************/ - -/* List Item */ - -ul.ui.list li, -ol.ui.list li, -.ui.list > .item, -.ui.list .list > .item { - display: list-item; - table-layout: fixed; - list-style-type: none; - list-style-position: outside; - padding: 0.21428571em 0; - line-height: 1.14285714em; -} - -ul.ui.list > li:first-child:after, -ol.ui.list > li:first-child:after, -.ui.list > .list > .item:after, -.ui.list > .item:after { - content: ''; - display: block; - height: 0; - clear: both; - visibility: hidden; -} - -ul.ui.list li:first-child, -ol.ui.list li:first-child, -.ui.list .list > .item:first-child, -.ui.list > .item:first-child { - padding-top: 0; -} - -ul.ui.list li:last-child, -ol.ui.list li:last-child, -.ui.list .list > .item:last-child, -.ui.list > .item:last-child { - padding-bottom: 0; -} - -/* Child List */ - -ul.ui.list ul, -ol.ui.list ol, -.ui.list .list:not(.icon) { - clear: both; - margin: 0; - padding: 0.75em 0 0.25em 0.5em; -} - -/* Child Item */ - -ul.ui.list ul li, -ol.ui.list ol li, -.ui.list .list > .item { - padding: 0.14285714em 0; - line-height: inherit; -} - -/* Icon */ - -.ui.list .list > .item > i.icon, -.ui.list > .item > i.icon { - display: table-cell; - min-width: 1.55em; - margin: 0; - padding-top: 0; - transition: color 0.1s ease; -} - -.ui.list .list > .item > i.icon:not(.loading), -.ui.list > .item > i.icon:not(.loading) { - padding-right: 0.28571429em; - vertical-align: top; -} - -.ui.list .list > .item > i.icon:only-child, -.ui.list > .item > i.icon:only-child { - display: inline-block; - min-width: auto; - vertical-align: top; -} - -/* Image */ - -.ui.list .list > .item > .image, -.ui.list > .item > .image { - display: table-cell; - background-color: transparent; - margin: 0; - vertical-align: top; -} - -.ui.list .list > .item > .image:not(:only-child):not(img), -.ui.list > .item > .image:not(:only-child):not(img) { - padding-right: 0.5em; -} - -.ui.list .list > .item > .image img, -.ui.list > .item > .image img { - vertical-align: top; -} - -.ui.list .list > .item > img.image, -.ui.list .list > .item > .image:only-child, -.ui.list > .item > img.image, -.ui.list > .item > .image:only-child { - display: inline-block; -} - -/* Content */ - -.ui.list .list > .item > .content, -.ui.list > .item > .content { - line-height: 1.14285714em; - color: rgba(0, 0, 0, 0.87); -} - -.ui.list .list > .item > .image + .content, -.ui.list .list > .item > i.icon + .content, -.ui.list > .item > .image + .content, -.ui.list > .item > i.icon + .content { - display: table-cell; - width: 100%; - padding: 0 0 0 0.5em; - vertical-align: top; -} - -.ui.list .list > .item > i.loading.icon + .content, -.ui.list > .item > i.loading.icon + .content { - padding-left: calc(0.2857142857142857em + 0.5em); -} - -.ui.list .list > .item > img.image + .content, -.ui.list > .item > img.image + .content { - display: inline-block; - width: auto; -} - -.ui.list .list > .item > .content > .list, -.ui.list > .item > .content > .list { - margin-left: 0; - padding-left: 0; -} - -/* Header */ - -.ui.list .list > .item .header, -.ui.list > .item .header { - display: block; - margin: 0; - font-family: var(--fonts-regular); - font-weight: 500; - color: rgba(0, 0, 0, 0.87); -} - -/* Description */ - -.ui.list .list > .item .description, -.ui.list > .item .description { - display: block; - color: rgba(0, 0, 0, 0.7); -} - -/* Child Link */ - -.ui.list > .item a, -.ui.list .list > .item a { - cursor: pointer; -} - -/* Linking Item */ - -.ui.list .list > a.item, -.ui.list > a.item { - cursor: pointer; - color: #4183C4; -} - -.ui.list .list > a.item:hover, -.ui.list > a.item:hover { - color: #1e70bf; -} - -/* Linked Item Icons */ - -.ui.list .list > a.item > i.icons, -.ui.list > a.item > i.icons, -.ui.list .list > a.item > i.icon, -.ui.list > a.item > i.icon { - color: rgba(0, 0, 0, 0.4); -} - -/* Header Link */ - -.ui.list .list > .item a.header, -.ui.list > .item a.header { - cursor: pointer; - color: #4183C4 !important; -} - -.ui.list .list > .item > a.header:hover, -.ui.list > .item > a.header:hover { - color: #1e70bf !important; -} - -/* Floated Content */ - -.ui[class*="left floated"].list { - float: left; -} - -.ui[class*="right floated"].list { - float: right; -} - -.ui.list .list > .item [class*="left floated"], -.ui.list > .item [class*="left floated"] { - float: left; - margin: 0 1em 0 0; -} - -.ui.list .list > .item [class*="right floated"], -.ui.list > .item [class*="right floated"] { - float: right; - margin: 0 0 0 1em; -} - -/******************************* - Coupling -*******************************/ - -.ui.menu .ui.list > .item, -.ui.menu .ui.list .list > .item { - display: list-item; - table-layout: fixed; - background-color: transparent; - list-style-type: none; - list-style-position: outside; - padding: 0.21428571em 0; - line-height: 1.14285714em; -} - -.ui.menu .ui.list .list > .item:before, -.ui.menu .ui.list > .item:before { - border: none; - background: none; -} - -.ui.menu .ui.list .list > .item:first-child, -.ui.menu .ui.list > .item:first-child { - padding-top: 0; -} - -.ui.menu .ui.list .list > .item:last-child, -.ui.menu .ui.list > .item:last-child { - padding-bottom: 0; -} - -/******************************* - Types -*******************************/ - -/*------------------- - Horizontal - --------------------*/ - -.ui.horizontal.list { - display: inline-block; - font-size: 0; -} - -.ui.horizontal.list > .item { - display: inline-block; - margin-right: 1em; - font-size: 1rem; -} - -.ui.horizontal.list:not(.celled) > .item:last-child { - margin-right: 0; - padding-right: 0; -} - -.ui.horizontal.list .list:not(.icon) { - padding-left: 0; - padding-bottom: 0; -} - -.ui.horizontal.list > .item > .image, -.ui.horizontal.list .list > .item > .image, -.ui.horizontal.list > .item > i.icon, -.ui.horizontal.list .list > .item > i.icon, -.ui.horizontal.list > .item > .content, -.ui.horizontal.list .list > .item > .content { - vertical-align: middle; -} - -/* Padding on all elements */ - -.ui.horizontal.list > .item:first-child, -.ui.horizontal.list > .item:last-child { - padding-top: 0.21428571em; - padding-bottom: 0.21428571em; -} - -/* Horizontal List */ - -.ui.horizontal.list > .item > i.icon, -.ui.horizontal.list .item > i.icons > i.icon { - margin: 0; - padding: 0 0.25em 0 0; -} - -.ui.horizontal.list > .item > .image + .content, -.ui.horizontal.list > .item > i.icon, -.ui.horizontal.list > .item > i.icon + .content { - float: none; - display: inline-block; - width: auto; -} - -.ui.horizontal.list > .item > .image { - display: inline-block; -} - -/******************************* - States -*******************************/ - -/*------------------- - Disabled - --------------------*/ - -.ui.list .list > .disabled.item, -.ui.list > .disabled.item { - pointer-events: none; - color: rgba(40, 40, 40, 0.3) !important; -} - -/*------------------- - Hover ---------------------*/ - -.ui.list .list > a.item:hover > .icons, -.ui.list > a.item:hover > .icons, -.ui.list .list > a.item:hover > i.icon, -.ui.list > a.item:hover > i.icon { - color: rgba(0, 0, 0, 0.87); -} - -/******************************* - Variations -*******************************/ - -/*------------------- - Aligned - --------------------*/ - -.ui.list[class*="top aligned"] .image, -.ui.list[class*="top aligned"] .content, -.ui.list [class*="top aligned"] { - vertical-align: top !important; -} - -.ui.list[class*="middle aligned"] .image, -.ui.list[class*="middle aligned"] .content, -.ui.list [class*="middle aligned"] { - vertical-align: middle !important; -} - -.ui.list[class*="bottom aligned"] .image, -.ui.list[class*="bottom aligned"] .content, -.ui.list [class*="bottom aligned"] { - vertical-align: bottom !important; -} - -/*------------------- - Link - --------------------*/ - -.ui.link.list .item, -.ui.link.list a.item, -.ui.link.list .item a:not(.ui) { - color: rgba(0, 0, 0, 0.4); - transition: 0.1s color ease; -} - -.ui.link.list.list a.item:hover, -.ui.link.list.list .item a:not(.ui):hover { - color: rgba(0, 0, 0, 0.8); -} - -.ui.link.list.list a.item:active, -.ui.link.list.list .item a:not(.ui):active { - color: rgba(0, 0, 0, 0.9); -} - -.ui.link.list.list .active.item, -.ui.link.list.list .active.item a:not(.ui) { - color: rgba(0, 0, 0, 0.95); -} - -/*------------------- - Selection - --------------------*/ - -.ui.selection.list .list > .item, -.ui.selection.list > .item { - cursor: pointer; - background: transparent; - padding: 0.5em 0.5em; - margin: 0; - color: rgba(0, 0, 0, 0.4); - border-radius: 0.5em; - transition: 0.1s color ease, 0.1s padding-left ease, 0.1s background-color ease; -} - -.ui.selection.list .list > .item:last-child, -.ui.selection.list > .item:last-child { - margin-bottom: 0; -} - -.ui.selection.list .list > .item:hover, -.ui.selection.list > .item:hover { - background: rgba(0, 0, 0, 0.03); - color: rgba(0, 0, 0, 0.8); -} - -.ui.selection.list .list > .item:active, -.ui.selection.list > .item:active { - background: rgba(0, 0, 0, 0.05); - color: rgba(0, 0, 0, 0.9); -} - -.ui.selection.list .list > .item.active, -.ui.selection.list > .item.active { - background: rgba(0, 0, 0, 0.05); - color: rgba(0, 0, 0, 0.95); -} - -/* Celled / Divided Selection List */ - -.ui.celled.selection.list .list > .item, -.ui.divided.selection.list .list > .item, -.ui.celled.selection.list > .item, -.ui.divided.selection.list > .item { - border-radius: 0; -} - -/*------------------- - Animated - --------------------*/ - -.ui.animated.list > .item { - transition: 0.25s color ease 0.1s, 0.25s padding-left ease 0.1s, 0.25s background-color ease 0.1s; -} - -.ui.animated.list:not(.horizontal) > .item:hover { - padding-left: 1em; -} - -/*------------------- - Fitted - --------------------*/ - -.ui.fitted.list:not(.selection) .list > .item, -.ui.fitted.list:not(.selection) > .item { - padding-left: 0; - padding-right: 0; -} - -.ui.fitted.selection.list .list > .item, -.ui.fitted.selection.list > .item { - margin-left: -0.5em; - margin-right: -0.5em; -} - -/*------------------- - Bulleted - --------------------*/ - -ul.ui.list, -.ui.bulleted.list { - margin-left: 1.25rem; -} - -ul.ui.list li, -.ui.bulleted.list .list > .item, -.ui.bulleted.list > .item { - position: relative; -} - -ul.ui.list li:before, -.ui.bulleted.list .list > .item:before, -.ui.bulleted.list > .item:before { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - pointer-events: none; - position: absolute; - top: auto; - left: auto; - font-weight: normal; - margin-left: -1.25rem; - content: '\2022'; - opacity: 1; - color: inherit; - vertical-align: top; -} - -ul.ui.list li:before, -.ui.bulleted.list .list > a.item:before, -.ui.bulleted.list > a.item:before { - color: rgba(0, 0, 0, 0.87); -} - -ul.ui.list ul, -.ui.bulleted.list .list:not(.icon) { - padding-left: 1.25rem; -} - -/* Horizontal Bulleted */ - -ul.ui.horizontal.bulleted.list, -.ui.horizontal.bulleted.list { - margin-left: 0; -} - -ul.ui.horizontal.bulleted.list li, -.ui.horizontal.bulleted.list > .item { - margin-left: 1.75rem; -} - -ul.ui.horizontal.bulleted.list li:first-child, -.ui.horizontal.bulleted.list > .item:first-child { - margin-left: 0; -} - -ul.ui.horizontal.bulleted.list li::before, -.ui.horizontal.bulleted.list > .item::before { - color: rgba(0, 0, 0, 0.87); -} - -ul.ui.horizontal.bulleted.list li:first-child::before, -.ui.horizontal.bulleted.list > .item:first-child::before { - display: none; -} - -/*------------------- - Ordered - --------------------*/ - -ol.ui.list, -.ui.ordered.list, -.ui.ordered.list .list:not(.icon), -ol.ui.list ol { - counter-reset: ordered; - margin-left: 1.25rem; - list-style-type: none; -} - -ol.ui.list li, -.ui.ordered.list .list > .item, -.ui.ordered.list > .item { - list-style-type: none; - position: relative; -} - -ol.ui.list li:before, -.ui.ordered.list .list > .item:before, -.ui.ordered.list > .item:before { - position: absolute; - top: auto; - left: auto; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - pointer-events: none; - margin-left: -1.25rem; - counter-increment: ordered; - content: counters(ordered, ".") " "; - text-align: right; - color: rgba(0, 0, 0, 0.87); - vertical-align: middle; - opacity: 0.8; -} - -/* Value */ - -.ui.ordered.list .list > .item[data-value]:before, -.ui.ordered.list > .item[data-value]:before { - content: attr(data-value); -} - -ol.ui.list li[value]:before { - content: attr(value); -} - -/* Child Lists */ - -ol.ui.list ol, -.ui.ordered.list .list:not(.icon) { - margin-left: 1em; -} - -ol.ui.list ol li:before, -.ui.ordered.list .list > .item:before { - margin-left: -2em; -} - -/* Horizontal Ordered */ - -ol.ui.horizontal.list, -.ui.ordered.horizontal.list { - margin-left: 0; -} - -ol.ui.horizontal.list li:before, -.ui.ordered.horizontal.list .list > .item:before, -.ui.ordered.horizontal.list > .item:before { - position: static; - margin: 0 0.5em 0 0; -} - -/* Suffixed Ordered */ - -ol.ui.suffixed.list li:before, -.ui.suffixed.ordered.list .list > .item:before, -.ui.suffixed.ordered.list > .item:before { - content: counters(ordered, ".") "."; -} - -/*------------------- - Divided - --------------------*/ - -.ui.divided.list > .item { - border-top: 1px solid rgba(34, 36, 38, 0.15); -} - -.ui.divided.list .list > .item { - border-top: none; -} - -.ui.divided.list .item .list > .item { - border-top: none; -} - -.ui.divided.list .list > .item:first-child, -.ui.divided.list > .item:first-child { - border-top: none; -} - -/* Sub Menu */ - -.ui.divided.list:not(.horizontal) .list > .item:first-child { - border-top-width: 1px; -} - -/* Divided bulleted */ - -.ui.divided.bulleted.list:not(.horizontal), -.ui.divided.bulleted.list .list:not(.icon) { - margin-left: 0; - padding-left: 0; -} - -.ui.divided.bulleted.list > .item:not(.horizontal) { - padding-left: 1.25rem; -} - -/* Divided Ordered */ - -.ui.divided.ordered.list { - margin-left: 0; -} - -.ui.divided.ordered.list .list > .item, -.ui.divided.ordered.list > .item { - padding-left: 1.25rem; -} - -.ui.divided.ordered.list .item .list:not(.icon) { - margin-left: 0; - margin-right: 0; - padding-bottom: 0.21428571em; -} - -.ui.divided.ordered.list .item .list > .item { - padding-left: 1em; -} - -/* Divided Selection */ - -.ui.divided.selection.list .list > .item, -.ui.divided.selection.list > .item { - margin: 0; - border-radius: 0; -} - -/* Divided horizontal */ - -.ui.divided.horizontal.list { - margin-left: 0; -} - -.ui.divided.horizontal.list > .item { - padding-left: 0.5em; -} - -.ui.divided.horizontal.list > .item:not(:last-child) { - padding-right: 0.5em; -} - -.ui.divided.horizontal.list > .item { - border-top: none; - border-right: 1px solid rgba(34, 36, 38, 0.15); - margin: 0; - line-height: 0.6; -} - -.ui.horizontal.divided.list > .item:last-child { - border-right: none; -} - -/*------------------- - Celled - --------------------*/ - -.ui.celled.list > .item, -.ui.celled.list > .list { - border-top: 1px solid rgba(34, 36, 38, 0.15); - padding-left: 0.5em; - padding-right: 0.5em; -} - -.ui.celled.list > .item:last-child { - border-bottom: 1px solid rgba(34, 36, 38, 0.15); -} - -/* Padding on all elements */ - -.ui.celled.list > .item:first-child, -.ui.celled.list > .item:last-child { - padding-top: 0.21428571em; - padding-bottom: 0.21428571em; -} - -/* Sub Menu */ - -.ui.celled.list .item .list > .item { - border-width: 0; -} - -.ui.celled.list .list > .item:first-child { - border-top-width: 0; -} - -/* Celled Bulleted */ - -.ui.celled.bulleted.list { - margin-left: 0; -} - -.ui.celled.bulleted.list .list > .item, -.ui.celled.bulleted.list > .item { - padding-left: 1.25rem; -} - -.ui.celled.bulleted.list .item .list:not(.icon) { - margin-left: -1.25rem; - margin-right: -1.25rem; - padding-bottom: 0.21428571em; -} - -/* Celled Ordered */ - -.ui.celled.ordered.list { - margin-left: 0; -} - -.ui.celled.ordered.list .list > .item, -.ui.celled.ordered.list > .item { - padding-left: 1.25rem; -} - -.ui.celled.ordered.list .item .list:not(.icon) { - margin-left: 0; - margin-right: 0; - padding-bottom: 0.21428571em; -} - -.ui.celled.ordered.list .list > .item { - padding-left: 1em; -} - -/* Celled Horizontal */ - -.ui.horizontal.celled.list { - margin-left: 0; -} - -.ui.horizontal.celled.list .list > .item, -.ui.horizontal.celled.list > .item { - border-top: none; - border-left: 1px solid rgba(34, 36, 38, 0.15); - margin: 0; - padding-left: 0.5em; - padding-right: 0.5em; - line-height: 0.6; -} - -.ui.horizontal.celled.list .list > .item:last-child, -.ui.horizontal.celled.list > .item:last-child { - border-bottom: none; - border-right: 1px solid rgba(34, 36, 38, 0.15); -} - -/*------------------- - Relaxed - --------------------*/ - -.ui.relaxed.list:not(.horizontal) > .item:not(:first-child) { - padding-top: 0.42857143em; -} - -.ui.relaxed.list:not(.horizontal) > .item:not(:last-child) { - padding-bottom: 0.42857143em; -} - -.ui.horizontal.relaxed.list .list > .item:not(:first-child), -.ui.horizontal.relaxed.list > .item:not(:first-child) { - padding-left: 1rem; -} - -.ui.horizontal.relaxed.list .list > .item:not(:last-child), -.ui.horizontal.relaxed.list > .item:not(:last-child) { - padding-right: 1rem; -} - -/* Very Relaxed */ - -.ui[class*="very relaxed"].list:not(.horizontal) > .item:not(:first-child) { - padding-top: 0.85714286em; -} - -.ui[class*="very relaxed"].list:not(.horizontal) > .item:not(:last-child) { - padding-bottom: 0.85714286em; -} - -.ui.horizontal[class*="very relaxed"].list .list > .item:not(:first-child), -.ui.horizontal[class*="very relaxed"].list > .item:not(:first-child) { - padding-left: 1.5rem; -} - -.ui.horizontal[class*="very relaxed"].list .list > .item:not(:last-child), -.ui.horizontal[class*="very relaxed"].list > .item:not(:last-child) { - padding-right: 1.5rem; -} - -/*------------------- - Sizes ---------------------*/ - -.ui.list { - font-size: 1em; -} - -.ui.mini.list { - font-size: 0.78571429em; -} - -.ui.mini.horizontal.list .list > .item, -.ui.mini.horizontal.list > .item { - font-size: 0.78571429rem; -} - -.ui.tiny.list { - font-size: 0.85714286em; -} - -.ui.tiny.horizontal.list .list > .item, -.ui.tiny.horizontal.list > .item { - font-size: 0.85714286rem; -} - -.ui.small.list { - font-size: 0.92857143em; -} - -.ui.small.horizontal.list .list > .item, -.ui.small.horizontal.list > .item { - font-size: 0.92857143rem; -} - -.ui.large.list { - font-size: 1.14285714em; -} - -.ui.large.horizontal.list .list > .item, -.ui.large.horizontal.list > .item { - font-size: 1.14285714rem; -} - -.ui.big.list { - font-size: 1.28571429em; -} - -.ui.big.horizontal.list .list > .item, -.ui.big.horizontal.list > .item { - font-size: 1.28571429rem; -} - -.ui.huge.list { - font-size: 1.42857143em; -} - -.ui.huge.horizontal.list .list > .item, -.ui.huge.horizontal.list > .item { - font-size: 1.42857143rem; -} - -.ui.massive.list { - font-size: 1.71428571em; -} - -.ui.massive.horizontal.list .list > .item, -.ui.massive.horizontal.list > .item { - font-size: 1.71428571rem; -} - -/******************************* - Theme Overrides -*******************************/ - -/******************************* - User Variable Overrides -*******************************/ /* * # Fomantic - Menu * http://github.com/fomantic/Fomantic-UI/ diff --git a/web_src/fomantic/build/semantic.js b/web_src/fomantic/build/semantic.js index 1199e9c82f..c150c8d9db 100644 --- a/web_src/fomantic/build/semantic.js +++ b/web_src/fomantic/build/semantic.js @@ -1184,883 +1184,6 @@ $.api.settings = { -})( jQuery, window, document ); - -/*! - * # Fomantic-UI - Checkbox - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - -;(function ($, window, document, undefined) { - -'use strict'; - -$.isFunction = $.isFunction || function(obj) { - return typeof obj === "function" && typeof obj.nodeType !== "number"; -}; - -window = (typeof window != 'undefined' && window.Math == Math) - ? window - : (typeof self != 'undefined' && self.Math == Math) - ? self - : Function('return this')() -; - -$.fn.checkbox = function(parameters) { - var - $allModules = $(this), - moduleSelector = $allModules.selector || '', - - time = new Date().getTime(), - performance = [], - - query = arguments[0], - methodInvoked = (typeof query == 'string'), - queryArguments = [].slice.call(arguments, 1), - returnedValue - ; - - $allModules - .each(function() { - var - settings = $.extend(true, {}, $.fn.checkbox.settings, parameters), - - className = settings.className, - namespace = settings.namespace, - selector = settings.selector, - error = settings.error, - - eventNamespace = '.' + namespace, - moduleNamespace = 'module-' + namespace, - - $module = $(this), - $label = $(this).children(selector.label), - $input = $(this).children(selector.input), - input = $input[0], - - initialLoad = false, - shortcutPressed = false, - instance = $module.data(moduleNamespace), - - observer, - element = this, - module - ; - - module = { - - initialize: function() { - module.verbose('Initializing checkbox', settings); - - module.create.label(); - module.bind.events(); - - module.set.tabbable(); - module.hide.input(); - - module.observeChanges(); - module.instantiate(); - module.setup(); - }, - - instantiate: function() { - module.verbose('Storing instance of module', module); - instance = module; - $module - .data(moduleNamespace, module) - ; - }, - - destroy: function() { - module.verbose('Destroying module'); - module.unbind.events(); - module.show.input(); - $module.removeData(moduleNamespace); - }, - - fix: { - reference: function() { - if( $module.is(selector.input) ) { - module.debug('Behavior called on adjusting invoked element'); - $module = $module.closest(selector.checkbox); - module.refresh(); - } - } - }, - - setup: function() { - module.set.initialLoad(); - if( module.is.indeterminate() ) { - module.debug('Initial value is indeterminate'); - module.indeterminate(); - } - else if( module.is.checked() ) { - module.debug('Initial value is checked'); - module.check(); - } - else { - module.debug('Initial value is unchecked'); - module.uncheck(); - } - module.remove.initialLoad(); - }, - - refresh: function() { - $label = $module.children(selector.label); - $input = $module.children(selector.input); - input = $input[0]; - }, - - hide: { - input: function() { - module.verbose('Modifying z-index to be unselectable'); - $input.addClass(className.hidden); - } - }, - show: { - input: function() { - module.verbose('Modifying z-index to be selectable'); - $input.removeClass(className.hidden); - } - }, - - observeChanges: function() { - if('MutationObserver' in window) { - observer = new MutationObserver(function(mutations) { - module.debug('DOM tree modified, updating selector cache'); - module.refresh(); - }); - observer.observe(element, { - childList : true, - subtree : true - }); - module.debug('Setting up mutation observer', observer); - } - }, - - attachEvents: function(selector, event) { - var - $element = $(selector) - ; - event = $.isFunction(module[event]) - ? module[event] - : module.toggle - ; - if($element.length > 0) { - module.debug('Attaching checkbox events to element', selector, event); - $element - .on('click' + eventNamespace, event) - ; - } - else { - module.error(error.notFound); - } - }, - - preventDefaultOnInputTarget: function() { - if(typeof event !== 'undefined' && event !== null && $(event.target).is(selector.input)) { - module.verbose('Preventing default check action after manual check action'); - event.preventDefault(); - } - }, - - event: { - change: function(event) { - if( !module.should.ignoreCallbacks() ) { - settings.onChange.call(input); - } - }, - click: function(event) { - var - $target = $(event.target) - ; - if( $target.is(selector.input) ) { - module.verbose('Using default check action on initialized checkbox'); - return; - } - if( $target.is(selector.link) ) { - module.debug('Clicking link inside checkbox, skipping toggle'); - return; - } - module.toggle(); - $input.focus(); - event.preventDefault(); - }, - keydown: function(event) { - var - key = event.which, - keyCode = { - enter : 13, - space : 32, - escape : 27, - left : 37, - up : 38, - right : 39, - down : 40 - } - ; - - var r = module.get.radios(), - rIndex = r.index($module), - rLen = r.length, - checkIndex = false; - - if(key == keyCode.left || key == keyCode.up) { - checkIndex = (rIndex === 0 ? rLen : rIndex) - 1; - } else if(key == keyCode.right || key == keyCode.down) { - checkIndex = rIndex === rLen-1 ? 0 : rIndex+1; - } - - if (!module.should.ignoreCallbacks() && checkIndex !== false) { - if(settings.beforeUnchecked.apply(input)===false) { - module.verbose('Option not allowed to be unchecked, cancelling key navigation'); - return false; - } - if (settings.beforeChecked.apply($(r[checkIndex]).children(selector.input)[0])===false) { - module.verbose('Next option should not allow check, cancelling key navigation'); - return false; - } - } - - if(key == keyCode.escape) { - module.verbose('Escape key pressed blurring field'); - $input.blur(); - shortcutPressed = true; - } - else if(!event.ctrlKey && ( key == keyCode.space || (key == keyCode.enter && settings.enableEnterKey)) ) { - module.verbose('Enter/space key pressed, toggling checkbox'); - module.toggle(); - shortcutPressed = true; - } - else { - shortcutPressed = false; - } - }, - keyup: function(event) { - if(shortcutPressed) { - event.preventDefault(); - } - } - }, - - check: function() { - if( !module.should.allowCheck() ) { - return; - } - module.debug('Checking checkbox', $input); - module.set.checked(); - if( !module.should.ignoreCallbacks() ) { - settings.onChecked.call(input); - module.trigger.change(); - } - module.preventDefaultOnInputTarget(); - }, - - uncheck: function() { - if( !module.should.allowUncheck() ) { - return; - } - module.debug('Unchecking checkbox'); - module.set.unchecked(); - if( !module.should.ignoreCallbacks() ) { - settings.onUnchecked.call(input); - module.trigger.change(); - } - module.preventDefaultOnInputTarget(); - }, - - indeterminate: function() { - if( module.should.allowIndeterminate() ) { - module.debug('Checkbox is already indeterminate'); - return; - } - module.debug('Making checkbox indeterminate'); - module.set.indeterminate(); - if( !module.should.ignoreCallbacks() ) { - settings.onIndeterminate.call(input); - module.trigger.change(); - } - }, - - determinate: function() { - if( module.should.allowDeterminate() ) { - module.debug('Checkbox is already determinate'); - return; - } - module.debug('Making checkbox determinate'); - module.set.determinate(); - if( !module.should.ignoreCallbacks() ) { - settings.onDeterminate.call(input); - module.trigger.change(); - } - }, - - enable: function() { - if( module.is.enabled() ) { - module.debug('Checkbox is already enabled'); - return; - } - module.debug('Enabling checkbox'); - module.set.enabled(); - if( !module.should.ignoreCallbacks() ) { - settings.onEnable.call(input); - // preserve legacy callbacks - settings.onEnabled.call(input); - module.trigger.change(); - } - }, - - disable: function() { - if( module.is.disabled() ) { - module.debug('Checkbox is already disabled'); - return; - } - module.debug('Disabling checkbox'); - module.set.disabled(); - if( !module.should.ignoreCallbacks() ) { - settings.onDisable.call(input); - // preserve legacy callbacks - settings.onDisabled.call(input); - module.trigger.change(); - } - }, - - get: { - radios: function() { - var - name = module.get.name() - ; - return $('input[name="' + name + '"]').closest(selector.checkbox); - }, - otherRadios: function() { - return module.get.radios().not($module); - }, - name: function() { - return $input.attr('name'); - } - }, - - is: { - initialLoad: function() { - return initialLoad; - }, - radio: function() { - return ($input.hasClass(className.radio) || $input.attr('type') == 'radio'); - }, - indeterminate: function() { - return $input.prop('indeterminate') !== undefined && $input.prop('indeterminate'); - }, - checked: function() { - return $input.prop('checked') !== undefined && $input.prop('checked'); - }, - disabled: function() { - return $input.prop('disabled') !== undefined && $input.prop('disabled'); - }, - enabled: function() { - return !module.is.disabled(); - }, - determinate: function() { - return !module.is.indeterminate(); - }, - unchecked: function() { - return !module.is.checked(); - } - }, - - should: { - allowCheck: function() { - if(module.is.determinate() && module.is.checked() && !module.is.initialLoad() ) { - module.debug('Should not allow check, checkbox is already checked'); - return false; - } - if(!module.should.ignoreCallbacks() && settings.beforeChecked.apply(input) === false) { - module.debug('Should not allow check, beforeChecked cancelled'); - return false; - } - return true; - }, - allowUncheck: function() { - if(module.is.determinate() && module.is.unchecked() && !module.is.initialLoad() ) { - module.debug('Should not allow uncheck, checkbox is already unchecked'); - return false; - } - if(!module.should.ignoreCallbacks() && settings.beforeUnchecked.apply(input) === false) { - module.debug('Should not allow uncheck, beforeUnchecked cancelled'); - return false; - } - return true; - }, - allowIndeterminate: function() { - if(module.is.indeterminate() && !module.is.initialLoad() ) { - module.debug('Should not allow indeterminate, checkbox is already indeterminate'); - return false; - } - if(!module.should.ignoreCallbacks() && settings.beforeIndeterminate.apply(input) === false) { - module.debug('Should not allow indeterminate, beforeIndeterminate cancelled'); - return false; - } - return true; - }, - allowDeterminate: function() { - if(module.is.determinate() && !module.is.initialLoad() ) { - module.debug('Should not allow determinate, checkbox is already determinate'); - return false; - } - if(!module.should.ignoreCallbacks() && settings.beforeDeterminate.apply(input) === false) { - module.debug('Should not allow determinate, beforeDeterminate cancelled'); - return false; - } - return true; - }, - ignoreCallbacks: function() { - return (initialLoad && !settings.fireOnInit); - } - }, - - can: { - change: function() { - return !( $module.hasClass(className.disabled) || $module.hasClass(className.readOnly) || $input.prop('disabled') || $input.prop('readonly') ); - }, - uncheck: function() { - return (typeof settings.uncheckable === 'boolean') - ? settings.uncheckable - : !module.is.radio() - ; - } - }, - - set: { - initialLoad: function() { - initialLoad = true; - }, - checked: function() { - module.verbose('Setting class to checked'); - $module - .removeClass(className.indeterminate) - .addClass(className.checked) - ; - if( module.is.radio() ) { - module.uncheckOthers(); - } - if(!module.is.indeterminate() && module.is.checked()) { - module.debug('Input is already checked, skipping input property change'); - return; - } - module.verbose('Setting state to checked', input); - $input - .prop('indeterminate', false) - .prop('checked', true) - ; - }, - unchecked: function() { - module.verbose('Removing checked class'); - $module - .removeClass(className.indeterminate) - .removeClass(className.checked) - ; - if(!module.is.indeterminate() && module.is.unchecked() ) { - module.debug('Input is already unchecked'); - return; - } - module.debug('Setting state to unchecked'); - $input - .prop('indeterminate', false) - .prop('checked', false) - ; - }, - indeterminate: function() { - module.verbose('Setting class to indeterminate'); - $module - .addClass(className.indeterminate) - ; - if( module.is.indeterminate() ) { - module.debug('Input is already indeterminate, skipping input property change'); - return; - } - module.debug('Setting state to indeterminate'); - $input - .prop('indeterminate', true) - ; - }, - determinate: function() { - module.verbose('Removing indeterminate class'); - $module - .removeClass(className.indeterminate) - ; - if( module.is.determinate() ) { - module.debug('Input is already determinate, skipping input property change'); - return; - } - module.debug('Setting state to determinate'); - $input - .prop('indeterminate', false) - ; - }, - disabled: function() { - module.verbose('Setting class to disabled'); - $module - .addClass(className.disabled) - ; - if( module.is.disabled() ) { - module.debug('Input is already disabled, skipping input property change'); - return; - } - module.debug('Setting state to disabled'); - $input - .prop('disabled', 'disabled') - ; - }, - enabled: function() { - module.verbose('Removing disabled class'); - $module.removeClass(className.disabled); - if( module.is.enabled() ) { - module.debug('Input is already enabled, skipping input property change'); - return; - } - module.debug('Setting state to enabled'); - $input - .prop('disabled', false) - ; - }, - tabbable: function() { - module.verbose('Adding tabindex to checkbox'); - if( $input.attr('tabindex') === undefined) { - $input.attr('tabindex', 0); - } - } - }, - - remove: { - initialLoad: function() { - initialLoad = false; - } - }, - - trigger: { - change: function() { - var - inputElement = $input[0] - ; - if(inputElement) { - var events = document.createEvent('HTMLEvents'); - module.verbose('Triggering native change event'); - events.initEvent('change', true, false); - inputElement.dispatchEvent(events); - } - } - }, - - - create: { - label: function() { - if($input.prevAll(selector.label).length > 0) { - $input.prev(selector.label).detach().insertAfter($input); - module.debug('Moving existing label', $label); - } - else if( !module.has.label() ) { - $label = $('