diff --git a/.drone.yml b/.drone.yml index d1ad625e329f9..1181b809b0da4 100644 --- a/.drone.yml +++ b/.drone.yml @@ -32,6 +32,7 @@ steps: pull: always commands: - make deps-backend + - make deps-tools volumes: - name: deps path: /go @@ -168,7 +169,7 @@ steps: --- kind: pipeline type: docker -name: testing-amd64 +name: testing-pgsql platform: os: linux @@ -191,51 +192,26 @@ volumes: temp: {} services: - - name: mysql - image: mysql:5.7 - pull: always - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: test - - - name: mysql8 - image: mysql:8 - pull: always - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: testgitea - - - name: mssql - image: mcr.microsoft.com/mssql/server:latest - pull: always + - name: pgsql + pull: default + image: postgres:15 environment: - ACCEPT_EULA: Y - MSSQL_PID: Standard - SA_PASSWORD: MwantsaSecurePassword1 + POSTGRES_DB: test + POSTGRES_PASSWORD: postgres - name: ldap image: gitea/test-openldap:latest pull: always - - name: elasticsearch - image: elasticsearch:7.5.0 - pull: always - environment: - discovery.type: single-node - - name: minio image: minio/minio:RELEASE.2021-03-12T00-00-47Z pull: always commands: - - minio server /data + - minio server /data environment: MINIO_ACCESS_KEY: 123456 MINIO_SECRET_KEY: 12345678 - - name: smtpimap - image: tabascoterrier/docker-imap-devel:latest - pull: always - steps: - name: fetch-tags image: docker:git @@ -257,12 +233,6 @@ steps: - name: deps path: /go - - name: tag-pre-condition - image: drone/git - pull: always - commands: - - git update-ref refs/heads/tag_test ${DRONE_COMMIT_SHA} - - name: prepare-test-env image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env pull: always @@ -278,88 +248,157 @@ steps: environment: GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not GOSUMDB: sum.golang.org - TAGS: bindata sqlite sqlite_unlock_notify + TAGS: bindata depends_on: [deps-backend, prepare-test-env] volumes: - name: deps path: /go - - name: unit-test + - name: test-pgsql image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - make unit-test-coverage test-check + - timeout -s ABRT 50m make test-pgsql-migration test-pgsql environment: GOPROXY: https://goproxy.io - TAGS: bindata sqlite sqlite_unlock_notify + TAGS: bindata gogit RACE_ENABLED: true - GITHUB_READ_TOKEN: - from_secret: github_read_token - depends_on: [deps-backend, prepare-test-env] + TEST_TAGS: gogit + TEST_LDAP: 1 + USE_REPO_TEST_DIR: 1 + depends_on: [build] volumes: - name: deps path: /go - - name: unit-test-gogit +--- +kind: pipeline +type: docker +name: testing-mysql + +platform: + os: linux + arch: amd64 + +depends_on: + - compliance + +trigger: + event: + - push + - tag + - pull_request + paths: + exclude: + - docs/** + +volumes: + - name: deps + temp: {} + +services: + - name: mysql + image: mysql:5.7 + pull: always + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: test + + - name: elasticsearch + image: elasticsearch:7.5.0 + pull: always + environment: + discovery.type: single-node + + - name: smtpimap + image: tabascoterrier/docker-imap-devel:latest + pull: always + +steps: + - name: fetch-tags + image: docker:git + pull: always + commands: + - git config --global --add safe.directory /drone/src + - git fetch --tags --force + when: + event: + exclude: + - pull_request + + - name: deps-backend + image: golang:1.20 + pull: always + commands: + - make deps-backend + volumes: + - name: deps + path: /go + + - name: prepare-test-env + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env + pull: always + commands: + - ./build/test-env-prepare.sh + + - name: build image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - make unit-test-coverage test-check + - ./build/test-env-check.sh + - make backend environment: - GOPROXY: https://goproxy.io - TAGS: bindata gogit sqlite sqlite_unlock_notify - RACE_ENABLED: true - GITHUB_READ_TOKEN: - from_secret: github_read_token + GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not + GOSUMDB: sum.golang.org + TAGS: bindata depends_on: [deps-backend, prepare-test-env] volumes: - name: deps path: /go - - name: test-mysql + - name: unit-test image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - make test-mysql-migration integration-test-coverage + - make unit-test-coverage test-check environment: GOPROXY: https://goproxy.io TAGS: bindata RACE_ENABLED: true - TEST_LDAP: 1 - USE_REPO_TEST_DIR: 1 - TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" - depends_on: [build] + GITHUB_READ_TOKEN: + from_secret: github_read_token + depends_on: [deps-backend, prepare-test-env] volumes: - name: deps path: /go - - name: test-mysql8 + - name: unit-test-gogit image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - timeout -s ABRT 50m make test-mysql8-migration test-mysql8 + - make unit-test-coverage test-check environment: GOPROXY: https://goproxy.io - TAGS: bindata + TAGS: bindata gogit RACE_ENABLED: true - TEST_LDAP: 1 - USE_REPO_TEST_DIR: 1 - depends_on: [build] + GITHUB_READ_TOKEN: + from_secret: github_read_token + depends_on: [deps-backend, prepare-test-env] volumes: - name: deps path: /go - - name: test-mssql + - name: test-mysql image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - make test-mssql-migration test-mssql + - make test-mysql-migration integration-test-coverage environment: GOPROXY: https://goproxy.io TAGS: bindata RACE_ENABLED: true - TEST_LDAP: 1 USE_REPO_TEST_DIR: 1 + TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" depends_on: [build] volumes: - name: deps @@ -398,11 +437,12 @@ steps: --- kind: pipeline -name: testing-arm64 +type: docker +name: testing-mysql8 platform: os: linux - arch: arm64 + arch: amd64 depends_on: - compliance @@ -421,16 +461,102 @@ volumes: temp: {} services: - - name: pgsql - pull: default - image: postgres:10 + - name: mysql8 + image: mysql:8 + pull: always environment: - POSTGRES_DB: test - POSTGRES_PASSWORD: postgres + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: testgitea - - name: ldap - pull: default - image: gitea/test-openldap:latest +steps: + - name: fetch-tags + image: docker:git + pull: always + commands: + - git config --global --add safe.directory /drone/src + - git fetch --tags --force + when: + event: + exclude: + - pull_request + + - name: deps-backend + image: golang:1.20 + pull: always + commands: + - make deps-backend + volumes: + - name: deps + path: /go + + - name: prepare-test-env + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env + pull: always + commands: + - ./build/test-env-prepare.sh + + - name: build + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env + user: gitea + commands: + - ./build/test-env-check.sh + - make backend + environment: + GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not + GOSUMDB: sum.golang.org + TAGS: bindata + depends_on: [deps-backend, prepare-test-env] + volumes: + - name: deps + path: /go + + - name: test-mysql8 + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env + user: gitea + commands: + - timeout -s ABRT 50m make test-mysql8-migration test-mysql8 + environment: + GOPROXY: https://goproxy.io + TAGS: bindata + USE_REPO_TEST_DIR: 1 + depends_on: [build] + volumes: + - name: deps + path: /go + +--- +kind: pipeline +type: docker +name: testing-mssql + +platform: + os: linux + arch: amd64 + +depends_on: + - compliance + +trigger: + event: + - push + - tag + - pull_request + paths: + exclude: + - docs/** + +volumes: + - name: deps + temp: {} + +services: + - name: mssql + image: mcr.microsoft.com/mssql/server:latest + pull: always + environment: + ACCEPT_EULA: Y + MSSQL_PID: Standard + SA_PASSWORD: MwantsaSecurePassword1 steps: - name: fetch-tags @@ -454,13 +580,13 @@ steps: path: /go - name: prepare-test-env - image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env pull: always commands: - ./build/test-env-prepare.sh - name: build - image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - ./build/test-env-check.sh @@ -468,39 +594,102 @@ steps: environment: GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not GOSUMDB: sum.golang.org - TAGS: bindata gogit sqlite sqlite_unlock_notify + TAGS: bindata depends_on: [deps-backend, prepare-test-env] volumes: - name: deps path: /go - - name: test-sqlite - image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env + - name: test-mssql + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - timeout -s ABRT 50m make test-sqlite-migration test-sqlite + - make test-mssql-migration test-mssql environment: GOPROXY: https://goproxy.io - TAGS: bindata gogit sqlite sqlite_unlock_notify - RACE_ENABLED: true - TEST_TAGS: gogit sqlite sqlite_unlock_notify + TAGS: bindata USE_REPO_TEST_DIR: 1 depends_on: [build] volumes: - name: deps path: /go - - name: test-pgsql +--- +kind: pipeline +name: testing-sqlite + +platform: + os: linux + arch: arm64 + +depends_on: + - compliance + +trigger: + event: + - push + - tag + - pull_request + paths: + exclude: + - docs/** + +volumes: + - name: deps + temp: {} + +steps: + - name: fetch-tags + image: docker:git + pull: always + commands: + - git config --global --add safe.directory /drone/src + - git fetch --tags --force + when: + event: + exclude: + - pull_request + + - name: deps-backend + image: golang:1.20 + pull: always + commands: + - make deps-backend + volumes: + - name: deps + path: /go + + - name: prepare-test-env + image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env + pull: always + commands: + - ./build/test-env-prepare.sh + + - name: build image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env user: gitea commands: - - timeout -s ABRT 50m make test-pgsql-migration test-pgsql + - ./build/test-env-check.sh + - make backend + environment: + GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not + GOSUMDB: sum.golang.org + TAGS: bindata gogit sqlite sqlite_unlock_notify + depends_on: [deps-backend, prepare-test-env] + volumes: + - name: deps + path: /go + + - name: test-sqlite + image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env + user: gitea + commands: + - timeout -s ABRT 50m make test-sqlite-migration test-sqlite environment: GOPROXY: https://goproxy.io - TAGS: bindata gogit + TAGS: bindata gogit sqlite sqlite_unlock_notify RACE_ENABLED: true - TEST_TAGS: gogit - TEST_LDAP: 1 + TEST_TAGS: gogit sqlite sqlite_unlock_notify USE_REPO_TEST_DIR: 1 depends_on: [build] volumes: @@ -530,15 +719,6 @@ volumes: - name: deps temp: {} -services: - - name: pgsql - pull: default - image: postgres:10 - environment: - POSTGRES_DB: testgitea-e2e - POSTGRES_PASSWORD: postgres - POSTGRES_INITDB_ARGS: --encoding=UTF8 --lc-collate='en_US.UTF-8' --lc-ctype='en_US.UTF-8' - steps: - name: deps-frontend image: node:18 @@ -568,14 +748,12 @@ steps: - curl -sLO https://go.dev/dl/go1.20.linux-amd64.tar.gz && tar -C /usr/local -xzf go1.20.linux-amd64.tar.gz - groupadd --gid 1001 gitea && useradd -m --gid 1001 --uid 1001 gitea - apt-get -qq update && apt-get -qqy install build-essential - - export TEST_PGSQL_SCHEMA='' - ./build/test-env-prepare.sh - - su gitea bash -c "export PATH=$PATH:/usr/local/go/bin && timeout -s ABRT 40m make test-e2e-pgsql" + - su gitea bash -c "export PATH=$PATH:/usr/local/go/bin && timeout -s ABRT 40m make test-e2e-sqlite" environment: GOPROXY: https://goproxy.io GOSUMDB: sum.golang.org USE_REPO_TEST_DIR: 1 - TEST_PGSQL_DBNAME: 'testgitea-e2e' DEBIAN_FRONTEND: noninteractive depends_on: [build-frontend, deps-backend] volumes: @@ -709,8 +887,11 @@ trigger: - docs/** depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite volumes: - name: deps @@ -842,8 +1023,11 @@ trigger: - tag depends_on: - - testing-arm64 - - testing-amd64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite volumes: - name: deps @@ -994,8 +1178,11 @@ platform: arch: amd64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1019,7 +1206,7 @@ steps: - git fetch --tags --force - name: publish - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest pull: always settings: auto_tag: true @@ -1031,13 +1218,17 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: - pull_request - name: publish-rootless - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest settings: dockerfile: Dockerfile.rootless auto_tag: true @@ -1049,6 +1240,10 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: @@ -1064,8 +1259,11 @@ platform: arch: amd64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1086,7 +1284,7 @@ steps: - git fetch --tags --force - name: publish - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest pull: always settings: tags: ${DRONE_TAG##v}-linux-amd64 @@ -1097,13 +1295,17 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: - pull_request - name: publish-rootless - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest settings: dockerfile: Dockerfile.rootless tags: ${DRONE_TAG##v}-linux-amd64-rootless @@ -1114,6 +1316,10 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: @@ -1129,8 +1335,11 @@ platform: arch: amd64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1148,7 +1357,7 @@ steps: - git fetch --tags --force - name: publish - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest pull: always settings: auto_tag: false @@ -1160,13 +1369,17 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: - pull_request - name: publish-rootless - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest settings: dockerfile: Dockerfile.rootless auto_tag: false @@ -1178,6 +1391,10 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: @@ -1192,8 +1409,11 @@ platform: arch: amd64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1211,7 +1431,7 @@ steps: - git fetch --tags --force - name: publish - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest pull: always settings: auto_tag: false @@ -1223,13 +1443,17 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: - pull_request - name: publish-rootless - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest settings: dockerfile: Dockerfile.rootless auto_tag: false @@ -1241,6 +1465,10 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: @@ -1249,7 +1477,7 @@ steps: --- kind: pipeline type: docker -name: docker-linux-arm64-dry-run +name: docker-linux-amd64-dry-run platform: os: linux @@ -1267,7 +1495,7 @@ trigger: steps: - name: dryrun - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest pull: always settings: dry_run: true @@ -1278,6 +1506,7 @@ steps: environment: PLUGIN_MIRROR: from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: - pull_request @@ -1292,8 +1521,11 @@ platform: arch: arm64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1317,7 +1549,7 @@ steps: - git fetch --tags --force - name: publish - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest pull: always settings: auto_tag: true @@ -1329,13 +1561,17 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: - pull_request - name: publish-rootless - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest settings: dockerfile: Dockerfile.rootless auto_tag: true @@ -1347,6 +1583,10 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: @@ -1362,8 +1602,11 @@ platform: arch: arm64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1384,7 +1627,7 @@ steps: - git fetch --tags --force - name: publish - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest pull: always settings: tags: ${DRONE_TAG##v}-linux-arm64 @@ -1395,13 +1638,17 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: - pull_request - name: publish-rootless - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest settings: dockerfile: Dockerfile.rootless tags: ${DRONE_TAG##v}-linux-arm64-rootless @@ -1412,6 +1659,10 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: @@ -1427,8 +1678,11 @@ platform: arch: arm64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1449,7 +1703,7 @@ steps: - git fetch --tags --force - name: publish - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest pull: always settings: auto_tag: false @@ -1461,13 +1715,17 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: - pull_request - name: publish-rootless - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest settings: dockerfile: Dockerfile.rootless auto_tag: false @@ -1479,6 +1737,10 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: @@ -1493,8 +1755,11 @@ platform: arch: arm64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1512,7 +1777,7 @@ steps: - git fetch --tags --force - name: publish - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest pull: always settings: auto_tag: false @@ -1524,13 +1789,17 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: - pull_request - name: publish-rootless - image: techknowlogick/drone-docker:latest + image: plugins/docker:latest settings: dockerfile: Dockerfile.rootless auto_tag: false @@ -1542,6 +1811,10 @@ steps: from_secret: docker_password username: from_secret: docker_username + environment: + PLUGIN_MIRROR: + from_secret: plugin_mirror + DOCKER_BUILDKIT: 1 when: event: exclude: @@ -1607,7 +1880,6 @@ platform: steps: - name: manifest-rootless - pull: always image: plugins/manifest pull: always settings: @@ -1671,8 +1943,11 @@ trigger: - failure depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite - release-version - release-latest - docker-linux-amd64-release diff --git a/.stylelintrc.yaml b/.stylelintrc.yaml index ca0f16b07b6c0..c488d0677f18a 100644 --- a/.stylelintrc.yaml +++ b/.stylelintrc.yaml @@ -1,12 +1,18 @@ plugins: - stylelint-declaration-strict-value +ignoreFiles: + - "**/*.go" + overrides: - files: ["**/*.less"] customSyntax: postcss-less - files: ["**/chroma/*", "**/codemirror/*", "**/standalone/*", "**/console/*"] rules: scale-unlimited/declaration-strict-value: null + - files: ["**/chroma/*", "**/codemirror/*"] + rules: + block-no-empty: null rules: alpha-value-notation: null diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000..979831eb9b8b2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,96 @@ +# Gitea Community Code of Conduct + +## About + +Online communities include people from many different backgrounds. The Gitea contributors are committed to providing a friendly, safe and welcoming environment for all, regardless of gender identity and expression, sexual orientation, disabilities, neurodiversity, physical appearance, body size, ethnicity, nationality, race, age, religion, or similar personal characteristics. + +The first goal of the Code of Conduct is to specify a baseline standard of behavior so that people with different social values and communication styles can talk about Gitea effectively, productively, and respectfully. + +The second goal is to provide a mechanism for resolving conflicts in the community when they arise. + +The third goal of the Code of Conduct is to make our community welcoming to people from different backgrounds. Diversity is critical to the project; for Gitea to be successful, it needs contributors and users from all backgrounds. + +We believe that healthy debate and disagreement are essential to a healthy project and community. However, it is never ok to be disrespectful. We value diverse opinions, but we value respectful behavior more. + +## Community values + +These are the values to which people in the Gitea community should aspire. + +- **Be friendly and welcoming.** +- **Be patient.** + - Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.) +- **Be thoughtful.** + - Productive communication requires effort. Think about how your words will be interpreted. + - Remember that sometimes it is best to refrain entirely from commenting. +- **Be respectful.** + - In particular, respect differences of opinion. +- **Be charitable.** + - Interpret the arguments of others in good faith, do not seek to disagree. + - When we do disagree, try to understand why. +- **Be constructive.** + - Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation. + - Avoid unconstructive criticism: don't merely decry the current state of affairs; offer—or at least solicit—suggestions as to how things may be improved. + - Avoid snarking (pithy, unproductive, sniping comments) + - Avoid discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict. + - Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group). +- **Be responsible.** + - What you say and do matters. Take responsibility for your words and actions, including their consequences, whether intended or otherwise. + +People are complicated. You should expect to be misunderstood and to misunderstand others; when this inevitably occurs, resist the urge to be defensive or assign blame. Try not to take offense where no offense was intended. Give people the benefit of the doubt. Even if the intent was to provoke, do not rise to it. It is the responsibility of all parties to de-escalate conflict when it arises. + +## Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others’ private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject: comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, as well as to ban (temporarily or permanently) any contributor for behaviors that they deem inappropriate, threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when the Project Stewards have a reasonable belief that an individual’s behavior may have a negative impact on the project or its community. + +### Conflict Resolution + +We do not believe that all conflict is bad; healthy debate and disagreement often yield positive results. However, it is never okay to be disrespectful or to engage in behavior that violates the project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address the behavior directly with those involved. Many issues can be resolved quickly and easily, and this gives people more control over the outcome of their dispute. If you are unable to resolve the matter for any reason, or if the behavior is threatening or harassing, report it. We are dedicated to providing an environment where participants feel welcome and safe. + +Reports should be directed to the Gitea Project Stewards at conduct@gitea.com. It is the Project Stewards’ duty to receive and address reported violations of the code of conduct. They will then work with a committee consisting of representatives from the technical-oversight-committee. + +We will investigate every complaint, but you may not receive a direct response. We will use our discretion in determining when and how to follow up on reported incidents, which may range from not taking action to permanent expulsion from the project and project-sponsored spaces. Under normal circumstances, we will notify the accused of the report and provide them an opportunity to discuss it before any action is taken. If there is a consensus between maintainers that such an endeavor would be useless (i.e. in case of an obvious spammer), we reserve the right to take action without notifying the accused first. The identity of the reporter will be omitted from the details of the report supplied to the accused. In potentially harmful situations, such as ongoing harassment or threats to anyone’s safety, we may take action without notice. + +### Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +## Summary + +- Treat everyone with respect and kindness. +- Be thoughtful in how you communicate. +- Don’t be destructive or inflammatory. +- If you encounter an issue, please mail conduct@gitea.com. diff --git a/Makefile b/Makefile index 19cd455aba30c..d770ed453f4fa 100644 --- a/Makefile +++ b/Makefile @@ -190,6 +190,7 @@ help: @echo " - deps install dependencies" @echo " - deps-frontend install frontend dependencies" @echo " - deps-backend install backend dependencies" + @echo " - deps-tools install tool dependencies" @echo " - lint lint everything" @echo " - lint-frontend lint frontend files" @echo " - lint-backend lint backend files" @@ -821,7 +822,7 @@ docs: cd docs; make trans-copy clean build-offline; .PHONY: deps -deps: deps-frontend deps-backend +deps: deps-frontend deps-backend deps-tools .PHONY: deps-frontend deps-frontend: node_modules @@ -829,6 +830,9 @@ deps-frontend: node_modules .PHONY: deps-backend deps-backend: $(GO) mod download + +.PHONY: deps-tools +deps-tools: $(GO) install $(AIR_PACKAGE) $(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) $(GO) install $(ERRCHECK_PACKAGE) diff --git a/cmd/admin.go b/cmd/admin.go index b913b817bdc5d..f9fb1b6c68f07 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -7,6 +7,7 @@ package cmd import ( "errors" "fmt" + "net/url" "os" "strings" "text/tabwriter" @@ -469,11 +470,19 @@ func runAddOauth(c *cli.Context) error { return err } + config := parseOAuth2Config(c) + if config.Provider == "openidConnect" { + discoveryURL, err := url.Parse(config.OpenIDConnectAutoDiscoveryURL) + if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") { + return fmt.Errorf("invalid Auto Discovery URL: %s (this must be a valid URL starting with http:// or https://)", config.OpenIDConnectAutoDiscoveryURL) + } + } + return auth_model.CreateSource(&auth_model.Source{ Type: auth_model.OAuth2, Name: c.String("name"), IsActive: true, - Cfg: parseOAuth2Config(c), + Cfg: config, }) } diff --git a/cmd/convert.go b/cmd/convert.go index 30e7d01e11a91..d9b89495c1bf0 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -35,7 +35,7 @@ func runConvert(ctx *cli.Context) error { log.Info("Log path: %s", setting.Log.RootPath) log.Info("Configuration file: %s", setting.CustomConf) - if !setting.Database.UseMySQL { + if !setting.Database.Type.IsMySQL() { fmt.Println("This command can only be used with a MySQL database") return nil } diff --git a/cmd/dump.go b/cmd/dump.go index 600ec4f32eb08..c802849f8e50f 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -279,7 +279,7 @@ func runDump(ctx *cli.Context) error { }() targetDBType := ctx.String("database") - if len(targetDBType) > 0 && targetDBType != setting.Database.Type { + if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() { log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType) } else { log.Info("Dumping database...") diff --git a/contrib/init/ubuntu/gitea b/contrib/init/ubuntu/gitea new file mode 100644 index 0000000000000..da56b6e4a9750 --- /dev/null +++ b/contrib/init/ubuntu/gitea @@ -0,0 +1,84 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: gitea +# Required-Start: $syslog $network +# Required-Stop: $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: A self-hosted Git service written in Go. +# Description: A self-hosted Git service written in Go. +### END INIT INFO + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin +DESC="Gitea - Git with a cup of tea" +NAME=gitea +SERVICEVERBOSE=yes +PIDFILE=/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME +WORKINGDIR=/var/lib/$NAME +DAEMON=/usr/local/bin/$NAME +DAEMON_ARGS="web -c /etc/$NAME/app.ini" +USER=git +STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/1/KILL/5}" + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +do_start() +{ + GITEA_ENVS="USER=$USER GITEA_WORK_DIR=$WORKINGDIR HOME=/home/$USER" + GITEA_EXEC="$DAEMON -- $DAEMON_ARGS" + sh -c "start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile \\ + --background --chdir $WORKINGDIR --chuid $USER \\ + --exec /bin/bash -- -c '/usr/bin/env $GITEA_ENVS $GITEA_EXEC'" +} + +do_stop() +{ + start-stop-daemon --stop --quiet --retry=$STOP_SCHEDULE --pidfile $PIDFILE --name $NAME --oknodo + rm -f $PIDFILE +} + +do_status() +{ + if [ -f $PIDFILE ]; then + if kill -0 $(cat "$PIDFILE"); then + echo "$NAME is running, PID is $(cat $PIDFILE)" + else + echo "$NAME process is dead, but pidfile exists" + fi + else + echo "$NAME is not running" + fi +} + +case "$1" in + start) + echo "Starting $DESC" "$NAME" + do_start + ;; + stop) + echo "Stopping $DESC" "$NAME" + do_stop + ;; + status) + do_status + ;; + restart) + echo "Restarting $DESC" "$NAME" + do_stop + do_start + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|status|restart}" >&2 + exit 2 + ;; +esac + +exit 0 diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index ca0565e49f072..b2b5af0af8e03 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -576,6 +576,22 @@ ROUTER = console ;; The routing level will default to that of the system but individual router level can be set in ;; [log..router] LEVEL ;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Print request id which parsed from request headers in access log, when access log is enabled. +;; * E.g: +;; * In request Header: X-Request-ID: test-id-123 +;; * Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID +;; * Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "test-id-123" +;; +;; If you configure more than one in the .ini file, it will match in the order of configuration, +;; and the first match will be finally printed in the log. +;; * E.g: +;; * In reuqest Header: X-Trace-ID: trace-id-1q2w3e4r +;; * Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID, X-Trace-ID, X-Req-ID +;; * Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "trace-id-1q2w3e4r" +;; +;; REQUEST_ID_HEADERS = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index a480eb0aedd19..b67d6cdf5f688 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -881,7 +881,13 @@ Default templates for project boards: - `Identity`: the SignedUserName or `"-"` if not logged in. - `Start`: the start time of the request. - `ResponseWriter`: the responseWriter from the request. + - `RequestID`: the value matching REQUEST_ID_HEADERS(default: `-`, if not matched). - You must be very careful to ensure that this template does not throw errors or panics as this template runs outside of the panic/recovery script. +- `REQUEST_ID_HEADERS`: **\**: You can configure multiple values that are splited by comma here. It will match in the order of configuration, and the first match will be finally printed in the access log. + - e.g. + - In the Request Header: X-Request-ID: **test-id-123** + - Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID + - Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "**test-id-123**" ... ### Log subsections (`log.name`, `log.name.*`) diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md index aae64d97bac1e..84186d0e9af1b 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -262,7 +262,22 @@ test01.xls: application/vnd.ms-excel; charset=binary - `ROOT_PATH`: 日志文件根目录。 - `MODE`: 日志记录模式,默认是为 `console`。如果要写到多个通道,用逗号分隔 -- `LEVEL`: 日志级别,默认为`Trace`。 +- `LEVEL`: 日志级别,默认为 `Trace`。 +- `DISABLE_ROUTER_LOG`: 关闭日志中的路由日志。 +- `ENABLE_ACCESS_LOG`: 是否开启 Access Log, 默认为 false。 +- `ACCESS_LOG_TEMPLATE`: `access.log` 输出内容的模板,默认模板:**`{{.Ctx.RemoteAddr}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}\" \"{{.Ctx.Req.UserAgent}}"`** + 模板支持以下参数: + - `Ctx`: 请求上下文。 + - `Identity`: 登录用户名,默认: “`-`”。 + - `Start`: 请求开始时间。 + - `ResponseWriter`: + - `RequestID`: 从请求头中解析得到的与 `REQUEST_ID_HEADERS` 匹配的值,默认: “`-`”。 + - 一定要谨慎配置该模板,否则可能会引起panic. +- `REQUEST_ID_HEADERS`: 从 Request Header 中匹配指定 Key,并将匹配到的值输出到 `access.log` 中(需要在 `ACCESS_LOG_TEMPLATE` 中指定输出位置)。如果在该参数中配置多个 Key, 请用逗号分割,程序将按照配置的顺序进行匹配。 + - 示例: + - 请求头: X-Request-ID: **test-id-123** + - 配置文件: REQUEST_ID_HEADERS = X-Request-ID + - 日志输出: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "**test-id-123**" ... ## Cron (`cron`) diff --git a/docs/content/doc/advanced/customizing-gitea.en-us.md b/docs/content/doc/advanced/customizing-gitea.en-us.md index 18fc1b3e73d16..bad6342aad44d 100644 --- a/docs/content/doc/advanced/customizing-gitea.en-us.md +++ b/docs/content/doc/advanced/customizing-gitea.en-us.md @@ -282,9 +282,22 @@ To add custom .gitignore, add a file with existing [.gitignore rules](https://gi ### Labels -To add a custom label set, add a file that follows the [label format](https://github.com/go-gitea/gitea/blob/main/options/label/Default) to `$GITEA_CUSTOM/options/label` +Starting with Gitea 1.19, you can add a file that follows the [YAML label format](https://github.com/go-gitea/gitea/blob/main/options/label/Advanced.yaml) to `$GITEA_CUSTOM/options/label`: + +```yaml +labels: + - name: "foo/bar" # name of the label that will appear in the dropdown + exclusive: true # whether to use the exclusive namespace for scoped labels. scoped delimiter is / + color: aabbcc # hex colour coding + description: Some label # long description of label intent + ``` + +The [legacy file format](https://github.com/go-gitea/gitea/blob/main/options/label/Default) can still be used following the format below, however we strongly recommend using the newer YAML format instead. + `#hex-color label name ; label description` +For more information, see the [labels documentation]({{< relref "doc/usage/labels.en-us.md" >}}). + ### Licenses To add a custom license, add a file with the license text to `$GITEA_CUSTOM/options/license` diff --git a/docs/content/doc/developers/hacking-on-gitea.zh-cn.md b/docs/content/doc/developers/hacking-on-gitea.zh-cn.md index 3e8cd3ec573a7..9c9141bf50cd4 100644 --- a/docs/content/doc/developers/hacking-on-gitea.zh-cn.md +++ b/docs/content/doc/developers/hacking-on-gitea.zh-cn.md @@ -1,6 +1,6 @@ --- date: "2016-12-01T16:00:00+02:00" -title: "加入 Gitea 开源" +title: "玩转 Gitea" slug: "hacking-on-gitea" weight: 10 toc: false @@ -8,36 +8,342 @@ draft: false menu: sidebar: parent: "developers" - name: "加入 Gitea 开源" + name: "玩转 Gitea" weight: 10 identifier: "hacking-on-gitea" --- # Hacking on Gitea -首先你需要一些运行环境,这和 [从源代码安装]({{< relref "doc/installation/from-source.zh-cn.md" >}}) 相同,如果你还没有设置好,可以先阅读那个章节。 +**目录** -如果你想为 Gitea 贡献代码,你需要 Fork 这个项目并且以 `master` 为开发分支。Gitea 使用 Govendor -来管理依赖,因此所有依赖项都被工具自动 copy 在 vendor 子目录下。用下面的命令来下载源码: +{{< toc >}} -``` -go get -d code.gitea.io/gitea -``` +## 快速入门 + +要获得快速工作的开发环境,您可以使用 Gitpod。 + +[![在 Gitpod 中打开](/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/go-gitea/gitea) + +## 安装 Golang + +您需要 [安装 go]( https://golang.org/doc/install ) 并设置您的 go 环境。 + +接下来,[使用 npm 安装 Node.js](https://nodejs.org/en/download/) ,这是构建 +JavaScript 和 CSS 文件的必要工具。最低支持的 Node.js 版本是 {{< min-node-version >}} +并且推荐使用最新的 LTS 版本。 + +**注意** :当执行需要外部工具的 make 任务时,比如 +`make watch-backend`,Gitea 会自动下载并构建这些必要的组件。为了能够使用这些,你必须 +将 `"$GOPATH"/bin` 目录加入到可执行路径上。如果你不把go bin目录添加到可执行路径你必须手动 +指定可执行程序路径。 -然后你可以在 Github 上 fork [Gitea 项目](https://github.com/go-gitea/gitea),之后可以通过下面的命令进入源码目录: +**注意2** :Go版本 {{< min-go-version >}} 或更高版本是必须的。Gitea 使用 `gofmt` 来 +格式化源代码。然而,`gofmt` 的结果可能因 `go` 的版本而有差异。因此推荐安装我们持续集成使用 +的 Go版本。截至上次更新,Go 版本应该是 {{< go-version >}}。 +## 安装 Make + +Gitea 大量使用 `Make` 来自动化任务和改进开发。本指南涵盖了如何安装 Make。 + +### 在 Linux 上 + +使用包管理器安装。 + +在 Ubuntu/Debian 上: + +```bash +sudo apt-get install make ``` -cd $GOPATH/src/code.gitea.io/gitea + +在 Fedora/RHEL/CentOS 上: + +```bash +sudo yum install make ``` -要创建 pull requests 你还需要在源码中新增一个 remote 指向你 Fork 的地址,直接推送到 origin 的话会告诉你没有写权限: +### 在 Windows 上 + +Make 的这三个发行版都可以在 Windows 上运行: +- [单个二进制构建]( http://www.equation.com/servlet/equation.cmd?fa=make )。复制到某处并添加到 `PATH`。 + - [32 位版本](http://www.equation.com/ftpdir/make/32/make.exe) + - [64 位版本](http://www.equation.com/ftpdir/make/64/make.exe) +- [MinGW-w64](https://www.mingw-w64.org) / [MSYS2](https://www.msys2.org/)。 + - MSYS2 是一个工具和库的集合,为您提供一个易于使用的环境来构建、安装和运行本机 Windows 软件,它包括 MinGW-w64。 + - 在 MingGW-w64 中,二进制文件称为 `mingw32-make.exe` 而不是 `make.exe`。将 `bin` 文件夹添加到 `PATH`。 + - 在 MSYS2 中,您可以直接使用 `make`。请参阅 [MSYS2 移植](https://www.msys2.org/wiki/Porting/)。 + - 要使用 CGO_ENABLED(例如:SQLite3)编译 Gitea,您可能需要使用 [tdm-gcc](https://jmeubank.github.io/tdm-gcc/) 而不是 MSYS2 gcc,因为 MSYS2 gcc 标头缺少一些 Windows -只有 CRT 函数像 _beginthread 一样。 +- [Chocolatey包管理器]( https://chocolatey.org/packages/make )。运行`choco install make` + +**注意** :如果您尝试在 Windows 命令提示符下使用 make 进行构建,您可能会遇到问题。建议使用上述提示(Git bash 或 MinGW),但是如果您只有命令提示符(或可能是 PowerShell),则可以使用 [set](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/set_1) 命令,例如 `set TAGS=bindata`。 + +## 下载并克隆 Gitea 源代码 + +获取源代码的推荐方法是使用 `git clone`。 + +```bash +git clone https://github.com/go-gitea/gitea ``` + +(自从go modules出现后,不再需要构建 go 项目从 `$GOPATH` 中获取,因此不再推荐使用 `go get` 方法。) + +## 派生 Gitea + +如上所述下载主要的 Gitea 源代码。然后,派生 [Gitea 仓库](https://github.com/go-gitea/gitea), +并为您的本地仓库切换 git 远程源,或添加另一个远程源: + +```bash +# 将原来的 Gitea origin 重命名为 upstream git remote rename origin upstream -git remote add origin git@github.com:/gitea.git +git remote add origin "git@github.com:$GITHUB_USERNAME/gitea.git" git fetch --all --prune ``` -然后你就可以开始开发了。你可以看一下 `Makefile` 的内容。`make test` 可以运行测试程序, `make build` 将生成一个 `gitea` 可运行文件在根目录。如果你的提交比较复杂,尽量多写一些单元测试代码。 +或者: + +```bash +# 为我们的 fork 添加新的远程 +git remote add "$FORK_NAME" "git@github.com:$GITHUB_USERNAME/gitea.git" +git fetch --all --prune +``` + +为了能够创建合并请求,应将分叉存储库添加为 Gitea 本地仓库的远程,否则无法推送更改。 + +## 构建 Gitea(基本) + +看看我们的 +说明 +关于如何 从源代码构建 。 + +从源代码构建的最简单推荐方法是: + +```bash +TAGS="bindata sqlite sqlite_unlock_notify" make build +``` + +`build` 目标将同时执行 `frontend` 和 `backend` 子目标。如果存在 `bindata` 标签,资源文件将被编译成二进制文件。建议在进行前端开发时省略 `bindata` 标签,以便实时反映更改。 + +有关所有可用的 `make` 目标,请参阅 `make help`。另请参阅 [`.drone.yml`](https://github.com/go-gitea/gitea/blob/main/.drone.yml) 以了解我们的持续集成是如何工作的。 + +## 持续构建 + +要在源文件更改时运行并持续构建: + +```bash +# 对于前端和后端 +make watch + +# 或者:只看前端文件(html/js/css) +make watch-frontend + +# 或者:只看后端文件 (go) +make watch-backend +``` + +在 macOS 上,监视所有后端源文件可能会达到默认的打开文件限制,这可以通过当前 shell 的 `ulimit -n 12288` 或所有未来 shell 的 shell 启动文件来增加。 + +### 格式化、代码分析和拼写检查 + +我们的持续集成将拒绝未通过代码检查(包括格式检查、代码分析和拼写检查)的 PR。 + +你应该格式化你的代码: + +```bash +make fmt +``` + +并检查源代码: + +```bash +# lint 前端和后端代码 +make lint +# 仅 lint 后端代码 +make lint-backend +``` + +**注意** :`gofmt` 的结果取决于 `go` 的版本。您应该运行与持续集成相同的 go 版本。 + +### 处理 JS 和 CSS + +前端开发应遵循 [Guidelines for Frontend Development]({{ < 相关参考 "doc/developers/guidelines-frontend.en-us.md" > }}) + +要使用前端资源构建,请使用上面提到的“watch-frontend”目标或只构建一次: + +```bash +make build && ./gitea +``` + +在提交之前,确保 linters 通过: + +```bash +make lint-frontend +``` + +### 配置本地 ElasticSearch 实例 + +使用 docker 启动本地 ElasticSearch 实例: + +```sh +mkdir -p $(pwd) /data/elasticsearch +sudo chown -R 1000:1000 $(pwd) /data/elasticsearch +docker run --rm --memory= "4g" -p 127.0.0.1:9200:9200 -p 127.0.0.1:9300:9300 -e "discovery.type=single-node" -v "$(pwd)/data /elasticsearch:/usr/share/elasticsearch/data" docker.elastic.co/elasticsearch/elasticsearch:7.16.3 +``` + +配置`app.ini`: + +```ini +[indexer] +ISSUE_INDEXER_TYPE = elasticsearch +ISSUE_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200 +REPO_INDEXER_ENABLED = true +REPO_INDEXER_TYPE = elasticsearch +REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200 +``` + +### 构建和添加 SVGs + +SVG 图标是使用 `make svg` 目标构建的,该目标将 `build/generate-svg.js` 中定义的图标源编译到输出目录 `public/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。 + +### 构建 Logo + +Gitea Logo的 PNG 和 SVG 版本是使用 `TAGS="gitea" make generate-images` 目标从单个 SVG 源文件 assets/logo.svg 构建的。要运行它,Node.js 和 npm 必须可用。 + +通过更新 `assets/logo.svg` 并运行 `make generate-images`,同样的过程也可用于从 SVG 源文件生成自定义 Logo PNG。忽略 gitea 编译选项将仅更新用户指定的 LOGO 文件。 + +### 更新 API + +创建新的 API 路由或修改现有的 API 路由时,您**必须** +更新和/或创建 [Swagger](https://swagger.io/docs/specification/2-0/what-is-swagger/) +这些使用 [go-swagger](https://goswagger.io/) 评论的文档。 +[规范]( https://goswagger.io/use/spec.html#annotation-syntax )中描述了这些注释的结构。 +如果您想了解更多有关 Swagger 结构的信息,可以查看 +[Swagger 2.0 文档](https://swagger.io/docs/specification/2-0/basic-structure/) +或与添加新 API 端点的先前 PR 进行比较,例如 [PR #5483](https://github.com/go-gitea/gitea/pull/5843/files#diff-2e0a7b644cf31e1c8ef7d76b444fe3aaR20) + +您应该注意不要破坏下游用户依赖的 API。在稳定的 API 上,一般来说添加是可以接受的,但删除 +或对 API 进行根本性更改将会被拒绝。 + +创建或更改 API 端点后,请用以下命令重新生成 Swagger 文档: + +```bash +make generate-swagger +``` + +您应该验证生成的 Swagger 文件并使用以下命令对其进行拼写检查: + +```bash +make swagger-validate misspell-check +``` + +您应该提交更改后的 swagger JSON 文件。持续集成服务器将使用以下方法检查是否已完成: + +```bash +make swagger-check +``` + +**注意** :请注意,您应该使用 Swagger 2.0 文档,而不是 OpenAPI 3 文档。 + +### 创建新的配置选项 + +创建新的配置选项时,将它们添加到 `modules/setting` 的对应文件。您应该将信息添加到 `custom/conf/app.ini` +并到 配置备忘单 +在 `docs/content/doc/advanced/config-cheat-sheet.en-us.md` 中找到 + +### 更改Logo + +更改 Gitea Logo SVG 时,您将需要运行并提交结果的: + +```bash +make generate-images +``` + +这将创建必要的 Gitea 图标和其他图标。 + +### 数据库迁移 + +如果您对数据库中的任何数据库持久结构进行重大更改 +`models/` 目录,您将需要进行新的迁移。可以找到这些 +在 `models/migrations/` 中。您可以确保您的迁移适用于主要 +数据库类型使用: + +```bash +make test-sqlite-migration # 将 SQLite 切换为适当的数据库 +``` + +## 测试 + +Gitea 运行两种类型的测试:单元测试和集成测试。 + +### 单元测试 + +`go test` 系统中的`*_test.go` 涵盖了单元测试。 +您可以设置环境变量 `GITEA_UNIT_TESTS_LOG_SQL=1` 以在详细模式下运行测试时显示所有 SQL 语句(即设置`GOTESTFLAGS=-v` 时)。 + +```bash +TAGS="bindata sqlite sqlite_unlock_notify" make test # Runs the unit tests +``` + +### 集成测试 + +单元测试不会也不能完全单独测试 Gitea。因此,我们编写了集成测试;但是,这些依赖于数据库。 + +```bash +TAGS="bindata sqlite sqlite_unlock_notify" make build test-sqlite +``` + +将在 SQLite 环境中运行集成测试。集成测试需要安装 `git lfs`。其他数据库测试可用,但 +可能需要适应当地环境。 + +看看 [`tests/integration/README.md`](https://github.com/go-gitea/gitea/blob/main/tests/integration/README.md) 有关更多信息以及如何运行单个测试。 + +### 测试 PR + +我们的持续集成将测试代码是否通过了单元测试,并且所有支持的数据库都将在 Docker 环境中通过集成测试。 +还将测试从几个最新版本的 Gitea 迁移。 + +请在PR中附带提交适当的单元测试和集成测试。 + +## 网站文档 + +该网站的文档位于 `docs/` 中。如果你改变了文档内容,你可以使用以下测试方法进行持续集成: + +```bash +# 来自 Gitea 中的 docs 目录 +make trans-copy clean build +``` + +运行此任务依赖于 [Hugo](https://gohugo.io/)。请注意:这可能会生成一些未跟踪的 Git 对象, +需要被清理干净。 + +## Visual Studio Code + +`contrib/ide/vscode` 中为 Visual Studio Code 提供了 `launch.json` 和 `tasks.json`。查看 +[`contrib/ide/README.md`](https://github.com/go-gitea/gitea/blob/main/contrib/ide/README.md) 了解更多信息。 + +## Goland + +单击 `/main.go` 中函数 `func main()` 上的 `Run Application` 箭头 +可以快速启动一个可调试的 Gitea 实例。 + +`Run/Debug Configuration` 中的 `Output Directory` 必须设置为 +gitea 项目目录(包含 `main.go` 和 `go.mod`), +否则,启动实例的工作目录是 GoLand 的临时目录 +并防止 Gitea 在开发环境中加载动态资源(例如:模板)。 + +要在 GoLand 中使用 SQLite 运行单元测试,请设置 `-tags sqlite,sqlite_unlock_notify` +在 `运行/调试配置` 的 `Go 工具参数` 中。 + +## 提交 PR + +对更改感到满意后,将它们推送并打开拉取请求。它建议您允许 Gitea Managers 和 Owners 修改您的 PR +分支,因为我们需要在合并之前将其更新为 main 和/或可能是能够直接帮助解决问题。 + +任何 PR 都需要 Gitea 维护者的两次批准,并且需要通过持续集成。看看我们的 +[CONTRIBUTING.md](https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md) +文档。 + +如果您需要更多帮助,请访问 [Discord](https://discord.gg/gitea) #Develop 频道 +并在那里聊天。 -好了,到这里你已经设置好了所有的开发 Gitea 所需的环境。欢迎成为 Gitea 的 Contributor。 +现在,您已准备好 Hacking Gitea。 diff --git a/docs/content/doc/developers/oauth2-provider.en-us.md b/docs/content/doc/developers/oauth2-provider.en-us.md index 17c12d22f2423..1ef30a7f0e4a2 100644 --- a/docs/content/doc/developers/oauth2-provider.en-us.md +++ b/docs/content/doc/developers/oauth2-provider.en-us.md @@ -60,6 +60,7 @@ Gitea supports the following scopes for tokens: |     **write:public_key** | Grant read/write access to public keys | |     **read:public_key** | Grant read-only access to public keys | | **admin:org_hook** | Grants full access to organizational-level hooks | +| **admin:user_hook** | Grants full access to user-level hooks | | **notification** | Grants full access to notifications | | **user** | Grants full access to user profile info | |     **read:user** | Grants read access to user's profile | diff --git a/docs/content/doc/packages/maven.en-us.md b/docs/content/doc/packages/maven.en-us.md index 22da3c16d2945..2ec5ca2ab710c 100644 --- a/docs/content/doc/packages/maven.en-us.md +++ b/docs/content/doc/packages/maven.en-us.md @@ -23,7 +23,7 @@ Publish [Maven](https://maven.apache.org) packages for your user or organization ## Requirements To work with the Maven package registry, you can use [Maven](https://maven.apache.org/install.html) or [Gradle](https://gradle.org/install/). -The following examples use `Maven`. +The following examples use `Maven` and `Gradle Groovy`. ## Configuring the package registry @@ -73,6 +73,40 @@ Afterwards add the following sections to your project `pom.xml` file: | `access_token` | Your [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}). | | `owner` | The owner of the package. | +### Gradle variant + +When you plan to add some packages from Gitea instance in your project, you should add it in repositories section: + +```groovy +repositories { + // other repositories + maven { url "https://gitea.example.com/api/packages/{owner}/maven" } +} +``` + +In Groovy gradle you may include next script in your publishing part: + +```groovy +publishing { + // other settings of publication + repositories { + maven { + name = "Gitea" + url = uri("https://gitea.example.com/api/packages/{owner}/maven") + + credentials(HttpHeaderCredentials) { + name = "Authorization" + value = "token {access_token}" + } + + authentication { + header(HttpHeaderAuthentication) + } + } + } +} +``` + ## Publish a package To publish a package simply run: @@ -81,6 +115,12 @@ To publish a package simply run: mvn deploy ``` +Or call `gradle` with task `publishAllPublicationsToGiteaRepository` in case you are using gradle: + +```groovy +./gradlew publishAllPublicationsToGiteaRepository +``` + If you want to publish a prebuild package to the registry, you can use [`mvn deploy:deploy-file`](https://maven.apache.org/plugins/maven-deploy-plugin/deploy-file-mojo.html): ```shell @@ -105,6 +145,12 @@ To install a Maven package from the package registry, add a new dependency to yo ``` +And analog in gradle groovy: + +```groovy +implementation "com.test.package:test_project:1.0.0" +``` + Afterwards run: ```shell diff --git a/docs/content/doc/usage/labels.en-us.md b/docs/content/doc/usage/labels.en-us.md index bf07c074edc68..8e5ff1cf8d9f1 100644 --- a/docs/content/doc/usage/labels.en-us.md +++ b/docs/content/doc/usage/labels.en-us.md @@ -23,17 +23,15 @@ For repositories, labels can be created by going to `Issues` and clicking on `La For organizations, you can define organization-wide labels that are shared with all organization repositories, including both already-existing repositories as well as newly created ones. Organization-wide labels can be created in the organization `Settings`. -Labels have a mandatory name, a mandatory color, an optional description, and must either be exclusive or not (see `Scoped labels` below). +Labels have a mandatory name, a mandatory color, an optional description, and must either be exclusive or not (see `Scoped Labels` below). When you create a repository, you can ensure certain labels exist by using the `Issue Labels` option. This option lists a number of available label sets that are [configured globally on your instance](../customizing-gitea/#labels). Its contained labels will all be created as well while creating the repository. ## Scoped Labels -A scoped label is a label that contains `/` in its name (not at either end of the name). For example labels `kind/bug` and `kind/enhancement` both have scope `kind`. Such labels will display the scope with slightly darker color. +Scoped labels are used to ensure at most a single label with the same scope is assigned to an issue or pull request. For example, if labels `kind/bug` and `kind/enhancement` have the Exclusive option set, an issue can only be classified as a bug or an enhancement. -The scope of a label is determined based on the **last** `/`, so for example the scope of label `scope/subscope/item` is `scope/subscope`. - -Scoped labels can be marked as exclusive. This ensures at most a single label with the same scope is assigned to an issue or pull request. For example, if `kind/bug` and `kind/enhancement` are marked exclusive, an issue can only be classified as a bug or an enhancement. +A scoped label must contain `/` in its name (not at either end of the name). The scope of a label is determined based on the **last** `/`, so for example the scope of label `scope/subscope/item` is `scope/subscope`. ## Filtering by Label diff --git a/models/activities/action.go b/models/activities/action.go index 1412d2c051d60..c1d17517baf08 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -99,7 +99,7 @@ func (a *Action) TableIndices() []*schemas.Index { actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted") indices := []*schemas.Index{actUserIndex, repoIndex} - if setting.Database.UsePostgreSQL { + if setting.Database.Type.IsPostgreSQL() { cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType) cudIndex.AddColumn("created_unix", "user_id", "is_deleted") indices = append(indices, cudIndex) @@ -640,7 +640,7 @@ func DeleteIssueActions(ctx context.Context, repoID, issueID int64) error { // CountActionCreatedUnixString count actions where created_unix is an empty string func CountActionCreatedUnixString(ctx context.Context) (int64, error) { - if setting.Database.UseSQLite3 { + if setting.Database.Type.IsSQLite3() { return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(Action)) } return 0, nil @@ -648,7 +648,7 @@ func CountActionCreatedUnixString(ctx context.Context) (int64, error) { // FixActionCreatedUnixString set created_unix to zero if it is an empty string func FixActionCreatedUnixString(ctx context.Context) (int64, error) { - if setting.Database.UseSQLite3 { + if setting.Database.Type.IsSQLite3() { res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ""`) if err != nil { return 0, err diff --git a/models/activities/action_test.go b/models/activities/action_test.go index 2fd86bb8f6e76..7044bcc004abb 100644 --- a/models/activities/action_test.go +++ b/models/activities/action_test.go @@ -243,7 +243,7 @@ func TestGetFeedsCorrupted(t *testing.T) { } func TestConsistencyUpdateAction(t *testing.T) { - if !setting.Database.UseSQLite3 { + if !setting.Database.Type.IsSQLite3() { t.Skip("Test is only for SQLite database.") } assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/activities/user_heatmap.go b/models/activities/user_heatmap.go index 3370103a556f2..d3f0f0db73b27 100644 --- a/models/activities/user_heatmap.go +++ b/models/activities/user_heatmap.go @@ -39,9 +39,9 @@ func getUserHeatmapData(user *user_model.User, team *organization.Team, doer *us groupBy := "created_unix / 900 * 900" groupByName := "timestamp" // We need this extra case because mssql doesn't allow grouping by alias switch { - case setting.Database.UseMySQL: + case setting.Database.Type.IsMySQL(): groupBy = "created_unix DIV 900 * 900" - case setting.Database.UseMSSQL: + case setting.Database.Type.IsMSSQL(): groupByName = groupBy } diff --git a/models/auth/token_scope.go b/models/auth/token_scope.go index 38733a1c8f305..06c89fecc2e4d 100644 --- a/models/auth/token_scope.go +++ b/models/auth/token_scope.go @@ -32,6 +32,8 @@ const ( AccessTokenScopeAdminOrgHook AccessTokenScope = "admin:org_hook" + AccessTokenScopeAdminUserHook AccessTokenScope = "admin:user_hook" + AccessTokenScopeNotification AccessTokenScope = "notification" AccessTokenScopeUser AccessTokenScope = "user" @@ -64,7 +66,7 @@ type AccessTokenScopeBitmap uint64 const ( // AccessTokenScopeAllBits is the bitmap of all access token scopes, except `sudo`. AccessTokenScopeAllBits AccessTokenScopeBitmap = AccessTokenScopeRepoBits | - AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits | + AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits | AccessTokenScopeAdminUserHookBits | AccessTokenScopeNotificationBits | AccessTokenScopeUserBits | AccessTokenScopeDeleteRepoBits | AccessTokenScopePackageBits | AccessTokenScopeAdminGPGKeyBits | AccessTokenScopeAdminApplicationBits @@ -86,6 +88,8 @@ const ( AccessTokenScopeAdminOrgHookBits AccessTokenScopeBitmap = 1 << iota + AccessTokenScopeAdminUserHookBits AccessTokenScopeBitmap = 1 << iota + AccessTokenScopeNotificationBits AccessTokenScopeBitmap = 1 << iota AccessTokenScopeUserBits AccessTokenScopeBitmap = 1< 0 { + if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 { // OK whilst we sort out our schema issues - create a schema aware postgres registerPostgresSchemaDriver() engine, err = xorm.NewEngine("postgresschema", connStr) } else { - engine, err = xorm.NewEngine(setting.Database.Type, connStr) + engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr) } if err != nil { diff --git a/models/db/index.go b/models/db/index.go index f840a62c8913f..7609d8fb6e6c2 100644 --- a/models/db/index.go +++ b/models/db/index.go @@ -73,7 +73,7 @@ func postgresGetNextResourceIndex(ctx context.Context, tableName string, groupID // GetNextResourceIndex generates a resource index, it must run in the same transaction where the resource is created func GetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) { - if setting.Database.UsePostgreSQL { + if setting.Database.Type.IsPostgreSQL() { return postgresGetNextResourceIndex(ctx, tableName, groupID) } diff --git a/models/db/iterate_test.go b/models/db/iterate_test.go index a713fe0d8b85c..6bcf740c238b0 100644 --- a/models/db/iterate_test.go +++ b/models/db/iterate_test.go @@ -25,7 +25,7 @@ func TestIterate(t *testing.T) { return nil }) assert.NoError(t, err) - assert.EqualValues(t, 83, repoCnt) + assert.EqualValues(t, 84, repoCnt) err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error { reopUnit2 := repo_model.RepoUnit{ID: repoUnit.ID} diff --git a/models/db/list_test.go b/models/db/list_test.go index ffef1e4948eea..1295692cec856 100644 --- a/models/db/list_test.go +++ b/models/db/list_test.go @@ -35,11 +35,11 @@ func TestFind(t *testing.T) { var repoUnits []repo_model.RepoUnit err := db.Find(db.DefaultContext, &opts, &repoUnits) assert.NoError(t, err) - assert.EqualValues(t, 83, len(repoUnits)) + assert.EqualValues(t, 84, len(repoUnits)) cnt, err := db.Count(db.DefaultContext, &opts, new(repo_model.RepoUnit)) assert.NoError(t, err) - assert.EqualValues(t, 83, cnt) + assert.EqualValues(t, 84, cnt) repoUnits = make([]repo_model.RepoUnit, 0, 10) newCnt, err := db.FindAndCount(db.DefaultContext, &opts, &repoUnits) diff --git a/models/db/sequence.go b/models/db/sequence.go index 6d801d022fbc0..f49ad935de099 100644 --- a/models/db/sequence.go +++ b/models/db/sequence.go @@ -13,7 +13,7 @@ import ( // CountBadSequences looks for broken sequences from recreate-table mistakes func CountBadSequences(_ context.Context) (int64, error) { - if !setting.Database.UsePostgreSQL { + if !setting.Database.Type.IsPostgreSQL() { return 0, nil } @@ -34,7 +34,7 @@ func CountBadSequences(_ context.Context) (int64, error) { // FixBadSequences fixes for broken sequences from recreate-table mistakes func FixBadSequences(_ context.Context) error { - if !setting.Database.UsePostgreSQL { + if !setting.Database.Type.IsPostgreSQL() { return nil } diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index 503b8c9ddfa46..ef0b7c1a941e2 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -569,3 +569,9 @@ type: 3 config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" created_unix: 946684810 + +- + id: 84 + repo_id: 56 + type: 1 + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index dd8facb7a3134..32ba8744d4522 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1634,3 +1634,16 @@ is_private: true num_issues: 1 status: 0 + +- + id: 56 + owner_id: 2 + owner_name: user2 + lower_name: readme-test + name: readme-test + default_branch: master + is_empty: false + is_archived: false + is_private: true + status: 0 + num_issues: 0 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index c6081f07d06b7..ce54defacdc67 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -66,7 +66,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 11 + num_repos: 12 num_teams: 0 num_members: 0 visibility: 0 diff --git a/models/fixtures/webhook.yml b/models/fixtures/webhook.yml index 5563dcada7b65..f62bae1f311ce 100644 --- a/models/fixtures/webhook.yml +++ b/models/fixtures/webhook.yml @@ -16,7 +16,7 @@ - id: 3 - org_id: 3 + owner_id: 3 repo_id: 3 url: www.example.com/url3 content_type: 1 # json diff --git a/models/git/commit_status.go b/models/git/commit_status.go index 489507f710503..82cbb2363739f 100644 --- a/models/git/commit_status.go +++ b/models/git/commit_status.go @@ -65,7 +65,7 @@ func postgresGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) // GetNextCommitStatusIndex retried 3 times to generate a resource index func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) { - if setting.Database.UsePostgreSQL { + if setting.Database.Type.IsPostgreSQL() { return postgresGetCommitStatusIndex(ctx, repoID, sha) } diff --git a/models/git/lfs_lock.go b/models/git/lfs_lock.go index 25480f3f96541..178fa72f09bf6 100644 --- a/models/git/lfs_lock.go +++ b/models/git/lfs_lock.go @@ -6,7 +6,6 @@ package git import ( "context" "fmt" - "path" "strings" "time" @@ -17,6 +16,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) // LFSLock represents a git lfs lock of repository. @@ -34,11 +34,7 @@ func init() { // BeforeInsert is invoked from XORM before inserting an object of this type. func (l *LFSLock) BeforeInsert() { - l.Path = cleanPath(l.Path) -} - -func cleanPath(p string) string { - return path.Clean("/" + p)[1:] + l.Path = util.CleanPath(l.Path) } // CreateLFSLock creates a new lock. @@ -53,7 +49,7 @@ func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLo return nil, err } - lock.Path = cleanPath(lock.Path) + lock.Path = util.CleanPath(lock.Path) lock.RepoID = repo.ID l, err := GetLFSLock(dbCtx, repo, lock.Path) @@ -73,7 +69,7 @@ func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLo // GetLFSLock returns release by given path. func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) (*LFSLock, error) { - path = cleanPath(path) + path = util.CleanPath(path) rel := &LFSLock{RepoID: repo.ID} has, err := db.GetEngine(ctx).Where("lower(path) = ?", strings.ToLower(path)).Get(rel) if err != nil { diff --git a/models/migrations/base/db.go b/models/migrations/base/db.go index dcf99c96ae81f..b038ad73372e4 100644 --- a/models/migrations/base/db.go +++ b/models/migrations/base/db.go @@ -89,7 +89,7 @@ func RecreateTable(sess *xorm.Session, bean interface{}) error { hasID = hasID || (column.IsPrimaryKey && column.IsAutoIncrement) } - if hasID && setting.Database.UseMSSQL { + if hasID && setting.Database.Type.IsMSSQL() { if _, err := sess.Exec(fmt.Sprintf("SET IDENTITY_INSERT `%s` ON", tempTableName)); err != nil { log.Error("Unable to set identity insert for table %s. Error: %v", tempTableName, err) return err @@ -143,7 +143,7 @@ func RecreateTable(sess *xorm.Session, bean interface{}) error { return err } - if hasID && setting.Database.UseMSSQL { + if hasID && setting.Database.Type.IsMSSQL() { if _, err := sess.Exec(fmt.Sprintf("SET IDENTITY_INSERT `%s` OFF", tempTableName)); err != nil { log.Error("Unable to switch off identity insert for table %s. Error: %v", tempTableName, err) return err @@ -151,7 +151,7 @@ func RecreateTable(sess *xorm.Session, bean interface{}) error { } switch { - case setting.Database.UseSQLite3: + case setting.Database.Type.IsSQLite3(): // SQLite will drop all the constraints on the old table if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil { log.Error("Unable to drop old table %s. Error: %v", tableName, err) @@ -178,7 +178,7 @@ func RecreateTable(sess *xorm.Session, bean interface{}) error { return err } - case setting.Database.UseMySQL: + case setting.Database.Type.IsMySQL(): // MySQL will drop all the constraints on the old table if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil { log.Error("Unable to drop old table %s. Error: %v", tableName, err) @@ -205,7 +205,7 @@ func RecreateTable(sess *xorm.Session, bean interface{}) error { log.Error("Unable to recreate uniques on table %s. Error: %v", tableName, err) return err } - case setting.Database.UsePostgreSQL: + case setting.Database.Type.IsPostgreSQL(): var originalSequences []string type sequenceData struct { LastValue int `xorm:"'last_value'"` @@ -296,7 +296,7 @@ func RecreateTable(sess *xorm.Session, bean interface{}) error { } - case setting.Database.UseMSSQL: + case setting.Database.Type.IsMSSQL(): // MSSQL will drop all the constraints on the old table if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil { log.Error("Unable to drop old table %s. Error: %v", tableName, err) @@ -323,7 +323,7 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin // TODO: This will not work if there are foreign keys switch { - case setting.Database.UseSQLite3: + case setting.Database.Type.IsSQLite3(): // First drop the indexes on the columns res, errIndex := sess.Query(fmt.Sprintf("PRAGMA index_list(`%s`)", tableName)) if errIndex != nil { @@ -405,7 +405,7 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin return err } - case setting.Database.UsePostgreSQL: + case setting.Database.Type.IsPostgreSQL(): cols := "" for _, col := range columnNames { if cols != "" { @@ -416,7 +416,7 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil { return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) } - case setting.Database.UseMySQL: + case setting.Database.Type.IsMySQL(): // Drop indexes on columns first sql := fmt.Sprintf("SHOW INDEX FROM %s WHERE column_name IN ('%s')", tableName, strings.Join(columnNames, "','")) res, err := sess.Query(sql) @@ -444,7 +444,7 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil { return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) } - case setting.Database.UseMSSQL: + case setting.Database.Type.IsMSSQL(): cols := "" for _, col := range columnNames { if cols != "" { @@ -543,13 +543,13 @@ func newXORMEngine() (*xorm.Engine, error) { func deleteDB() error { switch { - case setting.Database.UseSQLite3: + case setting.Database.Type.IsSQLite3(): if err := util.Remove(setting.Database.Path); err != nil { return err } return os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm) - case setting.Database.UseMySQL: + case setting.Database.Type.IsMySQL(): db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/", setting.Database.User, setting.Database.Passwd, setting.Database.Host)) if err != nil { @@ -565,7 +565,7 @@ func deleteDB() error { return err } return nil - case setting.Database.UsePostgreSQL: + case setting.Database.Type.IsPostgreSQL(): db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode)) if err != nil { @@ -612,7 +612,7 @@ func deleteDB() error { } return nil } - case setting.Database.UseMSSQL: + 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)) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 585457e474f05..4cbcd95d20df5 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -467,6 +467,8 @@ var migrations = []Migration{ // v244 -> v245 NewMigration("Add NeedApproval to actions tables", v1_20.AddNeedApprovalToActionRun), + // v245 -> v246 + NewMigration("Rename Webhook org_id to owner_id", v1_20.RenameWebhookOrgToOwner), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_12/v139.go b/models/migrations/v1_12/v139.go index 725b8fa30597f..279aa7df87dc4 100644 --- a/models/migrations/v1_12/v139.go +++ b/models/migrations/v1_12/v139.go @@ -13,9 +13,9 @@ func PrependRefsHeadsToIssueRefs(x *xorm.Engine) error { var query string switch { - case setting.Database.UseMSSQL: + case setting.Database.Type.IsMSSQL(): query = "UPDATE `issue` SET `ref` = 'refs/heads/' + `ref` WHERE `ref` IS NOT NULL AND `ref` <> '' AND `ref` NOT LIKE 'refs/%'" - case setting.Database.UseMySQL: + case setting.Database.Type.IsMySQL(): query = "UPDATE `issue` SET `ref` = CONCAT('refs/heads/', `ref`) WHERE `ref` IS NOT NULL AND `ref` <> '' AND `ref` NOT LIKE 'refs/%';" default: query = "UPDATE `issue` SET `ref` = 'refs/heads/' || `ref` WHERE `ref` IS NOT NULL AND `ref` <> '' AND `ref` NOT LIKE 'refs/%'" diff --git a/models/migrations/v1_13/v140.go b/models/migrations/v1_13/v140.go index 3de9eaaf7cdaf..4d87b955fdff7 100644 --- a/models/migrations/v1_13/v140.go +++ b/models/migrations/v1_13/v140.go @@ -41,7 +41,7 @@ func FixLanguageStatsToSaveSize(x *xorm.Engine) error { // Delete language stat statuses truncExpr := "TRUNCATE TABLE" - if setting.Database.UseSQLite3 { + if setting.Database.Type.IsSQLite3() { truncExpr = "DELETE FROM" } diff --git a/models/migrations/v1_13/v145.go b/models/migrations/v1_13/v145.go index c96e79f8a0ee3..ee40bfc77f862 100644 --- a/models/migrations/v1_13/v145.go +++ b/models/migrations/v1_13/v145.go @@ -21,7 +21,7 @@ func IncreaseLanguageField(x *xorm.Engine) error { return err } - if setting.Database.UseSQLite3 { + if setting.Database.Type.IsSQLite3() { // SQLite maps VARCHAR to TEXT without size so we're done return nil } @@ -41,11 +41,11 @@ func IncreaseLanguageField(x *xorm.Engine) error { } switch { - case setting.Database.UseMySQL: + case setting.Database.Type.IsMySQL(): if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE language_stat MODIFY COLUMN language %s", sqlType)); err != nil { return err } - case setting.Database.UseMSSQL: + case setting.Database.Type.IsMSSQL(): // Yet again MSSQL just has to be awkward. // Here we have to drop the constraints first and then rebuild them constraints := make([]string, 0) @@ -71,7 +71,7 @@ func IncreaseLanguageField(x *xorm.Engine) error { if err := sess.CreateUniques(new(LanguageStat)); err != nil { return err } - case setting.Database.UsePostgreSQL: + case setting.Database.Type.IsPostgreSQL(): if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE language_stat ALTER COLUMN language TYPE %s", sqlType)); err != nil { return err } diff --git a/models/migrations/v1_13/v151.go b/models/migrations/v1_13/v151.go index 9490c1778c6a7..9aa71ec29f13f 100644 --- a/models/migrations/v1_13/v151.go +++ b/models/migrations/v1_13/v151.go @@ -17,13 +17,13 @@ import ( func SetDefaultPasswordToArgon2(x *xorm.Engine) error { switch { - case setting.Database.UseMySQL: + case setting.Database.Type.IsMySQL(): _, err := x.Exec("ALTER TABLE `user` ALTER passwd_hash_algo SET DEFAULT 'argon2';") return err - case setting.Database.UsePostgreSQL: + case setting.Database.Type.IsPostgreSQL(): _, err := x.Exec("ALTER TABLE `user` ALTER COLUMN passwd_hash_algo SET DEFAULT 'argon2';") return err - case setting.Database.UseMSSQL: + case setting.Database.Type.IsMSSQL(): // need to find the constraint and drop it, then recreate it. sess := x.NewSession() defer sess.Close() @@ -53,7 +53,7 @@ func SetDefaultPasswordToArgon2(x *xorm.Engine) error { } return sess.Commit() - case setting.Database.UseSQLite3: + case setting.Database.Type.IsSQLite3(): // drop through default: log.Fatal("Unrecognized DB") diff --git a/models/migrations/v1_14/v158.go b/models/migrations/v1_14/v158.go index 7ea80a659ed29..2029829ff9901 100644 --- a/models/migrations/v1_14/v158.go +++ b/models/migrations/v1_14/v158.go @@ -62,7 +62,7 @@ func UpdateCodeCommentReplies(x *xorm.Engine) error { return err } - if setting.Database.UseMSSQL { + if setting.Database.Type.IsMSSQL() { if _, err := sess.Exec(sqlSelect + " INTO #temp_comments" + sqlTail); err != nil { log.Error("unable to create temporary table") return err @@ -72,13 +72,13 @@ func UpdateCodeCommentReplies(x *xorm.Engine) error { comments := make([]*Comment, 0, batchSize) switch { - case setting.Database.UseMySQL: + case setting.Database.Type.IsMySQL(): sqlCmd = sqlSelect + sqlTail + " LIMIT " + strconv.Itoa(batchSize) + ", " + strconv.Itoa(start) - case setting.Database.UsePostgreSQL: + case setting.Database.Type.IsPostgreSQL(): fallthrough - case setting.Database.UseSQLite3: + case setting.Database.Type.IsSQLite3(): sqlCmd = sqlSelect + sqlTail + " LIMIT " + strconv.Itoa(batchSize) + " OFFSET " + strconv.Itoa(start) - case setting.Database.UseMSSQL: + case setting.Database.Type.IsMSSQL(): sqlCmd = "SELECT TOP " + strconv.Itoa(batchSize) + " * FROM #temp_comments WHERE " + "(id NOT IN ( SELECT TOP " + strconv.Itoa(start) + " id FROM #temp_comments ORDER BY id )) ORDER BY id" default: diff --git a/models/migrations/v1_14/v175.go b/models/migrations/v1_14/v175.go index f1b9b974c6aa5..70d72b2600337 100644 --- a/models/migrations/v1_14/v175.go +++ b/models/migrations/v1_14/v175.go @@ -14,7 +14,7 @@ import ( ) func FixPostgresIDSequences(x *xorm.Engine) error { - if !setting.Database.UsePostgreSQL { + if !setting.Database.Type.IsPostgreSQL() { return nil } diff --git a/models/migrations/v1_15/v184.go b/models/migrations/v1_15/v184.go index 48f8b62165cc5..caf41b6048ed4 100644 --- a/models/migrations/v1_15/v184.go +++ b/models/migrations/v1_15/v184.go @@ -54,11 +54,11 @@ func RenameTaskErrorsToMessage(x *xorm.Engine) error { } switch { - case setting.Database.UseMySQL: + case setting.Database.Type.IsMySQL(): if _, err := sess.Exec("ALTER TABLE `task` CHANGE errors message text"); err != nil { return err } - case setting.Database.UseMSSQL: + case setting.Database.Type.IsMSSQL(): if _, err := sess.Exec("sp_rename 'task.errors', 'message', 'COLUMN'"); err != nil { return err } diff --git a/models/migrations/v1_16/v191.go b/models/migrations/v1_16/v191.go index 2d2c3d1a587a5..c618783c08e86 100644 --- a/models/migrations/v1_16/v191.go +++ b/models/migrations/v1_16/v191.go @@ -16,7 +16,7 @@ func AlterIssueAndCommentTextFieldsToLongText(x *xorm.Engine) error { return err } - if setting.Database.UseMySQL { + if setting.Database.Type.IsMySQL() { if _, err := sess.Exec("ALTER TABLE `issue` CHANGE `content` `content` LONGTEXT"); err != nil { return err } diff --git a/models/migrations/v1_17/v217.go b/models/migrations/v1_17/v217.go index 3ca9215f09378..3f970b68a540d 100644 --- a/models/migrations/v1_17/v217.go +++ b/models/migrations/v1_17/v217.go @@ -16,7 +16,7 @@ func AlterHookTaskTextFieldsToLongText(x *xorm.Engine) error { return err } - if setting.Database.UseMySQL { + if setting.Database.Type.IsMySQL() { if _, err := sess.Exec("ALTER TABLE `hook_task` CHANGE `payload_content` `payload_content` LONGTEXT, CHANGE `request_content` `request_content` LONGTEXT, change `response_content` `response_content` LONGTEXT"); err != nil { return err } diff --git a/models/migrations/v1_17/v218.go b/models/migrations/v1_17/v218.go index 675fd1df94fb3..ae91ba0c494bc 100644 --- a/models/migrations/v1_17/v218.go +++ b/models/migrations/v1_17/v218.go @@ -38,7 +38,7 @@ func (*improveActionTableIndicesAction) TableIndices() []*schemas.Index { actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType) actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted") indices := []*schemas.Index{actUserIndex, repoIndex} - if setting.Database.UsePostgreSQL { + if setting.Database.Type.IsPostgreSQL() { cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType) cudIndex.AddColumn("created_unix", "user_id", "is_deleted") indices = append(indices, cudIndex) diff --git a/models/migrations/v1_17/v223.go b/models/migrations/v1_17/v223.go index a23d9916b383a..6c61dbc53ae48 100644 --- a/models/migrations/v1_17/v223.go +++ b/models/migrations/v1_17/v223.go @@ -65,11 +65,11 @@ func RenameCredentialIDBytes(x *xorm.Engine) error { } switch { - case setting.Database.UseMySQL: + case setting.Database.Type.IsMySQL(): if _, err := sess.Exec("ALTER TABLE `webauthn_credential` CHANGE credential_id_bytes credential_id VARBINARY(1024)"); err != nil { return err } - case setting.Database.UseMSSQL: + case setting.Database.Type.IsMSSQL(): if _, err := sess.Exec("sp_rename 'webauthn_credential.credential_id_bytes', 'credential_id', 'COLUMN'"); err != nil { return err } diff --git a/models/migrations/v1_18/v225.go b/models/migrations/v1_18/v225.go index 786772c143bd0..b0ac3777fc248 100644 --- a/models/migrations/v1_18/v225.go +++ b/models/migrations/v1_18/v225.go @@ -16,7 +16,7 @@ func AlterPublicGPGKeyContentFieldsToMediumText(x *xorm.Engine) error { return err } - if setting.Database.UseMySQL { + if setting.Database.Type.IsMySQL() { if _, err := sess.Exec("ALTER TABLE `gpg_key` CHANGE `content` `content` MEDIUMTEXT"); err != nil { return err } diff --git a/models/migrations/v1_19/v232.go b/models/migrations/v1_19/v232.go index 89b595c54375f..9caf587c1e9ca 100644 --- a/models/migrations/v1_19/v232.go +++ b/models/migrations/v1_19/v232.go @@ -16,7 +16,7 @@ func AlterPackageVersionMetadataToLongText(x *xorm.Engine) error { return err } - if setting.Database.UseMySQL { + if setting.Database.Type.IsMySQL() { if _, err := sess.Exec("ALTER TABLE `package_version` MODIFY COLUMN `metadata_json` LONGTEXT"); err != nil { return err } diff --git a/models/migrations/v1_19/v242.go b/models/migrations/v1_19/v242.go index 517c7767b874c..4470835214f34 100644 --- a/models/migrations/v1_19/v242.go +++ b/models/migrations/v1_19/v242.go @@ -17,7 +17,7 @@ func AlterPublicGPGKeyImportContentFieldToMediumText(x *xorm.Engine) error { return err } - if setting.Database.UseMySQL { + if setting.Database.Type.IsMySQL() { if _, err := sess.Exec("ALTER TABLE `gpg_key_import` CHANGE `content` `content` MEDIUMTEXT"); err != nil { return err } diff --git a/models/migrations/v1_20/v245.go b/models/migrations/v1_20/v245.go new file mode 100644 index 0000000000000..466f21c239d09 --- /dev/null +++ b/models/migrations/v1_20/v245.go @@ -0,0 +1,74 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_20 //nolint + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm" +) + +func RenameWebhookOrgToOwner(x *xorm.Engine) error { + type Webhook struct { + OrgID int64 `xorm:"INDEX"` + } + + // This migration maybe rerun so that we should check if it has been run + ownerExist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webhook", "owner_id") + if err != nil { + return err + } + + if ownerExist { + orgExist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webhook", "org_id") + if err != nil { + return err + } + if !orgExist { + return nil + } + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + if err := sess.Sync2(new(Webhook)); err != nil { + return err + } + + if ownerExist { + if err := base.DropTableColumns(sess, "webhook", "owner_id"); err != nil { + return err + } + } + + switch { + case setting.Database.Type.IsMySQL(): + inferredTable, err := x.TableInfo(new(Webhook)) + if err != nil { + return err + } + sqlType := x.Dialect().SQLType(inferredTable.GetColumn("org_id")) + if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `webhook` CHANGE org_id owner_id %s", sqlType)); err != nil { + return err + } + case setting.Database.Type.IsMSSQL(): + if _, err := sess.Exec("sp_rename 'webhook.org_id', 'owner_id', 'COLUMN'"); err != nil { + return err + } + default: + if _, err := sess.Exec("ALTER TABLE `webhook` RENAME COLUMN org_id TO owner_id"); err != nil { + return err + } + } + + return sess.Commit() +} diff --git a/models/organization/org.go b/models/organization/org.go index f05027be729d4..269b3e83288dc 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -239,6 +239,32 @@ func (org *Organization) CustomAvatarRelativePath() string { return org.Avatar } +// UnitPermission returns unit permission +func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.User, unitType unit.Type) perm.AccessMode { + if doer != nil { + teams, err := GetUserOrgTeams(ctx, org.ID, doer.ID) + if err != nil { + log.Error("GetUserOrgTeams: %v", err) + return perm.AccessModeNone + } + + if err := teams.LoadUnits(ctx); err != nil { + log.Error("LoadUnits: %v", err) + return perm.AccessModeNone + } + + if len(teams) > 0 { + return teams.UnitMaxAccess(unitType) + } + } + + if org.Visibility.IsPublic() { + return perm.AccessModeRead + } + + return perm.AccessModeNone +} + // CreateOrganization creates record of a new organization. func CreateOrganization(org *Organization, owner *user_model.User) (err error) { if !owner.CanCreateOrganization() { diff --git a/models/project/project.go b/models/project/project.go index 46b5c07c424f1..f3ed723030cdb 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -409,7 +409,7 @@ func DeleteProjectByID(ctx context.Context, id int64) error { func DeleteProjectByRepoID(ctx context.Context, repoID int64) error { switch { - case setting.Database.UseSQLite3: + case setting.Database.Type.IsSQLite3(): if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_issue WHERE project_issue.id IN (SELECT project_issue.id FROM project_issue INNER JOIN project WHERE project.id = project_issue.project_id AND project.repo_id = ?)", repoID); err != nil { return err } @@ -419,7 +419,7 @@ func DeleteProjectByRepoID(ctx context.Context, repoID int64) error { if _, err := db.GetEngine(ctx).Table("project").Where("repo_id = ? ", repoID).Delete(&Project{}); err != nil { return err } - case setting.Database.UsePostgreSQL: + case setting.Database.Type.IsPostgreSQL(): if _, err := db.GetEngine(ctx).Exec("DELETE FROM project_issue USING project WHERE project.id = project_issue.project_id AND project.repo_id = ? ", repoID); err != nil { return err } diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index d64368daa6b28..d9cd905a1936a 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -498,7 +498,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { subQueryCond := builder.NewCond() // Topic checking. Topics are present. - if setting.Database.UsePostgreSQL { // postgres stores the topics as json and not as text + if setting.Database.Type.IsPostgreSQL() { // postgres stores the topics as json and not as text subQueryCond = subQueryCond.Or(builder.And(builder.NotNull{"topics"}, builder.Neq{"(topics)::text": "[]"})) } else { subQueryCond = subQueryCond.Or(builder.And(builder.Neq{"topics": "null"}, builder.Neq{"topics": "[]"})) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 2bc41084ba872..cff1489a7c7bc 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -76,7 +76,7 @@ func MainTest(m *testing.M, testOpts *TestOptions) { setting.SSH.BuiltinServerUser = "builtinuser" setting.SSH.Port = 3000 setting.SSH.Domain = "try.gitea.io" - setting.Database.UseSQLite3 = true + setting.Database.Type = "sqlite3" setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" repoRootPath, err := os.MkdirTemp(os.TempDir(), "repos") if err != nil { diff --git a/models/user/user.go b/models/user/user.go index f6fafe64f3915..454779b9ea361 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -393,6 +393,11 @@ func (u *User) IsOrganization() bool { return u.Type == UserTypeOrganization } +// IsIndividual returns true if user is actually a individual user. +func (u *User) IsIndividual() bool { + return u.Type == UserTypeIndividual +} + // DisplayName returns full name if it's not empty, // returns username otherwise. func (u *User) DisplayName() string { diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 64119f1494958..e3f6b593d977a 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -122,7 +122,7 @@ func IsValidHookContentType(name string) bool { type Webhook struct { ID int64 `xorm:"pk autoincr"` RepoID int64 `xorm:"INDEX"` // An ID of 0 indicates either a default or system webhook - OrgID int64 `xorm:"INDEX"` + OwnerID int64 `xorm:"INDEX"` IsSystemWebhook bool URL string `xorm:"url TEXT"` HTTPMethod string `xorm:"http_method"` @@ -412,11 +412,11 @@ func GetWebhookByRepoID(repoID, id int64) (*Webhook, error) { }) } -// GetWebhookByOrgID returns webhook of organization by given ID. -func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) { +// GetWebhookByOwnerID returns webhook of a user or organization by given ID. +func GetWebhookByOwnerID(ownerID, id int64) (*Webhook, error) { return getWebhook(&Webhook{ - ID: id, - OrgID: orgID, + ID: id, + OwnerID: ownerID, }) } @@ -424,7 +424,7 @@ func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) { type ListWebhookOptions struct { db.ListOptions RepoID int64 - OrgID int64 + OwnerID int64 IsActive util.OptionalBool } @@ -433,8 +433,8 @@ func (opts *ListWebhookOptions) toCond() builder.Cond { if opts.RepoID != 0 { cond = cond.And(builder.Eq{"webhook.repo_id": opts.RepoID}) } - if opts.OrgID != 0 { - cond = cond.And(builder.Eq{"webhook.org_id": opts.OrgID}) + if opts.OwnerID != 0 { + cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID}) } if !opts.IsActive.IsNone() { cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()}) @@ -503,10 +503,10 @@ func DeleteWebhookByRepoID(repoID, id int64) error { }) } -// DeleteWebhookByOrgID deletes webhook of organization by given ID. -func DeleteWebhookByOrgID(orgID, id int64) error { +// DeleteWebhookByOwnerID deletes webhook of a user or organization by given ID. +func DeleteWebhookByOwnerID(ownerID, id int64) error { return deleteWebhook(&Webhook{ - ID: id, - OrgID: orgID, + ID: id, + OwnerID: ownerID, }) } diff --git a/models/webhook/webhook_system.go b/models/webhook/webhook_system.go index 21dc0406a0d19..2e89f9547bba2 100644 --- a/models/webhook/webhook_system.go +++ b/models/webhook/webhook_system.go @@ -15,7 +15,7 @@ import ( func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) { webhooks := make([]*Webhook, 0, 5) return webhooks, db.GetEngine(ctx). - Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, false). + Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, false). Find(&webhooks) } @@ -23,7 +23,7 @@ func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) { func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error) { webhook := &Webhook{ID: id} has, err := db.GetEngine(ctx). - Where("repo_id=? AND org_id=?", 0, 0). + Where("repo_id=? AND owner_id=?", 0, 0). Get(webhook) if err != nil { return nil, err @@ -38,11 +38,11 @@ func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webh webhooks := make([]*Webhook, 0, 5) if isActive.IsNone() { return webhooks, db.GetEngine(ctx). - Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, true). + Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true). Find(&webhooks) } return webhooks, db.GetEngine(ctx). - Where("repo_id=? AND org_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()). + Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()). Find(&webhooks) } @@ -50,7 +50,7 @@ func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webh func DeleteDefaultSystemWebhook(ctx context.Context, id int64) error { return db.WithTx(ctx, func(ctx context.Context) error { count, err := db.GetEngine(ctx). - Where("repo_id=? AND org_id=?", 0, 0). + Where("repo_id=? AND owner_id=?", 0, 0). Delete(&Webhook{ID: id}) if err != nil { return err diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index c368fc620e2f2..74f7aeaa03028 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -109,13 +109,13 @@ func TestGetWebhookByRepoID(t *testing.T) { assert.True(t, IsErrWebhookNotExist(err)) } -func TestGetWebhookByOrgID(t *testing.T) { +func TestGetWebhookByOwnerID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - hook, err := GetWebhookByOrgID(3, 3) + hook, err := GetWebhookByOwnerID(3, 3) assert.NoError(t, err) assert.Equal(t, int64(3), hook.ID) - _, err = GetWebhookByOrgID(unittest.NonexistentID, unittest.NonexistentID) + _, err = GetWebhookByOwnerID(unittest.NonexistentID, unittest.NonexistentID) assert.Error(t, err) assert.True(t, IsErrWebhookNotExist(err)) } @@ -140,9 +140,9 @@ func TestGetWebhooksByRepoID(t *testing.T) { } } -func TestGetActiveWebhooksByOrgID(t *testing.T) { +func TestGetActiveWebhooksByOwnerID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OrgID: 3, IsActive: util.OptionalBoolTrue}) + hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OwnerID: 3, IsActive: util.OptionalBoolTrue}) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(3), hooks[0].ID) @@ -150,9 +150,9 @@ func TestGetActiveWebhooksByOrgID(t *testing.T) { } } -func TestGetWebhooksByOrgID(t *testing.T) { +func TestGetWebhooksByOwnerID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OrgID: 3}) + hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OwnerID: 3}) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(3), hooks[0].ID) @@ -181,13 +181,13 @@ func TestDeleteWebhookByRepoID(t *testing.T) { assert.True(t, IsErrWebhookNotExist(err)) } -func TestDeleteWebhookByOrgID(t *testing.T) { +func TestDeleteWebhookByOwnerID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OrgID: 3}) - assert.NoError(t, DeleteWebhookByOrgID(3, 3)) - unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OrgID: 3}) + unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OwnerID: 3}) + assert.NoError(t, DeleteWebhookByOwnerID(3, 3)) + unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OwnerID: 3}) - err := DeleteWebhookByOrgID(unittest.NonexistentID, unittest.NonexistentID) + err := DeleteWebhookByOwnerID(unittest.NonexistentID, unittest.NonexistentID) assert.Error(t, err) assert.True(t, IsErrWebhookNotExist(err)) } diff --git a/modules/cache/context.go b/modules/cache/context.go index f741a87445383..62bbf5dcba84a 100644 --- a/modules/cache/context.go +++ b/modules/cache/context.go @@ -6,6 +6,7 @@ package cache import ( "context" "sync" + "time" "code.gitea.io/gitea/modules/log" ) @@ -14,65 +15,151 @@ import ( // This is useful for caching data that is expensive to calculate and is likely to be // used multiple times in a request. type cacheContext struct { - ctx context.Context - data map[any]map[any]any - lock sync.RWMutex + data map[any]map[any]any + lock sync.RWMutex + created time.Time + discard bool } func (cc *cacheContext) Get(tp, key any) any { cc.lock.RLock() defer cc.lock.RUnlock() - if cc.data[tp] == nil { - return nil - } return cc.data[tp][key] } func (cc *cacheContext) Put(tp, key, value any) { cc.lock.Lock() defer cc.lock.Unlock() - if cc.data[tp] == nil { - cc.data[tp] = make(map[any]any) + + if cc.discard { + return + } + + d := cc.data[tp] + if d == nil { + d = make(map[any]any) + cc.data[tp] = d } - cc.data[tp][key] = value + d[key] = value } func (cc *cacheContext) Delete(tp, key any) { cc.lock.Lock() defer cc.lock.Unlock() - if cc.data[tp] == nil { - return - } delete(cc.data[tp], key) } +func (cc *cacheContext) Discard() { + cc.lock.Lock() + defer cc.lock.Unlock() + cc.data = nil + cc.discard = true +} + +func (cc *cacheContext) isDiscard() bool { + cc.lock.RLock() + defer cc.lock.RUnlock() + return cc.discard +} + +// cacheContextLifetime is the max lifetime of cacheContext. +// Since cacheContext is used to cache data in a request level context, 10s is enough. +// If a cacheContext is used more than 10s, it's probably misuse. +const cacheContextLifetime = 10 * time.Second + +var timeNow = time.Now + +func (cc *cacheContext) Expired() bool { + return timeNow().Sub(cc.created) > cacheContextLifetime +} + var cacheContextKey = struct{}{} +/* +Since there are both WithCacheContext and WithNoCacheContext, +it may be confusing when there is nesting. + +Some cases to explain the design: + +When: +- A, B or C means a cache context. +- A', B' or C' means a discard cache context. +- ctx means context.Backgrand(). +- A(ctx) means a cache context with ctx as the parent context. +- B(A(ctx)) means a cache context with A(ctx) as the parent context. +- With is alias of WithCacheContext. +- WithNo is alias of WithNoCacheContext. + +So: +- With(ctx) -> A(ctx) +- With(With(ctx)) -> A(ctx), not B(A(ctx)), always reuse parent cache context if possible. +- With(With(With(ctx))) -> A(ctx), not C(B(A(ctx))), ditto. +- WithNo(ctx) -> ctx, not A'(ctx), don't create new cache context if we don't have to. +- WithNo(With(ctx)) -> A'(ctx) +- WithNo(WithNo(With(ctx))) -> A'(ctx), not B'(A'(ctx)), don't create new cache context if we don't have to. +- With(WithNo(With(ctx))) -> B(A'(ctx)), not A(ctx), never reuse a discard cache context. +- WithNo(With(WithNo(With(ctx)))) -> B'(A'(ctx)) +- With(WithNo(With(WithNo(With(ctx))))) -> C(B'(A'(ctx))), so there's always only one not-discard cache context. +*/ + func WithCacheContext(ctx context.Context) context.Context { + if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok { + if !c.isDiscard() { + // reuse parent context + return ctx + } + } return context.WithValue(ctx, cacheContextKey, &cacheContext{ - ctx: ctx, - data: make(map[any]map[any]any), + data: make(map[any]map[any]any), + created: timeNow(), }) } +func WithNoCacheContext(ctx context.Context) context.Context { + if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok { + // The caller want to run long-life tasks, but the parent context is a cache context. + // So we should disable and clean the cache data, or it will be kept in memory for a long time. + c.Discard() + return ctx + } + + return ctx +} + func GetContextData(ctx context.Context, tp, key any) any { if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok { + if c.Expired() { + // The warning means that the cache context is misused for long-life task, + // it can be resolved with WithNoCacheContext(ctx). + log.Warn("cache context is expired, may be misused for long-life tasks: %v", c) + return nil + } return c.Get(tp, key) } - log.Warn("cannot get cache context when getting data: %v", ctx) return nil } func SetContextData(ctx context.Context, tp, key, value any) { if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok { + if c.Expired() { + // The warning means that the cache context is misused for long-life task, + // it can be resolved with WithNoCacheContext(ctx). + log.Warn("cache context is expired, may be misused for long-life tasks: %v", c) + return + } c.Put(tp, key, value) return } - log.Warn("cannot get cache context when setting data: %v", ctx) } func RemoveContextData(ctx context.Context, tp, key any) { if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok { + if c.Expired() { + // The warning means that the cache context is misused for long-life task, + // it can be resolved with WithNoCacheContext(ctx). + log.Warn("cache context is expired, may be misused for long-life tasks: %v", c) + return + } c.Delete(tp, key) } } diff --git a/modules/cache/context_test.go b/modules/cache/context_test.go index 77e3ecad2ca70..5315547865e19 100644 --- a/modules/cache/context_test.go +++ b/modules/cache/context_test.go @@ -6,6 +6,7 @@ package cache import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -25,7 +26,7 @@ func TestWithCacheContext(t *testing.T) { assert.EqualValues(t, 1, v.(int)) RemoveContextData(ctx, field, "my_config1") - RemoveContextData(ctx, field, "my_config2") // remove an non-exist key + RemoveContextData(ctx, field, "my_config2") // remove a non-exist key v = GetContextData(ctx, field, "my_config1") assert.Nil(t, v) @@ -38,4 +39,40 @@ func TestWithCacheContext(t *testing.T) { v = GetContextData(ctx, field, "my_config1") assert.EqualValues(t, 1, v) + + now := timeNow + defer func() { + timeNow = now + }() + timeNow = func() time.Time { + return now().Add(10 * time.Second) + } + v = GetContextData(ctx, field, "my_config1") + assert.Nil(t, v) +} + +func TestWithNoCacheContext(t *testing.T) { + ctx := context.Background() + + const field = "system_setting" + + v := GetContextData(ctx, field, "my_config1") + assert.Nil(t, v) + SetContextData(ctx, field, "my_config1", 1) + v = GetContextData(ctx, field, "my_config1") + assert.Nil(t, v) // still no cache + + ctx = WithCacheContext(ctx) + v = GetContextData(ctx, field, "my_config1") + assert.Nil(t, v) + SetContextData(ctx, field, "my_config1", 1) + v = GetContextData(ctx, field, "my_config1") + assert.NotNil(t, v) + + ctx = WithNoCacheContext(ctx) + v = GetContextData(ctx, field, "my_config1") + assert.Nil(t, v) + SetContextData(ctx, field, "my_config1", 1) + v = GetContextData(ctx, field, "my_config1") + assert.Nil(t, v) // still no cache } diff --git a/modules/context/access_log.go b/modules/context/access_log.go index 1aaba9dc2d5c9..515682b64b0e6 100644 --- a/modules/context/access_log.go +++ b/modules/context/access_log.go @@ -6,7 +6,9 @@ package context import ( "bytes" "context" + "fmt" "net/http" + "strings" "text/template" "time" @@ -20,13 +22,39 @@ type routerLoggerOptions struct { Start *time.Time ResponseWriter http.ResponseWriter Ctx map[string]interface{} + RequestID *string } var signedUserNameStringPointerKey interface{} = "signedUserNameStringPointerKey" +const keyOfRequestIDInTemplate = ".RequestID" + +// According to: +// TraceId: A valid trace identifier is a 16-byte array with at least one non-zero byte +// MD5 output is 16 or 32 bytes: md5-bytes is 16, md5-hex is 32 +// SHA1: similar, SHA1-bytes is 20, SHA1-hex is 40. +// UUID is 128-bit, 32 hex chars, 36 ASCII chars with 4 dashes +// So, we accept a Request ID with a maximum character length of 40 +const maxRequestIDByteLength = 40 + +func parseRequestIDFromRequestHeader(req *http.Request) string { + requestID := "-" + for _, key := range setting.Log.RequestIDHeaders { + if req.Header.Get(key) != "" { + requestID = req.Header.Get(key) + break + } + } + if len(requestID) > maxRequestIDByteLength { + requestID = fmt.Sprintf("%s...", requestID[:maxRequestIDByteLength]) + } + return requestID +} + // AccessLogger returns a middleware to log access logger func AccessLogger() func(http.Handler) http.Handler { logger := log.GetLogger("access") + needRequestID := len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate) logTemplate, _ := template.New("log").Parse(setting.Log.AccessLogTemplate) return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { @@ -34,6 +62,11 @@ func AccessLogger() func(http.Handler) http.Handler { identity := "-" r := req.WithContext(context.WithValue(req.Context(), signedUserNameStringPointerKey, &identity)) + var requestID string + if needRequestID { + requestID = parseRequestIDFromRequestHeader(req) + } + next.ServeHTTP(w, r) rw := w.(ResponseWriter) @@ -47,6 +80,7 @@ func AccessLogger() func(http.Handler) http.Handler { "RemoteAddr": req.RemoteAddr, "Req": req, }, + RequestID: &requestID, }) if err != nil { log.Error("Could not set up chi access logger: %v", err.Error()) diff --git a/modules/context/api.go b/modules/context/api.go index 3f938948aed66..f7a3384691246 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -244,7 +244,7 @@ func APIContexter() func(http.Handler) http.Handler { } } - httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 0, "no-transform") + httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform") ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) ctx.Data["Context"] = &ctx diff --git a/modules/context/context.go b/modules/context/context.go index 0c8d7411ed5db..50c34edae2e53 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -388,7 +388,7 @@ func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) { if duration == 0 { duration = 5 * time.Minute } - httpcache.AddCacheControlToHeader(header, duration) + httpcache.SetCacheControlInHeader(header, duration) if !opts.LastModified.IsZero() { header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat)) @@ -753,7 +753,7 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler { } } - httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 0, "no-transform") + httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform") ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) ctx.Data["CsrfToken"] = ctx.csrf.GetToken() diff --git a/modules/context/org.go b/modules/context/org.go index 0add7f2c0c3de..39a3038f910cb 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" ) @@ -31,29 +30,34 @@ type Organization struct { } func (org *Organization) CanWriteUnit(ctx *Context, unitType unit.Type) bool { - if ctx.Doer == nil { - return false - } - return org.UnitPermission(ctx, ctx.Doer.ID, unitType) >= perm.AccessModeWrite + return org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeWrite } -func (org *Organization) UnitPermission(ctx *Context, doerID int64, unitType unit.Type) perm.AccessMode { - if doerID > 0 { - teams, err := organization.GetUserOrgTeams(ctx, org.Organization.ID, doerID) - if err != nil { - log.Error("GetUserOrgTeams: %v", err) - return perm.AccessModeNone - } - if len(teams) > 0 { - return teams.UnitMaxAccess(unitType) - } - } +func (org *Organization) CanReadUnit(ctx *Context, unitType unit.Type) bool { + return org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeRead +} - if org.Organization.Visibility == structs.VisibleTypePublic { - return perm.AccessModeRead - } +func GetOrganizationByParams(ctx *Context) { + orgName := ctx.Params(":org") + + var err error - return perm.AccessModeNone + ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName) + if err != nil { + if organization.IsErrOrgNotExist(err) { + redirectUserID, err := user_model.LookupUserRedirect(orgName) + if err == nil { + RedirectToUser(ctx, orgName, redirectUserID) + } else if user_model.IsErrUserRedirectNotExist(err) { + ctx.NotFound("GetUserByName", err) + } else { + ctx.ServerError("LookupUserRedirect", err) + } + } else { + ctx.ServerError("GetUserByName", err) + } + return + } } // HandleOrgAssignment handles organization assignment @@ -77,25 +81,26 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { requireTeamAdmin = args[3] } - orgName := ctx.Params(":org") - var err error - ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName) - if err != nil { - if organization.IsErrOrgNotExist(err) { - redirectUserID, err := user_model.LookupUserRedirect(orgName) - if err == nil { - RedirectToUser(ctx, orgName, redirectUserID) - } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.NotFound("GetUserByName", err) - } else { - ctx.ServerError("LookupUserRedirect", err) + + if ctx.ContextUser == nil { + // if Organization is not defined, get it from params + if ctx.Org.Organization == nil { + GetOrganizationByParams(ctx) + if ctx.Written() { + return } - } else { - ctx.ServerError("GetUserByName", err) } + } else if ctx.ContextUser.IsOrganization() { + if ctx.Org == nil { + ctx.Org = &Organization{} + } + ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser) + } else { + // ContextUser is an individual User return } + org := ctx.Org.Organization // Handle Visibility @@ -156,6 +161,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { } ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember + ctx.Data["IsProjectEnabled"] = true ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["IsPublicMember"] = func(uid int64) bool { @@ -231,6 +237,10 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { return } } + + ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) + ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages) + ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode) } // OrgAssignment returns a middleware to handle organization assignment diff --git a/modules/doctor/dbconsistency.go b/modules/doctor/dbconsistency.go index bb560ac6a350b..541fc736f44b6 100644 --- a/modules/doctor/dbconsistency.go +++ b/modules/doctor/dbconsistency.go @@ -155,7 +155,7 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er // TODO: function to recalc all counters - if setting.Database.UsePostgreSQL { + if setting.Database.Type.IsPostgreSQL() { consistencyChecks = append(consistencyChecks, consistencyCheck{ Name: "Sequence values", Counter: db.CountBadSequences, diff --git a/modules/git/command.go b/modules/git/command.go index 0bc8103116a0b..9a65279a8cb5f 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -179,7 +179,7 @@ func (c *Command) AddDashesAndList(list ...string) *Command { } // ToTrustedCmdArgs converts a list of strings (trusted as argument) to TrustedCmdArgs -// In most cases, it shouldn't be used. Use AddXxx function instead +// In most cases, it shouldn't be used. Use NewCommand().AddXxx() function instead func ToTrustedCmdArgs(args []string) TrustedCmdArgs { ret := make(TrustedCmdArgs, len(args)) for i, arg := range args { diff --git a/modules/git/commit.go b/modules/git/commit.go index 4a55645d30346..6e8fcb3e0802e 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -218,6 +218,19 @@ func (c *Commit) HasPreviousCommit(commitHash SHA1) (bool, error) { return false, err } +// IsForcePush returns true if a push from oldCommitHash to this is a force push +func (c *Commit) IsForcePush(oldCommitID string) (bool, error) { + if oldCommitID == EmptySHA { + return false, nil + } + oldCommit, err := c.repo.GetCommit(oldCommitID) + if err != nil { + return false, err + } + hasPreviousCommit, err := c.HasPreviousCommit(oldCommit.ID) + return !hasPreviousCommit, err +} + // CommitsBeforeLimit returns num commits before current revision func (c *Commit) CommitsBeforeLimit(num int) ([]*Commit, error) { return c.repo.getCommitsBeforeLimit(c.ID, num) diff --git a/modules/git/pipeline/revlist.go b/modules/git/pipeline/revlist.go index 09bb2c8b3c844..d88ebe78ef200 100644 --- a/modules/git/pipeline/revlist.go +++ b/modules/git/pipeline/revlist.go @@ -42,7 +42,10 @@ func RevListObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync. defer revListWriter.Close() stderr := new(bytes.Buffer) var errbuf strings.Builder - cmd := git.NewCommand(ctx, "rev-list", "--objects").AddDynamicArguments(headSHA).AddArguments("--not").AddDynamicArguments(baseSHA) + cmd := git.NewCommand(ctx, "rev-list", "--objects").AddDynamicArguments(headSHA) + if baseSHA != "" { + cmd = cmd.AddArguments("--not").AddDynamicArguments(baseSHA) + } if err := cmd.Run(&git.RunOpts{ Dir: tmpBasePath, Stdout: revListWriter, diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index a62e0670fedca..0e1b00ce082e4 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -323,6 +323,27 @@ func (repo *Repository) CommitsBetweenLimit(last, before *Commit, limit, skip in return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) } +// CommitsBetweenNotBase returns a list that contains commits between [before, last), excluding commits in baseBranch. +// If before is detached (removed by reset + push) it is not included. +func (repo *Repository) CommitsBetweenNotBase(last, before *Commit, baseBranch string) ([]*Commit, error) { + var stdout []byte + var err error + if before == nil { + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) + } else { + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) + if err != nil && strings.Contains(err.Error(), "no merge base") { + // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. + // previously it would return the results of git rev-list before last so let's try that... + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) + } + } + if err != nil { + return nil, err + } + return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) +} + // CommitsBetweenIDs return commits between twoe commits func (repo *Repository) CommitsBetweenIDs(last, before string) ([]*Commit, error) { lastCommit, err := repo.GetCommit(last) diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go index f0caa30eb82b3..46e0152ef4553 100644 --- a/modules/httpcache/httpcache.go +++ b/modules/httpcache/httpcache.go @@ -15,8 +15,8 @@ import ( "code.gitea.io/gitea/modules/setting" ) -// AddCacheControlToHeader adds suitable cache-control headers to response -func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) { +// SetCacheControlInHeader sets suitable cache-control headers in the response +func SetCacheControlInHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) { directives := make([]string, 0, 2+len(additionalDirectives)) // "max-age=0 + must-revalidate" (aka "no-cache") is preferred instead of "no-store" @@ -31,7 +31,7 @@ func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDire directives = append(directives, "max-age=0", "private", "must-revalidate") // to remind users they are using non-prod setting. - h.Add("X-Gitea-Debug", "RUN_MODE="+setting.RunMode) + h.Set("X-Gitea-Debug", "RUN_MODE="+setting.RunMode) } h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", ")) @@ -50,7 +50,7 @@ func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) ( // HandleGenericTimeCache handles time-based caching for a HTTP request func HandleGenericTimeCache(req *http.Request, w http.ResponseWriter, lastModified time.Time) (handled bool) { - AddCacheControlToHeader(w.Header(), setting.StaticCacheTime) + SetCacheControlInHeader(w.Header(), setting.StaticCacheTime) ifModifiedSince := req.Header.Get("If-Modified-Since") if ifModifiedSince != "" { @@ -81,7 +81,7 @@ func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag strin return true } } - AddCacheControlToHeader(w.Header(), setting.StaticCacheTime) + SetCacheControlInHeader(w.Header(), setting.StaticCacheTime) return false } @@ -125,6 +125,6 @@ func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag s } } } - AddCacheControlToHeader(w.Header(), setting.StaticCacheTime) + SetCacheControlInHeader(w.Header(), setting.StaticCacheTime) return false } diff --git a/modules/label/parser.go b/modules/label/parser.go index 768c72a61b014..55bf570de6b95 100644 --- a/modules/label/parser.go +++ b/modules/label/parser.go @@ -36,17 +36,17 @@ func (err ErrTemplateLoad) Error() string { // GetTemplateFile loads the label template file by given name, // then parses and returns a list of name-color pairs and optionally description. func GetTemplateFile(name string) ([]*Label, error) { - data, err := options.GetRepoInitFile("label", name+".yaml") + data, err := options.Labels(name + ".yaml") if err == nil && len(data) > 0 { return parseYamlFormat(name+".yaml", data) } - data, err = options.GetRepoInitFile("label", name+".yml") + data, err = options.Labels(name + ".yml") if err == nil && len(data) > 0 { return parseYamlFormat(name+".yml", data) } - data, err = options.GetRepoInitFile("label", name) + data, err = options.Labels(name) if err != nil { return nil, ErrTemplateLoad{name, fmt.Errorf("GetRepoInitFile: %w", err)} } diff --git a/modules/options/base.go b/modules/options/base.go index 039e934b3a4d9..e83e8df5d0945 100644 --- a/modules/options/base.go +++ b/modules/options/base.go @@ -7,11 +7,52 @@ import ( "fmt" "io/fs" "os" + "path" "path/filepath" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) +// Locale reads the content of a specific locale from static/bindata or custom path. +func Locale(name string) ([]byte, error) { + return fileFromDir(path.Join("locale", util.CleanPath(name))) +} + +// Readme reads the content of a specific readme from static/bindata or custom path. +func Readme(name string) ([]byte, error) { + return fileFromDir(path.Join("readme", util.CleanPath(name))) +} + +// Gitignore reads the content of a gitignore locale from static/bindata or custom path. +func Gitignore(name string) ([]byte, error) { + return fileFromDir(path.Join("gitignore", util.CleanPath(name))) +} + +// License reads the content of a specific license from static/bindata or custom path. +func License(name string) ([]byte, error) { + return fileFromDir(path.Join("license", util.CleanPath(name))) +} + +// Labels reads the content of a specific labels from static/bindata or custom path. +func Labels(name string) ([]byte, error) { + return fileFromDir(path.Join("label", util.CleanPath(name))) +} + +// WalkLocales reads the content of a specific locale +func WalkLocales(callback func(path, name string, d fs.DirEntry, err error) error) error { + if IsDynamic() { + if err := walkAssetDir(filepath.Join(setting.StaticRootPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to walk locales. Error: %w", err) + } + } + + if err := walkAssetDir(filepath.Join(setting.CustomPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to walk locales. Error: %w", err) + } + return nil +} + func walkAssetDir(root string, callback func(path, name string, d fs.DirEntry, err error) error) error { if err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { // name is the path relative to the root @@ -37,3 +78,18 @@ func walkAssetDir(root string, callback func(path, name string, d fs.DirEntry, e } return nil } + +func statDirIfExist(dir string) ([]string, error) { + isDir, err := util.IsDir(dir) + if err != nil { + return nil, fmt.Errorf("unable to check if static directory %s is a directory. %w", dir, err) + } + if !isDir { + return nil, nil + } + files, err := util.StatDir(dir, true) + if err != nil { + return nil, fmt.Errorf("unable to read directory %q. %w", dir, err) + } + return files, nil +} diff --git a/modules/options/dynamic.go b/modules/options/dynamic.go index a20253676e671..8c954492ae51f 100644 --- a/modules/options/dynamic.go +++ b/modules/options/dynamic.go @@ -7,10 +7,8 @@ package options import ( "fmt" - "io/fs" "os" "path" - "path/filepath" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -27,76 +25,20 @@ func Dir(name string) ([]string, error) { var result []string - customDir := path.Join(setting.CustomPath, "options", name) - - isDir, err := util.IsDir(customDir) - if err != nil { - return []string{}, fmt.Errorf("Unabe to check if custom directory %s is a directory. %w", customDir, err) - } - if isDir { - files, err := util.StatDir(customDir, true) - if err != nil { - return []string{}, fmt.Errorf("Failed to read custom directory. %w", err) - } - - result = append(result, files...) - } - - staticDir := path.Join(setting.StaticRootPath, "options", name) - - isDir, err = util.IsDir(staticDir) - if err != nil { - return []string{}, fmt.Errorf("unable to check if static directory %s is a directory. %w", staticDir, err) - } - if isDir { - files, err := util.StatDir(staticDir, true) + for _, dir := range []string{ + path.Join(setting.CustomPath, "options", name), // custom dir + path.Join(setting.StaticRootPath, "options", name), // static dir + } { + files, err := statDirIfExist(dir) if err != nil { - return []string{}, fmt.Errorf("Failed to read static directory. %w", err) + return nil, err } - result = append(result, files...) } return directories.AddAndGet(name, result), nil } -// Locale reads the content of a specific locale from static or custom path. -func Locale(name string) ([]byte, error) { - return fileFromDir(path.Join("locale", name)) -} - -// WalkLocales reads the content of a specific locale from static or custom path. -func WalkLocales(callback func(path, name string, d fs.DirEntry, err error) error) error { - if err := walkAssetDir(filepath.Join(setting.StaticRootPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to walk locales. Error: %w", err) - } - - if err := walkAssetDir(filepath.Join(setting.CustomPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to walk locales. Error: %w", err) - } - return nil -} - -// Readme reads the content of a specific readme from static or custom path. -func Readme(name string) ([]byte, error) { - return fileFromDir(path.Join("readme", name)) -} - -// Gitignore reads the content of a specific gitignore from static or custom path. -func Gitignore(name string) ([]byte, error) { - return fileFromDir(path.Join("gitignore", name)) -} - -// License reads the content of a specific license from static or custom path. -func License(name string) ([]byte, error) { - return fileFromDir(path.Join("license", name)) -} - -// Labels reads the content of a specific labels from static or custom path. -func Labels(name string) ([]byte, error) { - return fileFromDir(path.Join("label", name)) -} - // fileFromDir is a helper to read files from static or custom path. func fileFromDir(name string) ([]byte, error) { customPath := path.Join(setting.CustomPath, "options", name) diff --git a/modules/options/repo.go b/modules/options/repo.go deleted file mode 100644 index 1480f7808176c..0000000000000 --- a/modules/options/repo.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package options - -import ( - "fmt" - "os" - "path" - "strings" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" -) - -// GetRepoInitFile returns repository init files -func GetRepoInitFile(tp, name string) ([]byte, error) { - cleanedName := strings.TrimLeft(path.Clean("/"+name), "/") - relPath := path.Join("options", tp, cleanedName) - - // Use custom file when available. - customPath := path.Join(setting.CustomPath, relPath) - isFile, err := util.IsFile(customPath) - if err != nil { - log.Error("Unable to check if %s is a file. Error: %v", customPath, err) - } - if isFile { - return os.ReadFile(customPath) - } - - switch tp { - case "readme": - return Readme(cleanedName) - case "gitignore": - return Gitignore(cleanedName) - case "license": - return License(cleanedName) - case "label": - return Labels(cleanedName) - default: - return []byte{}, fmt.Errorf("Invalid init file type") - } -} diff --git a/modules/options/static.go b/modules/options/static.go index ff3c86d3f84f6..549f4e25b11a3 100644 --- a/modules/options/static.go +++ b/modules/options/static.go @@ -8,10 +8,8 @@ package options import ( "fmt" "io" - "io/fs" "os" "path" - "path/filepath" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -28,17 +26,14 @@ func Dir(name string) ([]string, error) { var result []string - customDir := path.Join(setting.CustomPath, "options", name) - isDir, err := util.IsDir(customDir) - if err != nil { - return []string{}, fmt.Errorf("unable to check if custom directory %q is a directory. %w", customDir, err) - } - if isDir { - files, err := util.StatDir(customDir, true) + for _, dir := range []string{ + path.Join(setting.CustomPath, "options", name), // custom dir + // no static dir + } { + files, err := statDirIfExist(dir) if err != nil { - return []string{}, fmt.Errorf("unable to read custom directory %q. %w", customDir, err) + return nil, err } - result = append(result, files...) } @@ -69,39 +64,6 @@ func AssetDir(dirName string) ([]string, error) { return results, nil } -// Locale reads the content of a specific locale from bindata or custom path. -func Locale(name string) ([]byte, error) { - return fileFromDir(path.Join("locale", name)) -} - -// WalkLocales reads the content of a specific locale from static or custom path. -func WalkLocales(callback func(path, name string, d fs.DirEntry, err error) error) error { - if err := walkAssetDir(filepath.Join(setting.CustomPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to walk locales. Error: %w", err) - } - return nil -} - -// Readme reads the content of a specific readme from bindata or custom path. -func Readme(name string) ([]byte, error) { - return fileFromDir(path.Join("readme", name)) -} - -// Gitignore reads the content of a gitignore locale from bindata or custom path. -func Gitignore(name string) ([]byte, error) { - return fileFromDir(path.Join("gitignore", name)) -} - -// License reads the content of a specific license from bindata or custom path. -func License(name string) ([]byte, error) { - return fileFromDir(path.Join("license", name)) -} - -// Labels reads the content of a specific labels from static or custom path. -func Labels(name string) ([]byte, error) { - return fileFromDir(path.Join("label", name)) -} - // fileFromDir is a helper to read files from bindata or custom path. func fileFromDir(name string) ([]byte, error) { customPath := path.Join(setting.CustomPath, "options", name) diff --git a/modules/public/public.go b/modules/public/public.go index 42026f9b10549..e1d60d89eb9f9 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -6,7 +6,6 @@ package public import ( "net/http" "os" - "path" "path/filepath" "strings" @@ -14,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) // Options represents the available options to configure the handler. @@ -103,7 +103,7 @@ func setWellKnownContentType(w http.ResponseWriter, file string) { func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) bool { // use clean to keep the file is a valid path with no . or .. - f, err := fs.Open(path.Clean(file)) + f, err := fs.Open(util.CleanPath(file)) if err != nil { if os.IsNotExist(err) { return false diff --git a/modules/repository/init.go b/modules/repository/init.go index 49c8d2a904d1a..f9a33cd4f68c9 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -136,7 +136,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, } // README - data, err := options.GetRepoInitFile("readme", opts.Readme) + data, err := options.Readme(opts.Readme) if err != nil { return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) } @@ -164,7 +164,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, var buf bytes.Buffer names := strings.Split(opts.Gitignores, ",") for _, name := range names { - data, err = options.GetRepoInitFile("gitignore", name) + data, err = options.Gitignore(name) if err != nil { return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) } @@ -182,7 +182,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, // LICENSE if len(opts.License) > 0 { - data, err = options.GetRepoInitFile("license", opts.License) + data, err = options.License(opts.License) if err != nil { return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.License, err) } diff --git a/modules/repository/push.go b/modules/repository/push.go index 1fa711b359c34..aa1552351d515 100644 --- a/modules/repository/push.go +++ b/modules/repository/push.go @@ -4,10 +4,8 @@ package repository import ( - "context" "strings" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" ) @@ -96,19 +94,3 @@ func (opts *PushUpdateOptions) RefName() string { func (opts *PushUpdateOptions) RepoFullName() string { return opts.RepoUserName + "/" + opts.RepoName } - -// IsForcePush detect if a push is a force push -func IsForcePush(ctx context.Context, opts *PushUpdateOptions) (bool, error) { - if !opts.IsUpdateBranch() { - return false, nil - } - - output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(opts.OldCommitID, "^"+opts.NewCommitID). - RunStdString(&git.RunOpts{Dir: repo_model.RepoPath(opts.RepoUserName, opts.RepoName)}) - if err != nil { - return false, err - } else if len(output) > 0 { - return true, nil - } - return false, nil -} diff --git a/modules/setting/database.go b/modules/setting/database.go index 49865a38a2840..d7a5078fe914e 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -27,7 +27,7 @@ var ( // Database holds the database settings Database = struct { - Type string + Type DatabaseType Host string Name string User string @@ -39,10 +39,6 @@ var ( Charset string Timeout int // seconds SQLiteJournalMode string - UseSQLite3 bool - UseMySQL bool - UseMSSQL bool - UsePostgreSQL bool DBConnectRetries int DBConnectBackoff time.Duration MaxIdleConns int @@ -59,24 +55,13 @@ var ( // LoadDBSetting loads the database settings func LoadDBSetting() { sec := CfgProvider.Section("database") - Database.Type = sec.Key("DB_TYPE").String() + Database.Type = DatabaseType(sec.Key("DB_TYPE").String()) defaultCharset := "utf8" - Database.UseMySQL = false - Database.UseSQLite3 = false - Database.UsePostgreSQL = false - Database.UseMSSQL = false - switch Database.Type { - case "sqlite3": - Database.UseSQLite3 = true - case "mysql": - Database.UseMySQL = true + if Database.Type.IsMySQL() { defaultCharset = "utf8mb4" - case "postgres": - Database.UsePostgreSQL = true - case "mssql": - Database.UseMSSQL = true } + Database.Host = sec.Key("HOST").String() Database.Name = sec.Key("NAME").String() Database.User = sec.Key("USER").String() @@ -86,7 +71,7 @@ func LoadDBSetting() { Database.Schema = sec.Key("SCHEMA").String() Database.SSLMode = sec.Key("SSL_MODE").MustString("disable") Database.Charset = sec.Key("CHARSET").In(defaultCharset, []string{"utf8", "utf8mb4"}) - if Database.UseMySQL && defaultCharset != "utf8mb4" { + if Database.Type.IsMySQL() && defaultCharset != "utf8mb4" { log.Error("Deprecated database mysql charset utf8 support, please use utf8mb4 or convert utf8 to utf8mb4.") } @@ -95,7 +80,7 @@ func LoadDBSetting() { Database.SQLiteJournalMode = sec.Key("SQLITE_JOURNAL_MODE").MustString("") Database.MaxIdleConns = sec.Key("MAX_IDLE_CONNS").MustInt(2) - if Database.UseMySQL { + if Database.Type.IsMySQL() { Database.ConnMaxLifetime = sec.Key("CONN_MAX_LIFETIME").MustDuration(3 * time.Second) } else { Database.ConnMaxLifetime = sec.Key("CONN_MAX_LIFETIME").MustDuration(0) @@ -207,3 +192,25 @@ func ParseMSSQLHostPort(info string) (string, string) { } return host, port } + +type DatabaseType string + +func (t DatabaseType) String() string { + return string(t) +} + +func (t DatabaseType) IsSQLite3() bool { + return t == "sqlite3" +} + +func (t DatabaseType) IsMySQL() bool { + return t == "mysql" +} + +func (t DatabaseType) IsMSSQL() bool { + return t == "mssql" +} + +func (t DatabaseType) IsPostgreSQL() bool { + return t == "postgres" +} diff --git a/modules/setting/log.go b/modules/setting/log.go index 5448650aadb9c..dabdb543abdf5 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -38,6 +38,7 @@ var Log struct { EnableAccessLog bool AccessLogTemplate string BufferLength int64 + RequestIDHeaders []string } // GetLogDescriptions returns a race safe set of descriptions @@ -153,6 +154,7 @@ func loadLogFrom(rootCfg ConfigProvider) { Log.AccessLogTemplate = sec.Key("ACCESS_LOG_TEMPLATE").MustString( `{{.Ctx.RemoteAddr}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}\" \"{{.Ctx.Req.UserAgent}}"`, ) + Log.RequestIDHeaders = sec.Key("REQUEST_ID_HEADERS").Strings(",") // the `MustString` updates the default value, and `log.ACCESS` is used by `generateNamedLogger("access")` later _ = rootCfg.Section("log").Key("ACCESS").MustString("file") diff --git a/modules/storage/local.go b/modules/storage/local.go index a6a9d54a8ca30..05bf1fb28a56c 100644 --- a/modules/storage/local.go +++ b/modules/storage/local.go @@ -8,7 +8,6 @@ import ( "io" "net/url" "os" - "path" "path/filepath" "strings" @@ -59,7 +58,7 @@ func NewLocalStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error } func (l *LocalStorage) buildLocalPath(p string) string { - return filepath.Join(l.dir, path.Clean("/" + strings.ReplaceAll(p, "\\", "/"))[1:]) + return filepath.Join(l.dir, util.CleanPath(strings.ReplaceAll(p, "\\", "/"))) } // Open a file diff --git a/modules/storage/minio.go b/modules/storage/minio.go index c427d8d7e3122..24da14b634631 100644 --- a/modules/storage/minio.go +++ b/modules/storage/minio.go @@ -15,6 +15,7 @@ import ( "time" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" @@ -120,7 +121,7 @@ func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error } func (m *MinioStorage) buildMinioPath(p string) string { - return strings.TrimPrefix(path.Join(m.basePath, path.Clean("/" + strings.ReplaceAll(p, "\\", "/"))[1:]), "/") + return strings.TrimPrefix(path.Join(m.basePath, util.CleanPath(strings.ReplaceAll(p, "\\", "/"))), "/") } // Open open a file diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 19893c7c9d4de..822bbbd9b17c4 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -834,7 +834,7 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) string { // Make scope and item background colors slightly darker and lighter respectively. // More contrast needed with higher luminance, empirically tweaked. luminance := (0.299*r + 0.587*g + 0.114*b) / 255 - contrast := 0.01 + luminance*0.06 + contrast := 0.01 + luminance*0.03 // Ensure we add the same amount of contrast also near 0 and 1. darken := contrast + math.Max(luminance+contrast-1.0, 0.0) lighten := contrast + math.Max(contrast-luminance, 0.0) @@ -859,12 +859,10 @@ func RenderLabel(ctx context.Context, label *issues_model.Label) string { return fmt.Sprintf(""+ "
%s
"+ - "
 
"+ "
%s
"+ "
", description, textColor, scopeColor, scopeText, - itemColor, scopeColor, textColor, itemColor, itemText) } diff --git a/modules/typesniffer/typesniffer.go b/modules/typesniffer/typesniffer.go index c9fef953ce703..7887fd42b72ef 100644 --- a/modules/typesniffer/typesniffer.go +++ b/modules/typesniffer/typesniffer.go @@ -4,6 +4,7 @@ package typesniffer import ( + "bytes" "fmt" "io" "net/http" @@ -24,8 +25,9 @@ const ( ) var ( - svgTagRegex = regexp.MustCompile(`(?si)\A\s*(?:(||>))\s*)*\/]`) - svgTagInXMLRegex = regexp.MustCompile(`(?si)\A<\?xml\b.*?\?>\s*(?:(||>))\s*)*\/]`) + svgComment = regexp.MustCompile(`(?s)`) + svgTagRegex = regexp.MustCompile(`(?si)\A\s*(?:(|>))\s*)*\s*(?:(|>))\s*)*")).IsSvgImage()) assert.True(t, DetectContentType([]byte(" ")).IsSvgImage()) assert.True(t, DetectContentType([]byte(``)).IsSvgImage()) - assert.True(t, DetectContentType([]byte("")).IsSvgImage()) assert.True(t, DetectContentType([]byte(``)).IsSvgImage()) assert.True(t, DetectContentType([]byte(` `)).IsSvgImage()) @@ -57,6 +56,10 @@ func TestIsSvgImage(t *testing.T) { `)).IsSvgImage()) + + // the DetectContentType should work for incomplete data, because only beginning bytes are used for detection + assert.True(t, DetectContentType([]byte(`....`)).IsSvgImage()) + assert.False(t, DetectContentType([]byte{}).IsSvgImage()) assert.False(t, DetectContentType([]byte("svg")).IsSvgImage()) assert.False(t, DetectContentType([]byte("")).IsSvgImage()) @@ -68,6 +71,26 @@ func TestIsSvgImage(t *testing.T) { assert.False(t, DetectContentType([]byte(` `)).IsSvgImage()) + + assert.False(t, DetectContentType([]byte(` + +
+ + +
+`)).IsSvgImage()) + + assert.False(t, DetectContentType([]byte(` + +
+ + +
+`)).IsSvgImage()) + assert.False(t, DetectContentType([]byte(``)).IsSvgImage()) + assert.False(t, DetectContentType([]byte(``)).IsSvgImage()) } func TestIsPDF(t *testing.T) { @@ -86,6 +109,10 @@ func TestIsAudio(t *testing.T) { mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl") assert.True(t, DetectContentType(mp3).IsAudio()) assert.False(t, DetectContentType([]byte("plain text")).IsAudio()) + + assert.True(t, DetectContentType([]byte("ID3Toy\000")).IsAudio()) + assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ...")).IsText()) // test ID3 tag for plain text + assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char } func TestDetectContentTypeFromReader(t *testing.T) { diff --git a/modules/util/path.go b/modules/util/path.go index 74acb7a85fd87..5aa9e15f5c3e9 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -14,6 +14,14 @@ import ( "strings" ) +// CleanPath ensure to clean the path +func CleanPath(p string) string { + if strings.HasPrefix(p, "/") { + return path.Clean(p) + } + return path.Clean("/" + p)[1:] +} + // EnsureAbsolutePath ensure that a path is absolute, making it // relative to absoluteBase if necessary func EnsureAbsolutePath(path, absoluteBase string) string { diff --git a/modules/util/path_test.go b/modules/util/path_test.go index 93f4f67cf6486..2f020f924dd2a 100644 --- a/modules/util/path_test.go +++ b/modules/util/path_test.go @@ -136,3 +136,15 @@ func TestMisc_IsReadmeFileName(t *testing.T) { assert.Equal(t, testCase.idx, idx) } } + +func TestCleanPath(t *testing.T) { + cases := map[string]string{ + "../../test": "test", + "/test": "/test", + "/../test": "/test", + } + + for k, v := range cases { + assert.Equal(t, v, CleanPath(k)) + } +} diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 3c8d1a79fd8ce..543cd44045dec 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -57,6 +57,7 @@ new_mirror=Nové zrcadlo new_fork=Nové rozštěpení repozitáře new_org=Nová organizace new_project=Nový projekt +new_project_column=Nový sloupec manage_org=Spravovat organizace admin_panel=Administrace account_settings=Nastavení účtu @@ -90,9 +91,11 @@ disabled=Zakázané copy=Kopírovat copy_url=Kopírovat URL +copy_content=Kopírovat obsah copy_branch=Kopírovat jméno větve copy_success=Zkopírováno! copy_error=Kopírování se nezdařilo +copy_type_unsupported=Tento typ souboru nelze zkopírovat write=Zapsat preview=Náhled @@ -109,6 +112,10 @@ never=Nikdy rss_feed=RSS kanál [aria] +navbar=Navigační lišta +footer=Patička +footer.software=O softwaru +footer.links=Odkazy [filter] string.asc=A – Z @@ -292,23 +299,71 @@ code_search_results=Výsledky hledání pro „%s“ code_last_indexed_at=Naposledy indexováno %s relevant_repositories_tooltip=Repozitáře, které jsou rozštěpení nebo nemají žádné téma, ikonu a žádný popis jsou skryty. relevant_repositories=`Zobrazují se pouze relevantní repositáře, zobrazit nefiltrované výsledky. [auth] @@ -338,6 +393,7 @@ email_not_associate=Tato e-mailová adresa není spojena s žádným účtem. send_reset_mail=Zaslat e-mail pro obnovení účtu reset_password=Obnovení účtu invalid_code=Tento potvrzující kód je neplatný nebo mu vypršela platnost. +invalid_password=Vaše heslo se neshoduje s heslem, které bylo použito k vytvoření účtu. reset_password_helper=Obnovit účet reset_password_wrong_user=Jste přihlášen/a jako %s, ale odkaz pro obnovení účtu je pro %s password_too_short=Délka hesla musí být minimálně %d znaků. @@ -381,6 +437,7 @@ password_pwned_err=Nelze dokončit požadavek na HaveIBeenPwned [mail] view_it_on=Zobrazit na %s +reply=nebo přímo odpovědět na tento e-mail link_not_working_do_paste=Nefunguje? Zkuste jej zkopírovat a vložit do svého prohlížeče. hi_user_x=Ahoj %s, @@ -484,6 +541,7 @@ url_error=`„%s“ není platná adresa URL.` include_error=` musí obsahovat řetězec „%s“.` glob_pattern_error=`zástupný vzor je neplatný: %s.` regex_pattern_error=` regex vzor je neplatný: %s.` +invalid_group_team_map_error=` mapování je neplatné: %s` unknown_error=Neznámá chyba: captcha_incorrect=CAPTCHA kód není správný. password_not_match=Zadaná hesla nesouhlasí. @@ -520,10 +578,12 @@ team_not_exist=Tento tým neexistuje. last_org_owner=Nemůžete odstranit posledního uživatele z týmu „vlastníci“. Musí existovat alespoň jeden vlastník pro organizaci. cannot_add_org_to_team=Organizace nemůže být přidána jako člen týmu. duplicate_invite_to_team=Uživatel byl již pozván jako člen týmu. +organization_leave_success=Úspěšně jste opustili organizaci %s. invalid_ssh_key=Nelze ověřit váš SSH klíč: %s invalid_gpg_key=Nelze ověřit váš GPG klíč: %s invalid_ssh_principal=Neplatný SSH Principal certifikát: %s +must_use_public_key=Zadaný klíč je soukromý klíč. Nenahrávejte svůj soukromý klíč nikde. Místo toho použijte váš veřejný klíč. unable_verify_ssh_key=Nelze ověřit váš SSH klíč auth_failed=Ověření selhalo: %v @@ -823,6 +883,7 @@ remove_account_link=Odstranit propojený účet remove_account_link_desc=Odstraněním propojeného účtu zrušíte jeho přístup k vašemu Gitea účtu. Pokračovat? remove_account_link_success=Propojený účet byl odstraněn. + orgs_none=Nejste členem žádné organizace. repos_none=Nevlastníte žádné repozitáře @@ -1185,6 +1246,7 @@ commits.signed_by_untrusted_user_unmatched=Podepsáno nedůvěryhodným uživate commits.gpg_key_id=ID GPG klíče commits.ssh_key_fingerprint=Otisk klíče SSH +commit.operations=Operace commit.revert=Vrátit commit.revert-header=Vrátit: %s commit.revert-content=Vyberte větev pro návrat na: @@ -1217,11 +1279,21 @@ projects.type.bug_triage=Třídění chyb projects.template.desc=Šablona projektu projects.template.desc_helper=Vyberte šablonu projektu pro začátek projects.type.uncategorized=Nezařazené +projects.column.edit=Upravit sloupec projects.column.edit_title=Název projects.column.new_title=Název +projects.column.new_submit=Vytvořit sloupec +projects.column.new=Nový sloupec +projects.column.set_default=Nastavit jako výchozí +projects.column.set_default_desc=Nastavit tento sloupec jako výchozí pro nekategorizované úkoly a požadavky na natažení +projects.column.delete=Smazat sloupec +projects.column.deletion_desc=Smazání projektového sloupce přesune všechny související problémy do kategorie „Nezařazené“. Pokračovat? projects.column.color=Barva projects.open=Otevřít projects.close=Zavřít +projects.column.assigned_to=Přiřazeno k +projects.card_type.images_and_text=Obrázky a text +projects.card_type.text_only=Pouze text issues.desc=Organizování hlášení chyb, úkolů a milníků. issues.filter_assignees=Filtrovat zpracovatele @@ -1298,6 +1370,7 @@ issues.filter_label_no_select=Všechny štítky issues.filter_milestone=Milník issues.filter_milestone_no_select=Všechny milníky issues.filter_project=Projekt +issues.filter_project_all=Všechny projekty issues.filter_project_none=Žádný projekt issues.filter_assignee=Zpracovatel issues.filter_assginee_no_select=Všichni zpracovatelé @@ -1383,6 +1456,7 @@ issues.save=Uložit issues.label_title=Název štítku issues.label_description=Popis štítku issues.label_color=Barva štítku +issues.label_exclusive=Exkluzivní issues.label_count=%d štítků issues.label_open_issues=%d otevřených úkolů issues.label_edit=Upravit @@ -1828,6 +1902,7 @@ settings.mirror_sync_in_progress=Právě probíhá synchronizace zrcadla. Zkuste settings.site=Webová stránka settings.update_settings=Aktualizovat nastavení settings.branches.update_default_branch=Aktualizovat výchozí větev +settings.branches.add_new_rule=Přidat nové pravidlo settings.advanced_settings=Pokročilá nastavení settings.wiki_desc=Povolit Wiki repozitáře settings.use_internal_wiki=Používat vestavěnou Wiki @@ -2022,6 +2097,8 @@ settings.event_package=Balíček settings.event_package_desc=Balíček vytvořen nebo odstraněn v repozitáři. settings.branch_filter=Filtr větví settings.branch_filter_desc=Povolené větve pro události nahrání, vytvoření větve a smazání větve jsou určeny pomocí zástupného vzoru. Pokud je prázdný nebo *, všechny události jsou ohlášeny. Podívejte se na dokumentaci syntaxe na github.com/gobwas/glob. Příklady: master, {master,release*}. +settings.authorization_header=Autorizační hlavička +settings.authorization_header_desc=Pokud vyplněno, bude připojeno k požadavkům jako autorizační hlavička. Příklady: %s. settings.active=Aktivní settings.active_helper=Informace o spuštěných událostech budou odeslány na URL webového háčku. settings.add_hook_success=Webový háček byl přidán. @@ -2066,6 +2143,8 @@ settings.deploy_key_deletion_desc=Odstranění klíče pro nasazení zruší jeh settings.deploy_key_deletion_success=Klíč pro nasazení byl odstraněn. settings.branches=Větve settings.protected_branch=Ochrana větví +settings.protected_branch.save_rule=Uložit pravidlo +settings.protected_branch.delete_rule=Odstranit pravidlo settings.protected_branch_can_push=Povolit nahrání? settings.protected_branch_can_push_yes=Můžete nahrávat settings.protected_branch_can_push_no=Nemůžete nahrávat @@ -2543,6 +2622,9 @@ dashboard.delete_old_actions=Odstranit všechny staré akce z databáze dashboard.delete_old_actions.started=Začalo odstraňování všech starých akcí z databáze. dashboard.update_checker=Kontrola aktualizací dashboard.delete_old_system_notices=Odstranit všechna stará systémová upozornění z databáze +dashboard.stop_zombie_tasks=Zastavit zombie úkoly +dashboard.stop_endless_tasks=Zastavit nekonečné úkoly +dashboard.cancel_abandoned_jobs=Zrušit opuštěné úlohy users.user_manage_panel=Správa uživatelských účtů users.new_account=Vytvořit uživatelský účet @@ -2724,6 +2806,7 @@ auths.oauth2_required_claim_value_helper=Nastavte tuto hodnotu pro omezení při auths.oauth2_group_claim_name=Název tvrzení poskytující názvy skupin pro tento zdroj. (nepovinné) auths.oauth2_admin_group=Hodnota tvrzení pro skupinu uživatelů administrátorů. (Volitelné - vyžaduje název tvrzení výše) auths.oauth2_restricted_group=Hodnota tvrzení pro skupinu omezených uživatelů. (Volitelné - vyžaduje název tvrzení výše) +auths.oauth2_map_group_to_team_removal=Odebrat uživatele z synchronizovaných týmů, pokud uživatel nepatří do odpovídající skupiny. auths.enable_auto_register=Povolit zaregistrování se auths.sspi_auto_create_users=Automaticky vytvářet uživatele auths.sspi_auto_create_users_helper=Povolit SSPI autentizační metodě automaticky vytvářet nové účty pro uživatele, kteří se poprvé přihlásili @@ -2986,6 +3069,7 @@ monitor.queue.pool.cancel_desc=Opustit frontu bez skupin workerů může způsob notices.system_notice_list=Systémová oznámení notices.view_detail_header=Zobrazit detaily oznámení +notices.operations=Operace notices.select_all=Vybrat vše notices.deselect_all=Zrušit výběr všech notices.inverse_selection=Inverzní výběr @@ -3011,6 +3095,7 @@ reopen_pull_request=`znovuotevřel/a požadavek na natažení %[ comment_issue=`okomentoval/a problém %[3]s#%[2]s` comment_pull=`okomentoval/a požadavek na natažení %[3]s#%[2]s` merge_pull_request=`sloučil/a požadavek na natažení %[3]s#%[2]s` +auto_merge_pull_request=`automaticky sloučen požadavek na natažení %[3]s#%[2]s` transfer_repo=předal/a repozitář %s uživateli/organizaci %s push_tag=nahrál/a značku %[3]s do %[4]s delete_tag=smazal/a značku %[2]s z %[3]s @@ -3109,6 +3194,8 @@ keywords=Klíčová slova details=Podrobnosti details.author=Autor details.project_site=Stránka projektu +details.repository_site=Stránka repositáře +details.documentation_site=Stránka dokumentace details.license=Licence assets=Prostředky versions=Verze @@ -3116,7 +3203,13 @@ versions.on= versions.view_all=Zobrazit všechny dependency.id=ID dependency.version=Verze +cargo.install=Chcete-li nainstalovat balíček pomocí Cargo, spusťte následující příkaz: +cargo.documentation=Další informace o registru Cargo naleznete v dokumentaci. +cargo.details.repository_site=Stránka repositáře +cargo.details.documentation_site=Stránka dokumentace +chef.registry=Nastavit tento registr v souboru ~/.chef/config.rb: chef.install=Pro instalaci balíčku spusťte následující příkaz: +chef.documentation=Další informace o registru Chef naleznete v dokumentaci. composer.registry=Nastavit tento registr v souboru ~/.composer/config.json: composer.install=Pro instalaci balíčku pomocí Compposer spusťte následující příkaz: composer.documentation=Další informace o registru Composer naleznete v dokumentaci. @@ -3126,6 +3219,11 @@ conan.details.repository=Repozitář conan.registry=Nastavte tento registr z příkazového řádku: conan.install=Pro instalaci balíčku pomocí Conan spusťte následující příkaz: conan.documentation=Další informace o registru Conan naleznete v dokumentaci. +conda.registry=Nastavte tento registr jako Conda repozitář ve vašem .condarc: +conda.install=Pro instalaci balíčku pomocí Conda spusťte následující příkaz: +conda.documentation=Další informace o registru Conda naleznete v dokumentaci. +conda.details.repository_site=Stránka repositáře +conda.details.documentation_site=Stránka dokumentace container.details.type=Typ obrazu container.details.platform=Platforma container.pull=Stáhněte obraz z příkazové řádky: @@ -3184,26 +3282,79 @@ settings.delete.description=Smazání balíčku je trvalé a nelze ho vrátit zp settings.delete.notice=Chystáte se odstranit %s (%s). Tato operace je nevratná, jste si jisti? settings.delete.success=Balíček byl odstraněn. settings.delete.error=Nepodařilo se odstranit balíček. +owner.settings.cargo.initialize=Inicializovat index +owner.settings.cargo.initialize.description=K použití registru Cargo je zapotřebí speciální indexový repozitář git. Zde jej můžete (znovu) vytvořit s požadovanou konfigurací. +owner.settings.cargo.initialize.error=Nepodařilo se inicializovat Cargo index: %v +owner.settings.cargo.initialize.success=Index Cargo byl úspěšně vytvořen. +owner.settings.cargo.rebuild=Znovu vytvořit Index +owner.settings.cargo.rebuild.description=Pokud index není synchronizován s uloženými balíčky Cargo, můžete jej zde obnovit. +owner.settings.cargo.rebuild.error=Obnovení Cargo indexu se nezdařilo: %v +owner.settings.cargo.rebuild.success=Cargo Index byl úspěšně obnoven. +owner.settings.cleanuprules.title=Spravovat pravidla pro čištění +owner.settings.cleanuprules.add=Přidat pravidlo pro čištění +owner.settings.cleanuprules.edit=Upravit pravidlo pro čištění +owner.settings.cleanuprules.none=Nejsou k dispozici žádná pravidla čištění. Přečtěte si dokumentaci a dozvíte se více. +owner.settings.cleanuprules.preview=Náhled pravidla pro čištění +owner.settings.cleanuprules.preview.overview=%d balíčků má být odstraněno. +owner.settings.cleanuprules.preview.none=Pravidlo čištění neodpovídá žádným balíčkům. owner.settings.cleanuprules.enabled=Povolený +owner.settings.cleanuprules.pattern_full_match=Použít vzor na úplný název balíčku +owner.settings.cleanuprules.keep.title=Verze, které odpovídají těmto pravidlům, jsou zachovány, i když odpovídají níže uvedenému pravidlu pro odstranění. +owner.settings.cleanuprules.keep.count=Zachovat nejnovější +owner.settings.cleanuprules.keep.count.1=1 verze na balíček +owner.settings.cleanuprules.keep.count.n=%d verzí na balíček +owner.settings.cleanuprules.keep.pattern=Ponechat odpovídající verze +owner.settings.cleanuprules.keep.pattern.container=U balíčků Container je vždy zachována nejnovější verze. +owner.settings.cleanuprules.remove.title=Verze, které odpovídají těmto pravidlům, jsou odstraněny, pokud výše uvedené pravidlo neukládá jejich zachování. +owner.settings.cleanuprules.remove.days=Odstranit verze starší než +owner.settings.cleanuprules.remove.pattern=Odstranit odpovídající verze +owner.settings.cleanuprules.success.update=Pravidlo pro čištění bylo aktualizováno. +owner.settings.cleanuprules.success.delete=Pravidlo pro čištění bylo odstraněno. +owner.settings.chef.keypair=Generovat pár klíčů [secrets] +secrets=Tajné klíče value=Hodnota name=Název [actions] +actions=Akce +unit.desc=Spravovat akce +status.unknown=Neznámý +status.waiting=Čekání +status.running=Probíhá +status.success=Úspěch +status.failure=Chyba +status.cancelled=Zrušeno +status.skipped=Přeskočeno +status.blocked=Blokováno +runners.status=Status runners.id=ID runners.name=Název runners.owner_type=Typ runners.description=Popis runners.labels=Štítky +runners.last_online=Poslední čas online +runners.agent_labels=Štítky agenta +runners.custom_labels=Vlastní štítky +runners.custom_labels_helper=Vlastní štítky jsou štítky, které správce přidává ručně. Štítky se oddělují čárkou, bílé znaky na začátku a na konci každého štítku se ignorují. runners.task_list.run=Spustit +runners.task_list.status=Status runners.task_list.repository=Repozitář runners.task_list.commit=Commit +runners.task_list.done_at=Dokončeno v +runners.update_runner=Aktualizovat změny +runners.status.unspecified=Neznámý +runners.status.idle=Nečinný runners.status.active=Aktivní +runners.status.offline=Offline +runs.all_workflows=Všechny pracovní postupy +runs.open_tab=%d otevřeno +runs.closed_tab=%d uzavřeno runs.commit=Commit diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index e8e54ded92a17..9bee2104e4d65 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -792,6 +792,7 @@ remove_account_link=Verknüpften Account entfernen remove_account_link_desc=Wenn du den verknüpften Account entfernst, wirst du darüber nicht mehr auf deinen Gitea-Account zugreifen können. Fortfahren? remove_account_link_success=Der verknüpfte Account wurde entfernt. + orgs_none=Du bist kein Mitglied in einer Organisation. repos_none=Du besitzt keine Repositories diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 17e5f336c01f9..6affdf7e8be77 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -803,6 +803,7 @@ remove_account_link=Αφαίρεση Συνδεδεμένου Λογαριασμ remove_account_link_desc=Η κατάργηση ενός συνδεδεμένου λογαριασμού θα ανακαλέσει την πρόσβασή του στο λογαριασμό σας στο Gitea. Συνέχεια remove_account_link_success=Ο συνδεδεμένος λογαριασμός έχει αφαιρεθεί. + orgs_none=Δεν είστε μέλος σε κάποιο οργανισμό. repos_none=Δεν έχετε κανένα αποθετήριο diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0ff680e22471d..677af1397ddb6 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -821,6 +821,8 @@ remove_account_link = Remove Linked Account remove_account_link_desc = Removing a linked account will revoke its access to your Gitea account. Continue? remove_account_link_success = The linked account has been removed. +hooks.desc = Add webhooks which will be triggered for all repositories owned by this user. + orgs_none = You are not a member of any organizations. repos_none = You do not own any repositories @@ -2288,6 +2290,8 @@ release.edit_subheader = Releases organize project versions. release.tag_name = Tag name release.target = Target release.tag_helper = Choose an existing tag or create a new tag. +release.tag_helper_new = New tag. This tag will be created from the target. +release.tag_helper_existing = Existing tag. release.title = Title release.content = Content release.prerelease_desc = Mark as Pre-Release @@ -2808,6 +2812,8 @@ auths.still_in_used = The authentication source is still in use. Convert or dele auths.deletion_success = The authentication source has been deleted. auths.login_source_exist = The authentication source '%s' already exists. auths.login_source_of_type_exist = An authentication source of this type already exists. +auths.unable_to_initialize_openid = Unable to initialize OpenID Connect Provider: %s +auths.invalid_openIdConnectAutoDiscoveryURL = Invalid Auto Discovery URL (this must be a valid URL starting with http:// or https://) config.server_config = Server Configuration config.app_name = Site Title diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index c705740e46fe7..f104d3c49d308 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -802,6 +802,7 @@ remove_account_link=Eliminar cuenta vinculada remove_account_link_desc=Eliminar una cuenta vinculada revocará su acceso a su cuenta de Gitea. ¿Continuar? remove_account_link_success=La cuenta vinculada ha sido eliminada. + orgs_none=No eres miembro de ninguna organización. repos_none=No posees ningún repositorio diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 7786dd69a8811..449da63b74b96 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -733,6 +733,7 @@ remove_account_link=حذف حساب پیوند خرده remove_account_link_desc=با حذف پیوند خارجی حساب کاربری دسترسی شما به حساب کابریتان توسط آن از بین میرود. آیا ادامه می‌دهید؟ remove_account_link_success=پیوند حساب کاربری از حذف شد. + orgs_none=شما عضو هیچ سازمانی نیستید. repos_none=شما مالک هیچ مخزنی نیستید diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index d0f4c6e9ca522..a589d12c21fd6 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -652,6 +652,7 @@ remove_account_link=Poista linkitetty tili remove_account_link_desc=Linkitetyn tilin poistaminen peruuttaa pääsyn Gitea-tiliisi linkitetyn tili kautta. Jatketaanko? remove_account_link_success=Linkitetty tili on poistettu. + orgs_none=Et ole minkään organisaation jäsen. repos_none=Sinulla ei ole repoja diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 477db4f7adb6a..15d20011eaa2d 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -694,6 +694,7 @@ oauth2_application_edit=Éditer link_account=Lier un Compte + confirm_delete_account=Confirmez la suppression delete_account_title=Supprimer cet utilisateur diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 542979bbf93fe..328af14d57b24 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -574,6 +574,7 @@ remove_account_link=Csatolt fiók eltávolítása remove_account_link_desc=Egy kapcsolt fiók törlésével visszavonja a hozzáférését a fiókjához. Folytatja? remove_account_link_success=A kapcsolt fiók törölve lett. + orgs_none=Nem tagja egy szervezetnek sem. repos_none=Nincsen egyetlen saját tárolója sem diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index eb6a38afcdf8b..5b418231c4fe1 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -510,6 +510,7 @@ remove_account_link=Hapus Akun Tertaut remove_account_link_desc=Menghapus akun tertaut akan membuat akun itu tidak bisa mengakses akun Gitea Anda. Lanjutkan? remove_account_link_success=Akun tertaut sudah dihapus. + orgs_none=Anda bukan anggota dari organisasi apapun. repos_none=Anda tidak memiliki repositori apapun diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index 40915596adf15..bd596105d538c 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -579,6 +579,7 @@ link_account=Tengja Notanda + email_notifications.enable=Virkja Tölvupósttilkynningar email_notifications.onmention=Aðeins Tölvupóst Þegar Minnst Er á Mig email_notifications.disable=Óvirkja Tölvupósttilkynningar diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 411c34663f3d9..d77baee6e502f 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -789,6 +789,7 @@ remove_account_link=Rimuovi account collegato remove_account_link_desc=Rimuovere un account collegato ne revoca l'accesso al tuo account Gitea. Continuare? remove_account_link_success=L'account collegato è stato rimosso. + orgs_none=Non sei membro di alcuna organizzazione. repos_none=Non possiedi alcun repository diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 5ad66a510166b..8e77329f5cc9b 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -247,6 +247,7 @@ default_enable_timetracking_popup=新しいリポジトリのタイムトラッ no_reply_address=メールを隠すときのドメイン no_reply_address_helper=メールアドレスを隠しているユーザーに使用するドメイン名。 例えば 'noreply.example.org' と設定した場合、ユーザー名 'joe' はGitに 'joe@noreply.example.org' としてログインすることになります。 password_algorithm=パスワードハッシュアルゴリズム +invalid_password_algorithm=無効なパスワードハッシュアルゴリズム password_algorithm_helper=パスワードハッシュアルゴリズムを設定します。 アルゴリズムにより動作要件と強度が異なります。 `argon2`は良い特性を備えていますが、多くのメモリを使用するため小さなシステムには適さない場合があります。 enable_update_checker=アップデートチェッカーを有効にする enable_update_checker_helper=gitea.ioに接続して定期的に新しいバージョンのリリースを確認します。 @@ -820,6 +821,7 @@ remove_account_link=連携アカウントの削除 remove_account_link_desc=連携アカウントを削除し、Giteaアカウントへのアクセス権を取り消します。 続行しますか? remove_account_link_success=連携アカウントを削除しました。 + orgs_none=あなたはどの組織のメンバーでもありません。 repos_none=あなたはリポジトリを所有していません。 diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 9f4509839beb5..ca642ebf0049e 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -541,6 +541,7 @@ remove_account_link=연결된 계정 제거 remove_account_link_desc=해당 계정을 연결해제 하는 경우 Gitea 계정에 대한 접근 권한이 사라지게 됩니다. 계속하시겠습니까? remove_account_link_success=연결된 계정이 제거 되었습니다. + orgs_none=당신은 어떤 조직의 구성원도 아닙니다. repos_none=어떤 레포지터리도 존재하지 않습니다. diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 13c2cee221f1b..0f7862f8a3dee 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -2,6 +2,7 @@ home=Sākums dashboard=Infopanelis explore=Izpētīt help=Palīdzība +logo=Logo sign_in=Pierakstīties sign_in_with=Pierakstīties izmantojot sign_out=Izrakstīties @@ -56,6 +57,7 @@ new_mirror=Jauns spogulis new_fork=Jauns atdalīts repozitorijs new_org=Jauna organizācija new_project=Jauns projekts +new_project_column=Jauna kolonna manage_org=Pārvaldīt organizācijas admin_panel=Lapas administrēšana account_settings=Konta iestatījumi @@ -89,9 +91,11 @@ disabled=Atspējots copy=Kopēt copy_url=Kopēt saiti +copy_content=Kopēt saturu copy_branch=Kopēt atzara nosaukumu copy_success=Nokopēts! copy_error=Kopēšana neizdevās +copy_type_unsupported=Šī veida failus nav iespējams nokopēt write=Rakstīt preview=Priekšskatītījums @@ -108,8 +112,14 @@ never=Nekad rss_feed=RSS barotne [aria] +navbar=Navigācijas josla +footer=Kājene +footer.software=Par programmatūru +footer.links=Saites [filter] +string.asc=A - Z +string.desc=Z - A [error] occurred=Radusies kļūda @@ -237,7 +247,10 @@ default_enable_timetracking_popup=Repozitorijiem pēc noklusējuma tiks iespējo no_reply_address=Neatbildēt e-pasta adreses domēns no_reply_address_helper=Domēns lietotāja e-pasta adresei git žurnālos, ja lietotājs izvēlas paturēt savu e-pasta adresi privātu. Piemēram, ja lietotājs ir 'janis' un domēns 'neatbildet.piemers.lv', tad e-pasta adrese būs 'janis@neatbildet.piemers.lv'. password_algorithm=Paroles jaucējsummas algoritms +invalid_password_algorithm=Kļūdaina paroles jaucējfunkcija password_algorithm_helper=Norādiet paroles jaucējkoda algoritmu. Alogritmiem ir dažādas prasības un stiprums. Lai arī `argon2` ir nodrošina labu drošība, tas patērē daudz operatīvās atmiņas un var nebūt piemērots sistēmām ar nelieliem resursiem. +enable_update_checker=Iespējot jaunu versiju paziņojumus +enable_update_checker_helper=Periodiski pārbaudīt jaunu version pieejamību, izgūstot datus no gitea.io. [home] uname_holder=Lietotājvārds vai e-pasts @@ -346,22 +359,112 @@ authorize_application_created_by=Šo lietotni izveidoja %s. authorize_application_description=Ja piešķirsiet tiesības, tā varēs piekļūt un mainīt Jūsu konta informāciju, ieskaitot privātos repozitorijus un organizācijas. authorize_title=`Autorizēt "` +show_archived=Arhivētie +show_both_archived_unarchived=Attēlot gan arhivētos, gan nearhivētos +show_only_archived=Attēlot tikai arhivētos +show_only_unarchived=Attēlot tikai nearhivētos show_private=Privāts +show_both_private_public=Attēlot gan publiskos, gan privātos +show_only_private=Attēlot tikai privātos +show_only_public=Attēlot tikai publiskos +issues.in_your_repos=Jūsu repozitorijos [explore] repos=Repozitoriji +users=Lietotāji organizations=Organizācijas +search=Meklēt code=Kods +search.type.tooltip=Meklēšanas veids +search.fuzzy=Aptuveni +search.fuzzy.tooltip=Iekļaut meklēšanas rezultātos arī aptuvenas sakritības +search.match=Precīzi +search.match.tooltip=Iekļaut meklēšanas rezultātos tikai precīzas sakritības +code_search_unavailable=Pašlaik koda meklēšana nav pieejama. Sazinieties ar lapas administratoru. +repo_no_results=Netika atrasts neviens repozitorijs, kas atbilstu kritērijiem. +user_no_results=Netika atrasts neviens lietotājs, kas atbilstu kritērijiem. +org_no_results=Netika atrasta neviena organizācija, kas atbilstu kritērijiem. +code_no_results=Netika atrasts pirmkods, kas atbilstu kritērijiem. +code_search_results=Meklēšanas rezultāti '%s' +code_last_indexed_at=Pēdējo reizi indeksēts %s +relevant_repositories_tooltip=Repozitoriju, kas ir atdalīti vai kuriem nav tēmas, ikonas un apraksta ir paslēpti. +relevant_repositories=Tikai būtiskie repozitoriji tiek rādīti, pārādīt nefiltrētus rezultātus. [auth] +create_new_account=Reģistrēt kontu +register_helper_msg=Jau ir konts? Pieraksties tagad! +social_register_helper_msg=Jau ir konts? Piesaisti to! +disable_register_prompt=Reģistrācija ir atspējota. Lūdzu, sazinieties ar vietnes administratoru. +disable_register_mail=Reģistrācijas e-pasta apstiprināšana ir atspējota. +manual_activation_only=Sazinieties ar lapas administratoru, lai pabeigtu konta aktivizāciju. +remember_me=Atcerēties šo ierīci +forgot_password_title=Aizmirsu paroli +forgot_password=Aizmirsi paroli? +sign_up_now=Nepieciešams konts? Reģistrējies tagad. +sign_up_successful=Konts tika veiksmīgi izveidots. +confirmation_mail_sent_prompt=Jauns apstiprināšanas e-pasts ir nosūtīts uz %s, pārbaudies savu e-pasta kontu tuvāko %s laikā, lai pabeigtu reģistrācijas procesu. +must_change_password=Mainīt paroli +allow_password_change=Pieprasīt lietotājam mainīt paroli (ieteicams) +reset_password_mail_sent_prompt=Apstiprināšanas e-pasts tika nosūtīts uz %s. Pārbaudiet savu e-pasta kontu tuvāko %s laikā, lai pabeigtu paroles atjaunošanas procesu. +active_your_account=Aktivizēt savu kontu +account_activated=Konts ir aktivizēts +prohibit_login=Aizliegt pieteikšanos +prohibit_login_desc=Jūsu konts ir bloķēts, sazinieties ar sistēmas administratoru. +resent_limit_prompt=Jūs pieprasījāt aktivizācijas e-pastu pārāk bieži. Lūdzu, uzgaidiet 3 minūtes un mēģiniet vēlreiz. +has_unconfirmed_mail=Sveiki %s, Jums ir neapstiprināta e-pasta adrese (%s). Ja neesat saņēmis apstiprināšanas e-pastu vai Jums ir nepieciešams nosūtīt jaunu, lūdzu, nospiediet pogu, kas atrodas zemāk. +resend_mail=Nospiediet šeit, lai vēlreiz nosūtītu aktivizācijas e-pastu +email_not_associate=Šī e-pasta adrese nav saistīta ar nevienu kontu. +send_reset_mail=Nosūtīt paroles atjaunošanas e-pastu +reset_password=Paroles atjaunošana +invalid_code=Jūsu apstiprināšanas kodam ir beidzies derīguma termiņš vai arī tas ir nepareizs. +invalid_password=Jūsu parole neatbilst parolei, kas tika ievadīta veidojot so kontu. +reset_password_helper=Atjaunot paroli +reset_password_wrong_user=Jūs esat autorizējies kā %s, bet paroles atjaunošanas saite ir lietotājam %s +password_too_short=Paroles garums nedrīkst būt mazāks par %d simboliem. +non_local_account=Ārējie konti nevar mainīt paroli, izmantojot, Gitea saskarni. +verify=Pārbaudīt +scratch_code=Vienreizējais kods +use_scratch_code=Izmantot vienreizējo kodu +twofa_scratch_used=Jūs esat izmantojis vienreizējo kodu. Jūs tikāt pārsūtīts uz divu faktoru iestatījumu lapu, lai varētu piesaistīto ierīci vai lai uzģenerētu jaunu vienreizējo kodu. +twofa_passcode_incorrect=Jūsu kods nav pareizs. Ja esat pazaudējis ierīci, izmantojiet vienreizējo kodu, lai autorizētos. +twofa_scratch_token_incorrect=Ievadīts nepareizs vienreizējais kods. login_userpass=Pierakstīties +login_openid=OpenID +oauth_signup_tab=Reģistrēt jaunu kontu +oauth_signup_title=Pabeigt konta veidošanu +oauth_signup_submit=Pabeigt reģistrāciju +oauth_signin_tab=Savienot ar esošu kontu +oauth_signin_title=Pierakstīties, lai autorizētu saistīto kontu oauth_signin_submit=Saistītie konti +oauth.signin.error=Radās kļūda apstrādājot autorizācijas pieprasījumu. Ja šī kļūda atkārtojas, sazinieties ar lapas administratoru. +oauth.signin.error.access_denied=Autorizācijas pieprasījums tika noraidīts. +oauth.signin.error.temporarily_unavailable=Autorizācija neizdevās, jo autentifikācijas serveris ir īslaicīgi nepieejams. Mēģiniet autorizēties vēlāk. +openid_connect_submit=Pievienoties +openid_connect_title=Pievienoties jau esošam kontam +openid_connect_desc=Izvēlētais OpenID konts sistēmā netika atpazīts, bet Jūs to varat piesaistīt esošam kontam. +openid_register_title=Izveidot jaunu kontu +openid_register_desc=Izvēlētais OpenID konts sistēmā netika atpazīts, bet Jūs to varat piesaistīt esošam kontam. +openid_signin_desc=Ievadiet savu OpenID URI, piemēram: https://anna.me, peteris.openid.org.lv, gnusocial.net/janis. +disable_forgot_password_mail=Konta atjaunošana ir atspējota, jo nav uzstādīti e-pasta servera iestatījumi. Sazinieties ar lapas administratoru. +disable_forgot_password_mail_admin=Kontu atjaunošana ir pieejama tikai, ja ir veikta e-pasta servera iestatījumu konfigurēšana. Norādiet e-pasta servera iestatījumus, lai iespējotu kontu atjaunošanu. +email_domain_blacklisted=Nav atļauts reģistrēties ar šādu e-pasta adresi. +authorize_application=Autorizēt lietotni +authorize_redirect_notice=Jūs tiksiet nosūtīts uz %s, ja autorizēsiet šo lietotni. +authorize_application_created_by=Šo lietotni izveidoja %s. +authorize_application_description=Ja piešķirsiet tiesības, tā varēs piekļūt un mainīt Jūsu konta informāciju, ieskaitot privātos repozitorijus un organizācijas. +authorize_title=Autorizēt "%s" piekļuvi jūsu kontam? +authorization_failed=Autorizācija neizdevās +authorization_failed_desc=Autorizācija neizdevās, jo tika veikts kļūdains pieprasījums. Sazinieties ar lietojumprogrammas, ar kuru mēģinājāt autorizēties, uzturētāju. +sspi_auth_failed=SSPI autentifikācija neizdevās +password_pwned=Ievadītā parole ir zagto paroļu sarakstā, kas ir kādā no publicētājām datu zādzībām. Mēģiniet vēlreiz citu paroli. +password_pwned_err=Neizdevās pabeigt pieprasījumu uz HaveIBeenPwned [mail] view_it_on=Aplūkot %s +reply=vai atbildiet uz e-pastu link_not_working_do_paste=Ja saite nestrādā, mēģiniet to nokopēt un atvērt pārlūkā. hi_user_x=Sveiki %s, @@ -420,6 +523,10 @@ repo.transfer.body=Ja vēlaties to noraidīt vai apstiprināt, tad apmeklējiet repo.collaborator.added.subject=%s pievienoja Jūs repozitorijam %s repo.collaborator.added.text=Jūs tikāt pievienots kā līdzstrādnieks repozitorijam: +team_invite.subject=%[1]s uzaicināja Jūs pievienoties organizācijai %[2]s +team_invite.text_1=%[1]s uzaicināja Jūs pievienoties komandai %[2] organizācijā %[3]s. +team_invite.text_2=Uzspiediet uz šīs saites, lai pievienoties komandai: +team_invite.text_3=Piezīme: Šis uzaicinājums ir paredzēts %[1]s. Ja uzskatāt, ka tas nav domāts Jums, varat ignorēt šo e-pastu. [modal] yes=Jā @@ -433,6 +540,7 @@ Email=E-pasta adrese Password=Parole Retype=Atkārtoti ievadiet paroli SSHTitle=SSH atslēgas nosaukums +HttpsUrl=HTTPS URL PayloadUrl=Vērtuma URL TeamName=Komandas nosaukums AuthName=Autorizācijas nosaukums @@ -460,6 +568,8 @@ url_error=`'%s' nav korekts URL.` include_error=` ir jāsatur tekstu '%s'.` glob_pattern_error=` glob šablons nav korekts: %s.` regex_pattern_error=` regulārā izteiksme nav korekta: %s.` +username_error=` drīkst saturēt tikai burtus un ciparus ('0-9','a-z','A-Z'), domuzīme ('-'), apakšsvītra ('_') un punkts ('.'). Nevar sākties vai beigties ar simbolu, kas nav burts vai skaitlis, kā arī nevar būt vairāki simboli pēc kārtas, kas nav burti vai skaitļi.` +invalid_group_team_map_error=` sasaiste nav korekta: %s` unknown_error=Nezināma kļūda: captcha_incorrect=Ievadīts nepareizs drošības kods. password_not_match=Izvēlētā parole nesakrīt ar atkārtoti ievadīto. @@ -495,10 +605,13 @@ user_not_exist=Lietotājs neeksistē. team_not_exist=Komanda neeksistē. last_org_owner=Nevar noņemt pēdejo lietotāju no īpašnieku komandas. Organizācijai ir jābūt vismaz vienam īpašniekam. cannot_add_org_to_team=Organizāciju nevar pievienot kā komandas biedru. +duplicate_invite_to_team=Lietotājs jau ir uzaicināts kā komandas biedrs. +organization_leave_success=Jūs esat pametis organizāciju %s. invalid_ssh_key=Nav iespējams pārbaudīt SSH atslēgu: %s invalid_gpg_key=Nav iespējams pārbaudīt GPG atslēgu: %s invalid_ssh_principal=Kļūdaina identitāte: %s +must_use_public_key=Atslēga, ko norādījāt ir privātā atslēga. Nekad nenodotiet savu privātu atslēgu nevienam. Izmantojiet publisko atslēgu. unable_verify_ssh_key=SSH atslēgu nav iespējams pārbaudīt, pārliecinieties, ka tajā nav kļūdu. auth_failed=Autentifikācija neizdevās: %v @@ -670,6 +783,7 @@ gpg_invalid_token_signature=Norādītā GPG atslēga, paraksts un talons neatbil gpg_token_required=Jānorāda paraksts zemāk esošajam talonam gpg_token=Talons gpg_token_help=Parakstu ir iespējams uzģenerēt izmantojot komandu: +gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature=Tekstuāls GPG paraksts key_signature_gpg_placeholder=Sākas ar '-----BEGIN PGP SIGNATURE-----' verify_gpg_key_success=GPG atslēga '%s' veiksmīgi pārbaudīta. @@ -734,6 +848,8 @@ access_token_deletion_cancel_action=Atcelt access_token_deletion_confirm_action=Dzēst access_token_deletion_desc=Izdzēšot talonu, tam tiks liegta piekļuve šim kontam. Šī darbība ir neatgriezeniska. Vai turpināt? delete_token_success=Piekļuves talons tika noņemts. Neaizmirstiet atjaunot informāciju lietojumprogrammās, kas izmantoja šo talonu. +select_scopes=Norādiet apgabalus +scopes_list=Apgabali: manage_oauth2_applications=Pārvaldīt OAuth2 lietotnes edit_oauth2_application=Labot OAuth2 lietotni @@ -746,6 +862,7 @@ create_oauth2_application_button=Izveidot lietotni create_oauth2_application_success=OAuth2 lietotne veiksmīgi izveidota. update_oauth2_application_success=OAuth2 lietotne veiksmīgi atjaunināta. oauth2_application_name=Lietotnes nosaukums +oauth2_confidential_client=Konfidenciāls klients. Norādiet lietotēm, kas glabā noslēpumu slepenībā, piemēram, tīmekļa lietotnēm. Nenorādiet instalējamām lietotnēm, tai skaitā darbavirsmas vai mobilajām lietotnēm. oauth2_redirect_uri=Novirzīšanas URI save_application=Saglabāt oauth2_client_id=Klienta ID @@ -796,6 +913,8 @@ remove_account_link=Noņemt saistīto kontu remove_account_link_desc=Noņemot saistīto kontu, tam tiks liegta piekļuve Jūsu Gitea kontam. Vai turpināt? remove_account_link_success=Saistītais konts tika noņemts. +hooks.desc=Pievienot tīmekļā āķus, kas izpildīsies visiem repozitorijiem, kas pieder šim lietotājam. + orgs_none=Jūs neesat nevienas organizācijas biedrs. repos_none=Jums nepieder neviens repozitorijs @@ -1005,10 +1124,12 @@ unstar=Noņemt zvaigznīti star=Pievienot zvaigznīti fork=Atdalīts download_archive=Lejupielādēt repozitoriju +more_operations=Vairāk darbību no_desc=Nav apraksta quick_guide=Īsa pamācība clone_this_repo=Klonēt šo repozitoriju +cite_this_repo=Citēt šo repozitoriju create_new_repo_command=Izveidot jaunu repozitoriju komandrindā push_exist_repo=Nosūtīt izmaiņas no komandrindas eksistējošam repozitorijam empty_message=Repozitorijs ir tukšs. @@ -1046,6 +1167,13 @@ file_view_rendered=Skatīt rezultātu file_view_raw=Rādīt neapstrādātu file_permalink=Patstāvīgā saite file_too_large=Šis fails ir par lielu, lai to parādītu. +invisible_runes_header=`Šis fails satur satur neredzamus unikoda simbolus!` +invisible_runes_description=`Šis fails satur neredzamus unikoda simbolus, kas var mainīt kā saturs tiek attēlots. Ja tie ir izmantoti ar pamatotu nodumu, tad varat ignorēt šo brīdinājumu. Izmantojiet Kodēt pogu, lai parādītu šos neredzamos simbolus.` +ambiguous_runes_header=`Šis fails satur neviennozīmīgus unikoda simbolus!` +ambiguous_runes_description=`Šis fails satur nevienozīmīgus unikoda simbolus, kas var būt mulsinoši un grūti atšķirami. Ja tie ir izmantoti ar pamatotu nodumu, tad varat ignorēt šo brīdinājumu. Izmantojiet Kodēt pogu, lai parādītu šos neredzamos simbolus.` +invisible_runes_line=`Šī līnija satur neredzamus unikoda simbolus` +ambiguous_runes_line=`Šī līnija satur neviennozīmīgus unikoda simbolus` +ambiguous_character=`%[1]c [U+%04[1]X] var tikt sajaukts ar %[2]c [U+%04[2]X]` escape_control_characters=Kodēt unescape_control_characters=Atkodēt @@ -1100,6 +1228,7 @@ editor.commit_directly_to_this_branch=Apstiprināt revīzijas izmaiņas atzarā editor.create_new_branch=Izveidot jaunu atzaru un izmaiņu pieprasījumu šai revīzijai. editor.create_new_branch_np=Izveidot jaunu atzaru šai revīzijai. editor.propose_file_change=Ieteikt faila izmaiņas +editor.new_branch_name=Jaunā atzara nosaukums šai revīzijai editor.new_branch_name_desc=Jaunā atzara nosaukums… editor.cancel=Atcelt editor.filename_cannot_be_empty=Faila nosaukums nevar būt tukšs. @@ -1151,6 +1280,7 @@ commits.signed_by_untrusted_user_unmatched=Parakstījis neuzticams lietotājs, k commits.gpg_key_id=GPG atslēgas ID commits.ssh_key_fingerprint=SSH atslēgas identificējošā zīmju virkne +commit.operations=Darbības commit.revert=Atgriezt commit.revert-header=Atgriezt: %s commit.revert-content=Norādiet atzaru uz kuru atgriezt: @@ -1183,11 +1313,22 @@ projects.type.bug_triage=Kļūdu šķirošana projects.template.desc=Projekta sagatave projects.template.desc_helper=Izvēlieties projekta sagatavi, lai sāktu darbu projects.type.uncategorized=Bez kategorijas +projects.column.edit=Rediģēt kolonnas projects.column.edit_title=Nosaukums projects.column.new_title=Nosaukums +projects.column.new_submit=Izveidot kolonnu +projects.column.new=Jauna kolonna +projects.column.set_default=Izvēlēties kā noklusēto +projects.column.set_default_desc=Izvēlēties šo kolonnu kā noklusēto nekategorizētām problēmām un izmaiņu pieteikumiem +projects.column.delete=Dzēst kolonnu +projects.column.deletion_desc=Dzēšot projekta kolonnu visas tam piesaistītās problēmas tiks pārliktas kā nekategorizētas. Vai turpināt? projects.column.color=Krāsa projects.open=Aktīvie projects.close=Pabeigtie +projects.column.assigned_to=Piešķirts +projects.card_type.desc=Kartītes priekšskatījums +projects.card_type.images_and_text=Attēli un teksts +projects.card_type.text_only=Tikai teksts issues.desc=Organizēt kļūdu ziņojumus, uzdevumus un atskaites punktus. issues.filter_assignees=Filtrēt pēc atbildīgajiem @@ -1223,6 +1364,8 @@ issues.new.add_reviewer_title=Pieprasīt recenziju issues.choose.get_started=Sākt darbu issues.choose.blank=Noklusējuma issues.choose.blank_about=Izveidot problēmu ar noklusējuma sagatavi. +issues.choose.ignore_invalid_templates=Kļūdainās sagataves tika izlaistas +issues.choose.invalid_templates=%v ķļūdaina sagatave(s) atrastas issues.no_ref=Nav norādīts atzars/tags issues.create=Pieteikt problēmu issues.new_label=Jauna etiķete @@ -1262,16 +1405,19 @@ issues.filter_label_no_select=Visas etiķetes issues.filter_milestone=Atskaites punkts issues.filter_milestone_no_select=Visi atskaites punkti issues.filter_project=Projektus +issues.filter_project_all=Visi projekti issues.filter_project_none=Nav projektu issues.filter_assignee=Atbildīgais issues.filter_assginee_no_select=Visi atbildīgie issues.filter_poster=Autors +issues.filter_poster_no_select=Visi autori issues.filter_type=Veids issues.filter_type.all_issues=Visas problēmas issues.filter_type.assigned_to_you=Piešķirtās Jums issues.filter_type.created_by_you=Jūsu izveidotās issues.filter_type.mentioning_you=Esat pieminēts issues.filter_type.review_requested=Pieprasīta recenzija +issues.filter_type.reviewed_by_you=Tavi recenzētie issues.filter_sort=Kārtot issues.filter_sort.latest=Jaunākie issues.filter_sort.oldest=Vecakie @@ -1293,6 +1439,8 @@ issues.action_milestone=Atskaites punkts issues.action_milestone_no_select=Nav atskaites punkta issues.action_assignee=Atbildīgais issues.action_assignee_no_select=Nav atbildīgā +issues.action_check=Atzīmēt/Notīrīt +issues.action_check_all=Atzīmēt/Notīrīt visus ierakstus issues.opened_by=%[3]s atvēra %[1]s pulls.merged_by=%[3]s sapludināja %[1]s pulls.merged_by_fake=%[2]s sapludināja %[1]s @@ -1346,6 +1494,9 @@ issues.save=Saglabāt issues.label_title=Etiķetes nosaukums issues.label_description=Etiķetes apraksts issues.label_color=Etiķetes krāsa +issues.label_exclusive=Ekskluzīvs +issues.label_exclusive_desc=Nosauciet etiķeti grupa/nosaukums, lai grupētu etiķētes un varētu norādīt tās kā ekskluzīvas ar citām grupa/ etiķetēm. +issues.label_exclusive_warning=Jebkura konfliktējoša ekskluzīvas grupas etiķete tiks noņemta, labojot pieteikumu vai izmaiņu pietikumu etiķetes. issues.label_count=%d etiķetes issues.label_open_issues=%d atvērtas problēmas issues.label_edit=Labot @@ -1414,6 +1565,7 @@ issues.push_commit_1=iesūtīja %d revīziju %s issues.push_commits_n=iesūtīja %d revīzijas %s issues.force_push_codes=`veica piespiedu izmaiņu iesūtīšanu atzarā %[1]s no revīzijas %[2]s uz %[4]s %[6]s` issues.force_push_compare=Salīdzināt +issues.due_date_form=dd.mm.yyyy issues.due_date_form_add=Pievienot izpildes termiņu issues.due_date_form_edit=Labot issues.due_date_form_remove=Noņemt @@ -1599,6 +1751,8 @@ pulls.reopened_at=`atkārtoti atvēra šo izmaiņu pieprasījumu komandrindas instrukcijas.` pulls.merge_instruction_step1_desc=Projekta repozitorijā izveidojiet jaunu jaunu atzaru un pārbaudiet savas izmaiņas. pulls.merge_instruction_step2_desc=Sapludināt izmaiņas un atjaunot tās Gitea. +pulls.clear_merge_message=Notīrīt sapludināšanas ziņojumu +pulls.clear_merge_message_hint=Notīrot sapludināšanas ziņojumu tiks noņemts tikai pats ziņojums, bet tiks paturēti ģenerētie git ziņojumu, kā "Co-Authored-By …". pulls.auto_merge_button_when_succeed=(Kad pārbaudes veiksmīgas) pulls.auto_merge_when_succeed=Automātiski sapludināt, kad visas pārbaudes veiksmīgas @@ -1754,8 +1908,11 @@ activity.git_stats_deletion_n=%d dzēšanas search=Meklēt search.search_repo=Meklēšana repozitorijā +search.type.tooltip=Meklēšanas veids search.fuzzy=Aptuveni +search.fuzzy.tooltip=Iekļaut meklēšanas rezultātos arī aptuvenas sakritības search.match=Precīzi +search.match.tooltip=Iekļaut meklēšanas rezultātos tikai precīzas sakritības search.results=Meklēšanas rezultāti nosacījumam "%s" repozitorijā %s search.code_no_results=Netika atrasts pirmkods, kas atbilstu kritērijiem. search.code_search_unavailable=Pašlaik koda meklēšana nav pieejama. Sazinieties ar lapas administratoru. @@ -1787,6 +1944,7 @@ settings.mirror_sync_in_progress=Notiek spoguļa sinhronizācija. Atjaunojiet la settings.site=Mājas lapa settings.update_settings=Mainīt iestatījumus settings.branches.update_default_branch=Atjaunot noklusēto atzaru +settings.branches.add_new_rule=Pievienot jaunu noteikumu settings.advanced_settings=Papildu iestatījumi settings.wiki_desc=Iespējot vikivietnes settings.use_internal_wiki=Izmantot iebūvēto vikivietni @@ -1816,8 +1974,11 @@ settings.pulls.ignore_whitespace=Pārbaudot konfliktus, ignorēt izmaiņas atsta settings.pulls.enable_autodetect_manual_merge=Iespējot manuālo sapludināšanas noteikšanu (Piezīme: dažos speciālos gadījumos, tas var nostrādāt nekorekti) settings.pulls.allow_rebase_update=Iespējot izmaiņu pieprasījuma atjaunošanu ar pārbāzēšanu settings.pulls.default_delete_branch_after_merge=Pēc noklusējuma dzēst izmaiņu pieprasījuma atzaru pēc sapludināšanas +settings.pulls.default_allow_edits_from_maintainers=Atļaut uzturētājiem labot pēc noklusējuma +settings.releases_desc=Iespējot repozitorija laidienus settings.packages_desc=Iespējot repozitorija pakotņu reģistru settings.projects_desc=Iespējot repozitorija projektus +settings.actions_desc=Iespējot repozitorija darbības settings.admin_settings=Administratora iestatījumi settings.admin_enable_health_check=Iespējot veselības pārbaudi (git fsck) šim repozitorijam settings.admin_code_indexer=Izejas koda indeksētājs @@ -1884,6 +2045,7 @@ settings.confirm_delete=Dzēst repozitoriju settings.add_collaborator=Pievienot līdzstrādnieku settings.add_collaborator_success=Jauns līdzstrādnieks tika pievienots. settings.add_collaborator_inactive_user=Nevar pievienot neaktīvu lietotāju kā līdzstrādnieku. +settings.add_collaborator_owner=Nevar pievienot īpašnieku kā līdzstrādnieku. settings.add_collaborator_duplicate=Līdzstrādnieks jau ir pievienots šim repozitorijam. settings.delete_collaborator=Noņemt settings.collaborator_deletion=Noņemt līdzstrādnieku @@ -1943,6 +2105,7 @@ settings.event_delete_desc=Atzars vai tags izdzēsts. settings.event_fork=Atdalīts settings.event_fork_desc=Repozitorijs atdalīts. settings.event_wiki=Vikivietni +settings.event_wiki_desc=Vikivietnes lapa izveidota, pārsaukta, labota vai dzēsta. settings.event_release=Laidiens settings.event_release_desc=Publicēts, atjaunots vai dzēsts laidiens repozitorijā. settings.event_push=Izmaiņu nosūtīšana @@ -2025,6 +2188,8 @@ settings.deploy_key_deletion_desc=Noņemot izvietošanas atslēgu, tai tiks lieg settings.deploy_key_deletion_success=Izvietošanas atslēga tika noņemta. settings.branches=Atzari settings.protected_branch=Atzaru aizsargāšana +settings.protected_branch.save_rule=Saglabāt noteikumu +settings.protected_branch.delete_rule=Dzēst noteikumu settings.protected_branch_can_push=Atļaut izmaiņu nosūtīšanu? settings.protected_branch_can_push_yes=Jūs varat nosūtīt izmaiņas settings.protected_branch_can_push_no=Jūs nevarat nosūtīt izmaiņas @@ -2059,6 +2224,7 @@ settings.dismiss_stale_approvals=Pieprasīt apstiprinājumus jaunākajām izmai settings.dismiss_stale_approvals_desc=Kad tiek iesūtītas jaunas revīzijas, kas izmaina izmaiņu pieprasījuma saturu, iepriekšējie apstiprinājumi tiks atzīmēti kā novecojuši un būs nepieciešams apstiprināt tos atkāroti. settings.require_signed_commits=Pieprasīt parakstītas revīzijas settings.require_signed_commits_desc=Noraidīt iesūtītās izmaiņas šim atzaram, ja tās nav parakstītas vai nav iespējams pārbaudīt. +settings.protect_branch_name_pattern=Aizsargātā zara šablons settings.protect_protected_file_patterns=Aizsargāto failu šablons (vairākus var norādīt atdalot ar semikolu '\;'): settings.protect_protected_file_patterns_desc=Aizsargātie faili, ko nevar mainīt, pat ja lietotājam ir tiesības veidot jaunus, labot vai dzēst failus šajā atzarā. Vairākus šablons ir iespējams norādīt atdalot tos ar semikolu ('\;'). Sīkāka informācija par šabloniem pieejama github.com/gobwas/glob dokumentācijā. Piemēram, .drone.yml, /docs/**/*.txt. settings.protect_unprotected_file_patterns=Neaizsargāto failu šablons (vairākus var norādīt atdalot ar semikolu '\;'): @@ -2067,6 +2233,7 @@ settings.add_protected_branch=Iespējot aizsargāšanu settings.delete_protected_branch=Atspējot aizsargāšanu settings.update_protect_branch_success=Atzara aizsardzība atzaram '%s' tika saglabāta. settings.remove_protected_branch_success=Atzara aizsardzība atzaram '%s' tika atspējota. +settings.remove_protected_branch_failed=Neizdevās izdzēst atzara '%s' aizsardzību. settings.protected_branch_deletion=Atspējot atzara aizsardzību settings.protected_branch_deletion_desc=Atspējojot atzara aizsardzību, ļaus lietotājiem ar rakstīšanas tiesībām nosūtīt izmaiņas uz atzaru. Vai turpināt? settings.block_rejected_reviews=Neļaut sapludināt izmaiņu pieprasījumus, kam ir pieprasītas izmaiņas @@ -2076,10 +2243,13 @@ settings.block_on_official_review_requests_desc=Sapludināšana nebūs iespējam settings.block_outdated_branch=Bloķēt sapludināšanau, ja izmaiņu pieprasījums ir novecojis settings.block_outdated_branch_desc=Sapludināšana nebūs pieejama, ja atzars būs atpalicis no bāzes atzara. settings.default_branch_desc=Norādiet noklusēto repozitorija atzaru izmaiņu pieprasījumiem un koda revīzijām: +settings.merge_style_desc=Sapludināšanas veidi settings.default_merge_style_desc=Noklusētais sapludināšanas veids izmaiņu pieprasījumiem: settings.choose_branch=Izvēlieties atzaru… settings.no_protected_branch=Nav neviena aizsargātā atzara. settings.edit_protected_branch=Labot +settings.protected_branch_required_rule_name=Nav norādīts noteikuma nosaukums +settings.protected_branch_duplicate_rule_name=Dublējošs noteikuma nosaukumu settings.protected_branch_required_approvals_min=Pieprasīto recenziju skaits nevar būt negatīvs. settings.tags=Tagi settings.tags.protection=Tagu aizsargāšana @@ -2212,6 +2382,8 @@ release.edit_subheader=Laidieni palīdz organizēt projekta versijas. release.tag_name=Taga nosaukums release.target=Mērķis release.tag_helper=Izvēlieties jau esošu tagu vai izveidojiet jaunu. +release.tag_helper_new=Jauns tags. Šis tags tiks izveidots no mērķa. +release.tag_helper_existing=Esošs tags. release.title=Virsraksts release.content=Saturs release.prerelease_desc=Atzīmēt kā pirmslaidiena versiju @@ -2235,6 +2407,8 @@ release.downloads=Lejupielādes release.download_count=Lejupielādes: %s release.add_tag_msg=Izmantot laidiena nosaukumu un saturu kā taga aprakstu. release.add_tag=Izveidot tikai tagu +release.releases_for=Repozitorja %s laidieni +release.tags_for=Repozitorija %s tagi branch.name=Atzara nosaukums branch.search=Meklēt atzarus @@ -2502,6 +2676,10 @@ dashboard.delete_old_actions=Dzēst visas darbības no datu bāzes dashboard.delete_old_actions.started=Uzsākta visu novecojušo darbību dzēšana no datu bāzes. dashboard.update_checker=Atjauninājumu pārbaudītājs dashboard.delete_old_system_notices=Dzēst vecos sistēmas paziņojumus no datubāzes +dashboard.gc_lfs=Veikt atkritumu uzkopšanas darbus LFS meta objektiem +dashboard.stop_zombie_tasks=Apturēt zombija uzdevumus +dashboard.stop_endless_tasks=Apturēt nepārtrauktus uzdevumus +dashboard.cancel_abandoned_jobs=Atcelt pamestus darbus users.user_manage_panel=Lietotāju kontu pārvaldība users.new_account=Izveidot lietotāja kontu @@ -2590,6 +2768,7 @@ repos.size=Izmērs packages.package_manage_panel=Pakotņu pārvaldība packages.total_size=Kopējais izmērs: %s +packages.unreferenced_size=Izmērs bez atsauces: %s packages.owner=Īpašnieks packages.creator=Izveidotājs packages.name=Nosaukums @@ -2683,6 +2862,8 @@ auths.oauth2_required_claim_value_helper=Uzstādiet šo vērtību, lai ierobežo auths.oauth2_group_claim_name=Prasības nosaukums, kas nodrošina grupu nosaukumus šim avotam. (Neobligāts) auths.oauth2_admin_group=Grupas prasības vērtība administratoriem. (Neobligāta - nepieciešams prasības nosaukums augstāk) auths.oauth2_restricted_group=Grupas prasības vērtība ierobežotajiem lietotājiem. (Neobligāta - nepieciešams prasības nosaukums augstāk) +auths.oauth2_map_group_to_team=Sasaistīt prasības grupas ar organizācijas komandām. (Neobligāts - nepieciešams prasības nosaukums augstāk) +auths.oauth2_map_group_to_team_removal=Noņemt lietotājus no sinhronizētajām komandām, ja lietotājs nav piesaistīts attiecīgajai grupai. auths.enable_auto_register=Iespējot automātisko reģistrāciju auths.sspi_auto_create_users=Automātiski izveidot lietotājus auths.sspi_auto_create_users_helper=Ļauj SSPI autentifikācijas metodei automātiski izveidot jaunus kontus lietotājiem, kas autorizējas pirmo reizi @@ -2723,6 +2904,8 @@ auths.still_in_used=Šo autentificēšanās avotu joprojām izmanto viens vai va auths.deletion_success=Autentifikācijas avots tika atjaunots. auths.login_source_exist=Autentifikācijas avots ar nosaukumu '%s' jau eksistē. auths.login_source_of_type_exist=Autentifikācijas avots ar šādu veidu jau eksistē. +auths.unable_to_initialize_openid=Nevarēja inicializēt OpenID Connect sliedzēju: %s +auths.invalid_openIdConnectAutoDiscoveryURL=Kļūdains automātiskās atklāšanas URL (jābūt korektam URL, kas sākas ar http:// vai https://) config.server_config=Servera konfigurācija config.app_name=Vietnes nosaukums @@ -2945,6 +3128,7 @@ monitor.queue.pool.cancel_desc=Atstājot rindu bez nevienas strādņu grupas, va notices.system_notice_list=Sistēmas paziņojumi notices.view_detail_header=Skatīt paziņojuma detaļas +notices.operations=Darbības notices.select_all=Iezīmēt visu notices.deselect_all=Atcelt visa iezīmēšanu notices.inverse_selection=Apgriezeniskā iezīmēšana @@ -3069,6 +3253,8 @@ keywords=Atslēgvārdi details=Papildu informācija details.author=Autors details.project_site=Projekta lapa +details.repository_site=Repozitorija lapa +details.documentation_site=Dokumentācijas lapa details.license=Licence assets=Resursi versions=Versijas @@ -3076,7 +3262,14 @@ versions.on=publicēta versions.view_all=Parādīt visas dependency.id=ID dependency.version=Versija +cargo.registry=Uzstādiet šo reģistru Cargo konfigurācijas failā, piemēram, ~/.cargo/config.toml: +cargo.install=Lai instalētu Cargo pakotni, izpildiet sekojošu komandu: +cargo.documentation=Papildus informācija par Cargo reģistru pieejama dokumentācijā. +cargo.details.repository_site=Repozitorija lapa +cargo.details.documentation_site=Dokumentācijas lapa +chef.registry=Uzstādiet šo reģistru failā ~/.chef/config.rb: chef.install=Lai instalētu pakotni, nepieciešams izpildīt sekojošu komandu: +chef.documentation=Papildus informācija par Chef reģistru pieejama dokumentācijā. composer.registry=Pievienojiet šo reģistru savā ~/.composer/config.json failā: composer.install=Lai instalētu Composer pakotni, izpildiet sekojošu komandu: composer.documentation=Papildus informācija par Composer reģistru pieejama dokumentācijā. @@ -3086,6 +3279,11 @@ conan.details.repository=Repozitorijs conan.registry=Konfigurējiet šo reģistru no komandrindas: conan.install=Lai instalētu Conan pakotni, izpildiet sekojošu komandu: conan.documentation=Papildus informācija par Conan reģistru pieejama dokumentācijā. +conda.registry=Uzstādiet šo reģistru kā Conda repozitoriju failā .condarc: +conda.install=Lai instalētu Conda pakotni, izpildiet sekojošu komandu: +conda.documentation=Papildus informācija par Conda reģistru pieejama dokumentācijā. +conda.details.repository_site=Repozitorija lapa +conda.details.documentation_site=Dokumentācijas lapa container.details.type=Attēla formāts container.details.platform=Platforma container.pull=Atgādājiet šo attēlu no komandrindas: @@ -3144,26 +3342,110 @@ settings.delete.description=Pakotne tiks neatgriezeniski izdzēsta. settings.delete.notice=Tiks dzēsts %s (%s). Šī darbība ir neatgriezeniska. Vai vēlaties turpināt? settings.delete.success=Pakotne tika izdzēsta. settings.delete.error=Neizdevās izdzēst pakotni. +owner.settings.cargo.title=Cargo reģistra inkdess +owner.settings.cargo.initialize=Inicializēt indeksu +owner.settings.cargo.initialize.description=Lai izmantotu Cargo reģistru, nepieciešams speciāls indeksa git repozitorijs. Šeit to var izveidot ar nepieciešamo konfigurāciju. +owner.settings.cargo.initialize.error=Neizdevās inicializēt Cargo indeksu: %v +owner.settings.cargo.initialize.success=Cargo indekss tika veiksmīgi inicializēts. +owner.settings.cargo.rebuild=Pārbūvēt indeksu +owner.settings.cargo.rebuild.description=Ja indekss neatbilst tajā saglabātajām Cargo pakotnē, to var pārbūvēt šeit. +owner.settings.cargo.rebuild.error=Neizdevās pārbūvēt Cargo indeksu: %v +owner.settings.cargo.rebuild.success=Cargo indekss tika veiksmīgi pārbūvēts. +owner.settings.cleanuprules.title=Pārvaldīt notīrīšanas noteikumus +owner.settings.cleanuprules.add=Pievienot notīrīšanas noteikumu +owner.settings.cleanuprules.edit=Labot notīrīšanas noteikumu +owner.settings.cleanuprules.none=Nav pieejams neviens notīrīšanas noteikums. Sīkāka informācija ir pieejama dokumentācijā. +owner.settings.cleanuprules.preview=Notīrīšānas noteikuma priekšskatījums +owner.settings.cleanuprules.preview.overview=Ir ieplānota %d paku dzēšana. +owner.settings.cleanuprules.preview.none=Notīrīšanas noteikumam neatbilst neviena pakotne. owner.settings.cleanuprules.enabled=Iespējots +owner.settings.cleanuprules.pattern_full_match=Piešķirt šablonu visam pakotnes nosaukumam +owner.settings.cleanuprules.keep.title=Versijas, kas atbilst šiem noteikumiem tiks saglabātas, pat ja tās atbilst noņemšanas noteikumiem zemāk. +owner.settings.cleanuprules.keep.count=Saglabāt jaunāko versiju +owner.settings.cleanuprules.keep.count.1=1 versija katrai pakotnei +owner.settings.cleanuprules.keep.count.n=%d versijas katrai pakotnei +owner.settings.cleanuprules.keep.pattern=Paturēt versijas, kas atbilst +owner.settings.cleanuprules.keep.pattern.container=Versija latest vienmēr tiks paturēta konteineru pakotnēm. +owner.settings.cleanuprules.remove.title=Versijas, kas atbilst šiem noteikumiem tiks noņemtas, ja vien neatbilst arī noteikumiem augstāk, lai tās paturētu. +owner.settings.cleanuprules.remove.days=Noņemt versijas vecākas kā +owner.settings.cleanuprules.remove.pattern=Noņemt versijas, kas atbilst +owner.settings.cleanuprules.success.update=Notīrīšanas noteikumi tika atjaunoti. +owner.settings.cleanuprules.success.delete=Notīrīšanas noteikumi tika izdzēsti. +owner.settings.chef.title=Chef reģistrs +owner.settings.chef.keypair=Ģenerēt atslēgu pāri +owner.settings.chef.keypair.description=Ģenerēt atslēgu pāri, ko izmantot, lai autentificētos pret Chef reģistru. Iepriekšējo atslēgu vairs nevarēs izmantot. [secrets] +secrets=Noslēpumi +description=Noslēpumi tiks padoti atsevišķām darbībām un citādi nevar tikt nolasīti. +none=Pagaidām nav neviena noslēpuma. value=Vērtība name=Nosaukums +creation=Pievienot noslēpumu +creation.name_placeholder=reģistr-nejūtīgs, tikai burti, cipari un apakšsvītras, nevar sākties ar GITEA_ vai GITHUB_ +creation.value_placeholder=Ievadiet jebkādu saturu. Atstarpes sākumā un beigā tiks noņemtas. +creation.success=Noslēpums '%s' tika pievienots. +creation.failed=Neizdevās pievienot noslēpumu. +deletion=Dzēst noslēpumu +deletion.description=Noslēpuma dzēšana ir neatgriezeniska. Vai turpināt? +deletion.success=Noslēpums tika izdzēsts. +deletion.failed=Neizdevās dzēst noslēpumu. [actions] - - - +actions=Darbības + +unit.desc=Pārvaldīt darbības + +status.unknown=Nezināms +status.waiting=Gaida +status.running=Izpildās +status.success=Pabeigts +status.failure=Neveiksmīgs +status.cancelled=Atcelts +status.skipped=Izlaists +status.blocked=Bloķēts + +runners=Izpildītāji +runners.runner_manage_panel=Izpildītāju pārvaldība +runners.new=Pievienot jaunu izpildītāju +runners.new_notice=Kā uzstādīt izpildītāju +runners.status=Statuss runners.id=ID runners.name=Nosaukums runners.owner_type=Veids runners.description=Apraksts runners.labels=Etiķetes +runners.last_online=Pēdējo reizi tiešsaistē +runners.agent_labels=Aģenta etiķetes +runners.custom_labels=Pielāgotas etiķetes +runners.custom_labels_helper=Pielāgotas etiķetes, ko administrators pievienojis manuāli. Ar komatu atdalītas etiķetes, atstapres pirms un pēc etiķetes nosaukuma tiek ignorētas. +runners.runner_title=Izpildītājs +runners.task_list=Pēdējās darbības, kas izpildītas runners.task_list.run=Palaist +runners.task_list.status=Statuss runners.task_list.repository=Repozitorijs runners.task_list.commit=Revīzija +runners.task_list.done_at=Beigu laiks +runners.edit_runner=Labot izpildītāju +runners.update_runner=Atjaunot izpildītāju +runners.update_runner_success=Izpildītājs veiksmīgi atjaunots +runners.update_runner_failed=Neizdevās atjaunot izpildītāju +runners.delete_runner=Dzēst izpildītāju +runners.delete_runner_success=Izpildītājs veiksmīgi izdzēsts +runners.delete_runner_failed=Neizdevās izdzēst izpildītāju +runners.delete_runner_header=Apstiprināt izpildītāja izdzēšanu +runners.delete_runner_notice=Ja šis izpildītājs veic kādus uzdevumus, tad tie tiks apturēti un atzīmēti kā neizdevušies. Tas var sabojāt būvēšanas darbaplūsmas. +runners.none=Nav pieejami izpildītāji +runners.status.unspecified=Nezināms +runners.status.idle=Dīkstāvē runners.status.active=Aktīvs +runners.status.offline=Bezsaistē +runs.all_workflows=Visas darbaplūsmas +runs.open_tab=%d atvērti +runs.closed_tab=%d aizvērti runs.commit=Revīzija +runs.pushed_by=Iesūtītājs +need_approval_desc=Nepieciešams apstiprinājums, lai izpildītu izmaiņu pieprasījumu darbaplūsmas no atdalītiem repozitorijiem. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 0feda076afb53..450df1c3ab481 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -789,6 +789,7 @@ remove_account_link=Gekoppeld account verwijderen remove_account_link_desc=Als je een gekoppeld account verwijdert, verliest dit account toegang tot je Gitea-account. Doorgaan? remove_account_link_success=Het gekoppelde account is verwijderd. + orgs_none=U bent geen lid van een organisatie. repos_none=U bezit geen repositories diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 4f5811d53f6b7..e43e7a554009b 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -744,6 +744,7 @@ remove_account_link=Usuń powiązane konto remove_account_link_desc=Usunięcie powiązanego konta unieważni jego dostęp do Twojego konta Gitea. Kontynuować? remove_account_link_success=Powiązane konto zostało odłączone. + orgs_none=Nie jesteś członkiem żadnej organizacji. repos_none=Nie posiadasz żadnych repozytoriów diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 8909235d012f2..55529df8829af 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -819,6 +819,7 @@ remove_account_link=Remover conta vinculada remove_account_link_desc=A exclusão da chave SSH revogará o acesso à sua conta. Continuar? remove_account_link_success=A conta vinculada foi removida. + orgs_none=Você não é membro de nenhuma organização. repos_none=Você não possui nenhum repositório diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index fb2a191b4a06a..ef7a400ec8b8c 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -821,6 +821,8 @@ remove_account_link=Remover conta vinculada remove_account_link_desc=A remoção de uma conta vinculada revogará o acesso dessa conta à sua conta do Gitea. Quer continuar? remove_account_link_success=A conta vinculada foi removida. +hooks.desc=Adicionar automatismos web que serão despoletados para todos os repositórios pertencentes a este utilizador. + orgs_none=Não é membro de nenhuma organização. repos_none=Não tem nenhum repositório @@ -2288,6 +2290,8 @@ release.edit_subheader=Lançamentos organizam as versões do trabalho. release.tag_name=Nome da etiqueta release.target=Alvo release.tag_helper=Escolha uma etiqueta existente ou crie uma nova. +release.tag_helper_new=Nova etiqueta. Esta etiqueta será criada a partir do destino. +release.tag_helper_existing=Etiqueta existente. release.title=Título release.content=Conteúdo release.prerelease_desc=Marcar como pré-lançamento @@ -2808,6 +2812,8 @@ auths.still_in_used=A fonte de autenticação ainda está em uso. Tem que primei auths.deletion_success=A fonte de autenticação foi eliminada. auths.login_source_exist=A fonte de autenticação '%s' já existe. auths.login_source_of_type_exist=Já existe uma fonte de autenticação deste tipo. +auths.unable_to_initialize_openid=Não é possível inicializar o Fornecedor de Ligação OpenID: %s +auths.invalid_openIdConnectAutoDiscoveryURL=URL de descoberta automática inválido (tem que ser um URL válido começando com http:// ou https://) config.server_config=Configuração do servidor config.app_name=Título do sítio diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 7a400451f3081..9516a2963ad28 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -782,6 +782,7 @@ remove_account_link=Удалить привязанный аккаунт remove_account_link_desc=Удаление привязанной учетной записи отменит её доступ к вашей учетной записи Gitea. Продолжить? remove_account_link_success=Привязанная учетная запись удалена. + orgs_none=Вы не состоите ни в одной организации. repos_none=Вы не владеете репозиториями diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 23ab03b008970..1f87009005ad8 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -715,6 +715,7 @@ remove_account_link=සම්බන්ධිත ගිණුම ඉවත් ක remove_account_link_desc=සම්බන්ධිත ගිණුමක් ඉවත් කිරීම ඔබගේ Gitea ගිණුමට එහි ප්රවේශය අවලංගු කරනු ඇත. දිගටම? remove_account_link_success=සම්බන්ධිත ගිණුම ඉවත් කර ඇත. + orgs_none=ඔබ කිසිදු සංවිධානයක සාමාජිකයෙකු නොවේ. repos_none=ඔබට කිසිදු ගබඩාවක් නොමැත diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index f7612d4c503fd..87942926ea55f 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -785,6 +785,7 @@ manage_account_links=Spravovať prepojené kontá manage_account_links_desc=Tieto externé účty sú prepojené s vaším účtom Gitea. link_account=Pripojiť účet + orgs_none=Nieste členom žiadnej organizácie. repos_none=Nevlastníte žiadne repozitáre diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 6afe9e71181fe..c79f99d512edd 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -619,6 +619,7 @@ remove_account_link=Ta Bort Länkat Konto remove_account_link_desc=Borttagning av länkade konton kommer häva dess åtkomst till ditt Gitea-konto. Vill du fortsätta? remove_account_link_success=Det länkade konton har tagits bort. + orgs_none=Du är inte en medlem i någon organisation. repos_none=Du har inga utvecklingskataloger associerade med ditt konto diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index c0e5bb220847b..131dfa69af192 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -334,7 +334,7 @@ non_local_account=Yerel olmayan kullanıcılar parolalarını Gitea web arayüz verify=Doğrula scratch_code=Çizgi kodu use_scratch_code=Bir çizgi kodu kullanınız -twofa_scratch_used=Çizgi kodunuzu kullandınız. İki aşamalı ayarlar sayfasına yönlendirildiniz, burada cihaz kaydınızı kaldırabilir veya yeni bir çizgi kodu oluşturabilirsiniz. +twofa_scratch_used=Geçici kodunuzu kullandınız. İki aşamalı ayarlar sayfasına yönlendirildiniz, burada aygıt kaydınızı kaldırabilir veya yeni bir geçici kod oluşturabilirsiniz. twofa_passcode_incorrect=Şifreniz yanlış. Aygıtınızı yanlış yerleştirdiyseniz, oturum açmak için çizgi kodunuzu kullanın. twofa_scratch_token_incorrect=Çizgi kodunuz doğru değildir. login_userpass=Oturum Aç @@ -736,12 +736,12 @@ unbind=Bağlantıyı Kaldır unbind_success=Sosyal hesabın bağlantısı Gitea hesabınızdan kaldırılmıştır. manage_access_token=Erişim Jetonlarını Yönet -generate_new_token=Yeni Jeton Üret +generate_new_token=Yeni Erişim Anahtarı Üret tokens_desc=Bu jetonlar Gitea API'sini kullanarak hesabınıza erişim sağlar. new_token_desc=Jeton kullanan uygulamalar hesabınıza tam erişime sahiptir. token_name=Jeton İsmi -generate_token=Jeton Üret -generate_token_success=Yeni bir jeton oluşturuldu. Tekrar gösterilmeyeceği için şimdi kopyalayın. +generate_token=Erişim Anahtarı Üret +generate_token_success=Yeni bir erişim anahtarı oluşturuldu. Tekrar gösterilmeyeceği için şimdi kopyalayın. generate_token_name_duplicate=%s zaten bir uygulama adı olarak kullanılmış. Lütfen yeni bir tane kullanın. delete_token=Sil access_token_deletion=Erişim Jetonunu Sil @@ -784,12 +784,12 @@ twofa_desc=İki faktörlü kimlik doğrulama, hesabınızın güvenliğini artı twofa_is_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmiş. twofa_not_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmemiş. twofa_disable=İki Aşamalı Doğrulamayı Devre Dışı Bırak -twofa_scratch_token_regenerate=Kazıma Belirtecini Yenile -twofa_scratch_token_regenerated=Kazıma belirteciniz şimdi %s. Güvenli bir yerde saklayın. +twofa_scratch_token_regenerate=Geçici Kodu Yeniden Üret +twofa_scratch_token_regenerated=Geçici kodunuz artık %s. Güvenilir bir yerde saklayın. twofa_enroll=İki Faktörlü Kimlik Doğrulamaya Kaydolun twofa_disable_note=Gerekirse iki faktörlü kimlik doğrulamayı devre dışı bırakabilirsiniz. twofa_disable_desc=İki faktörlü kimlik doğrulamayı devre dışı bırakmak hesabınızı daha az güvenli hale getirir. Devam edilsin mi? -regenerate_scratch_token_desc=Karalama belirtecinizi yanlış yerleştirdiyseniz veya oturum açmak için kullandıysanız, buradan sıfırlayabilirsiniz. +regenerate_scratch_token_desc=Geçici kodunuzu kaybettiyseniz veya oturum açmak için kullandıysanız, buradan sıfırlayabilirsiniz. twofa_disabled=İki faktörlü kimlik doğrulama devre dışı bırakıldı. scan_this_image=Kim doğrulama uygulamanızla bu görüntüyü tarayın: or_enter_secret=Veya gizli şeyi girin: %s @@ -812,6 +812,7 @@ remove_account_link=Bağlantılı Hesabı Kaldır remove_account_link_desc=Bağlantılı bir hesabı kaldırmak, onunla Gitea hesabınıza erişimi iptal edecektir. Devam edilsin mi? remove_account_link_success=Bağlantılı hesap kaldırıldı. + orgs_none=Herhangi bir organizasyonun bir üyesi değilsiniz. repos_none=Herhangi bir depoya sahip değilsiniz diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 293803c10e478..3f39f0bafa5fa 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -739,6 +739,7 @@ remove_account_link=Видалити облікові записи remove_account_link_desc=Видалення пов'язаного облікового запису відкликає його доступ до вашого облікового запису Gitea. Продовжити? remove_account_link_success=Зв'язаний обліковий запис видалено. + orgs_none=Ви не є учасником будь-якої організації. repos_none=У вас немає власних репозиторіїв diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 36afb5d442f80..aa4cc4dde9744 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -821,6 +821,7 @@ remove_account_link=删除已绑定的账号 remove_account_link_desc=删除已绑定帐户将吊销其对您的 Gitea 帐户的访问权限。继续? remove_account_link_success=已取消绑定帐户。 + orgs_none=您现在还不是任何组织的成员。 repos_none=你并不拥有任何仓库 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 288188e911412..6fa062fc65e61 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -279,6 +279,7 @@ or_enter_secret=或者輸入密碼: %s link_account=連結帳戶 + orgs_none=您尚未成為任一組織的成員。 repos_none=您不擁有任何存儲庫 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index edf7376e7cc45..e98e55b77700d 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -54,9 +54,10 @@ mirror=鏡像 new_repo=新增儲存庫 new_migrate=遷移外部儲存庫 new_mirror=新鏡像 -new_fork=新增儲存庫 fork +new_fork=新增儲存庫 Fork new_org=新增組織 new_project=新增專案 +new_project_column=新增欄位 manage_org=管理組織 admin_panel=網站管理 account_settings=帳戶設定 @@ -90,9 +91,11 @@ disabled=已停用 copy=複製 copy_url=複製 URL +copy_content=複製內容 copy_branch=複製分支名稱 copy_success=複製成功! copy_error=複製失敗 +copy_type_unsupported=無法複製此類型的檔案 write=撰寫 preview=預覽 @@ -109,8 +112,14 @@ never=從來沒有 rss_feed=RSS 摘要 [aria] +navbar=導航列 +footer=頁尾 +footer.software=關於軟體 +footer.links=連結 [filter] +string.asc=A - Z +string.desc=Z - A [error] occurred=發生錯誤 @@ -238,7 +247,10 @@ default_enable_timetracking_popup=預設情況下啟用新存儲庫的時間跟 no_reply_address=隱藏電子信箱域名 no_reply_address_helper=作為隱藏電子信箱使用者的域名。例如,如果隱藏的電子信箱域名設定為「noreply.example.org」,帳號「joe」將以「joe@noreply.example.org」的身分登錄到 Git 中。 password_algorithm=密碼雜湊演算法 +invalid_password_algorithm=無效的密碼雜湊演算法 password_algorithm_helper=設定密碼雜湊演算法。演算法有不同的需求和強度。「argon2」雖然有優秀的特性但會占用大量記憶體,所以可能不適用於小型系統。 +enable_update_checker=啟用更新檢查器 +enable_update_checker_helper=定期連線到 gitea.io 檢查更新。 [home] uname_holder=帳號或電子信箱 @@ -285,6 +297,8 @@ org_no_results=沒有找到符合的組織。 code_no_results=找不到符合您關鍵字的原始碼。 code_search_results=「%s」的搜尋結果 code_last_indexed_at=最後索引 %s +relevant_repositories_tooltip=已隱藏缺少主題、圖示、說明、Fork 的儲存庫。 +relevant_repositories=只顯示相關的儲存庫,顯示未篩選的結果。 [auth] @@ -314,6 +328,7 @@ email_not_associate=此電子信箱未與任何帳戶連結 send_reset_mail=發送帳戶救援信 reset_password=帳戶救援 invalid_code=您的確認代碼無效或已過期。 +invalid_password=您的密碼和用來建立帳戶的不符。 reset_password_helper=帳戶救援 reset_password_wrong_user=您已經使用 %s 的帳戶登入,但帳戶救援連結是給 %s 的 password_too_short=密碼長度不能少於 %d 個字! @@ -357,6 +372,7 @@ password_pwned_err=無法完成對 HaveIBeenPwned 的請求。 [mail] view_it_on=在 %s 上查看 +reply=或是直接回覆此電子郵件 link_not_working_do_paste=無法開啟?請複製超連結到瀏覽器貼上。 hi_user_x=%s 您好, @@ -415,6 +431,10 @@ repo.transfer.body=請造訪 %s 以接受或拒絕轉移,您也可以忽略它 repo.collaborator.added.subject=%s 把您加入到 %s repo.collaborator.added.text=您已被新增為儲存庫的協作者: +team_invite.subject=%[1]s 邀請您加入組織 %[2]s +team_invite.text_1=%[1]s 邀請您加入組織 %[3]s 中的 %[2]s 團隊 +team_invite.text_2=請點擊下方連結加入團隊: +team_invite.text_3=備註: 這是寄給 %[1]s 的邀請。若您未預期收到此邀請,請忽略此郵件。 [modal] yes=是 @@ -431,7 +451,7 @@ SSHTitle=SSH 金鑰名稱 HttpsUrl=HTTPS URL 地址 PayloadUrl=推送地址 TeamName=團隊名稱 -AuthName=認證名稱 +AuthName=授權名稱 AdminEmail=管理員電子信箱 NewBranchName=新的分支名稱 @@ -456,6 +476,8 @@ url_error=`'%s' 是無效的 URL。` include_error=` 必須包含子字串「%s」。 glob_pattern_error=` glob 比對模式無效:%s.` regex_pattern_error=` 正規表示式模式無效:%s.` +username_error=`只能包含英文字母數字 ('0-9'、'a-z'、'A-Z')、破折號 ('-')、底線 ('_')、句點 ('.'),不能以非英文字母數字開頭或結尾,也不允許連續的非英文字母數字。` +invalid_group_team_map_error=` 對應無效: %s` unknown_error=未知錯誤: captcha_incorrect=驗證碼不正確。 password_not_match=密碼錯誤。 @@ -491,18 +513,21 @@ user_not_exist=該用戶名不存在 team_not_exist=團隊不存在 last_org_owner=你不能從「Owners」團隊中刪除最後一個使用者。每個組織中至少要有一個擁有者。 cannot_add_org_to_team=組織不能被新增為團隊成員。 +duplicate_invite_to_team=該使用者已經被邀請為團隊成員。 +organization_leave_success=您已成功離開組織 %s。 invalid_ssh_key=無法驗證您的 SSH 密鑰:%s invalid_gpg_key=無法驗證您的 GPG 密鑰:%s invalid_ssh_principal=無效的主體: %s +must_use_public_key=您提供的金鑰是私有金鑰,請勿上傳您的私有金鑰到任何地方,請使用您的公鑰。 unable_verify_ssh_key=無法驗證 SSH 密鑰 auth_failed=授權認證失敗:%v -still_own_repo=此帳戶仍然擁有一個或多個儲存庫,您必須先刪除或轉移它們。 -still_has_org=此帳戶仍是一個或多個組織的成員,您必須先離開它們。 -still_own_packages=您的帳戶擁有一個或多個套件,請先刪除他們。 -org_still_own_repo=該組織仍然是某些儲存庫的擁有者,您必須先轉移或刪除它們才能執行刪除組織! -org_still_own_packages=此組織擁有一個或多個套件,請先刪除他們。 +still_own_repo=您的帳戶擁有一個以上的儲存庫,請先刪除或轉移它們。 +still_has_org=您的帳戶是一個或多個組織的成員,請先離開它們。 +still_own_packages=您的帳戶擁有一個以上的套件,請先刪除它們。 +org_still_own_repo=此組織仍然擁有一個以上的儲存庫,請先刪除或轉移它們。 +org_still_own_packages=此組織仍然擁有一個以上的套件,請先刪除它們。 target_branch_not_exist=目標分支不存在 @@ -731,6 +756,8 @@ access_token_deletion_cancel_action=取消 access_token_deletion_confirm_action=刪除 access_token_deletion_desc=刪除 Token 後,使用此 Token 的應用程式將無法再存取您的帳戶,此動作不可還原。是否繼續? delete_token_success=已刪除 Token。使用此 Token 的應用程式無法再存取您的帳戶。 +select_scopes=選擇範圍 (Scope) +scopes_list=範圍 (Scope): manage_oauth2_applications=管理 OAuth2 應用程式 edit_oauth2_application=編輯 OAuth2 應用程式 @@ -743,6 +770,7 @@ create_oauth2_application_button=建立應用程式 create_oauth2_application_success=您已成功新增一個 OAuth2 應用程式。 update_oauth2_application_success=您已成功更新了 OAuth2 應用程式。 oauth2_application_name=應用程式名稱 +oauth2_confidential_client=機密客戶端 (Confidential Client)。請為能保持機密性的程式勾選,例如網頁應用程式。使用原生程式時不要勾選,包含桌面、行動應用程式。 oauth2_redirect_uri=重新導向 URI save_application=儲存 oauth2_client_id=客戶端 ID @@ -793,6 +821,7 @@ remove_account_link=刪除已連結的帳戶 remove_account_link_desc=刪除連結帳戶將撤銷其對 Gitea 帳戶的存取權限。是否繼續? remove_account_link_success=已移除連結的帳戶。 + orgs_none=您尚未成為任一組織的成員。 repos_none=您不擁有任何存儲庫 @@ -1002,10 +1031,12 @@ unstar=移除星號 star=加上星號 fork=Fork download_archive=下載此儲存庫 +more_operations=更多操作 no_desc=暫無描述 quick_guide=快速幫助 clone_this_repo=Clone 此儲存庫 +cite_this_repo=引用此儲存庫 create_new_repo_command=從命令列建立新儲存庫。 push_exist_repo=從命令列推送已存在的儲存庫 empty_message=此儲存庫未包含任何內容。 @@ -1104,6 +1135,7 @@ editor.commit_directly_to_this_branch=直接提交到 %[3]s pulls.merged_by=由 %[3]s 建立,合併於 %[1]s pulls.merged_by_fake=由 %[2]s 建立,合併於 %[1]s @@ -1350,11 +1398,14 @@ issues.sign_in_require_desc= 登入 才能加入這對話。 issues.edit=編輯 issues.cancel=取消 issues.save=儲存 -issues.label_title=標籤名稱 -issues.label_description=標籤描述 -issues.label_color=標籤顏色 +issues.label_title=名稱 +issues.label_description=描述 +issues.label_color=顏色 +issues.label_exclusive=互斥 +issues.label_exclusive_desc=請以此格式命名標籤: scope/item,使它和其他 scope/ (相同範圍) 標籤互斥。 +issues.label_exclusive_warning=在編輯問題及合併請求的標籤時,將會刪除任何有相同範圍的標籤。 issues.label_count=%d 個標籤 -issues.label_open_issues=%d 個開放中的問題 +issues.label_open_issues=%d 個開放中的問題/合併請求 issues.label_edit=編輯 issues.label_delete=刪除 issues.label_modify=編輯標籤 @@ -1607,6 +1658,8 @@ pulls.reopened_at=`重新開放了這個合併請求 命令列指南。` pulls.merge_instruction_step1_desc=在您的儲存庫中切換到新分支並測試變更。 pulls.merge_instruction_step2_desc=合併變更並更新到 Gitea。 +pulls.clear_merge_message=清除合併訊息 +pulls.clear_merge_message_hint=清除合併訊息將僅移除提交訊息內容,留下產生的 git 結尾,如「Co-Authored-By …」。 pulls.auto_merge_button_when_succeed=(當通過檢查後) pulls.auto_merge_when_succeed=通過所有檢查後自動合併 @@ -1798,6 +1851,7 @@ settings.mirror_sync_in_progress=鏡像同步正在進行中。 請稍後再回 settings.site=網站 settings.update_settings=更新設定 settings.branches.update_default_branch=更新預設分支 +settings.branches.add_new_rule=加入新規則 settings.advanced_settings=進階設定 settings.wiki_desc=啟用儲存庫 Wiki settings.use_internal_wiki=使用內建 Wiki @@ -1827,8 +1881,11 @@ settings.pulls.ignore_whitespace=衝突時忽略空白 settings.pulls.enable_autodetect_manual_merge=啟用自動偵測手動合併 (注意: 在某些特殊情況下可能發生誤判) settings.pulls.allow_rebase_update=啟用透過 Rebase 更新合併請求分支 settings.pulls.default_delete_branch_after_merge=預設在合併後刪除合併請求分支 +settings.pulls.default_allow_edits_from_maintainers=預設允許維護者進行編輯 +settings.releases_desc=啟用儲存庫版本發佈 settings.packages_desc=啟用儲存庫套件註冊中心 settings.projects_desc=啟用儲存庫專案 +settings.actions_desc=啟用儲存庫 Actions settings.admin_settings=管理員設定 settings.admin_enable_health_check=啟用儲存庫的健康檢查 (git fsck) settings.admin_code_indexer=程式碼索引器 @@ -2038,6 +2095,8 @@ settings.deploy_key_deletion_desc=移除部署金鑰將拒絕它存取此儲存 settings.deploy_key_deletion_success=部署金鑰已移除。 settings.branches=分支 settings.protected_branch=分支保護 +settings.protected_branch.save_rule=儲存規則 +settings.protected_branch.delete_rule=刪除規則 settings.protected_branch_can_push=允許推送? settings.protected_branch_can_push_yes=你可以推送 settings.protected_branch_can_push_no=你不能推送 @@ -2072,6 +2131,7 @@ settings.dismiss_stale_approvals=捨棄過時的核可 settings.dismiss_stale_approvals_desc=當新的提交有修改到合併請求的內容,並被推送到此分支時,將捨棄舊的核可。 settings.require_signed_commits=僅接受經簽署的提交 settings.require_signed_commits_desc=拒絕未經簽署或未經驗證的提交推送到此分支。 +settings.protect_branch_name_pattern=受保護的分支名稱模式 settings.protect_protected_file_patterns=受保護的檔案模式 (以分號區隔「\;」): settings.protect_protected_file_patterns_desc=即便使用者有權限新增、修改、刪除此分支的檔案,仍不允許直接修改受保護的檔案。可以用半形分號「\;」分隔多個模式。請於github.com/gobwas/glob 文件查看模式格式。範例:.drone.yml, /docs/**/*.txt。 settings.protect_unprotected_file_patterns=未受保護的檔案模式 (以分號區隔「\;」): @@ -2080,6 +2140,7 @@ settings.add_protected_branch=啟用保護 settings.delete_protected_branch=停用保護 settings.update_protect_branch_success=已更新「%s」的分支保護。 settings.remove_protected_branch_success=已停用「%s」的分支保護。 +settings.remove_protected_branch_failed=刪除分支保護規則「%s」失敗。 settings.protected_branch_deletion=停用分支保護 settings.protected_branch_deletion_desc=停用分支保護將允許有寫入權限的使用者推送至該分支,是否繼續? settings.block_rejected_reviews=有退回的審核時阻擋合併 @@ -2089,9 +2150,13 @@ settings.block_on_official_review_requests_desc=如果有官方的審核請求 settings.block_outdated_branch=如果合併請求已經過時則阻擋合併 settings.block_outdated_branch_desc=當 head 分支落後於基礎分支時不得合併。 settings.default_branch_desc=請選擇用來提交程式碼和合併請求的預設分支。 +settings.merge_style_desc=合併方式 +settings.default_merge_style_desc=預設合併方式 settings.choose_branch=選擇一個分支... settings.no_protected_branch=沒有受保護的分支。 settings.edit_protected_branch=編輯 +settings.protected_branch_required_rule_name=必須填寫規則名稱 +settings.protected_branch_duplicate_rule_name=規則名稱已存在 settings.protected_branch_required_approvals_min=需要的核可數量不能為負數。 settings.tags=標籤 settings.tags.protection=標籤保護 @@ -2223,7 +2288,7 @@ release.new_subheader=發布、整理專案的版本。 release.edit_subheader=發布、整理專案的版本。 release.tag_name=標籤名稱 release.target=目標分支 -release.tag_helper=新增或選擇既有的標籤。 +release.tag_helper=新增或選擇現有的標籤。 release.title=標題 release.content=內容 release.prerelease_desc=標記為 Pre-Release @@ -2247,6 +2312,8 @@ release.downloads=下載附件 release.download_count=下載次數:%s release.add_tag_msg=使用此版本的標題和內容作為標籤訊息。 release.add_tag=只建立標籤 +release.releases_for=%s 的版本發佈 +release.tags_for=%s 的標籤 branch.name=分支名稱 branch.search=搜尋分支 @@ -2305,7 +2372,7 @@ org_full_name_holder=組織全名 org_name_helper=組織名稱應該要簡短且方便記憶 create_org=建立組織 repo_updated=更新於 -members=成員數 +members=成員 teams=團隊 code=程式碼 lower_members=名成員 @@ -2376,7 +2443,7 @@ teams.leave.detail=確定要離開 %s 嗎? teams.can_create_org_repo=建立儲存庫 teams.can_create_org_repo_helper=成員可以在組織中新增儲存庫。建立者將自動取得新儲存庫的管理員權限。 teams.none_access=沒有權限 -teams.none_access_helper=成員無法檢視此單元或對其執行其他動作。 +teams.none_access_helper=成員無法檢視此單元或對其執行其他動作,這對公開儲存庫沒有影響。 teams.general_access=一般權限 teams.general_access_helper=成員權限將由下列權限表決定。 teams.read_access=讀取 @@ -2514,6 +2581,10 @@ dashboard.delete_old_actions=從資料庫刪除所有舊行為 dashboard.delete_old_actions.started=從資料庫刪除所有舊行為的任務已啟動。 dashboard.update_checker=更新檢查器 dashboard.delete_old_system_notices=從資料庫刪除所有舊系統提示 +dashboard.gc_lfs=對 LFS meta objects 進行垃圾回收 +dashboard.stop_zombie_tasks=停止殭屍任務 +dashboard.stop_endless_tasks=停止永不停止的任務 +dashboard.cancel_abandoned_jobs=取消已放棄的工作 users.user_manage_panel=使用者帳戶管理 users.new_account=建立使用者帳戶 @@ -2553,7 +2624,7 @@ users.still_own_repo=這個使用者還擁有一個或更多的儲存庫。請 users.still_has_org=此使用者是組織的成員。請先將他從組織中移除。 users.purge=清除使用者 users.purge_help=強制刪除使用者和他擁有的所有儲存庫、組織、套件,所有留言也會被刪除。 -users.still_own_packages=此使用者擁有一個或多個套件,請先刪除這些套件。 +users.still_own_packages=此使用者仍然擁有一個以上的套件,請先刪除這些套件。 users.deletion_success=使用者帳戶已被刪除。 users.reset_2fa=重設兩步驟驗證 users.list_status_filter.menu_text=篩選 @@ -2583,7 +2654,7 @@ emails.change_email_header=更新電子信箱屬性 emails.change_email_text=您確定要更新這個電子信箱? orgs.org_manage_panel=組織管理 -orgs.name=組織名稱 +orgs.name=名稱 orgs.teams=團隊數 orgs.members=成員數 orgs.new_orga=新增組織 @@ -2592,7 +2663,7 @@ repos.repo_manage_panel=儲存庫管理 repos.unadopted=未接管的儲存庫 repos.unadopted.no_more=找不到其他未接管的儲存庫 repos.owner=擁有者 -repos.name=儲存庫名稱 +repos.name=名稱 repos.private=私有 repos.watches=關注數 repos.stars=星號數 @@ -2602,6 +2673,7 @@ repos.size=大小 packages.package_manage_panel=套件管理 packages.total_size=總大小: %s +packages.unreferenced_size=未參考大小: %s packages.owner=擁有者 packages.creator=建立者 packages.name=名稱 @@ -2623,8 +2695,8 @@ systemhooks.update_webhook=更新系統 Webhook auths.auth_manage_panel=認證來源管理 auths.new=新增認證來源 -auths.name=認證名稱 -auths.type=認證類型 +auths.name=名稱 +auths.type=類型 auths.enabled=已啟用 auths.syncenabled=啟用使用者同步 auths.updated=最後更新時間 @@ -2695,6 +2767,8 @@ auths.oauth2_required_claim_value_helper=填寫此名稱以限制 Claim 中有 auths.oauth2_group_claim_name=Claim 名稱提供群組名稱給此來源。(選用) auths.oauth2_admin_group=管理員使用者的群組 Claim 值。(選用 - 需要上面的 Claim 名稱) auths.oauth2_restricted_group=受限制使用者的群組 Claim 值。(選用 - 需要上面的 Claim 名稱) +auths.oauth2_map_group_to_team=將已 Claim 的群組對應到組織團隊。(選用 - 需要上述 Claim 名稱) +auths.oauth2_map_group_to_team_removal=如果使用者不屬於相對應的群組,將使用者從已同步的團隊移除。 auths.enable_auto_register=允許授權用戶自動註冊 auths.sspi_auto_create_users=自動建立使用者 auths.sspi_auto_create_users_helper=允許 SSPI 認證方法於使用者首次登入時自動建立新帳戶 @@ -2775,7 +2849,7 @@ config.lfs_http_auth_expiry=LFS HTTP 驗證有效時間 config.db_config=資料庫組態 config.db_type=資料庫類型 config.db_host=主機地址 -config.db_name=資料庫名稱 +config.db_name=名稱 config.db_user=使用者名稱 config.db_schema=結構描述 config.db_ssl_mode=SSL @@ -2878,7 +2952,7 @@ config.get_setting_failed=讀取設定值 %s 失敗 config.set_setting_failed=寫入設定值 %s 失敗 monitor.cron=Cron 任務 -monitor.name=任務名稱 +monitor.name=名稱 monitor.schedule=任務安排 monitor.next=下次執行時間 monitor.previous=上次執行時間 @@ -2957,12 +3031,13 @@ monitor.queue.pool.cancel_desc=讓佇列沒有任何工作者群組可能造成 notices.system_notice_list=系統提示 notices.view_detail_header=查看提示細節 +notices.operations=操作 notices.select_all=選取全部 notices.deselect_all=取消所有選取 notices.inverse_selection=反向選取 notices.delete_selected=刪除選取項 notices.delete_all=刪除所有提示 -notices.type=提示類型 +notices.type=類型 notices.type_1=儲存庫 notices.type_2=任務 notices.desc=描述 @@ -3081,6 +3156,8 @@ keywords=關鍵字 details=詳情 details.author=作者 details.project_site=專案網站 +details.repository_site=儲存庫網站 +details.documentation_site=文件網站 details.license=授權條款 assets=檔案 versions=版本 @@ -3088,7 +3165,14 @@ versions.on=於 versions.view_all=檢視全部 dependency.id=ID dependency.version=版本 +cargo.registry=在 Cargo 組態檔設定此註冊中心 (例如: ~/.cargo/config.toml): +cargo.install=執行下列命令以使用 Cargo 安裝此套件: +cargo.documentation=關於 Cargo registry 的詳情請參閱說明文件。 +cargo.details.repository_site=儲存庫網站 +cargo.details.documentation_site=文件網站 +chef.registry=在您的 ~/.chef/config.rb 檔設定此註冊中心: chef.install=執行下列命令安裝此套件: +chef.documentation=關於 Chef registry 的詳情請參閱說明文件。 composer.registry=在您的 ~/.composer/config.json 檔設定此註冊中心: composer.install=執行下列命令以使用 Composer 安裝此套件: composer.documentation=關於 Composer registry 的詳情請參閱說明文件。 @@ -3098,6 +3182,11 @@ conan.details.repository=儲存庫 conan.registry=透過下列命令設定此註冊中心: conan.install=執行下列命令以使用 Conan 安裝此套件: conan.documentation=關於 Conan registry 的詳情請參閱說明文件。 +conda.registry=在您的 .condarc 檔設定此註冊中心為 Conda 存儲庫: +conda.install=執行下列命令以使用 Conda 安裝此套件: +conda.documentation=關於 Conda registry 的詳情請參閱說明文件。 +conda.details.repository_site=儲存庫網站 +conda.details.documentation_site=文件網站 container.details.type=映像檔類型 container.details.platform=平台 container.pull=透過下列命令拉取映像檔: @@ -3113,7 +3202,7 @@ generic.documentation=關於通用 registry 的詳情請參閱說明文件。 -maven.registry=在您的 pom.xml 檔設定此註冊中心: +maven.registry=在您專案的 pom.xml 檔設定此註冊中心: maven.install=若要使用此套件,請在您 pom.xml 檔的 dependencies 段落加入下列內容: maven.install2=透過下列命令執行: maven.download=透過下列命令下載相依性: @@ -3122,7 +3211,7 @@ nuget.registry=透過下列命令設定此註冊中心: nuget.install=執行下列命令以使用 NuGet 安裝此套件: nuget.documentation=關於 NuGet registry 的詳情請參閱說明文件。 nuget.dependency.framework=目標框架 -npm.registry=在您的 .npmrc 檔設定此註冊中心: +npm.registry=在您專案的 .npmrc 檔設定此註冊中心: npm.install=執行下列命令以使用 npm 安裝此套件: npm.install2=或將它加到 package.json 檔: npm.documentation=關於 npm registry 的詳情請參閱說明文件。 @@ -3156,26 +3245,110 @@ settings.delete.description=刪除套件是永久且不可還原的。 settings.delete.notice=您正要刪除 %s (%s),此動作是無法還原的,您確定嗎? settings.delete.success=已刪除該套件。 settings.delete.error=刪除套件失敗。 +owner.settings.cargo.title=Cargo Registry 索引 +owner.settings.cargo.initialize=初始化索引 +owner.settings.cargo.initialize.description=使用 Cargo Registry 時需要一個特別的 Git 儲存庫作為索引。您可以在此使用必要的設定建立和重建它。 +owner.settings.cargo.initialize.error=初始化 Cargo 索引失敗: %v +owner.settings.cargo.initialize.success=成功建立了 Cargo 索引。 +owner.settings.cargo.rebuild=重建索引 +owner.settings.cargo.rebuild.description=如果索引和 Cargo 套件不同步,您可在此重建它。 +owner.settings.cargo.rebuild.error=重建 Cargo 索引失敗: %v +owner.settings.cargo.rebuild.success=成功重建了 Cargo 索引。 +owner.settings.cleanuprules.title=管理清理規則 +owner.settings.cleanuprules.add=加入清理規則 +owner.settings.cleanuprules.edit=編輯清理規則 +owner.settings.cleanuprules.none=沒有可用的清理規則。閱讀文件以了解更多。 +owner.settings.cleanuprules.preview=清理規則預覽 +owner.settings.cleanuprules.preview.overview=已排定要移除 %d 個套件。 +owner.settings.cleanuprules.preview.none=清理規則不符合任何套件。 owner.settings.cleanuprules.enabled=已啟用 +owner.settings.cleanuprules.pattern_full_match=將比對規則套用到完整的套件名稱 +owner.settings.cleanuprules.keep.title=符合這些規則的版本即使符合下面的刪除規則也會被保留。 +owner.settings.cleanuprules.keep.count=保留最新的 +owner.settings.cleanuprules.keep.count.1=每個套件 1 個版本 +owner.settings.cleanuprules.keep.count.n=每個套件 %d 個版本 +owner.settings.cleanuprules.keep.pattern=保留版本的比對規則 +owner.settings.cleanuprules.keep.pattern.container=Container 套件的最新版本總是會保留。 +owner.settings.cleanuprules.remove.title=符合這些規則的版本將被移除,除非前述的規則要求保留它們。 +owner.settings.cleanuprules.remove.days=移除早於天數的版本 +owner.settings.cleanuprules.remove.pattern=移除版本的比對規則 +owner.settings.cleanuprules.success.update=已更新清理規則。 +owner.settings.cleanuprules.success.delete=已刪除清理規則。 +owner.settings.chef.title=Chef Registry +owner.settings.chef.keypair=產生密鑰組 +owner.settings.chef.keypair.description=產生用來認證 Chef Registry 的密鑰組,之前的密鑰未來將無法使用。 [secrets] +secrets=Secret +description=Secret 會被傳給特定的 Action,其他情況無法讀取。 +none=還沒有 Secret。 value=值 -name=組織名稱 +name=名稱 +creation=加入 Secret +creation.name_placeholder=不區分大小寫,只能包含英文字母、數字、底線 ('_'),不能以 GITEA_ 或 GITHUB_ 開頭。 +creation.value_placeholder=輸入任何內容,頭尾的空白都會被忽略。 +creation.success=已加入 Secret「%s」。 +creation.failed=加入 Secret 失敗。 +deletion=移除 Secret +deletion.description=移除 Secret 是永久的且不可還原,是否繼續? +deletion.success=已移除此 Secret。 +deletion.failed=移除 Secret 失敗。 [actions] - - - +actions=Actions + +unit.desc=管理 Actions + +status.unknown=未知 +status.waiting=正在等候 +status.running=正在執行 +status.success=成功 +status.failure=失敗 +status.cancelled=已取消 +status.skipped=已略過 +status.blocked=已阻塞 + +runners=Runner +runners.runner_manage_panel=Runner 管理 +runners.new=建立 Runner +runners.new_notice=如何啟動 Runner +runners.status=狀態 runners.id=ID -runners.name=組織名稱 -runners.owner_type=認證類型 +runners.name=名稱 +runners.owner_type=類型 runners.description=組織描述 runners.labels=標籤 +runners.last_online=最後上線時間 +runners.agent_labels=代理程式標籤 +runners.custom_labels=自訂標籤 +runners.custom_labels_helper=自訂標籤是由管理員手動加上的標籤。標籤之間以半形逗號「,」分隔,標籤頭尾的空白將被忽略。 +runners.runner_title=Runner +runners.task_list=最近在此 Runner 上的任務 runners.task_list.run=執行 +runners.task_list.status=狀態 runners.task_list.repository=儲存庫 runners.task_list.commit=提交 +runners.task_list.done_at=完成於 +runners.edit_runner=編輯 Runner +runners.update_runner=更新變更 +runners.update_runner_success=更新 Runner 成功 +runners.update_runner_failed=更新 Runner 失敗 +runners.delete_runner=刪除此 Runner +runners.delete_runner_success=刪除 Runner 成功 +runners.delete_runner_failed=刪除 Runner 失敗 +runners.delete_runner_header=確認刪除此 Runner +runners.delete_runner_notice=如果有任務正在此 Runner 上執行,它可能會被中止並標記為失敗,這可能會打斷建置工作流程。 +runners.none=沒有可用的 Runner +runners.status.unspecified=未知 +runners.status.idle=閒置 runners.status.active=啟用 +runners.status.offline=離線 +runs.all_workflows=所有工作流程 +runs.open_tab=%d 開放中 +runs.closed_tab=%d 已關閉 runs.commit=提交 +runs.pushed_by=推送者 +need_approval_desc=來自 Frok 儲存庫的合併請求需要核可才能執行工作流程。 diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go index 2aed4139f3cb6..8264503c9d2f9 100644 --- a/routers/api/v1/admin/hooks.go +++ b/routers/api/v1/admin/hooks.go @@ -105,10 +105,7 @@ func CreateHook(ctx *context.APIContext) { // "$ref": "#/responses/Hook" form := web.GetForm(ctx).(*api.CreateHookOption) - // TODO in body params - if !utils.CheckCreateHookOption(ctx, form) { - return - } + utils.AddSystemHook(ctx, form) } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 1d2f8b18e0190..735939a5517c9 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -835,6 +835,13 @@ func Routes(ctx gocontext.Context) *web.Route { m.Get("/stopwatches", reqToken(auth_model.AccessTokenScopeRepo), repo.GetStopwatches) m.Get("/subscriptions", reqToken(auth_model.AccessTokenScopeRepo), user.GetMyWatchedRepos) m.Get("/teams", reqToken(auth_model.AccessTokenScopeRepo), org.ListUserTeams) + m.Group("/hooks", func() { + m.Combo("").Get(user.ListHooks). + Post(bind(api.CreateHookOption{}), user.CreateHook) + m.Combo("/{id}").Get(user.GetHook). + Patch(bind(api.EditHookOption{}), user.EditHook). + Delete(user.DeleteHook) + }, reqToken(auth_model.AccessTokenScopeAdminUserHook), reqWebhooksEnabled()) }, reqToken("")) // Repositories diff --git a/routers/api/v1/org/hook.go b/routers/api/v1/org/hook.go index 4e435c9599a16..a6ea618a7d896 100644 --- a/routers/api/v1/org/hook.go +++ b/routers/api/v1/org/hook.go @@ -6,7 +6,6 @@ package org import ( "net/http" - webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/context" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" @@ -39,34 +38,10 @@ func ListHooks(ctx *context.APIContext) { // "200": // "$ref": "#/responses/HookList" - opts := &webhook_model.ListWebhookOptions{ - ListOptions: utils.GetListOptions(ctx), - OrgID: ctx.Org.Organization.ID, - } - - count, err := webhook_model.CountWebhooksByOpts(opts) - if err != nil { - ctx.InternalServerError(err) - return - } - - orgHooks, err := webhook_model.ListWebhooksByOpts(ctx, opts) - if err != nil { - ctx.InternalServerError(err) - return - } - - hooks := make([]*api.Hook, len(orgHooks)) - for i, hook := range orgHooks { - hooks[i], err = webhook_service.ToHook(ctx.Org.Organization.AsUser().HomeLink(), hook) - if err != nil { - ctx.InternalServerError(err) - return - } - } - - ctx.SetTotalCountHeader(count) - ctx.JSON(http.StatusOK, hooks) + utils.ListOwnerHooks( + ctx, + ctx.ContextUser, + ) } // GetHook get an organization's hook by id @@ -92,14 +67,12 @@ func GetHook(ctx *context.APIContext) { // "200": // "$ref": "#/responses/Hook" - org := ctx.Org.Organization - hookID := ctx.ParamsInt64(":id") - hook, err := utils.GetOrgHook(ctx, org.ID, hookID) + hook, err := utils.GetOwnerHook(ctx, ctx.ContextUser.ID, ctx.ParamsInt64("id")) if err != nil { return } - apiHook, err := webhook_service.ToHook(org.AsUser().HomeLink(), hook) + apiHook, err := webhook_service.ToHook(ctx.ContextUser.HomeLink(), hook) if err != nil { ctx.InternalServerError(err) return @@ -131,15 +104,14 @@ func CreateHook(ctx *context.APIContext) { // "201": // "$ref": "#/responses/Hook" - form := web.GetForm(ctx).(*api.CreateHookOption) - // TODO in body params - if !utils.CheckCreateHookOption(ctx, form) { - return - } - utils.AddOrgHook(ctx, form) + utils.AddOwnerHook( + ctx, + ctx.ContextUser, + web.GetForm(ctx).(*api.CreateHookOption), + ) } -// EditHook modify a hook of a repository +// EditHook modify a hook of an organization func EditHook(ctx *context.APIContext) { // swagger:operation PATCH /orgs/{org}/hooks/{id} organization orgEditHook // --- @@ -168,11 +140,12 @@ func EditHook(ctx *context.APIContext) { // "200": // "$ref": "#/responses/Hook" - form := web.GetForm(ctx).(*api.EditHookOption) - - // TODO in body params - hookID := ctx.ParamsInt64(":id") - utils.EditOrgHook(ctx, form, hookID) + utils.EditOwnerHook( + ctx, + ctx.ContextUser, + web.GetForm(ctx).(*api.EditHookOption), + ctx.ParamsInt64("id"), + ) } // DeleteHook delete a hook of an organization @@ -198,15 +171,9 @@ func DeleteHook(ctx *context.APIContext) { // "204": // "$ref": "#/responses/empty" - org := ctx.Org.Organization - hookID := ctx.ParamsInt64(":id") - if err := webhook_model.DeleteWebhookByOrgID(org.ID, hookID); err != nil { - if webhook_model.IsErrWebhookNotExist(err) { - ctx.NotFound() - } else { - ctx.Error(http.StatusInternalServerError, "DeleteWebhookByOrgID", err) - } - return - } - ctx.Status(http.StatusNoContent) + utils.DeleteOwnerHook( + ctx, + ctx.ContextUser, + ctx.ParamsInt64("id"), + ) } diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index fd54d1f740ef7..39d83912b016e 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -223,12 +223,8 @@ func CreateHook(ctx *context.APIContext) { // responses: // "201": // "$ref": "#/responses/Hook" - form := web.GetForm(ctx).(*api.CreateHookOption) - if !utils.CheckCreateHookOption(ctx, form) { - return - } - utils.AddRepoHook(ctx, form) + utils.AddRepoHook(ctx, web.GetForm(ctx).(*api.CreateHookOption)) } // EditHook modify a hook of a repository diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go index 8cbd2e11b699c..92e11386882cc 100644 --- a/routers/api/v1/repo/issue_attachment.go +++ b/routers/api/v1/repo/issue_attachment.go @@ -176,7 +176,7 @@ func CreateIssueAttachment(ctx *context.APIContext) { filename = query } - attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, &repo_model.Attachment{ + attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{ Name: filename, UploaderID: ctx.Doer.ID, RepoID: ctx.Repo.Repository.ID, diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go index 4c8452380f033..6fe4dbc977155 100644 --- a/routers/api/v1/repo/issue_comment_attachment.go +++ b/routers/api/v1/repo/issue_comment_attachment.go @@ -180,7 +180,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { filename = query } - attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, &repo_model.Attachment{ + attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{ Name: filename, UploaderID: ctx.Doer.ID, RepoID: ctx.Repo.Repository.ID, diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go index 2d1f3291f8b9c..74969f2cad7c8 100644 --- a/routers/api/v1/repo/notes.go +++ b/routers/api/v1/repo/notes.go @@ -58,8 +58,18 @@ func getNote(ctx *context.APIContext, identifier string) { return } + commitSHA, err := ctx.Repo.GitRepo.ConvertToSHA1(identifier) + if err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound(err) + } else { + ctx.Error(http.StatusInternalServerError, "ConvertToSHA1", err) + } + return + } + var note git.Note - if err := git.GetNote(ctx, ctx.Repo.GitRepo, identifier, ¬e); err != nil { + if err := git.GetNote(ctx, ctx.Repo.GitRepo, commitSHA.String(), ¬e); err != nil { if git.IsErrNotExist(err) { ctx.NotFound(identifier) return diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index 597578aac578b..305b2808df543 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -194,7 +194,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { } // Create a new attachment and save the file - attach, err := attachment.UploadAttachment(file, setting.Repository.Release.AllowedTypes, &repo_model.Attachment{ + attach, err := attachment.UploadAttachment(file, setting.Repository.Release.AllowedTypes, header.Size, &repo_model.Attachment{ Name: filename, UploaderID: ctx.Doer.ID, RepoID: release.RepoID, diff --git a/routers/api/v1/user/hook.go b/routers/api/v1/user/hook.go new file mode 100644 index 0000000000000..50be519c815fa --- /dev/null +++ b/routers/api/v1/user/hook.go @@ -0,0 +1,154 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" + webhook_service "code.gitea.io/gitea/services/webhook" +) + +// ListHooks list the authenticated user's webhooks +func ListHooks(ctx *context.APIContext) { + // swagger:operation GET /user/hooks user userListHooks + // --- + // summary: List the authenticated user's webhooks + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/HookList" + + utils.ListOwnerHooks( + ctx, + ctx.Doer, + ) +} + +// GetHook get the authenticated user's hook by id +func GetHook(ctx *context.APIContext) { + // swagger:operation GET /user/hooks/{id} user userGetHook + // --- + // summary: Get a hook + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the hook to get + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/Hook" + + hook, err := utils.GetOwnerHook(ctx, ctx.Doer.ID, ctx.ParamsInt64("id")) + if err != nil { + return + } + + apiHook, err := webhook_service.ToHook(ctx.Doer.HomeLink(), hook) + if err != nil { + ctx.InternalServerError(err) + return + } + ctx.JSON(http.StatusOK, apiHook) +} + +// CreateHook create a hook for the authenticated user +func CreateHook(ctx *context.APIContext) { + // swagger:operation POST /user/hooks user userCreateHook + // --- + // summary: Create a hook + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/CreateHookOption" + // responses: + // "201": + // "$ref": "#/responses/Hook" + + utils.AddOwnerHook( + ctx, + ctx.Doer, + web.GetForm(ctx).(*api.CreateHookOption), + ) +} + +// EditHook modify a hook of the authenticated user +func EditHook(ctx *context.APIContext) { + // swagger:operation PATCH /user/hooks/{id} user userEditHook + // --- + // summary: Update a hook + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the hook to update + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/EditHookOption" + // responses: + // "200": + // "$ref": "#/responses/Hook" + + utils.EditOwnerHook( + ctx, + ctx.Doer, + web.GetForm(ctx).(*api.EditHookOption), + ctx.ParamsInt64("id"), + ) +} + +// DeleteHook delete a hook of the authenticated user +func DeleteHook(ctx *context.APIContext) { + // swagger:operation DELETE /user/hooks/{id} user userDeleteHook + // --- + // summary: Delete a hook + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the hook to delete + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + + utils.DeleteOwnerHook( + ctx, + ctx.Doer, + ctx.ParamsInt64("id"), + ) +} diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index f6aaf74aff123..44625cc9b81b8 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" @@ -18,15 +19,46 @@ import ( webhook_service "code.gitea.io/gitea/services/webhook" ) -// GetOrgHook get an organization's webhook. If there is an error, write to -// `ctx` accordingly and return the error -func GetOrgHook(ctx *context.APIContext, orgID, hookID int64) (*webhook.Webhook, error) { - w, err := webhook.GetWebhookByOrgID(orgID, hookID) +// ListOwnerHooks lists the webhooks of the provided owner +func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) { + opts := &webhook.ListWebhookOptions{ + ListOptions: GetListOptions(ctx), + OwnerID: owner.ID, + } + + count, err := webhook.CountWebhooksByOpts(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + hooks, err := webhook.ListWebhooksByOpts(ctx, opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiHooks := make([]*api.Hook, len(hooks)) + for i, hook := range hooks { + apiHooks[i], err = webhook_service.ToHook(owner.HomeLink(), hook) + if err != nil { + ctx.InternalServerError(err) + return + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiHooks) +} + +// GetOwnerHook gets an user or organization webhook. Errors are written to ctx. +func GetOwnerHook(ctx *context.APIContext, ownerID, hookID int64) (*webhook.Webhook, error) { + w, err := webhook.GetWebhookByOwnerID(ownerID, hookID) if err != nil { if webhook.IsErrWebhookNotExist(err) { ctx.NotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetWebhookByOrgID", err) + ctx.Error(http.StatusInternalServerError, "GetWebhookByOwnerID", err) } return nil, err } @@ -48,9 +80,9 @@ func GetRepoHook(ctx *context.APIContext, repoID, hookID int64) (*webhook.Webhoo return w, nil } -// CheckCreateHookOption check if a CreateHookOption form is valid. If invalid, +// checkCreateHookOption check if a CreateHookOption form is valid. If invalid, // write the appropriate error to `ctx`. Return whether the form is valid -func CheckCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) bool { +func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) bool { if !webhook_service.IsValidHookTaskType(form.Type) { ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Invalid hook type: %s", form.Type)) return false @@ -81,14 +113,13 @@ func AddSystemHook(ctx *context.APIContext, form *api.CreateHookOption) { } } -// AddOrgHook add a hook to an organization. Writes to `ctx` accordingly -func AddOrgHook(ctx *context.APIContext, form *api.CreateHookOption) { - org := ctx.Org.Organization - hook, ok := addHook(ctx, form, org.ID, 0) +// AddOwnerHook adds a hook to an user or organization +func AddOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.CreateHookOption) { + hook, ok := addHook(ctx, form, owner.ID, 0) if !ok { return } - apiHook, ok := toAPIHook(ctx, org.AsUser().HomeLink(), hook) + apiHook, ok := toAPIHook(ctx, owner.HomeLink(), hook) if !ok { return } @@ -128,14 +159,18 @@ func pullHook(events []string, event string) bool { return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true) } -// addHook add the hook specified by `form`, `orgID` and `repoID`. If there is +// addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is // an error, write to `ctx` accordingly. Return (webhook, ok) -func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID int64) (*webhook.Webhook, bool) { +func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) { + if !checkCreateHookOption(ctx, form) { + return nil, false + } + if len(form.Events) == 0 { form.Events = []string{"push"} } w := &webhook.Webhook{ - OrgID: orgID, + OwnerID: ownerID, RepoID: repoID, URL: form.Config["url"], ContentType: webhook.ToHookContentType(form.Config["content_type"]), @@ -234,21 +269,20 @@ func EditSystemHook(ctx *context.APIContext, form *api.EditHookOption, hookID in ctx.JSON(http.StatusOK, h) } -// EditOrgHook edit webhook `w` according to `form`. Writes to `ctx` accordingly -func EditOrgHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) { - org := ctx.Org.Organization - hook, err := GetOrgHook(ctx, org.ID, hookID) +// EditOwnerHook updates a webhook of an user or organization +func EditOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.EditHookOption, hookID int64) { + hook, err := GetOwnerHook(ctx, owner.ID, hookID) if err != nil { return } if !editHook(ctx, form, hook) { return } - updated, err := GetOrgHook(ctx, org.ID, hookID) + updated, err := GetOwnerHook(ctx, owner.ID, hookID) if err != nil { return } - apiHook, ok := toAPIHook(ctx, org.AsUser().HomeLink(), updated) + apiHook, ok := toAPIHook(ctx, owner.HomeLink(), updated) if !ok { return } @@ -362,3 +396,16 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh } return true } + +// DeleteOwnerHook deletes the hook owned by the owner. +func DeleteOwnerHook(ctx *context.APIContext, owner *user_model.User, hookID int64) { + if err := webhook.DeleteWebhookByOwnerID(owner.ID, hookID); err != nil { + if webhook.IsErrWebhookNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "DeleteWebhookByOwnerID", err) + } + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/init.go b/routers/init.go index d3f822dc8345a..8cf53fc108de0 100644 --- a/routers/init.go +++ b/routers/init.go @@ -141,7 +141,7 @@ func GlobalInitInstalled(ctx context.Context) { if setting.EnableSQLite3 { log.Info("SQLite3 support is enabled") - } else if setting.Database.UseSQLite3 { + } else if setting.Database.Type.IsSQLite3() { log.Fatal("SQLite3 support is disabled, but it is used for database setting. Please get or build a Gitea release with SQLite3 support.") } diff --git a/routers/install/install.go b/routers/install/install.go index a377c2950ba31..8e2d19c73271e 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -104,7 +104,7 @@ func Install(ctx *context.Context) { form.DbSchema = setting.Database.Schema form.Charset = setting.Database.Charset - curDBType := setting.Database.Type + curDBType := setting.Database.Type.String() var isCurDBTypeSupported bool for _, dbType := range setting.SupportedDatabaseTypes { if dbType == curDBType { @@ -272,7 +272,7 @@ func SubmitInstall(ctx *context.Context) { // ---- Basic checks are passed, now test configuration. // Test database setting. - setting.Database.Type = form.DbType + setting.Database.Type = setting.DatabaseType(form.DbType) setting.Database.Host = form.DbHost setting.Database.User = form.DbUser setting.Database.Passwd = form.DbPasswd @@ -392,7 +392,7 @@ func SubmitInstall(ctx *context.Context) { log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err) } } - cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type) + cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String()) cfg.Section("database").Key("HOST").SetValue(setting.Database.Host) cfg.Section("database").Key("NAME").SetValue(setting.Database.Name) cfg.Section("database").Key("USER").SetValue(setting.Database.User) diff --git a/routers/install/routes.go b/routers/install/routes.go index a8efc92fe17ca..82d9c34b41f18 100644 --- a/routers/install/routes.go +++ b/routers/install/routes.go @@ -64,7 +64,7 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler { "SignedUserName": "", } - httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform") + httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform") w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) if !setting.IsProd { diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 8ce45720fec37..d2953f753d7a4 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -271,6 +271,15 @@ func NewAuthSourcePost(ctx *context.Context) { } case auth.OAuth2: config = parseOAuth2Config(form) + oauth2Config := config.(*oauth2.Source) + if oauth2Config.Provider == "openidConnect" { + discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL) + if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") { + ctx.Data["Err_DiscoveryURL"] = true + ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form) + return + } + } case auth.SSPI: var err error config, err = parseSSPIConfig(ctx, form) @@ -305,6 +314,10 @@ func NewAuthSourcePost(ctx *context.Context) { if auth.IsErrSourceAlreadyExist(err) { ctx.Data["Err_Name"] = true ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form) + } else if oauth2.IsErrOpenIDConnectInitialize(err) { + ctx.Data["Err_DiscoveryURL"] = true + unwrapped := err.(oauth2.ErrOpenIDConnectInitialize).Unwrap() + ctx.RenderWithErr(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form) } else { ctx.ServerError("auth.CreateSource", err) } @@ -389,6 +402,15 @@ func EditAuthSourcePost(ctx *context.Context) { } case auth.OAuth2: config = parseOAuth2Config(form) + oauth2Config := config.(*oauth2.Source) + if oauth2Config.Provider == "openidConnect" { + discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL) + if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") { + ctx.Data["Err_DiscoveryURL"] = true + ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form) + return + } + } case auth.SSPI: config, err = parseSSPIConfig(ctx, form) if err != nil { @@ -408,6 +430,7 @@ func EditAuthSourcePost(ctx *context.Context) { if err := auth.UpdateSource(source); err != nil { if oauth2.IsErrOpenIDConnectInitialize(err) { ctx.Flash.Error(err.Error(), true) + ctx.Data["Err_DiscoveryURL"] = true ctx.HTML(http.StatusOK, tplAuthEdit) } else { ctx.ServerError("UpdateSource", err) diff --git a/routers/web/base.go b/routers/web/base.go index b0d8a7c3f1e6d..2eb0b6f39118a 100644 --- a/routers/web/base.go +++ b/routers/web/base.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/routing" "code.gitea.io/gitea/services/auth" @@ -44,7 +45,7 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor routing.UpdateFuncInfo(req.Context(), funcInfo) rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") - rPath = path.Clean("/" + strings.ReplaceAll(rPath, "\\", "/"))[1:] + rPath = util.CleanPath(strings.ReplaceAll(rPath, "\\", "/")) u, err := objStore.URL(rPath, path.Base(rPath)) if err != nil { @@ -80,7 +81,7 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor routing.UpdateFuncInfo(req.Context(), funcInfo) rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") - rPath = path.Clean("/" + strings.ReplaceAll(rPath, "\\", "/"))[1:] + rPath = util.CleanPath(strings.ReplaceAll(rPath, "\\", "/")) if rPath == "" { http.Error(w, "file not found", http.StatusNotFound) return @@ -158,7 +159,7 @@ func Recovery(ctx goctx.Context) func(next http.Handler) http.Handler { store["SignedUserName"] = "" } - httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform") + httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform") w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) if !setting.IsProd { diff --git a/routers/web/healthcheck/check.go b/routers/web/healthcheck/check.go index 1142a0aec5d55..e11dd2aca2e80 100644 --- a/routers/web/healthcheck/check.go +++ b/routers/web/healthcheck/check.go @@ -100,7 +100,7 @@ func checkDatabase(checks checks) status { st.Time = getCheckTime() } - if setting.Database.UseSQLite3 && st.Status == pass { + if setting.Database.Type.IsSQLite3() && st.Status == pass { if !setting.EnableSQLite3 { st.Status = fail st.Time = getCheckTime() diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 4cc364acd3a01..8c9cc8a9d86be 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -156,6 +156,7 @@ func Home(ctx *context.Context) { pager.SetDefaultParams(ctx) pager.AddParam(ctx, "language", "Language") ctx.Data["Page"] = pager + ctx.Data["ContextUser"] = ctx.ContextUser ctx.HTML(http.StatusOK, tplOrgHome) } diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 6449d12de105a..c9d63fec5df0c 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -103,7 +103,7 @@ func Projects(ctx *context.Context) { pager.AddParam(ctx, "state", "State") ctx.Data["Page"] = pager - ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) + ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["PageIsViewProjects"] = true ctx.Data["SortType"] = sortType @@ -111,7 +111,7 @@ func Projects(ctx *context.Context) { ctx.HTML(http.StatusOK, tplProjects) } -func canWriteUnit(ctx *context.Context) bool { +func canWriteProjects(ctx *context.Context) bool { if ctx.ContextUser.IsOrganization() { return ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) } @@ -122,7 +122,8 @@ func canWriteUnit(ctx *context.Context) bool { func NewProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.new") ctx.Data["BoardTypes"] = project_model.GetBoardConfig() - ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) + ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) + ctx.Data["PageIsViewProjects"] = true ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() shared_user.RenderUserHeader(ctx) ctx.HTML(http.StatusOK, tplProjectsNew) @@ -135,7 +136,7 @@ func NewProjectPost(ctx *context.Context) { shared_user.RenderUserHeader(ctx) if ctx.HasError() { - ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) + ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["PageIsViewProjects"] = true ctx.Data["BoardTypes"] = project_model.GetBoardConfig() ctx.HTML(http.StatusOK, tplProjectsNew) @@ -193,7 +194,7 @@ func DeleteProject(ctx *context.Context) { } return } - if p.RepoID != ctx.Repo.Repository.ID { + if p.OwnerID != ctx.ContextUser.ID { ctx.NotFound("", nil) return } @@ -205,7 +206,7 @@ func DeleteProject(ctx *context.Context) { } ctx.JSON(http.StatusOK, map[string]interface{}{ - "redirect": ctx.Repo.RepoLink + "/projects", + "redirect": ctx.ContextUser.HomeLink() + "/-/projects", }) } @@ -214,7 +215,7 @@ func EditProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.edit") ctx.Data["PageIsEditProjects"] = true ctx.Data["PageIsViewProjects"] = true - ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) + ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) shared_user.RenderUserHeader(ctx) p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) @@ -226,13 +227,14 @@ func EditProject(ctx *context.Context) { } return } - if p.RepoID != ctx.Repo.Repository.ID { + if p.OwnerID != ctx.ContextUser.ID { ctx.NotFound("", nil) return } ctx.Data["title"] = p.Title ctx.Data["content"] = p.Description + ctx.Data["redirect"] = ctx.FormString("redirect") ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -243,7 +245,7 @@ func EditProjectPost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.edit") ctx.Data["PageIsEditProjects"] = true ctx.Data["PageIsViewProjects"] = true - ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) + ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) shared_user.RenderUserHeader(ctx) if ctx.HasError() { @@ -260,7 +262,7 @@ func EditProjectPost(ctx *context.Context) { } return } - if p.RepoID != ctx.Repo.Repository.ID { + if p.OwnerID != ctx.ContextUser.ID { ctx.NotFound("", nil) return } @@ -273,7 +275,11 @@ func EditProjectPost(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title)) - ctx.Redirect(ctx.Repo.RepoLink + "/projects") + if ctx.FormString("redirect") == "project" { + ctx.Redirect(p.Link()) + } else { + ctx.Redirect(ctx.ContextUser.HomeLink() + "/-/projects") + } } // ViewProject renders the project board for a project @@ -332,7 +338,7 @@ func ViewProject(ctx *context.Context) { project.RenderedContent = project.Description ctx.Data["LinkedPRs"] = linkedPrsMap ctx.Data["PageIsViewProjects"] = true - ctx.Data["CanWriteProjects"] = canWriteUnit(ctx) + ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Boards"] = boards diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index f713d096639bc..b57ebfbcda23a 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -218,9 +218,9 @@ func Webhooks(ctx *context.Context) { ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks" ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc") - ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OrgID: ctx.Org.Organization.ID}) + ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OwnerID: ctx.Org.Organization.ID}) if err != nil { - ctx.ServerError("GetWebhooksByOrgId", err) + ctx.ServerError("ListWebhooksByOpts", err) return } @@ -230,8 +230,8 @@ func Webhooks(ctx *context.Context) { // DeleteWebhook response for delete webhook func DeleteWebhook(ctx *context.Context) { - if err := webhook.DeleteWebhookByOrgID(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil { - ctx.Flash.Error("DeleteWebhookByOrgID: " + err.Error()) + if err := webhook.DeleteWebhookByOwnerID(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil { + ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) } diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index e5496676a9dd3..0e7a95ed073aa 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -133,6 +133,8 @@ func List(ctx *context.Context) { pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) pager.SetDefaultParams(ctx) + pager.AddParamString("workflow", workflow) + pager.AddParamString("state", ctx.FormString("state")) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplListActions) diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 589632ad6e10d..c6d8828fac603 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -44,7 +44,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) { } defer file.Close() - attach, err := attachment.UploadAttachment(file, allowedTypes, &repo_model.Attachment{ + attach, err := attachment.UploadAttachment(file, allowedTypes, header.Size, &repo_model.Attachment{ Name: header.Filename, UploaderID: ctx.Doer.ID, RepoID: repoID, diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index e5ba4ad2c1398..4f208098e4766 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -726,7 +726,7 @@ func UploadFilePost(ctx *context.Context) { func cleanUploadFileName(name string) string { // Rebase the filename - name = strings.Trim(path.Clean("/"+name), "/") + name = strings.Trim(util.CleanPath(name), "/") // Git disallows any filenames to have a .git directory in them. for _, part := range strings.Split(name, "/") { if strings.ToLower(part) == ".git" { diff --git a/routers/web/repo/lfs.go b/routers/web/repo/lfs.go index 869a69c377219..43f5527986b9b 100644 --- a/routers/web/repo/lfs.go +++ b/routers/web/repo/lfs.go @@ -207,7 +207,7 @@ func LFSLockFile(ctx *context.Context) { ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") return } - lockPath = path.Clean("/" + lockPath)[1:] + lockPath = util.CleanPath(lockPath) if len(lockPath) == 0 { ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath)) ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks") diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 967b81c608516..e15f548a38dc3 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -113,7 +113,7 @@ func Projects(ctx *context.Context) { pager.AddParam(ctx, "state", "State") ctx.Data["Page"] = pager - ctx.Data["CanWriteProjects"] = true + ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["IsShowClosed"] = isShowClosed ctx.Data["IsProjectsPage"] = true ctx.Data["SortType"] = sortType @@ -235,6 +235,7 @@ func EditProject(ctx *context.Context) { ctx.Data["title"] = p.Title ctx.Data["content"] = p.Description ctx.Data["card_type"] = p.CardType + ctx.Data["redirect"] = ctx.FormString("redirect") ctx.HTML(http.StatusOK, tplProjectsNew) } @@ -275,7 +276,11 @@ func EditProjectPost(ctx *context.Context) { } ctx.Flash.Success(ctx.Tr("repo.projects.edit_success", p.Title)) - ctx.Redirect(ctx.Repo.RepoLink + "/projects") + if ctx.FormString("redirect") == "project" { + ctx.Redirect(p.Link()) + } else { + ctx.Redirect(ctx.Repo.RepoLink + "/projects") + } } // ViewProject renders the project board for a project diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index d27d0f1bf01d8..f30588967e836 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -33,6 +33,7 @@ const ( tplHooks base.TplName = "repo/settings/webhook/base" tplHookNew base.TplName = "repo/settings/webhook/new" tplOrgHookNew base.TplName = "org/settings/hook_new" + tplUserHookNew base.TplName = "user/settings/hook_new" tplAdminHookNew base.TplName = "admin/hook_new" ) @@ -54,8 +55,8 @@ func Webhooks(ctx *context.Context) { ctx.HTML(http.StatusOK, tplHooks) } -type orgRepoCtx struct { - OrgID int64 +type ownerRepoCtx struct { + OwnerID int64 RepoID int64 IsAdmin bool IsSystemWebhook bool @@ -64,10 +65,10 @@ type orgRepoCtx struct { NewTemplate base.TplName } -// getOrgRepoCtx determines whether this is a repo, organization, or admin (both default and system) context. -func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) { - if len(ctx.Repo.RepoLink) > 0 { - return &orgRepoCtx{ +// getOwnerRepoCtx determines whether this is a repo, owner, or admin (both default and system) context. +func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) { + if is, ok := ctx.Data["IsRepositoryWebhook"]; ok && is.(bool) { + return &ownerRepoCtx{ RepoID: ctx.Repo.Repository.ID, Link: path.Join(ctx.Repo.RepoLink, "settings/hooks"), LinkNew: path.Join(ctx.Repo.RepoLink, "settings/hooks"), @@ -75,37 +76,35 @@ func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) { }, nil } - if len(ctx.Org.OrgLink) > 0 { - return &orgRepoCtx{ - OrgID: ctx.Org.Organization.ID, + if is, ok := ctx.Data["IsOrganizationWebhook"]; ok && is.(bool) { + return &ownerRepoCtx{ + OwnerID: ctx.ContextUser.ID, Link: path.Join(ctx.Org.OrgLink, "settings/hooks"), LinkNew: path.Join(ctx.Org.OrgLink, "settings/hooks"), NewTemplate: tplOrgHookNew, }, nil } - if ctx.Doer.IsAdmin { - // Are we looking at default webhooks? - if ctx.Params(":configType") == "default-hooks" { - return &orgRepoCtx{ - IsAdmin: true, - Link: path.Join(setting.AppSubURL, "/admin/hooks"), - LinkNew: path.Join(setting.AppSubURL, "/admin/default-hooks"), - NewTemplate: tplAdminHookNew, - }, nil - } + if is, ok := ctx.Data["IsUserWebhook"]; ok && is.(bool) { + return &ownerRepoCtx{ + OwnerID: ctx.Doer.ID, + Link: path.Join(setting.AppSubURL, "/user/settings/hooks"), + LinkNew: path.Join(setting.AppSubURL, "/user/settings/hooks"), + NewTemplate: tplUserHookNew, + }, nil + } - // Must be system webhooks instead - return &orgRepoCtx{ + if ctx.Doer.IsAdmin { + return &ownerRepoCtx{ IsAdmin: true, - IsSystemWebhook: true, + IsSystemWebhook: ctx.Params(":configType") == "system-hooks", Link: path.Join(setting.AppSubURL, "/admin/hooks"), LinkNew: path.Join(setting.AppSubURL, "/admin/system-hooks"), NewTemplate: tplAdminHookNew, }, nil } - return nil, errors.New("unable to set OrgRepo context") + return nil, errors.New("unable to set OwnerRepo context") } func checkHookType(ctx *context.Context) string { @@ -122,9 +121,9 @@ func WebhooksNew(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook_module.HookEvent{}} - orCtx, err := getOrgRepoCtx(ctx) + orCtx, err := getOwnerRepoCtx(ctx) if err != nil { - ctx.ServerError("getOrgRepoCtx", err) + ctx.ServerError("getOwnerRepoCtx", err) return } @@ -205,9 +204,9 @@ func createWebhook(ctx *context.Context, params webhookParams) { ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook_module.HookEvent{}} ctx.Data["HookType"] = params.Type - orCtx, err := getOrgRepoCtx(ctx) + orCtx, err := getOwnerRepoCtx(ctx) if err != nil { - ctx.ServerError("getOrgRepoCtx", err) + ctx.ServerError("getOwnerRepoCtx", err) return } ctx.Data["BaseLink"] = orCtx.LinkNew @@ -236,7 +235,7 @@ func createWebhook(ctx *context.Context, params webhookParams) { IsActive: params.WebhookForm.Active, Type: params.Type, Meta: string(meta), - OrgID: orCtx.OrgID, + OwnerID: orCtx.OwnerID, IsSystemWebhook: orCtx.IsSystemWebhook, } err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader) @@ -577,19 +576,19 @@ func packagistHookParams(ctx *context.Context) webhookParams { } } -func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) { - orCtx, err := getOrgRepoCtx(ctx) +func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) { + orCtx, err := getOwnerRepoCtx(ctx) if err != nil { - ctx.ServerError("getOrgRepoCtx", err) + ctx.ServerError("getOwnerRepoCtx", err) return nil, nil } ctx.Data["BaseLink"] = orCtx.Link var w *webhook.Webhook if orCtx.RepoID > 0 { - w, err = webhook.GetWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) - } else if orCtx.OrgID > 0 { - w, err = webhook.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")) + w, err = webhook.GetWebhookByRepoID(orCtx.RepoID, ctx.ParamsInt64(":id")) + } else if orCtx.OwnerID > 0 { + w, err = webhook.GetWebhookByOwnerID(orCtx.OwnerID, ctx.ParamsInt64(":id")) } else if orCtx.IsAdmin { w, err = webhook.GetSystemOrDefaultWebhook(ctx, ctx.ParamsInt64(":id")) } diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 94e59e2a490fd..05e45f999eed9 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -9,6 +9,8 @@ import ( ) func RenderUserHeader(ctx *context.Context) { + ctx.Data["IsProjectEnabled"] = true + ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["ContextUser"] = ctx.ContextUser } diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go index 2dba74822e531..7ad65cd51e39c 100644 --- a/routers/web/user/avatar.go +++ b/routers/web/user/avatar.go @@ -17,7 +17,7 @@ func cacheableRedirect(ctx *context.Context, location string) { // here we should not use `setting.StaticCacheTime`, it is pretty long (default: 6 hours) // we must make sure the redirection cache time is short enough, otherwise a user won't see the updated avatar in 6 hours // it's OK to make the cache time short, it is only a redirection, and doesn't cost much to make a new request - httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute) + httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 5*time.Minute) ctx.Redirect(location) } diff --git a/routers/web/user/code.go b/routers/web/user/code.go index 81e3e65b4b67a..b3adbcb8d3a8f 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -24,6 +24,7 @@ func CodeSearch(ctx *context.Context) { return } + ctx.Data["IsProjectEnabled"] = true ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["Title"] = ctx.Tr("explore.code") diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index b4452604599f7..f4d458c040d72 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -304,6 +304,7 @@ func Profile(ctx *context.Context) { pager.AddParam(ctx, "date", "Date") } ctx.Data["Page"] = pager + ctx.Data["IsProjectEnabled"] = true ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled diff --git a/routers/web/user/setting/webhooks.go b/routers/web/user/setting/webhooks.go new file mode 100644 index 0000000000000..9b0b0c9611c1a --- /dev/null +++ b/routers/web/user/setting/webhooks.go @@ -0,0 +1,48 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + + "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSettingsHooks base.TplName = "user/settings/hooks" +) + +// Webhooks render webhook list page +func Webhooks(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsHooks"] = true + ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/hooks" + ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/hooks" + ctx.Data["Description"] = ctx.Tr("settings.hooks.desc") + + ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OwnerID: ctx.Doer.ID}) + if err != nil { + ctx.ServerError("ListWebhooksByOpts", err) + return + } + + ctx.Data["Webhooks"] = ws + ctx.HTML(http.StatusOK, tplSettingsHooks) +} + +// DeleteWebhook response for delete webhook +func DeleteWebhook(ctx *context.Context) { + if err := webhook.DeleteWebhookByOwnerID(ctx.Doer.ID, ctx.FormInt64("id")); err != nil { + ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/hooks", + }) +} diff --git a/routers/web/web.go b/routers/web/web.go index ff312992dda0f..292268dc8055a 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -315,6 +315,35 @@ func RegisterRoutes(m *web.Route) { } } + addWebhookAddRoutes := func() { + m.Get("/{type}/new", repo.WebhooksNew) + m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) + m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) + m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) + m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) + m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) + m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) + m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) + m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) + m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) + m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) + m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) + } + + addWebhookEditRoutes := func() { + m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) + m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) + m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) + m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) + m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) + m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) + m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) + m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) + m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) + m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) + m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) + } + // FIXME: not all routes need go through same middleware. // Especially some AJAX requests, we can reduce middleware number to improve performance. // Routers. @@ -482,6 +511,19 @@ func RegisterRoutes(m *web.Route) { m.Get("/organization", user_setting.Organization) m.Get("/repos", user_setting.Repos) m.Post("/repos/unadopted", user_setting.AdoptOrDeleteRepository) + + m.Group("/hooks", func() { + m.Get("", user_setting.Webhooks) + m.Post("/delete", user_setting.DeleteWebhook) + addWebhookAddRoutes() + m.Group("/{id}", func() { + m.Get("", repo.WebHooksEdit) + m.Post("/replay/{uuid}", repo.ReplayWebhook) + }) + addWebhookEditRoutes() + }, webhooksEnabled, func(ctx *context.Context) { + ctx.Data["IsUserWebhook"] = true + }) }, reqSignIn, func(ctx *context.Context) { ctx.Data["PageIsUserSettings"] = true ctx.Data["AllThemes"] = setting.UI.Themes @@ -575,32 +617,11 @@ func RegisterRoutes(m *web.Route) { m.Get("", repo.WebHooksEdit) m.Post("/replay/{uuid}", repo.ReplayWebhook) }) - m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) - m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) - m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) - m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) - m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) - m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) - m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) - m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) - m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) - m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) - m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) + addWebhookEditRoutes() }, webhooksEnabled) m.Group("/{configType:default-hooks|system-hooks}", func() { - m.Get("/{type}/new", repo.WebhooksNew) - m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) - m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) - m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) - m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) - m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) - m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) - m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) - m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) - m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) - m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) - m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) + addWebhookAddRoutes() }) m.Group("/auths", func() { @@ -690,6 +711,21 @@ func RegisterRoutes(m *web.Route) { } } + reqUnitAccess := func(unitType unit.Type, accessMode perm.AccessMode) func(ctx *context.Context) { + return func(ctx *context.Context) { + if ctx.ContextUser == nil { + ctx.NotFound(unitType.String(), nil) + return + } + if ctx.ContextUser.IsOrganization() { + if ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unitType) < accessMode { + ctx.NotFound(unitType.String(), nil) + return + } + } + } + } + // ***** START: Organization ***** m.Group("/org", func() { m.Group("/{org}", func() { @@ -759,32 +795,15 @@ func RegisterRoutes(m *web.Route) { m.Group("/hooks", func() { m.Get("", org.Webhooks) m.Post("/delete", org.DeleteWebhook) - m.Get("/{type}/new", repo.WebhooksNew) - m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) - m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) - m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) - m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) - m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) - m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) - m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) - m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) - m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) - m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) + addWebhookAddRoutes() m.Group("/{id}", func() { m.Get("", repo.WebHooksEdit) m.Post("/replay/{uuid}", repo.ReplayWebhook) }) - m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) - m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) - m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) - m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) - m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) - m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) - m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) - m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) - m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) - m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) - }, webhooksEnabled) + addWebhookEditRoutes() + }, webhooksEnabled, func(ctx *context.Context) { + ctx.Data["IsOrganizationWebhook"] = true + }) m.Group("/labels", func() { m.Get("", org.RetrieveLabels, org.Labels) @@ -869,8 +888,10 @@ func RegisterRoutes(m *web.Route) { } m.Group("/projects", func() { - m.Get("", org.Projects) - m.Get("/{id}", org.ViewProject) + m.Group("", func() { + m.Get("", org.Projects) + m.Get("/{id}", org.ViewProject) + }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead)) m.Group("", func() { //nolint:dupl m.Get("/new", org.NewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) @@ -890,25 +911,18 @@ func RegisterRoutes(m *web.Route) { m.Post("/move", org.MoveIssues) }) }) - }, reqSignIn, func(ctx *context.Context) { - if ctx.ContextUser == nil { - ctx.NotFound("NewProject", nil) - return - } - if ctx.ContextUser.IsOrganization() { - if !ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) { - ctx.NotFound("NewProject", nil) - return - } - } else if ctx.ContextUser.ID != ctx.Doer.ID { + }, reqSignIn, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite), func(ctx *context.Context) { + if ctx.ContextUser.IsIndividual() && ctx.ContextUser.ID != ctx.Doer.ID { ctx.NotFound("NewProject", nil) return } }) }, repo.MustEnableProjects) - m.Get("/code", user.CodeSearch) - }, context_service.UserAssignmentWeb()) + m.Group("", func() { + m.Get("/code", user.CodeSearch) + }, reqUnitAccess(unit.TypeCode, perm.AccessModeRead)) + }, context_service.UserAssignmentWeb(), context.OrgAssignment()) // ***** Release Attachment Download without Signin m.Get("/{username}/{reponame}/releases/download/{vTag}/{fileName}", ignSignIn, context.RepoAssignment, repo.MustBeNotEmpty, repo.RedirectDownload) @@ -962,35 +976,16 @@ func RegisterRoutes(m *web.Route) { m.Group("/hooks", func() { m.Get("", repo.Webhooks) m.Post("/delete", repo.DeleteWebhook) - m.Get("/{type}/new", repo.WebhooksNew) - m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) - m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) - m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) - m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) - m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) - m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) - m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) - m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) - m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) - m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) - m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) + addWebhookAddRoutes() m.Group("/{id}", func() { m.Get("", repo.WebHooksEdit) m.Post("/test", repo.TestWebhook) m.Post("/replay/{uuid}", repo.ReplayWebhook) }) - m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) - m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) - m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) - m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) - m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) - m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) - m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) - m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) - m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) - m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) - m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) - }, webhooksEnabled) + addWebhookEditRoutes() + }, webhooksEnabled, func(ctx *context.Context) { + ctx.Data["IsRepositoryWebhook"] = true + }) m.Group("/keys", func() { m.Combo("").Get(repo.DeployKeys). diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go index 7fdacc6aae505..3e7df0cee0c0e 100644 --- a/services/attachment/attachment.go +++ b/services/attachment/attachment.go @@ -19,14 +19,14 @@ import ( ) // NewAttachment creates a new attachment object, but do not verify. -func NewAttachment(attach *repo_model.Attachment, file io.Reader) (*repo_model.Attachment, error) { +func NewAttachment(attach *repo_model.Attachment, file io.Reader, size int64) (*repo_model.Attachment, error) { if attach.RepoID == 0 { return nil, fmt.Errorf("attachment %s should belong to a repository", attach.Name) } err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { attach.UUID = uuid.New().String() - size, err := storage.Attachments.Save(attach.RelativePath(), file, -1) + size, err := storage.Attachments.Save(attach.RelativePath(), file, size) if err != nil { return fmt.Errorf("Create: %w", err) } @@ -39,7 +39,7 @@ func NewAttachment(attach *repo_model.Attachment, file io.Reader) (*repo_model.A } // UploadAttachment upload new attachment into storage and update database -func UploadAttachment(file io.Reader, allowedTypes string, opts *repo_model.Attachment) (*repo_model.Attachment, error) { +func UploadAttachment(file io.Reader, allowedTypes string, fileSize int64, opts *repo_model.Attachment) (*repo_model.Attachment, error) { buf := make([]byte, 1024) n, _ := util.ReadAtMost(file, buf) buf = buf[:n] @@ -48,5 +48,5 @@ func UploadAttachment(file io.Reader, allowedTypes string, opts *repo_model.Atta return nil, err } - return NewAttachment(opts, io.MultiReader(bytes.NewReader(buf), file)) + return NewAttachment(opts, io.MultiReader(bytes.NewReader(buf), file), fileSize) } diff --git a/services/attachment/attachment_test.go b/services/attachment/attachment_test.go index 72d1b2ab3a445..1b9af34427e53 100644 --- a/services/attachment/attachment_test.go +++ b/services/attachment/attachment_test.go @@ -36,7 +36,7 @@ func TestUploadAttachment(t *testing.T) { RepoID: 1, UploaderID: user.ID, Name: filepath.Base(fPath), - }, f) + }, f, -1) assert.NoError(t, err) attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, attach.UUID) diff --git a/services/auth/source/oauth2/source_register.go b/services/auth/source/oauth2/source_register.go index 3527d54b65c59..82a36acaa67d4 100644 --- a/services/auth/source/oauth2/source_register.go +++ b/services/auth/source/oauth2/source_register.go @@ -36,6 +36,10 @@ func (err ErrOpenIDConnectInitialize) Error() string { return fmt.Sprintf("Failed to initialize OpenID Connect Provider with name '%s' with url '%s': %v", err.ProviderName, err.OpenIDConnectAutoDiscoveryURL, err.Cause) } +func (err ErrOpenIDConnectInitialize) Unwrap() error { + return err.Cause +} + // wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2 // inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models func wrapOpenIDConnectInitializeError(err error, providerName string, source *Source) error { diff --git a/services/context/user.go b/services/context/user.go index 7642cba4e1f03..9dc84c3ac15e1 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -8,7 +8,6 @@ import ( "net/http" "strings" - org_model "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" ) @@ -57,14 +56,6 @@ func userAssignment(ctx *context.Context, errCb func(int, string, interface{})) } else { errCb(http.StatusInternalServerError, "GetUserByName", err) } - } else { - if ctx.ContextUser.IsOrganization() { - if ctx.Org == nil { - ctx.Org = &context.Organization{} - } - ctx.Org.Organization = (*org_model.Organization)(ctx.ContextUser) - ctx.Data["Org"] = ctx.Org.Organization - } } } } diff --git a/services/mailer/incoming/incoming_handler.go b/services/mailer/incoming/incoming_handler.go index 454039c60fb5f..b594e35189bf5 100644 --- a/services/mailer/incoming/incoming_handler.go +++ b/services/mailer/incoming/incoming_handler.go @@ -87,7 +87,7 @@ func (h *ReplyHandler) Handle(ctx context.Context, content *MailContent, doer *u attachmentIDs := make([]string, 0, len(content.Attachments)) if setting.Attachment.Enabled { for _, attachment := range content.Attachments { - a, err := attachment_service.UploadAttachment(bytes.NewReader(attachment.Content), setting.Attachment.AllowedTypes, &repo_model.Attachment{ + a, err := attachment_service.UploadAttachment(bytes.NewReader(attachment.Content), setting.Attachment.AllowedTypes, int64(len(attachment.Content)), &repo_model.Attachment{ Name: attachment.Name, UploaderID: doer.ID, RepoID: issue.Repo.ID, diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 8b259a362b1eb..ca961524d12c2 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -9,7 +9,6 @@ import ( "fmt" "io" "os" - "path" "path/filepath" "strconv" "strings" @@ -30,6 +29,7 @@ import ( "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/uri" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/pull" "github.com/google/uuid" @@ -866,7 +866,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { } // SECURITY: The TreePath must be cleaned! - comment.TreePath = path.Clean("/" + comment.TreePath)[1:] + comment.TreePath = util.CleanPath(comment.TreePath) var patch string reader, writer := io.Pipe() diff --git a/services/packages/container/blob_uploader.go b/services/packages/container/blob_uploader.go index ba92b0507343a..860672587d2b4 100644 --- a/services/packages/container/blob_uploader.go +++ b/services/packages/container/blob_uploader.go @@ -8,13 +8,13 @@ import ( "errors" "io" "os" - "path" "path/filepath" "strings" packages_model "code.gitea.io/gitea/models/packages" packages_module "code.gitea.io/gitea/modules/packages" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) var ( @@ -33,7 +33,7 @@ type BlobUploader struct { } func buildFilePath(id string) string { - return filepath.Join(setting.Packages.ChunkedUploadPath, path.Clean("/" + strings.ReplaceAll(id, "\\", "/"))[1:]) + return filepath.Join(setting.Packages.ChunkedUploadPath, util.CleanPath(strings.ReplaceAll(id, "\\", "/"))) } // NewBlobUploader creates a new blob uploader for the given id diff --git a/services/pull/comment.go b/services/pull/comment.go index 068aca6cd128b..933ad09a85e9a 100644 --- a/services/pull/comment.go +++ b/services/pull/comment.go @@ -14,58 +14,6 @@ import ( issue_service "code.gitea.io/gitea/services/issue" ) -type commitBranchCheckItem struct { - Commit *git.Commit - Checked bool -} - -func commitBranchCheck(gitRepo *git.Repository, startCommit *git.Commit, endCommitID, baseBranch string, commitList map[string]*commitBranchCheckItem) error { - if startCommit.ID.String() == endCommitID { - return nil - } - - checkStack := make([]string, 0, 10) - checkStack = append(checkStack, startCommit.ID.String()) - - for len(checkStack) > 0 { - commitID := checkStack[0] - checkStack = checkStack[1:] - - item, ok := commitList[commitID] - if !ok { - continue - } - - if item.Commit.ID.String() == endCommitID { - continue - } - - if err := item.Commit.LoadBranchName(); err != nil { - return err - } - - if item.Commit.Branch == baseBranch { - continue - } - - if item.Checked { - continue - } - - item.Checked = true - - parentNum := item.Commit.ParentCount() - for i := 0; i < parentNum; i++ { - parentCommit, err := item.Commit.Parent(i) - if err != nil { - return err - } - checkStack = append(checkStack, parentCommit.ID.String()) - } - } - return nil -} - // getCommitIDsFromRepo get commit IDs from repo in between oldCommitID and newCommitID // isForcePush will be true if oldCommit isn't on the branch // Commit on baseBranch will skip @@ -82,47 +30,33 @@ func getCommitIDsFromRepo(ctx context.Context, repo *repo_model.Repository, oldC return nil, false, err } - if err = oldCommit.LoadBranchName(); err != nil { - return nil, false, err - } - - if len(oldCommit.Branch) == 0 { - commitIDs = make([]string, 2) - commitIDs[0] = oldCommitID - commitIDs[1] = newCommitID - - return commitIDs, true, err - } - newCommit, err := gitRepo.GetCommit(newCommitID) if err != nil { return nil, false, err } - commits, err := newCommit.CommitsBeforeUntil(oldCommitID) + isForcePush, err = newCommit.IsForcePush(oldCommitID) if err != nil { return nil, false, err } - commitIDs = make([]string, 0, len(commits)) - commitChecks := make(map[string]*commitBranchCheckItem) + if isForcePush { + commitIDs = make([]string, 2) + commitIDs[0] = oldCommitID + commitIDs[1] = newCommitID - for _, commit := range commits { - commitChecks[commit.ID.String()] = &commitBranchCheckItem{ - Commit: commit, - Checked: false, - } + return commitIDs, isForcePush, err } - if err = commitBranchCheck(gitRepo, newCommit, oldCommitID, baseBranch, commitChecks); err != nil { - return + // Find commits between new and old commit exclusing base branch commits + commits, err := gitRepo.CommitsBetweenNotBase(newCommit, oldCommit, baseBranch) + if err != nil { + return nil, false, err } + commitIDs = make([]string, 0, len(commits)) for i := len(commits) - 1; i >= 0; i-- { - commitID := commits[i].ID.String() - if item, ok := commitChecks[commitID]; ok && item.Checked { - commitIDs = append(commitIDs, commitID) - } + commitIDs = append(commitIDs, commits[i].ID.String()) } return commitIDs, isForcePush, err diff --git a/services/pull/lfs.go b/services/pull/lfs.go index dc4ca006e4915..39433724684e1 100644 --- a/services/pull/lfs.go +++ b/services/pull/lfs.go @@ -116,7 +116,7 @@ func createLFSMetaObjectsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg } // Then we need to check that this pointer is in the db - if _, err := git_model.GetLFSMetaObjectByOid(db.DefaultContext, pr.HeadRepo.ID, pointer.Oid); err != nil { + if _, err := git_model.GetLFSMetaObjectByOid(db.DefaultContext, pr.HeadRepoID, pointer.Oid); err != nil { if err == git_model.ErrLFSObjectNotExist { log.Warn("During merge of: %d in %-v, there is a pointer to LFS Oid: %s which although present in the LFS store is not associated with the head repo %-v", pr.Index, pr.BaseRepo, pointer.Oid, pr.HeadRepo) continue diff --git a/services/pull/merge.go b/services/pull/merge.go index ad428427cc187..12e01e8ce74a5 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -5,8 +5,6 @@ package pull import ( - "bufio" - "bytes" "context" "fmt" "os" @@ -14,7 +12,6 @@ import ( "regexp" "strconv" "strings" - "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" @@ -34,22 +31,17 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - asymkey_service "code.gitea.io/gitea/services/asymkey" issue_service "code.gitea.io/gitea/services/issue" ) // GetDefaultMergeMessage returns default message used when merging pull request func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle) (message, body string, err error) { - if err := pr.LoadHeadRepo(ctx); err != nil { - return "", "", err - } if err := pr.LoadBaseRepo(ctx); err != nil { return "", "", err } - if pr.BaseRepo == nil { - return "", "", repo_model.ErrRepoNotExist{ID: pr.BaseRepoID} + if err := pr.LoadHeadRepo(ctx); err != nil { + return "", "", err } - if err := pr.LoadIssue(ctx); err != nil { return "", "", err } @@ -144,18 +136,19 @@ func expandDefaultMergeMessage(template string, vars map[string]string) (message // Merge merges pull request to base repository. // Caller should check PR is ready to be merged (review and status checks) func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error { - if err := pr.LoadHeadRepo(ctx); err != nil { - log.Error("LoadHeadRepo: %v", err) - return fmt.Errorf("LoadHeadRepo: %w", err) - } else if err := pr.LoadBaseRepo(ctx); err != nil { - log.Error("LoadBaseRepo: %v", err) - return fmt.Errorf("LoadBaseRepo: %w", err) + if err := pr.LoadBaseRepo(ctx); err != nil { + log.Error("Unable to load base repo: %v", err) + return fmt.Errorf("unable to load base repo: %w", err) + } else if err := pr.LoadHeadRepo(ctx); err != nil { + log.Error("Unable to load head repo: %v", err) + return fmt.Errorf("unable to load head repo: %w", err) } pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID)) // Removing an auto merge pull and ignore if not exist + // FIXME: is this the correct point to do this? Shouldn't this be after IsMergeStyleAllowed? if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) { return err } @@ -179,7 +172,7 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U // Run the merge in the hammer context to prevent cancellation hammerCtx := graceful.GetManager().HammerContext() - pr.MergedCommitID, err = rawMerge(hammerCtx, pr, doer, mergeStyle, expectedHeadCommitID, message) + pr.MergedCommitID, err = doMergeAndPush(hammerCtx, pr, doer, mergeStyle, expectedHeadCommitID, message) if err != nil { return err } @@ -189,18 +182,18 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U pr.MergerID = doer.ID if _, err := pr.SetMerged(hammerCtx); err != nil { - log.Error("SetMerged [%d]: %v", pr.ID, err) + log.Error("SetMerged %-v: %v", pr, err) } if err := pr.LoadIssue(hammerCtx); err != nil { - log.Error("LoadIssue [%d]: %v", pr.ID, err) + log.Error("LoadIssue %-v: %v", pr, err) } if err := pr.Issue.LoadRepo(hammerCtx); err != nil { - log.Error("LoadRepo for issue [%d]: %v", pr.ID, err) + log.Error("pr.Issue.LoadRepo %-v: %v", pr, err) } if err := pr.Issue.Repo.LoadOwner(hammerCtx); err != nil { - log.Error("LoadOwner for PR [%d]: %v", pr.ID, err) + log.Error("LoadOwner for %-v: %v", pr, err) } if wasAutoMerged { @@ -239,326 +232,43 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U return nil } -// rawMerge perform the merge operation without changing any pull information in database -func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) { +// doMergeAndPush performs the merge operation without changing any pull information in database and pushes it up to the base repository +func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) { // Clone base repo. - tmpBasePath, err := createTemporaryRepo(ctx, pr) + mergeCtx, cancel, err := createTemporaryRepoForMerge(ctx, pr, doer, expectedHeadCommitID) if err != nil { - log.Error("CreateTemporaryPath: %v", err) return "", err } - defer func() { - if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { - log.Error("Merge: RemoveTemporaryPath: %s", err) - } - }() - - baseBranch := "base" - trackingBranch := "tracking" - stagingBranch := "staging" - - if expectedHeadCommitID != "" { - trackingCommitID, _, err := git.NewCommand(ctx, "show-ref", "--hash").AddDynamicArguments(git.BranchPrefix + trackingBranch).RunStdString(&git.RunOpts{Dir: tmpBasePath}) - if err != nil { - log.Error("show-ref[%s] --hash refs/heads/trackingn: %v", tmpBasePath, git.BranchPrefix+trackingBranch, err) - return "", fmt.Errorf("getDiffTree: %w", err) - } - if strings.TrimSpace(trackingCommitID) != expectedHeadCommitID { - return "", models.ErrSHADoesNotMatch{ - GivenSHA: expectedHeadCommitID, - CurrentSHA: trackingCommitID, - } - } - } - - var outbuf, errbuf strings.Builder - - // Enable sparse-checkout - sparseCheckoutList, err := getDiffTree(ctx, tmpBasePath, baseBranch, trackingBranch) - if err != nil { - log.Error("getDiffTree(%s, %s, %s): %v", tmpBasePath, baseBranch, trackingBranch, err) - return "", fmt.Errorf("getDiffTree: %w", err) - } - - infoPath := filepath.Join(tmpBasePath, ".git", "info") - if err := os.MkdirAll(infoPath, 0o700); err != nil { - log.Error("Unable to create .git/info in %s: %v", tmpBasePath, err) - return "", fmt.Errorf("Unable to create .git/info in tmpBasePath: %w", err) - } - - sparseCheckoutListPath := filepath.Join(infoPath, "sparse-checkout") - if err := os.WriteFile(sparseCheckoutListPath, []byte(sparseCheckoutList), 0o600); err != nil { - log.Error("Unable to write .git/info/sparse-checkout file in %s: %v", tmpBasePath, err) - return "", fmt.Errorf("Unable to write .git/info/sparse-checkout file in tmpBasePath: %w", err) - } - - gitConfigCommand := func() *git.Command { - return git.NewCommand(ctx, "config", "--local") - } - - // Switch off LFS process (set required, clean and smudge here also) - if err := gitConfigCommand().AddArguments("filter.lfs.process", ""). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("git config [filter.lfs.process -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("git config [filter.lfs.process -> <> ]: %w\n%s\n%s", err, outbuf.String(), errbuf.String()) - } - outbuf.Reset() - errbuf.Reset() - - if err := gitConfigCommand().AddArguments("filter.lfs.required", "false"). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("git config [filter.lfs.required -> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("git config [filter.lfs.required -> ]: %w\n%s\n%s", err, outbuf.String(), errbuf.String()) - } - outbuf.Reset() - errbuf.Reset() - - if err := gitConfigCommand().AddArguments("filter.lfs.clean", ""). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("git config [filter.lfs.clean -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("git config [filter.lfs.clean -> <> ]: %w\n%s\n%s", err, outbuf.String(), errbuf.String()) - } - outbuf.Reset() - errbuf.Reset() - - if err := gitConfigCommand().AddArguments("filter.lfs.smudge", ""). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("git config [filter.lfs.smudge -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("git config [filter.lfs.smudge -> <> ]: %w\n%s\n%s", err, outbuf.String(), errbuf.String()) - } - outbuf.Reset() - errbuf.Reset() - - if err := gitConfigCommand().AddArguments("core.sparseCheckout", "true"). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("git config [core.sparseCheckout -> true ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("git config [core.sparsecheckout -> true]: %w\n%s\n%s", err, outbuf.String(), errbuf.String()) - } - outbuf.Reset() - errbuf.Reset() - - // Read base branch index - if err := git.NewCommand(ctx, "read-tree", "HEAD"). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("git read-tree HEAD: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("Unable to read base branch in to the index: %w\n%s\n%s", err, outbuf.String(), errbuf.String()) - } - outbuf.Reset() - errbuf.Reset() - - sig := doer.NewGitSig() - committer := sig - - // Determine if we should sign. If no signKeyID, use --no-gpg-sign to countermand the sign config (from gitconfig) - var signArgs git.TrustedCmdArgs - sign, signKeyID, signer, _ := asymkey_service.SignMerge(ctx, pr, doer, tmpBasePath, "HEAD", trackingBranch) - if sign { - if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { - committer = signer - } - signArgs = git.ToTrustedCmdArgs([]string{"-S" + signKeyID}) - } else { - signArgs = append(signArgs, "--no-gpg-sign") - } - - commitTimeStr := time.Now().Format(time.RFC3339) - - // Because this may call hooks we should pass in the environment - env := append(os.Environ(), - "GIT_AUTHOR_NAME="+sig.Name, - "GIT_AUTHOR_EMAIL="+sig.Email, - "GIT_AUTHOR_DATE="+commitTimeStr, - "GIT_COMMITTER_NAME="+committer.Name, - "GIT_COMMITTER_EMAIL="+committer.Email, - "GIT_COMMITTER_DATE="+commitTimeStr, - ) + defer cancel() // Merge commits. switch mergeStyle { case repo_model.MergeStyleMerge: - cmd := git.NewCommand(ctx, "merge", "--no-ff", "--no-commit").AddDynamicArguments(trackingBranch) - if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil { - log.Error("Unable to merge tracking into base: %v", err) + if err := doMergeStyleMerge(mergeCtx, message); err != nil { return "", err } - - if err := commitAndSignNoAuthor(ctx, pr, message, signArgs, tmpBasePath, env); err != nil { - log.Error("Unable to make final commit: %v", err) + case repo_model.MergeStyleRebase, repo_model.MergeStyleRebaseMerge: + if err := doMergeStyleRebase(mergeCtx, mergeStyle, message); err != nil { return "", err } - case repo_model.MergeStyleRebase: - fallthrough - case repo_model.MergeStyleRebaseUpdate: - fallthrough - case repo_model.MergeStyleRebaseMerge: - // Checkout head branch - if err := git.NewCommand(ctx, "checkout", "-b").AddDynamicArguments(stagingBranch, trackingBranch). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - } - outbuf.Reset() - errbuf.Reset() - - // Rebase before merging - if err := git.NewCommand(ctx, "rebase").AddDynamicArguments(baseBranch). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - // Rebase will leave a REBASE_HEAD file in .git if there is a conflict - if _, statErr := os.Stat(filepath.Join(tmpBasePath, ".git", "REBASE_HEAD")); statErr == nil { - var commitSha string - ok := false - failingCommitPaths := []string{ - filepath.Join(tmpBasePath, ".git", "rebase-apply", "original-commit"), // Git < 2.26 - filepath.Join(tmpBasePath, ".git", "rebase-merge", "stopped-sha"), // Git >= 2.26 - } - for _, failingCommitPath := range failingCommitPaths { - if _, statErr := os.Stat(failingCommitPath); statErr == nil { - commitShaBytes, readErr := os.ReadFile(failingCommitPath) - if readErr != nil { - // Abandon this attempt to handle the error - log.Error("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - } - commitSha = strings.TrimSpace(string(commitShaBytes)) - ok = true - break - } - } - if !ok { - log.Error("Unable to determine failing commit sha for this rebase message. Cannot cast as models.ErrRebaseConflicts.") - log.Error("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - } - log.Debug("RebaseConflict at %s [%s:%s -> %s:%s]: %v\n%s\n%s", commitSha, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - return "", models.ErrRebaseConflicts{ - Style: mergeStyle, - CommitSHA: commitSha, - StdOut: outbuf.String(), - StdErr: errbuf.String(), - Err: err, - } - } - log.Error("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - } - outbuf.Reset() - errbuf.Reset() - - // not need merge, just update by rebase. so skip - if mergeStyle == repo_model.MergeStyleRebaseUpdate { - break - } - - // Checkout base branch again - if err := git.NewCommand(ctx, "checkout").AddDynamicArguments(baseBranch). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - } - outbuf.Reset() - errbuf.Reset() - - cmd := git.NewCommand(ctx, "merge") - if mergeStyle == repo_model.MergeStyleRebase { - cmd.AddArguments("--ff-only") - } else { - cmd.AddArguments("--no-ff", "--no-commit") - } - cmd.AddDynamicArguments(stagingBranch) - - // Prepare merge with commit - if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil { - log.Error("Unable to merge staging into base: %v", err) - return "", err - } - if mergeStyle == repo_model.MergeStyleRebaseMerge { - if err := commitAndSignNoAuthor(ctx, pr, message, signArgs, tmpBasePath, env); err != nil { - log.Error("Unable to make final commit: %v", err) - return "", err - } - } case repo_model.MergeStyleSquash: - // Merge with squash - cmd := git.NewCommand(ctx, "merge", "--squash").AddDynamicArguments(trackingBranch) - if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil { - log.Error("Unable to merge --squash tracking into base: %v", err) + if err := doMergeStyleSquash(mergeCtx, message); err != nil { return "", err } - - if err = pr.Issue.LoadPoster(ctx); err != nil { - log.Error("LoadPoster: %v", err) - return "", fmt.Errorf("LoadPoster: %w", err) - } - sig := pr.Issue.Poster.NewGitSig() - if setting.Repository.PullRequest.AddCoCommitterTrailers && committer.String() != sig.String() { - // add trailer - message += fmt.Sprintf("\nCo-authored-by: %s\nCo-committed-by: %s\n", sig.String(), sig.String()) - } - if err := git.NewCommand(ctx, "commit"). - AddArguments(signArgs...). - AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email). - AddOptionFormat("--message=%s", message). - Run(&git.RunOpts{ - Env: env, - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("git commit [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - } - outbuf.Reset() - errbuf.Reset() default: return "", models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle} } // OK we should cache our current head and origin/headbranch - mergeHeadSHA, err := git.GetFullCommitID(ctx, tmpBasePath, "HEAD") + mergeHeadSHA, err := git.GetFullCommitID(ctx, mergeCtx.tmpBasePath, "HEAD") if err != nil { return "", fmt.Errorf("Failed to get full commit id for HEAD: %w", err) } - mergeBaseSHA, err := git.GetFullCommitID(ctx, tmpBasePath, "original_"+baseBranch) + mergeBaseSHA, err := git.GetFullCommitID(ctx, mergeCtx.tmpBasePath, "original_"+baseBranch) if err != nil { return "", fmt.Errorf("Failed to get full commit id for origin/%s: %w", pr.BaseBranch, err) } - mergeCommitID, err := git.GetFullCommitID(ctx, tmpBasePath, baseBranch) + mergeCommitID, err := git.GetFullCommitID(ctx, mergeCtx.tmpBasePath, baseBranch) if err != nil { return "", fmt.Errorf("Failed to get full commit id for the new merge: %w", err) } @@ -567,7 +277,7 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode // I think in the interests of data safety - failures to push to the lfs should prevent // the merge as you can always remerge. if setting.LFS.StartServer { - if err := LFSPush(ctx, tmpBasePath, mergeHeadSHA, mergeBaseSHA, pr); err != nil { + if err := LFSPush(ctx, mergeCtx.tmpBasePath, mergeHeadSHA, mergeBaseSHA, pr); err != nil { return "", err } } @@ -576,167 +286,97 @@ func rawMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_mode err = pr.HeadRepo.LoadOwner(ctx) if err != nil { if !user_model.IsErrUserNotExist(err) { - log.Error("Can't find user: %d for head repository - %v", pr.HeadRepo.OwnerID, err) + log.Error("Can't find user: %d for head repository in %-v: %v", pr.HeadRepo.OwnerID, pr, err) return "", err } - log.Error("Can't find user: %d for head repository - defaulting to doer: %s - %v", pr.HeadRepo.OwnerID, doer.Name, err) + log.Warn("Can't find user: %d for head repository in %-v - defaulting to doer: %s - %v", pr.HeadRepo.OwnerID, pr, doer.Name, err) headUser = doer } else { headUser = pr.HeadRepo.Owner } - var pushCmd *git.Command - if mergeStyle == repo_model.MergeStyleRebaseUpdate { - // force push the rebase result to head branch - env = repo_module.FullPushingEnvironment( - headUser, - doer, - pr.HeadRepo, - pr.HeadRepo.Name, - pr.ID, - ) - pushCmd = git.NewCommand(ctx, "push", "-f", "head_repo").AddDynamicArguments(stagingBranch + ":" + git.BranchPrefix + pr.HeadBranch) - } else { - env = repo_module.FullPushingEnvironment( - headUser, - doer, - pr.BaseRepo, - pr.BaseRepo.Name, - pr.ID, - ) - pushCmd = git.NewCommand(ctx, "push", "origin").AddDynamicArguments(baseBranch + ":" + git.BranchPrefix + pr.BaseBranch) - } + mergeCtx.env = repo_module.FullPushingEnvironment( + headUser, + doer, + pr.BaseRepo, + pr.BaseRepo.Name, + pr.ID, + ) + pushCmd := git.NewCommand(ctx, "push", "origin").AddDynamicArguments(baseBranch + ":" + git.BranchPrefix + pr.BaseBranch) // Push back to upstream. // TODO: this cause an api call to "/api/internal/hook/post-receive/...", // that prevents us from doint the whole merge in one db transaction - if err := pushCmd.Run(&git.RunOpts{ - Env: env, - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - if strings.Contains(errbuf.String(), "non-fast-forward") { + if err := pushCmd.Run(mergeCtx.RunOpts()); err != nil { + if strings.Contains(mergeCtx.errbuf.String(), "non-fast-forward") { return "", &git.ErrPushOutOfDate{ - StdOut: outbuf.String(), - StdErr: errbuf.String(), + StdOut: mergeCtx.outbuf.String(), + StdErr: mergeCtx.errbuf.String(), Err: err, } - } else if strings.Contains(errbuf.String(), "! [remote rejected]") { + } else if strings.Contains(mergeCtx.errbuf.String(), "! [remote rejected]") { err := &git.ErrPushRejected{ - StdOut: outbuf.String(), - StdErr: errbuf.String(), + StdOut: mergeCtx.outbuf.String(), + StdErr: mergeCtx.errbuf.String(), Err: err, } err.GenerateMessage() return "", err } - return "", fmt.Errorf("git push: %s", errbuf.String()) + return "", fmt.Errorf("git push: %s", mergeCtx.errbuf.String()) } - outbuf.Reset() - errbuf.Reset() + mergeCtx.outbuf.Reset() + mergeCtx.errbuf.Reset() return mergeCommitID, nil } -func commitAndSignNoAuthor(ctx context.Context, pr *issues_model.PullRequest, message string, signArgs git.TrustedCmdArgs, tmpBasePath string, env []string) error { - var outbuf, errbuf strings.Builder - if err := git.NewCommand(ctx, "commit").AddArguments(signArgs...).AddOptionFormat("--message=%s", message). - Run(&git.RunOpts{ - Env: env, - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - return fmt.Errorf("git commit [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) +func commitAndSignNoAuthor(ctx *mergeContext, message string) error { + cmdCommit := git.NewCommand(ctx, "commit").AddOptionFormat("--message=%s", message) + if ctx.signKeyID == "" { + cmdCommit.AddArguments("--no-gpg-sign") + } else { + cmdCommit.AddOptionFormat("-S%s", ctx.signKeyID) + } + if err := cmdCommit.Run(ctx.RunOpts()); err != nil { + log.Error("git commit %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) + return fmt.Errorf("git commit %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) } return nil } -func runMergeCommand(pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle, cmd *git.Command, tmpBasePath string) error { - var outbuf, errbuf strings.Builder - if err := cmd.Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { +func runMergeCommand(ctx *mergeContext, mergeStyle repo_model.MergeStyle, cmd *git.Command) error { + if err := cmd.Run(ctx.RunOpts()); err != nil { // Merge will leave a MERGE_HEAD file in the .git folder if there is a conflict - if _, statErr := os.Stat(filepath.Join(tmpBasePath, ".git", "MERGE_HEAD")); statErr == nil { + if _, statErr := os.Stat(filepath.Join(ctx.tmpBasePath, ".git", "MERGE_HEAD")); statErr == nil { // We have a merge conflict error - log.Debug("MergeConflict [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + log.Debug("MergeConflict %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) return models.ErrMergeConflicts{ Style: mergeStyle, - StdOut: outbuf.String(), - StdErr: errbuf.String(), + StdOut: ctx.outbuf.String(), + StdErr: ctx.errbuf.String(), Err: err, } - } else if strings.Contains(errbuf.String(), "refusing to merge unrelated histories") { - log.Debug("MergeUnrelatedHistories [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + } else if strings.Contains(ctx.errbuf.String(), "refusing to merge unrelated histories") { + log.Debug("MergeUnrelatedHistories %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) return models.ErrMergeUnrelatedHistories{ Style: mergeStyle, - StdOut: outbuf.String(), - StdErr: errbuf.String(), + StdOut: ctx.outbuf.String(), + StdErr: ctx.errbuf.String(), Err: err, } } - log.Error("git merge [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) - return fmt.Errorf("git merge [%s:%s -> %s:%s]: %w\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + log.Error("git merge %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) + return fmt.Errorf("git merge %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) } + ctx.outbuf.Reset() + ctx.errbuf.Reset() return nil } var escapedSymbols = regexp.MustCompile(`([*[?! \\])`) -func getDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string) (string, error) { - getDiffTreeFromBranch := func(repoPath, baseBranch, headBranch string) (string, error) { - var outbuf, errbuf strings.Builder - // Compute the diff-tree for sparse-checkout - if err := git.NewCommand(ctx, "diff-tree", "--no-commit-id", "--name-only", "-r", "-z", "--root").AddDynamicArguments(baseBranch, headBranch). - Run(&git.RunOpts{ - Dir: repoPath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - return "", fmt.Errorf("git diff-tree [%s base:%s head:%s]: %s", repoPath, baseBranch, headBranch, errbuf.String()) - } - return outbuf.String(), nil - } - - scanNullTerminatedStrings := func(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - if i := bytes.IndexByte(data, '\x00'); i >= 0 { - return i + 1, data[0:i], nil - } - if atEOF { - return len(data), data, nil - } - return 0, nil, nil - } - - list, err := getDiffTreeFromBranch(repoPath, baseBranch, headBranch) - if err != nil { - return "", err - } - - // Prefixing '/' for each entry, otherwise all files with the same name in subdirectories would be matched. - out := bytes.Buffer{} - scanner := bufio.NewScanner(strings.NewReader(list)) - scanner.Split(scanNullTerminatedStrings) - for scanner.Scan() { - filepath := scanner.Text() - // escape '*', '?', '[', spaces and '!' prefix - filepath = escapedSymbols.ReplaceAllString(filepath, `\$1`) - // no necessary to escape the first '#' symbol because the first symbol is '/' - fmt.Fprintf(&out, "/%s\n", filepath) - } - - return out.String(), nil -} - // IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) { if user == nil { diff --git a/services/pull/merge_merge.go b/services/pull/merge_merge.go new file mode 100644 index 0000000000000..0f7664297aa81 --- /dev/null +++ b/services/pull/merge_merge.go @@ -0,0 +1,25 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pull + +import ( + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" +) + +// doMergeStyleMerge merges the tracking into the current HEAD - which is assumed to tbe staging branch (equal to the pr.BaseBranch) +func doMergeStyleMerge(ctx *mergeContext, message string) error { + cmd := git.NewCommand(ctx, "merge", "--no-ff", "--no-commit").AddDynamicArguments(trackingBranch) + if err := runMergeCommand(ctx, repo_model.MergeStyleMerge, cmd); err != nil { + log.Error("%-v Unable to merge tracking into base: %v", ctx.pr, err) + return err + } + + if err := commitAndSignNoAuthor(ctx, message); err != nil { + log.Error("%-v Unable to make final commit: %v", ctx.pr, err) + return err + } + return nil +} diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go new file mode 100644 index 0000000000000..88f6c037ebc90 --- /dev/null +++ b/services/pull/merge_prepare.go @@ -0,0 +1,288 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pull + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "code.gitea.io/gitea/models" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + asymkey_service "code.gitea.io/gitea/services/asymkey" +) + +type mergeContext struct { + *prContext + doer *user_model.User + sig *git.Signature + committer *git.Signature + signKeyID string // empty for no-sign, non-empty to sign + env []string +} + +func (ctx *mergeContext) RunOpts() *git.RunOpts { + ctx.outbuf.Reset() + ctx.errbuf.Reset() + return &git.RunOpts{ + Env: ctx.env, + Dir: ctx.tmpBasePath, + Stdout: ctx.outbuf, + Stderr: ctx.errbuf, + } +} + +func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, expectedHeadCommitID string) (mergeCtx *mergeContext, cancel context.CancelFunc, err error) { + // Clone base repo. + prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) + if err != nil { + log.Error("createTemporaryRepoForPR: %v", err) + return nil, cancel, err + } + + mergeCtx = &mergeContext{ + prContext: prCtx, + doer: doer, + } + + if expectedHeadCommitID != "" { + trackingCommitID, _, err := git.NewCommand(ctx, "show-ref", "--hash").AddDynamicArguments(git.BranchPrefix + trackingBranch).RunStdString(&git.RunOpts{Dir: mergeCtx.tmpBasePath}) + if err != nil { + defer cancel() + log.Error("failed to get sha of head branch in %-v: show-ref[%s] --hash refs/heads/tracking: %v", mergeCtx.pr, mergeCtx.tmpBasePath, err) + return nil, nil, fmt.Errorf("unable to get sha of head branch in %v %w", pr, err) + } + if strings.TrimSpace(trackingCommitID) != expectedHeadCommitID { + defer cancel() + return nil, nil, models.ErrSHADoesNotMatch{ + GivenSHA: expectedHeadCommitID, + CurrentSHA: trackingCommitID, + } + } + } + + mergeCtx.outbuf.Reset() + mergeCtx.errbuf.Reset() + if err := prepareTemporaryRepoForMerge(mergeCtx); err != nil { + defer cancel() + return nil, nil, err + } + + mergeCtx.sig = doer.NewGitSig() + mergeCtx.committer = mergeCtx.sig + + // Determine if we should sign + sign, keyID, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch) + if sign { + mergeCtx.signKeyID = keyID + if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { + mergeCtx.committer = signer + } + } + + commitTimeStr := time.Now().Format(time.RFC3339) + + // Because this may call hooks we should pass in the environment + mergeCtx.env = append(os.Environ(), + "GIT_AUTHOR_NAME="+mergeCtx.sig.Name, + "GIT_AUTHOR_EMAIL="+mergeCtx.sig.Email, + "GIT_AUTHOR_DATE="+commitTimeStr, + "GIT_COMMITTER_NAME="+mergeCtx.committer.Name, + "GIT_COMMITTER_EMAIL="+mergeCtx.committer.Email, + "GIT_COMMITTER_DATE="+commitTimeStr, + ) + + return mergeCtx, cancel, nil +} + +// prepareTemporaryRepoForMerge takes a repository that has been created using createTemporaryRepo +// it then sets up the sparse-checkout and other things +func prepareTemporaryRepoForMerge(ctx *mergeContext) error { + infoPath := filepath.Join(ctx.tmpBasePath, ".git", "info") + if err := os.MkdirAll(infoPath, 0o700); err != nil { + log.Error("%-v Unable to create .git/info in %s: %v", ctx.pr, ctx.tmpBasePath, err) + return fmt.Errorf("Unable to create .git/info in tmpBasePath: %w", err) + } + + // Enable sparse-checkout + // Here we use the .git/info/sparse-checkout file as described in the git documentation + sparseCheckoutListFile, err := os.OpenFile(filepath.Join(infoPath, "sparse-checkout"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + log.Error("%-v Unable to write .git/info/sparse-checkout file in %s: %v", ctx.pr, ctx.tmpBasePath, err) + return fmt.Errorf("Unable to write .git/info/sparse-checkout file in tmpBasePath: %w", err) + } + defer sparseCheckoutListFile.Close() // we will close it earlier but we need to ensure it is closed if there is an error + + if err := getDiffTree(ctx, ctx.tmpBasePath, baseBranch, trackingBranch, sparseCheckoutListFile); err != nil { + log.Error("%-v getDiffTree(%s, %s, %s): %v", ctx.pr, ctx.tmpBasePath, baseBranch, trackingBranch, err) + return fmt.Errorf("getDiffTree: %w", err) + } + + if err := sparseCheckoutListFile.Close(); err != nil { + log.Error("%-v Unable to close .git/info/sparse-checkout file in %s: %v", ctx.pr, ctx.tmpBasePath, err) + return fmt.Errorf("Unable to close .git/info/sparse-checkout file in tmpBasePath: %w", err) + } + + setConfig := func(key, value string) error { + if err := git.NewCommand(ctx, "config", "--local").AddDynamicArguments(key, value). + Run(ctx.RunOpts()); err != nil { + log.Error("git config [%s -> %q]: %v\n%s\n%s", key, value, err, ctx.outbuf.String(), ctx.errbuf.String()) + return fmt.Errorf("git config [%s -> %q]: %w\n%s\n%s", key, value, err, ctx.outbuf.String(), ctx.errbuf.String()) + } + ctx.outbuf.Reset() + ctx.errbuf.Reset() + + return nil + } + + // Switch off LFS process (set required, clean and smudge here also) + if err := setConfig("filter.lfs.process", ""); err != nil { + return err + } + + if err := setConfig("filter.lfs.required", "false"); err != nil { + return err + } + + if err := setConfig("filter.lfs.clean", ""); err != nil { + return err + } + + if err := setConfig("filter.lfs.smudge", ""); err != nil { + return err + } + + if err := setConfig("core.sparseCheckout", "true"); err != nil { + return err + } + + // Read base branch index + if err := git.NewCommand(ctx, "read-tree", "HEAD"). + Run(ctx.RunOpts()); err != nil { + log.Error("git read-tree HEAD: %v\n%s\n%s", err, ctx.outbuf.String(), ctx.errbuf.String()) + return fmt.Errorf("Unable to read base branch in to the index: %w\n%s\n%s", err, ctx.outbuf.String(), ctx.errbuf.String()) + } + ctx.outbuf.Reset() + ctx.errbuf.Reset() + + return nil +} + +// getDiffTree returns a string containing all the files that were changed between headBranch and baseBranch +// the filenames are escaped so as to fit the format required for .git/info/sparse-checkout +func getDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string, out io.Writer) error { + diffOutReader, diffOutWriter, err := os.Pipe() + if err != nil { + log.Error("Unable to create os.Pipe for %s", repoPath) + return err + } + defer func() { + _ = diffOutReader.Close() + _ = diffOutWriter.Close() + }() + + scanNullTerminatedStrings := func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, '\x00'); i >= 0 { + return i + 1, data[0:i], nil + } + if atEOF { + return len(data), data, nil + } + return 0, nil, nil + } + + err = git.NewCommand(ctx, "diff-tree", "--no-commit-id", "--name-only", "-r", "-r", "-z", "--root").AddDynamicArguments(baseBranch, headBranch). + Run(&git.RunOpts{ + Dir: repoPath, + Stdout: diffOutWriter, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + // Close the writer end of the pipe to begin processing + _ = diffOutWriter.Close() + defer func() { + // Close the reader on return to terminate the git command if necessary + _ = diffOutReader.Close() + }() + + // Now scan the output from the command + scanner := bufio.NewScanner(diffOutReader) + scanner.Split(scanNullTerminatedStrings) + for scanner.Scan() { + filepath := scanner.Text() + // escape '*', '?', '[', spaces and '!' prefix + filepath = escapedSymbols.ReplaceAllString(filepath, `\$1`) + // no necessary to escape the first '#' symbol because the first symbol is '/' + fmt.Fprintf(out, "/%s\n", filepath) + } + return scanner.Err() + }, + }) + return err +} + +// rebaseTrackingOnToBase checks out the tracking branch as staging and rebases it on to the base branch +// if there is a conflict it will return a models.ErrRebaseConflicts +func rebaseTrackingOnToBase(ctx *mergeContext, mergeStyle repo_model.MergeStyle) error { + // Checkout head branch + if err := git.NewCommand(ctx, "checkout", "-b").AddDynamicArguments(stagingBranch, trackingBranch). + Run(ctx.RunOpts()); err != nil { + return fmt.Errorf("unable to git checkout tracking as staging in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) + } + ctx.outbuf.Reset() + ctx.errbuf.Reset() + + // Rebase before merging + if err := git.NewCommand(ctx, "rebase").AddDynamicArguments(baseBranch). + Run(ctx.RunOpts()); err != nil { + // Rebase will leave a REBASE_HEAD file in .git if there is a conflict + if _, statErr := os.Stat(filepath.Join(ctx.tmpBasePath, ".git", "REBASE_HEAD")); statErr == nil { + var commitSha string + ok := false + failingCommitPaths := []string{ + filepath.Join(ctx.tmpBasePath, ".git", "rebase-apply", "original-commit"), // Git < 2.26 + filepath.Join(ctx.tmpBasePath, ".git", "rebase-merge", "stopped-sha"), // Git >= 2.26 + } + for _, failingCommitPath := range failingCommitPaths { + if _, statErr := os.Stat(failingCommitPath); statErr == nil { + commitShaBytes, readErr := os.ReadFile(failingCommitPath) + if readErr != nil { + // Abandon this attempt to handle the error + return fmt.Errorf("unable to git rebase staging on to base in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) + } + commitSha = strings.TrimSpace(string(commitShaBytes)) + ok = true + break + } + } + if !ok { + log.Error("Unable to determine failing commit sha for failing rebase in temp repo for %-v. Cannot cast as models.ErrRebaseConflicts.", ctx.pr) + return fmt.Errorf("unable to git rebase staging on to base in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) + } + log.Debug("Conflict when rebasing staging on to base in %-v at %s: %v\n%s\n%s", ctx.pr, commitSha, err, ctx.outbuf.String(), ctx.errbuf.String()) + return models.ErrRebaseConflicts{ + CommitSHA: commitSha, + Style: mergeStyle, + StdOut: ctx.outbuf.String(), + StdErr: ctx.errbuf.String(), + Err: err, + } + } + return fmt.Errorf("unable to git rebase staging on to base in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) + } + ctx.outbuf.Reset() + ctx.errbuf.Reset() + return nil +} diff --git a/services/pull/merge_rebase.go b/services/pull/merge_rebase.go new file mode 100644 index 0000000000000..d3bb86d4aa081 --- /dev/null +++ b/services/pull/merge_rebase.go @@ -0,0 +1,50 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pull + +import ( + "fmt" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" +) + +// doMergeStyleRebase rebaases the tracking branch on the base branch as the current HEAD with or with a merge commit to the original pr branch +func doMergeStyleRebase(ctx *mergeContext, mergeStyle repo_model.MergeStyle, message string) error { + if err := rebaseTrackingOnToBase(ctx, mergeStyle); err != nil { + return err + } + + // Checkout base branch again + if err := git.NewCommand(ctx, "checkout").AddDynamicArguments(baseBranch). + Run(ctx.RunOpts()); err != nil { + log.Error("git checkout base prior to merge post staging rebase %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) + return fmt.Errorf("git checkout base prior to merge post staging rebase %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) + } + ctx.outbuf.Reset() + ctx.errbuf.Reset() + + cmd := git.NewCommand(ctx, "merge") + if mergeStyle == repo_model.MergeStyleRebase { + cmd.AddArguments("--ff-only") + } else { + cmd.AddArguments("--no-ff", "--no-commit") + } + cmd.AddDynamicArguments(stagingBranch) + + // Prepare merge with commit + if err := runMergeCommand(ctx, mergeStyle, cmd); err != nil { + log.Error("Unable to merge staging into base: %v", err) + return err + } + if mergeStyle == repo_model.MergeStyleRebaseMerge { + if err := commitAndSignNoAuthor(ctx, message); err != nil { + log.Error("Unable to make final commit: %v", err) + return err + } + } + + return nil +} diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go new file mode 100644 index 0000000000000..f52a2301d906c --- /dev/null +++ b/services/pull/merge_squash.go @@ -0,0 +1,85 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pull + +import ( + "fmt" + + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +// doMergeStyleSquash gets a commit author signature for squash commits +func getAuthorSignatureSquash(ctx *mergeContext) (*git.Signature, error) { + if err := ctx.pr.Issue.LoadPoster(ctx); err != nil { + log.Error("%-v Issue[%d].LoadPoster: %v", ctx.pr, ctx.pr.Issue.ID, err) + return nil, err + } + + // Try to get an signature from the same user in one of the commits, as the + // poster email might be private or commits might have a different signature + // than the primary email address of the poster. + gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, ctx.tmpBasePath) + if err != nil { + log.Error("%-v Unable to open base repository: %v", ctx.pr, err) + return nil, err + } + defer closer.Close() + + commits, err := gitRepo.CommitsBetweenIDs(trackingBranch, "HEAD") + if err != nil { + log.Error("%-v Unable to get commits between: %s %s: %v", ctx.pr, "HEAD", trackingBranch, err) + return nil, err + } + + uniqueEmails := make(container.Set[string]) + for _, commit := range commits { + if commit.Author != nil && uniqueEmails.Add(commit.Author.Email) { + commitUser, _ := user_model.GetUserByEmail(ctx, commit.Author.Email) + if commitUser != nil && commitUser.ID == ctx.pr.Issue.Poster.ID { + return commit.Author, nil + } + } + } + + return ctx.pr.Issue.Poster.NewGitSig(), nil +} + +// doMergeStyleSquash squashes the tracking branch on the current HEAD (=base) +func doMergeStyleSquash(ctx *mergeContext, message string) error { + sig, err := getAuthorSignatureSquash(ctx) + if err != nil { + return fmt.Errorf("getAuthorSignatureSquash: %w", err) + } + + cmdMerge := git.NewCommand(ctx, "merge", "--squash").AddDynamicArguments(trackingBranch) + if err := runMergeCommand(ctx, repo_model.MergeStyleSquash, cmdMerge); err != nil { + log.Error("%-v Unable to merge --squash tracking into base: %v", ctx.pr, err) + return err + } + + if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() { + // add trailer + message += fmt.Sprintf("\nCo-authored-by: %s\nCo-committed-by: %s\n", sig.String(), sig.String()) + } + cmdCommit := git.NewCommand(ctx, "commit"). + AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email). + AddOptionFormat("--message=%s", message) + if ctx.signKeyID == "" { + cmdCommit.AddArguments("--no-gpg-sign") + } else { + cmdCommit.AddOptionFormat("-S%s", ctx.signKeyID) + } + if err := cmdCommit.Run(ctx.RunOpts()); err != nil { + log.Error("git commit %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) + return fmt.Errorf("git commit [%s:%s -> %s:%s]: %w\n%s\n%s", ctx.pr.HeadRepo.FullName(), ctx.pr.HeadBranch, ctx.pr.BaseRepo.FullName(), ctx.pr.BaseBranch, err, ctx.outbuf.String(), ctx.errbuf.String()) + } + ctx.outbuf.Reset() + ctx.errbuf.Reset() + return nil +} diff --git a/services/pull/patch.go b/services/pull/patch.go index c2ccc75bdccdd..9277355720dc5 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -64,25 +63,21 @@ func TestPatch(pr *issues_model.PullRequest) error { defer finished() // Clone base repo. - tmpBasePath, err := createTemporaryRepo(ctx, pr) + prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) if err != nil { - log.Error("CreateTemporaryPath: %v", err) + log.Error("createTemporaryRepoForPR %-v: %v", pr, err) return err } - defer func() { - if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { - log.Error("Merge: RemoveTemporaryPath: %s", err) - } - }() + defer cancel() - gitRepo, err := git.OpenRepository(ctx, tmpBasePath) + gitRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath) if err != nil { return fmt.Errorf("OpenRepository: %w", err) } defer gitRepo.Close() // 1. update merge base - pr.MergeBase, _, err = git.NewCommand(ctx, "merge-base", "--", "base", "tracking").RunStdString(&git.RunOpts{Dir: tmpBasePath}) + pr.MergeBase, _, err = git.NewCommand(ctx, "merge-base", "--", "base", "tracking").RunStdString(&git.RunOpts{Dir: prCtx.tmpBasePath}) if err != nil { var err2 error pr.MergeBase, err2 = gitRepo.GetRefCommitID(git.BranchPrefix + "base") @@ -101,7 +96,7 @@ func TestPatch(pr *issues_model.PullRequest) error { } // 2. Check for conflicts - if conflicts, err := checkConflicts(ctx, pr, gitRepo, tmpBasePath); err != nil || conflicts || pr.Status == issues_model.PullRequestStatusEmpty { + if conflicts, err := checkConflicts(ctx, pr, gitRepo, prCtx.tmpBasePath); err != nil || conflicts || pr.Status == issues_model.PullRequestStatusEmpty { return err } diff --git a/services/pull/pull.go b/services/pull/pull.go index a19e88b33b657..e40e59a2c5002 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -349,18 +349,14 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, // checkIfPRContentChanged checks if diff to target branch has changed by push // A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) { - tmpBasePath, err := createTemporaryRepo(ctx, pr) + prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) if err != nil { - log.Error("CreateTemporaryRepo: %v", err) + log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err) return false, err } - defer func() { - if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { - log.Error("checkIfPRContentChanged: RemoveTemporaryPath: %s", err) - } - }() + defer cancel() - tmpRepo, err := git.OpenRepository(ctx, tmpBasePath) + tmpRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath) if err != nil { return false, fmt.Errorf("OpenRepository: %w", err) } @@ -379,7 +375,7 @@ func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, } if err := cmd.Run(&git.RunOpts{ - Dir: tmpBasePath, + Dir: prCtx.tmpBasePath, Stdout: stdoutWriter, PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { _ = stdoutWriter.Close() @@ -673,7 +669,12 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ authorString := commit.Author.String() if uniqueAuthors.Add(authorString) && authorString != posterSig { - authors = append(authors, authorString) + // Compare use account as well to avoid adding the same author multiple times + // times when email addresses are private or multiple emails are used. + commitUser, _ := user_model.GetUserByEmail(ctx, commit.Author.Email) + if commitUser == nil || commitUser.ID != pr.Issue.Poster.ID { + authors = append(authors, authorString) + } } } @@ -694,7 +695,10 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ for _, commit := range commits { authorString := commit.Author.String() if uniqueAuthors.Add(authorString) && authorString != posterSig { - authors = append(authors, authorString) + commitUser, _ := user_model.GetUserByEmail(ctx, commit.Author.Email) + if commitUser == nil || commitUser.ID != pr.Issue.Poster.ID { + authors = append(authors, authorString) + } } } skip += limit diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go index 2bef671555e00..146470780671e 100644 --- a/services/pull/temp_repo.go +++ b/services/pull/temp_repo.go @@ -19,49 +19,85 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" ) -// createTemporaryRepo creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch +// Temporary repos created here use standard branch names to help simplify +// merging code +const ( + baseBranch = "base" // equivalent to pr.BaseBranch + trackingBranch = "tracking" // equivalent to pr.HeadBranch + stagingBranch = "staging" // this is used for a working branch +) + +type prContext struct { + context.Context + tmpBasePath string + pr *issues_model.PullRequest + outbuf *strings.Builder // we keep these around to help reduce needless buffer recreation, + errbuf *strings.Builder // any use should be preceded by a Reset and preferably after use +} + +func (ctx *prContext) RunOpts() *git.RunOpts { + ctx.outbuf.Reset() + ctx.errbuf.Reset() + return &git.RunOpts{ + Dir: ctx.tmpBasePath, + Stdout: ctx.outbuf, + Stderr: ctx.errbuf, + } +} + +// createTemporaryRepoForPR creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch // it also create a second base branch called "original_base" -func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (string, error) { +func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest) (prCtx *prContext, cancel context.CancelFunc, err error) { if err := pr.LoadHeadRepo(ctx); err != nil { - log.Error("LoadHeadRepo: %v", err) - return "", fmt.Errorf("LoadHeadRepo: %w", err) + log.Error("%-v LoadHeadRepo: %v", pr, err) + return nil, nil, fmt.Errorf("%v LoadHeadRepo: %w", pr, err) } else if pr.HeadRepo == nil { - log.Error("Pr %d HeadRepo %d does not exist", pr.ID, pr.HeadRepoID) - return "", &repo_model.ErrRepoNotExist{ + log.Error("%-v HeadRepo %d does not exist", pr, pr.HeadRepoID) + return nil, nil, &repo_model.ErrRepoNotExist{ ID: pr.HeadRepoID, } } else if err := pr.LoadBaseRepo(ctx); err != nil { - log.Error("LoadBaseRepo: %v", err) - return "", fmt.Errorf("LoadBaseRepo: %w", err) + log.Error("%-v LoadBaseRepo: %v", pr, err) + return nil, nil, fmt.Errorf("%v LoadBaseRepo: %w", pr, err) } else if pr.BaseRepo == nil { - log.Error("Pr %d BaseRepo %d does not exist", pr.ID, pr.BaseRepoID) - return "", &repo_model.ErrRepoNotExist{ + log.Error("%-v BaseRepo %d does not exist", pr, pr.BaseRepoID) + return nil, nil, &repo_model.ErrRepoNotExist{ ID: pr.BaseRepoID, } } else if err := pr.HeadRepo.LoadOwner(ctx); err != nil { - log.Error("HeadRepo.LoadOwner: %v", err) - return "", fmt.Errorf("HeadRepo.LoadOwner: %w", err) + log.Error("%-v HeadRepo.LoadOwner: %v", pr, err) + return nil, nil, fmt.Errorf("%v HeadRepo.LoadOwner: %w", pr, err) } else if err := pr.BaseRepo.LoadOwner(ctx); err != nil { - log.Error("BaseRepo.LoadOwner: %v", err) - return "", fmt.Errorf("BaseRepo.LoadOwner: %w", err) + log.Error("%-v BaseRepo.LoadOwner: %v", pr, err) + return nil, nil, fmt.Errorf("%v BaseRepo.LoadOwner: %w", pr, err) } // Clone base repo. tmpBasePath, err := repo_module.CreateTemporaryPath("pull") if err != nil { - log.Error("CreateTemporaryPath: %v", err) - return "", err + log.Error("CreateTemporaryPath[%-v]: %v", pr, err) + return nil, nil, err + } + prCtx = &prContext{ + Context: ctx, + tmpBasePath: tmpBasePath, + pr: pr, + outbuf: &strings.Builder{}, + errbuf: &strings.Builder{}, + } + cancel = func() { + if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { + log.Error("Error whilst removing removing temporary repo for %-v: %v", pr, err) + } } baseRepoPath := pr.BaseRepo.RepoPath() headRepoPath := pr.HeadRepo.RepoPath() if err := git.InitRepository(ctx, tmpBasePath, false); err != nil { - log.Error("git init tmpBasePath: %v", err) - if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { - log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) - } - return "", err + log.Error("Unable to init tmpBasePath for %-v: %v", pr, err) + cancel() + return nil, nil, err } remoteRepoName := "head_repo" @@ -73,99 +109,63 @@ func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (str fetchArgs = append(fetchArgs, "--no-write-commit-graph") } - // Add head repo remote. - addCacheRepo := func(staging, cache string) error { - p := filepath.Join(staging, ".git", "objects", "info", "alternates") + // addCacheRepo adds git alternatives for the cacheRepoPath in the repoPath + addCacheRepo := func(repoPath, cacheRepoPath string) error { + p := filepath.Join(repoPath, ".git", "objects", "info", "alternates") f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { - log.Error("Could not create .git/objects/info/alternates file in %s: %v", staging, err) + log.Error("Could not create .git/objects/info/alternates file in %s: %v", repoPath, err) return err } defer f.Close() - data := filepath.Join(cache, "objects") + data := filepath.Join(cacheRepoPath, "objects") if _, err := fmt.Fprintln(f, data); err != nil { - log.Error("Could not write to .git/objects/info/alternates file in %s: %v", staging, err) + log.Error("Could not write to .git/objects/info/alternates file in %s: %v", repoPath, err) return err } return nil } + // Add head repo remote. if err := addCacheRepo(tmpBasePath, baseRepoPath); err != nil { - log.Error("Unable to add base repository to temporary repo [%s -> %s]: %v", pr.BaseRepo.FullName(), tmpBasePath, err) - if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { - log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) - } - return "", fmt.Errorf("Unable to add base repository to temporary repo [%s -> tmpBasePath]: %w", pr.BaseRepo.FullName(), err) + log.Error("%-v Unable to add base repository to temporary repo [%s -> %s]: %v", pr, pr.BaseRepo.FullName(), tmpBasePath, err) + cancel() + return nil, nil, fmt.Errorf("Unable to add base repository to temporary repo [%s -> tmpBasePath]: %w", pr.BaseRepo.FullName(), err) } - var outbuf, errbuf strings.Builder if err := git.NewCommand(ctx, "remote", "add", "-t").AddDynamicArguments(pr.BaseBranch).AddArguments("-m").AddDynamicArguments(pr.BaseBranch).AddDynamicArguments("origin", baseRepoPath). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("Unable to add base repository as origin [%s -> %s]: %v\n%s\n%s", pr.BaseRepo.FullName(), tmpBasePath, err, outbuf.String(), errbuf.String()) - if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { - log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) - } - return "", fmt.Errorf("Unable to add base repository as origin [%s -> tmpBasePath]: %w\n%s\n%s", pr.BaseRepo.FullName(), err, outbuf.String(), errbuf.String()) + Run(prCtx.RunOpts()); err != nil { + log.Error("%-v Unable to add base repository as origin [%s -> %s]: %v\n%s\n%s", pr, pr.BaseRepo.FullName(), tmpBasePath, err, prCtx.outbuf.String(), prCtx.errbuf.String()) + cancel() + return nil, nil, fmt.Errorf("Unable to add base repository as origin [%s -> tmpBasePath]: %w\n%s\n%s", pr.BaseRepo.FullName(), err, prCtx.outbuf.String(), prCtx.errbuf.String()) } - outbuf.Reset() - errbuf.Reset() if err := git.NewCommand(ctx, "fetch", "origin").AddArguments(fetchArgs...).AddDashesAndList(pr.BaseBranch+":"+baseBranch, pr.BaseBranch+":original_"+baseBranch). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("Unable to fetch origin base branch [%s:%s -> base, original_base in %s]: %v:\n%s\n%s", pr.BaseRepo.FullName(), pr.BaseBranch, tmpBasePath, err, outbuf.String(), errbuf.String()) - if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { - log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) - } - return "", fmt.Errorf("Unable to fetch origin base branch [%s:%s -> base, original_base in tmpBasePath]: %w\n%s\n%s", pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + Run(prCtx.RunOpts()); err != nil { + log.Error("%-v Unable to fetch origin base branch [%s:%s -> base, original_base in %s]: %v:\n%s\n%s", pr, pr.BaseRepo.FullName(), pr.BaseBranch, tmpBasePath, err, prCtx.outbuf.String(), prCtx.errbuf.String()) + cancel() + return nil, nil, fmt.Errorf("Unable to fetch origin base branch [%s:%s -> base, original_base in tmpBasePath]: %w\n%s\n%s", pr.BaseRepo.FullName(), pr.BaseBranch, err, prCtx.outbuf.String(), prCtx.errbuf.String()) } - outbuf.Reset() - errbuf.Reset() if err := git.NewCommand(ctx, "symbolic-ref").AddDynamicArguments("HEAD", git.BranchPrefix+baseBranch). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("Unable to set HEAD as base branch [%s]: %v\n%s\n%s", tmpBasePath, err, outbuf.String(), errbuf.String()) - if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { - log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) - } - return "", fmt.Errorf("Unable to set HEAD as base branch [tmpBasePath]: %w\n%s\n%s", err, outbuf.String(), errbuf.String()) + Run(prCtx.RunOpts()); err != nil { + log.Error("%-v Unable to set HEAD as base branch in [%s]: %v\n%s\n%s", pr, tmpBasePath, err, prCtx.outbuf.String(), prCtx.errbuf.String()) + cancel() + return nil, nil, fmt.Errorf("Unable to set HEAD as base branch in tmpBasePath: %w\n%s\n%s", err, prCtx.outbuf.String(), prCtx.errbuf.String()) } - outbuf.Reset() - errbuf.Reset() if err := addCacheRepo(tmpBasePath, headRepoPath); err != nil { - log.Error("Unable to add head repository to temporary repo [%s -> %s]: %v", pr.HeadRepo.FullName(), tmpBasePath, err) - if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { - log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) - } - return "", fmt.Errorf("Unable to head base repository to temporary repo [%s -> tmpBasePath]: %w", pr.HeadRepo.FullName(), err) + log.Error("%-v Unable to add head repository to temporary repo [%s -> %s]: %v", pr, pr.HeadRepo.FullName(), tmpBasePath, err) + cancel() + return nil, nil, fmt.Errorf("Unable to add head base repository to temporary repo [%s -> tmpBasePath]: %w", pr.HeadRepo.FullName(), err) } if err := git.NewCommand(ctx, "remote", "add").AddDynamicArguments(remoteRepoName, headRepoPath). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - log.Error("Unable to add head repository as head_repo [%s -> %s]: %v\n%s\n%s", pr.HeadRepo.FullName(), tmpBasePath, err, outbuf.String(), errbuf.String()) - if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { - log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) - } - return "", fmt.Errorf("Unable to add head repository as head_repo [%s -> tmpBasePath]: %w\n%s\n%s", pr.HeadRepo.FullName(), err, outbuf.String(), errbuf.String()) + Run(prCtx.RunOpts()); err != nil { + log.Error("%-v Unable to add head repository as head_repo [%s -> %s]: %v\n%s\n%s", pr, pr.HeadRepo.FullName(), tmpBasePath, err, prCtx.outbuf.String(), prCtx.errbuf.String()) + cancel() + return nil, nil, fmt.Errorf("Unable to add head repository as head_repo [%s -> tmpBasePath]: %w\n%s\n%s", pr.HeadRepo.FullName(), err, prCtx.outbuf.String(), prCtx.errbuf.String()) } - outbuf.Reset() - errbuf.Reset() trackingBranch := "tracking" // Fetch head branch @@ -178,24 +178,18 @@ func createTemporaryRepo(ctx context.Context, pr *issues_model.PullRequest) (str headBranch = pr.GetGitRefName() } if err := git.NewCommand(ctx, "fetch").AddArguments(fetchArgs...).AddDynamicArguments(remoteRepoName, headBranch+":"+trackingBranch). - Run(&git.RunOpts{ - Dir: tmpBasePath, - Stdout: &outbuf, - Stderr: &errbuf, - }); err != nil { - if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil { - log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) - } + Run(prCtx.RunOpts()); err != nil { + cancel() if !git.IsBranchExist(ctx, pr.HeadRepo.RepoPath(), pr.HeadBranch) { - return "", models.ErrBranchDoesNotExist{ + return nil, nil, models.ErrBranchDoesNotExist{ BranchName: pr.HeadBranch, } } - log.Error("Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, tmpBasePath, err, outbuf.String(), errbuf.String()) - return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %w\n%s\n%s", pr.HeadRepo.FullName(), headBranch, err, outbuf.String(), errbuf.String()) + log.Error("%-v Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr, pr.HeadRepo.FullName(), pr.HeadBranch, tmpBasePath, err, prCtx.outbuf.String(), prCtx.errbuf.String()) + return nil, nil, fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %w\n%s\n%s", pr.HeadRepo.FullName(), headBranch, err, prCtx.outbuf.String(), prCtx.errbuf.String()) } - outbuf.Reset() - errbuf.Reset() + prCtx.outbuf.Reset() + prCtx.errbuf.Reset() - return tmpBasePath, nil + return prCtx, cancel, nil } diff --git a/services/pull/update.go b/services/pull/update.go index b9525cf0c980f..b977dbdba9fe2 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -16,61 +16,67 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - repo_module "code.gitea.io/gitea/modules/repository" ) // Update updates pull request with base branch. -func Update(ctx context.Context, pull *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error { - var ( - pr *issues_model.PullRequest - style repo_model.MergeStyle - ) +func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error { + if pr.Flow == issues_model.PullRequestFlowAGit { + // TODO: update of agit flow pull request's head branch is unsupported + return fmt.Errorf("update of agit flow pull request's head branch is unsupported") + } - pullWorkingPool.CheckIn(fmt.Sprint(pull.ID)) - defer pullWorkingPool.CheckOut(fmt.Sprint(pull.ID)) + pullWorkingPool.CheckIn(fmt.Sprint(pr.ID)) + defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID)) - if rebase { - pr = pull - style = repo_model.MergeStyleRebaseUpdate - } else { - // use merge functions but switch repo's and branch's - pr = &issues_model.PullRequest{ - HeadRepoID: pull.BaseRepoID, - BaseRepoID: pull.HeadRepoID, - HeadBranch: pull.BaseBranch, - BaseBranch: pull.HeadBranch, - } - style = repo_model.MergeStyleMerge + diffCount, err := GetDiverging(ctx, pr) + if err != nil { + return err + } else if diffCount.Behind == 0 { + return fmt.Errorf("HeadBranch of PR %d is up to date", pr.Index) } - if pull.Flow == issues_model.PullRequestFlowAGit { - // TODO: Not support update agit flow pull request's head branch - return fmt.Errorf("Not support update agit flow pull request's head branch") + if rebase { + defer func() { + go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") + }() + + return updateHeadByRebaseOnToBase(ctx, pr, doer, message) } + if err := pr.LoadBaseRepo(ctx); err != nil { + log.Error("unable to load BaseRepo for %-v during update-by-merge: %v", pr, err) + return fmt.Errorf("unable to load BaseRepo for PR[%d] during update-by-merge: %w", pr.ID, err) + } if err := pr.LoadHeadRepo(ctx); err != nil { - log.Error("LoadHeadRepo: %v", err) - return fmt.Errorf("LoadHeadRepo: %w", err) - } else if err = pr.LoadBaseRepo(ctx); err != nil { - log.Error("LoadBaseRepo: %v", err) - return fmt.Errorf("LoadBaseRepo: %w", err) + log.Error("unable to load HeadRepo for PR %-v during update-by-merge: %v", pr, err) + return fmt.Errorf("unable to load HeadRepo for PR[%d] during update-by-merge: %w", pr.ID, err) + } + if pr.HeadRepo == nil { + // LoadHeadRepo will swallow ErrRepoNotExist so if pr.HeadRepo is still nil recreate the error + err := repo_model.ErrRepoNotExist{ + ID: pr.HeadRepoID, + } + log.Error("unable to load HeadRepo for PR %-v during update-by-merge: %v", pr, err) + return fmt.Errorf("unable to load HeadRepo for PR[%d] during update-by-merge: %w", pr.ID, err) } - diffCount, err := GetDiverging(ctx, pull) - if err != nil { - return err - } else if diffCount.Behind == 0 { - return fmt.Errorf("HeadBranch of PR %d is up to date", pull.Index) + // use merge functions but switch repos and branches + reversePR := &issues_model.PullRequest{ + ID: pr.ID, + + HeadRepoID: pr.BaseRepoID, + HeadRepo: pr.BaseRepo, + HeadBranch: pr.BaseBranch, + + BaseRepoID: pr.HeadRepoID, + BaseRepo: pr.HeadRepo, + BaseBranch: pr.HeadBranch, } - _, err = rawMerge(ctx, pr, doer, style, "", message) + _, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message) defer func() { - if rebase { - go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") - return - } - go AddTestPullRequestTask(doer, pr.HeadRepo.ID, pr.HeadBranch, false, "", "") + go AddTestPullRequestTask(doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "") }() return err @@ -159,27 +165,16 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, // GetDiverging determines how many commits a PR is ahead or behind the PR base branch func GetDiverging(ctx context.Context, pr *issues_model.PullRequest) (*git.DivergeObject, error) { - log.Trace("GetDiverging[%d]: compare commits", pr.ID) - if err := pr.LoadBaseRepo(ctx); err != nil { - return nil, err - } - if err := pr.LoadHeadRepo(ctx); err != nil { - return nil, err - } - - tmpRepo, err := createTemporaryRepo(ctx, pr) + log.Trace("GetDiverging[%-v]: compare commits", pr) + prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) if err != nil { if !models.IsErrBranchDoesNotExist(err) { - log.Error("CreateTemporaryRepo: %v", err) + log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err) } return nil, err } - defer func() { - if err := repo_module.RemoveTemporaryPath(tmpRepo); err != nil { - log.Error("Merge: RemoveTemporaryPath: %s", err) - } - }() + defer cancel() - diff, err := git.GetDivergingCommits(ctx, tmpRepo, "base", "tracking") + diff, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch) return &diff, err } diff --git a/services/pull/update_rebase.go b/services/pull/update_rebase.go new file mode 100644 index 0000000000000..8e7bfa0ffd4c2 --- /dev/null +++ b/services/pull/update_rebase.go @@ -0,0 +1,107 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pull + +import ( + "context" + "fmt" + "strings" + + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" +) + +// updateHeadByRebaseOnToBase handles updating a PR's head branch by rebasing it on the PR current base branch +func updateHeadByRebaseOnToBase(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, message string) error { + // "Clone" base repo and add the cache headers for the head repo and branch + mergeCtx, cancel, err := createTemporaryRepoForMerge(ctx, pr, doer, "") + if err != nil { + return err + } + defer cancel() + + // Determine the old merge-base before the rebase - we use this for LFS push later on + oldMergeBase, _, _ := git.NewCommand(ctx, "merge-base").AddDashesAndList(baseBranch, trackingBranch).RunStdString(&git.RunOpts{Dir: mergeCtx.tmpBasePath}) + oldMergeBase = strings.TrimSpace(oldMergeBase) + + // Rebase the tracking branch on to the base as the staging branch + if err := rebaseTrackingOnToBase(mergeCtx, repo_model.MergeStyleRebaseUpdate); err != nil { + return err + } + + if setting.LFS.StartServer { + // Now we need to ensure that the head repository contains any LFS objects between the new base and the old mergebase + // It's questionable about where this should go - either after or before the push + // I think in the interests of data safety - failures to push to the lfs should prevent + // the push as you can always re-rebase. + if err := LFSPush(ctx, mergeCtx.tmpBasePath, baseBranch, oldMergeBase, &issues_model.PullRequest{ + HeadRepoID: pr.BaseRepoID, + BaseRepoID: pr.HeadRepoID, + }); err != nil { + log.Error("Unable to push lfs objects between %s and %s up to head branch in %-v: %v", baseBranch, oldMergeBase, pr, err) + return err + } + } + + // Now determine who the pushing author should be + var headUser *user_model.User + if err := pr.HeadRepo.LoadOwner(ctx); err != nil { + if !user_model.IsErrUserNotExist(err) { + log.Error("Can't find user: %d for head repository in %-v - %v", pr.HeadRepo.OwnerID, pr, err) + return err + } + log.Error("Can't find user: %d for head repository in %-v - defaulting to doer: %-v - %v", pr.HeadRepo.OwnerID, pr, doer, err) + headUser = doer + } else { + headUser = pr.HeadRepo.Owner + } + + pushCmd := git.NewCommand(ctx, "push", "-f", "head_repo"). + AddDynamicArguments(stagingBranch + ":" + git.BranchPrefix + pr.HeadBranch) + + // Push back to the head repository. + // TODO: this cause an api call to "/api/internal/hook/post-receive/...", + // that prevents us from doint the whole merge in one db transaction + mergeCtx.outbuf.Reset() + mergeCtx.errbuf.Reset() + + if err := pushCmd.Run(&git.RunOpts{ + Env: repo_module.FullPushingEnvironment( + headUser, + doer, + pr.HeadRepo, + pr.HeadRepo.Name, + pr.ID, + ), + Dir: mergeCtx.tmpBasePath, + Stdout: mergeCtx.outbuf, + Stderr: mergeCtx.errbuf, + }); err != nil { + if strings.Contains(mergeCtx.errbuf.String(), "non-fast-forward") { + return &git.ErrPushOutOfDate{ + StdOut: mergeCtx.outbuf.String(), + StdErr: mergeCtx.errbuf.String(), + Err: err, + } + } else if strings.Contains(mergeCtx.errbuf.String(), "! [remote rejected]") { + err := &git.ErrPushRejected{ + StdOut: mergeCtx.outbuf.String(), + StdErr: mergeCtx.errbuf.String(), + Err: err, + } + err.GenerateMessage() + return err + } + return fmt.Errorf("git push: %s", mergeCtx.errbuf.String()) + } + mergeCtx.outbuf.Reset() + mergeCtx.errbuf.Reset() + + return nil +} diff --git a/services/release/release_test.go b/services/release/release_test.go index 9b8aaa364983a..805269413d729 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -106,11 +106,13 @@ func TestRelease_Create(t *testing.T) { IsTag: false, }, nil, "")) + testPlayload := "testtest" + attach, err := attachment.NewAttachment(&repo_model.Attachment{ RepoID: repo.ID, UploaderID: user.ID, Name: "test.txt", - }, strings.NewReader("testtest")) + }, strings.NewReader(testPlayload), int64(len([]byte(testPlayload)))) assert.NoError(t, err) release := repo_model.Release{ @@ -239,11 +241,12 @@ func TestRelease_Update(t *testing.T) { assert.Equal(t, tagName, release.TagName) // Add new attachments + samplePayload := "testtest" attach, err := attachment.NewAttachment(&repo_model.Attachment{ RepoID: repo.ID, UploaderID: user.ID, Name: "test.txt", - }, strings.NewReader("testtest")) + }, strings.NewReader(samplePayload), int64(len([]byte(samplePayload)))) assert.NoError(t, err) assert.NoError(t, UpdateRelease(user, gitRepo, release, []string{attach.UUID}, nil, nil)) diff --git a/services/repository/files/file.go b/services/repository/files/file.go index 2bac4372d378c..7939491aec624 100644 --- a/services/repository/files/file.go +++ b/services/repository/files/file.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "net/url" - "path" "strings" "time" @@ -15,6 +14,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) // GetFileResponseFromCommit Constructs a FileResponse from a Commit object @@ -129,7 +129,7 @@ func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_m // CleanUploadFileName Trims a filename and returns empty string if it is a .git directory func CleanUploadFileName(name string) string { // Rebase the filename - name = strings.Trim(path.Clean("/"+name), "/") + name = strings.Trim(util.CleanPath(name), "/") // Git disallows any filenames to have a .git directory in them. for _, part := range strings.Split(name, "/") { if strings.ToLower(part) == ".git" { diff --git a/services/repository/hooks.go b/services/repository/hooks.go index a8b6f7a622280..8506fa341369f 100644 --- a/services/repository/hooks.go +++ b/services/repository/hooks.go @@ -101,7 +101,7 @@ func GenerateWebhooks(ctx context.Context, templateRepo, generateRepo *repo_mode HookEvent: templateWebhook.HookEvent, IsActive: templateWebhook.IsActive, Type: templateWebhook.Type, - OrgID: templateWebhook.OrgID, + OwnerID: templateWebhook.OwnerID, Events: templateWebhook.Events, Meta: templateWebhook.Meta, }) diff --git a/services/repository/push.go b/services/repository/push.go index 355c2878113fd..4b574e3440679 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -80,7 +80,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PushUpdates: %s/%s", optsList[0].RepoUserName, optsList[0].RepoName)) defer finished() - ctx = cache.WithCacheContext(ctx) repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, optsList[0].RepoUserName, optsList[0].RepoName) if err != nil { @@ -207,12 +206,12 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { return fmt.Errorf("newCommit.CommitsBeforeUntil: %w", err) } - isForce, err := repo_module.IsForcePush(ctx, opts) + isForcePush, err := newCommit.IsForcePush(opts.OldCommitID) if err != nil { - log.Error("isForcePush %s:%s failed: %v", repo.FullName(), branch, err) + log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err) } - if isForce { + if isForcePush { log.Trace("Push %s is a force push", opts.NewCommitID) cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true)) diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index afd8e3c105cb1..b862d5bff10e4 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -229,16 +229,16 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu owner = source.Repository.MustOwner(ctx) } - // check if owner is an org and append additional webhooks - if owner != nil && owner.IsOrganization() { - orgHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ - OrgID: owner.ID, + // append additional webhooks of a user or organization + if owner != nil { + ownerHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ + OwnerID: owner.ID, IsActive: util.OptionalBoolTrue, }) if err != nil { return fmt.Errorf("ListWebhooksByOpts: %w", err) } - ws = append(ws, orgHooks...) + ws = append(ws, ownerHooks...) } // Add any admin-defined system webhooks diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl index b7ee00822f522..1080937f9134c 100644 --- a/templates/admin/auth/source/oauth.tmpl +++ b/templates/admin/auth/source/oauth.tmpl @@ -24,7 +24,7 @@ -
+
diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl index 7ca47cd32c56e..2a359d811134c 100644 --- a/templates/org/menu.tmpl +++ b/templates/org/menu.tmpl @@ -3,16 +3,18 @@ {{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}} + {{if and .IsProjectEnabled .CanReadProjects}} - {{svg "octicon-project"}} {{.locale.Tr "user.projects"}} + {{svg "octicon-project-symlink"}} {{.locale.Tr "user.projects"}} - {{if .IsPackageEnabled}} + {{end}} + {{if and .IsPackageEnabled .CanReadPackages}} {{svg "octicon-package"}} {{.locale.Tr "packages.title"}} {{end}} - {{if .IsRepoIndexerEnabled}} - + {{if and .IsRepoIndexerEnabled .CanReadCode}} + {{svg "octicon-code"}} {{$.locale.Tr "org.code"}} {{end}} diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index 8d9594e2b4773..4a21c0fd28ce2 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -12,7 +12,7 @@ {{template "base/alert" .}} -{{if or .CanWriteIssues .CanWritePulls}} +{{if $.CanWriteProjects}}