mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-04-23 03:21:35 +02:00
Merge branch 'forgejo' into report_abuse
This commit is contained in:
commit
6ad0f88a45
47 changed files with 539 additions and 94 deletions
|
@ -164,7 +164,7 @@ jobs:
|
|||
|
||||
- name: build container & release
|
||||
if: ${{ secrets.TOKEN != '' }}
|
||||
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.1
|
||||
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.4
|
||||
with:
|
||||
forgejo: "${{ env.GITHUB_SERVER_URL }}"
|
||||
owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"
|
||||
|
@ -183,7 +183,7 @@ jobs:
|
|||
|
||||
- name: build rootless container
|
||||
if: ${{ secrets.TOKEN != '' }}
|
||||
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.1
|
||||
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.4
|
||||
with:
|
||||
forgejo: "${{ env.GITHUB_SERVER_URL }}"
|
||||
owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"
|
||||
|
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
|
||||
- name: copy & sign
|
||||
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.3.1
|
||||
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.3.4
|
||||
with:
|
||||
from-forgejo: ${{ vars.FORGEJO }}
|
||||
to-forgejo: ${{ vars.FORGEJO }}
|
||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
|
||||
runs-on: docker
|
||||
container:
|
||||
image: data.forgejo.org/renovate/renovate:39.171.2
|
||||
image: data.forgejo.org/renovate/renovate:39.178.1
|
||||
|
||||
steps:
|
||||
- name: Load renovate repo cache
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.23-alpine3.21 AS build-env
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 AS build-env
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-direct}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
|
||||
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.23-alpine3.21 AS build-env
|
||||
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 AS build-env
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY=${GOPROXY:-direct}
|
||||
|
@ -50,6 +50,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
|
|||
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
|
||||
|
||||
FROM data.forgejo.org/oci/alpine:3.21
|
||||
ARG RELEASE_VERSION
|
||||
LABEL maintainer="contact@forgejo.org" \
|
||||
org.opencontainers.image.authors="Forgejo" \
|
||||
org.opencontainers.image.url="https://forgejo.org" \
|
||||
|
|
4
Makefile
4
Makefile
|
@ -48,8 +48,8 @@ GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 # renovate: datasour
|
|||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go
|
||||
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.30.0 # renovate: datasource=go
|
||||
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.4.0 # renovate: datasource=go
|
||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.18.0 # renovate: datasource=go
|
||||
RENOVATE_NPM_PACKAGE ?= renovate@39.171.2 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
|
||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.18.1 # renovate: datasource=go
|
||||
RENOVATE_NPM_PACKAGE ?= renovate@39.178.1 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
|
||||
|
||||
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
|
||||
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...
|
||||
|
|
10
go.mod
10
go.mod
|
@ -1,8 +1,8 @@
|
|||
module code.gitea.io/gitea
|
||||
|
||||
go 1.23
|
||||
go 1.24
|
||||
|
||||
toolchain go1.23.6
|
||||
toolchain go1.24.0
|
||||
|
||||
require (
|
||||
code.forgejo.org/f3/gof3/v3 v3.10.2
|
||||
|
@ -84,7 +84,7 @@ require (
|
|||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/prometheus/client_golang v1.21.0
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
|
||||
|
@ -101,7 +101,7 @@ require (
|
|||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
gitlab.com/gitlab-org/api/client-go v0.119.0
|
||||
go.uber.org/mock v0.4.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/net v0.35.0
|
||||
golang.org/x/oauth2 v0.24.0
|
||||
|
@ -239,7 +239,7 @@ require (
|
|||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rhysd/actionlint v1.6.27 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
|
|
12
go.sum
12
go.sum
|
@ -1331,15 +1331,15 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
|||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
|
||||
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
|
@ -1507,8 +1507,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
|||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
|
1
models/fixtures/secret.yml
Normal file
1
models/fixtures/secret.yml
Normal file
|
@ -0,0 +1 @@
|
|||
[] # empty
|
|
@ -7,7 +7,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
func EvaluateDefault(used Used, forSubject LimitSubject) bool {
|
||||
func EvaluateDefault(used Used, forSubject LimitSubject) (bool, int64) {
|
||||
groups := GroupList{
|
||||
&Group{
|
||||
Name: "builtin-default-group",
|
||||
|
|
|
@ -5,6 +5,7 @@ package quota
|
|||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
@ -199,15 +200,20 @@ var affectsMap = map[LimitSubject]LimitSubjects{
|
|||
},
|
||||
}
|
||||
|
||||
func (g *Group) Evaluate(used Used, forSubject LimitSubject) (bool, bool) {
|
||||
// Evaluate returns whether the size used is acceptable for the topic if a rule
|
||||
// was found, and returns the smallest limit of all applicable rules or the
|
||||
// first limit found to be unacceptable for the size used.
|
||||
func (g *Group) Evaluate(used Used, forSubject LimitSubject) (bool, bool, int64) {
|
||||
var found bool
|
||||
foundLimit := int64(math.MaxInt64)
|
||||
for _, rule := range g.Rules {
|
||||
ok, has := rule.Evaluate(used, forSubject)
|
||||
if has {
|
||||
found = true
|
||||
if !ok {
|
||||
return false, true
|
||||
return false, true, rule.Limit
|
||||
}
|
||||
found = true
|
||||
foundLimit = min(foundLimit, rule.Limit)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,32 +222,35 @@ func (g *Group) Evaluate(used Used, forSubject LimitSubject) (bool, bool) {
|
|||
// subjects below
|
||||
|
||||
for _, subject := range affectsMap[forSubject] {
|
||||
ok, has := g.Evaluate(used, subject)
|
||||
ok, has, limit := g.Evaluate(used, subject)
|
||||
if has {
|
||||
found = true
|
||||
if !ok {
|
||||
return false, true
|
||||
return false, true, limit
|
||||
}
|
||||
found = true
|
||||
foundLimit = min(foundLimit, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, found
|
||||
return true, found, foundLimit
|
||||
}
|
||||
|
||||
func (gl *GroupList) Evaluate(used Used, forSubject LimitSubject) bool {
|
||||
// Evaluate returns if the used size is acceptable for the subject and the
|
||||
// lowest limit that is acceptable for the subject.
|
||||
func (gl *GroupList) Evaluate(used Used, forSubject LimitSubject) (bool, int64) {
|
||||
// If there are no groups, use the configured defaults:
|
||||
if gl == nil || len(*gl) == 0 {
|
||||
return EvaluateDefault(used, forSubject)
|
||||
}
|
||||
|
||||
for _, group := range *gl {
|
||||
ok, has := group.Evaluate(used, forSubject)
|
||||
ok, has, limit := group.Evaluate(used, forSubject)
|
||||
if has && ok {
|
||||
return true
|
||||
return true, limit
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, 0
|
||||
}
|
||||
|
||||
func GetGroupByName(ctx context.Context, name string) (*Group, error) {
|
||||
|
|
|
@ -32,5 +32,6 @@ func EvaluateForUser(ctx context.Context, userID int64, subject LimitSubject) (b
|
|||
return false, err
|
||||
}
|
||||
|
||||
return groups.Evaluate(*used, subject), nil
|
||||
acceptable, _ := groups.Evaluate(*used, subject)
|
||||
return acceptable, nil
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package quota_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
quota_model "code.gitea.io/gitea/models/quota"
|
||||
|
@ -36,9 +37,10 @@ func TestQuotaGroupAllRulesMustPass(t *testing.T) {
|
|||
|
||||
// Within a group, *all* rules must pass. Thus, if we have a deny-all rule,
|
||||
// and an unlimited rule, that will always fail.
|
||||
ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.True(t, has)
|
||||
assert.False(t, ok)
|
||||
assert.EqualValues(t, 0, limit)
|
||||
}
|
||||
|
||||
func TestQuotaGroupRuleScenario1(t *testing.T) {
|
||||
|
@ -66,21 +68,25 @@ func TestQuotaGroupRuleScenario1(t *testing.T) {
|
|||
used.Size.Assets.Packages.All = 256
|
||||
used.Size.Git.LFS = 16
|
||||
|
||||
ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeAssetsAttachmentsReleases)
|
||||
ok, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeAssetsAttachmentsReleases)
|
||||
assert.True(t, has, "size:assets:attachments:releases is covered")
|
||||
assert.True(t, ok, "size:assets:attachments:releases passes")
|
||||
assert.EqualValues(t, 1024, limit)
|
||||
|
||||
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll)
|
||||
ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll)
|
||||
assert.True(t, has, "size:assets:packages:all is covered")
|
||||
assert.True(t, ok, "size:assets:packages:all passes")
|
||||
assert.EqualValues(t, 1024, limit)
|
||||
|
||||
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS)
|
||||
ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS)
|
||||
assert.True(t, has, "size:git:lfs is covered")
|
||||
assert.False(t, ok, "size:git:lfs fails")
|
||||
assert.EqualValues(t, 0, limit)
|
||||
|
||||
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.True(t, has, "size:all is covered")
|
||||
assert.False(t, ok, "size:all fails")
|
||||
assert.EqualValues(t, 0, limit)
|
||||
}
|
||||
|
||||
func TestQuotaGroupRuleCombination(t *testing.T) {
|
||||
|
@ -109,23 +115,27 @@ func TestQuotaGroupRuleCombination(t *testing.T) {
|
|||
}
|
||||
|
||||
// Git LFS isn't covered by any rule
|
||||
_, has := group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS)
|
||||
_, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeGitLFS)
|
||||
assert.False(t, has)
|
||||
assert.EqualValues(t, math.MaxInt, limit)
|
||||
|
||||
// repos:all is covered, and is passing
|
||||
ok, has := group.Evaluate(used, quota_model.LimitSubjectSizeReposAll)
|
||||
ok, has, limit := group.Evaluate(used, quota_model.LimitSubjectSizeReposAll)
|
||||
assert.True(t, has)
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, 4096, limit)
|
||||
|
||||
// packages:all is covered, and is failing
|
||||
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll)
|
||||
ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAssetsPackagesAll)
|
||||
assert.True(t, has)
|
||||
assert.False(t, ok)
|
||||
assert.EqualValues(t, 0, limit)
|
||||
|
||||
// size:all is covered, and is failing (due to packages:all being over quota)
|
||||
ok, has = group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, has, limit = group.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.True(t, has, "size:all should be covered")
|
||||
assert.False(t, ok, "size:all should fail")
|
||||
assert.EqualValues(t, 0, limit)
|
||||
}
|
||||
|
||||
func TestQuotaGroupListsRequireOnlyOnePassing(t *testing.T) {
|
||||
|
@ -159,8 +169,9 @@ func TestQuotaGroupListsRequireOnlyOnePassing(t *testing.T) {
|
|||
used.Size.Repos.Public = 1024
|
||||
|
||||
// In a group list, if any group passes, the entire evaluation passes.
|
||||
ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, limit := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, -1, limit)
|
||||
}
|
||||
|
||||
func TestQuotaGroupListAllFailing(t *testing.T) {
|
||||
|
@ -193,8 +204,9 @@ func TestQuotaGroupListAllFailing(t *testing.T) {
|
|||
used := quota_model.Used{}
|
||||
used.Size.Repos.Public = 2048
|
||||
|
||||
ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, limit := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.False(t, ok)
|
||||
assert.EqualValues(t, 0, limit)
|
||||
}
|
||||
|
||||
func TestQuotaGroupListEmpty(t *testing.T) {
|
||||
|
@ -203,6 +215,7 @@ func TestQuotaGroupListEmpty(t *testing.T) {
|
|||
used := quota_model.Used{}
|
||||
used.Size.Repos.Public = 2048
|
||||
|
||||
ok := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
ok, limit := groups.Evaluate(used, quota_model.LimitSubjectSizeAll)
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, -1, limit)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,22 @@ func (r *Rule) TableName() string {
|
|||
return "quota_rule"
|
||||
}
|
||||
|
||||
func (r Rule) Acceptable(used Used) bool {
|
||||
if r.Limit == -1 {
|
||||
return true
|
||||
}
|
||||
|
||||
return r.Sum(used) <= r.Limit
|
||||
}
|
||||
|
||||
func (r Rule) Sum(used Used) int64 {
|
||||
var sum int64
|
||||
for _, subject := range r.Subjects {
|
||||
sum += used.CalculateFor(subject)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (r Rule) Evaluate(used Used, forSubject LimitSubject) (bool, bool) {
|
||||
// If there's no limit, short circuit out
|
||||
if r.Limit == -1 {
|
||||
|
@ -31,11 +47,7 @@ func (r Rule) Evaluate(used Used, forSubject LimitSubject) (bool, bool) {
|
|||
return false, false
|
||||
}
|
||||
|
||||
var sum int64
|
||||
for _, subject := range r.Subjects {
|
||||
sum += used.CalculateFor(subject)
|
||||
}
|
||||
return sum <= r.Limit, true
|
||||
return r.Sum(used) <= r.Limit, true
|
||||
}
|
||||
|
||||
func (r *Rule) Edit(ctx context.Context, limit *int64, subjects *LimitSubjects) (*Rule, error) {
|
||||
|
|
|
@ -232,6 +232,11 @@ table_modal.placeholder.content = Content
|
|||
table_modal.label.rows = Rows
|
||||
table_modal.label.columns = Columns
|
||||
|
||||
link_modal.header = Add a link
|
||||
link_modal.url = Url
|
||||
link_modal.description = Description
|
||||
link_modal.paste_reminder = Hint: With a URL in your clipboard, you can paste directly into the editor to create a link.
|
||||
|
||||
[filter]
|
||||
string.asc = A - Z
|
||||
string.desc = Z - A
|
||||
|
@ -744,6 +749,8 @@ organization = Organizations
|
|||
uid = UID
|
||||
webauthn = Two-factor authentication (Security keys)
|
||||
blocked_users = Blocked users
|
||||
storage_overview = Storage overview
|
||||
quota = Quota
|
||||
|
||||
public_profile = Public profile
|
||||
biography_placeholder = Tell others a little bit about yourself! (Markdown is supported)
|
||||
|
@ -1049,6 +1056,25 @@ user_unblock_success = The user has been unblocked successfully.
|
|||
user_block_success = The user has been blocked successfully.
|
||||
user_block_yourself = You cannot block yourself.
|
||||
|
||||
quota.applies_to_user = The following quota rules apply to your account
|
||||
quota.applies_to_org = The following quota rules apply to this organisation
|
||||
quota.rule.exceeded = Exceeded
|
||||
quota.rule.exceeded.helper = The total size of objects for this rule has exceeded the quota.
|
||||
quota.rule.no_limit = Unlimited
|
||||
quota.sizes.all = All
|
||||
quota.sizes.repos.all = Repositories
|
||||
quota.sizes.repos.public = Public repositories
|
||||
quota.sizes.repos.private = Private repositories
|
||||
quota.sizes.git.all = Git content
|
||||
quota.sizes.git.lfs = Git LFS
|
||||
quota.sizes.assets.all = Assets
|
||||
quota.sizes.assets.attachments.all = Attachments
|
||||
quota.sizes.assets.attachments.issues = Issue attachments
|
||||
quota.sizes.assets.attachments.releases = Release attachments
|
||||
quota.sizes.assets.artifacts = Artifacts
|
||||
quota.sizes.assets.packages.all = Packages
|
||||
quota.sizes.wiki = Wiki
|
||||
|
||||
[repo]
|
||||
rss.must_be_on_branch = You must be on a branch to have an RSS feed.
|
||||
|
||||
|
|
|
@ -34,13 +34,6 @@ func GetRegistrationToken(ctx *context.APIContext, ownerID, repoID int64) {
|
|||
ctx.JSON(http.StatusOK, RegistrationToken{Token: token.Token})
|
||||
}
|
||||
|
||||
// RunJobList is a list of action run jobs
|
||||
// swagger:response RunJobList
|
||||
type RunJobList struct {
|
||||
// in:body
|
||||
Body []*structs.ActionRunJob `json:"body"`
|
||||
}
|
||||
|
||||
func GetActionRunJobs(ctx *context.APIContext, ownerID, repoID int64) {
|
||||
labels := strings.Split(ctx.FormTrim("labels"), ",")
|
||||
|
||||
|
@ -54,8 +47,7 @@ func GetActionRunJobs(ctx *context.APIContext, ownerID, repoID int64) {
|
|||
return
|
||||
}
|
||||
|
||||
res := new(RunJobList)
|
||||
res.Body = fromRunJobModelToResponse(total, labels)
|
||||
res := fromRunJobModelToResponse(total, labels)
|
||||
|
||||
ctx.JSON(http.StatusOK, res)
|
||||
}
|
||||
|
|
|
@ -32,3 +32,10 @@ type swaggerResponseVariableList struct {
|
|||
// in:body
|
||||
Body []api.ActionVariable `json:"body"`
|
||||
}
|
||||
|
||||
// RunJobList is a list of action run jobs
|
||||
// swagger:response RunJobList
|
||||
type swaggerRunJobList struct {
|
||||
// in:body
|
||||
Body []*api.ActionRunJob `json:"body"`
|
||||
}
|
||||
|
|
20
routers/web/org/setting/storage_overview.go
Normal file
20
routers/web/org/setting/storage_overview.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/routers/web/shared"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplSettingsStorageOverview base.TplName = "org/settings/storage_overview"
|
||||
)
|
||||
|
||||
// StorageOverview render a size overview of the organization, as well as relevant
|
||||
// quota limits of the instance.
|
||||
func StorageOverview(ctx *context.Context) {
|
||||
shared.StorageOverview(ctx, ctx.Org.Organization.ID, tplSettingsStorageOverview)
|
||||
}
|
|
@ -474,7 +474,7 @@ func Download(ctx *context.Context) {
|
|||
uri := ctx.Params("*")
|
||||
ext, tp, err := archiver_service.ParseFileName(uri)
|
||||
if err != nil {
|
||||
ctx.ServerError("ParseFileName", err)
|
||||
ctx.NotFound("ParseFileName", err)
|
||||
return
|
||||
}
|
||||
aReq, err := archiver_service.NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp)
|
||||
|
@ -554,7 +554,7 @@ func InitiateDownload(ctx *context.Context) {
|
|||
uri := ctx.Params("*")
|
||||
ext, tp, err := archiver_service.ParseFileName(uri)
|
||||
if err != nil {
|
||||
ctx.ServerError("ParseFileName", err)
|
||||
ctx.NotFound("ParseFileName", err)
|
||||
return
|
||||
}
|
||||
aReq, err := archiver_service.NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp)
|
||||
|
|
90
routers/web/shared/storage_overview.go
Normal file
90
routers/web/shared/storage_overview.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package shared
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
quota_model "code.gitea.io/gitea/models/quota"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// StorageOverview render a size overview of the user, as well as relevant
|
||||
// quota limits of the instance.
|
||||
func StorageOverview(ctx *context.Context, userID int64, tpl base.TplName) {
|
||||
if !setting.Quota.Enabled {
|
||||
ctx.NotFound("MustEnableQuota", nil)
|
||||
}
|
||||
ctx.Data["Title"] = ctx.Tr("settings.storage_overview")
|
||||
ctx.Data["PageIsStorageOverview"] = true
|
||||
|
||||
ctx.Data["Color"] = func(subject quota_model.LimitSubject) float64 {
|
||||
return float64(subject) * 137.50776405003785 // Golden angle.
|
||||
}
|
||||
|
||||
ctx.Data["PrettySubject"] = func(subject quota_model.LimitSubject) template.HTML {
|
||||
switch subject {
|
||||
case quota_model.LimitSubjectSizeAll:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.all")
|
||||
case quota_model.LimitSubjectSizeReposAll:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.repos.all")
|
||||
case quota_model.LimitSubjectSizeReposPublic:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.repos.public")
|
||||
case quota_model.LimitSubjectSizeReposPrivate:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.repos.private")
|
||||
case quota_model.LimitSubjectSizeGitAll:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.git.all")
|
||||
case quota_model.LimitSubjectSizeGitLFS:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.git.lfs")
|
||||
case quota_model.LimitSubjectSizeAssetsAll:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.assets.all")
|
||||
case quota_model.LimitSubjectSizeAssetsAttachmentsAll:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.assets.attachments.all")
|
||||
case quota_model.LimitSubjectSizeAssetsAttachmentsIssues:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.assets.attachments.issues")
|
||||
case quota_model.LimitSubjectSizeAssetsAttachmentsReleases:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.assets.attachments.releases")
|
||||
case quota_model.LimitSubjectSizeAssetsArtifacts:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.assets.artifacts")
|
||||
case quota_model.LimitSubjectSizeAssetsPackagesAll:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.assets.packages.all")
|
||||
case quota_model.LimitSubjectSizeWiki:
|
||||
return ctx.Locale.Tr("settings.quota.sizes.wiki")
|
||||
default:
|
||||
panic("unrecognized subject: " + subject.String())
|
||||
}
|
||||
}
|
||||
|
||||
sizeUsed, err := quota_model.GetUsedForUser(ctx, userID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUsedForUser", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["SizeUsed"] = sizeUsed
|
||||
|
||||
quotaGroups, err := quota_model.GetGroupsForUser(ctx, userID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetGroupsForUser", err)
|
||||
return
|
||||
}
|
||||
if len(quotaGroups) == 0 {
|
||||
quotaGroups = append(quotaGroups, "a_model.Group{
|
||||
Name: "Global quota",
|
||||
Rules: []quota_model.Rule{
|
||||
{
|
||||
Name: "Default",
|
||||
Limit: setting.Quota.Default.Total,
|
||||
Subjects: quota_model.LimitSubjects{quota_model.LimitSubjectSizeAll},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
ctx.Data["QuotaGroups"] = quotaGroups
|
||||
|
||||
ctx.HTML(http.StatusOK, tpl)
|
||||
}
|
20
routers/web/user/setting/storage_overview.go
Normal file
20
routers/web/user/setting/storage_overview.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/routers/web/shared"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplSettingsStorageOverview base.TplName = "user/settings/storage_overview"
|
||||
)
|
||||
|
||||
// StorageOverview render a size overview of the user, as well as relevant
|
||||
// quota limits of the instance.
|
||||
func StorageOverview(ctx *context.Context) {
|
||||
shared.StorageOverview(ctx, ctx.Doer.ID, tplSettingsStorageOverview)
|
||||
}
|
|
@ -644,7 +644,8 @@ func registerRoutes(m *web.Route) {
|
|||
m.Get("", user_setting.BlockedUsers)
|
||||
m.Post("/unblock", user_setting.UnblockUser)
|
||||
})
|
||||
}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
|
||||
m.Get("/storage_overview", user_setting.StorageOverview)
|
||||
}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled, "EnableQuota", setting.Quota.Enabled))
|
||||
|
||||
m.Group("/user", func() {
|
||||
m.Get("/activate", auth.Activate)
|
||||
|
@ -930,6 +931,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Post("/block", org_setting.BlockedUsersBlock)
|
||||
m.Post("/unblock", org_setting.BlockedUsersUnblock)
|
||||
})
|
||||
m.Get("/storage_overview", org_setting.StorageOverview)
|
||||
|
||||
m.Group("/packages", func() {
|
||||
m.Get("", org.Packages)
|
||||
|
@ -949,7 +951,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Post("/rebuild", org.RebuildCargoIndex)
|
||||
})
|
||||
}, packagesEnabled)
|
||||
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
|
||||
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "EnableQuota", setting.Quota.Enabled, "PageIsOrgSettings", true))
|
||||
}, context.OrgAssignment(true, true))
|
||||
}, reqSignIn)
|
||||
// ***** END: Organization *****
|
||||
|
|
|
@ -26,6 +26,14 @@
|
|||
{{svg "octicon-x"}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex-item-body">
|
||||
<b>{{ctx.Locale.Tr "admin.users.prohibit_login"}}:</b>
|
||||
{{if .User.ProhibitLogin}}
|
||||
{{svg "octicon-check"}}
|
||||
{{else}}
|
||||
{{svg "octicon-x"}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex-item-body">
|
||||
<b>{{ctx.Locale.Tr "admin.users.restricted"}}:</b>
|
||||
{{if .User.IsRestricted}}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="flex-item">
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-text-block">
|
||||
{{.Email}}
|
||||
<a href="mailto:{{.Email}}">{{.Email}}</a>
|
||||
{{if .IsPrimary}}
|
||||
<div class="ui primary label">{{ctx.Locale.Tr "settings.primary"}}</div>
|
||||
{{end}}
|
||||
|
|
|
@ -38,9 +38,14 @@
|
|||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
|
||||
<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
|
||||
{{ctx.Locale.Tr "settings.blocked_users"}}
|
||||
</a>
|
||||
{{if .EnableQuota}}
|
||||
<a class="{{if .PageIsSettingsStorageOverview}}active {{end}}item" href="{{.OrgLink}}/settings/storage_overview">
|
||||
{{ctx.Locale.Tr "settings.storage_overview"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete">
|
||||
{{ctx.Locale.Tr "org.settings.delete"}}
|
||||
</a>
|
||||
|
|
5
templates/org/settings/storage_overview.tmpl
Normal file
5
templates/org/settings/storage_overview.tmpl
Normal file
|
@ -0,0 +1,5 @@
|
|||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings size-overview")}}
|
||||
<div class="org-setting-content">
|
||||
{{template "shared/quota_overview" .}}
|
||||
</div>
|
||||
{{template "org/settings/layout_footer" .}}
|
|
@ -141,20 +141,20 @@
|
|||
{{template "repo/cite/cite_modal" .}}
|
||||
{{end}}
|
||||
{{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
|
||||
<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
|
||||
{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
|
||||
</a>
|
||||
{{if not $.DisableDownloadSourceArchives}}
|
||||
<div class="clone-panel ui action tiny input">
|
||||
<button class="ui small jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
|
||||
<div class="button-row folder-actions">
|
||||
<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
|
||||
{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
|
||||
</a>
|
||||
{{if not $.DisableDownloadSourceArchives}}
|
||||
<button class="ui jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}">
|
||||
{{svg "octicon-kebab-horizontal"}}
|
||||
<div class="menu">
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments (printf "%s:%s" $.RefName .TreePath)}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments (printf "%s:%s" $.RefName .TreePath)}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments (printf "%s:%s" $.RefName .TreePath)}}.zip" rel="nofollow" type="application/zip">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_zip"}}</a>
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments (printf "%s:%s" $.RefName .TreePath)}}.tar.gz" rel="nofollow" type="application/gzip">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}}</a>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -29,7 +29,7 @@ Template Attributes:
|
|||
<div class="markdown-toolbar-group">
|
||||
<md-quote class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.quote.tooltip"}}">{{svg "octicon-quote"}}</md-quote>
|
||||
<md-code class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.code.tooltip"}}">{{svg "octicon-code"}}</md-code>
|
||||
<md-link class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.link.tooltip"}}">{{svg "octicon-link"}}</md-link>
|
||||
<button class="markdown-toolbar-button show-modal button" data-md-button data-md-action="new-link" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.link.tooltip"}}">{{svg "octicon-link"}}</button>
|
||||
</div>
|
||||
<div class="markdown-toolbar-group">
|
||||
<md-unordered-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.unordered.tooltip"}}">{{svg "octicon-list-unordered"}}</md-unordered-list>
|
||||
|
@ -88,4 +88,30 @@ Template Attributes:
|
|||
<button class="ui primary button" data-selector-name="ok-button">{{ctx.Locale.Tr "ok"}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui small modal" data-modal-name="new-markdown-link">
|
||||
<div class="header">{{ctx.Locale.Tr "editor.link_modal.header"}}</div>
|
||||
|
||||
<fieldset class="content">
|
||||
<div class="ui form" data-selector-name="form">
|
||||
<label>
|
||||
{{ctx.Locale.Tr "editor.link_modal.url"}}
|
||||
<input name="link-url" required dir="auto" autocomplete="off">
|
||||
</label>
|
||||
<label>
|
||||
{{ctx.Locale.Tr "editor.link_modal.description"}}
|
||||
<input name="link-description" required dir="auto" autocomplete="off">
|
||||
</label>
|
||||
|
||||
<div class="help">
|
||||
{{ctx.Locale.Tr "editor.link_modal.paste_reminder"}}
|
||||
</div>
|
||||
|
||||
<div class="text right actions">
|
||||
<button class="ui cancel button" data-selector-name="cancel-button">{{ctx.Locale.Tr "cancel"}}</button>
|
||||
<button class="ui primary button" data-selector-name="ok-button">{{ctx.Locale.Tr "ok"}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
|
32
templates/shared/quota_overview.tmpl
Normal file
32
templates/shared/quota_overview.tmpl
Normal file
|
@ -0,0 +1,32 @@
|
|||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "settings.quota"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>{{if .ContextUser.IsOrganization}}{{ctx.Locale.Tr "settings.quota.applies_to_org"}}{{else}}{{ctx.Locale.Tr "settings.quota.applies_to_user"}}{{end}}:</p>
|
||||
{{range $group := .QuotaGroups}}
|
||||
<p class="tw-my-4"><strong>{{$group.Name}}</strong></p>
|
||||
<div class="tw-ml-4">
|
||||
{{range $rule := .Rules}}
|
||||
<div class="tw-flex tw-justify-between">
|
||||
<span class="tw-flex tw-items-center tw-gap-2{{if eq $rule.Limit -1}} tw-mb-5{{end}}">
|
||||
{{if $rule.Acceptable ($.SizeUsed)}}
|
||||
{{svg "octicon-check-circle-fill" 16 "text green"}}
|
||||
{{$rule.Name}}
|
||||
{{else}}
|
||||
{{svg "octicon-alert-fill" 16 "text red"}}
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "settings.quota.rule.exceeded.helper"}}" data-tooltip-placement="top">
|
||||
{{$rule.Name}} – {{ctx.Locale.Tr "settings.quota.rule.exceeded"}}
|
||||
</span>
|
||||
{{end}}
|
||||
</span>
|
||||
<span>{{ctx.Locale.TrSize ($rule.Sum $.SizeUsed)}} / {{if eq $rule.Limit -1 -}}{{ctx.Locale.Tr "settings.quota.rule.no_limit"}}{{else}}{{ctx.Locale.TrSize $rule.Limit}}{{end}}</span>
|
||||
</div>
|
||||
<div class="ui segment">
|
||||
{{range $idx, $subject := .Subjects}}
|
||||
<div class="bar" style="width: calc(max(1%, {{Eval 100.0 "*" ($.SizeUsed.CalculateFor $subject) "/" $rule.Limit}}%)); background-color: oklch(80% 30% {{call $.Color $subject}}deg)" data-tooltip-placement="top" data-tooltip-content="{{call $.PrettySubject $subject}} – {{ctx.Locale.TrSize ($.SizeUsed.CalculateFor $subject)}}" data-tooltip-follow-cursor="horizontal"></div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
|
@ -51,6 +51,11 @@
|
|||
<a class="{{if .PageIsSettingsRepos}}active {{end}}item" href="{{AppSubUrl}}/user/settings/repos">
|
||||
{{ctx.Locale.Tr "settings.repos"}}
|
||||
</a>
|
||||
{{if .EnableQuota}}
|
||||
<a class="{{if .PageIsStorageOverview}}active {{end}}item" href="{{AppSubUrl}}/user/settings/storage_overview">
|
||||
{{ctx.Locale.Tr "settings.storage_overview"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsBlockedUsers}}active {{end}}item" href="{{AppSubUrl}}/user/settings/blocked_users">
|
||||
{{ctx.Locale.Tr "settings.blocked_users"}}
|
||||
</a>
|
||||
|
|
5
templates/user/settings/storage_overview.tmpl
Normal file
5
templates/user/settings/storage_overview.tmpl
Normal file
|
@ -0,0 +1,5 @@
|
|||
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings size-overview")}}
|
||||
<div class="user-setting-content">
|
||||
{{template "shared/quota_overview" .}}
|
||||
</div>
|
||||
{{template "user/settings/layout_footer" .}}
|
|
@ -37,6 +37,7 @@ func TestMain(m *testing.M) {
|
|||
defer cancel()
|
||||
|
||||
tests.InitTest(true)
|
||||
setting.Quota.Enabled = true
|
||||
initChangedFiles()
|
||||
testE2eWebRoutes = routers.NormalRoutes()
|
||||
|
||||
|
|
12
tests/e2e/fixtures/attachment.yml
Normal file
12
tests/e2e/fixtures/attachment.yml
Normal file
|
@ -0,0 +1,12 @@
|
|||
-
|
||||
id: 1001
|
||||
uuid: 5792c349-8a26-46b8-b4cd-2c290f118285
|
||||
repo_id: 1
|
||||
issue_id: 1
|
||||
release_id: 0
|
||||
uploader_id: 3
|
||||
comment_id: 1
|
||||
name: forgejo-secrets.txt
|
||||
download_count: 0
|
||||
size: 536870911
|
||||
created_unix: 1730000000
|
1
tests/e2e/fixtures/quota_group.yml
Normal file
1
tests/e2e/fixtures/quota_group.yml
Normal file
|
@ -0,0 +1 @@
|
|||
- name: trusted-user
|
5
tests/e2e/fixtures/quota_group_mapping.yml
Normal file
5
tests/e2e/fixtures/quota_group_mapping.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
-
|
||||
id: 1001
|
||||
kind: 0
|
||||
mapped_id: 2
|
||||
group_name: trusted-user
|
14
tests/e2e/fixtures/quota_group_rule_mapping.yml
Normal file
14
tests/e2e/fixtures/quota_group_rule_mapping.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
-
|
||||
id: 1001
|
||||
group_name: trusted-user
|
||||
rule_name: git-lfs
|
||||
|
||||
-
|
||||
id: 1002
|
||||
group_name: trusted-user
|
||||
rule_name: "all:assets"
|
||||
|
||||
-
|
||||
id: 1003
|
||||
group_name: trusted-user
|
||||
rule_name: "Multi subjects"
|
13
tests/e2e/fixtures/quota_rule.yml
Normal file
13
tests/e2e/fixtures/quota_rule.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
-
|
||||
name: git-lfs
|
||||
limit: 512
|
||||
subjects: [6]
|
||||
|
||||
-
|
||||
name: "all:assets"
|
||||
limit: -1
|
||||
subjects: [7]
|
||||
|
||||
- name: "Multi subjects"
|
||||
limit: 5000000000
|
||||
subjects: [8,6]
|
12
tests/e2e/fixtures/repository.yml
Normal file
12
tests/e2e/fixtures/repository.yml
Normal file
|
@ -0,0 +1,12 @@
|
|||
-
|
||||
id: 1001
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: large-lfs
|
||||
name: large-lfs
|
||||
default_branch: main
|
||||
is_empty: false
|
||||
is_archived: false
|
||||
is_private: true
|
||||
status: 0
|
||||
lfs_size: 8192
|
|
@ -5,6 +5,7 @@
|
|||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {accessibilityCheck} from './shared/accessibility.ts';
|
||||
import {save_visual, test} from './utils_e2e.ts';
|
||||
|
||||
test.use({user: 'user2'});
|
||||
|
@ -224,6 +225,33 @@ test('markdown insert table', async ({page}) => {
|
|||
await save_visual(page);
|
||||
});
|
||||
|
||||
test('markdown insert link', async ({page}) => {
|
||||
const response = await page.goto('/user2/repo1/issues/new');
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
const newLinkButton = page.locator('button[data-md-action="new-link"]');
|
||||
await newLinkButton.click();
|
||||
|
||||
const newLinkModal = page.locator('div[data-markdown-link-modal-id="0"]');
|
||||
await expect(newLinkModal).toBeVisible();
|
||||
await accessibilityCheck({page}, ['[data-modal-name="new-markdown-link"]'], [], []);
|
||||
await save_visual(page);
|
||||
|
||||
const url = 'https://example.com';
|
||||
const description = 'Where does this lead?';
|
||||
|
||||
await newLinkModal.locator('input[name="link-url"]').fill(url);
|
||||
await newLinkModal.locator('input[name="link-description"]').fill(description);
|
||||
|
||||
await newLinkModal.locator('button[data-selector-name="ok-button"]').click();
|
||||
|
||||
await expect(newLinkModal).toBeHidden();
|
||||
|
||||
const textarea = page.locator('textarea[name=content]');
|
||||
await expect(textarea).toHaveValue(`[${description}](${url})`);
|
||||
await save_visual(page);
|
||||
});
|
||||
|
||||
test('text expander has higher prio then prefix continuation', async ({page}) => {
|
||||
const response = await page.goto('/user2/repo1/issues/new');
|
||||
expect(response?.status()).toBe(200);
|
||||
|
|
|
@ -63,3 +63,12 @@ test('User: Profile settings', async ({browser}, workerInfo) => {
|
|||
await page.goto('/user2?tab=activity');
|
||||
await expect(page.getByText('Your activity is visible to everyone')).toBeVisible();
|
||||
});
|
||||
|
||||
test('User: Storage overview', async ({browser}, workerInfo) => {
|
||||
const page = await login({browser}, workerInfo);
|
||||
await page.goto('/user/settings/storage_overview');
|
||||
await page.waitForLoadState();
|
||||
await page.getByLabel('Git LFS – 8 KiB').nth(1).hover({position: {x: 250, y: 2}});
|
||||
await expect(page.getByText('Git LFS')).toBeVisible();
|
||||
await save_visual(page);
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -31,9 +31,9 @@ func TestAPISearchActionJobs_GlobalRunner(t *testing.T) {
|
|||
).AddTokenAuth(token)
|
||||
res := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var jobs shared.RunJobList
|
||||
var jobs []*api.ActionRunJob
|
||||
DecodeJSON(t, res, &jobs)
|
||||
|
||||
assert.Len(t, jobs.Body, 1)
|
||||
assert.EqualValues(t, job.ID, jobs.Body[0].ID)
|
||||
assert.Len(t, jobs, 1)
|
||||
assert.EqualValues(t, job.ID, jobs[0].ID)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -30,9 +30,9 @@ func TestAPISearchActionJobs_OrgRunner(t *testing.T) {
|
|||
AddTokenAuth(token)
|
||||
res := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var jobs shared.RunJobList
|
||||
var jobs []*api.ActionRunJob
|
||||
DecodeJSON(t, res, &jobs)
|
||||
|
||||
assert.Len(t, jobs.Body, 1)
|
||||
assert.EqualValues(t, job.ID, jobs.Body[0].ID)
|
||||
assert.Len(t, jobs, 1)
|
||||
assert.EqualValues(t, job.ID, jobs[0].ID)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -35,9 +35,9 @@ func TestAPISearchActionJobs_RepoRunner(t *testing.T) {
|
|||
).AddTokenAuth(token)
|
||||
res := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var jobs shared.RunJobList
|
||||
var jobs []*api.ActionRunJob
|
||||
DecodeJSON(t, res, &jobs)
|
||||
|
||||
assert.Len(t, jobs.Body, 1)
|
||||
assert.EqualValues(t, job.ID, jobs.Body[0].ID)
|
||||
assert.Len(t, jobs, 1)
|
||||
assert.EqualValues(t, job.ID, jobs[0].ID)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -30,9 +30,9 @@ func TestAPISearchActionJobs_UserRunner(t *testing.T) {
|
|||
AddTokenAuth(token)
|
||||
res := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var jobs shared.RunJobList
|
||||
var jobs []*api.ActionRunJob
|
||||
DecodeJSON(t, res, &jobs)
|
||||
|
||||
assert.Len(t, jobs.Body, 1)
|
||||
assert.EqualValues(t, job.ID, jobs.Body[0].ID)
|
||||
assert.Len(t, jobs, 1)
|
||||
assert.EqualValues(t, job.ID, jobs[0].ID)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
@ -38,6 +39,10 @@ func TestRepoDownloadArchive(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Empty(t, resp.Header().Get("Content-Encoding"))
|
||||
assert.Len(t, bs, 320)
|
||||
|
||||
// Verify that unrecognized archive type returns 404
|
||||
req = NewRequest(t, "GET", "/user2/repo1/archive/master.invalid")
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestRepoDownloadArchiveSubdir(t *testing.T) {
|
||||
|
@ -55,9 +60,10 @@ func TestRepoDownloadArchiveSubdir(t *testing.T) {
|
|||
|
||||
t.Run("Frontend", func(t *testing.T) {
|
||||
resp := MakeRequest(t, NewRequestf(t, "GET", "/%s/src/branch/master/subdir", repo.FullName()), http.StatusOK)
|
||||
page := NewHTMLParser(t, resp.Body)
|
||||
|
||||
assert.Contains(t, resp.Body.String(), fmt.Sprintf("/%s/archive/master:subdir.zip", repo.FullName()))
|
||||
assert.Contains(t, resp.Body.String(), fmt.Sprintf("/%s/archive/master:subdir.tar.gz", repo.FullName()))
|
||||
page.AssertElement(t, fmt.Sprintf(".folder-actions a.archive-link[href='/%s/archive/master:subdir.zip'][type='application/zip']", repo.FullName()), true)
|
||||
page.AssertElement(t, fmt.Sprintf(".folder-actions a.archive-link[href='/%s/archive/master:subdir.tar.gz'][type='application/gzip']", repo.FullName()), true)
|
||||
})
|
||||
|
||||
t.Run("Backend", func(t *testing.T) {
|
||||
|
|
|
@ -153,7 +153,9 @@
|
|||
border-radius: 0 var(--border-radius) var(--border-radius) 0 !important;
|
||||
}
|
||||
|
||||
.repository .clone-panel .dropdown .menu {
|
||||
/* Dropdown alignment */
|
||||
.repository .clone-panel .dropdown .menu,
|
||||
.repository .folder-actions .dropdown .menu {
|
||||
right: 0 !important;
|
||||
left: auto !important;
|
||||
}
|
||||
|
@ -2033,6 +2035,14 @@ details.repo-search-result summary::marker {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.size-overview .segment:has(> .bar) {
|
||||
display: flex;
|
||||
height: 10px;
|
||||
padding: 0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#cite-repo-modal #citation-panel {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
|
|
@ -49,6 +49,7 @@ class ComboMarkdownEditor {
|
|||
this.setupDropzone();
|
||||
this.setupTextarea();
|
||||
this.setupTableInserter();
|
||||
this.setupLinkInserter();
|
||||
|
||||
await this.switchToUserPreference();
|
||||
|
||||
|
@ -93,6 +94,7 @@ class ComboMarkdownEditor {
|
|||
this.indentSelection(true);
|
||||
});
|
||||
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-table"]')?.setAttribute('data-modal', `div[data-markdown-table-modal-id="${elementIdCounter}"]`);
|
||||
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-link"]')?.setAttribute('data-modal', `div[data-markdown-link-modal-id="${elementIdCounter}"]`);
|
||||
|
||||
this.textarea.addEventListener('keydown', (e) => {
|
||||
if (e.shiftKey) {
|
||||
|
@ -228,6 +230,58 @@ class ComboMarkdownEditor {
|
|||
button.addEventListener('click', this.addNewTable);
|
||||
}
|
||||
|
||||
addNewLink(event) {
|
||||
const elementId = event.target.getAttribute('data-element-id');
|
||||
const newLinkModal = document.querySelector(`div[data-markdown-link-modal-id="${elementId}"]`);
|
||||
const form = newLinkModal.querySelector('div[data-selector-name="form"]');
|
||||
|
||||
// Validate input fields
|
||||
for (const currentInput of form.querySelectorAll('input')) {
|
||||
if (!currentInput.checkValidity()) {
|
||||
currentInput.reportValidity();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const url = form.querySelector('input[name="link-url"]').value;
|
||||
const description = form.querySelector('input[name="link-description"]').value;
|
||||
|
||||
const code = `[${description}](${url})`;
|
||||
|
||||
replaceTextareaSelection(document.getElementById(`_combo_markdown_editor_${elementId}`), code);
|
||||
|
||||
// Close the modal then clear its fields in case the user wants to add another one.
|
||||
newLinkModal.querySelector('button[data-selector-name="cancel-button"]').click();
|
||||
form.querySelector('input[name="link-url"]').value = '';
|
||||
form.querySelector('input[name="link-description"]').value = '';
|
||||
}
|
||||
|
||||
setupLinkInserter() {
|
||||
const newLinkModal = this.container.querySelector('div[data-modal-name="new-markdown-link"]');
|
||||
newLinkModal.setAttribute('data-markdown-link-modal-id', elementIdCounter);
|
||||
const textarea = document.getElementById(`_combo_markdown_editor_${elementIdCounter}`);
|
||||
|
||||
$(newLinkModal).modal({
|
||||
// Pre-fill the description field from the selection to create behavior similar
|
||||
// to pasting an URL over selected text.
|
||||
onShow: () => {
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
|
||||
if (start !== end) {
|
||||
const selection = textarea.value.slice(start ?? undefined, end ?? undefined);
|
||||
newLinkModal.querySelector('input[name="link-description"]').value = selection;
|
||||
} else {
|
||||
newLinkModal.querySelector('input[name="link-description"]').value = '';
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const button = newLinkModal.querySelector('button[data-selector-name="ok-button"]');
|
||||
button.setAttribute('data-element-id', elementIdCounter);
|
||||
button.addEventListener('click', this.addNewLink);
|
||||
}
|
||||
|
||||
prepareEasyMDEToolbarActions() {
|
||||
this.easyMDEToolbarDefault = [
|
||||
'bold', 'italic', 'strikethrough', '|', 'heading-1', 'heading-2', 'heading-3',
|
||||
|
|
Loading…
Add table
Reference in a new issue