Merge remote-tracking branch 'upstream/main' into limit-repo-size

This commit is contained in:
DmitryFrolovTri
2025-12-18 19:42:45 +00:00
144 changed files with 3052 additions and 2074 deletions
+9
View File
@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
cooldown:
default-days: 5
+3 -1
View File
@@ -9,8 +9,10 @@ jobs:
cron-licenses:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea'
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
+3 -1
View File
@@ -9,8 +9,10 @@ jobs:
crowdin-pull:
runs-on: ubuntu-latest
if: github.repository == 'go-gitea/gitea'
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: crowdin/github-action@v1
with:
upload_sources: true
+3 -1
View File
@@ -24,6 +24,8 @@ jobs:
detect:
runs-on: ubuntu-latest
timeout-minutes: 3
permissions:
contents: read
outputs:
backend: ${{ steps.changes.outputs.backend }}
frontend: ${{ steps.changes.outputs.frontend }}
@@ -34,7 +36,7 @@ jobs:
swagger: ${{ steps.changes.outputs.swagger }}
yaml: ${{ steps.changes.outputs.yaml }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: changes
with:
+38 -12
View File
@@ -10,13 +10,17 @@ concurrency:
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
permissions:
contents: read
lint-backend:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -30,8 +34,10 @@ jobs:
if: needs.files-changed.outputs.templates == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v6
- run: uv python install 3.12
- uses: pnpm/action-setup@v4
@@ -46,8 +52,10 @@ jobs:
if: needs.files-changed.outputs.yaml == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v6
- run: uv python install 3.12
- run: make deps-py
@@ -57,8 +65,10 @@ jobs:
if: needs.files-changed.outputs.swagger == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
@@ -70,8 +80,10 @@ jobs:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -82,8 +94,10 @@ jobs:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -99,8 +113,10 @@ jobs:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -114,8 +130,10 @@ jobs:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -127,8 +145,10 @@ jobs:
if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
@@ -143,8 +163,10 @@ jobs:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -175,8 +197,10 @@ jobs:
if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
@@ -188,8 +212,10 @@ jobs:
if: needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
+17 -5
View File
@@ -10,11 +10,15 @@ concurrency:
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
permissions:
contents: read
test-pgsql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
services:
pgsql:
image: postgres:14
@@ -38,7 +42,7 @@ jobs:
ports:
- "9000:9000"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -65,8 +69,10 @@ jobs:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -90,6 +96,8 @@ jobs:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
services:
elasticsearch:
image: elasticsearch:7.5.0
@@ -124,7 +132,7 @@ jobs:
ports:
- 10000:10000
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -152,6 +160,8 @@ jobs:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
services:
mysql:
# the bitnami mysql image has more options than the official one, it's easier to customize
@@ -177,7 +187,7 @@ jobs:
- "587:587"
- "993:993"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
@@ -203,6 +213,8 @@ jobs:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
services:
mssql:
image: mcr.microsoft.com/mssql/server:2019-latest
@@ -217,7 +229,7 @@ jobs:
ports:
- 10000:10000
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
+5 -1
View File
@@ -10,13 +10,17 @@ concurrency:
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
permissions:
contents: read
container:
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v3
- name: Build regular container image
uses: docker/build-push-action@v5
+6 -2
View File
@@ -11,8 +11,10 @@ concurrency:
jobs:
nightly-binary:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
@@ -56,12 +58,14 @@ jobs:
- name: upload binaries to s3
run: |
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
nightly-container:
runs-on: namespace-profile-gitea-release-docker
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
+6 -2
View File
@@ -12,8 +12,10 @@ concurrency:
jobs:
binary:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
@@ -66,12 +68,14 @@ jobs:
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
container:
runs-on: namespace-profile-gitea-release-docker
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
+5 -2
View File
@@ -15,9 +15,10 @@ jobs:
binary:
runs-on: namespace-profile-gitea-release-binary
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
@@ -70,12 +71,14 @@ jobs:
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
container:
runs-on: namespace-profile-gitea-release-docker
permissions:
contents: read
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
+2 -3
View File
@@ -6,10 +6,12 @@ linters:
default: none
enable:
- bidichk
- bodyclose
- depguard
- dupl
- errcheck
- forbidigo
- gocheckcompilerdirectives
- gocritic
- govet
- ineffassign
@@ -109,9 +111,6 @@ linters:
- require-error
usetesting:
os-temp-dir: true
modernize:
disable:
- stringsbuilder
perfsprint:
concat-loop: false
govet:
+2 -2
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# Build stage
FROM docker.io/library/golang:1.25-alpine3.22 AS build-env
FROM docker.io/library/golang:1.25-alpine3.23 AS build-env
ARG GOPROXY=direct
@@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
/tmp/local/etc/s6/.s6-svscan/* \
/go/src/code.gitea.io/gitea/gitea
FROM docker.io/library/alpine:3.22 AS gitea
FROM docker.io/library/alpine:3.23 AS gitea
EXPOSE 22 3000
+2 -2
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# Build stage
FROM docker.io/library/golang:1.25-alpine3.22 AS build-env
FROM docker.io/library/golang:1.25-alpine3.23 AS build-env
ARG GOPROXY=direct
@@ -33,7 +33,7 @@ COPY docker/rootless /tmp/local
RUN chmod 755 /tmp/local/usr/local/bin/* \
/go/src/code.gitea.io/gitea/gitea
FROM docker.io/library/alpine:3.22 AS gitea-rootless
FROM docker.io/library/alpine:3.23 AS gitea-rootless
EXPOSE 2222 3000
+3 -3
View File
@@ -32,7 +32,7 @@ XGO_VERSION := go-1.25.x
AIR_PACKAGE ?= github.com/air-verse/air@v1
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.0
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
@@ -339,12 +339,12 @@ lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backen
.PHONY: lint-js
lint-js: node_modules ## lint js files
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --flag unstable_native_nodejs_ts_config $(ESLINT_FILES)
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES)
$(NODE_VARS) pnpm exec vue-tsc
.PHONY: lint-js-fix
lint-js-fix: node_modules ## lint js files and fix issues
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --flag unstable_native_nodejs_ts_config $(ESLINT_FILES) --fix
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES) --fix
$(NODE_VARS) pnpm exec vue-tsc
.PHONY: lint-css
+2 -2
View File
@@ -233,7 +233,7 @@ func TestAddLdapBindDn(t *testing.T) {
},
getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
assert.FailNow(t, "getAuthSourceByID called", "case %d: should not call getAuthSourceByID", n)
return nil, nil
return nil, nil //nolint:nilnil // mock function covering improper behavior
},
}
@@ -463,7 +463,7 @@ func TestAddLdapSimpleAuth(t *testing.T) {
},
getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) {
assert.FailNow(t, "getAuthSourceById called", "case %d: should not call getAuthSourceByID", n)
return nil, nil
return nil, nil //nolint:nilnil // mock function covering improper behavior
},
}
+3
View File
@@ -733,6 +733,9 @@ LEVEL = Info
;DISABLE_CORE_PROTECT_NTFS=false
;; Disable the usage of using partial clones for git.
;DISABLE_PARTIAL_CLONE = false
;; Set the similarity threshold passed to git commands via `--find-renames=<threshold>`.
;; Default is 50%, the same as git. Must be a integer percentage between 0% and 100%.
;DIFF_RENAME_SIMILARITY_THRESHOLD = 50%
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Git Operation timeout in seconds
+4 -3
View File
@@ -215,7 +215,7 @@ export default defineConfig([
'@typescript-eslint/no-unnecessary-condition': [0],
'@typescript-eslint/no-unnecessary-qualifier': [0],
'@typescript-eslint/no-unnecessary-template-expression': [0],
'@typescript-eslint/no-unnecessary-type-arguments': [0],
'@typescript-eslint/no-unnecessary-type-arguments': [2],
'@typescript-eslint/no-unnecessary-type-assertion': [2],
'@typescript-eslint/no-unnecessary-type-constraint': [2],
'@typescript-eslint/no-unnecessary-type-conversion': [2],
@@ -228,11 +228,12 @@ export default defineConfig([
'@typescript-eslint/no-unsafe-member-access': [0],
'@typescript-eslint/no-unsafe-return': [0],
'@typescript-eslint/no-unsafe-unary-minus': [2],
'@typescript-eslint/no-unused-expressions': [0],
'@typescript-eslint/no-unused-expressions': [2],
'@typescript-eslint/no-unused-private-class-members': [2],
'@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'all', caughtErrors: 'all', ignoreRestSiblings: false, argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'}],
'@typescript-eslint/no-use-before-define': [2, {functions: false, classes: true, variables: true, allowNamedExports: true, typedefs: false, enums: false, ignoreTypeReferences: true}],
'@typescript-eslint/no-useless-constructor': [0],
'@typescript-eslint/no-useless-default-assignment': [0], // https://github.com/typescript-eslint/typescript-eslint/issues/11847
'@typescript-eslint/no-useless-empty-export': [0],
'@typescript-eslint/no-wrapper-object-types': [2],
'@typescript-eslint/non-nullable-type-assertion-style': [0],
@@ -584,7 +585,7 @@ export default defineConfig([
'no-unreachable': [2],
'no-unsafe-finally': [2],
'no-unsafe-negation': [2],
'no-unused-expressions': [2],
'no-unused-expressions': [0], // handled by @typescript-eslint/no-unused-expressions
'no-unused-labels': [2],
'no-unused-private-class-members': [0], // handled by @typescript-eslint/no-unused-private-class-members
'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars
+1 -1
View File
@@ -28,7 +28,7 @@ require (
github.com/ProtonMail/go-crypto v1.3.0
github.com/PuerkitoBio/goquery v1.10.3
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0
github.com/alecthomas/chroma/v2 v2.20.0
github.com/alecthomas/chroma/v2 v2.21.0
github.com/aws/aws-sdk-go-v2/credentials v1.18.10
github.com/aws/aws-sdk-go-v2/service/codecommit v1.32.2
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
+4 -4
View File
@@ -98,11 +98,11 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/chroma/v2 v2.21.0 h1:YVW9qQAFnQm2OFPPFQg6G/TpMxKSsUr/KUPDi/BEqtY=
github.com/alecthomas/chroma/v2 v2.21.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
+17 -2
View File
@@ -14,6 +14,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
@@ -324,12 +325,26 @@ func (prs PullRequestList) LoadReviews(ctx context.Context) (ReviewList, error)
// HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bool, error) {
return db.GetEngine(ctx).
return HasMergedPullRequestInRepoBefore(ctx, repoID, posterID, 0, 0)
}
// HasMergedPullRequestInRepoBefore returns whether the user has a merged PR before a timestamp (0 = no limit)
func HasMergedPullRequestInRepoBefore(ctx context.Context, repoID, posterID int64, beforeUnix timeutil.TimeStamp, excludePullID int64) (bool, error) {
sess := db.GetEngine(ctx).
Join("INNER", "pull_request", "pull_request.issue_id = issue.id").
Where("repo_id=?", repoID).
And("poster_id=?", posterID).
And("is_pull=?", true).
And("pull_request.has_merged=?", true).
And("pull_request.has_merged=?", true)
if beforeUnix > 0 {
sess.And("pull_request.merged_unix < ?", beforeUnix)
}
if excludePullID > 0 {
sess.And("pull_request.id != ?", excludePullID)
}
return sess.
Select("issue.id").
Limit(1).
Get(new(Issue))
+9 -7
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"slices"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
@@ -169,7 +170,8 @@ func (p *Permission) ReadableUnitTypes() []unit.Type {
}
func (p *Permission) LogString() string {
format := "<Permission AccessMode=%s, %d Units, %d UnitsMode(s): ["
var format strings.Builder
format.WriteString("<Permission AccessMode=%s, %d Units, %d UnitsMode(s): [")
args := []any{p.AccessMode.ToString(), len(p.units), len(p.unitsMode)}
for i, u := range p.units {
@@ -181,19 +183,19 @@ func (p *Permission) LogString() string {
config = err.Error()
}
}
format += "\n\tunits[%d]: ID=%d RepoID=%d Type=%s Config=%s"
format.WriteString("\n\tunits[%d]: ID=%d RepoID=%d Type=%s Config=%s")
args = append(args, i, u.ID, u.RepoID, u.Type.LogString(), config)
}
for key, value := range p.unitsMode {
format += "\n\tunitsMode[%-v]: %-v"
format.WriteString("\n\tunitsMode[%-v]: %-v")
args = append(args, key.LogString(), value.LogString())
}
format += "\n\tanonymousAccessMode: %-v"
format.WriteString("\n\tanonymousAccessMode: %-v")
args = append(args, p.anonymousAccessMode)
format += "\n\teveryoneAccessMode: %-v"
format.WriteString("\n\teveryoneAccessMode: %-v")
args = append(args, p.everyoneAccessMode)
format += "\n\t]>"
return fmt.Sprintf(format, args...)
format.WriteString("\n\t]>")
return fmt.Sprintf(format.String(), args...)
}
func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) {
+12
View File
@@ -1461,3 +1461,15 @@ func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) {
}
return id, nil
}
// GetUserOrOrgByName returns the user or org by name
func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) {
var u User
has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u)
if err != nil {
return nil, err
} else if !has {
return nil, ErrUserNotExist{Name: name}
}
return &u, nil
}
+68 -86
View File
@@ -5,12 +5,10 @@ package charset
import (
"bytes"
"fmt"
"io"
"strings"
"unicode/utf8"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -23,60 +21,39 @@ import (
var UTF8BOM = []byte{'\xef', '\xbb', '\xbf'}
type ConvertOpts struct {
KeepBOM bool
KeepBOM bool
ErrorReplacement []byte
ErrorReturnOrigin bool
}
var ToUTF8WithFallbackReaderPrefetchSize = 16 * 1024
// ToUTF8WithFallbackReader detects the encoding of content and converts to UTF-8 reader if possible
func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
buf := make([]byte, 2048)
buf := make([]byte, ToUTF8WithFallbackReaderPrefetchSize)
n, err := util.ReadAtMost(rd, buf)
if err != nil {
return io.MultiReader(bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), rd)
}
charsetLabel, err := DetectEncoding(buf[:n])
if err != nil || charsetLabel == "UTF-8" {
return io.MultiReader(bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), rd)
}
encoding, _ := charset.Lookup(charsetLabel)
if encoding == nil {
// read error occurs, don't do any processing
return io.MultiReader(bytes.NewReader(buf[:n]), rd)
}
return transform.NewReader(
io.MultiReader(
bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)),
rd,
),
encoding.NewDecoder(),
)
}
// ToUTF8 converts content to UTF8 encoding
func ToUTF8(content []byte, opts ConvertOpts) (string, error) {
charsetLabel, err := DetectEncoding(content)
if err != nil {
return "", err
} else if charsetLabel == "UTF-8" {
return string(MaybeRemoveBOM(content, opts)), nil
charsetLabel, _ := DetectEncoding(buf[:n])
if charsetLabel == "UTF-8" {
// is utf-8, try to remove BOM and read it as-is
return io.MultiReader(bytes.NewReader(maybeRemoveBOM(buf[:n], opts)), rd)
}
encoding, _ := charset.Lookup(charsetLabel)
if encoding == nil {
return string(content), fmt.Errorf("Unknown encoding: %s", charsetLabel)
// unknown charset, don't do any processing
return io.MultiReader(bytes.NewReader(buf[:n]), rd)
}
// If there is an error, we concatenate the nicely decoded part and the
// original left over. This way we won't lose much data.
result, n, err := transform.Bytes(encoding.NewDecoder(), content)
if err != nil {
result = append(result, content[n:]...)
}
result = MaybeRemoveBOM(result, opts)
return string(result), err
// convert from charset to utf-8
return transform.NewReader(
io.MultiReader(bytes.NewReader(buf[:n]), rd),
encoding.NewDecoder(),
)
}
// ToUTF8WithFallback detects the encoding of content and converts to UTF-8 if possible
@@ -85,73 +62,84 @@ func ToUTF8WithFallback(content []byte, opts ConvertOpts) []byte {
return bs
}
// ToUTF8DropErrors makes sure the return string is valid utf-8; attempts conversion if possible
func ToUTF8DropErrors(content []byte, opts ConvertOpts) []byte {
charsetLabel, err := DetectEncoding(content)
if err != nil || charsetLabel == "UTF-8" {
return MaybeRemoveBOM(content, opts)
func ToUTF8DropErrors(content []byte) []byte {
return ToUTF8(content, ConvertOpts{ErrorReplacement: []byte{' '}})
}
func ToUTF8(content []byte, opts ConvertOpts) []byte {
charsetLabel, _ := DetectEncoding(content)
if charsetLabel == "UTF-8" {
return maybeRemoveBOM(content, opts)
}
encoding, _ := charset.Lookup(charsetLabel)
if encoding == nil {
setting.PanicInDevOrTesting("unsupported detected charset %q, it shouldn't happen", charsetLabel)
return content
}
// We ignore any non-decodable parts from the file.
// Some parts might be lost
var decoded []byte
decoder := encoding.NewDecoder()
idx := 0
for {
for idx < len(content) {
result, n, err := transform.Bytes(decoder, content[idx:])
decoded = append(decoded, result...)
if err == nil {
break
}
decoded = append(decoded, ' ')
idx = idx + n + 1
if idx >= len(content) {
break
if opts.ErrorReturnOrigin {
return content
}
if opts.ErrorReplacement == nil {
decoded = append(decoded, content[idx+n])
} else {
decoded = append(decoded, opts.ErrorReplacement...)
}
idx += n + 1
}
return MaybeRemoveBOM(decoded, opts)
return maybeRemoveBOM(decoded, opts)
}
// MaybeRemoveBOM removes a UTF-8 BOM from a []byte when opts.KeepBOM is false
func MaybeRemoveBOM(content []byte, opts ConvertOpts) []byte {
// maybeRemoveBOM removes a UTF-8 BOM from a []byte when opts.KeepBOM is false
func maybeRemoveBOM(content []byte, opts ConvertOpts) []byte {
if opts.KeepBOM {
return content
}
if len(content) > 2 && bytes.Equal(content[0:3], UTF8BOM) {
return content[3:]
}
return content
return bytes.TrimPrefix(content, UTF8BOM)
}
// DetectEncoding detect the encoding of content
func DetectEncoding(content []byte) (string, error) {
// it always returns a detected or guessed "encoding" string, no matter error happens or not
func DetectEncoding(content []byte) (encoding string, _ error) {
// First we check if the content represents valid utf8 content excepting a truncated character at the end.
// Now we could decode all the runes in turn but this is not necessarily the cheapest thing to do
// instead we walk backwards from the end to trim off a the incomplete character
// instead we walk backwards from the end to trim off the incomplete character
toValidate := content
end := len(toValidate) - 1
if end < 0 {
// no-op
} else if toValidate[end]>>5 == 0b110 {
// Incomplete 1 byte extension e.g. © <c2><a9> which has been truncated to <c2>
toValidate = toValidate[:end]
} else if end > 0 && toValidate[end]>>6 == 0b10 && toValidate[end-1]>>4 == 0b1110 {
// Incomplete 2 byte extension e.g. ⛔ <e2><9b><94> which has been truncated to <e2><9b>
toValidate = toValidate[:end-1]
} else if end > 1 && toValidate[end]>>6 == 0b10 && toValidate[end-1]>>6 == 0b10 && toValidate[end-2]>>3 == 0b11110 {
// Incomplete 3 byte extension e.g. 💩 <f0><9f><92><a9> which has been truncated to <f0><9f><92>
toValidate = toValidate[:end-2]
// U+0000 U+007F 0yyyzzzz
// U+0080 U+07FF 110xxxyy 10yyzzzz
// U+0800 U+FFFF 1110wwww 10xxxxyy 10yyzzzz
// U+010000 U+10FFFF 11110uvv 10vvwwww 10xxxxyy 10yyzzzz
cnt := 0
for end >= 0 && cnt < 4 {
c := toValidate[end]
if c>>5 == 0b110 || c>>4 == 0b1110 || c>>3 == 0b11110 {
// a leading byte
toValidate = toValidate[:end]
break
} else if c>>6 == 0b10 {
// a continuation byte
end--
} else {
// not an utf-8 byte
break
}
cnt++
}
if utf8.Valid(toValidate) {
log.Debug("Detected encoding: utf-8 (fast)")
return "UTF-8", nil
}
@@ -160,7 +148,7 @@ func DetectEncoding(content []byte) (string, error) {
if len(content) < 1024 {
// Check if original content is valid
if _, err := textDetector.DetectBest(content); err != nil {
return "", err
return util.IfZero(setting.Repository.AnsiCharset, "UTF-8"), err
}
times := 1024 / len(content)
detectContent = make([]byte, 0, times*len(content))
@@ -171,14 +159,10 @@ func DetectEncoding(content []byte) (string, error) {
detectContent = content
}
// Now we can't use DetectBest or just results[0] because the result isn't stable - so we need a tie break
// Now we can't use DetectBest or just results[0] because the result isn't stable - so we need a tie-break
results, err := textDetector.DetectAll(detectContent)
if err != nil {
if err == chardet.NotDetectedError && len(setting.Repository.AnsiCharset) > 0 {
log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
return setting.Repository.AnsiCharset, nil
}
return "", err
return util.IfZero(setting.Repository.AnsiCharset, "UTF-8"), err
}
topConfidence := results[0].Confidence
@@ -201,11 +185,9 @@ func DetectEncoding(content []byte) (string, error) {
}
// FIXME: to properly decouple this function the fallback ANSI charset should be passed as an argument
if topResult.Charset != "UTF-8" && len(setting.Repository.AnsiCharset) > 0 {
log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
if topResult.Charset != "UTF-8" && setting.Repository.AnsiCharset != "" {
return setting.Repository.AnsiCharset, err
}
log.Debug("Detected encoding: %s", topResult.Charset)
return topResult.Charset, err
return topResult.Charset, nil
}
+67 -202
View File
@@ -4,108 +4,89 @@
package charset
import (
"bytes"
"io"
"os"
"strings"
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func resetDefaultCharsetsOrder() {
defaultDetectedCharsetsOrder := make([]string, 0, len(setting.Repository.DetectedCharsetsOrder))
for _, charset := range setting.Repository.DetectedCharsetsOrder {
defaultDetectedCharsetsOrder = append(defaultDetectedCharsetsOrder, strings.ToLower(strings.TrimSpace(charset)))
}
func TestMain(m *testing.M) {
setting.Repository.DetectedCharsetScore = map[string]int{}
i := 0
for _, charset := range defaultDetectedCharsetsOrder {
canonicalCharset := strings.ToLower(strings.TrimSpace(charset))
if _, has := setting.Repository.DetectedCharsetScore[canonicalCharset]; !has {
setting.Repository.DetectedCharsetScore[canonicalCharset] = i
i++
}
for i, charset := range setting.Repository.DetectedCharsetsOrder {
setting.Repository.DetectedCharsetScore[strings.ToLower(charset)] = i
}
os.Exit(m.Run())
}
func TestMaybeRemoveBOM(t *testing.T) {
res := MaybeRemoveBOM([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
res := maybeRemoveBOM([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
res = MaybeRemoveBOM([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
res = maybeRemoveBOM([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
}
func TestToUTF8(t *testing.T) {
resetDefaultCharsetsOrder()
// Note: golang compiler seems so behave differently depending on the current
// locale, so some conversions might behave differently. For that reason, we don't
// depend on particular conversions but in expected behaviors.
res, err := ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{})
assert.NoError(t, err)
assert.Equal(t, "ABC", res)
res := ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{})
assert.Equal(t, "ABC", string(res))
// "áéíóú"
res, err = ToUTF8([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
assert.NoError(t, err)
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res))
res = ToUTF8([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
// "áéíóú"
res, err = ToUTF8([]byte{
res = ToUTF8([]byte{
0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3,
0xc3, 0xba,
}, ConvertOpts{})
assert.NoError(t, err)
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res))
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
res, err = ToUTF8([]byte{
res = ToUTF8([]byte{
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73, 0x41, 0x41, 0x41, 0x2e,
}, ConvertOpts{})
assert.NoError(t, err)
stringMustStartWith(t, "Hola,", res)
stringMustEndWith(t, "AAA.", res)
res, err = ToUTF8([]byte{
res = ToUTF8([]byte{
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73, 0x41, 0x41, 0x41, 0x2e,
}, ConvertOpts{})
assert.NoError(t, err)
stringMustStartWith(t, "Hola,", res)
stringMustEndWith(t, "AAA.", res)
res, err = ToUTF8([]byte{
res = ToUTF8([]byte{
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
0xF3, 0x6D, 0x6F, 0x20, 0x81, 0xA4, 0x6F, 0x73, 0x41, 0x41, 0x41, 0x2e,
}, ConvertOpts{})
assert.NoError(t, err)
stringMustStartWith(t, "Hola,", res)
stringMustEndWith(t, "AAA.", res)
// Japanese (Shift-JIS)
// 日属秘ぞしちゅ。
res, err = ToUTF8([]byte{
res = ToUTF8([]byte{
0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82,
0xBF, 0x82, 0xE3, 0x81, 0x42,
}, ConvertOpts{})
assert.NoError(t, err)
assert.Equal(t, []byte{
0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3,
0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82,
},
[]byte(res))
}, res)
res, err = ToUTF8([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{})
assert.NoError(t, err)
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, []byte(res))
res = ToUTF8([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{})
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, res)
}
func TestToUTF8WithFallback(t *testing.T) {
resetDefaultCharsetsOrder()
// "ABC"
res := ToUTF8WithFallback([]byte{0x41, 0x42, 0x43}, ConvertOpts{})
assert.Equal(t, []byte{0x41, 0x42, 0x43}, res)
@@ -152,54 +133,58 @@ func TestToUTF8WithFallback(t *testing.T) {
}
func TestToUTF8DropErrors(t *testing.T) {
resetDefaultCharsetsOrder()
// "ABC"
res := ToUTF8DropErrors([]byte{0x41, 0x42, 0x43}, ConvertOpts{})
res := ToUTF8DropErrors([]byte{0x41, 0x42, 0x43})
assert.Equal(t, []byte{0x41, 0x42, 0x43}, res)
// "áéíóú"
res = ToUTF8DropErrors([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
res = ToUTF8DropErrors([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba})
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
// UTF8 BOM + "áéíóú"
res = ToUTF8DropErrors([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
res = ToUTF8DropErrors([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba})
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
// "Hola, así cómo ños"
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73}, ConvertOpts{})
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73})
assert.Equal(t, []byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73}, res[:8])
assert.Equal(t, []byte{0x73}, res[len(res)-1:])
// "Hola, así cómo "
minmatch := []byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xC3, 0xAD, 0x20, 0x63, 0xC3, 0xB3, 0x6D, 0x6F, 0x20}
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73}, ConvertOpts{})
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73})
// Do not fail for differences in invalid cases, as the library might change the conversion criteria for those
assert.Equal(t, minmatch, res[0:len(minmatch)])
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x81, 0xA4, 0x6F, 0x73}, ConvertOpts{})
res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x81, 0xA4, 0x6F, 0x73})
// Do not fail for differences in invalid cases, as the library might change the conversion criteria for those
assert.Equal(t, minmatch, res[0:len(minmatch)])
// Japanese (Shift-JIS)
// "日属秘ぞしちゅ。"
res = ToUTF8DropErrors([]byte{0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82, 0xBF, 0x82, 0xE3, 0x81, 0x42}, ConvertOpts{})
res = ToUTF8DropErrors([]byte{0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82, 0xBF, 0x82, 0xE3, 0x81, 0x42})
assert.Equal(t, []byte{
0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3,
0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82,
}, res)
res = ToUTF8DropErrors([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{})
res = ToUTF8DropErrors([]byte{0x00, 0x00, 0x00, 0x00})
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, res)
}
func TestDetectEncoding(t *testing.T) {
resetDefaultCharsetsOrder()
testSuccess := func(b []byte, expected string) {
encoding, err := DetectEncoding(b)
assert.NoError(t, err)
assert.Equal(t, expected, encoding)
}
// invalid bytes
encoding, err := DetectEncoding([]byte{0xfa})
assert.Error(t, err)
assert.Equal(t, "UTF-8", encoding)
// utf-8
b := []byte("just some ascii")
testSuccess(b, "UTF-8")
@@ -214,169 +199,49 @@ func TestDetectEncoding(t *testing.T) {
// iso-8859-1: d<accented e>cor<newline>
b = []byte{0x44, 0xe9, 0x63, 0x6f, 0x72, 0x0a}
encoding, err := DetectEncoding(b)
encoding, err = DetectEncoding(b)
assert.NoError(t, err)
assert.Contains(t, encoding, "ISO-8859-1")
old := setting.Repository.AnsiCharset
setting.Repository.AnsiCharset = "placeholder"
defer func() {
setting.Repository.AnsiCharset = old
}()
testSuccess(b, "placeholder")
// invalid bytes
b = []byte{0xfa}
_, err = DetectEncoding(b)
assert.Error(t, err)
defer test.MockVariableValue(&setting.Repository.AnsiCharset, "MyEncoding")()
testSuccess(b, "MyEncoding")
}
func stringMustStartWith(t *testing.T, expected, value string) {
assert.Equal(t, expected, value[:len(expected)])
func stringMustStartWith(t *testing.T, expected string, value []byte) {
assert.Equal(t, expected, string(value[:len(expected)]))
}
func stringMustEndWith(t *testing.T, expected, value string) {
assert.Equal(t, expected, value[len(value)-len(expected):])
func stringMustEndWith(t *testing.T, expected string, value []byte) {
assert.Equal(t, expected, string(value[len(value)-len(expected):]))
}
func TestToUTF8WithFallbackReader(t *testing.T) {
resetDefaultCharsetsOrder()
test.MockVariableValue(&ToUTF8WithFallbackReaderPrefetchSize)
for testLen := range 2048 {
pattern := " test { () }\n"
input := ""
for len(input) < testLen {
input += pattern
}
input = input[:testLen]
input += "// Выключаем"
rd := ToUTF8WithFallbackReader(bytes.NewReader([]byte(input)), ConvertOpts{})
block := "aá啊🤔"
runes := []rune(block)
assert.Len(t, string(runes[0]), 1)
assert.Len(t, string(runes[1]), 2)
assert.Len(t, string(runes[2]), 3)
assert.Len(t, string(runes[3]), 4)
content := strings.Repeat(block, 2)
for i := 1; i < len(content); i++ {
encoding, err := DetectEncoding([]byte(content[:i]))
assert.NoError(t, err)
assert.Equal(t, "UTF-8", encoding)
ToUTF8WithFallbackReaderPrefetchSize = i
rd := ToUTF8WithFallbackReader(strings.NewReader(content), ConvertOpts{})
r, _ := io.ReadAll(rd)
assert.Equalf(t, input, string(r), "testing string len=%d", testLen)
assert.Equal(t, content, string(r))
}
for _, r := range runes {
content = "abc abc " + string(r) + string(r) + string(r)
for i := 0; i < len(content); i++ {
encoding, err := DetectEncoding([]byte(content[:i]))
assert.NoError(t, err)
assert.Equal(t, "UTF-8", encoding)
}
}
truncatedOneByteExtension := failFastBytes
encoding, _ := DetectEncoding(truncatedOneByteExtension)
assert.Equal(t, "UTF-8", encoding)
truncatedTwoByteExtension := failFastBytes
truncatedTwoByteExtension[len(failFastBytes)-1] = 0x9b
truncatedTwoByteExtension[len(failFastBytes)-2] = 0xe2
encoding, _ = DetectEncoding(truncatedTwoByteExtension)
assert.Equal(t, "UTF-8", encoding)
truncatedThreeByteExtension := failFastBytes
truncatedThreeByteExtension[len(failFastBytes)-1] = 0x92
truncatedThreeByteExtension[len(failFastBytes)-2] = 0x9f
truncatedThreeByteExtension[len(failFastBytes)-3] = 0xf0
encoding, _ = DetectEncoding(truncatedThreeByteExtension)
assert.Equal(t, "UTF-8", encoding)
}
var failFastBytes = []byte{
0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x6f, 0x72, 0x67, 0x2e, 0x61, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x74, 0x6f,
0x6f, 0x6c, 0x73, 0x2e, 0x61, 0x6e, 0x74, 0x2e, 0x74, 0x61, 0x73, 0x6b, 0x64, 0x65, 0x66, 0x73, 0x2e, 0x63, 0x6f, 0x6e,
0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4f, 0x73, 0x0a, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x6f, 0x72, 0x67,
0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f,
0x74, 0x2e, 0x67, 0x72, 0x61, 0x64, 0x6c, 0x65, 0x2e, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x2e, 0x72, 0x75, 0x6e, 0x2e, 0x42,
0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x0a, 0x0a, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x69, 0x64, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d,
0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x22, 0x29, 0x0a, 0x7d, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x65,
0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a,
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x61, 0x70, 0x69, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d,
0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74,
0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x61, 0x70, 0x69, 0x2d, 0x64, 0x6f, 0x63, 0x73, 0x22, 0x29,
0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x64, 0x62,
0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a,
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x66,
0x73, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
0x3a, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x6d, 0x71, 0x22, 0x29, 0x29, 0x0a, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22,
0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
0x2d, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2d, 0x73, 0x74, 0x61, 0x72, 0x74,
0x65, 0x72, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63,
0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d, 0x68, 0x61, 0x6c, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e,
0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x22, 0x29, 0x0a,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28,
0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b,
0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d, 0x73, 0x74,
0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x77, 0x65, 0x62, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69,
0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72,
0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x61, 0x6f, 0x70,
0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f,
0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d,
0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x61, 0x63, 0x74, 0x75, 0x61, 0x74, 0x6f, 0x72, 0x22, 0x29, 0x0a, 0x20,
0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f,
0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63,
0x6c, 0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74,
0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x22, 0x29, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72,
0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6c,
0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74, 0x61,
0x72, 0x74, 0x65, 0x72, 0x2d, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2d, 0x61, 0x6c, 0x6c, 0x22, 0x29, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72,
0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6c,
0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74, 0x61,
0x72, 0x74, 0x65, 0x72, 0x2d, 0x73, 0x6c, 0x65, 0x75, 0x74, 0x68, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d,
0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70,
0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x72, 0x65, 0x74, 0x72, 0x79, 0x3a,
0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x72, 0x65, 0x74, 0x72, 0x79, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x63, 0x68, 0x2e, 0x71,
0x6f, 0x73, 0x2e, 0x6c, 0x6f, 0x67, 0x62, 0x61, 0x63, 0x6b, 0x3a, 0x6c, 0x6f, 0x67, 0x62, 0x61, 0x63, 0x6b, 0x2d, 0x63,
0x6c, 0x61, 0x73, 0x73, 0x69, 0x63, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x69, 0x6f, 0x2e, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x6d, 0x65,
0x74, 0x65, 0x72, 0x3a, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x2d, 0x72, 0x65, 0x67, 0x69, 0x73,
0x74, 0x72, 0x79, 0x2d, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x6b, 0x6f, 0x74,
0x6c, 0x69, 0x6e, 0x28, 0x22, 0x73, 0x74, 0x64, 0x6c, 0x69, 0x62, 0x22, 0x29, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
0x2f, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64,
0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74,
0x65, 0x73, 0x74, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a,
0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d,
0x74, 0x65, 0x73, 0x74, 0x22, 0x29, 0x0a, 0x7d, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x20, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4a,
0x61, 0x72, 0x20, 0x62, 0x79, 0x20, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72,
0x69, 0x6e, 0x67, 0x28, 0x4a, 0x61, 0x72, 0x3a, 0x3a, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x65, 0x72, 0x2e,
0x73, 0x65, 0x74, 0x28, 0x22, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x76, 0x61, 0x6c, 0x20, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x70, 0x61, 0x74, 0x68,
0x20, 0x62, 0x79, 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x67,
0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74,
0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65,
0x73, 0x28, 0x22, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x2d, 0x50, 0x61, 0x74, 0x68, 0x22, 0x20, 0x74, 0x6f, 0x20, 0x6f, 0x62,
0x6a, 0x65, 0x63, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70,
0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x20, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x20, 0x3d,
0x20, 0x22, 0x66, 0x69, 0x6c, 0x65, 0x3a, 0x2f, 0x2b, 0x22, 0x2e, 0x74, 0x6f, 0x52, 0x65, 0x67, 0x65, 0x78, 0x28, 0x29,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64,
0x65, 0x20, 0x66, 0x75, 0x6e, 0x20, 0x74, 0x6f, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x28, 0x29, 0x3a, 0x20, 0x53, 0x74,
0x72, 0x69, 0x6e, 0x67, 0x20, 0x3d, 0x20, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x70,
0x61, 0x74, 0x68, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x6f, 0x53, 0x74, 0x72, 0x69,
0x6e, 0x67, 0x28, 0x22, 0x20, 0x22, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x74, 0x2e, 0x74, 0x6f, 0x55, 0x52, 0x49, 0x28, 0x29, 0x2e, 0x74, 0x6f, 0x55,
0x52, 0x4c, 0x28, 0x29, 0x2e, 0x74, 0x6f, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x28, 0x29, 0x2e, 0x72, 0x65, 0x70, 0x6c,
0x61, 0x63, 0x65, 0x46, 0x69, 0x72, 0x73, 0x74, 0x28, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x2c, 0x20, 0x22, 0x2f,
0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x7d, 0x0a, 0x0a, 0x74, 0x61, 0x73,
0x6b, 0x73, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x64, 0x3c, 0x42, 0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x3e, 0x28, 0x22, 0x62,
0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x22, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x4f,
0x73, 0x2e, 0x69, 0x73, 0x46, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x28, 0x4f, 0x73, 0x2e, 0x46, 0x41, 0x4d, 0x49, 0x4c, 0x59,
0x5f, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x53, 0x29, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x70, 0x61, 0x74, 0x68, 0x20, 0x3d, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x28, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x64, 0x28, 0x22, 0x6d, 0x61, 0x69,
0x6e, 0x22, 0x29, 0x2e, 0x6d, 0x61, 0x70, 0x20, 0x7b, 0x20, 0x69, 0x74, 0x2e, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x20,
0x7d, 0x2c, 0x20, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4a, 0x61, 0x72, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0xd0,
}
+5 -2
View File
@@ -20,14 +20,17 @@ import (
// RuneNBSP is the codepoint for NBSP
const RuneNBSP = 0xa0
// EscapeControlHTML escapes the unicode control sequences in a provided html document
// EscapeControlHTML escapes the Unicode control sequences in a provided html document
func EscapeControlHTML(html template.HTML, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output template.HTML) {
if !setting.UI.AmbiguousUnicodeDetection {
return &EscapeStatus{}, html
}
sb := &strings.Builder{}
escaped, _ = EscapeControlReader(strings.NewReader(string(html)), sb, locale, allowed...) // err has been handled in EscapeControlReader
return escaped, template.HTML(sb.String())
}
// EscapeControlReader escapes the unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus
// EscapeControlReader escapes the Unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus
func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
if !setting.UI.AmbiguousUnicodeDetection {
_, err = io.Copy(writer, reader)
-218
View File
@@ -1,218 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"bufio"
"bytes"
"context"
"io"
"os"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
// BlamePart represents block of blame - continuous lines with one sha
type BlamePart struct {
Sha string
Lines []string
PreviousSha string
PreviousPath string
}
// BlameReader returns part of file blame one by one
type BlameReader struct {
output io.WriteCloser
reader io.ReadCloser
bufferedReader *bufio.Reader
done chan error
lastSha *string
ignoreRevsFile string
objectFormat ObjectFormat
cleanupFuncs []func()
}
func (r *BlameReader) UsesIgnoreRevs() bool {
return r.ignoreRevsFile != ""
}
// NextPart returns next part of blame (sequential code lines with the same commit)
func (r *BlameReader) NextPart() (*BlamePart, error) {
var blamePart *BlamePart
if r.lastSha != nil {
blamePart = &BlamePart{
Sha: *r.lastSha,
Lines: make([]string, 0),
}
}
const previousHeader = "previous "
var lineBytes []byte
var isPrefix bool
var err error
for err != io.EOF {
lineBytes, isPrefix, err = r.bufferedReader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
if len(lineBytes) == 0 {
// isPrefix will be false
continue
}
var objectID string
objectFormatLength := r.objectFormat.FullLength()
if len(lineBytes) > objectFormatLength && lineBytes[objectFormatLength] == ' ' && r.objectFormat.IsValid(string(lineBytes[0:objectFormatLength])) {
objectID = string(lineBytes[0:objectFormatLength])
}
if len(objectID) > 0 {
if blamePart == nil {
blamePart = &BlamePart{
Sha: objectID,
Lines: make([]string, 0),
}
}
if blamePart.Sha != objectID {
r.lastSha = &objectID
// need to munch to end of line...
for isPrefix {
_, isPrefix, err = r.bufferedReader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
}
return blamePart, nil
}
} else if lineBytes[0] == '\t' {
blamePart.Lines = append(blamePart.Lines, string(lineBytes[1:]))
} else if bytes.HasPrefix(lineBytes, []byte(previousHeader)) {
offset := len(previousHeader) // already includes a space
blamePart.PreviousSha = string(lineBytes[offset : offset+objectFormatLength])
offset += objectFormatLength + 1 // +1 for space
blamePart.PreviousPath = string(lineBytes[offset:])
}
// need to munch to end of line...
for isPrefix {
_, isPrefix, err = r.bufferedReader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
}
}
r.lastSha = nil
return blamePart, nil
}
// Close BlameReader - don't run NextPart after invoking that
func (r *BlameReader) Close() error {
if r.bufferedReader == nil {
return nil
}
err := <-r.done
r.bufferedReader = nil
_ = r.reader.Close()
_ = r.output.Close()
for _, cleanup := range r.cleanupFuncs {
if cleanup != nil {
cleanup()
}
}
return err
}
// CreateBlameReader creates reader for given repository, commit and file
func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, err error) {
var ignoreRevsFileName string
var ignoreRevsFileCleanup func()
defer func() {
if err != nil && ignoreRevsFileCleanup != nil {
ignoreRevsFileCleanup()
}
}()
cmd := gitcmd.NewCommand("blame", "--porcelain")
if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
ignoreRevsFileName, ignoreRevsFileCleanup, err = tryCreateBlameIgnoreRevsFile(commit)
if err != nil && !IsErrNotExist(err) {
return nil, err
}
if ignoreRevsFileName != "" {
// Possible improvement: use --ignore-revs-file /dev/stdin on unix
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
cmd.AddOptionValues("--ignore-revs-file", ignoreRevsFileName)
}
}
cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
done := make(chan error, 1)
reader, stdout, err := os.Pipe()
if err != nil {
return nil, err
}
go func() {
stderr := bytes.Buffer{}
// TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
err := cmd.WithDir(repoPath).
WithUseContextTimeout(true).
WithStdout(stdout).
WithStderr(&stderr).
Run(ctx)
done <- err
_ = stdout.Close()
if err != nil {
log.Error("Error running git blame (dir: %v): %v, stderr: %v", repoPath, err, stderr.String())
}
}()
bufferedReader := bufio.NewReader(reader)
return &BlameReader{
output: stdout,
reader: reader,
bufferedReader: bufferedReader,
done: done,
ignoreRevsFile: ignoreRevsFileName,
objectFormat: objectFormat,
cleanupFuncs: []func(){ignoreRevsFileCleanup},
}, nil
}
func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func(), error) {
entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs")
if err != nil {
return "", nil, err
}
r, err := entry.Blob().DataAsync()
if err != nil {
return "", nil, err
}
defer r.Close()
f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs")
if err != nil {
return "", nil, err
}
filename := f.Name()
_, err = io.Copy(f, r)
_ = f.Close()
if err != nil {
cleanup()
return "", nil, err
}
return filename, cleanup, nil
}
-8
View File
@@ -323,14 +323,6 @@ func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, err
return strings.TrimSpace(commitID), nil
}
// GetRepositoryDefaultPublicGPGKey returns the default public key for this commit
func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) {
if c.repo == nil {
return nil, nil
}
return c.repo.GetDefaultPublicGPGKey(forceUpdate)
}
func IsStringLikelyCommitID(objFmt ObjectFormat, s string, minLength ...int) bool {
maxLen := 64 // sha256
if objFmt != nil {
+7 -2
View File
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
// RawDiffType type of a raw diff.
@@ -47,7 +48,9 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
switch diffType {
case RawDiffNormal:
if len(startCommit) != 0 {
cmd.AddArguments("diff", "-M").AddDynamicArguments(startCommit, endCommit).AddDashesAndList(files...)
cmd.AddArguments("diff").
AddOptionFormat("--find-renames=%s", setting.Git.DiffRenameSimilarityThreshold).
AddDynamicArguments(startCommit, endCommit).AddDashesAndList(files...)
} else if commit.ParentCount() == 0 {
cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...)
} else {
@@ -55,7 +58,9 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
if err != nil {
return err
}
cmd.AddArguments("diff", "-M").AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...)
cmd.AddArguments("diff").
AddOptionFormat("--find-renames=%s", setting.Git.DiffRenameSimilarityThreshold).
AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...)
}
case RawDiffPatch:
if len(startCommit) != 0 {
+3 -3
View File
@@ -75,9 +75,9 @@ func (f Format) Parser(r io.Reader) *Parser {
// hexEscaped produces hex-escpaed characters from a string. For example, "\n\0"
// would turn into "%0a%00".
func (f Format) hexEscaped(delim []byte) string {
escaped := ""
var escaped strings.Builder
for i := range delim {
escaped += "%" + hex.EncodeToString([]byte{delim[i]})
escaped.WriteString("%" + hex.EncodeToString([]byte{delim[i]}))
}
return escaped
return escaped.String()
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"fmt"
"os"
"strings"
"sync"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/process"
)
// GPGSettings represents the default GPG settings for this repository
type GPGSettings struct {
Sign bool
KeyID string
Email string
Name string
PublicKeyContent string
Format string
}
// LoadPublicKeyContent will load the key from gpg
func (gpgSettings *GPGSettings) LoadPublicKeyContent() error {
if gpgSettings.PublicKeyContent != "" {
return nil
}
if gpgSettings.Format == SigningKeyFormatSSH {
content, err := os.ReadFile(gpgSettings.KeyID)
if err != nil {
return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err)
}
gpgSettings.PublicKeyContent = string(content)
return nil
}
content, stderr, err := process.GetManager().Exec(
"gpg -a --export",
"gpg", "-a", "--export", gpgSettings.KeyID)
if err != nil {
return fmt.Errorf("unable to get default signing key: %s, %s, %w", gpgSettings.KeyID, stderr, err)
}
gpgSettings.PublicKeyContent = content
return nil
}
var (
loadPublicGPGKeyMutex sync.RWMutex
globalGPGSettings *GPGSettings
)
// GetDefaultPublicGPGKey will return and cache the default public GPG settings
func GetDefaultPublicGPGKey(ctx context.Context, forceUpdate bool) (*GPGSettings, error) {
if !forceUpdate {
loadPublicGPGKeyMutex.RLock()
if globalGPGSettings != nil {
defer loadPublicGPGKeyMutex.RUnlock()
return globalGPGSettings, nil
}
loadPublicGPGKeyMutex.RUnlock()
}
loadPublicGPGKeyMutex.Lock()
defer loadPublicGPGKeyMutex.Unlock()
if globalGPGSettings != nil && !forceUpdate {
return globalGPGSettings, nil
}
globalGPGSettings = &GPGSettings{
Sign: true,
}
value, _, _ := gitcmd.NewCommand("config", "--global", "--get", "commit.gpgsign").RunStdString(ctx)
sign, valid := ParseBool(strings.TrimSpace(value))
if !sign || !valid {
globalGPGSettings.Sign = false
return globalGPGSettings, nil
}
signingKey, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.signingkey").RunStdString(ctx)
globalGPGSettings.KeyID = strings.TrimSpace(signingKey)
format, _, _ := gitcmd.NewCommand("config", "--global", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(ctx)
globalGPGSettings.Format = strings.TrimSpace(format)
defaultEmail, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.email").RunStdString(ctx)
globalGPGSettings.Email = strings.TrimSpace(defaultEmail)
defaultName, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.name").RunStdString(ctx)
globalGPGSettings.Name = strings.TrimSpace(defaultName)
if err := globalGPGSettings.LoadPublicKeyContent(); err != nil {
return nil, err
}
return globalGPGSettings, nil
}
+6 -6
View File
@@ -32,23 +32,23 @@ func (s *SigningKey) String() string {
}
// GetSigningKey returns the KeyID and git Signature for the repo
func GetSigningKey(ctx context.Context, repoPath string) (*SigningKey, *Signature) {
func GetSigningKey(ctx context.Context) (*SigningKey, *Signature) {
if setting.Repository.Signing.SigningKey == "none" {
return nil, nil
}
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
// Can ignore the error here as it means that commit.gpgsign is not set
value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").WithDir(repoPath).RunStdString(ctx)
value, _, _ := gitcmd.NewCommand("config", "--global", "--get", "commit.gpgsign").RunStdString(ctx)
sign, valid := ParseBool(strings.TrimSpace(value))
if !sign || !valid {
return nil, nil
}
format, _, _ := gitcmd.NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").WithDir(repoPath).RunStdString(ctx)
signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").WithDir(repoPath).RunStdString(ctx)
signingName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").WithDir(repoPath).RunStdString(ctx)
signingEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").WithDir(repoPath).RunStdString(ctx)
format, _, _ := gitcmd.NewCommand("config", "--global", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(ctx)
signingKey, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.signingkey").RunStdString(ctx)
signingName, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.name").RunStdString(ctx)
signingEmail, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.email").RunStdString(ctx)
if strings.TrimSpace(signingKey) == "" {
return nil, nil
-10
View File
@@ -20,16 +20,6 @@ import (
"code.gitea.io/gitea/modules/proxy"
)
// GPGSettings represents the default GPG settings for this repository
type GPGSettings struct {
Sign bool
KeyID string
Email string
Name string
PublicKeyContent string
Format string
}
const prettyLogFormat = `--pretty=format:%H`
func (repo *Repository) ShowPrettyFormatLogToList(ctx context.Context, revisionRange string) ([]*Commit, error) {
-1
View File
@@ -32,7 +32,6 @@ type Repository struct {
gogitRepo *gogit.Repository
gogitStorage *filesystem.Storage
gpgSettings *GPGSettings
Ctx context.Context
LastCommitCache *LastCommitCache
-2
View File
@@ -23,8 +23,6 @@ type Repository struct {
tagCache *ObjectCache[*Tag]
gpgSettings *GPGSettings
batchInUse bool
batch *Batch
-71
View File
@@ -1,71 +0,0 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"fmt"
"os"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/process"
)
// LoadPublicKeyContent will load the key from gpg
func (gpgSettings *GPGSettings) LoadPublicKeyContent() error {
if gpgSettings.Format == SigningKeyFormatSSH {
content, err := os.ReadFile(gpgSettings.KeyID)
if err != nil {
return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err)
}
gpgSettings.PublicKeyContent = string(content)
return nil
}
content, stderr, err := process.GetManager().Exec(
"gpg -a --export",
"gpg", "-a", "--export", gpgSettings.KeyID)
if err != nil {
return fmt.Errorf("unable to get default signing key: %s, %s, %w", gpgSettings.KeyID, stderr, err)
}
gpgSettings.PublicKeyContent = content
return nil
}
// GetDefaultPublicGPGKey will return and cache the default public GPG settings for this repository
func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) {
if repo.gpgSettings != nil && !forceUpdate {
return repo.gpgSettings, nil
}
gpgSettings := &GPGSettings{
Sign: true,
}
value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").WithDir(repo.Path).RunStdString(repo.Ctx)
sign, valid := ParseBool(strings.TrimSpace(value))
if !sign || !valid {
gpgSettings.Sign = false
repo.gpgSettings = gpgSettings
return gpgSettings, nil
}
signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").WithDir(repo.Path).RunStdString(repo.Ctx)
gpgSettings.KeyID = strings.TrimSpace(signingKey)
format, _, _ := gitcmd.NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").WithDir(repo.Path).RunStdString(repo.Ctx)
gpgSettings.Format = strings.TrimSpace(format)
defaultEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").WithDir(repo.Path).RunStdString(repo.Ctx)
gpgSettings.Email = strings.TrimSpace(defaultEmail)
defaultName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").WithDir(repo.Path).RunStdString(repo.Ctx)
gpgSettings.Name = strings.TrimSpace(defaultName)
if err := gpgSettings.LoadPublicKeyContent(); err != nil {
return nil, err
}
repo.gpgSettings = gpgSettings
return repo.gpgSettings, nil
}
+208
View File
@@ -4,9 +4,16 @@
package gitrepo
import (
"bufio"
"bytes"
"context"
"io"
"os"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
func LineBlame(ctx context.Context, repo Repository, revision, file string, line uint) (string, error) {
@@ -16,3 +23,204 @@ func LineBlame(ctx context.Context, repo Repository, revision, file string, line
AddOptionValues("-p", revision).
AddDashesAndList(file))
}
// BlamePart represents block of blame - continuous lines with one sha
type BlamePart struct {
Sha string
Lines []string
PreviousSha string
PreviousPath string
}
// BlameReader returns part of file blame one by one
type BlameReader struct {
output io.WriteCloser
reader io.ReadCloser
bufferedReader *bufio.Reader
done chan error
lastSha *string
ignoreRevsFile string
objectFormat git.ObjectFormat
cleanupFuncs []func()
}
func (r *BlameReader) UsesIgnoreRevs() bool {
return r.ignoreRevsFile != ""
}
// NextPart returns next part of blame (sequential code lines with the same commit)
func (r *BlameReader) NextPart() (*BlamePart, error) {
var blamePart *BlamePart
if r.lastSha != nil {
blamePart = &BlamePart{
Sha: *r.lastSha,
Lines: make([]string, 0),
}
}
const previousHeader = "previous "
var lineBytes []byte
var isPrefix bool
var err error
for err != io.EOF {
lineBytes, isPrefix, err = r.bufferedReader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
if len(lineBytes) == 0 {
// isPrefix will be false
continue
}
var objectID string
objectFormatLength := r.objectFormat.FullLength()
if len(lineBytes) > objectFormatLength && lineBytes[objectFormatLength] == ' ' && r.objectFormat.IsValid(string(lineBytes[0:objectFormatLength])) {
objectID = string(lineBytes[0:objectFormatLength])
}
if len(objectID) > 0 {
if blamePart == nil {
blamePart = &BlamePart{
Sha: objectID,
Lines: make([]string, 0),
}
}
if blamePart.Sha != objectID {
r.lastSha = &objectID
// need to munch to end of line...
for isPrefix {
_, isPrefix, err = r.bufferedReader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
}
return blamePart, nil
}
} else if lineBytes[0] == '\t' {
blamePart.Lines = append(blamePart.Lines, string(lineBytes[1:]))
} else if bytes.HasPrefix(lineBytes, []byte(previousHeader)) {
offset := len(previousHeader) // already includes a space
blamePart.PreviousSha = string(lineBytes[offset : offset+objectFormatLength])
offset += objectFormatLength + 1 // +1 for space
blamePart.PreviousPath = string(lineBytes[offset:])
}
// need to munch to end of line...
for isPrefix {
_, isPrefix, err = r.bufferedReader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
}
}
r.lastSha = nil
return blamePart, nil
}
// Close BlameReader - don't run NextPart after invoking that
func (r *BlameReader) Close() error {
if r.bufferedReader == nil {
return nil
}
err := <-r.done
r.bufferedReader = nil
_ = r.reader.Close()
_ = r.output.Close()
for _, cleanup := range r.cleanupFuncs {
if cleanup != nil {
cleanup()
}
}
return err
}
// CreateBlameReader creates reader for given repository, commit and file
func CreateBlameReader(ctx context.Context, objectFormat git.ObjectFormat, repo Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, err error) {
var ignoreRevsFileName string
var ignoreRevsFileCleanup func()
defer func() {
if err != nil && ignoreRevsFileCleanup != nil {
ignoreRevsFileCleanup()
}
}()
cmd := gitcmd.NewCommand("blame", "--porcelain")
if git.DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
ignoreRevsFileName, ignoreRevsFileCleanup, err = tryCreateBlameIgnoreRevsFile(commit)
if err != nil && !git.IsErrNotExist(err) {
return nil, err
}
if ignoreRevsFileName != "" {
// Possible improvement: use --ignore-revs-file /dev/stdin on unix
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
cmd.AddOptionValues("--ignore-revs-file", ignoreRevsFileName)
}
}
cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
done := make(chan error, 1)
reader, stdout, err := os.Pipe()
if err != nil {
return nil, err
}
go func() {
stderr := bytes.Buffer{}
// TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
err := RunCmd(ctx, repo, cmd.WithUseContextTimeout(true).
WithStdout(stdout).
WithStderr(&stderr),
)
done <- err
_ = stdout.Close()
if err != nil {
log.Error("Error running git blame (dir: %v): %v, stderr: %v", repoPath, err, stderr.String())
}
}()
bufferedReader := bufio.NewReader(reader)
return &BlameReader{
output: stdout,
reader: reader,
bufferedReader: bufferedReader,
done: done,
ignoreRevsFile: ignoreRevsFileName,
objectFormat: objectFormat,
cleanupFuncs: []func(){ignoreRevsFileCleanup},
}, nil
}
func tryCreateBlameIgnoreRevsFile(commit *git.Commit) (string, func(), error) {
entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs")
if err != nil {
return "", nil, err
}
r, err := entry.Blob().DataAsync()
if err != nil {
return "", nil, err
}
defer r.Close()
f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs")
if err != nil {
return "", nil, err
}
filename := f.Name()
_, err = io.Copy(f, r)
_ = f.Close()
if err != nil {
cleanup()
return "", nil, err
}
return filename, cleanup, nil
}
@@ -1,12 +1,13 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
package gitrepo
import (
"context"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@@ -17,13 +18,14 @@ func TestReadingBlameOutputSha256(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
if isGogit {
if git.DefaultFeatures().UsingGogit {
t.Skip("Skipping test since gogit does not support sha256")
return
}
t.Run("Without .git-blame-ignore-revs", func(t *testing.T) {
repo, err := OpenRepository(ctx, "./tests/repos/repo5_pulls_sha256")
storage := &mockRepository{path: "repo5_pulls_sha256"}
repo, err := OpenRepository(ctx, storage)
assert.NoError(t, err)
defer repo.Close()
@@ -47,7 +49,7 @@ func TestReadingBlameOutputSha256(t *testing.T) {
}
for _, bypass := range []bool{false, true} {
blameReader, err := CreateBlameReader(ctx, Sha256ObjectFormat, "./tests/repos/repo5_pulls_sha256", commit, "README.md", bypass)
blameReader, err := CreateBlameReader(ctx, git.Sha256ObjectFormat, storage, commit, "README.md", bypass)
assert.NoError(t, err)
assert.NotNil(t, blameReader)
defer blameReader.Close()
@@ -68,7 +70,8 @@ func TestReadingBlameOutputSha256(t *testing.T) {
})
t.Run("With .git-blame-ignore-revs", func(t *testing.T) {
repo, err := OpenRepository(ctx, "./tests/repos/repo6_blame_sha256")
storage := &mockRepository{path: "repo6_blame_sha256"}
repo, err := OpenRepository(ctx, storage)
assert.NoError(t, err)
defer repo.Close()
@@ -131,7 +134,7 @@ func TestReadingBlameOutputSha256(t *testing.T) {
for _, c := range cases {
commit, err := repo.GetCommit(c.CommitID)
assert.NoError(t, err)
blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass)
blameReader, err := CreateBlameReader(ctx, objectFormat, storage, commit, "blame.txt", c.Bypass)
assert.NoError(t, err)
assert.NotNil(t, blameReader)
defer blameReader.Close()
@@ -1,12 +1,13 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
package gitrepo
import (
"context"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@@ -18,10 +19,10 @@ func TestReadingBlameOutput(t *testing.T) {
defer cancel()
t.Run("Without .git-blame-ignore-revs", func(t *testing.T) {
repo, err := OpenRepository(ctx, "./tests/repos/repo5_pulls")
storage := &mockRepository{path: "repo5_pulls"}
repo, err := OpenRepository(ctx, storage)
assert.NoError(t, err)
defer repo.Close()
commit, err := repo.GetCommit("f32b0a9dfd09a60f616f29158f772cedd89942d2")
assert.NoError(t, err)
@@ -42,7 +43,7 @@ func TestReadingBlameOutput(t *testing.T) {
}
for _, bypass := range []bool{false, true} {
blameReader, err := CreateBlameReader(ctx, Sha1ObjectFormat, "./tests/repos/repo5_pulls", commit, "README.md", bypass)
blameReader, err := CreateBlameReader(ctx, git.Sha1ObjectFormat, storage, commit, "README.md", bypass)
assert.NoError(t, err)
assert.NotNil(t, blameReader)
defer blameReader.Close()
@@ -63,7 +64,8 @@ func TestReadingBlameOutput(t *testing.T) {
})
t.Run("With .git-blame-ignore-revs", func(t *testing.T) {
repo, err := OpenRepository(ctx, "./tests/repos/repo6_blame")
storage := &mockRepository{path: "repo6_blame"}
repo, err := OpenRepository(ctx, storage)
assert.NoError(t, err)
defer repo.Close()
@@ -127,7 +129,7 @@ func TestReadingBlameOutput(t *testing.T) {
commit, err := repo.GetCommit(c.CommitID)
assert.NoError(t, err)
blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
blameReader, err := CreateBlameReader(ctx, objectFormat, storage, commit, "blame.txt", c.Bypass)
assert.NoError(t, err)
assert.NotNil(t, blameReader)
defer blameReader.Close()
+2 -2
View File
@@ -109,8 +109,8 @@ func IsRepoDirExist(ctx context.Context, repo Repository, relativeDirPath string
return util.IsDir(absoluteDirPath)
}
func RemoveRepoFile(ctx context.Context, repo Repository, relativeFilePath string) error {
absoluteFilePath := filepath.Join(repoPath(repo), relativeFilePath)
func RemoveRepoFileOrDir(ctx context.Context, repo Repository, relativeFileOrDirPath string) error {
absoluteFilePath := filepath.Join(repoPath(repo), relativeFileOrDirPath)
return util.Remove(absoluteFilePath)
}
+2 -2
View File
@@ -9,6 +9,6 @@ import (
"code.gitea.io/gitea/modules/git"
)
func GetSigningKey(ctx context.Context, repo Repository) (*git.SigningKey, *git.Signature) {
return git.GetSigningKey(ctx, repoPath(repo))
func GetSigningKey(ctx context.Context) (*git.SigningKey, *git.Signature) {
return git.GetSigningKey(ctx)
}
+33 -1
View File
@@ -56,7 +56,39 @@ func NewContext() {
})
}
// Code returns a HTML version of code string with chroma syntax highlighting classes and the matched lexer name
// UnsafeSplitHighlightedLines splits highlighted code into lines preserving HTML tags
// It always includes '\n', '\n' can appear at the end of each line or in the middle of HTML tags
// The '\n' is necessary for copying code from web UI to preserve original code lines
// ATTENTION: It uses the unsafe conversion between string and []byte for performance reason
// DO NOT make any modification to the returned [][]byte slice items
func UnsafeSplitHighlightedLines(code template.HTML) (ret [][]byte) {
buf := util.UnsafeStringToBytes(string(code))
lineCount := bytes.Count(buf, []byte("\n")) + 1
ret = make([][]byte, 0, lineCount)
nlTagClose := []byte("\n</")
for {
pos := bytes.IndexByte(buf, '\n')
if pos == -1 {
if len(buf) > 0 {
ret = append(ret, buf)
}
return ret
}
// Chroma highlighting output sometimes have "</span>" right after \n, sometimes before.
// * "<span>text\n</span>"
// * "<span>text</span>\n"
if bytes.HasPrefix(buf[pos:], nlTagClose) {
pos1 := bytes.IndexByte(buf[pos:], '>')
if pos1 != -1 {
pos += pos1
}
}
ret = append(ret, buf[:pos+1])
buf = buf[pos+1:]
}
}
// Code returns an HTML version of code string with chroma syntax highlighting classes and the matched lexer name
func Code(fileName, language, code string) (output template.HTML, lexerName string) {
NewContext()
+18
View File
@@ -181,3 +181,21 @@ c=2`),
})
}
}
func TestUnsafeSplitHighlightedLines(t *testing.T) {
ret := UnsafeSplitHighlightedLines("")
assert.Empty(t, ret)
ret = UnsafeSplitHighlightedLines("a")
assert.Len(t, ret, 1)
assert.Equal(t, "a", string(ret[0]))
ret = UnsafeSplitHighlightedLines("\n")
assert.Len(t, ret, 1)
assert.Equal(t, "\n", string(ret[0]))
ret = UnsafeSplitHighlightedLines("<span>a</span>\n<span>b\n</span>")
assert.Len(t, ret, 2)
assert.Equal(t, "<span>a</span>\n", string(ret[0]))
assert.Equal(t, "<span>b\n</span>", string(ret[1]))
}
+1 -6
View File
@@ -19,7 +19,6 @@ import (
charsetModule "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
@@ -109,11 +108,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byt
}
if isPlain {
charset, err := charsetModule.DetectEncoding(mineBuf)
if err != nil {
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", opts.Filename, err)
charset = "utf-8"
}
charset, _ := charsetModule.DetectEncoding(mineBuf)
opts.ContentTypeCharset = strings.ToLower(charset)
}
+1 -1
View File
@@ -203,7 +203,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
RepoID: repo.ID,
CommitID: commitSha,
Filename: update.Filename,
Content: string(charset.ToUTF8DropErrors(fileContents, charset.ConvertOpts{})),
Content: string(charset.ToUTF8DropErrors(fileContents)),
Language: analyze.GetCodeLanguage(update.Filename, fileContents),
UpdatedAt: time.Now().UTC(),
})
@@ -191,7 +191,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
Doc(map[string]any{
"repo_id": repo.ID,
"filename": update.Filename,
"content": string(charset.ToUTF8DropErrors(fileContents, charset.ConvertOpts{})),
"content": string(charset.ToUTF8DropErrors(fileContents)),
"commit_id": sha,
"language": analyze.GetCodeLanguage(update.Filename, fileContents),
"updated_at": timeutil.TimeStampNow(),
@@ -30,7 +30,11 @@ func TestElasticsearchIndexer(t *testing.T) {
require.Eventually(t, func() bool {
resp, err := http.Get(url)
return err == nil && resp.StatusCode == http.StatusOK
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}, time.Minute, time.Second, "Expected elasticsearch to be up")
indexer := NewIndexer(url, fmt.Sprintf("test_elasticsearch_indexer_%d", time.Now().Unix()))
@@ -36,7 +36,11 @@ func TestMeilisearchIndexer(t *testing.T) {
require.Eventually(t, func() bool {
resp, err := http.Get(url)
return err == nil && resp.StatusCode == http.StatusOK
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}, time.Minute, time.Second, "Expected meilisearch to be up")
indexer := NewIndexer(url, key, fmt.Sprintf("test_meilisearch_indexer_%d", time.Now().Unix()))
+1
View File
@@ -14,6 +14,7 @@ type Uploader interface {
CreateMilestones(ctx context.Context, milestones ...*Milestone) error
CreateReleases(ctx context.Context, releases ...*Release) error
SyncTags(ctx context.Context) error
SyncBranches(ctx context.Context) error
CreateLabels(ctx context.Context, labels ...*Label) error
CreateIssues(ctx context.Context, issues ...*Issue) error
CreateComments(ctx context.Context, comments ...*Comment) error
+23 -2
View File
@@ -62,7 +62,28 @@ type PackageMetadata struct {
Author User `json:"author"`
ReadmeFilename string `json:"readmeFilename,omitempty"`
Users map[string]bool `json:"users,omitempty"`
License string `json:"license,omitempty"`
License License `json:"license,omitempty"`
}
type License string
func (l *License) UnmarshalJSON(data []byte) error {
switch data[0] {
case '"':
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
*l = License(value)
case '{':
var values map[string]any
if err := json.Unmarshal(data, &values); err != nil {
return err
}
value, _ := values["type"].(string)
*l = License(value)
}
return nil
}
// PackageMetadataVersion documentation: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
@@ -74,7 +95,7 @@ type PackageMetadataVersion struct {
Description string `json:"description"`
Author User `json:"author"`
Homepage string `json:"homepage,omitempty"`
License string `json:"license,omitempty"`
License License `json:"license,omitempty"`
Repository Repository `json:"repository"`
Keywords []string `json:"keywords,omitempty"`
Dependencies map[string]string `json:"dependencies,omitempty"`
+27 -1
View File
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParsePackage(t *testing.T) {
@@ -291,11 +292,36 @@ func TestParsePackage(t *testing.T) {
assert.Equal(t, packageDescription, p.Metadata.Readme)
assert.Equal(t, packageAuthor, p.Metadata.Author)
assert.Equal(t, packageBin, p.Metadata.Bin["bin"])
assert.Equal(t, "MIT", p.Metadata.License)
assert.Equal(t, "MIT", string(p.Metadata.License))
assert.Equal(t, "https://gitea.io/", p.Metadata.ProjectURL)
assert.Contains(t, p.Metadata.Dependencies, "package")
assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"])
assert.Equal(t, repository.Type, p.Metadata.Repository.Type)
assert.Equal(t, repository.URL, p.Metadata.Repository.URL)
})
t.Run("ValidLicenseMap", func(t *testing.T) {
packageJSON := `{
"versions": {
"0.1.1": {
"name": "dev-null",
"version": "0.1.1",
"license": {
"type": "MIT"
},
"dist": {
"integrity": "sha256-"
}
}
},
"_attachments": {
"foo": {
"data": "AAAA"
}
}
}`
p, err := ParsePackage(strings.NewReader(packageJSON))
require.NoError(t, err)
require.Equal(t, "MIT", string(p.Metadata.License))
})
}
+1 -1
View File
@@ -12,7 +12,7 @@ type Metadata struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Author string `json:"author,omitempty"`
License string `json:"license,omitempty"`
License License `json:"license,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Dependencies map[string]string `json:"dependencies,omitempty"`
+4 -3
View File
@@ -4,6 +4,7 @@
package setting
import (
"strings"
"sync"
"code.gitea.io/gitea/modules/log"
@@ -23,11 +24,11 @@ type OpenWithEditorApp struct {
type OpenWithEditorAppsType []OpenWithEditorApp
func (t OpenWithEditorAppsType) ToTextareaString() string {
ret := ""
var ret strings.Builder
for _, app := range t {
ret += app.DisplayName + " = " + app.OpenURL + "\n"
ret.WriteString(app.DisplayName + " = " + app.OpenURL + "\n")
}
return ret
return ret.String()
}
func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
+35 -27
View File
@@ -5,6 +5,7 @@ package setting
import (
"path/filepath"
"regexp"
"strings"
"time"
@@ -17,20 +18,21 @@ var Git = struct {
HomePath string
DisableDiffHighlight bool
MaxGitDiffLines int
MaxGitDiffLineCharacters int
MaxGitDiffFiles int
CommitsRangeSize int // CommitsRangeSize the default commits range size
BranchesRangeSize int // BranchesRangeSize the default branches range size
VerbosePush bool
VerbosePushDelay time.Duration
GCArgs []string `ini:"GC_ARGS" delim:" "`
EnableAutoGitWireProtocol bool
PullRequestPushMessage bool
LargeObjectThreshold int64
DisableCoreProtectNTFS bool
DisablePartialClone bool
Timeout struct {
MaxGitDiffLines int
MaxGitDiffLineCharacters int
MaxGitDiffFiles int
CommitsRangeSize int // CommitsRangeSize the default commits range size
BranchesRangeSize int // BranchesRangeSize the default branches range size
VerbosePush bool
VerbosePushDelay time.Duration
GCArgs []string `ini:"GC_ARGS" delim:" "`
EnableAutoGitWireProtocol bool
PullRequestPushMessage bool
LargeObjectThreshold int64
DisableCoreProtectNTFS bool
DisablePartialClone bool
DiffRenameSimilarityThreshold string
Timeout struct {
Default int
Migrate int
Mirror int
@@ -39,19 +41,20 @@ var Git = struct {
GC int `ini:"GC"`
} `ini:"git.timeout"`
}{
DisableDiffHighlight: false,
MaxGitDiffLines: 1000,
MaxGitDiffLineCharacters: 5000,
MaxGitDiffFiles: 100,
CommitsRangeSize: 50,
BranchesRangeSize: 20,
VerbosePush: true,
VerbosePushDelay: 5 * time.Second,
GCArgs: []string{},
EnableAutoGitWireProtocol: true,
PullRequestPushMessage: true,
LargeObjectThreshold: 1024 * 1024,
DisablePartialClone: false,
DisableDiffHighlight: false,
MaxGitDiffLines: 1000,
MaxGitDiffLineCharacters: 5000,
MaxGitDiffFiles: 100,
CommitsRangeSize: 50,
BranchesRangeSize: 20,
VerbosePush: true,
VerbosePushDelay: 5 * time.Second,
GCArgs: []string{},
EnableAutoGitWireProtocol: true,
PullRequestPushMessage: true,
LargeObjectThreshold: 1024 * 1024,
DisablePartialClone: false,
DiffRenameSimilarityThreshold: "50%",
Timeout: struct {
Default int
Migrate int
@@ -117,4 +120,9 @@ func loadGitFrom(rootCfg ConfigProvider) {
} else {
Git.HomePath = filepath.Clean(Git.HomePath)
}
// validate for a integer percentage between 0% and 100%
if !regexp.MustCompile(`^([0-9]|[1-9][0-9]|100)%$`).MatchString(Git.DiffRenameSimilarityThreshold) {
log.Fatal("Invalid git.DIFF_RENAME_SIMILARITY_THRESHOLD: %s", Git.DiffRenameSimilarityThreshold)
}
}
+1
View File
@@ -240,4 +240,5 @@ func PanicInDevOrTesting(msg string, a ...any) {
if !IsProd || IsInTesting {
panic(fmt.Sprintf(msg, a...))
}
log.Error(msg, a...)
}
+5 -4
View File
@@ -249,17 +249,18 @@ func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:rev
func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML {
isPullRequest := issue != nil && issue.IsPull
baseLink := fmt.Sprintf("%s/%s", repoLink, util.Iif(isPullRequest, "pulls", "issues"))
htmlCode := `<span class="labels-list">`
var htmlCode strings.Builder
htmlCode.WriteString(`<span class="labels-list">`)
for _, label := range labels {
// Protect against nil value in labels - shouldn't happen but would cause a panic if so
if label == nil {
continue
}
link := fmt.Sprintf("%s?labels=%d", baseLink, label.ID)
htmlCode += string(ut.RenderLabelWithLink(label, template.URL(link)))
htmlCode.WriteString(string(ut.RenderLabelWithLink(label, template.URL(link))))
}
htmlCode += "</span>"
return template.HTML(htmlCode)
htmlCode.WriteString("</span>")
return template.HTML(htmlCode.String())
}
func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize int) template.HTML {
+9
View File
@@ -1306,6 +1306,7 @@ commit = Commit
release = Release
releases = Releases
tag = Tag
git_tag = Git Tag
released_this = released this
tagged_this = tagged this
file.title = %s at %s
@@ -1874,6 +1875,7 @@ pulls.desc = Enable pull requests and code reviews.
pulls.new = New Pull Request
pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
pulls.new.must_collaborator = You must be a collaborator to create pull request.
pulls.new.already_existed = A pull request between these branches already exists
pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes.
pulls.view = View Pull Request
pulls.compare_changes = New Pull Request
@@ -2767,6 +2769,13 @@ release.add_tag_msg = Use the title and content of release as tag message.
release.add_tag = Create Tag Only
release.releases_for = Releases for %s
release.tags_for = Tags for %s
release.notes = Release notes
release.generate_notes = Generate release notes
release.generate_notes_desc = Automatically add merged pull requests and a changelog link for this release.
release.previous_tag = Previous tag
release.generate_notes_tag_not_found = Tag "%s" does not exist in this repository.
release.generate_notes_target_not_found = The release target "%s" cannot be found.
release.generate_notes_missing_tag = Enter a tag name to generate release notes.
branch.name = Branch Name
branch.already_exists = A branch named "%s" already exists.
+4
View File
@@ -215,6 +215,7 @@ more=更多的
buttons.heading.tooltip=添加标题
buttons.bold.tooltip=添加粗体文本
buttons.italic.tooltip=添加斜体文本
buttons.strikethrough.tooltip=添加划线文本
buttons.quote.tooltip=引用文本
buttons.code.tooltip=添加代码
buttons.link.tooltip=添加链接
@@ -1355,8 +1356,11 @@ editor.this_file_locked=文件已锁定
editor.must_be_on_a_branch=您必须在某个分支上才能对此文件进行修改操作。
editor.fork_before_edit=您必须派生这个仓库才能对此文件进行修改操作。
editor.delete_this_file=删除文件
editor.delete_this_directory=删除目录
editor.must_have_write_access=您必须具有写权限才能对此文件进行修改操作。
editor.file_delete_success=文件「%s」已删除。
editor.directory_delete_success=目录「%s」已被删除。
editor.delete_directory=删除目录「%s」
editor.name_your_file=命名文件…
editor.filename_help=通过键入名称后跟斜线 ("/") 来添加目录。通过在输入框的开头键入「退格」来删除目录。
editor.or=
+19 -17
View File
@@ -1,6 +1,6 @@
{
"type": "module",
"packageManager": "pnpm@10.24.0",
"packageManager": "pnpm@10.26.0",
"engines": {
"node": ">= 22.6.0",
"pnpm": ">= 10.0.0"
@@ -12,7 +12,7 @@
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
"@github/paste-markdown": "1.5.3",
"@github/relative-time-element": "4.5.1",
"@github/relative-time-element": "5.0.0",
"@github/text-expander-element": "2.9.2",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.21.1",
@@ -21,11 +21,12 @@
"@techknowlogick/license-checker-webpack-plugin": "0.3.0",
"add-asset-webpack-plugin": "3.1.1",
"ansi_up": "6.0.6",
"asciinema-player": "3.12.1",
"asciinema-player": "3.13.5",
"chart.js": "4.5.1",
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.2.0",
"clippie": "4.1.9",
"compare-versions": "6.1.1",
"cropperjs": "1.6.2",
"css-loader": "7.1.2",
"dayjs": "1.11.19",
@@ -35,32 +36,31 @@
"htmx.org": "2.0.8",
"idiomorph": "0.7.4",
"jquery": "3.7.1",
"katex": "0.16.25",
"katex": "0.16.27",
"mermaid": "11.12.2",
"mini-css-extract-plugin": "2.9.4",
"monaco-editor": "0.55.1",
"monaco-editor-webpack-plugin": "7.1.1",
"online-3d-viewer": "0.16.0",
"online-3d-viewer": "0.17.0",
"pdfobject": "2.3.1",
"perfect-debounce": "2.0.0",
"postcss": "8.5.6",
"postcss-loader": "8.2.0",
"sortablejs": "1.15.6",
"swagger-ui-dist": "5.30.3",
"swagger-ui-dist": "5.31.0",
"tailwindcss": "3.4.17",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tippy.js": "6.3.7",
"toastify-js": "1.12.0",
"tributejs": "5.1.3",
"typescript": "5.9.3",
"uint8-to-base64": "0.2.1",
"vanilla-colorful": "0.7.2",
"vue": "3.5.25",
"vue-bar-graph": "2.2.0",
"vue-chartjs": "5.3.3",
"vue-loader": "17.4.2",
"webpack": "5.103.0",
"webpack": "5.104.0",
"webpack-cli": "6.0.1",
"wrap-ansi": "9.0.2"
},
@@ -79,10 +79,10 @@
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.4",
"@typescript-eslint/parser": "8.48.1",
"@vitejs/plugin-vue": "6.0.2",
"@vitest/eslint-plugin": "1.5.1",
"eslint": "9.39.1",
"@typescript-eslint/parser": "8.50.0",
"@vitejs/plugin-vue": "6.0.3",
"@vitest/eslint-plugin": "1.5.2",
"eslint": "9.39.2",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-array-func": "5.1.0",
"eslint-plugin-github": "6.0.0",
@@ -96,7 +96,8 @@
"eslint-plugin-wc": "3.0.2",
"globals": "16.5.0",
"happy-dom": "20.0.11",
"markdownlint-cli": "0.46.0",
"jiti": "2.6.1",
"markdownlint-cli": "0.47.0",
"material-icon-theme": "5.29.0",
"nolyfill": "1.0.44",
"postcss-html": "1.8.0",
@@ -107,11 +108,12 @@
"stylelint-declaration-strict-value": "1.10.11",
"stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "4.0.0",
"typescript-eslint": "8.48.1",
"updates": "17.0.4",
"typescript": "5.9.3",
"typescript-eslint": "8.50.0",
"updates": "17.0.7",
"vite-string-plugin": "1.4.9",
"vitest": "4.0.15",
"vue-tsc": "3.1.5"
"vitest": "4.0.16",
"vue-tsc": "3.1.8"
},
"browserslist": [
"defaults"
+823 -530
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -433,15 +433,16 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model.
}
func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) {
ret := "---\n"
var ret strings.Builder
ret.WriteString("---\n")
for _, v := range versions {
dep, err := makePackageVersionDependency(ctx, v, c)
if err != nil {
return "", err
}
ret += dep + "\n"
ret.WriteString(dep + "\n")
}
return ret, nil
return ret.String(), nil
}
func makeGemFullFileName(gemName, version, platform string) string {
+2 -2
View File
@@ -152,7 +152,7 @@ func repoAssignment() func(ctx *context.APIContext) {
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
context.RedirectToUser(ctx.Base, userName, redirectUserID)
context.RedirectToUser(ctx.Base, ctx.Doer, userName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.APIErrorNotFound("GetUserByName", err)
} else {
@@ -612,7 +612,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam("org"))
if err == nil {
context.RedirectToUser(ctx.Base, ctx.PathParam("org"), redirectUserID)
context.RedirectToUser(ctx.Base, ctx.Doer, ctx.PathParam("org"), redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.APIErrorNotFound("GetOrgByName", err)
} else {
+2 -7
View File
@@ -10,13 +10,8 @@ import (
)
func getSigningKey(ctx *context.APIContext, expectedFormat string) {
// if the handler is in the repo's route group, get the repo's signing key
// otherwise, get the global signing key
path := ""
if ctx.Repo != nil && ctx.Repo.Repository != nil {
path = ctx.Repo.Repository.RepoPath()
}
content, format, err := asymkey_service.PublicSigningKey(ctx, path)
// get the global signing key
content, format, err := asymkey_service.PublicSigningKey(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
+10 -14
View File
@@ -30,6 +30,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/context"
@@ -1082,7 +1083,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} else if len(headInfos) == 2 {
// There is a head repository (the head repository could also be the same base repo)
headRefToGuess = headInfos[1]
headUser, err = user_model.GetUserByName(ctx, headInfos[0])
headUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound("GetUserByName")
@@ -1098,28 +1099,23 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
isSameRepo := ctx.Repo.Owner.ID == headUser.ID
// Check if current user has fork of repository or in the same repository.
headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
if headRepo == nil && !isSameRepo {
err = baseRepo.GetBaseRepo(ctx)
var headRepo *repo_model.Repository
if isSameRepo {
headRepo = baseRepo
} else {
headRepo, err = common.FindHeadRepo(ctx, baseRepo, headUser.ID)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
// Check if baseRepo's base repository is the same as headUser's repository.
if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID {
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
ctx.APIErrorNotFound("GetBaseRepo")
if headRepo == nil {
ctx.APIErrorNotFound("head repository not found")
return nil, nil
}
// Assign headRepo so it can be used below.
headRepo = baseRepo.BaseRepo
}
var headGitRepo *git.Repository
if isSameRepo {
headRepo = ctx.Repo.Repository
headGitRepo = ctx.Repo.GitRepo
closer = func() {} // no need to close the head repo because it shares the base repo
} else {
@@ -1143,7 +1139,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
return nil, nil
}
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
if !permBase.CanRead(unit.TypeCode) {
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode")
return nil, nil
+1 -1
View File
@@ -16,7 +16,7 @@ func GetUserByPathParam(ctx *context.APIContext, name string) *user_model.User {
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err2 := user_model.LookupUserRedirect(ctx, username); err2 == nil {
context.RedirectToUser(ctx.Base, username, redirectUserID)
context.RedirectToUser(ctx.Base, ctx.Doer, username, redirectUserID)
} else {
ctx.APIErrorNotFound("GetUserByName", err)
}
+53
View File
@@ -4,6 +4,8 @@
package common
import (
"context"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
@@ -20,3 +22,54 @@ type CompareInfo struct {
HeadBranch string
DirectComparison bool
}
// maxForkTraverseLevel defines the maximum levels to traverse when searching for the head repository.
const maxForkTraverseLevel = 10
// FindHeadRepo tries to find the head repository based on the base repository and head user ID.
func FindHeadRepo(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64) (*repo_model.Repository, error) {
if baseRepo.IsFork {
curRepo := baseRepo
for curRepo.OwnerID != headUserID { // We assume the fork deepth is not too deep.
if err := curRepo.GetBaseRepo(ctx); err != nil {
return nil, err
}
if curRepo.BaseRepo == nil {
return findHeadRepoFromRootBase(ctx, curRepo, headUserID, maxForkTraverseLevel)
}
curRepo = curRepo.BaseRepo
}
return curRepo, nil
}
return findHeadRepoFromRootBase(ctx, baseRepo, headUserID, maxForkTraverseLevel)
}
func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64, traverseLevel int) (*repo_model.Repository, error) {
if traverseLevel == 0 {
return nil, nil
}
// test if we are lucky
repo, err := repo_model.GetUserFork(ctx, baseRepo.ID, headUserID)
if err != nil {
return nil, err
}
if repo != nil {
return repo, nil
}
firstLevelForkedRepos, err := repo_model.GetRepositoriesByForkID(ctx, baseRepo.ID)
if err != nil {
return nil, err
}
for _, repo := range firstLevelForkedRepos {
forked, err := findHeadRepoFromRootBase(ctx, repo, headUserID, traverseLevel-1)
if err != nil {
return nil, err
}
if forked != nil {
return forked, nil
}
}
return nil, nil
}
+3 -3
View File
@@ -179,11 +179,11 @@ func AuthorizeOAuth(ctx *context.Context) {
errs := binding.Errors{}
errs = form.Validate(ctx.Req, errs)
if len(errs) > 0 {
errstring := ""
var errstring strings.Builder
for _, e := range errs {
errstring += e.Error() + "\n"
errstring.WriteString(e.Error() + "\n")
}
ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring))
ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring.String()))
return
}
+62 -73
View File
@@ -4,8 +4,9 @@
package repo
import (
"bytes"
"fmt"
gotemplate "html/template"
"html/template"
"net/http"
"net/url"
"path"
@@ -16,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/languagestats"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -25,18 +27,17 @@ import (
)
type blameRow struct {
RowNumber int
Avatar gotemplate.HTML
RepoLink string
PartSha string
RowNumber int
Avatar template.HTML
PreviousSha string
PreviousShaURL string
IsFirstCommit bool
CommitURL string
CommitMessage string
CommitSince gotemplate.HTML
Code gotemplate.HTML
EscapeStatus *charset.EscapeStatus
CommitSince template.HTML
Code template.HTML
EscapeStatus *charset.EscapeStatus
}
// RefBlame render blame page
@@ -99,7 +100,7 @@ func RefBlame(ctx *context.Context) {
}
type blameResult struct {
Parts []*git.BlamePart
Parts []*gitrepo.BlamePart
UsesIgnoreRevs bool
FaultyIgnoreRevsFile bool
}
@@ -107,7 +108,7 @@ type blameResult struct {
func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
objectFormat := ctx.Repo.GetObjectFormat()
blameReader, err := git.CreateBlameReader(ctx, objectFormat, repo.RepoPath(), commit, file, bypassBlameIgnore)
blameReader, err := gitrepo.CreateBlameReader(ctx, objectFormat, repo, commit, file, bypassBlameIgnore)
if err != nil {
return nil, err
}
@@ -123,7 +124,7 @@ func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git
if len(r.Parts) == 0 && r.UsesIgnoreRevs {
// try again without ignored revs
blameReader, err = git.CreateBlameReader(ctx, objectFormat, repo.RepoPath(), commit, file, true)
blameReader, err = gitrepo.CreateBlameReader(ctx, objectFormat, repo, commit, file, true)
if err != nil {
return nil, err
}
@@ -143,12 +144,12 @@ func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git
return r, nil
}
func fillBlameResult(br *git.BlameReader, r *blameResult) error {
func fillBlameResult(br *gitrepo.BlameReader, r *blameResult) error {
r.UsesIgnoreRevs = br.UsesIgnoreRevs()
previousHelper := make(map[string]*git.BlamePart)
previousHelper := make(map[string]*gitrepo.BlamePart)
r.Parts = make([]*git.BlamePart, 0, 5)
r.Parts = make([]*gitrepo.BlamePart, 0, 5)
for {
blamePart, err := br.NextPart()
if err != nil {
@@ -173,7 +174,7 @@ func fillBlameResult(br *git.BlameReader, r *blameResult) error {
return nil
}
func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[string]*user_model.UserCommit {
func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) map[string]*user_model.UserCommit {
// store commit data by SHA to look up avatar info etc
commitNames := make(map[string]*user_model.UserCommit)
// and as blameParts can reference the same commits multiple
@@ -220,76 +221,64 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
return commitNames
}
func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
repoLink := ctx.Repo.RepoLink
func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.AvatarUtils, part *gitrepo.BlamePart, commit *user_model.UserCommit, br *blameRow) {
if commit.User != nil {
br.Avatar = avatarUtils.Avatar(commit.User, 18)
} else {
br.Avatar = avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18)
}
br.PreviousSha = part.PreviousSha
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
br.CommitMessage = commit.CommitMessage
br.CommitSince = templates.TimeSince(commit.Author.When)
}
func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNames map[string]*user_model.UserCommit) {
language, err := languagestats.GetFileLanguage(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
if err != nil {
log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
}
lines := make([]string, 0)
buf := &bytes.Buffer{}
rows := make([]*blameRow, 0)
avatarUtils := templates.NewAvatarUtils(ctx)
rowNumber := 0 // will be 1-based
for _, part := range blameParts {
for partLineIdx, line := range part.Lines {
rowNumber++
br := &blameRow{RowNumber: rowNumber}
rows = append(rows, br)
if int64(buf.Len()) < setting.UI.MaxDisplayFileSize {
buf.WriteString(line)
buf.WriteByte('\n')
}
if partLineIdx == 0 {
renderBlameFillFirstBlameRow(ctx.Repo.RepoLink, avatarUtils, part, commitNames[part.Sha], br)
}
}
}
escapeStatus := &charset.EscapeStatus{}
var lexerName string
avatarUtils := templates.NewAvatarUtils(ctx)
i := 0
commitCnt := 0
for _, part := range blameParts {
for index, line := range part.Lines {
i++
lines = append(lines, line)
br := &blameRow{
RowNumber: i,
}
commit := commitNames[part.Sha]
if index == 0 {
// Count commit number
commitCnt++
// User avatar image
commitSince := templates.TimeSince(commit.Author.When)
var avatar string
if commit.User != nil {
avatar = string(avatarUtils.Avatar(commit.User, 18))
} else {
avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "tw-mr-2"))
}
br.Avatar = gotemplate.HTML(avatar)
br.RepoLink = repoLink
br.PartSha = part.Sha
br.PreviousSha = part.PreviousSha
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
br.CommitMessage = commit.CommitMessage
br.CommitSince = commitSince
}
if i != len(lines)-1 {
line += "\n"
}
line, lexerNameForLine := highlight.Code(path.Base(ctx.Repo.TreePath), language, line)
// set lexer name to the first detected lexer. this is certainly suboptimal and
// we should instead highlight the whole file at once
if lexerName == "" {
lexerName = lexerNameForLine
}
br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale)
rows = append(rows, br)
escapeStatus = escapeStatus.Or(br.EscapeStatus)
bufContent := buf.Bytes()
bufContent = charset.ToUTF8(bufContent, charset.ConvertOpts{})
highlighted, lexerName := highlight.Code(path.Base(ctx.Repo.TreePath), language, util.UnsafeBytesToString(bufContent))
unsafeLines := highlight.UnsafeSplitHighlightedLines(highlighted)
for i, br := range rows {
var line template.HTML
if i < len(rows) {
line = template.HTML(util.UnsafeBytesToString(unsafeLines[i]))
}
br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale)
escapeStatus = escapeStatus.Or(br.EscapeStatus)
}
ctx.Data["EscapeStatus"] = escapeStatus
ctx.Data["BlameRows"] = rows
ctx.Data["CommitCnt"] = commitCnt
ctx.Data["LexerName"] = lexerName
}
+1 -1
View File
@@ -259,7 +259,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
} else if len(headInfos) == 2 {
headInfosSplit := strings.Split(headInfos[0], "/")
if len(headInfosSplit) == 1 {
ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
ci.HeadUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound(nil)
+1 -5
View File
@@ -317,11 +317,7 @@ func EditFile(ctx *context.Context) {
ctx.ServerError("ReadAll", err)
return
}
if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
ctx.Data["FileContent"] = string(buf)
} else {
ctx.Data["FileContent"] = content
}
ctx.Data["FileContent"] = string(charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true, ErrorReturnOrigin: true}))
}
}
+1 -3
View File
@@ -36,9 +36,7 @@ func CherryPick(ctx *context.Context) {
ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
} else {
ctx.Data["CherryPickType"] = "cherry-pick"
splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
ctx.Data["commit_summary"] = splits[0]
ctx.Data["commit_message"] = splits[1]
ctx.Data["commit_summary"], ctx.Data["commit_message"], _ = strings.Cut(cherryPickCommit.Message(), "\n")
}
ctx.HTML(http.StatusOK, tplCherryPick)
+1 -1
View File
@@ -495,7 +495,7 @@ func preparePullViewSigning(ctx *context.Context, issue *issues_model.Issue) {
pull := issue.PullRequest
ctx.Data["WillSign"] = false
if ctx.Doer != nil {
sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitHeadRefName())
sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, ctx.Repo.GitRepo, pull.BaseBranch, pull.GetGitHeadRefName())
ctx.Data["WillSign"] = sign
ctx.Data["SigningKeyMergeDisplay"] = asymkey_model.GetDisplaySigningKey(key)
if err != nil {
+11
View File
@@ -1340,6 +1340,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return
}
// Check if a pull request already exists with the same head and base branch.
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
ctx.ServerError("GetUnmergedPullRequest", err)
return
}
if pr != nil {
ctx.JSONError(ctx.Tr("repo.pulls.new.already_existed"))
return
}
content := form.Content
if filename := ctx.Req.Form.Get("template-file"); filename != "" {
if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {
+37 -4
View File
@@ -392,6 +392,32 @@ func NewRelease(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplReleaseNew)
}
// GenerateReleaseNotes builds release notes content for the given tag and base.
func GenerateReleaseNotes(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.GenerateReleaseNotesForm)
if ctx.HasError() {
ctx.JSONError(ctx.GetErrMsg())
return
}
content, err := release_service.GenerateReleaseNotes(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, release_service.GenerateReleaseNotesOptions{
TagName: form.TagName,
TagTarget: form.TagTarget,
PreviousTag: form.PreviousTag,
})
if err != nil {
if errTr := util.ErrorAsTranslatable(err); errTr != nil {
ctx.JSONError(errTr.Translate(ctx.Locale))
} else {
ctx.ServerError("GenerateReleaseNotes", err)
}
return
}
ctx.JSON(http.StatusOK, map[string]any{"content": content})
}
// NewReleasePost response for creating a release
func NewReleasePost(ctx *context.Context) {
newReleaseCommon(ctx)
@@ -520,11 +546,13 @@ func NewReleasePost(ctx *context.Context) {
// EditRelease render release edit page
func EditRelease(ctx *context.Context) {
newReleaseCommon(ctx)
if ctx.Written() {
return
}
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
ctx.Data["PageIsReleaseList"] = true
ctx.Data["PageIsEditRelease"] = true
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "release")
tagName := ctx.PathParam("*")
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName)
@@ -565,8 +593,13 @@ func EditRelease(ctx *context.Context) {
// EditReleasePost response for edit release
func EditReleasePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditReleaseForm)
newReleaseCommon(ctx)
if ctx.Written() {
return
}
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
ctx.Data["PageIsReleaseList"] = true
ctx.Data["PageIsEditRelease"] = true
tagName := ctx.PathParam("*")
+2 -2
View File
@@ -68,7 +68,7 @@ func SettingsCtxData(ctx *context.Context) {
ctx.Data["LFSSizeLimit"] = setting.LFSSizeLimit
ctx.Data["LFSSizeInRepoSize"] = setting.LFSSizeInRepoSize
signing, _ := gitrepo.GetSigningKey(ctx, ctx.Repo.Repository)
signing, _ := gitrepo.GetSigningKey(ctx)
ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
@@ -111,7 +111,7 @@ func SettingsPost(ctx *context.Context) {
ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
signing, _ := gitrepo.GetSigningKey(ctx, ctx.Repo.Repository)
signing, _ := gitrepo.GetSigningKey(ctx)
ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+1
View File
@@ -1404,6 +1404,7 @@ func registerWebRoutes(m *web.Router) {
m.Group("/releases", func() {
m.Get("/new", repo.NewRelease)
m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost)
m.Post("/generate-notes", web.Bind(forms.GenerateReleaseNotesForm{}), repo.GenerateReleaseNotes)
m.Post("/delete", repo.DeleteRelease)
m.Post("/attachments", repo.UploadReleaseAttachment)
m.Post("/attachments/remove", repo.DeleteAttachment)
+1 -1
View File
@@ -162,7 +162,7 @@ func parseCommitWithGPGSignature(ctx context.Context, c *git.Commit, committer *
}
}
defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false)
defaultGPGSettings, err := git.GetDefaultPublicGPGKey(ctx, false)
if err != nil {
log.Error("Error getting default public gpg key: %v", err)
} else if defaultGPGSettings == nil {
+14 -48
View File
@@ -108,34 +108,34 @@ func IsErrWontSign(err error) bool {
return ok
}
// PublicSigningKey gets the public signing key within a provided repository directory
func PublicSigningKey(ctx context.Context, repoPath string) (content, format string, err error) {
signingKey, _ := git.GetSigningKey(ctx, repoPath)
// PublicSigningKey gets the public signing key of the entire instance
func PublicSigningKey(ctx context.Context) (content, format string, err error) {
signingKey, _ := git.GetSigningKey(ctx)
if signingKey == nil {
return "", "", nil
}
if signingKey.Format == git.SigningKeyFormatSSH {
content, err := os.ReadFile(signingKey.KeyID)
if err != nil {
log.Error("Unable to read SSH public key file in %s: %s, %v", repoPath, signingKey, err)
log.Error("Unable to read SSH public key file: %s, %v", signingKey, err)
return "", signingKey.Format, err
}
return string(content), signingKey.Format, nil
}
content, stderr, err := process.GetManager().ExecDir(ctx, -1, repoPath,
content, stderr, err := process.GetManager().ExecDir(ctx, -1, setting.Git.HomePath,
"gpg --export -a", "gpg", "--export", "-a", signingKey.KeyID)
if err != nil {
log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err)
log.Error("Unable to get default signing key: %s, %s, %v", signingKey, stderr, err)
return "", signingKey.Format, err
}
return content, signingKey.Format, nil
}
// SignInitialCommit determines if we should sign the initial commit to this repository
func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
func SignInitialCommit(ctx context.Context, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
signingKey, sig := git.GetSigningKey(ctx, repoPath)
signingKey, sig := git.GetSigningKey(ctx)
if signingKey == nil {
return false, nil, nil, &ErrWontSign{noKey}
}
@@ -169,9 +169,9 @@ Loop:
}
// SignWikiCommit determines if we should sign the commits to this repository wiki
func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
signingKey, sig := gitrepo.GetSigningKey(ctx, repo.WikiStorageRepo())
signingKey, sig := gitrepo.GetSigningKey(ctx)
if signingKey == nil {
return false, nil, nil, &ErrWontSign{noKey}
}
@@ -200,11 +200,6 @@ Loop:
return false, nil, nil, &ErrWontSign{twofa}
}
case parentSigned:
gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
if err != nil {
return false, nil, nil, err
}
defer gitRepo.Close()
commit, err := gitRepo.GetCommit("HEAD")
if err != nil {
return false, nil, nil, err
@@ -222,9 +217,9 @@ Loop:
}
// SignCRUDAction determines if we should sign a CRUD commit to this repository
func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) {
func SignCRUDAction(ctx context.Context, u *user_model.User, gitRepo *git.Repository, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
signingKey, sig := git.GetSigningKey(ctx, repoPath)
signingKey, sig := git.GetSigningKey(ctx)
if signingKey == nil {
return false, nil, nil, &ErrWontSign{noKey}
}
@@ -253,11 +248,6 @@ Loop:
return false, nil, nil, &ErrWontSign{twofa}
}
case parentSigned:
gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
if err != nil {
return false, nil, nil, err
}
defer gitRepo.Close()
isEmpty, err := gitRepo.IsEmpty()
if err != nil {
return false, nil, nil, err
@@ -281,22 +271,19 @@ Loop:
}
// SignMerge determines if we should sign a PR merge commit to the base repository
func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, *git.SigningKey, *git.Signature, error) {
func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, gitRepo *git.Repository, baseCommit, headCommit string) (bool, *git.SigningKey, *git.Signature, error) {
if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to get Base Repo for pull request")
return false, nil, nil, err
}
repo := pr.BaseRepo
signingKey, signer := gitrepo.GetSigningKey(ctx, repo)
signingKey, signer := gitrepo.GetSigningKey(ctx)
if signingKey == nil {
return false, nil, nil, &ErrWontSign{noKey}
}
rules := signingModeFromStrings(setting.Repository.Signing.Merges)
var gitRepo *git.Repository
var err error
Loop:
for _, rule := range rules {
switch rule {
@@ -332,13 +319,6 @@ Loop:
return false, nil, nil, &ErrWontSign{approved}
}
case baseSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
if err != nil {
return false, nil, nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(baseCommit)
if err != nil {
return false, nil, nil, err
@@ -348,13 +328,6 @@ Loop:
return false, nil, nil, &ErrWontSign{baseSigned}
}
case headSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
if err != nil {
return false, nil, nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(headCommit)
if err != nil {
return false, nil, nil, err
@@ -364,13 +337,6 @@ Loop:
return false, nil, nil, &ErrWontSign{headSigned}
}
case commitsSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
if err != nil {
return false, nil, nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(headCommit)
if err != nil {
return false, nil, nil, err
+14 -2
View File
@@ -20,15 +20,27 @@ import (
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware"
)
// RedirectToUser redirect to a differently-named user
func RedirectToUser(ctx *Base, userName string, redirectUserID int64) {
func RedirectToUser(ctx *Base, doer *user_model.User, userName string, redirectUserID int64) {
user, err := user_model.GetUserByID(ctx, redirectUserID)
if err != nil {
ctx.HTTPError(http.StatusInternalServerError, "unable to get user")
if user_model.IsErrUserNotExist(err) {
ctx.HTTPError(http.StatusNotFound, "user does not exist")
} else {
ctx.HTTPError(http.StatusInternalServerError, "unable to get user")
}
return
}
// Handle Visibility
if user.Visibility != structs.VisibleTypePublic && doer == nil {
// We must be signed in to see limited or private organizations
ctx.HTTPError(http.StatusNotFound, "user does not exist")
return
}
+3 -2
View File
@@ -49,7 +49,7 @@ func GetOrganizationByParams(ctx *Context) {
if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(ctx, orgName)
if err == nil {
RedirectToUser(ctx.Base, orgName, redirectUserID)
RedirectToUser(ctx.Base, ctx.Doer, orgName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound(err)
} else {
@@ -70,8 +70,9 @@ type OrgAssignmentOptions struct {
}
// OrgAssignment returns a middleware to handle organization assignment
func OrgAssignment(opts OrgAssignmentOptions) func(ctx *Context) {
func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
return func(ctx *Context) {
opts := orgAssignmentOpts // it must be a copy, because the values will be changed
var err error
if ctx.ContextUser == nil {
// if Organization is not defined, get it from params
+8 -2
View File
@@ -140,7 +140,13 @@ func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *r
protectionRequireSigned = protectedBranch.RequireSignedCommits
}
willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String())
targetGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, targetRepo)
if err != nil {
return nil, err
}
defer closer.Close()
willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, doer, targetGitRepo, refName.String())
wontSignReason := ""
if asymkey_service.IsErrWontSign(err) {
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
@@ -443,7 +449,7 @@ func RepoAssignment(ctx *Context) {
}
if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
RedirectToUser(ctx.Base, userName, redirectUserID)
RedirectToUser(ctx.Base, ctx.Doer, userName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound(nil)
} else {
+1 -1
View File
@@ -69,7 +69,7 @@ func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (con
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil {
RedirectToUser(ctx, username, redirectUserID)
RedirectToUser(ctx, doer, username, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
errCb(http.StatusNotFound, err)
} else {
+6 -8
View File
@@ -5,12 +5,10 @@ package doctor
import (
"context"
"os"
"path/filepath"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
func checkOldArchives(ctx context.Context, logger log.Logger, autofix bool) error {
@@ -21,18 +19,18 @@ func checkOldArchives(ctx context.Context, logger log.Logger, autofix bool) erro
return nil
}
p := filepath.Join(repo.RepoPath(), "archives")
isDir, err := util.IsDir(p)
isDir, err := gitrepo.IsRepoDirExist(ctx, repo, "archives")
if err != nil {
log.Warn("check if %s is directory failed: %v", p, err)
log.Warn("check if %s is directory failed: %v", repo.FullName(), err)
}
if isDir {
numRepos++
if autofix {
if err := os.RemoveAll(p); err == nil {
err := gitrepo.RemoveRepoFileOrDir(ctx, repo, "archives")
if err == nil {
numReposUpdated++
} else {
log.Warn("remove %s failed: %v", p, err)
log.Warn("remove %s failed: %v", repo.FullName(), err)
}
}
}
+1 -1
View File
@@ -151,7 +151,7 @@ func checkDaemonExport(ctx context.Context, logger log.Logger, autofix bool) err
numNeedUpdate++
if autofix {
if !isPublic && isExist {
if err = gitrepo.RemoveRepoFile(ctx, repo, daemonExportFile); err != nil {
if err = gitrepo.RemoveRepoFileOrDir(ctx, repo, daemonExportFile); err != nil {
log.Error("Failed to remove %s:%s: %v", repo.FullName(), daemonExportFile, err)
}
} else if isPublic && !isExist {
+13
View File
@@ -648,6 +648,19 @@ func (f *NewReleaseForm) Validate(req *http.Request, errs binding.Errors) bindin
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// GenerateReleaseNotesForm retrieves release notes recommendations.
type GenerateReleaseNotesForm struct {
TagName string `form:"tag_name" binding:"Required;GitRefName;MaxSize(255)"`
TagTarget string `form:"tag_target" binding:"MaxSize(255)"`
PreviousTag string `form:"previous_tag" binding:"MaxSize(255)"`
}
// Validate validates the fields
func (f *GenerateReleaseNotesForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// EditReleaseForm form for changing release
type EditReleaseForm struct {
Title string `form:"title" binding:"Required;MaxSize(255)"`
+4 -1
View File
@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
type DiffTree struct {
@@ -56,7 +57,9 @@ func runGitDiffTree(ctx context.Context, gitRepo *git.Repository, useMergeBase b
return nil, err
}
cmd := gitcmd.NewCommand("diff-tree", "--raw", "-r", "--find-renames", "--root")
cmd := gitcmd.NewCommand("diff-tree", "--raw", "-r", "--root").
AddOptionFormat("--find-renames=%s", setting.Git.DiffRenameSimilarityThreshold)
if useMergeBase {
cmd.AddArguments("--merge-base")
}
+16 -14
View File
@@ -835,11 +835,11 @@ parsingLoop:
if buffer.Len() == 0 {
continue
}
charsetLabel, err := charset.DetectEncoding(buffer.Bytes())
if charsetLabel != "UTF-8" && err == nil {
encoding, _ := stdcharset.Lookup(charsetLabel)
if encoding != nil {
diffLineTypeDecoders[lineType] = encoding.NewDecoder()
charsetLabel, _ := charset.DetectEncoding(buffer.Bytes())
if charsetLabel != "UTF-8" {
charsetEncoding, _ := stdcharset.Lookup(charsetLabel)
if charsetEncoding != nil {
diffLineTypeDecoders[lineType] = charsetEncoding.NewDecoder()
}
}
}
@@ -1225,8 +1225,9 @@ func getDiffBasic(ctx context.Context, gitRepo *git.Repository, opts *DiffOption
}
cmdDiff := gitcmd.NewCommand().
AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M").
AddArguments(opts.WhitespaceBehavior...)
AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/").
AddArguments(opts.WhitespaceBehavior...).
AddOptionFormat("--find-renames=%s", setting.Git.DiffRenameSimilarityThreshold)
// In git 2.31, git diff learned --skip-to which we can use to shortcut skip to file
// so if we are using at least this version of git we don't have to tell ParsePatch to do
@@ -1325,10 +1326,10 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
shouldFullFileHighlight := !setting.Git.DisableDiffHighlight && attrDiff.Value() == ""
if shouldFullFileHighlight {
if limitedContent.LeftContent != nil && limitedContent.LeftContent.buf.Len() < MaxDiffHighlightEntireFileSize {
diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.String())
diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.Bytes())
}
if limitedContent.RightContent != nil && limitedContent.RightContent.buf.Len() < MaxDiffHighlightEntireFileSize {
diffFile.highlightedRightLines = highlightCodeLines(diffFile, false /* right */, limitedContent.RightContent.buf.String())
diffFile.highlightedRightLines = highlightCodeLines(diffFile, false /* right */, limitedContent.RightContent.buf.Bytes())
}
}
}
@@ -1336,10 +1337,11 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
return diff, nil
}
func highlightCodeLines(diffFile *DiffFile, isLeft bool, content string) map[int]template.HTML {
func highlightCodeLines(diffFile *DiffFile, isLeft bool, rawContent []byte) map[int]template.HTML {
content := util.UnsafeBytesToString(charset.ToUTF8(rawContent, charset.ConvertOpts{}))
highlightedNewContent, _ := highlight.Code(diffFile.Name, diffFile.Language, content)
splitLines := strings.Split(string(highlightedNewContent), "\n")
lines := make(map[int]template.HTML, len(splitLines))
unsafeLines := highlight.UnsafeSplitHighlightedLines(highlightedNewContent)
lines := make(map[int]template.HTML, len(unsafeLines))
// only save the highlighted lines we need, but not the whole file, to save memory
for _, sec := range diffFile.Sections {
for _, ln := range sec.Lines {
@@ -1349,8 +1351,8 @@ func highlightCodeLines(diffFile *DiffFile, isLeft bool, content string) map[int
}
if lineIdx >= 1 {
idx := lineIdx - 1
if idx < len(splitLines) {
lines[idx] = template.HTML(splitLines[idx])
if idx < len(unsafeLines) {
lines[idx] = template.HTML(util.UnsafeBytesToString(unsafeLines[idx]))
}
}
}
+39
View File
@@ -5,6 +5,7 @@
package gitdiff
import (
"html/template"
"strconv"
"strings"
"testing"
@@ -1106,3 +1107,41 @@ func TestDiffLine_GetExpandDirection(t *testing.T) {
assert.Equal(t, c.direction, c.diffLine.GetExpandDirection(), "case %s expected direction: %s", c.name, c.direction)
}
}
func TestHighlightCodeLines(t *testing.T) {
t.Run("CharsetDetecting", func(t *testing.T) {
diffFile := &DiffFile{
Name: "a.c",
Language: "c",
Sections: []*DiffSection{
{
Lines: []*DiffLine{{LeftIdx: 1}},
},
},
}
ret := highlightCodeLines(diffFile, true, []byte("// abc\xcc def\xcd")) // ISO-8859-1 bytes
assert.Equal(t, "<span class=\"c1\">// abcÌ defÍ\n</span>", string(ret[0]))
})
t.Run("LeftLines", func(t *testing.T) {
diffFile := &DiffFile{
Name: "a.c",
Language: "c",
Sections: []*DiffSection{
{
Lines: []*DiffLine{
{LeftIdx: 1},
{LeftIdx: 2},
{LeftIdx: 3},
},
},
},
}
const nl = "\n"
ret := highlightCodeLines(diffFile, true, []byte("a\nb\n"))
assert.Equal(t, map[int]template.HTML{
0: `<span class="n">a</span>` + nl,
1: `<span class="n">b</span>`,
}, ret)
})
}
+4 -4
View File
@@ -25,12 +25,12 @@ func TestDiffWithHighlight(t *testing.T) {
t.Run("CleanUp", func(t *testing.T) {
hcd := newHighlightCodeDiff()
codeA := template.HTML(`<span class="cm>this is a comment</span>`)
codeB := template.HTML(`<span class="cm>this is updated comment</span>`)
codeA := template.HTML(`<span class="cm">this is a comment</span>`)
codeB := template.HTML(`<span class="cm">this is updated comment</span>`)
outDel := hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB)
assert.Equal(t, `<span class="cm>this is <span class="removed-code">a</span> comment</span>`, string(outDel))
assert.Equal(t, `<span class="cm">this is <span class="removed-code">a</span> comment</span>`, string(outDel))
outAdd := hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB)
assert.Equal(t, `<span class="cm>this is <span class="added-code">updated</span> comment</span>`, string(outAdd))
assert.Equal(t, `<span class="cm">this is <span class="added-code">updated</span> comment</span>`, string(outAdd))
})
t.Run("OpenCloseTags", func(t *testing.T) {
+12 -19
View File
@@ -4,10 +4,8 @@
package sender
import (
"errors"
"io"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
type Sender interface {
@@ -16,23 +14,18 @@ type Sender interface {
var Send = send
func send(sender Sender, msgs ...*Message) error {
if setting.MailService == nil {
log.Error("Mailer: Send is being invoked but mail service hasn't been initialized")
return nil
func send(sender Sender, msg *Message) error {
m := msg.ToMessage()
froms := m.GetFrom()
to, err := m.GetRecipients()
if err != nil {
return err
}
for _, msg := range msgs {
m := msg.ToMessage()
froms := m.GetFrom()
to, err := m.GetRecipients()
if err != nil {
return err
}
// TODO: implement sending from multiple addresses
if err := sender.Send(froms[0].Address, to, m); err != nil {
return err
}
// TODO: implement sending from multiple addresses
if len(froms) == 0 {
// FIXME: no idea why sometimes the "froms" can be empty, need to figure out the root problem
return errors.New("no FROM specified")
}
return nil
return sender.Send(froms[0].Address, to, m)
}
@@ -40,7 +40,7 @@ func TestRenderHelperCodePreview(t *testing.T) {
<td class="lines-code chroma"><div class="code-inner"><span class="gh"># repo1</div></td>
</tr><tr>
<td class="lines-num"><span data-line-number="2"></span></td>
<td class="lines-code chroma"><div class="code-inner"></span><span class="gh"></span></div></td>
<td class="lines-code chroma"><div class="code-inner"></span></div></td>
</tr></tbody>
</table>
</div>
+7 -1
View File
@@ -306,14 +306,15 @@ func (g *RepositoryDumper) CreateReleases(_ context.Context, releases ...*base.R
if err != nil {
return err
}
defer rc.Close()
} else {
resp, err := http.Get(*asset.DownloadURL)
if err != nil {
return err
}
defer resp.Body.Close()
rc = resp.Body
}
defer rc.Close()
fw, err := os.Create(attachPath)
if err != nil {
@@ -357,6 +358,11 @@ func (g *RepositoryDumper) SyncTags(ctx context.Context) error {
return nil
}
// SyncBranches syncs branches in the database
func (g *RepositoryDumper) SyncBranches(ctx context.Context) error {
return nil
}
// CreateIssues creates issues
func (g *RepositoryDumper) CreateIssues(_ context.Context, issues ...*base.Issue) error {
var err error
@@ -27,6 +27,7 @@ func TestGiteaDownloadRepo(t *testing.T) {
if err != nil || resp.StatusCode != http.StatusOK {
t.Skipf("Can't reach https://gitea.com, skipping %s", t.Name())
}
defer resp.Body.Close()
ctx := t.Context()
downloader, err := NewGiteaDownloader(ctx, "https://gitea.com", "gitea/test_repo", "", "", giteaToken)
require.NoError(t, err, "NewGiteaDownloader error occur")
+5
View File
@@ -368,6 +368,11 @@ func (g *GiteaLocalUploader) SyncTags(ctx context.Context) error {
return repo_module.SyncReleasesWithTags(ctx, g.repo, g.gitRepo)
}
func (g *GiteaLocalUploader) SyncBranches(ctx context.Context) error {
_, err := repo_module.SyncRepoBranchesWithRepo(ctx, g.repo, g.gitRepo, g.doer.ID)
return err
}
// CreateIssues creates issues
func (g *GiteaLocalUploader) CreateIssues(ctx context.Context, issues ...*base.Issue) error {
iss := make([]*issues_model.Issue, 0, len(issues))
-302
View File
@@ -5,9 +5,6 @@
package migrations
import (
"fmt"
"os"
"path/filepath"
"strconv"
"testing"
"time"
@@ -17,15 +14,10 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
repo_service "code.gitea.io/gitea/services/repository"
"github.com/stretchr/testify/assert"
@@ -228,297 +220,3 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, linkedUser.ID, target.GetUserID())
}
func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
unittest.PrepareTestEnv(t)
//
// fromRepo master
//
fromRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
baseRef := "master"
// this is very different from the real situation. It should be a bare repository for all the Gitea managed repositories
assert.NoError(t, git.InitRepository(t.Context(), fromRepo.RepoPath(), false, fromRepo.ObjectFormatName))
err := gitrepo.RunCmd(t.Context(), fromRepo, gitcmd.NewCommand("symbolic-ref").AddDynamicArguments("HEAD", git.BranchPrefix+baseRef))
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(fromRepo.RepoPath(), "README.md"), []byte("# Testing Repository\n\nOriginally created in: "+fromRepo.RepoPath()), 0o644))
assert.NoError(t, git.AddChanges(t.Context(), fromRepo.RepoPath(), true))
signature := git.Signature{
Email: "test@example.com",
Name: "test",
When: time.Now(),
}
assert.NoError(t, git.CommitChanges(t.Context(), fromRepo.RepoPath(), git.CommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: "Initial Commit",
}))
fromGitRepo, err := gitrepo.OpenRepository(t.Context(), fromRepo)
assert.NoError(t, err)
defer fromGitRepo.Close()
baseSHA, err := fromGitRepo.GetBranchCommitID(baseRef)
assert.NoError(t, err)
//
// fromRepo branch1
//
headRef := "branch1"
_, err = gitrepo.RunCmdString(t.Context(), fromRepo, gitcmd.NewCommand("checkout", "-b").AddDynamicArguments(headRef))
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(fromRepo.RepoPath(), "README.md"), []byte("SOMETHING"), 0o644))
assert.NoError(t, git.AddChanges(t.Context(), fromRepo.RepoPath(), true))
signature.When = time.Now()
assert.NoError(t, git.CommitChanges(t.Context(), fromRepo.RepoPath(), git.CommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: "Pull request",
}))
assert.NoError(t, err)
headSHA, err := fromGitRepo.GetBranchCommitID(headRef)
assert.NoError(t, err)
fromRepoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: fromRepo.OwnerID})
//
// forkRepo branch2
//
forkHeadRef := "branch2"
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8})
assert.NoError(t, git.Clone(t.Context(), fromRepo.RepoPath(), forkRepo.RepoPath(), git.CloneRepoOptions{
Branch: headRef,
}))
_, err = gitrepo.RunCmdString(t.Context(), forkRepo, gitcmd.NewCommand("checkout", "-b").AddDynamicArguments(forkHeadRef))
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(forkRepo.RepoPath(), "README.md"), []byte("# branch2 "+forkRepo.RepoPath()), 0o644))
assert.NoError(t, git.AddChanges(t.Context(), forkRepo.RepoPath(), true))
assert.NoError(t, git.CommitChanges(t.Context(), forkRepo.RepoPath(), git.CommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: "branch2 commit",
}))
forkGitRepo, err := gitrepo.OpenRepository(t.Context(), forkRepo)
assert.NoError(t, err)
defer forkGitRepo.Close()
forkHeadSHA, err := forkGitRepo.GetBranchCommitID(forkHeadRef)
assert.NoError(t, err)
toRepoName := "migrated"
ctx := t.Context()
uploader := NewGiteaLocalUploader(ctx, fromRepoOwner, fromRepoOwner.Name, toRepoName)
uploader.gitServiceType = structs.GiteaService
assert.NoError(t, repo_service.Init(t.Context()))
assert.NoError(t, uploader.CreateRepo(ctx, &base.Repository{
Description: "description",
OriginalURL: fromRepo.RepoPath(),
CloneURL: fromRepo.RepoPath(),
IsPrivate: false,
IsMirror: true,
}, base.MigrateOptions{
GitServiceType: structs.GiteaService,
Private: false,
Mirror: true,
}))
for _, testCase := range []struct {
name string
head string
logFilter []string
logFiltered []bool
pr base.PullRequest
}{
{
name: "fork, good Head.SHA",
head: fmt.Sprintf("%s/%s", forkRepo.OwnerName, forkHeadRef),
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: forkRepo.RepoPath(),
Ref: forkHeadRef,
SHA: forkHeadSHA,
RepoName: forkRepo.Name,
OwnerName: forkRepo.OwnerName,
},
},
},
{
name: "fork, invalid Head.Ref",
head: "unknown repository",
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: forkRepo.RepoPath(),
Ref: "INVALID",
SHA: forkHeadSHA,
RepoName: forkRepo.Name,
OwnerName: forkRepo.OwnerName,
},
},
logFilter: []string{"Fetch branch from"},
logFiltered: []bool{true},
},
{
name: "invalid fork CloneURL",
head: "unknown repository",
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: "UNLIKELY",
Ref: forkHeadRef,
SHA: forkHeadSHA,
RepoName: forkRepo.Name,
OwnerName: "WRONG",
},
},
logFilter: []string{"AddRemote"},
logFiltered: []bool{true},
},
{
name: "no fork, good Head.SHA",
head: headRef,
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: headRef,
SHA: headSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
},
},
{
name: "no fork, empty Head.SHA",
head: headRef,
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: headRef,
SHA: "",
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
},
logFilter: []string{"Empty reference", "Cannot remove local head"},
logFiltered: []bool{true, false},
},
{
name: "no fork, invalid Head.SHA",
head: headRef,
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: headRef,
SHA: "brokenSHA",
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
},
logFilter: []string{"Deprecated local head"},
logFiltered: []bool{true},
},
{
name: "no fork, not found Head.SHA",
head: headRef,
pr: base.PullRequest{
PatchURL: "",
Number: 1,
State: "open",
Base: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: baseRef,
SHA: baseSHA,
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
Head: base.PullRequestBranch{
CloneURL: fromRepo.RepoPath(),
Ref: headRef,
SHA: "2697b352310fcd01cbd1f3dbd43b894080027f68",
RepoName: fromRepo.Name,
OwnerName: fromRepo.OwnerName,
},
},
logFilter: []string{"Deprecated local head", "Cannot remove local head"},
logFiltered: []bool{true, false},
},
} {
t.Run(testCase.name, func(t *testing.T) {
stopMark := fmt.Sprintf(">>>>>>>>>>>>>STOP: %s<<<<<<<<<<<<<<<", testCase.name)
logChecker, cleanup := test.NewLogChecker(log.DEFAULT)
logChecker.Filter(testCase.logFilter...).StopMark(stopMark)
defer cleanup()
testCase.pr.EnsuredSafe = true
head, err := uploader.updateGitForPullRequest(ctx, &testCase.pr)
assert.NoError(t, err)
assert.Equal(t, testCase.head, head)
log.Info(stopMark)
logFiltered, logStopped := logChecker.Check(5 * time.Second)
assert.True(t, logStopped)
if len(testCase.logFilter) > 0 {
assert.Equal(t, testCase.logFiltered, logFiltered, "for log message filters: %v", testCase.logFilter)
}
})
}
}
+1
View File
@@ -30,6 +30,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
if err != nil || resp.StatusCode != http.StatusOK {
t.Skipf("Can't access test repo, skipping %s", t.Name())
}
defer resp.Body.Close()
ctx := t.Context()
downloader, err := NewGitlabDownloader(ctx, "https://gitlab.com", "gitea/test_repo", gitlabPersonalAccessToken)
if err != nil {
+1
View File
@@ -27,6 +27,7 @@ func TestGogsDownloadRepo(t *testing.T) {
t.Skipf("visit test repo failed, ignored")
return
}
defer resp.Body.Close()
ctx := t.Context()
downloader := NewGogsDownloader(ctx, "https://try.gogs.io", "", "", gogsPersonalAccessToken, "lunnytest", "TESTREPO")
repo, err := downloader.GetRepoInfo(ctx)
+9
View File
@@ -478,6 +478,15 @@ func migrateRepository(ctx context.Context, doer *user_model.User, downloader ba
break
}
}
if len(mapInsertedPRIndexes) > 0 {
// The pull requests migrating process may created head branches in the base repository
// because head repository maybe a fork one which will not be migrated. So that we need
// to sync branches again.
log.Trace("syncing branches after migrating pull requests")
if err = uploader.SyncBranches(ctx); err != nil {
return err
}
}
}
if opts.Comments && supportAllComments {
+7 -7
View File
@@ -77,19 +77,19 @@ type OneDevDownloader struct {
}
// NewOneDevDownloader creates a new downloader
func NewOneDevDownloader(_ context.Context, baseURL *url.URL, username, password, repoPath string) *OneDevDownloader {
func NewOneDevDownloader(ctx context.Context, baseURL *url.URL, username, password, repoPath string) *OneDevDownloader {
httpTransport := NewMigrationHTTPTransport()
downloader := &OneDevDownloader{
baseURL: baseURL,
repoPath: repoPath,
client: &http.Client{
Transport: &http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
if len(username) > 0 && len(password) > 0 {
Transport: roundTripperFunc(
func(req *http.Request) (*http.Response, error) {
if username != "" && password != "" {
req.SetBasicAuth(username, password)
}
return nil, nil
},
},
return httpTransport.RoundTrip(req.WithContext(ctx))
}),
},
userMap: make(map[int64]*onedevUser),
milestoneMap: make(map[int64]string),
+1
View File
@@ -19,6 +19,7 @@ func TestOneDevDownloadRepo(t *testing.T) {
if err != nil || resp.StatusCode != http.StatusOK {
t.Skipf("Can't access test repo, skipping %s", t.Name())
}
defer resp.Body.Close()
u, _ := url.Parse("https://code.onedev.io")
ctx := t.Context()
+7 -1
View File
@@ -232,7 +232,13 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer
return true, nil
}
sign, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitHeadRefName())
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
if err != nil {
return false, err
}
defer closer.Close()
sign, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo, pr.BaseBranch, pr.GetGitHeadRefName())
return sign, err
}

Some files were not shown because too many files have changed in this diff Show More