From 61036235966773a0af6b690b10b33ff8222df1d7 Mon Sep 17 00:00:00 2001
From: yp05327 <576951401@qq.com>
Date: Fri, 29 Mar 2024 04:15:39 +0900
Subject: [PATCH 0001/1946] Fix `DEFAULT_SHOW_FULL_NAME=false` has no effect in
commit list and commit graph page (#30096)
Fix #20446
This PR will fix the username in:
repo home page

repo commit list page

repo commit graph page

pr commit page

Will not fix:
wiki revisions page:

ps: the author name is `FullName` by default
---
templates/repo/commits_list.tmpl | 2 +-
templates/repo/graph/commits.tmpl | 2 +-
templates/repo/latest_commit.tmpl | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl
index bae9924141..53052333fa 100644
--- a/templates/repo/commits_list.tmpl
+++ b/templates/repo/commits_list.tmpl
@@ -16,7 +16,7 @@
{{$userName := .Author.Name}}
{{if .User}}
- {{if .User.FullName}}
+ {{if and .User.FullName DefaultShowFullName}}
{{$userName = .User.FullName}}
{{end}}
{{ctx.AvatarUtils.Avatar .User 28 "tw-mr-2"}}{{$userName}}
diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl
index 96d09072da..f141dbeada 100644
--- a/templates/repo/graph/commits.tmpl
+++ b/templates/repo/graph/commits.tmpl
@@ -61,7 +61,7 @@
{{$userName := $commit.Commit.Author.Name}}
{{if $commit.User}}
- {{if $commit.User.FullName}}
+ {{if and $commit.User.FullName DefaultShowFullName}}
{{$userName = $commit.User.FullName}}
{{end}}
{{ctx.AvatarUtils.Avatar $commit.User}}
diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl
index f945e9dfa1..8bacb427bf 100644
--- a/templates/repo/latest_commit.tmpl
+++ b/templates/repo/latest_commit.tmpl
@@ -3,7 +3,7 @@
{{else}}
{{if .LatestCommitUser}}
{{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "tw-mr-1"}}
- {{if .LatestCommitUser.FullName}}
+ {{if and .LatestCommitUser.FullName DefaultShowFullName}}
{{.LatestCommitUser.FullName}}
{{else}}
{{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}}
From 62b073e6f31645e446c7e8d6b5a506f61b47924e Mon Sep 17 00:00:00 2001
From: sillyguodong <33891828+sillyguodong@users.noreply.github.com>
Date: Fri, 29 Mar 2024 04:40:35 +0800
Subject: [PATCH 0002/1946] Add API for `Variables` (#29520)
close #27801
---------
Co-authored-by: silverwind
---
models/actions/variable.go | 27 +-
modules/structs/variable.go | 37 +
modules/util/util.go | 9 +
modules/util/util_test.go | 5 +
routers/api/v1/api.go | 27 +
routers/api/v1/org/variables.go | 291 +++++++
routers/api/v1/repo/action.go | 296 ++++++++
routers/api/v1/swagger/action.go | 14 +
routers/api/v1/swagger/options.go | 6 +
routers/api/v1/user/action.go | 250 +++++++
routers/web/shared/actions/variables.go | 67 +-
routers/web/shared/secrets/secrets.go | 4 +-
services/actions/variables.go | 100 +++
templates/swagger/v1_json.tmpl | 750 ++++++++++++++++++-
tests/integration/api_repo_variables_test.go | 149 ++++
tests/integration/api_user_variables_test.go | 144 ++++
16 files changed, 2102 insertions(+), 74 deletions(-)
create mode 100644 modules/structs/variable.go
create mode 100644 routers/api/v1/org/variables.go
create mode 100644 services/actions/variables.go
create mode 100644 tests/integration/api_repo_variables_test.go
create mode 100644 tests/integration/api_user_variables_test.go
diff --git a/models/actions/variable.go b/models/actions/variable.go
index 14ded60fac..b0a455e675 100644
--- a/models/actions/variable.go
+++ b/models/actions/variable.go
@@ -6,13 +6,11 @@ package actions
import (
"context"
"errors"
- "fmt"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
@@ -55,24 +53,24 @@ type FindVariablesOpts struct {
db.ListOptions
OwnerID int64
RepoID int64
+ Name string
}
func (opts FindVariablesOpts) ToConds() builder.Cond {
cond := builder.NewCond()
+ // Since we now support instance-level variables,
+ // there is no need to check for null values for `owner_id` and `repo_id`
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
+
+ if opts.Name != "" {
+ cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})
+ }
return cond
}
-func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) {
- var variable ActionVariable
- has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable)
- if err != nil {
- return nil, err
- } else if !has {
- return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist)
- }
- return &variable, nil
+func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) {
+ return db.Find[ActionVariable](ctx, opts)
}
func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
@@ -84,6 +82,13 @@ func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error)
return count != 0, err
}
+func DeleteVariable(ctx context.Context, id int64) error {
+ if _, err := db.DeleteByID[ActionVariable](ctx, id); err != nil {
+ return err
+ }
+ return nil
+}
+
func GetVariablesOfRun(ctx context.Context, run *ActionRun) (map[string]string, error) {
variables := map[string]string{}
diff --git a/modules/structs/variable.go b/modules/structs/variable.go
new file mode 100644
index 0000000000..cc846cf0ec
--- /dev/null
+++ b/modules/structs/variable.go
@@ -0,0 +1,37 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package structs
+
+// CreateVariableOption the option when creating variable
+// swagger:model
+type CreateVariableOption struct {
+ // Value of the variable to create
+ //
+ // required: true
+ Value string `json:"value" binding:"Required"`
+}
+
+// UpdateVariableOption the option when updating variable
+// swagger:model
+type UpdateVariableOption struct {
+ // New name for the variable. If the field is empty, the variable name won't be updated.
+ Name string `json:"name"`
+ // Value of the variable to update
+ //
+ // required: true
+ Value string `json:"value" binding:"Required"`
+}
+
+// ActionVariable return value of the query API
+// swagger:model
+type ActionVariable struct {
+ // the owner to which the variable belongs
+ OwnerID int64 `json:"owner_id"`
+ // the repository to which the variable belongs
+ RepoID int64 `json:"repo_id"`
+ // the name of the variable
+ Name string `json:"name"`
+ // the value of the variable
+ Data string `json:"data"`
+}
diff --git a/modules/util/util.go b/modules/util/util.go
index c94fb91047..b6e730eb54 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -221,3 +221,12 @@ func IfZero[T comparable](v, def T) T {
}
return v
}
+
+func ReserveLineBreakForTextarea(input string) string {
+ // Since the content is from a form which is a textarea, the line endings are \r\n.
+ // It's a standard behavior of HTML.
+ // But we want to store them as \n like what GitHub does.
+ // And users are unlikely to really need to keep the \r.
+ // Other than this, we should respect the original content, even leading or trailing spaces.
+ return strings.ReplaceAll(input, "\r\n", "\n")
+}
diff --git a/modules/util/util_test.go b/modules/util/util_test.go
index 819e12ee91..5c5b13d04b 100644
--- a/modules/util/util_test.go
+++ b/modules/util/util_test.go
@@ -235,3 +235,8 @@ func TestToPointer(t *testing.T) {
val123 := 123
assert.False(t, &val123 == ToPointer(val123))
}
+
+func TestReserveLineBreakForTextarea(t *testing.T) {
+ assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata"), "test\ndata")
+ assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata\r\n"), "test\ndata\n")
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index c65650c388..e870378c4b 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -955,6 +955,15 @@ func Routes() *web.Route {
Delete(user.DeleteSecret)
})
+ m.Group("/variables", func() {
+ m.Get("", user.ListVariables)
+ m.Combo("/{variablename}").
+ Get(user.GetVariable).
+ Delete(user.DeleteVariable).
+ Post(bind(api.CreateVariableOption{}), user.CreateVariable).
+ Put(bind(api.UpdateVariableOption{}), user.UpdateVariable)
+ })
+
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
})
@@ -1073,6 +1082,15 @@ func Routes() *web.Route {
Delete(reqToken(), reqOwner(), repo.DeleteSecret)
})
+ m.Group("/variables", func() {
+ m.Get("", reqToken(), reqOwner(), repo.ListVariables)
+ m.Combo("/{variablename}").
+ Get(reqToken(), reqOwner(), repo.GetVariable).
+ Delete(reqToken(), reqOwner(), repo.DeleteVariable).
+ Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable).
+ Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable)
+ })
+
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken)
})
@@ -1452,6 +1470,15 @@ func Routes() *web.Route {
Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret)
})
+ m.Group("/variables", func() {
+ m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables)
+ m.Combo("/{variablename}").
+ Get(reqToken(), reqOrgOwnership(), org.GetVariable).
+ Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable).
+ Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable).
+ Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable)
+ })
+
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken)
})
diff --git a/routers/api/v1/org/variables.go b/routers/api/v1/org/variables.go
new file mode 100644
index 0000000000..eaf7bdc45b
--- /dev/null
+++ b/routers/api/v1/org/variables.go
@@ -0,0 +1,291 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "errors"
+ "net/http"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ actions_service "code.gitea.io/gitea/services/actions"
+ "code.gitea.io/gitea/services/context"
+)
+
+// ListVariables list org-level variables
+func ListVariables(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
+ // ---
+ // summary: Get an org-level variables list
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/VariableList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+ OwnerID: ctx.Org.Organization.ID,
+ ListOptions: utils.GetListOptions(ctx),
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+ return
+ }
+
+ variables := make([]*api.ActionVariable, len(vars))
+ for i, v := range vars {
+ variables[i] = &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ Data: v.Data,
+ }
+ }
+
+ ctx.SetTotalCountHeader(count)
+ ctx.JSON(http.StatusOK, variables)
+}
+
+// GetVariable get an org-level variable
+func GetVariable(ctx *context.APIContext) {
+ // swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable
+ // ---
+ // summary: Get an org-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActionVariable"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ctx.Org.Organization.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ variable := &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ Data: v.Data,
+ }
+
+ ctx.JSON(http.StatusOK, variable)
+}
+
+// DeleteVariable delete an org-level variable
+func DeleteVariable(ctx *context.APIContext) {
+ // swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable
+ // ---
+ // summary: Delete an org-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActionVariable"
+ // "201":
+ // description: response when deleting a variable
+ // "204":
+ // description: response when deleting a variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("variablename")); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+ } else if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// CreateVariable create an org-level variable
+func CreateVariable(ctx *context.APIContext) {
+ // swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable
+ // ---
+ // summary: Create an org-level variable
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/CreateVariableOption"
+ // responses:
+ // "201":
+ // description: response when creating an org-level variable
+ // "204":
+ // description: response when creating an org-level variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+ ownerID := ctx.Org.Organization.ID
+ variableName := ctx.Params("variablename")
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ownerID,
+ Name: variableName,
+ })
+ if err != nil && !errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ return
+ }
+ if v != nil && v.ID > 0 {
+ ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+ return
+ }
+
+ if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update an org-level variable
+func UpdateVariable(ctx *context.APIContext) {
+ // swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable
+ // ---
+ // summary: Update an org-level variable
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: name of the organization
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/UpdateVariableOption"
+ // responses:
+ // "201":
+ // description: response when updating an org-level variable
+ // "204":
+ // description: response when updating an org-level variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ctx.Org.Organization.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ if opt.Name == "" {
+ opt.Name = ctx.Params("variablename")
+ }
+ if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index e0af276c71..03321d956d 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -7,9 +7,13 @@ import (
"errors"
"net/http"
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
@@ -127,3 +131,295 @@ func DeleteSecret(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
+
+// GetVariable get a repo-level variable
+func GetVariable(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable
+ // ---
+ // summary: Get a repo-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActionVariable"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ RepoID: ctx.Repo.Repository.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ variable := &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ Data: v.Data,
+ }
+
+ ctx.JSON(http.StatusOK, variable)
+}
+
+// DeleteVariable delete a repo-level variable
+func DeleteVariable(ctx *context.APIContext) {
+ // swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable
+ // ---
+ // summary: Delete a repo-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActionVariable"
+ // "201":
+ // description: response when deleting a variable
+ // "204":
+ // description: response when deleting a variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("variablename")); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+ } else if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// CreateVariable create a repo-level variable
+func CreateVariable(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable
+ // ---
+ // summary: Create a repo-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/CreateVariableOption"
+ // responses:
+ // "201":
+ // description: response when creating a repo-level variable
+ // "204":
+ // description: response when creating a repo-level variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+ repoID := ctx.Repo.Repository.ID
+ variableName := ctx.Params("variablename")
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ RepoID: repoID,
+ Name: variableName,
+ })
+ if err != nil && !errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ return
+ }
+ if v != nil && v.ID > 0 {
+ ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+ return
+ }
+
+ if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update a repo-level variable
+func UpdateVariable(ctx *context.APIContext) {
+ // swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable
+ // ---
+ // summary: Update a repo-level variable
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/UpdateVariableOption"
+ // responses:
+ // "201":
+ // description: response when updating a repo-level variable
+ // "204":
+ // description: response when updating a repo-level variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ RepoID: ctx.Repo.Repository.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ if opt.Name == "" {
+ opt.Name = ctx.Params("variablename")
+ }
+ if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// ListVariables list repo-level variables
+func ListVariables(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList
+ // ---
+ // summary: Get repo-level variables list
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/VariableList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+ RepoID: ctx.Repo.Repository.ID,
+ ListOptions: utils.GetListOptions(ctx),
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+ return
+ }
+
+ variables := make([]*api.ActionVariable, len(vars))
+ for i, v := range vars {
+ variables[i] = &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ }
+ }
+
+ ctx.SetTotalCountHeader(count)
+ ctx.JSON(http.StatusOK, variables)
+}
diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go
index 3771780718..665f4d0b85 100644
--- a/routers/api/v1/swagger/action.go
+++ b/routers/api/v1/swagger/action.go
@@ -18,3 +18,17 @@ type swaggerResponseSecret struct {
// in:body
Body api.Secret `json:"body"`
}
+
+// ActionVariable
+// swagger:response ActionVariable
+type swaggerResponseActionVariable struct {
+ // in:body
+ Body api.ActionVariable `json:"body"`
+}
+
+// VariableList
+// swagger:response VariableList
+type swaggerResponseVariableList struct {
+ // in:body
+ Body []api.ActionVariable `json:"body"`
+}
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 471e7d9c4e..cd551cbdfa 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -193,4 +193,10 @@ type swaggerParameterBodies struct {
// in:body
UserBadgeOption api.UserBadgeOption
+
+ // in:body
+ CreateVariableOption api.CreateVariableOption
+
+ // in:body
+ UpdateVariableOption api.UpdateVariableOption
}
diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go
index babb8c0cf7..bf78c2c864 100644
--- a/routers/api/v1/user/action.go
+++ b/routers/api/v1/user/action.go
@@ -7,9 +7,13 @@ import (
"errors"
"net/http"
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/api/v1/utils"
+ actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
@@ -101,3 +105,249 @@ func DeleteSecret(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
+
+// CreateVariable create a user-level variable
+func CreateVariable(ctx *context.APIContext) {
+ // swagger:operation POST /user/actions/variables/{variablename} user createUserVariable
+ // ---
+ // summary: Create a user-level variable
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/CreateVariableOption"
+ // responses:
+ // "201":
+ // description: response when creating a variable
+ // "204":
+ // description: response when creating a variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.CreateVariableOption)
+
+ ownerID := ctx.Doer.ID
+ variableName := ctx.Params("variablename")
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ownerID,
+ Name: variableName,
+ })
+ if err != nil && !errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ return
+ }
+ if v != nil && v.ID > 0 {
+ ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
+ return
+ }
+
+ if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "CreateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// UpdateVariable update a user-level variable which is created by current doer
+func UpdateVariable(ctx *context.APIContext) {
+ // swagger:operation PUT /user/actions/variables/{variablename} user updateUserVariable
+ // ---
+ // summary: Update a user-level variable which is created by current doer
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/UpdateVariableOption"
+ // responses:
+ // "201":
+ // description: response when updating a variable
+ // "204":
+ // description: response when updating a variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ opt := web.GetForm(ctx).(*api.UpdateVariableOption)
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ctx.Doer.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ if opt.Name == "" {
+ opt.Name = ctx.Params("variablename")
+ }
+ if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// DeleteVariable delete a user-level variable which is created by current doer
+func DeleteVariable(ctx *context.APIContext) {
+ // swagger:operation DELETE /user/actions/variables/{variablename} user deleteUserVariable
+ // ---
+ // summary: Delete a user-level variable which is created by current doer
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "201":
+ // description: response when deleting a variable
+ // "204":
+ // description: response when deleting a variable
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ if err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.Params("variablename")); err != nil {
+ if errors.Is(err, util.ErrInvalidArgument) {
+ ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
+ } else if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// GetVariable get a user-level variable which is created by current doer
+func GetVariable(ctx *context.APIContext) {
+ // swagger:operation GET /user/actions/variables/{variablename} user getUserVariable
+ // ---
+ // summary: Get a user-level variable which is created by current doer
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: variablename
+ // in: path
+ // description: name of the variable
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActionVariable"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ctx.Doer.ID,
+ Name: ctx.Params("variablename"),
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetVariable", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetVariable", err)
+ }
+ return
+ }
+
+ variable := &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ Data: v.Data,
+ }
+
+ ctx.JSON(http.StatusOK, variable)
+}
+
+// ListVariables list user-level variables
+func ListVariables(ctx *context.APIContext) {
+ // swagger:operation GET /user/actions/variables user getUserVariablesList
+ // ---
+ // summary: Get the user-level list of variables which is created by current doer
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/VariableList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
+ OwnerID: ctx.Doer.ID,
+ ListOptions: utils.GetListOptions(ctx),
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindVariables", err)
+ return
+ }
+
+ variables := make([]*api.ActionVariable, len(vars))
+ for i, v := range vars {
+ variables[i] = &api.ActionVariable{
+ OwnerID: v.OwnerID,
+ RepoID: v.RepoID,
+ Name: v.Name,
+ Data: v.Data,
+ }
+ }
+
+ ctx.SetTotalCountHeader(count)
+ ctx.JSON(http.StatusOK, variables)
+}
diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go
index 0f705399c9..79c03e4e8c 100644
--- a/routers/web/shared/actions/variables.go
+++ b/routers/web/shared/actions/variables.go
@@ -4,17 +4,13 @@
package actions
import (
- "errors"
- "regexp"
- "strings"
-
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
+ actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
- secret_service "code.gitea.io/gitea/services/secrets"
)
func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
@@ -29,41 +25,16 @@ func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
ctx.Data["Variables"] = variables
}
-// some regular expression of `variables` and `secrets`
-// reference to:
-// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
-// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
-var (
- forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
-)
-
-func envNameCIRegexMatch(name string) error {
- if forbiddenEnvNameCIRx.MatchString(name) {
- log.Error("Env Name cannot be ci")
- return errors.New("env name cannot be ci")
- }
- return nil
-}
-
func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.EditVariableForm)
- if err := secret_service.ValidateName(form.Name); err != nil {
- ctx.JSONError(err.Error())
- return
- }
-
- if err := envNameCIRegexMatch(form.Name); err != nil {
- ctx.JSONError(err.Error())
- return
- }
-
- v, err := actions_model.InsertVariable(ctx, ownerID, repoID, form.Name, ReserveLineBreakForTextarea(form.Data))
+ v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data)
if err != nil {
- log.Error("InsertVariable error: %v", err)
+ log.Error("CreateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}
+
ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
ctx.JSONRedirect(redirectURL)
}
@@ -72,23 +43,8 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
id := ctx.ParamsInt64(":variable_id")
form := web.GetForm(ctx).(*forms.EditVariableForm)
- if err := secret_service.ValidateName(form.Name); err != nil {
- ctx.JSONError(err.Error())
- return
- }
-
- if err := envNameCIRegexMatch(form.Name); err != nil {
- ctx.JSONError(err.Error())
- return
- }
-
- ok, err := actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
- ID: id,
- Name: strings.ToUpper(form.Name),
- Data: ReserveLineBreakForTextarea(form.Data),
- })
- if err != nil || !ok {
- log.Error("UpdateVariable error: %v", err)
+ if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok {
+ log.Error("UpdateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
return
}
@@ -99,7 +55,7 @@ func UpdateVariable(ctx *context.Context, redirectURL string) {
func DeleteVariable(ctx *context.Context, redirectURL string) {
id := ctx.ParamsInt64(":variable_id")
- if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil {
+ if err := actions_service.DeleteVariableByID(ctx, id); err != nil {
log.Error("Delete variable [%d] failed: %v", id, err)
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
return
@@ -107,12 +63,3 @@ func DeleteVariable(ctx *context.Context, redirectURL string) {
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
ctx.JSONRedirect(redirectURL)
}
-
-func ReserveLineBreakForTextarea(input string) string {
- // Since the content is from a form which is a textarea, the line endings are \r\n.
- // It's a standard behavior of HTML.
- // But we want to store them as \n like what GitHub does.
- // And users are unlikely to really need to keep the \r.
- // Other than this, we should respect the original content, even leading or trailing spaces.
- return strings.ReplaceAll(input, "\r\n", "\n")
-}
diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go
index 73505ec372..3bd421f86a 100644
--- a/routers/web/shared/secrets/secrets.go
+++ b/routers/web/shared/secrets/secrets.go
@@ -7,8 +7,8 @@ import (
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/web/shared/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
secret_service "code.gitea.io/gitea/services/secrets"
@@ -27,7 +27,7 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.AddSecretForm)
- s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, actions.ReserveLineBreakForTextarea(form.Data))
+ s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
if err != nil {
log.Error("CreateOrUpdateSecret failed: %v", err)
ctx.JSONError(ctx.Tr("secrets.creation.failed"))
diff --git a/services/actions/variables.go b/services/actions/variables.go
new file mode 100644
index 0000000000..8dde9c4af5
--- /dev/null
+++ b/services/actions/variables.go
@@ -0,0 +1,100 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "context"
+ "regexp"
+ "strings"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
+ secret_service "code.gitea.io/gitea/services/secrets"
+)
+
+func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) {
+ if err := secret_service.ValidateName(name); err != nil {
+ return nil, err
+ }
+
+ if err := envNameCIRegexMatch(name); err != nil {
+ return nil, err
+ }
+
+ v, err := actions_model.InsertVariable(ctx, ownerID, repoID, name, util.ReserveLineBreakForTextarea(data))
+ if err != nil {
+ return nil, err
+ }
+
+ return v, nil
+}
+
+func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) {
+ if err := secret_service.ValidateName(name); err != nil {
+ return false, err
+ }
+
+ if err := envNameCIRegexMatch(name); err != nil {
+ return false, err
+ }
+
+ return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
+ ID: variableID,
+ Name: strings.ToUpper(name),
+ Data: util.ReserveLineBreakForTextarea(data),
+ })
+}
+
+func DeleteVariableByID(ctx context.Context, variableID int64) error {
+ return actions_model.DeleteVariable(ctx, variableID)
+}
+
+func DeleteVariableByName(ctx context.Context, ownerID, repoID int64, name string) error {
+ if err := secret_service.ValidateName(name); err != nil {
+ return err
+ }
+
+ if err := envNameCIRegexMatch(name); err != nil {
+ return err
+ }
+
+ v, err := GetVariable(ctx, actions_model.FindVariablesOpts{
+ OwnerID: ownerID,
+ RepoID: repoID,
+ Name: name,
+ })
+ if err != nil {
+ return err
+ }
+
+ return actions_model.DeleteVariable(ctx, v.ID)
+}
+
+func GetVariable(ctx context.Context, opts actions_model.FindVariablesOpts) (*actions_model.ActionVariable, error) {
+ vars, err := actions_model.FindVariables(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+ if len(vars) != 1 {
+ return nil, util.NewNotExistErrorf("variable not found")
+ }
+ return vars[0], nil
+}
+
+// some regular expression of `variables` and `secrets`
+// reference to:
+// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
+// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
+var (
+ forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
+)
+
+func envNameCIRegexMatch(name string) error {
+ if forbiddenEnvNameCIRx.MatchString(name) {
+ log.Error("Env Name cannot be ci")
+ return util.NewInvalidArgumentErrorf("env name cannot be ci")
+ }
+ return nil
+}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index ef6126ff85..b5677c77e0 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -1844,6 +1844,232 @@
}
}
},
+ "/orgs/{org}/actions/variables": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "Get an org-level variables list",
+ "operationId": "getOrgVariablesList",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "page number of results to return (1-based)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page size of results",
+ "name": "limit",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/VariableList"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ }
+ },
+ "/orgs/{org}/actions/variables/{variablename}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "Get an org-level variable",
+ "operationId": "getOrgVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/ActionVariable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "Update an org-level variable",
+ "operationId": "updateOrgVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/UpdateVariableOption"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "response when updating an org-level variable"
+ },
+ "204": {
+ "description": "response when updating an org-level variable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "Create an org-level variable",
+ "operationId": "createOrgVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/CreateVariableOption"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "response when creating an org-level variable"
+ },
+ "204": {
+ "description": "response when creating an org-level variable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "Delete an org-level variable",
+ "operationId": "deleteOrgVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the organization",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/ActionVariable"
+ },
+ "201": {
+ "description": "response when deleting a variable"
+ },
+ "204": {
+ "description": "response when deleting a variable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ }
+ },
"/orgs/{org}/activities/feeds": {
"get": {
"produces": [
@@ -3723,6 +3949,261 @@
}
}
},
+ "/repos/{owner}/{repo}/actions/variables": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Get repo-level variables list",
+ "operationId": "getRepoVariablesList",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the owner",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repository",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "integer",
+ "description": "page number of results to return (1-based)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page size of results",
+ "name": "limit",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/VariableList"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ }
+ },
+ "/repos/{owner}/{repo}/actions/variables/{variablename}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Get a repo-level variable",
+ "operationId": "getRepoVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the owner",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repository",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/ActionVariable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "put": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Update a repo-level variable",
+ "operationId": "updateRepoVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the owner",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repository",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/UpdateVariableOption"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "response when updating a repo-level variable"
+ },
+ "204": {
+ "description": "response when updating a repo-level variable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Create a repo-level variable",
+ "operationId": "createRepoVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the owner",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repository",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/CreateVariableOption"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "response when creating a repo-level variable"
+ },
+ "204": {
+ "description": "response when creating a repo-level variable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Delete a repo-level variable",
+ "operationId": "deleteRepoVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the owner",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repository",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/ActionVariable"
+ },
+ "201": {
+ "description": "response when deleting a variable"
+ },
+ "204": {
+ "description": "response when deleting a variable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ }
+ },
"/repos/{owner}/{repo}/activities/feeds": {
"get": {
"produces": [
@@ -15050,6 +15531,194 @@
}
}
},
+ "/user/actions/variables": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "user"
+ ],
+ "summary": "Get the user-level list of variables which is created by current doer",
+ "operationId": "getUserVariablesList",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "page number of results to return (1-based)",
+ "name": "page",
+ "in": "query"
+ },
+ {
+ "type": "integer",
+ "description": "page size of results",
+ "name": "limit",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/VariableList"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ }
+ },
+ "/user/actions/variables/{variablename}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "user"
+ ],
+ "summary": "Get a user-level variable which is created by current doer",
+ "operationId": "getUserVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/ActionVariable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "put": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "user"
+ ],
+ "summary": "Update a user-level variable which is created by current doer",
+ "operationId": "updateUserVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/UpdateVariableOption"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "response when updating a variable"
+ },
+ "204": {
+ "description": "response when updating a variable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "user"
+ ],
+ "summary": "Create a user-level variable",
+ "operationId": "createUserVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/CreateVariableOption"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "response when creating a variable"
+ },
+ "204": {
+ "description": "response when creating a variable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "user"
+ ],
+ "summary": "Delete a user-level variable which is created by current doer",
+ "operationId": "deleteUserVariable",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "name of the variable",
+ "name": "variablename",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "response when deleting a variable"
+ },
+ "204": {
+ "description": "response when deleting a variable"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ }
+ },
"/user/applications/oauth2": {
"get": {
"produces": [
@@ -17193,6 +17862,35 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "ActionVariable": {
+ "description": "ActionVariable return value of the query API",
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "the value of the variable",
+ "type": "string",
+ "x-go-name": "Data"
+ },
+ "name": {
+ "description": "the name of the variable",
+ "type": "string",
+ "x-go-name": "Name"
+ },
+ "owner_id": {
+ "description": "the owner to which the variable belongs",
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "OwnerID"
+ },
+ "repo_id": {
+ "description": "the repository to which the variable belongs",
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "RepoID"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"Activity": {
"type": "object",
"properties": {
@@ -19079,6 +19777,21 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "CreateVariableOption": {
+ "description": "CreateVariableOption the option when creating variable",
+ "type": "object",
+ "required": [
+ "value"
+ ],
+ "properties": {
+ "value": {
+ "description": "Value of the variable to create",
+ "type": "string",
+ "x-go-name": "Value"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"CreateWikiPageOptions": {
"description": "CreateWikiPageOptions form for creating wiki",
"type": "object",
@@ -23371,6 +24084,26 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "UpdateVariableOption": {
+ "description": "UpdateVariableOption the option when updating variable",
+ "type": "object",
+ "required": [
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "description": "New name for the variable. If the field is empty, the variable name won't be updated.",
+ "type": "string",
+ "x-go-name": "Name"
+ },
+ "value": {
+ "description": "Value of the variable to update",
+ "type": "string",
+ "x-go-name": "Value"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"User": {
"description": "User represents a user",
"type": "object",
@@ -23752,6 +24485,12 @@
}
}
},
+ "ActionVariable": {
+ "description": "ActionVariable",
+ "schema": {
+ "$ref": "#/definitions/ActionVariable"
+ }
+ },
"ActivityFeedsList": {
"description": "ActivityFeedsList",
"schema": {
@@ -24635,6 +25374,15 @@
}
}
},
+ "VariableList": {
+ "description": "VariableList",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ActionVariable"
+ }
+ }
+ },
"WatchInfo": {
"description": "WatchInfo",
"schema": {
@@ -24710,7 +25458,7 @@
"parameterBodies": {
"description": "parameterBodies",
"schema": {
- "$ref": "#/definitions/UserBadgeOption"
+ "$ref": "#/definitions/UpdateVariableOption"
}
},
"redirect": {
diff --git a/tests/integration/api_repo_variables_test.go b/tests/integration/api_repo_variables_test.go
new file mode 100644
index 0000000000..7847962b07
--- /dev/null
+++ b/tests/integration/api_repo_variables_test.go
@@ -0,0 +1,149 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+)
+
+func TestAPIRepoVariables(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+ t.Run("CreateRepoVariable", func(t *testing.T) {
+ cases := []struct {
+ Name string
+ ExpectedStatus int
+ }{
+ {
+ Name: "-",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: "_",
+ ExpectedStatus: http.StatusNoContent,
+ },
+ {
+ Name: "TEST_VAR",
+ ExpectedStatus: http.StatusNoContent,
+ },
+ {
+ Name: "test_var",
+ ExpectedStatus: http.StatusConflict,
+ },
+ {
+ Name: "ci",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: "123var",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: "var@test",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: "github_var",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: "gitea_var",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ }
+
+ for _, c := range cases {
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.CreateVariableOption{
+ Value: "value",
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, c.ExpectedStatus)
+ }
+ })
+
+ t.Run("UpdateRepoVariable", func(t *testing.T) {
+ variableName := "test_update_var"
+ url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName)
+ req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+ Value: "initial_val",
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ cases := []struct {
+ Name string
+ UpdateName string
+ ExpectedStatus int
+ }{
+ {
+ Name: "not_found_var",
+ ExpectedStatus: http.StatusNotFound,
+ },
+ {
+ Name: variableName,
+ UpdateName: "1invalid",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: variableName,
+ UpdateName: "invalid@name",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: variableName,
+ UpdateName: "ci",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: variableName,
+ UpdateName: "updated_var_name",
+ ExpectedStatus: http.StatusNoContent,
+ },
+ {
+ Name: variableName,
+ ExpectedStatus: http.StatusNotFound,
+ },
+ {
+ Name: "updated_var_name",
+ ExpectedStatus: http.StatusNoContent,
+ },
+ }
+
+ for _, c := range cases {
+ req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), c.Name), api.UpdateVariableOption{
+ Name: c.UpdateName,
+ Value: "updated_val",
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, c.ExpectedStatus)
+ }
+ })
+
+ t.Run("DeleteRepoVariable", func(t *testing.T) {
+ variableName := "test_delete_var"
+ url := fmt.Sprintf("/api/v1/repos/%s/actions/variables/%s", repo.FullName(), variableName)
+
+ req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+ Value: "initial_val",
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+}
diff --git a/tests/integration/api_user_variables_test.go b/tests/integration/api_user_variables_test.go
new file mode 100644
index 0000000000..dd5501f0b9
--- /dev/null
+++ b/tests/integration/api_user_variables_test.go
@@ -0,0 +1,144 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/tests"
+)
+
+func TestAPIUserVariables(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
+
+ t.Run("CreateRepoVariable", func(t *testing.T) {
+ cases := []struct {
+ Name string
+ ExpectedStatus int
+ }{
+ {
+ Name: "-",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: "_",
+ ExpectedStatus: http.StatusNoContent,
+ },
+ {
+ Name: "TEST_VAR",
+ ExpectedStatus: http.StatusNoContent,
+ },
+ {
+ Name: "test_var",
+ ExpectedStatus: http.StatusConflict,
+ },
+ {
+ Name: "ci",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: "123var",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: "var@test",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: "github_var",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: "gitea_var",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ }
+
+ for _, c := range cases {
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.CreateVariableOption{
+ Value: "value",
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, c.ExpectedStatus)
+ }
+ })
+
+ t.Run("UpdateRepoVariable", func(t *testing.T) {
+ variableName := "test_update_var"
+ url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName)
+ req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+ Value: "initial_val",
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ cases := []struct {
+ Name string
+ UpdateName string
+ ExpectedStatus int
+ }{
+ {
+ Name: "not_found_var",
+ ExpectedStatus: http.StatusNotFound,
+ },
+ {
+ Name: variableName,
+ UpdateName: "1invalid",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: variableName,
+ UpdateName: "invalid@name",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: variableName,
+ UpdateName: "ci",
+ ExpectedStatus: http.StatusBadRequest,
+ },
+ {
+ Name: variableName,
+ UpdateName: "updated_var_name",
+ ExpectedStatus: http.StatusNoContent,
+ },
+ {
+ Name: variableName,
+ ExpectedStatus: http.StatusNotFound,
+ },
+ {
+ Name: "updated_var_name",
+ ExpectedStatus: http.StatusNoContent,
+ },
+ }
+
+ for _, c := range cases {
+ req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/user/actions/variables/%s", c.Name), api.UpdateVariableOption{
+ Name: c.UpdateName,
+ Value: "updated_val",
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, c.ExpectedStatus)
+ }
+ })
+
+ t.Run("DeleteRepoVariable", func(t *testing.T) {
+ variableName := "test_delete_var"
+ url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName)
+
+ req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{
+ Value: "initial_val",
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusNoContent)
+
+ req = NewRequest(t, "DELETE", url).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+}
From dd8dde2be89921b2b1497c6cc5eafdde213429cb Mon Sep 17 00:00:00 2001
From: silverwind
Date: Fri, 29 Mar 2024 04:00:07 +0100
Subject: [PATCH 0003/1946] replace jquery-minicolors with coloris (#30055)
Get rid of one more jQuery dependant and have a nicer color picker as
well.
Now there is only a single global color picker init because that is all
that's necessary because the elements are present on the page when the
init code runs. The init is slightly weird because the module only takes
a selector instead of DOM elements directly.
The label modals now also perform form validation because previously it
was possible to trigger a 500 error `Color cannot be empty.` by clearing
out the color value on labels.
---
.dockerignore | 1 -
.gitignore | 1 -
Makefile | 2 +-
package-lock.json | 15 +-
package.json | 2 +-
templates/projects/view.tmpl | 8 +-
.../repo/issue/labels/edit_delete_label.tmpl | 4 +-
templates/repo/issue/labels/label_new.tmpl | 4 +-
web_src/css/base.css | 5 -
web_src/css/features/colorpicker.css | 164 ++++++++++++++++++
web_src/css/features/projects.css | 23 ---
web_src/css/repo.css | 18 --
web_src/js/features/colorpicker.js | 35 +++-
web_src/js/features/common-global.js | 6 +-
web_src/js/features/comp/ColorPicker.js | 16 --
web_src/js/features/comp/LabelEdit.js | 17 +-
web_src/js/index.js | 2 +
webpack.config.js | 7 -
18 files changed, 224 insertions(+), 106 deletions(-)
create mode 100644 web_src/css/features/colorpicker.css
delete mode 100644 web_src/js/features/comp/ColorPicker.js
diff --git a/.dockerignore b/.dockerignore
index 7143c039fd..b299c7313d 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -78,7 +78,6 @@ cpu.out
/public/assets/css
/public/assets/fonts
/public/assets/img/avatar
-/public/assets/img/webpack
/vendor
/web_src/fomantic/node_modules
/web_src/fomantic/build/*
diff --git a/.gitignore b/.gitignore
index abf9565cff..501fef7dcf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,7 +77,6 @@ cpu.out
/public/assets/css
/public/assets/fonts
/public/assets/licenses.txt
-/public/assets/img/webpack
/vendor
/web_src/fomantic/node_modules
/web_src/fomantic/build/*
diff --git a/Makefile b/Makefile
index b4fa62e05e..8489520920 100644
--- a/Makefile
+++ b/Makefile
@@ -119,7 +119,7 @@ FOMANTIC_WORK_DIR := web_src/fomantic
WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
WEBPACK_CONFIGS := webpack.config.js tailwind.config.js
WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
-WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/img/webpack
+WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts
BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))
diff --git a/package-lock.json b/package-lock.json
index fa4f80fbe8..25fe14e1a6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,11 +9,11 @@
"@citation-js/plugin-bibtex": "0.7.9",
"@citation-js/plugin-csl": "0.7.9",
"@citation-js/plugin-software-formats": "0.6.1",
- "@claviska/jquery-minicolors": "2.3.6",
"@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.4.0",
"@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
+ "@melloware/coloris": "0.23.0",
"@primer/octicons": "19.9.0",
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
@@ -394,14 +394,6 @@
"node": ">=14.0.0"
}
},
- "node_modules/@claviska/jquery-minicolors": {
- "version": "2.3.6",
- "resolved": "https://registry.npmjs.org/@claviska/jquery-minicolors/-/jquery-minicolors-2.3.6.tgz",
- "integrity": "sha512-8Ro6D4GCrmOl41+6w4NFhEOpx8vjxwVRI69bulXsFDt49uVRKhLU5TnzEV7AmOJrylkVq+ugnYNMiGHBieeKUQ==",
- "peerDependencies": {
- "jquery": ">= 1.7.x"
- }
- },
"node_modules/@csstools/css-parser-algorithms": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz",
@@ -1297,6 +1289,11 @@
"@mcaptcha/core-glue": "^0.1.0-alpha-5"
}
},
+ "node_modules/@melloware/coloris": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@melloware/coloris/-/coloris-0.23.0.tgz",
+ "integrity": "sha512-VGIjI9+IQwg6BHjIE10yl0K2ARYz5bsjn6BgFEs1y1ErPAQymgdoxwVcSVL4Ai5t9OVs8xaCB7JKHqFu2N96Ow=="
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
diff --git a/package.json b/package.json
index b5bfda9dc6..d5a1d46056 100644
--- a/package.json
+++ b/package.json
@@ -8,11 +8,11 @@
"@citation-js/plugin-bibtex": "0.7.9",
"@citation-js/plugin-csl": "0.7.9",
"@citation-js/plugin-software-formats": "0.6.1",
- "@claviska/jquery-minicolors": "2.3.6",
"@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.4.0",
"@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
+ "@melloware/coloris": "0.23.0",
"@primer/octicons": "19.9.0",
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index b45174b086..33dd758c79 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -42,8 +42,8 @@
-
@@ -114,8 +114,8 @@
-
diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl
index 98e0f47020..fcf69217ea 100644
--- a/templates/repo/issue/labels/edit_delete_label.tmpl
+++ b/templates/repo/issue/labels/edit_delete_label.tmpl
@@ -52,8 +52,8 @@
-
diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl
index 2b2b2336c4..32fd8e76d7 100644
--- a/templates/repo/issue/labels/label_new.tmpl
+++ b/templates/repo/issue/labels/label_new.tmpl
@@ -27,8 +27,8 @@
-
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 368bc56126..21090f67ba 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1436,11 +1436,6 @@ table th[data-sortt-desc] .svg {
vertical-align: -0.15em;
}
-/* for the jquery.minicolors plugin */
-.minicolors-panel {
- background: var(--color-secondary-dark-1) !important;
-}
-
.ui.tabular.menu {
border-color: var(--color-secondary);
}
diff --git a/web_src/css/features/colorpicker.css b/web_src/css/features/colorpicker.css
new file mode 100644
index 0000000000..0c651cfeb3
--- /dev/null
+++ b/web_src/css/features/colorpicker.css
@@ -0,0 +1,164 @@
+/* This is a stripped-down version of coloris's CSS tailored to our needs. It does only include
+ opaqua colors, and if more features like opacity are needed, the CSS needs to be extended
+ based on upstream: https://github.com/mdbassit/Coloris/blob/main/src/coloris.css. */
+
+.js-color-picker-input {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.js-color-picker-input input {
+ padding-top: 8px !important;
+ padding-bottom: 8px !important;
+ padding-left: 32px !important;
+}
+
+.clr-picker {
+ display: none;
+ flex-wrap: wrap;
+ position: absolute;
+ width: 200px;
+ z-index: 1002; /* above .ui.modal which has 1001 */
+ border-radius: var(--border-radius);
+ background-color: var(--color-menu);
+ justify-content: flex-end;
+ direction: ltr;
+ box-shadow: 0 5px 20px var(--color-shadow);
+ user-select: none;
+}
+
+.clr-picker.clr-open {
+ display: flex;
+}
+
+.clr-gradient {
+ position: relative;
+ width: 100%;
+ height: 100px;
+ border-radius: 3px 3px 0 0;
+ background: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentcolor); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
+ cursor: pointer;
+}
+
+.clr-marker {
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ margin: -6px 0 0 -6px;
+ border: 1px solid var(--color-white);
+ border-radius: 50%;
+ background-color: currentcolor;
+ cursor: pointer;
+}
+
+.clr-picker input[type="range"]::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 16px;
+}
+
+.clr-picker input[type="range"]::-webkit-slider-thumb {
+ width: 16px;
+ height: 16px;
+ -webkit-appearance: none;
+}
+
+.clr-picker input[type="range"]::-moz-range-track {
+ width: 100%;
+ height: 16px;
+ border: 0;
+}
+
+.clr-picker input[type="range"]::-moz-range-thumb {
+ width: 16px;
+ height: 16px;
+ border: 0;
+}
+
+.clr-hue {
+ background: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
+ position: relative;
+ width: calc(100% - 40px);
+ height: 10px;
+ margin: 10px 20px;
+ border-radius: 4px;
+}
+
+.clr-hue input[type="range"] {
+ position: absolute;
+ width: calc(100% + 32px);
+ margin: 0;
+ background-color: transparent;
+ opacity: 0;
+ cursor: pointer;
+ appearance: none;
+}
+
+.clr-hue div {
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ left: 0;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ border: 2px solid var(--color-white);
+ border-radius: 50%;
+ background-color: currentcolor;
+ box-shadow: 0 0 1px var(--color-shadow);
+ pointer-events: none;
+}
+
+.clr-field {
+ flex: 1;
+ position: relative;
+ color: transparent;
+}
+
+.clr-field button {
+ position: absolute;
+ aspect-ratio: 1;
+ height: 16px;
+ left: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ margin: 0;
+ padding: 0;
+ border: 0;
+ color: inherit;
+ pointer-events: none;
+ border-radius: 2px;
+ background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
+ background-position: 0 0, 4px 4px;
+ background-size: 8px 8px;
+}
+
+.clr-field button::after {
+ content: "";
+ display: block;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ border-radius: inherit;
+ background-color: currentcolor;
+}
+
+.clr-marker:focus {
+ outline: none;
+}
+
+.clr-keyboard-nav .clr-marker:focus,
+.clr-keyboard-nav .clr-hue input:focus + div,
+.clr-keyboard-nav .clr-alpha input:focus + div {
+ outline: none;
+ box-shadow: 0 0 2px 2px var(--color-white);
+}
+
+.clr-picker .clr-preview,
+.clr-picker .clr-clear,
+.clr-picker .clr-swatches,
+.clr-picker .clr-format,
+.clr-picker .clr-alpha,
+.clr-picker .clr-color {
+ display: none;
+}
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css
index 30df994c38..cec5e6fc64 100644
--- a/web_src/css/features/projects.css
+++ b/web_src/css/features/projects.css
@@ -102,26 +102,3 @@
.card-ghost * {
opacity: 0;
}
-
-.color-field .minicolors.minicolors-theme-default {
- display: block;
-}
-
-.color-field .minicolors.minicolors-theme-default .minicolors-input {
- height: 38px;
- padding-left: 2rem;
-}
-
-.color-field .minicolors.minicolors-theme-default .minicolors-swatch {
- top: 10px;
-}
-
-.edit-project-column-modal .color.picker.column,
-.new-project-column-modal .color.picker.column {
- display: flex;
-}
-
-.edit-project-column-modal .color.picker.column .minicolors,
-.new-project-column-modal .color.picker.column .minicolors {
- flex: 1;
-}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 18f28dc4a6..780093fb7f 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -2260,24 +2260,6 @@
padding-top: 15px;
}
-.edit-label.modal .form .color.picker.column,
-.new-label.modal .form .color.picker.column {
- display: flex;
-}
-
-.edit-label.modal .form .color.picker.column .minicolors,
-.new-label.modal .form .color.picker.column .minicolors {
- flex: 1;
-}
-
-.edit-label.modal .form .minicolors-swatch.minicolors-sprite,
-.new-label.modal .form .minicolors-swatch.minicolors-sprite {
- top: 10px;
- left: 10px;
- width: 15px;
- height: 15px;
-}
-
.tab-size-1 {
tab-size: 1 !important;
-moz-tab-size: 1 !important;
diff --git a/web_src/js/features/colorpicker.js b/web_src/js/features/colorpicker.js
index df0353376d..f342598e66 100644
--- a/web_src/js/features/colorpicker.js
+++ b/web_src/js/features/colorpicker.js
@@ -1,12 +1,31 @@
-import $ from 'jquery';
+export async function initColorPickers(selector = '.js-color-picker-input input', opts = {}) {
+ const inputEls = document.querySelectorAll(selector);
+ if (!inputEls.length) return;
-export async function createColorPicker(els) {
- if (!els.length) return;
-
- await Promise.all([
- import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors'),
- import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors/jquery.minicolors.css'),
+ const [{coloris, init}] = await Promise.all([
+ import(/* webpackChunkName: "colorpicker" */'@melloware/coloris'),
+ import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
]);
- return $(els).minicolors();
+ init();
+ coloris({
+ el: selector,
+ alpha: false,
+ focusInput: true,
+ selectInput: false,
+ ...opts,
+ });
+
+ for (const inputEl of inputEls) {
+ const parent = inputEl.closest('.js-color-picker-input');
+ // prevent tabbing on the color preview `button` inside the input
+ parent.querySelector('button').tabIndex = -1;
+ // init precolors
+ for (const el of parent.querySelectorAll('.precolors .color')) {
+ el.addEventListener('click', (e) => {
+ inputEl.value = e.target.getAttribute('data-color-hex');
+ inputEl.dispatchEvent(new Event('input', {bubbles: true}));
+ });
+ }
+ }
}
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 18849ba7c1..ce702f041f 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -2,7 +2,6 @@ import $ from 'jquery';
import '../vendor/jquery.are-you-sure.js';
import {clippie} from 'clippie';
import {createDropzone} from './dropzone.js';
-import {initCompColorPicker} from './comp/ColorPicker.js';
import {showGlobalErrorMessage} from '../bootstrap.js';
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
import {svg} from '../svg.js';
@@ -379,10 +378,7 @@ function initGlobalShowModal() {
$attrTarget.text(attrib.value); // FIXME: it should be more strict here, only handle div/span/p
}
}
- const $colorPickers = $modal.find('.color-picker');
- if ($colorPickers.length > 0) {
- initCompColorPicker(); // FIXME: this might cause duplicate init
- }
+
$modal.modal('setting', {
onApprove: () => {
// "form-fetch-action" can handle network errors gracefully,
diff --git a/web_src/js/features/comp/ColorPicker.js b/web_src/js/features/comp/ColorPicker.js
deleted file mode 100644
index d7e7038803..0000000000
--- a/web_src/js/features/comp/ColorPicker.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import $ from 'jquery';
-import {createColorPicker} from '../colorpicker.js';
-
-export function initCompColorPicker() {
- (async () => {
- await createColorPicker(document.querySelectorAll('.color-picker'));
-
- for (const el of document.querySelectorAll('.precolors .color')) {
- el.addEventListener('click', (e) => {
- const color = e.target.getAttribute('data-color-hex');
- const parent = e.target.closest('.color.picker');
- $(parent.querySelector('.color-picker')).minicolors('value', color);
- });
- }
- })();
-}
diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js
index 843657a6b6..2cc75cc6b0 100644
--- a/web_src/js/features/comp/LabelEdit.js
+++ b/web_src/js/features/comp/LabelEdit.js
@@ -1,5 +1,4 @@
import $ from 'jquery';
-import {initCompColorPicker} from './ColorPicker.js';
function isExclusiveScopeName(name) {
return /.*[^/]\/[^/].*/.test(name);
@@ -28,13 +27,17 @@ function updateExclusiveLabelEdit(form) {
export function initCompLabelEdit(selector) {
if (!$(selector).length) return;
- initCompColorPicker();
// Create label
$('.new-label.button').on('click', () => {
updateExclusiveLabelEdit('.new-label');
$('.new-label.modal').modal({
onApprove() {
+ const form = document.querySelector('.new-label.form');
+ if (!form.checkValidity()) {
+ form.reportValidity();
+ return false;
+ }
$('.new-label.form').trigger('submit');
},
}).modal('show');
@@ -60,10 +63,18 @@ export function initCompLabelEdit(selector) {
updateExclusiveLabelEdit('.edit-label');
$('.edit-label .label-desc-input').val(this.getAttribute('data-description'));
- $('.edit-label .color-picker').minicolors('value', this.getAttribute('data-color'));
+
+ const colorInput = document.querySelector('.edit-label .js-color-picker-input input');
+ colorInput.value = this.getAttribute('data-color');
+ colorInput.dispatchEvent(new Event('input', {bubbles: true}));
$('.edit-label.modal').modal({
onApprove() {
+ const form = document.querySelector('.edit-label.form');
+ if (!form.checkValidity()) {
+ form.reportValidity();
+ return false;
+ }
$('.edit-label.form').trigger('submit');
},
}).modal('show');
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 4c707486bd..fc2f6b9b0b 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -86,6 +86,7 @@ import {initRepoRecentCommits} from './features/recent-commits.js';
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
import {initDirAuto} from './modules/dirauto.js';
import {initRepositorySearch} from './features/repo-search.js';
+import {initColorPickers} from './features/colorpicker.js';
// Init Gitea's Fomantic settings
initGiteaFomantic();
@@ -188,4 +189,5 @@ onDomReady(() => {
initRepoDiffView();
initPdfViewer();
initScopedAccessTokenCategories();
+ initColorPickers();
});
diff --git a/webpack.config.js b/webpack.config.js
index 0b0e7403e8..fdf80a5313 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -192,13 +192,6 @@ export default {
filename: 'fonts/[name].[contenthash:8][ext]',
},
},
- {
- test: /\.png$/i,
- type: 'asset/resource',
- generator: {
- filename: 'img/webpack/[name].[contenthash:8][ext]',
- },
- },
],
},
plugins: [
From 8acc7aab4c254c4819f45e512b86cf5a4255091f Mon Sep 17 00:00:00 2001
From: Lunny Xiao
Date: Fri, 29 Mar 2024 11:38:16 +0800
Subject: [PATCH 0004/1946] Refactor topic Find functions and add more tests
for pagination (#30127)
This also fixed #22238
---
models/repo/topic.go | 31 ++++++++++--------------
models/repo/topic_test.go | 14 +++++------
routers/api/v1/repo/topic.go | 7 +++---
routers/web/explore/topic.go | 2 +-
routers/web/repo/view.go | 2 +-
tests/integration/api_repo_topic_test.go | 22 ++++++++++++++++-
tests/integration/repo_topic_test.go | 24 +++++++++++++++++-
7 files changed, 70 insertions(+), 32 deletions(-)
diff --git a/models/repo/topic.go b/models/repo/topic.go
index 79b13e320d..430a60f603 100644
--- a/models/repo/topic.go
+++ b/models/repo/topic.go
@@ -178,7 +178,7 @@ type FindTopicOptions struct {
Keyword string
}
-func (opts *FindTopicOptions) toConds() builder.Cond {
+func (opts *FindTopicOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.RepoID > 0 {
cond = cond.And(builder.Eq{"repo_topic.repo_id": opts.RepoID})
@@ -191,29 +191,24 @@ func (opts *FindTopicOptions) toConds() builder.Cond {
return cond
}
-// FindTopics retrieves the topics via FindTopicOptions
-func FindTopics(ctx context.Context, opts *FindTopicOptions) ([]*Topic, int64, error) {
- sess := db.GetEngine(ctx).Select("topic.*").Where(opts.toConds())
+func (opts *FindTopicOptions) ToOrders() string {
orderBy := "topic.repo_count DESC"
if opts.RepoID > 0 {
- sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result
}
- if opts.PageSize != 0 && opts.Page != 0 {
- sess = db.SetSessionPagination(sess, opts)
- }
- topics := make([]*Topic, 0, 10)
- total, err := sess.OrderBy(orderBy).FindAndCount(&topics)
- return topics, total, err
+ return orderBy
}
-// CountTopics counts the number of topics matching the FindTopicOptions
-func CountTopics(ctx context.Context, opts *FindTopicOptions) (int64, error) {
- sess := db.GetEngine(ctx).Where(opts.toConds())
- if opts.RepoID > 0 {
- sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
+func (opts *FindTopicOptions) ToJoins() []db.JoinFunc {
+ if opts.RepoID <= 0 {
+ return nil
+ }
+ return []db.JoinFunc{
+ func(e db.Engine) error {
+ e.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
+ return nil
+ },
}
- return sess.Count(new(Topic))
}
// GetRepoTopicByName retrieves topic from name for a repo if it exist
@@ -283,7 +278,7 @@ func DeleteTopic(ctx context.Context, repoID int64, topicName string) (*Topic, e
// SaveTopics save topics to a repository
func SaveTopics(ctx context.Context, repoID int64, topicNames ...string) error {
- topics, _, err := FindTopics(ctx, &FindTopicOptions{
+ topics, err := db.Find[Topic](ctx, &FindTopicOptions{
RepoID: repoID,
})
if err != nil {
diff --git a/models/repo/topic_test.go b/models/repo/topic_test.go
index 2b609e6d66..1600896b6e 100644
--- a/models/repo/topic_test.go
+++ b/models/repo/topic_test.go
@@ -19,18 +19,18 @@ func TestAddTopic(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- topics, _, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
+ topics, err := db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
- topics, total, err := repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+ topics, total, err := db.FindAndCount[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
ListOptions: db.ListOptions{Page: 1, PageSize: 2},
})
assert.NoError(t, err)
assert.Len(t, topics, 2)
assert.EqualValues(t, 6, total)
- topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+ topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
RepoID: 1,
})
assert.NoError(t, err)
@@ -38,11 +38,11 @@ func TestAddTopic(t *testing.T) {
assert.NoError(t, repo_model.SaveTopics(db.DefaultContext, 2, "golang"))
repo2NrOfTopics := 1
- topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
+ topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
- topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+ topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
RepoID: 2,
})
assert.NoError(t, err)
@@ -55,11 +55,11 @@ func TestAddTopic(t *testing.T) {
assert.NoError(t, err)
assert.EqualValues(t, 1, topic.RepoCount)
- topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{})
+ topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{})
assert.NoError(t, err)
assert.Len(t, topics, totalNrOfTopics)
- topics, _, err = repo_model.FindTopics(db.DefaultContext, &repo_model.FindTopicOptions{
+ topics, err = db.Find[repo_model.Topic](db.DefaultContext, &repo_model.FindTopicOptions{
RepoID: 2,
})
assert.NoError(t, err)
diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go
index 1d8e675bde..9852caa989 100644
--- a/routers/api/v1/repo/topic.go
+++ b/routers/api/v1/repo/topic.go
@@ -7,6 +7,7 @@ import (
"net/http"
"strings"
+ "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
@@ -53,7 +54,7 @@ func ListTopics(ctx *context.APIContext) {
RepoID: ctx.Repo.Repository.ID,
}
- topics, total, err := repo_model.FindTopics(ctx, opts)
+ topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
@@ -172,7 +173,7 @@ func AddTopic(ctx *context.APIContext) {
}
// Prevent adding more topics than allowed to repo
- count, err := repo_model.CountTopics(ctx, &repo_model.FindTopicOptions{
+ count, err := db.Count[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
@@ -287,7 +288,7 @@ func TopicSearch(ctx *context.APIContext) {
ListOptions: utils.GetListOptions(ctx),
}
- topics, total, err := repo_model.FindTopics(ctx, opts)
+ topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
diff --git a/routers/web/explore/topic.go b/routers/web/explore/topic.go
index 95fecfe2b8..b4507ba28d 100644
--- a/routers/web/explore/topic.go
+++ b/routers/web/explore/topic.go
@@ -23,7 +23,7 @@ func TopicSearch(ctx *context.Context) {
},
}
- topics, total, err := repo_model.FindTopics(ctx, opts)
+ topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
ctx.Error(http.StatusInternalServerError)
return
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 73a7be4e89..93e0f5bcbd 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -899,7 +899,7 @@ func renderLanguageStats(ctx *context.Context) {
}
func renderRepoTopics(ctx *context.Context) {
- topics, _, err := repo_model.FindTopics(ctx, &repo_model.FindTopicOptions{
+ topics, err := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
diff --git a/tests/integration/api_repo_topic_test.go b/tests/integration/api_repo_topic_test.go
index c41bc4abb6..a10e159b78 100644
--- a/tests/integration/api_repo_topic_test.go
+++ b/tests/integration/api_repo_topic_test.go
@@ -26,14 +26,34 @@ func TestAPITopicSearch(t *testing.T) {
TopicNames []*api.TopicResponse `json:"topics"`
}
+ // search all topics
+ res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.Len(t, topics.TopicNames, 6)
+ assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+ // pagination search topics first page
+ topics.TopicNames = nil
query := url.Values{"page": []string{"1"}, "limit": []string{"4"}}
searchURL.RawQuery = query.Encode()
- res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Len(t, topics.TopicNames, 4)
assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+ // pagination search topics second page
+ topics.TopicNames = nil
+ query = url.Values{"page": []string{"2"}, "limit": []string{"4"}}
+
+ searchURL.RawQuery = query.Encode()
+ res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.Len(t, topics.TopicNames, 2)
+ assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+ // add keyword search
+ query = url.Values{"page": []string{"1"}, "limit": []string{"4"}}
query.Add("q", "topic")
searchURL.RawQuery = query.Encode()
res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
diff --git a/tests/integration/repo_topic_test.go b/tests/integration/repo_topic_test.go
index 58fee8418f..f198397007 100644
--- a/tests/integration/repo_topic_test.go
+++ b/tests/integration/repo_topic_test.go
@@ -21,20 +21,42 @@ func TestTopicSearch(t *testing.T) {
TopicNames []*api.TopicResponse `json:"topics"`
}
+ // search all topics
+ res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.Len(t, topics.TopicNames, 6)
+ assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+ // pagination search topics
+ topics.TopicNames = nil
query := url.Values{"page": []string{"1"}, "limit": []string{"4"}}
searchURL.RawQuery = query.Encode()
- res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Len(t, topics.TopicNames, 4)
assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+ // second page
+ topics.TopicNames = nil
+ query = url.Values{"page": []string{"2"}, "limit": []string{"4"}}
+
+ searchURL.RawQuery = query.Encode()
+ res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
+ DecodeJSON(t, res, &topics)
+ assert.Len(t, topics.TopicNames, 2)
+ assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
+
+ // add keyword search
+ topics.TopicNames = nil
+ query = url.Values{"page": []string{"1"}, "limit": []string{"4"}}
query.Add("q", "topic")
searchURL.RawQuery = query.Encode()
res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
DecodeJSON(t, res, &topics)
assert.Len(t, topics.TopicNames, 2)
+ topics.TopicNames = nil
query.Set("q", "database")
searchURL.RawQuery = query.Encode()
res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
From 8fd15990c5c8980caf2b9ffefc0b3427efacdc04 Mon Sep 17 00:00:00 2001
From: silverwind
Date: Fri, 29 Mar 2024 05:56:01 +0100
Subject: [PATCH 0005/1946] Remove fomantic checkbox module (#30162)
CSS is pretty slim already and the `.ui.toggle.checkbox` sliders on
admin page also still work. The only necessary JS is the one that links
`input` and `label` so that it can be toggled via label. All checkboxes
except the markdown ones render at `--checkbox-size: 16px` now.
---------
Co-authored-by: delvh
---
templates/admin/config_settings.tmpl | 4 +-
.../repo/issue/view_content/sidebar.tmpl | 2 +-
web_src/css/base.css | 1 +
web_src/css/form.css | 59 +-
web_src/css/index.css | 1 +
web_src/css/modules/animations.css | 1 -
web_src/css/modules/checkbox.css | 120 +++
web_src/css/org.css | 4 -
web_src/css/repo/issue-list.css | 1 +
web_src/fomantic/build/semantic.css | 709 --------------
web_src/fomantic/build/semantic.js | 877 ------------------
web_src/fomantic/semantic.json | 1 -
web_src/js/features/admin/common.js | 23 +-
web_src/js/features/common-global.js | 2 -
web_src/js/features/repo-issue.js | 42 +-
web_src/js/modules/fomantic.js | 2 -
web_src/js/modules/fomantic/aria.md | 17 +-
web_src/js/modules/fomantic/checkbox.js | 43 +-
18 files changed, 180 insertions(+), 1729 deletions(-)
create mode 100644 web_src/css/modules/checkbox.css
diff --git a/templates/admin/config_settings.tmpl b/templates/admin/config_settings.tmpl
index d7fb022274..02ab5fd0fb 100644
--- a/templates/admin/config_settings.tmpl
+++ b/templates/admin/config_settings.tmpl
@@ -7,14 +7,14 @@
{{ctx.Locale.Tr "admin.config.disable_gravatar"}}
-
+
{{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}
-
+
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 5913916ae4..c917c78e68 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -677,7 +677,7 @@
{{if and (not (eq .Issue.PullRequest.HeadRepo.FullName .Issue.PullRequest.BaseRepo.FullName)) .CanWriteToHeadRepo}}
- label,
.ui.form .inline.fields .field > p,
.ui.form .inline.field > label,
-.ui.form .inline.field > p,
-.ui.checkbox label,
-.ui.checkbox + label,
-.ui.checkbox label:hover,
-.ui.checkbox + label:hover,
-.ui.checkbox input:focus ~ label,
-.ui.checkbox input:active ~ label {
+.ui.form .inline.field > p {
color: var(--color-text);
}
.ui.form .required.fields:not(.grouped) > .field > label::after,
.ui.form .required.fields.grouped > label::after,
.ui.form .required.field > label::after,
-.ui.form .required.fields:not(.grouped) > .field > .checkbox::after,
-.ui.form .required.field > .checkbox::after,
.ui.form label.required::after {
color: var(--color-red);
}
-.ui.input,
-.ui.checkbox input:focus ~ label::after,
-.ui.checkbox input:checked ~ label::after,
-.ui.checkbox label:active::after,
-.ui.checkbox input:not([type="radio"]):indeterminate ~ label::after,
-.ui.checkbox input:not([type="radio"]):indeterminate:focus ~ label::after,
-.ui.checkbox input:checked:focus ~ label::after,
-.ui.disabled.checkbox label,
-.ui.checkbox input[disabled] ~ label {
+.ui.input {
color: var(--color-input-text);
}
-.ui.radio.checkbox input:focus ~ label::after,
-.ui.radio.checkbox input:checked ~ label::after,
-.ui.radio.checkbox input:focus:checked ~ label::after {
- background: var(--color-input-text);
-}
-
-.ui.toggle.checkbox label::before {
- background: var(--color-input-toggle-background);
-}
-
-.ui.toggle.checkbox label,
-.ui.toggle.checkbox input:checked ~ label,
-.ui.toggle.checkbox input:focus:checked ~ label {
- color: var(--color-text) !important;
-}
-
-.ui.toggle.checkbox input:checked ~ label::before,
-.ui.toggle.checkbox input:focus:checked ~ label::before {
- background: var(--color-primary) !important;
-}
-
/* match
{{end}}
|