mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-05 00:09:33 +09:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab295e62e4 | ||
|
|
d919301d36 | ||
|
|
666e6e2d17 | ||
|
|
4d00ed2a1e | ||
|
|
ce0da8a378 | ||
|
|
f26a314636 | ||
|
|
5c68ba3e0a | ||
|
|
78eb95ab19 | ||
|
|
5c723ee0fd |
No files matched your search
@@ -1,31 +0,0 @@
|
||||
name: docker-dryrun
|
||||
description: Composite action that performs the container build steps for a single platform.
|
||||
|
||||
inputs:
|
||||
platform:
|
||||
description: "The target platform: linux/amd64, linux/arm64, linux/riscv64."
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
with:
|
||||
cache-image: false
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
- name: Build regular image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ inputs.platform }}
|
||||
push: false
|
||||
file: Dockerfile
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||
- name: Build rootless image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ inputs.platform }}
|
||||
push: false
|
||||
file: Dockerfile.rootless
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
|
||||
@@ -15,8 +15,6 @@ on:
|
||||
value: ${{ jobs.detect.outputs.templates }}
|
||||
docker:
|
||||
value: ${{ jobs.detect.outputs.docker }}
|
||||
dockerfile:
|
||||
value: ${{ jobs.detect.outputs.dockerfile }}
|
||||
swagger:
|
||||
value: ${{ jobs.detect.outputs.swagger }}
|
||||
yaml:
|
||||
@@ -42,7 +40,6 @@ jobs:
|
||||
actions: ${{ steps.changes.outputs.actions }}
|
||||
templates: ${{ steps.changes.outputs.templates }}
|
||||
docker: ${{ steps.changes.outputs.docker }}
|
||||
dockerfile: ${{ steps.changes.outputs.dockerfile }}
|
||||
swagger: ${{ steps.changes.outputs.swagger }}
|
||||
yaml: ${{ steps.changes.outputs.yaml }}
|
||||
json: ${{ steps.changes.outputs.json }}
|
||||
@@ -103,16 +100,11 @@ jobs:
|
||||
|
||||
docker:
|
||||
- ".github/workflows/pull-docker-dryrun.yml"
|
||||
- ".github/actions/docker-dryrun/**"
|
||||
- "Dockerfile"
|
||||
- "Dockerfile.rootless"
|
||||
- "docker/**"
|
||||
- "Makefile"
|
||||
|
||||
dockerfile:
|
||||
- "Dockerfile"
|
||||
- "Dockerfile.rootless"
|
||||
|
||||
swagger:
|
||||
- "templates/swagger/*.json"
|
||||
- "Makefile"
|
||||
|
||||
@@ -14,34 +14,26 @@ jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
|
||||
# QEMU-based build is slow (40-50 minutes), so run arm64 and riscv64 when dockerfile changes.
|
||||
# Run amd64 when any docker-related files change, which is fast (4 minutes).
|
||||
container-amd64:
|
||||
container:
|
||||
if: needs.files-changed.outputs.docker == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
- uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
with:
|
||||
platform: linux/amd64
|
||||
|
||||
container-arm64:
|
||||
if: needs.files-changed.outputs.dockerfile == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
cache-image: false
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
- name: Build regular image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
platform: linux/arm64
|
||||
|
||||
container-riscv64:
|
||||
if: needs.files-changed.outputs.dockerfile == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: ./.github/actions/docker-dryrun
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||
- name: Build rootless image
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
platform: linux/riscv64
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
file: Dockerfile.rootless
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless
|
||||
+7
-3
@@ -8,8 +8,8 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --froze
|
||||
COPY --exclude=.git/ . .
|
||||
RUN make frontend
|
||||
|
||||
# Build backend for each target platform
|
||||
FROM docker.io/library/golang:1.27-alpine3.24 AS build-env
|
||||
# Cross-compile backend on the native platform to avoid slow QEMU emulation
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS build-env
|
||||
|
||||
ARG GITEA_VERSION
|
||||
ARG TAGS=""
|
||||
@@ -28,11 +28,15 @@ RUN go mod download
|
||||
# ".git" directory is mounted separately later only for version data extraction.
|
||||
COPY --exclude=.git/ . .
|
||||
COPY --from=frontend-build /src/public/assets public/assets
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" make generate-backend
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
# Build gitea, .git mount is required for version data
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" \
|
||||
--mount=type=bind,source=".git/",target=".git/" \
|
||||
make backend
|
||||
GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} make gitea
|
||||
|
||||
COPY docker/root /tmp/local
|
||||
|
||||
|
||||
+7
-3
@@ -8,8 +8,8 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --froze
|
||||
COPY --exclude=.git/ . .
|
||||
RUN make frontend
|
||||
|
||||
# Build backend for each target platform
|
||||
FROM docker.io/library/golang:1.27-alpine3.24 AS build-env
|
||||
# Cross-compile backend on the native platform
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.27-alpine3.24 AS build-env
|
||||
|
||||
ARG GITEA_VERSION
|
||||
ARG TAGS=""
|
||||
@@ -27,11 +27,15 @@ RUN go mod download
|
||||
# See the comments in Dockerfile
|
||||
COPY --exclude=.git/ . .
|
||||
COPY --from=frontend-build /src/public/assets public/assets
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" make generate-backend
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
# Build gitea, .git mount is required for version data
|
||||
RUN --mount=type=cache,target="/root/.cache/go-build" \
|
||||
--mount=type=bind,source=".git/",target=".git/" \
|
||||
make backend
|
||||
GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} make gitea
|
||||
|
||||
COPY docker/rootless /tmp/local
|
||||
|
||||
|
||||
Generated
-5
@@ -964,11 +964,6 @@
|
||||
"path": "github.com/yohcop/openid-go/LICENSE",
|
||||
"licenseText": "Copyright 2015 Yohann Coppel\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/yuin/goldmark-highlighting/v2",
|
||||
"path": "github.com/yuin/goldmark-highlighting/v2/LICENSE",
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2019 Yusuke Inuzuka\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/yuin/goldmark",
|
||||
"path": "github.com/yuin/goldmark/LICENSE",
|
||||
|
||||
@@ -84,7 +84,6 @@ require (
|
||||
github.com/wneessen/go-mail v0.8.1
|
||||
github.com/yohcop/openid-go v1.0.1
|
||||
github.com/yuin/goldmark v1.8.6
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
gitlab.com/gitlab-org/api/client-go/v3 v3.39.1
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525
|
||||
golang.org/x/crypto v0.58.0
|
||||
|
||||
@@ -48,10 +48,8 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0 h1:LvK7+C6qgz8BPnmn7x
|
||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:I28hc9eaiqKCoOB+9Wh/P1IOScfm0xCgyWC6DAo0lmo=
|
||||
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.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
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-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
@@ -162,8 +160,6 @@ github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454Wv
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2/v2 v2.11.0 h1:z5uRPHav+eTVpoffDSuUP2KPS4f1ONZaPDK73S8HA0w=
|
||||
@@ -528,7 +524,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
@@ -559,11 +554,8 @@ github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3j
|
||||
github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBzPCRjkCNs=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
|
||||
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
|
||||
@@ -441,6 +441,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(362, "Add reusable_workflow_ref column to action_run_job", v29.AddReusableWorkflowRefToActionRunJob),
|
||||
newMigration(363, "Add release_id to reaction table", v29.AddReleaseIDToReaction),
|
||||
newMigration(364, "Add new table project_workflow", v29.AddProjectWorkflow),
|
||||
newMigration(365, "Normalize legacy repository unit and login source JSON", v29.NormalizeLegacyConfigJSON),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
|
||||
"gitea.dev/modelmigration/base"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/util"
|
||||
)
|
||||
|
||||
// unmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
|
||||
// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
|
||||
func unmarshalHandleDoubleEncode(bs []byte, v any) error {
|
||||
if len(bs) == 0 {
|
||||
// json.Unmarshal will report errors if input is empty (nil or zero-length)
|
||||
// It seems that XORM ignores the nil but still passes zero-length string into this function
|
||||
// To be consistent, we should treat all empty inputs as success
|
||||
return nil
|
||||
}
|
||||
err := json.Unmarshal(bs, v)
|
||||
if err != nil {
|
||||
ok := true
|
||||
rs := []byte{}
|
||||
temp := make([]byte, 2)
|
||||
for _, rn := range string(bs) {
|
||||
if rn > 0xffff {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
binary.LittleEndian.PutUint16(temp, uint16(rn))
|
||||
rs = append(rs, temp...)
|
||||
}
|
||||
if ok {
|
||||
if len(rs) > 1 && rs[0] == 0xff && rs[1] == 0xfe {
|
||||
rs = rs[2:]
|
||||
}
|
||||
err = json.Unmarshal(rs, v)
|
||||
}
|
||||
}
|
||||
if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
|
||||
err = json.Unmarshal(bs[2:], v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func normalizeLegacyConfigJSON(config string) string {
|
||||
if config == "" || json.Valid(util.UnsafeStringToBytes(config)) {
|
||||
return config
|
||||
}
|
||||
|
||||
m := map[any]any{}
|
||||
err := unmarshalHandleDoubleEncode(util.UnsafeStringToBytes(config), &m)
|
||||
if err != nil {
|
||||
return config
|
||||
}
|
||||
if bs, err := json.Marshal(m); err == nil {
|
||||
return string(bs)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func NormalizeLegacyConfigJSON(ctx context.Context, x base.EngineMigration) error {
|
||||
type RepoUnit struct {
|
||||
ID int64
|
||||
Config string
|
||||
}
|
||||
type LoginSource struct {
|
||||
ID int64
|
||||
Cfg string
|
||||
}
|
||||
return base.EngineTransaction(x, func(sess base.Session) error {
|
||||
if err := base.IterateTableByColumn(ctx, x, sess, "id", nil, func(_ context.Context, unit *RepoUnit) error {
|
||||
config := normalizeLegacyConfigJSON(unit.Config)
|
||||
if config == unit.Config {
|
||||
return nil
|
||||
}
|
||||
_, err := sess.ID(unit.ID).Cols("config").Update(&RepoUnit{Config: config})
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return base.IterateTableByColumn(ctx, x, sess, "id", nil, func(_ context.Context, source *LoginSource) error {
|
||||
config := normalizeLegacyConfigJSON(source.Cfg)
|
||||
if config == source.Cfg {
|
||||
return nil
|
||||
}
|
||||
_, err := sess.ID(source.ID).Cols("cfg").Update(&LoginSource{Cfg: config})
|
||||
return err
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v29
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modelmigration/migrationtest"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNormalizeLegacyConfigJSONValue(t *testing.T) {
|
||||
const (
|
||||
config = `{"Host":"smtp.example.com"}`
|
||||
packed = "≻潈瑳㨢猢瑭硥浡汰潣≭⁽"
|
||||
)
|
||||
for _, tc := range []struct {
|
||||
name, input, want string
|
||||
}{
|
||||
{"empty", "", ""},
|
||||
{"valid", config + " ", config + " "},
|
||||
{"unicode and large number", ` {"name":"例😀","id":9007199254740993} `, ` {"name":"例😀","id":9007199254740993} `},
|
||||
{"double encoded", packed, config},
|
||||
{"double encoded with BOM", "\ufeff" + packed, config},
|
||||
{"BOM", "\xff\xfe" + config, config},
|
||||
{"BOM with non-BMP rune", "\xff\xfe" + `{"name":"😀"}`, `{"name":"😀"}`},
|
||||
{"corrupt", "invalid", "invalid"},
|
||||
{"corrupt non-BMP rune", "invalid😀", "invalid😀"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := normalizeLegacyConfigJSON(tc.input)
|
||||
assert.Equal(t, tc.want, got)
|
||||
assert.Equal(t, got, normalizeLegacyConfigJSON(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLegacyConfigJSON(t *testing.T) {
|
||||
type RepoUnit struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64
|
||||
Config string `xorm:"TEXT"`
|
||||
}
|
||||
type LoginSource struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Name string
|
||||
Cfg string `xorm:"TEXT"`
|
||||
}
|
||||
x, cleanup := migrationtest.PrepareTestEnv(t, 0, new(RepoUnit), new(LoginSource))
|
||||
defer cleanup()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
defer test.MockVariableValue(&setting.Database.IterateBufferSize, 2)()
|
||||
|
||||
inputs := []string{
|
||||
` {"id":9007199254740993,"name":"例😀"} `,
|
||||
"≻潈瑳㨢猢瑭硥浡汰潣≭⁽",
|
||||
"\ufeff≻潈瑳㨢猢瑭硥浡汰潣≭⁽",
|
||||
"",
|
||||
"invalid",
|
||||
}
|
||||
want := []string{inputs[0], `{"Host":"smtp.example.com"}`, `{"Host":"smtp.example.com"}`, "", "invalid", ""}
|
||||
for _, input := range inputs {
|
||||
_, err := x.Insert(&RepoUnit{RepoID: 42, Config: input})
|
||||
require.NoError(t, err)
|
||||
_, err = x.Insert(&LoginSource{Name: "test", Cfg: input})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
_, err := x.Exec("INSERT INTO `repo_unit` (`repo_id`, `config`) VALUES (?, NULL)", 42)
|
||||
require.NoError(t, err)
|
||||
_, err = x.Exec("INSERT INTO `login_source` (`name`, `cfg`) VALUES (?, NULL)", "test")
|
||||
require.NoError(t, err)
|
||||
|
||||
for range 2 {
|
||||
require.NoError(t, NormalizeLegacyConfigJSON(t.Context(), x))
|
||||
var units []RepoUnit
|
||||
require.NoError(t, x.Asc("id").Find(&units))
|
||||
require.Len(t, units, len(want))
|
||||
var sources []LoginSource
|
||||
require.NoError(t, x.Asc("id").Find(&sources))
|
||||
require.Len(t, sources, len(want))
|
||||
for i, config := range want {
|
||||
assert.Equal(t, config, units[i].Config)
|
||||
assert.EqualValues(t, 42, units[i].RepoID)
|
||||
assert.Equal(t, config, sources[i].Cfg)
|
||||
assert.Equal(t, "test", sources[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@ package actions
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"xorm.io/xorm/convert"
|
||||
@@ -30,7 +30,7 @@ type OwnerActionsConfig struct {
|
||||
var _ convert.ConversionFrom = (*OwnerActionsConfig)(nil)
|
||||
|
||||
func (cfg *OwnerActionsConfig) FromDB(bytes []byte) error {
|
||||
_ = json.Unmarshal(bytes, cfg)
|
||||
db.UnmarshalFromDB(bytes, cfg)
|
||||
cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -180,15 +180,14 @@ func updateIssueNotification(ctx context.Context, userID, issueID, commentID, up
|
||||
}
|
||||
|
||||
// NOTICE: Only update comment id when the before notification on this issue is read, otherwise you may miss some old comments.
|
||||
// But we need update update_by so that the notification will be reorder
|
||||
var cols []string
|
||||
// Always update so that updated_unix moves the notification to the top of the list
|
||||
// Actually, it's unclear what UpdatedBy is used for, or maybe not used at all, or maybe it should just be dropped.
|
||||
notification.UpdatedBy = updatedByID
|
||||
cols := []string{"updated_by"}
|
||||
if notification.Status == NotificationStatusRead {
|
||||
notification.Status = NotificationStatusUnread
|
||||
notification.CommentID = commentID
|
||||
cols = []string{"status", "update_by", "comment_id"}
|
||||
} else {
|
||||
notification.UpdatedBy = updatedByID
|
||||
cols = []string{"update_by"}
|
||||
cols = append(cols, "status", "comment_id")
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).ID(notification.ID).Cols(cols...).Update(notification)
|
||||
|
||||
@@ -71,16 +71,6 @@ func (opts FindNotificationOptions) ToOrders() string {
|
||||
// receiverID > 0 just send to receiver, else send to all watcher
|
||||
// Returns the set of user IDs whose notification rows were created or updated.
|
||||
func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) ([]int64, error) {
|
||||
var notifiedIDs []int64
|
||||
err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
var innerErr error
|
||||
notifiedIDs, innerErr = createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID)
|
||||
return innerErr
|
||||
})
|
||||
return notifiedIDs, err
|
||||
}
|
||||
|
||||
func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) ([]int64, error) {
|
||||
// init
|
||||
var toNotify container.Set[int64]
|
||||
notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
|
||||
@@ -181,7 +171,11 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
|
||||
if notificationExists(notifications, issue.ID, userID) {
|
||||
err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID)
|
||||
} else {
|
||||
err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID)
|
||||
if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
|
||||
if errUpdate := updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); errUpdate == nil {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -32,6 +32,18 @@ func TestCreateOrUpdateIssueNotifications(t *testing.T) {
|
||||
|
||||
notf = unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{UserID: 4, IssueID: issue.ID})
|
||||
assert.Equal(t, activities_model.NotificationStatusUnread, notf.Status)
|
||||
|
||||
// unread notification should get latest updated_unix when re-notified
|
||||
_, err = db.GetEngine(t.Context()).ID(notf.ID).NoAutoTime().Cols("updated_unix").Update(&activities_model.Notification{UpdatedUnix: 123})
|
||||
assert.NoError(t, err)
|
||||
notf = unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: notf.ID})
|
||||
assert.EqualValues(t, 123, notf.UpdatedUnix)
|
||||
|
||||
_, err = activities_model.CreateOrUpdateIssueNotifications(t.Context(), issue.ID, 0, 3, 0)
|
||||
assert.NoError(t, err)
|
||||
notf = unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: notf.ID})
|
||||
assert.NotEqualValues(t, 123, notf.UpdatedUnix)
|
||||
assert.EqualValues(t, 3, notf.UpdatedBy)
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateIssueNotificationsForAssigneeAndReviewer(t *testing.T) {
|
||||
|
||||
+30
-3
@@ -10,6 +10,7 @@ import (
|
||||
"reflect"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -60,14 +61,40 @@ var Names = map[Type]string{
|
||||
type Config interface {
|
||||
convert.Conversion
|
||||
SetAuthSource(*Source)
|
||||
GetAuthSource() *Source
|
||||
}
|
||||
|
||||
type ConfigBase struct {
|
||||
AuthSource *Source
|
||||
AuthSource *Source `json:"-"`
|
||||
}
|
||||
|
||||
func (p *ConfigBase) SetAuthSource(s *Source) {
|
||||
p.AuthSource = s
|
||||
func (c *ConfigBase) SetAuthSource(source *Source) {
|
||||
c.AuthSource = source
|
||||
}
|
||||
|
||||
func (c *ConfigBase) GetAuthSource() *Source {
|
||||
return c.AuthSource
|
||||
}
|
||||
|
||||
func UnmarshalAuthSourceConfig(bs []byte, v Config) {
|
||||
if db.UnmarshalFromDB(bs, &v) {
|
||||
return
|
||||
}
|
||||
// This is only a workaround because we don't know whether this unmarshal happens on sign-in page or admin panel.
|
||||
// * Admin should have the chance to edit the auth source in admin panel, so FromDB should not return an error
|
||||
// * The sign-in page should not use the auth source in the next login attempt, but we don't have a clear way to disable it immediately (acceptable)
|
||||
// Actually, corrupted database value is a really rare case, just because AI thinks it should be handled.
|
||||
authSourceID := v.GetAuthSource().ID
|
||||
log.Error("Unable to unmarshal login source id=%d, will mark it as inactive to avoid unexpected sign-in issues", authSourceID)
|
||||
go graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
|
||||
// A small time window to avoid race condition with admin's editing because there is no other way.
|
||||
// The UpdateUnix field is not filled when this function is called.
|
||||
_, err := db.GetEngine(ctx).ID(authSourceID).And("is_active = ? AND updated_unix < ?", true, timeutil.TimeStampNow()-5).
|
||||
Cols("is_active").Update(&Source{IsActive: false})
|
||||
if err != nil {
|
||||
log.Error("Unable to set login source id=%d to inactive: %v", authSourceID, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
|
||||
|
||||
@@ -6,10 +6,15 @@ package auth_test
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -24,7 +29,8 @@ type TestSource struct {
|
||||
|
||||
// FromDB fills up a LDAPConfig from serialized format.
|
||||
func (source *TestSource) FromDB(bs []byte) error {
|
||||
return json.Unmarshal(bs, &source)
|
||||
auth_model.UnmarshalAuthSourceConfig(bs, source)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a LDAPConfig to a serialized format.
|
||||
@@ -53,3 +59,37 @@ func TestDumpAuthSource(t *testing.T) {
|
||||
// INSERT INTO `login_source` (`id`, `type`, `name`, `is_active`, `is_sync_enabled`, `two_factor_policy`, `cfg`, `created_unix`, `updated_unix`) VALUES (1,6,'TestSource',0,0,'','{"TestField":"TestValue"}',1774179784,1774179784);
|
||||
assert.Contains(t, sb.String(), `'{"TestField":"TestValue"}'`)
|
||||
}
|
||||
|
||||
func TestUnmarshalAuthSourceConfig(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
defer test.MockVariableValue(&setting.IsProd, true)()
|
||||
defer test.MockVariableValue(&setting.IsInTesting, false)()
|
||||
|
||||
cfg := &TestSource{}
|
||||
source := &auth_model.Source{Type: auth_model.OAuth2, Name: t.Name(), IsActive: true, Cfg: cfg}
|
||||
err := db.Insert(t.Context(), source)
|
||||
require.NoError(t, err)
|
||||
cfg.SetAuthSource(source)
|
||||
|
||||
t.Run("ValidConfig", func(t *testing.T) {
|
||||
for _, data := range []string{`{"TestField":"TestValue"}`, "", "null"} {
|
||||
auth_model.UnmarshalAuthSourceConfig([]byte(data), cfg)
|
||||
assert.Equal(t, "TestValue", cfg.TestField)
|
||||
assert.Same(t, source, cfg.GetAuthSource())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidConfigDeactivatesSource", func(t *testing.T) {
|
||||
_, err = db.GetEngine(t.Context()).ID(source.ID).NoAutoTime().Cols("updated_unix").Update(&auth_model.Source{UpdatedUnix: timeutil.TimeStampNow() - 60})
|
||||
require.NoError(t, err)
|
||||
has, _ := db.GetEngine(t.Context()).ID(source.ID).And("is_active=?", true).Get(&auth_model.Source{})
|
||||
assert.True(t, has)
|
||||
|
||||
auth_model.UnmarshalAuthSourceConfig([]byte(`{"TestField":false}`), cfg)
|
||||
assert.Same(t, source, cfg.GetAuthSource())
|
||||
require.Eventually(t, func() bool {
|
||||
has, _ := db.GetEngine(t.Context()).ID(source.ID).And("is_active=?", false).Get(&auth_model.Source{})
|
||||
return has
|
||||
}, time.Second, time.Millisecond)
|
||||
})
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package db
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
@@ -84,3 +85,21 @@ func CellToInt[T ~int | int64](cell xorm.Cell, def T) (ret T, has bool, err erro
|
||||
}
|
||||
return T(val), true, err
|
||||
}
|
||||
|
||||
func UnmarshalFromDB(data []byte, v any) bool {
|
||||
if len(data) == 0 {
|
||||
return true
|
||||
}
|
||||
err := json.Unmarshal(data, v)
|
||||
if err != nil {
|
||||
// Even if database value is corrupted, we should not let it cause unnecessary 500 error or block user's operation
|
||||
// Corrupted database value is a really rare case, it should never happen unless site admin manually modified the database.
|
||||
// Although some database values will be printed into logs, it isn't a real problem
|
||||
// because such case should never really happen. If it happens, logging the value
|
||||
// is the only way to tell site admin which record is wrong since there is no other context information,
|
||||
// and then they need to re-create the config (with new secrets)
|
||||
setting.PanicInDevOrTesting("UnmarshalFromDB %q failed: %v", string(data), err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func UpdateLanguageStats(ctx context.Context, repo *Repository, commitID string,
|
||||
var topLang string
|
||||
var s int64
|
||||
for lang, size := range stats {
|
||||
if size > s {
|
||||
if size > s || (size == s && lang < topLang) {
|
||||
s = size
|
||||
topLang = lang
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdateLanguageStatsPrimaryTie(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
require.NoError(t, repo_model.UpdateLanguageStats(t.Context(), repo, "1234567890", map[string]int64{"Go": 10, "C": 10}))
|
||||
|
||||
stats, err := repo_model.GetLanguageStats(t.Context(), repo)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, stats, 2)
|
||||
for _, stat := range stats {
|
||||
assert.Equal(t, stat.Language == "C", stat.IsPrimary, stat.Language)
|
||||
}
|
||||
}
|
||||
+13
-35
@@ -56,12 +56,11 @@ func init() {
|
||||
// UnitConfig describes common unit config
|
||||
type UnitConfig struct{}
|
||||
|
||||
// FromDB fills up a UnitConfig from serialized format.
|
||||
func (cfg *UnitConfig) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a UnitConfig to a serialized format.
|
||||
func (cfg *UnitConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
@@ -71,12 +70,11 @@ type ExternalWikiConfig struct {
|
||||
ExternalWikiURL string
|
||||
}
|
||||
|
||||
// FromDB fills up a ExternalWikiConfig from serialized format.
|
||||
func (cfg *ExternalWikiConfig) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a ExternalWikiConfig to a serialized format.
|
||||
func (cfg *ExternalWikiConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
@@ -89,12 +87,11 @@ type ExternalTrackerConfig struct {
|
||||
ExternalTrackerRegexpPattern string
|
||||
}
|
||||
|
||||
// FromDB fills up a ExternalTrackerConfig from serialized format.
|
||||
func (cfg *ExternalTrackerConfig) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a ExternalTrackerConfig to a serialized format.
|
||||
func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
@@ -106,12 +103,11 @@ type IssuesConfig struct {
|
||||
EnableDependencies bool
|
||||
}
|
||||
|
||||
// FromDB fills up a IssuesConfig from serialized format.
|
||||
func (cfg *IssuesConfig) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a IssuesConfig to a serialized format.
|
||||
func (cfg *IssuesConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
@@ -155,16 +151,14 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
|
||||
return cfg
|
||||
}
|
||||
|
||||
// FromDB fills up a PullRequestsConfig from serialized format.
|
||||
func (cfg *PullRequestsConfig) FromDB(bs []byte) error {
|
||||
// set default values for existing PullRequestConfig in DB
|
||||
*cfg = *DefaultPullRequestsConfig()
|
||||
_ = json.UnmarshalHandleDoubleEncode(bs, &cfg) // don't let corrupted database value cause unnecessary 500 error
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
cfg.DefaultUpdateStyle = util.IfZero(cfg.DefaultUpdateStyle, UpdateStyleMerge)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a PullRequestsConfig to a serialized format.
|
||||
func (cfg *PullRequestsConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
@@ -228,34 +222,18 @@ type ProjectsConfig struct {
|
||||
ProjectsMode ProjectsMode
|
||||
}
|
||||
|
||||
// FromDB fills up a ProjectsConfig from serialized format.
|
||||
func (cfg *ProjectsConfig) FromDB(bs []byte) error {
|
||||
// TODO: remove GetProjectsMode, only use ProjectsMode
|
||||
cfg.ProjectsMode = ProjectsModeAll
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
cfg.ProjectsMode = util.IfZero(cfg.ProjectsMode, ProjectsModeAll)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a ProjectsConfig to a serialized format.
|
||||
func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
|
||||
if cfg.ProjectsMode != "" {
|
||||
return cfg.ProjectsMode
|
||||
}
|
||||
|
||||
return ProjectsModeAll
|
||||
}
|
||||
|
||||
func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
|
||||
projectsMode := cfg.GetProjectsMode()
|
||||
|
||||
if m == ProjectsModeNone {
|
||||
return true
|
||||
}
|
||||
|
||||
return projectsMode == m || projectsMode == ProjectsModeAll
|
||||
return m == ProjectsModeNone || cfg.ProjectsMode == m || cfg.ProjectsMode == ProjectsModeAll
|
||||
}
|
||||
|
||||
// BeforeSet is invoked from XORM before setting the value of a field of this object.
|
||||
|
||||
@@ -6,6 +6,7 @@ package repo
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/json"
|
||||
@@ -173,14 +174,12 @@ func (cfg *ActionsConfig) ClampPermissions(perms ActionsTokenPermissions) Action
|
||||
return ClampActionsTokenPermissions(perms, maxPerms)
|
||||
}
|
||||
|
||||
// FromDB fills up a ActionsConfig from serialized format.
|
||||
func (cfg *ActionsConfig) FromDB(bs []byte) error {
|
||||
_ = json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
db.UnmarshalFromDB(bs, cfg)
|
||||
cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a ActionsConfig to a serialized format.
|
||||
func (cfg *ActionsConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
)
|
||||
|
||||
@@ -82,6 +83,13 @@ func htmlEscape(code string) template.HTML {
|
||||
return template.HTML(gohtml.EscapeString(code))
|
||||
}
|
||||
|
||||
func normalizeLexer(lexer chroma.Lexer) chroma.Lexer {
|
||||
if lexer != lexers.Fallback {
|
||||
lexer = chroma.Coalesce(lexer)
|
||||
}
|
||||
return lexer
|
||||
}
|
||||
|
||||
// RenderCodeSlowGuess tries to get a lexer by file name and language first,
|
||||
// if not found, it will try to guess the lexer by code content, which is slow (more than several hundreds of milliseconds).
|
||||
func RenderCodeSlowGuess(fileName, language, code string) (output template.HTML, lexer chroma.Lexer, lexerDisplayName string) {
|
||||
@@ -96,11 +104,15 @@ func RenderCodeSlowGuess(fileName, language, code string) (output template.HTML,
|
||||
}
|
||||
|
||||
lexer = detectChromaLexerWithAnalyze(fileName, language, util.UnsafeStringToBytes(code)) // it is also slow
|
||||
lexer = normalizeLexer(lexer)
|
||||
return RenderCodeByLexer(lexer, code), lexer, formatLexerName(lexer.Config().Name)
|
||||
}
|
||||
|
||||
// RenderCodeByLexer returns a HTML version of code string with chroma syntax highlighting classes
|
||||
// RenderCodeByLexer returns an HTML version of code string with chroma syntax highlighting classes
|
||||
func RenderCodeByLexer(lexer chroma.Lexer, code string) template.HTML {
|
||||
if lexer == lexers.Fallback {
|
||||
return htmlEscape(code) // chroma is 3x slower for plain text
|
||||
}
|
||||
formatter := chromahtml.New(chromahtml.WithClasses(true),
|
||||
chromahtml.WithLineNumbers(false),
|
||||
chromahtml.PreventSurroundingPre(true),
|
||||
@@ -128,6 +140,7 @@ func RenderFullFile(fileName, language string, code []byte) ([]template.HTML, st
|
||||
return renderPlainText(code), formatLexerName(LanguagePlaintext)
|
||||
}
|
||||
lexer := detectChromaLexerWithAnalyze(fileName, language, code)
|
||||
lexer = normalizeLexer(lexer)
|
||||
lexerName := formatLexerName(lexer.Config().Name)
|
||||
rendered := RenderCodeByLexer(lexer, util.UnsafeBytesToString(code))
|
||||
unsafeLines := UnsafeSplitHighlightedLines(rendered)
|
||||
|
||||
@@ -215,7 +215,7 @@ func normalizeFileNameLang(fileName, fileLang string) (string, string) {
|
||||
|
||||
func DetectChromaLexerByFileName(fileName, fileLang string) chroma.Lexer {
|
||||
lexer, _ := detectChromaLexerByFileName(fileName, fileLang)
|
||||
return lexer
|
||||
return normalizeLexer(lexer)
|
||||
}
|
||||
|
||||
func detectChromaLexerByFileName(fileName, fileLang string) (_ chroma.Lexer, byLang bool) {
|
||||
@@ -289,14 +289,14 @@ func detectChromaLexerWithAnalyze(fileName, lang string, code []byte) chroma.Lex
|
||||
|
||||
// if lang is provided, and it matches a lexer, use it directly
|
||||
if byLang {
|
||||
return chroma.Coalesce(lexer)
|
||||
return lexer
|
||||
}
|
||||
|
||||
// if a lexer is detected and there is no conflict for the file extension, use it directly
|
||||
fileExt := path.Ext(fileName)
|
||||
_, hasConflicts := chromaLexers().conflictingExtLangMap[fileExt]
|
||||
if !hasConflicts && lexer != lexers.Fallback {
|
||||
return chroma.Coalesce(lexer)
|
||||
return lexer
|
||||
}
|
||||
|
||||
// try to detect language by content, for best guessing for the language
|
||||
@@ -308,5 +308,5 @@ func detectChromaLexerWithAnalyze(fileName, lang string, code []byte) chroma.Lex
|
||||
log.Warn("No chroma lexer found for enry detected language: %s (file: %s), need to fix the language mapping between enry and chroma.", analyzedLanguage, fileName)
|
||||
}
|
||||
}
|
||||
return chroma.Coalesce(lexer)
|
||||
return lexer
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/languagestats"
|
||||
"gitea.dev/modules/globallock"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/process"
|
||||
@@ -23,6 +24,13 @@ func (db *DBIndexer) Index(id int64) error {
|
||||
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().ShutdownContext(), fmt.Sprintf("Stats.DB Index Repo[%d]", id))
|
||||
defer finished()
|
||||
|
||||
// the unique queue doesn't stop two workers from indexing the same repository at once
|
||||
releaser, err := globallock.Lock(ctx, fmt.Sprintf("repo_stats_index_%d", id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer releaser()
|
||||
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,7 +5,6 @@ package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json" //nolint:depguard // this package wraps it
|
||||
"io"
|
||||
)
|
||||
@@ -81,38 +80,3 @@ func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
||||
func Valid(data []byte) bool {
|
||||
return json.Valid(data)
|
||||
}
|
||||
|
||||
// UnmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
|
||||
// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
|
||||
func UnmarshalHandleDoubleEncode(bs []byte, v any) error {
|
||||
if len(bs) == 0 {
|
||||
// json.Unmarshal will report errors if input is empty (nil or zero-length)
|
||||
// It seems that XORM ignores the nil but still passes zero-length string into this function
|
||||
// To be consistent, we should treat all empty inputs as success
|
||||
return nil
|
||||
}
|
||||
err := json.Unmarshal(bs, v)
|
||||
if err != nil {
|
||||
ok := true
|
||||
rs := []byte{}
|
||||
temp := make([]byte, 2)
|
||||
for _, rn := range string(bs) {
|
||||
if rn > 0xffff {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
binary.LittleEndian.PutUint16(temp, uint16(rn))
|
||||
rs = append(rs, temp...)
|
||||
}
|
||||
if ok {
|
||||
if len(rs) > 1 && rs[0] == 0xff && rs[1] == 0xfe {
|
||||
rs = rs[2:]
|
||||
}
|
||||
err = json.Unmarshal(rs, v)
|
||||
}
|
||||
}
|
||||
if err != nil && len(bs) > 2 && bs[0] == 0xff && bs[1] == 0xfe {
|
||||
err = json.Unmarshal(bs[2:], v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -10,14 +10,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGiteaDBJSONUnmarshal(t *testing.T) {
|
||||
var m map[any]any
|
||||
err := UnmarshalHandleDoubleEncode(nil, &m)
|
||||
assert.NoError(t, err)
|
||||
err = UnmarshalHandleDoubleEncode([]byte(""), &m)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIndent(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
err := Indent(buf, []byte(`{"a":1}`), ">", " ")
|
||||
|
||||
@@ -26,18 +26,15 @@ import (
|
||||
// and more closely matches the output from the shurcooL sanitizer
|
||||
// In particular Unicode letters and numbers are a lot more than a-zA-Z0-9...
|
||||
func CleanValue(value []byte) []byte {
|
||||
value = bytes.TrimSpace(value)
|
||||
rs := bytes.Runes(value)
|
||||
result := make([]rune, 0, len(rs))
|
||||
for _, r := range rs {
|
||||
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' || r == '-' {
|
||||
result = append(result, unicode.ToLower(r))
|
||||
return bytes.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r == ' ':
|
||||
return '-'
|
||||
case r == '-' || unicode.In(r, unicode.L, unicode.M, unicode.Nd, unicode.Nl, unicode.Pc, unicode.Other_Alphabetic):
|
||||
return unicode.ToLower(r)
|
||||
}
|
||||
if unicode.IsSpace(r) {
|
||||
result = append(result, '-')
|
||||
}
|
||||
}
|
||||
return []byte(string(result))
|
||||
return -1
|
||||
}, bytes.TrimSpace(value))
|
||||
}
|
||||
|
||||
// Most of what follows is a subtly changed version of github.com/yuin/goldmark/extension/footnote.go
|
||||
|
||||
@@ -50,9 +50,12 @@ func TestCleanValue(t *testing.T) {
|
||||
{"tes\\a", "tesa"},
|
||||
{"tes/a", "tesa"},
|
||||
{"a啊啊b", "a啊啊b"},
|
||||
{"c🤔️🤔️d", "cd"},
|
||||
{"c🤔️🤔️d", "c\ufe0f\ufe0fd"},
|
||||
{"a⚡a", "aa"},
|
||||
{"e.~f", "ef"},
|
||||
{"a \u2003 b", "a--b"},
|
||||
{"हिन्दी", "हिन्दी"},
|
||||
{"m²", "m"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, []byte(test.expect), CleanValue([]byte(test.param)), test.param)
|
||||
|
||||
@@ -70,7 +70,7 @@ func (r *RenderInternal) ProtectSafeAttrs(content template.HTML) template.HTML {
|
||||
return template.HTML(reAttrClass().ReplaceAllString(string(content), `$1 data-attr-class="`+r.secureIDPrefix+`$2"$3`))
|
||||
}
|
||||
|
||||
func (r *RenderInternal) FormatWithSafeAttrs(w io.Writer, fmt template.HTML, a ...any) error {
|
||||
func (r *RenderInternal) FormatWithSafeAttrsf(w io.Writer, fmt template.HTML, a ...any) error {
|
||||
htmlStr := r.ProtectSafeAttrs(htmlutil.HTMLFormat(fmt, a...))
|
||||
_, err := io.WriteString(w, string(htmlStr))
|
||||
return err
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/highlight"
|
||||
"gitea.dev/modules/htmlutil"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/markup/internal"
|
||||
"gitea.dev/modules/markup/markdown/math"
|
||||
@@ -145,23 +144,29 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(KindSummary, r.renderSummary)
|
||||
reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
|
||||
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
|
||||
reg.Register(ast.KindFencedCodeBlock, r.renderCodeBlock)
|
||||
reg.Register(KindAttention, r.renderAttention)
|
||||
reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||
reg.Register(KindRawHTML, r.renderRawHTML)
|
||||
}
|
||||
|
||||
// renderCodeBlock wraps indented code blocks like the fenced renderer
|
||||
func (r *HTMLRenderer) renderCodeBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
preAttrs, codeAttrs := highlight.CodeBlockAttributes("") // no language
|
||||
opening := r.renderInternal.ProtectSafeAttrs(htmlutil.HTMLFormat(`<div class="code-block-container code-overflow-scroll"><pre %s><code %s>`, preAttrs, codeAttrs))
|
||||
if _, err := w.WriteString(string(opening)); err != nil {
|
||||
var language string
|
||||
if fenced, ok := n.(*ast.FencedCodeBlock); ok {
|
||||
language = string(fenced.Language(source))
|
||||
}
|
||||
preAttrs, codeAttrs := highlight.CodeBlockAttributes(language)
|
||||
err := r.renderInternal.FormatWithSafeAttrsf(w, `<div class="code-block-container code-overflow-scroll"><pre %s><code %s>`, preAttrs, codeAttrs)
|
||||
if err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
lines := n.Lines()
|
||||
for i := 0; i < lines.Len(); i++ {
|
||||
line := lines.At(i)
|
||||
r.Writer.RawWrite(w, line.Value(source))
|
||||
|
||||
code := n.Lines().Value(source)
|
||||
lexer := highlight.DetectChromaLexerByFileName("", language)
|
||||
renderedHTML := highlight.RenderCodeByLexer(lexer, string(code))
|
||||
if _, err = w.WriteString(string(renderedHTML)); err != nil {
|
||||
return ast.WalkStop, err
|
||||
}
|
||||
} else {
|
||||
if _, err := w.WriteString("</code></pre></div>"); err != nil {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/highlight"
|
||||
"gitea.dev/modules/htmlutil"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/markup"
|
||||
@@ -22,9 +21,7 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
giteautil "gitea.dev/modules/util"
|
||||
|
||||
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/yuin/goldmark"
|
||||
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
@@ -68,32 +65,6 @@ func newParserContext(ctx *markup.RenderContext) parser.Context {
|
||||
return pc
|
||||
}
|
||||
|
||||
type GoldmarkRender struct {
|
||||
ctx *markup.RenderContext
|
||||
|
||||
goldmarkMarkdown goldmark.Markdown
|
||||
}
|
||||
|
||||
func (r *GoldmarkRender) Convert(source []byte, writer io.Writer, opts ...parser.ParseOption) error {
|
||||
return r.goldmarkMarkdown.Convert(source, writer, opts...)
|
||||
}
|
||||
|
||||
func (r *GoldmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
|
||||
if entering {
|
||||
languageBytes, _ := c.Language()
|
||||
preAttrs, codeAttrs := highlight.CodeBlockAttributes(string(languageBytes))
|
||||
err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<div class="code-block-container code-overflow-scroll"><pre %s><code %s>`, preAttrs, codeAttrs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err := w.WriteString("</code></pre></div>")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type goldmarkEmphasisParser struct {
|
||||
parser.InlineParser
|
||||
}
|
||||
@@ -130,11 +101,10 @@ func goldmarkDefaultParser() parser.Parser {
|
||||
}
|
||||
|
||||
// SpecializedMarkdown sets up the Gitea specific markdown extensions
|
||||
func SpecializedMarkdown(ctx *markup.RenderContext) *GoldmarkRender {
|
||||
func SpecializedMarkdown(ctx *markup.RenderContext) goldmark.Markdown {
|
||||
// TODO: it could use a pool to cache the renderers to reuse them with different contexts
|
||||
// at the moment it is fast enough (see the benchmarks)
|
||||
r := &GoldmarkRender{ctx: ctx}
|
||||
r.goldmarkMarkdown = goldmark.New(
|
||||
return goldmark.New(
|
||||
goldmark.WithParser(goldmarkDefaultParser()),
|
||||
goldmark.WithExtensions(
|
||||
extension.NewTable(extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)),
|
||||
@@ -142,13 +112,6 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GoldmarkRender {
|
||||
extension.TaskList,
|
||||
extension.DefinitionList,
|
||||
common.FootnoteExtension,
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithFormatOptions(
|
||||
chromahtml.WithClasses(true),
|
||||
chromahtml.PreventSurroundingPre(true),
|
||||
),
|
||||
highlighting.WithWrapperRenderer(r.highlightingRenderer),
|
||||
),
|
||||
math.NewExtension(&ctx.RenderInternal, math.Options{
|
||||
Enabled: setting.Markdown.EnableMath,
|
||||
ParseInlineDollar: setting.Markdown.MathCodeBlockOptions.ParseInlineDollar,
|
||||
@@ -161,15 +124,11 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GoldmarkRender {
|
||||
parser.WithAttribute(),
|
||||
parser.WithASTTransformers(util.Prioritized(NewASTTransformer(&ctx.RenderInternal), 10000)),
|
||||
),
|
||||
goldmark.WithRendererOptions(html.WithUnsafe()),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
renderer.WithNodeRenderers(util.Prioritized(NewHTMLRenderer(&ctx.RenderInternal), 10)),
|
||||
),
|
||||
)
|
||||
|
||||
// Override the original Tasklist renderer!
|
||||
r.goldmarkMarkdown.Renderer().AddOptions(
|
||||
renderer.WithNodeRenderers(util.Prioritized(NewHTMLRenderer(&ctx.RenderInternal), 10)),
|
||||
)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// render calls goldmark render to convert Markdown to HTML
|
||||
@@ -275,14 +234,18 @@ func FeedExcerpt(ctx context.Context, content string) string {
|
||||
pc := newParserContext(rctx)
|
||||
pc.Set(renderConfigKey, &RenderConfig{})
|
||||
start, texts := len(content), []text.Segment(nil)
|
||||
_ = ast.Walk(SpecializedMarkdown(rctx).goldmarkMarkdown.Parser().Parse(text.NewReader([]byte(content)), parser.WithContext(pc)), func(node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
|
||||
mdParser := SpecializedMarkdown(rctx).Parser()
|
||||
mdReader := text.NewReader([]byte(content))
|
||||
mdWalker := func(node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering && node.Type() == ast.TypeBlock && node.Lines().Len() > 0 {
|
||||
start = min(start, strings.LastIndexByte(content[:node.Lines().At(0).Start], '\n')+1)
|
||||
} else if textNode, ok := node.(*ast.Text); ok && entering && node.Parent().Type() == ast.TypeBlock {
|
||||
texts = append(texts, textNode.Segment)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
})
|
||||
}
|
||||
_ = ast.Walk(mdParser.Parse(mdReader, parser.WithContext(pc)), mdWalker)
|
||||
|
||||
excerpt := giteautil.EllipsisDisplayString(content[start:], 200)
|
||||
kept, ok := strings.CutSuffix(excerpt, "…")
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
// Block render output:
|
||||
// <pre class="code-block is-loading"><code class="language-math">...</code></pre>
|
||||
//
|
||||
// Keep in mind that there is another "code block" render in "func (r *GoldmarkRender) highlightingRenderer"
|
||||
// "highlightingRenderer" outputs the math block with extra "chroma" class:
|
||||
// Keep in mind that there is another "code block" render in "func (r *HTMLRenderer) renderCodeBlock"
|
||||
// "renderCodeBlock" outputs the math block with extra "chroma" class:
|
||||
// <pre class="code-block is-loading"><code class="chroma language-math">...</code></pre>
|
||||
//
|
||||
// Special classes:
|
||||
|
||||
@@ -39,7 +39,7 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
|
||||
r.Writer.RawWrite(w, value)
|
||||
}
|
||||
case *ColorPreview:
|
||||
_ = r.renderInternal.FormatWithSafeAttrs(w, `<span class="color-preview" style="background-color: %s"></span>`, v.Color)
|
||||
_ = r.renderInternal.FormatWithSafeAttrsf(w, `<span class="color-preview" style="background-color: %s"></span>`, v.Color)
|
||||
}
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/niklasfasching/go-org/org"
|
||||
)
|
||||
|
||||
@@ -58,10 +57,8 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||
|
||||
preAttrs, codeAttrs := highlight.CodeBlockAttributes(lang)
|
||||
lexer := highlight.DetectChromaLexerByFileName("", lang) // don't use content to detect, it is too slow
|
||||
lexer = chroma.Coalesce(lexer)
|
||||
|
||||
sb := &strings.Builder{}
|
||||
_ = ctx.RenderInternal.FormatWithSafeAttrs(sb, `<pre %s><code %s>`, preAttrs, codeAttrs)
|
||||
_ = ctx.RenderInternal.FormatWithSafeAttrsf(sb, `<pre %s><code %s>`, preAttrs, codeAttrs)
|
||||
_, _ = sb.WriteString(string(highlight.RenderCodeByLexer(lexer, source)))
|
||||
_, _ = sb.WriteString("</code></pre>")
|
||||
return sb.String()
|
||||
|
||||
@@ -11,6 +11,8 @@ import "strconv"
|
||||
// * non-existing value are represented as an empty slice (None)
|
||||
// * existing value is represented as a slice with one element (Some)
|
||||
// * multiple values are represented as a slice with multiple elements (Some), the Value is the first element (not well-defined in this case)
|
||||
//
|
||||
// Attention: Option[T] doesn't work with some web middleware binding rules like "MaxSize" at the moment.
|
||||
type Option[T any] []T
|
||||
|
||||
func None[T any]() Option[T] {
|
||||
|
||||
@@ -43,7 +43,7 @@ type WikiPageMetaData struct {
|
||||
// CreateWikiPageOptions form for creating wiki
|
||||
type CreateWikiPageOptions struct {
|
||||
// page title. leave empty to keep unchanged
|
||||
Title string `json:"title" binding:"TrimSpace;Required"`
|
||||
Title string `json:"title" binding:"TrimSpace"`
|
||||
// content must be base64 encoded
|
||||
ContentBase64 string `json:"content_base64"`
|
||||
// optional commit message summarizing the change
|
||||
|
||||
+2
-1
@@ -12,4 +12,5 @@ dev = [
|
||||
|
||||
[tool.djlint]
|
||||
profile="golang"
|
||||
ignore="H005,H013,H016,H020,H021,H030,H045,H046,H054"
|
||||
ignore="H005,H013,H016,H020,H021,H030,H045"
|
||||
include="H018"
|
||||
@@ -49,6 +49,11 @@ func NewWikiPage(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
form := web.GetForm[*api.CreateWikiPageOptions](ctx)
|
||||
// EditWikiPage shares the form but allows an empty title
|
||||
if form.Title == "" {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "title is required")
|
||||
return
|
||||
}
|
||||
wikiName := wiki_service.UserTitleToWebPath("", form.Title)
|
||||
|
||||
if len(form.Message) == 0 {
|
||||
@@ -117,10 +122,9 @@ func EditWikiPage(ctx *context.APIContext) {
|
||||
form := web.GetForm[*api.CreateWikiPageOptions](ctx)
|
||||
|
||||
oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
|
||||
newWikiName := wiki_service.UserTitleToWebPath("", form.Title)
|
||||
|
||||
if len(newWikiName) == 0 {
|
||||
newWikiName = oldWikiName
|
||||
newWikiName := oldWikiName
|
||||
if form.Title != "" {
|
||||
newWikiName = wiki_service.UserTitleToWebPath("", form.Title)
|
||||
}
|
||||
|
||||
if len(form.Message) == 0 {
|
||||
|
||||
@@ -54,71 +54,55 @@ func Profile(ctx *context.Context) {
|
||||
|
||||
// ProfilePost response for change user's profile
|
||||
func ProfilePost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings_title")
|
||||
ctx.Data["PageIsSettingsProfile"] = true
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplSettingsProfile)
|
||||
form := context.GetFetchActionForm[*forms.UpdateProfileForm](ctx)
|
||||
if form == nil {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm[*forms.UpdateProfileForm](ctx)
|
||||
|
||||
if form.Name != "" {
|
||||
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeUsername) {
|
||||
ctx.Flash.Error(ctx.Tr("user.form.change_username_disabled"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings")
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.change_username_disabled"), "Name")
|
||||
return
|
||||
}
|
||||
if err := user_service.RenameUser(ctx, ctx.Doer, form.Name, ctx.Doer); err != nil {
|
||||
switch {
|
||||
case user_model.IsErrUserIsNotLocal(err):
|
||||
ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user"))
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.username_change_not_local_user"), "Name")
|
||||
case user_model.IsErrUserAlreadyExist(err):
|
||||
ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.username_been_taken"), "Name")
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.Flash.Error(ctx.Tr("user.form.name_reserved", form.Name))
|
||||
ctx.JSONErrorWithField(ctx.Tr("user.form.name_reserved", form.Name), "Name")
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", form.Name))
|
||||
ctx.JSONErrorWithField(ctx.Tr("user.form.name_pattern_not_allowed", form.Name), "Name")
|
||||
case db.IsErrNameCharsNotAllowed(err):
|
||||
ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", form.Name))
|
||||
ctx.JSONErrorWithField(ctx.Tr("user.form.name_chars_not_allowed", form.Name), "Name")
|
||||
default:
|
||||
ctx.ServerError("RenameUser", err)
|
||||
return
|
||||
ctx.JSONErrorAuto(err)
|
||||
}
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
opts := &user_service.UpdateOptions{
|
||||
if form.FullName != nil && user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeFullName) {
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.change_full_name_disabled"), "FullName")
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, ctx.Doer, &user_service.UpdateOptions{
|
||||
KeepEmailPrivate: optional.Some(form.KeepEmailPrivate),
|
||||
FullName: optional.FromPtr(form.FullName),
|
||||
Description: optional.Some(form.Description),
|
||||
Website: optional.Some(form.Website),
|
||||
Location: optional.Some(form.Location),
|
||||
Visibility: optional.Some(form.Visibility),
|
||||
KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
|
||||
}
|
||||
|
||||
if form.FullName != "" {
|
||||
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeFullName) {
|
||||
ctx.Flash.Error(ctx.Tr("user.form.change_full_name_disabled"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings")
|
||||
return
|
||||
}
|
||||
opts.FullName = optional.Some(form.FullName)
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
}); err != nil {
|
||||
ctx.JSONErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("User settings updated: %s", ctx.Doer.Name)
|
||||
ctx.Flash.Success(ctx.Tr("settings.update_profile_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings")
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/user/settings")
|
||||
}
|
||||
|
||||
// UpdateAvatarSetting update user's avatar
|
||||
|
||||
+1
-1
@@ -635,7 +635,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
|
||||
m.Group("/user/settings", func() {
|
||||
m.Get("", user_setting.Profile)
|
||||
m.Post("", web.Bind[*forms.UpdateProfileForm](), user_setting.ProfilePost)
|
||||
m.Post("", user_setting.ProfilePost)
|
||||
m.Post("/update_preferences", user_setting.UpdatePreferences)
|
||||
m.Get("/change_password", auth.MustChangePassword)
|
||||
m.Post("/change_password", web.Bind[*forms.MustChangePasswordForm](), auth.MustChangePasswordPost)
|
||||
|
||||
@@ -13,14 +13,7 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
// .____ ________ _____ __________
|
||||
// | | \______ \ / _ \\______ \
|
||||
// | | | | \ / /_\ \| ___/
|
||||
// | |___ | ` \/ | \ |
|
||||
// |_______ \/_______ /\____|__ /____|
|
||||
// \/ \/ \/
|
||||
|
||||
// Package ldap provide functions & structure to query a LDAP ldap directory
|
||||
// Package ldap provide functions & structure to query an LDAP directory
|
||||
// For now, it's mainly tested again an MS Active Directory service, see README.md for more information
|
||||
|
||||
// Source Basic LDAP authentication service
|
||||
@@ -60,12 +53,8 @@ type Source struct {
|
||||
UserUID string // User Attribute listed in Group
|
||||
}
|
||||
|
||||
// FromDB fills up a LDAPConfig from serialized format.
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
err := json.UnmarshalHandleDoubleEncode(bs, &source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (source *Source) FromDB(bs []byte) (err error) {
|
||||
auth.UnmarshalAuthSourceConfig(bs, source)
|
||||
if source.BindPasswordEncrypt != "" {
|
||||
source.BindPassword, err = secret.DecryptSecret(setting.SecretKey, source.BindPasswordEncrypt)
|
||||
if err != nil {
|
||||
@@ -76,7 +65,6 @@ func (source *Source) FromDB(bs []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a LDAPConfig to a serialized format.
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
var err error
|
||||
source.BindPasswordEncrypt, err = secret.EncryptSecret(setting.SecretKey, source.BindPassword)
|
||||
|
||||
@@ -33,12 +33,11 @@ type Source struct {
|
||||
ExternalIDClaim string
|
||||
}
|
||||
|
||||
// FromDB fills up an OAuth2Config from serialized format.
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &source)
|
||||
auth.UnmarshalAuthSourceConfig(bs, source)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports an OAuth2Config to a serialized format.
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
@@ -8,13 +8,6 @@ import (
|
||||
"gitea.dev/modules/json"
|
||||
)
|
||||
|
||||
// __________ _____ _____
|
||||
// \______ \/ _ \ / \
|
||||
// | ___/ /_\ \ / \ / \
|
||||
// | | / | \/ Y \
|
||||
// |____| \____|__ /\____|__ /
|
||||
// \/ \/
|
||||
|
||||
// Source holds configuration for the PAM login source.
|
||||
type Source struct {
|
||||
auth.ConfigBase `json:"-"`
|
||||
@@ -23,12 +16,11 @@ type Source struct {
|
||||
EmailDomain string
|
||||
}
|
||||
|
||||
// FromDB fills up a PAMConfig from serialized format.
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &source)
|
||||
auth.UnmarshalAuthSourceConfig(bs, source)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports a PAMConfig to a serialized format.
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
@@ -8,13 +8,6 @@ import (
|
||||
"gitea.dev/modules/json"
|
||||
)
|
||||
|
||||
// _________ __________________________
|
||||
// / _____/ / \__ ___/\______ \
|
||||
// \_____ \ / \ / \| | | ___/
|
||||
// / \/ Y \ | | |
|
||||
// /_______ /\____|__ /____| |____|
|
||||
// \/ \/
|
||||
|
||||
// Source holds configuration for the SMTP login source.
|
||||
type Source struct {
|
||||
auth.ConfigBase `json:"-"`
|
||||
@@ -29,12 +22,11 @@ type Source struct {
|
||||
DisableHelo bool
|
||||
}
|
||||
|
||||
// FromDB fills up an SMTPConfig from serialized format.
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &source)
|
||||
auth.UnmarshalAuthSourceConfig(bs, source)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports an SMTPConfig to a serialized format.
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
@@ -8,13 +8,6 @@ import (
|
||||
"gitea.dev/modules/json"
|
||||
)
|
||||
|
||||
// _________ ___________________.___
|
||||
// / _____// _____/\______ \ |
|
||||
// \_____ \ \_____ \ | ___/ |
|
||||
// / \/ \ | | | |
|
||||
// /_______ /_______ / |____| |___|
|
||||
// \/ \/
|
||||
|
||||
// Source holds configuration for SSPI single sign-on.
|
||||
type Source struct {
|
||||
auth.ConfigBase `json:"-"`
|
||||
@@ -26,14 +19,13 @@ type Source struct {
|
||||
DefaultLanguage string
|
||||
}
|
||||
|
||||
// FromDB fills up an SSPIConfig from serialized format.
|
||||
func (cfg *Source) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
func (source *Source) FromDB(bs []byte) error {
|
||||
auth.UnmarshalAuthSourceConfig(bs, source)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToDB exports an SSPIConfig to a serialized format.
|
||||
func (cfg *Source) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -160,8 +160,8 @@ type IntrospectTokenForm struct {
|
||||
// UpdateProfileForm form for updating profile
|
||||
type UpdateProfileForm struct {
|
||||
middleware.FormDefaultValidator
|
||||
Name string `binding:"Username;MaxSize(40)"`
|
||||
FullName string `binding:"MaxSize(100)"`
|
||||
Name string `binding:"Username;MaxSize(40)"`
|
||||
FullName *string `binding:"MaxSize(100)"`
|
||||
KeepEmailPrivate bool
|
||||
Website string `binding:"ValidSiteUrl;MaxSize(255)"`
|
||||
Location string `binding:"MaxSize(50)"`
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -15,17 +15,9 @@
|
||||
<a id="collapse-files-btn" class="item">{{ctx.Locale.Tr "repo.pulls.collapse_files"}}</a>
|
||||
{{if .Issue.Index}}
|
||||
{{if .ShowOutdatedComments}}
|
||||
<a class="item" href="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated=false">
|
||||
<label class="tw-pointer-events-none">
|
||||
{{ctx.Locale.Tr "repo.issues.review.option.hide_outdated_comments"}}
|
||||
</label>
|
||||
</a>
|
||||
<a class="item" href="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated=false">{{ctx.Locale.Tr "repo.issues.review.option.hide_outdated_comments"}}</a>
|
||||
{{else}}
|
||||
<a class="item" href="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated=true">
|
||||
<label class="tw-pointer-events-none">
|
||||
{{ctx.Locale.Tr "repo.issues.review.option.show_outdated_comments"}}
|
||||
</label>
|
||||
</a>
|
||||
<a class="item" href="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated=true">{{ctx.Locale.Tr "repo.issues.review.option.show_outdated_comments"}}</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
<div class="ui dropdown tiny basic button" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.whitespace_button"}}">
|
||||
{{svg "gitea-whitespace"}}
|
||||
<div class="menu">
|
||||
<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=show-all&show-outdated={{$.ShowOutdatedComments}}">
|
||||
<label class="tw-pointer-events-none">
|
||||
<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "show-all"}} checked{{end}}>
|
||||
{{ctx.Locale.Tr "repo.diff.whitespace_show_everything"}}
|
||||
</label>
|
||||
<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=show-all&show-outdated={{$.ShowOutdatedComments}}" aria-current="{{eq .WhitespaceBehavior "show-all"}}">
|
||||
{{svg "octicon-check" 16 (Iif (eq .WhitespaceBehavior "show-all") "" "tw-invisible")}}
|
||||
{{ctx.Locale.Tr "repo.diff.whitespace_show_everything"}}
|
||||
</a>
|
||||
<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-all&show-outdated={{$.ShowOutdatedComments}}">
|
||||
<label class="tw-pointer-events-none">
|
||||
<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-all"}} checked{{end}}>
|
||||
{{ctx.Locale.Tr "repo.diff.whitespace_ignore_all_whitespace"}}
|
||||
</label>
|
||||
<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-all&show-outdated={{$.ShowOutdatedComments}}" aria-current="{{eq .WhitespaceBehavior "ignore-all"}}">
|
||||
{{svg "octicon-check" 16 (Iif (eq .WhitespaceBehavior "ignore-all") "" "tw-invisible")}}
|
||||
{{ctx.Locale.Tr "repo.diff.whitespace_ignore_all_whitespace"}}
|
||||
</a>
|
||||
<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-change&show-outdated={{$.ShowOutdatedComments}}">
|
||||
<label class="tw-pointer-events-none">
|
||||
<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-change"}} checked{{end}}>
|
||||
{{ctx.Locale.Tr "repo.diff.whitespace_ignore_amount_changes"}}
|
||||
</label>
|
||||
<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-change&show-outdated={{$.ShowOutdatedComments}}" aria-current="{{eq .WhitespaceBehavior "ignore-change"}}">
|
||||
{{svg "octicon-check" 16 (Iif (eq .WhitespaceBehavior "ignore-change") "" "tw-invisible")}}
|
||||
{{ctx.Locale.Tr "repo.diff.whitespace_ignore_amount_changes"}}
|
||||
</a>
|
||||
<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-eol&show-outdated={{$.ShowOutdatedComments}}">
|
||||
<label class="tw-pointer-events-none">
|
||||
<input class="tw-mr-2 tw-pointer-events-none" type="radio"{{if eq .WhitespaceBehavior "ignore-eol"}} checked{{end}}>
|
||||
{{ctx.Locale.Tr "repo.diff.whitespace_ignore_at_eol"}}
|
||||
</label>
|
||||
<a class="item" href="?style={{if .IsSplitStyle}}split{{else}}unified{{end}}&whitespace=ignore-eol&show-outdated={{$.ShowOutdatedComments}}" aria-current="{{eq .WhitespaceBehavior "ignore-eol"}}">
|
||||
{{svg "octicon-check" 16 (Iif (eq .WhitespaceBehavior "ignore-eol") "" "tw-invisible")}}
|
||||
{{ctx.Locale.Tr "repo.diff.whitespace_ignore_at_eol"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{{if $attachments}}
|
||||
<div class="card-attachment-images">
|
||||
{{range $att := $attachments}}
|
||||
<img loading="lazy" src="{{$att.DownloadURL ctx}}" alt="{{$att.Name}}" />
|
||||
<img loading="lazy" src="{{$att.DownloadURL ctx}}" alt="{{$att.Name}}">
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="ui two column grid">
|
||||
<div class="column">
|
||||
<strong>{{ctx.Locale.Tr "repo.issues.choose.blank"}}</strong>
|
||||
<br/>{{ctx.Locale.Tr "repo.issues.choose.blank_about"}}
|
||||
<br>{{ctx.Locale.Tr "repo.issues.choose.blank_about"}}
|
||||
</div>
|
||||
<div class="column tw-text-right">
|
||||
<a href="{{.RepoLink}}/issues/new?{{$.PrefillQuery}}" class="ui primary button">{{ctx.Locale.Tr "repo.issues.choose.get_started"}}</a>
|
||||
|
||||
@@ -466,19 +466,19 @@
|
||||
</p>
|
||||
<div class="ui dropdown selection">
|
||||
<select name="projects_mode">
|
||||
<option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option>
|
||||
<option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option>
|
||||
<option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option>
|
||||
<option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option>
|
||||
<option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option>
|
||||
<option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option>
|
||||
</select>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="default text">
|
||||
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "repo")}}
|
||||
{{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "repo")}}
|
||||
{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}
|
||||
{{end}}
|
||||
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "owner")}}
|
||||
{{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "owner")}}
|
||||
{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}
|
||||
{{end}}
|
||||
{{if (eq $projectsUnit.ProjectsConfig.GetProjectsMode "all")}}
|
||||
{{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "all")}}
|
||||
{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
{{$addFilePath = ""}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
<button type="button" class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanContentChange}}disabled{{end}}>
|
||||
<div class="ui dropdown basic compact jump button repo-add-file{{if not .Repository.CanContentChange}} disabled{{end}}"{{if not .Repository.CanContentChange}} tabindex="-1" aria-disabled="true"{{end}}>
|
||||
{{ctx.Locale.Tr "repo.editor.add_file"}}
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
@@ -86,10 +86,10 @@
|
||||
{{svg "octicon-diff" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.editor.patch"}}
|
||||
</a>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{if and (not .IsViewFile) (not $isTreePathRoot)}}
|
||||
<button type="button" class="ui dropdown basic compact jump button tw-px-3" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
|
||||
<div class="ui dropdown basic compact jump button tw-px-3" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
|
||||
{{svg "octicon-kebab-horizontal"}}
|
||||
<div class="menu">
|
||||
<a class="item" data-clipboard-text="{{.Repository.HTMLURL ctx}}/src/commit/{{.CommitID}}/{{PathEscapeSegments .TreePath}}">
|
||||
@@ -107,7 +107,7 @@
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<!-- Only show clone panel in repository home page -->
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<div class="content">
|
||||
<div class="ui warning message">{{ctx.Locale.Tr "user.block.info"}} <a target="_blank" href="https://docs.gitea.com/usage/access-control/blocking-user">{{ctx.Locale.Tr "user.block.info.docs"}}</a></div>
|
||||
<form class="ui form modal-form" method="post">
|
||||
<input type="hidden" name="action" value="block" />
|
||||
<input type="hidden" name="blockee" class="modal-blockee" />
|
||||
<input type="hidden" name="action" value="block">
|
||||
<input type="hidden" name="blockee" class="modal-blockee">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "user.block.user_to_block"}}: <span class="tw-text-red modal-blockee-name"></span></label>
|
||||
</div>
|
||||
|
||||
@@ -14,22 +14,19 @@
|
||||
<form class="ui form" action="{{.SignInLink}}" method="post">
|
||||
<div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
|
||||
<label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label>
|
||||
<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required tabindex="1">
|
||||
<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
|
||||
</div>
|
||||
{{if or (not .DisablePassword) .LinkAccountMode}}
|
||||
<div class="required field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
|
||||
<div class="tw-flex tw-mb-1">
|
||||
<label for="password" class="tw-flex-1">{{ctx.Locale.Tr "password"}}</label>
|
||||
<a href="{{AppSubUrl}}/user/forgot_password" tabindex="4">{{ctx.Locale.Tr "auth.forgot_password"}}</a>
|
||||
</div>
|
||||
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="current-password" required tabindex="2">
|
||||
<label for="password">{{ctx.Locale.Tr "password"}}</label>
|
||||
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="current-password" required>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if not .LinkAccountMode}}
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
|
||||
<input name="remember" type="checkbox" tabindex="5">
|
||||
<input name="remember" type="checkbox">
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -37,7 +34,7 @@
|
||||
{{template "user/auth/captcha" .}}
|
||||
|
||||
<div class="field">
|
||||
<button type="submit" class="ui primary button tw-w-full" tabindex="3">
|
||||
<button type="submit" class="ui primary button tw-w-full">
|
||||
{{if .LinkAccountMode}}
|
||||
{{ctx.Locale.Tr "auth.oauth_signin_submit"}}
|
||||
{{else}}
|
||||
@@ -45,6 +42,11 @@
|
||||
{{end}}
|
||||
</button>
|
||||
</div>
|
||||
{{if or (not .DisablePassword) .LinkAccountMode}}
|
||||
<div class="field tw-text-center">
|
||||
<a href="{{AppSubUrl}}/user/forgot_password">{{ctx.Locale.Tr "auth.forgot_password"}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</form>
|
||||
{{end}}{{/*end if .EnablePasswordSignInForm*/}}
|
||||
{{$showExternalAuthMethods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
|
||||
@@ -59,14 +61,18 @@
|
||||
|
||||
{{if or .EnablePasskeyAuth .ShowRegistrationButton}}
|
||||
<div class="ui container fluid">
|
||||
<div class="ui attached segment header top tw-max-w-2xl tw-m-auto tw-flex tw-flex-col tw-items-center">
|
||||
<div class="ui segment flex-relaxed-list tw-items-center">
|
||||
{{if .EnablePasskeyAuth}}
|
||||
{{template "user/auth/webauthn_error" .}}
|
||||
<a class="signin-passkey">{{ctx.Locale.Tr "auth.signin_passkey"}}</a>
|
||||
<div class="item signin-passkey-container">
|
||||
<div class="tw-flex tw-flex-col tw-items-center">
|
||||
{{template "user/auth/webauthn_error" .}}
|
||||
<button type="button" class="btn interact-link signin-passkey tw-p-1">{{ctx.Locale.Tr "auth.signin_passkey"}}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .ShowRegistrationButton}}
|
||||
<div class="field">
|
||||
<div class="item">
|
||||
<span>{{ctx.Locale.Tr "auth.need_account"}}</span>
|
||||
<a href="{{AppSubUrl}}/user/sign_up">{{ctx.Locale.Tr "auth.sign_up_now"}}</a>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>{{ctx.Locale.Tr "settings.profile_desc"}}</p>
|
||||
<form class="ui form" action="{{.Link}}" method="post" data-global-init="initUserSettingsForm">
|
||||
<form class="ui form form-fetch-action" action="{{.Link}}" method="post" data-global-init="initUserSettingsForm">
|
||||
<div class="required field {{if .Err_Name}}error{{end}}">
|
||||
<label for="username">{{ctx.Locale.Tr "username"}}
|
||||
<span class="tw-text-red tw-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span>
|
||||
|
||||
+13
-3
@@ -7,11 +7,21 @@ test('homepage', async ({page}) => {
|
||||
await expect(page.getByRole('img', {name: 'Logo'})).toHaveAttribute('src', '/assets/img/logo.svg');
|
||||
});
|
||||
|
||||
test('login form and logout', async ({page}) => {
|
||||
test('login form in tab order and logout', async ({page}) => {
|
||||
await page.goto('/user/login');
|
||||
await page.getByLabel('Username or Email Address').fill(env.GITEA_TEST_E2E_USER);
|
||||
await page.getByLabel('Password').fill(env.GITEA_TEST_E2E_PASSWORD);
|
||||
await page.getByRole('button', {name: 'Sign In'}).click();
|
||||
await page.keyboard.press('Tab');
|
||||
const password = page.getByLabel('Password');
|
||||
await expect(password).toBeFocused();
|
||||
await password.fill(env.GITEA_TEST_E2E_PASSWORD);
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.getByLabel('Remember This Device')).toBeFocused();
|
||||
await page.keyboard.press('Tab');
|
||||
const signIn = page.getByRole('button', {name: 'Sign In', exact: true});
|
||||
await expect(signIn).toBeFocused();
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.getByRole('link', {name: 'Forgot password?'})).toBeFocused();
|
||||
await signIn.click();
|
||||
await expect(page.getByRole('link', {name: 'Sign In'})).toBeHidden();
|
||||
await logout(page);
|
||||
});
|
||||
@@ -9,9 +9,11 @@ test('update profile biography', async ({page, request}) => {
|
||||
await page.goto('/user/settings');
|
||||
await page.getByLabel('Biography').fill(bio);
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByText('Your profile has been updated.', {exact: true})).toBeVisible();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue(bio);
|
||||
await page.getByLabel('Biography').fill('');
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByText('Your profile has been updated.', {exact: true})).toBeVisible();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue('');
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ async function signInWithPassword(page: Page, username: string) {
|
||||
await page.goto('/user/login');
|
||||
await page.getByLabel('Username or Email Address').fill(username);
|
||||
await page.getByLabel('Password').fill(testUserPassword);
|
||||
await page.getByRole('button', {name: 'Sign In'}).click();
|
||||
await page.getByRole('button', {name: 'Sign In', exact: true}).click();
|
||||
}
|
||||
|
||||
// regression: credProtect level 3 hid the credential from the second-factor login
|
||||
|
||||
@@ -785,7 +785,7 @@ func TestPackageContainer(t *testing.T) {
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
t.Run(fmt.Sprintf("Catalog[%s]", newOwnerName), checkCatalog(newOwnerName))
|
||||
|
||||
@@ -794,6 +794,6 @@ func TestPackageContainer(t *testing.T) {
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
}
|
||||
@@ -172,6 +172,17 @@ func TestAPIGetWikiPage(t *testing.T) {
|
||||
session := loginUser(t, username)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
for _, title := range []string{"", " "} {
|
||||
content := base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "Edited with title %q", title))
|
||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1/wiki/page/Home", &api.CreateWikiPageOptions{
|
||||
Title: title,
|
||||
ContentBase64: content,
|
||||
}).AddTokenAuth(token)
|
||||
page := DecodeJSON(t, MakeRequest(t, req, http.StatusOK), &api.WikiPage{})
|
||||
assert.Equal(t, "Home", page.Title)
|
||||
assert.Equal(t, content, page.ContentBase64)
|
||||
}
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/Page-With-Spaced-Name", username, "repo1")
|
||||
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, &api.CreateWikiPageOptions{
|
||||
@@ -197,13 +208,14 @@ func testAPICreateWikiPage(t *testing.T, session *TestSession, userName, repoNam
|
||||
}
|
||||
|
||||
func TestAPINewWikiPage(t *testing.T) {
|
||||
for _, title := range []string{
|
||||
"New page",
|
||||
"&&&&",
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
for title, status := range map[string]int{
|
||||
"New page": http.StatusCreated,
|
||||
"&&&&": http.StatusCreated,
|
||||
"": http.StatusUnprocessableEntity,
|
||||
" ": http.StatusUnprocessableEntity,
|
||||
} {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
username := "user2"
|
||||
session := loginUser(t, username)
|
||||
testAPICreateWikiPage(t, session, username, "repo1", title, http.StatusCreated)
|
||||
testAPICreateWikiPage(t, session, "user2", "repo1", title, status)
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func testPrivateActivityHelperEnablePrivateActivity(t *testing.T) {
|
||||
"language": "en-US",
|
||||
"keep_activity_private": "1",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc *HTMLDoc) bool {
|
||||
|
||||
@@ -10,14 +10,36 @@ import (
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/translation"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserSettings(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("RevokeOAuth2GrantOfOtherUser", testRevokeOAuth2GrantOfOtherUser)
|
||||
t.Run("Account", testUserSettingsAccount)
|
||||
t.Run("UpdatePassword", testUserSettingsUpdatePassword)
|
||||
t.Run("UpdateEmail", testUserSettingsUpdateEmail)
|
||||
t.Run("DeleteEmail", testUserSettingsDeleteEmail)
|
||||
t.Run("Delete", testUserSettingsDelete)
|
||||
t.Run("Pages", testUserSettingsPages)
|
||||
t.Run("Security", testUserSettingsSecurity)
|
||||
t.Run("Applications", testUserSettingsApplications)
|
||||
t.Run("Keys", testUserSettingsKeys)
|
||||
t.Run("Secrets", testUserSettingsSecrets)
|
||||
t.Run("RenameInvalidUsername", testRenameInvalidUsername)
|
||||
t.Run("RenameReservedUsername", testRenameReservedUsername)
|
||||
t.Run("ChangeFullName", testChangeFullName)
|
||||
t.Run("LocationMapLink", testUserLocationMapLink)
|
||||
t.Run("RenameUsername", testRenameUsername)
|
||||
}
|
||||
|
||||
// Validate that each navbar setting is correct. This checks that the
|
||||
// appropriate context is passed everywhere the navbar is rendered
|
||||
func assertNavbar(t *testing.T, doc *HTMLDoc) {
|
||||
@@ -56,9 +78,7 @@ func WithDisabledFeatures(t *testing.T, features ...string) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRevokeOAuth2GrantOfOtherUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testRevokeOAuth2GrantOfOtherUser(t *testing.T) {
|
||||
// grant 1 belongs to user1, so user2 must not be able to act on it
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings/applications/oauth2/1/revoke/1", nil)
|
||||
@@ -68,9 +88,7 @@ func TestRevokeOAuth2GrantOfOtherUser(t *testing.T) {
|
||||
assert.EqualValues(t, 1, grant.UserID)
|
||||
}
|
||||
|
||||
func TestUserSettingsAccount(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsAccount(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
features []string
|
||||
@@ -123,9 +141,7 @@ func TestUserSettingsAccount(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsUpdatePassword(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsUpdatePassword(t *testing.T) {
|
||||
t.Run("enabled", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -148,9 +164,7 @@ func TestUserSettingsUpdatePassword(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsUpdateEmail(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsUpdateEmail(t *testing.T) {
|
||||
t.Run("credentials disabled", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -183,9 +197,7 @@ func TestUserSettingsUpdateEmail(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserSettingsDeleteEmail(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsDeleteEmail(t *testing.T) {
|
||||
t.Run("credentials disabled", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -196,9 +208,7 @@ func TestUserSettingsDeleteEmail(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsDelete(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsDelete(t *testing.T) {
|
||||
t.Run("deletion disabled", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -210,10 +220,10 @@ func TestUserSettingsDelete(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsPages(t *testing.T) {
|
||||
func testUserSettingsPages(t *testing.T) {
|
||||
for _, page := range []string{"appearance", "packages", "packages/rules/add", "organization", "repos", "blocked_users"} {
|
||||
t.Run(page, func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequest(t, "GET", "/user/settings/"+page)
|
||||
@@ -225,9 +235,7 @@ func TestUserSettingsPages(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserSettingsSecurity(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsSecurity(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
feature string
|
||||
@@ -261,9 +269,7 @@ func TestUserSettingsSecurity(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsApplications(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsApplications(t *testing.T) {
|
||||
t.Run("Applications", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -342,9 +348,7 @@ func TestUserSettingsApplications(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsKeys(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsKeys(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
feature string
|
||||
@@ -384,9 +388,7 @@ func TestUserSettingsKeys(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsSecrets(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsSecrets(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequest(t, "GET", "/user/settings/actions/secrets")
|
||||
if setting.Actions.Enabled {
|
||||
@@ -398,3 +400,144 @@ func TestUserSettingsSecrets(t *testing.T) {
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func testRenameInvalidUsername(t *testing.T) {
|
||||
invalidUsernames := []string{
|
||||
"%2f*",
|
||||
"%2f.",
|
||||
"%2f..",
|
||||
"%00",
|
||||
"thisHas ASpace",
|
||||
"p<A>tho>lo<gical",
|
||||
".",
|
||||
"..",
|
||||
".well-known",
|
||||
".abc",
|
||||
"abc.",
|
||||
"a..bc",
|
||||
"a...bc",
|
||||
"a.-bc",
|
||||
"a._bc",
|
||||
"a_-bc",
|
||||
"a/bc",
|
||||
"☁️",
|
||||
"-",
|
||||
"--diff",
|
||||
"-im-here",
|
||||
"a space",
|
||||
}
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
for _, invalidUsername := range invalidUsernames {
|
||||
t.Logf("Testing username %s", invalidUsername)
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": invalidUsername,
|
||||
"email": "user2@example.com",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
data := DecodeJSON(t, resp, map[string]any{})
|
||||
assert.Equal(t, []any{"Name"}, data["errorFields"])
|
||||
assert.Contains(t,
|
||||
data["errorMessage"],
|
||||
translation.NewLocale("en-US").TrString("form.username_error", "Name"),
|
||||
)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername})
|
||||
}
|
||||
}
|
||||
|
||||
func testRenameReservedUsername(t *testing.T) {
|
||||
// ".", "..", ".well-known" are also reserved but invalid as form input.
|
||||
reservedNames := []string{
|
||||
"api",
|
||||
"openapi3.v1.json",
|
||||
"swagger.v1.json",
|
||||
}
|
||||
patternNotAllowedNames := []string{
|
||||
"name.keys",
|
||||
}
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
locale := translation.NewLocale("en-US")
|
||||
check := func(name, msgKey string) {
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": name,
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
data := DecodeJSON(t, resp, map[string]any{})
|
||||
assert.Equal(t, locale.TrString(msgKey, name), data["errorMessage"])
|
||||
assert.Equal(t, []any{"Name"}, data["errorFields"])
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: name})
|
||||
}
|
||||
for _, name := range reservedNames {
|
||||
check(name, "user.form.name_reserved")
|
||||
}
|
||||
for _, name := range patternNotAllowedNames {
|
||||
check(name, "user.form.name_pattern_not_allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func testChangeFullName(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
postSettings := func(t *testing.T, values map[string]string, expectedStatus int) map[string]any {
|
||||
t.Helper()
|
||||
resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user/settings", values), expectedStatus)
|
||||
return DecodeJSON(t, resp, map[string]any{})
|
||||
}
|
||||
|
||||
t.Run("Change", func(t *testing.T) {
|
||||
postSettings(t, map[string]string{"full_name": "New Name"}, http.StatusOK)
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2", FullName: "New Name"})
|
||||
})
|
||||
|
||||
t.Run("Disabled", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Admin.UserDisabledFeatures, container.SetOf(setting.UserFeatureChangeFullName))()
|
||||
postSettings(t, map[string]string{"location": "Elsewhere"}, http.StatusOK)
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2", FullName: "New Name", Location: "Elsewhere"})
|
||||
|
||||
data := postSettings(t, map[string]string{"full_name": "Other Name"}, http.StatusBadRequest)
|
||||
assert.Equal(t, []any{"FullName"}, data["errorFields"])
|
||||
assert.Equal(t, "Changing full name is disabled.", data["errorMessage"])
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2", FullName: "New Name"})
|
||||
})
|
||||
|
||||
t.Run("Clear", func(t *testing.T) {
|
||||
postSettings(t, map[string]string{"full_name": ""}, http.StatusOK)
|
||||
assert.Empty(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}).FullName)
|
||||
})
|
||||
}
|
||||
|
||||
func testRenameUsername(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": "newUsername",
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "/user/settings", DecodeJSON(t, resp, map[string]any{})["redirect"])
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "newUsername"})
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: "user2"})
|
||||
}
|
||||
|
||||
func testUserLocationMapLink(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.UserLocationMapURL, "https://example/foo/")()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": "user2",
|
||||
"email": "user@example.com",
|
||||
"language": "en-US",
|
||||
"location": "A/b",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "GET", "/user2/")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
AssertHTMLElement(t, htmlDoc, `a[href="https://example/foo/A%2Fb"]`, true)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
@@ -18,7 +17,6 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/translation"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -29,14 +27,10 @@ func TestUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("ViewUser", testViewUser)
|
||||
t.Run("FollowUser", testUserFollow)
|
||||
t.Run("RenameInvalidUsername", testRenameInvalidUsername)
|
||||
t.Run("RenameReservedUsername", testRenameReservedUsername)
|
||||
t.Run("ViewLimitedAndPrivateUserAndRename", testViewLimitedAndPrivateUserAndRename)
|
||||
t.Run("ExportUserGPGKeys", testExportUserGPGKeys)
|
||||
t.Run("GetUserRss", testGetUserRss)
|
||||
t.Run("ListStopWatches", testUserListStopWatches)
|
||||
t.Run("LocationMapLink", testUserLocationMapLink)
|
||||
t.Run("RenameUsername", testRenameUsername)
|
||||
}
|
||||
|
||||
func testViewUser(t *testing.T) {
|
||||
@@ -71,19 +65,6 @@ ssh-rsa AAAA
|
||||
`, resp.Body.String())
|
||||
}
|
||||
|
||||
func testRenameUsername(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": "newUsername",
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "newUsername"})
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: "user2"})
|
||||
}
|
||||
|
||||
func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||
// user 22 is a limited visibility org
|
||||
org22 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
|
||||
@@ -146,7 +127,7 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||
"name": newName,
|
||||
"visibility": "2", // private
|
||||
})
|
||||
session2.MakeRequest(t, req, http.StatusSeeOther)
|
||||
session2.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: newName})
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: oldName})
|
||||
@@ -157,87 +138,6 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||
session.MakeRequest(t, req, http.StatusTemporaryRedirect) // login user2 can visit private visibility user via old name
|
||||
}
|
||||
|
||||
func testRenameInvalidUsername(t *testing.T) {
|
||||
invalidUsernames := []string{
|
||||
"%2f*",
|
||||
"%2f.",
|
||||
"%2f..",
|
||||
"%00",
|
||||
"thisHas ASpace",
|
||||
"p<A>tho>lo<gical",
|
||||
".",
|
||||
"..",
|
||||
".well-known",
|
||||
".abc",
|
||||
"abc.",
|
||||
"a..bc",
|
||||
"a...bc",
|
||||
"a.-bc",
|
||||
"a._bc",
|
||||
"a_-bc",
|
||||
"a/bc",
|
||||
"☁️",
|
||||
"-",
|
||||
"--diff",
|
||||
"-im-here",
|
||||
"a space",
|
||||
}
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
for _, invalidUsername := range invalidUsernames {
|
||||
t.Logf("Testing username %s", invalidUsername)
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": invalidUsername,
|
||||
"email": "user2@example.com",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
assert.Contains(t,
|
||||
htmlDoc.doc.Find(".ui.negative.message").Text(),
|
||||
translation.NewLocale("en-US").TrString("form.username_error", "Name"),
|
||||
)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername})
|
||||
}
|
||||
}
|
||||
|
||||
func testRenameReservedUsername(t *testing.T) {
|
||||
// ".", "..", ".well-known" are also reserved but invalid as form input.
|
||||
reservedNames := []string{
|
||||
"api",
|
||||
"openapi3.v1.json",
|
||||
"swagger.v1.json",
|
||||
}
|
||||
patternNotAllowedNames := []string{
|
||||
"name.keys",
|
||||
}
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
locale := translation.NewLocale("en-US")
|
||||
check := func(name, msgKey string) {
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": name,
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
actualMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.negative.message").Text())
|
||||
assert.Equal(t, locale.TrString(msgKey, name), actualMsg)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: name})
|
||||
}
|
||||
for _, name := range reservedNames {
|
||||
check(name, "user.form.name_reserved")
|
||||
}
|
||||
for _, name := range patternNotAllowedNames {
|
||||
check(name, "user.form.name_pattern_not_allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func testExportUserGPGKeys(t *testing.T) {
|
||||
testExportUserGPGKeys := func(t *testing.T, user, expected string) {
|
||||
t.Helper()
|
||||
@@ -370,24 +270,6 @@ func testUserListStopWatches(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testUserLocationMapLink(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.UserLocationMapURL, "https://example/foo/")()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": "user2",
|
||||
"email": "user@example.com",
|
||||
"language": "en-US",
|
||||
"location": "A/b",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
req = NewRequest(t, "GET", "/user2/")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
AssertHTMLElement(t, htmlDoc, `a[href="https://example/foo/A%2Fb"]`, true)
|
||||
}
|
||||
|
||||
func testUserFollow(t *testing.T) {
|
||||
bot := &user_model.User{
|
||||
Name: "follow-bot",
|
||||
|
||||
@@ -331,11 +331,21 @@ a.suppressed:hover {
|
||||
a:focus-visible,
|
||||
button:focus-visible,
|
||||
.ui.button:focus-visible,
|
||||
.ui.dropdown:not(.selection):focus-visible,
|
||||
[role="button"]:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
.ui.button:focus-visible {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.ui:is(.primary, .red, .green).button:focus-visible,
|
||||
.ui:is(.primary, .red, .green).buttons .button:focus-visible {
|
||||
outline-color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
a.silenced:hover {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ Gitea's private styles use `g-` prefix.
|
||||
.interact-bg:hover { background: var(--color-hover) !important; }
|
||||
.interact-bg:active { background: var(--color-active) !important; }
|
||||
|
||||
/* sometimes, we'd like to make "<button>" look like a link */
|
||||
.interact-link { color: var(--color-primary); cursor: pointer; }
|
||||
.interact-link:hover { text-decoration: underline; }
|
||||
|
||||
/* primary-color dot centered on the top-right corner of the element */
|
||||
.indicator-dot {
|
||||
position: relative;
|
||||
|
||||
@@ -306,6 +306,7 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* sometimes, we'd like to make "<a href>" look like a button */
|
||||
a.btn,
|
||||
a.btn:hover {
|
||||
color: inherit;
|
||||
|
||||
@@ -17,7 +17,8 @@ export function initUserAuthWebAuthn() {
|
||||
|
||||
const errorType = detectWebAuthnSupport();
|
||||
if (errorType) {
|
||||
if (elSignInPasskeyBtn) hideElem(elSignInPasskeyBtn);
|
||||
// no webauthn support, completely hide the passkey related UI elements, including the error message
|
||||
if (elSignInPasskeyBtn) hideElem(elSignInPasskeyBtn.closest('.signin-passkey-container')!);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user