Merge branch 'forgejo' into report_abuse

This commit is contained in:
floss4good 2025-02-26 15:25:55 +00:00
commit 6ad0f88a45
47 changed files with 539 additions and 94 deletions

View file

@ -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 }}"

View file

@ -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 }}

View file

@ -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

View file

@ -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}

View file

@ -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" \

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -0,0 +1 @@
[] # empty

View file

@ -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",

View file

@ -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) {

View file

@ -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
}

View file

@ -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)
}

View file

@ -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) {

View file

@ -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.

View file

@ -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)
}

View file

@ -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"`
}

View 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)
}

View file

@ -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)

View 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, &quota_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)
}

View 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)
}

View file

@ -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 *****

View file

@ -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}}

View file

@ -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}}

View file

@ -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>

View 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" .}}

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View 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" .}}

View file

@ -37,6 +37,7 @@ func TestMain(m *testing.M) {
defer cancel()
tests.InitTest(true)
setting.Quota.Enabled = true
initChangedFiles()
testE2eWebRoutes = routers.NormalRoutes()

View 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

View file

@ -0,0 +1 @@
- name: trusted-user

View file

@ -0,0 +1,5 @@
-
id: 1001
kind: 0
mapped_id: 2
group_name: trusted-user

View 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"

View 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]

View 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

View file

@ -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);

View file

@ -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);
});

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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) {

View file

@ -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%;

View file

@ -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',