Files
devstar/routers/web/devcontainer/variables.go
xinitx 9071a754f4 !108 给devcontainer增加变量和脚本功能
给devcontainer增加变量和脚本功能

- 能从devstar.cn上获取预定义的DEVSTAR_开头的变量或脚本
- 添加到脚本管理中的变量名,在devcontainer启动时会自动执行,然后才执行devcontainer.json中用户自定义脚本,其中可以调用设置的变量或脚本
- 变量或脚本在用户设置、项目设置和后台管理中都可以添加,如有重名优先级为:用户设置 > 项目设置 > 后台管理
2025-10-18 08:53:50 +00:00

426 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package devcontainer
import (
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"code.gitea.io/gitea/models/db"
devcontainer_model "code.gitea.io/gitea/models/devcontainer"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
devcontainer_service "code.gitea.io/gitea/services/devcontainer"
"code.gitea.io/gitea/services/forms"
)
const (
tplRepoVariables templates.TplName = "repo/settings/devcontainer"
tplOrgVariables templates.TplName = "org/settings/devcontainer"
tplUserVariables templates.TplName = "user/settings/devcontainer"
tplAdminVariables templates.TplName = "admin/devcontainer"
)
type variablesCtx struct {
OwnerID int64
RepoID int64
IsRepo bool
IsOrg bool
IsUser bool
IsGlobal bool
VariablesTemplate templates.TplName
RedirectLink string
}
func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
if ctx.Data["PageIsRepoSettings"] == true {
return &variablesCtx{
OwnerID: 0,
RepoID: ctx.Repo.Repository.ID,
IsRepo: true,
VariablesTemplate: tplRepoVariables,
RedirectLink: ctx.Repo.RepoLink + "/settings/devcontainer/variables",
}, nil
}
if ctx.Data["PageIsOrgSettings"] == true {
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("RenderUserOrgHeader", err)
return nil, nil
}
return &variablesCtx{
OwnerID: ctx.ContextUser.ID,
RepoID: 0,
IsOrg: true,
VariablesTemplate: tplOrgVariables,
RedirectLink: ctx.Org.OrgLink + "/settings/devcontainer/variables",
}, nil
}
if ctx.Data["PageIsUserSettings"] == true {
return &variablesCtx{
OwnerID: ctx.Doer.ID,
RepoID: 0,
IsUser: true,
VariablesTemplate: tplUserVariables,
RedirectLink: setting.AppSubURL + "/user/settings/devcontainer/variables",
}, nil
}
if ctx.Data["PageIsAdmin"] == true {
return &variablesCtx{
OwnerID: 0,
RepoID: 0,
IsGlobal: true,
VariablesTemplate: tplAdminVariables,
RedirectLink: setting.AppSubURL + "/-/admin/devcontainer/variables",
}, nil
}
return nil, errors.New("unable to set Variables context")
}
func Variables(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("devcontainer.variables")
ctx.Data["PageType"] = "variables"
ctx.Data["PageIsSharedSettingsDevcontainerVariables"] = true
vCtx, err := getVariablesCtx(ctx)
if err != nil {
ctx.ServerError("getVariablesCtx", err)
return
}
variables, err := db.Find[devcontainer_model.DevcontainerVariable](ctx, devcontainer_model.FindVariablesOpts{
OwnerID: vCtx.OwnerID,
RepoID: vCtx.RepoID,
})
if err != nil {
ctx.ServerError("FindVariables", err)
return
}
var tags []string
// 使用JOIN查询关联DevcontainerScript表和devcontainer_variable表
err = db.GetEngine(ctx).
Select("variable_name").
Table("devcontainer_script").
Where("user_id = ? AND repo_id = ?", vCtx.OwnerID, vCtx.RepoID).
Find(&tags)
// 将tags转换为JSON格式的字符串
tagsJSON, err := json.Marshal(tags)
if err != nil {
ctx.ServerError("Marshal tags", err)
return
}
// 确保tagsJSON不为null
tagsJSONStr := string(tagsJSON)
if tagsJSONStr == "null" {
tagsJSONStr = "[]"
}
// 创建一个新的请求
req, err := http.NewRequest("GET", "http://devstar.cn/variables/export", nil)
if err != nil {
ctx.Data["DevstarVariables"] = []*devcontainer_model.DevcontainerVariable{}
} else {
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
ctx.Data["DevstarVariables"] = []*devcontainer_model.DevcontainerVariable{}
} else {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
ctx.Data["DevstarVariables"] = []*devcontainer_model.DevcontainerVariable{}
} else {
var devstarVariables []*devcontainer_model.DevcontainerVariable
err = json.Unmarshal(body, &devstarVariables)
if err != nil {
ctx.Data["DevstarVariables"] = []*devcontainer_model.DevcontainerVariable{}
} else {
// 创建一个本地变量名称的映射,用于快速查找
localVariableNames := make(map[string]bool)
for _, variable := range variables {
localVariableNames[variable.Name] = true
}
// 筛选出不与本地变量同名的devstar变量
var filteredDevstarVariables []*devcontainer_model.DevcontainerVariable
for _, devstarVar := range devstarVariables {
if !localVariableNames[devstarVar.Name] {
filteredDevstarVariables = append(filteredDevstarVariables, devstarVar)
}
}
ctx.Data["DevstarVariables"] = filteredDevstarVariables
}
}
}
}
ctx.Data["Variables"] = variables
ctx.Data["Tags"] = tagsJSONStr
ctx.Data["DescriptionMaxLength"] = devcontainer_model.VariableDescriptionMaxLength
ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
}
func GetExportVariables(ctx *context.Context) {
globalVariables, err := db.Find[devcontainer_model.DevcontainerVariable](ctx, devcontainer_model.FindVariablesOpts{})
if err != nil {
ctx.ServerError("Get Global Variables", err)
return
}
// 筛选出键以"DEVSTAR_"开头的脚本
var devstarVariables []devcontainer_model.DevcontainerVariable
for _, value := range globalVariables {
if strings.HasPrefix(value.Name, "DEVSTAR_") {
devstarVariables = append(devstarVariables, *value)
}
}
ctx.JSON(http.StatusOK, devstarVariables)
}
func VariableCreate(ctx *context.Context) {
vCtx, err := getVariablesCtx(ctx)
if err != nil {
ctx.ServerError("getVariablesCtx", err)
return
}
if ctx.HasError() { // form binding validation error
ctx.JSONError(ctx.GetErrMsg())
return
}
form := web.GetForm(ctx).(*forms.EditVariableForm)
v, err := devcontainer_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data, form.Description)
if err != nil {
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(vCtx.RedirectLink)
}
func ScriptCreate(ctx *context.Context) {
vCtx, err := getVariablesCtx(ctx)
if err != nil {
ctx.ServerError("getVariablesCtx", err)
return
}
if ctx.HasError() { // form binding validation error
ctx.JSONError(ctx.GetErrMsg())
return
}
query := ctx.Req.URL.Query()
var script *devcontainer_model.DevcontainerScript
// 首先检查变量是否在DevcontainerVariable表中存在
exists, err := db.GetEngine(ctx).
Table("devcontainer_variable").
Where("(owner_id = 0 AND repo_id = 0) OR (owner_id = ? AND repo_id = 0) OR (owner_id = 0 AND repo_id = ?)", vCtx.OwnerID, vCtx.RepoID).
And("name = ?", strings.ToUpper(query.Get("name"))).
Exist()
if err != nil {
log.Error("Check variable existence: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}
if !exists {
// 创建一个新的请求来获取devstar变量
req, err := http.NewRequest("GET", "http://devstar.cn/variables/export", nil)
if err != nil {
log.Error("Failed to create request for devstar variables: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Error("Failed to fetch devstar variables: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error("Failed to read devstar variables response: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}
var devstarVariables []*devcontainer_model.DevcontainerVariable
err = json.Unmarshal(body, &devstarVariables)
if err != nil {
log.Error("Failed to unmarshal devstar variables: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}
// 查找是否有匹配的devstar变量
foundInDevstar := false
searchName := strings.ToUpper(query.Get("name"))
for _, devstarVar := range devstarVariables {
if devstarVar.Name == searchName {
foundInDevstar = true
break
}
}
if !foundInDevstar {
log.Error("Variable %s does not exist", query.Get("name"))
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}
}
// 创建devcontainer_script记录
script = &devcontainer_model.DevcontainerScript{
UserId: vCtx.OwnerID,
RepoId: vCtx.RepoID,
VariableName: strings.ToUpper(query.Get("name")),
}
_, err = db.GetEngine(ctx).Insert(script)
if err != nil {
log.Error("CreateScript: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}
}
func VariableUpdate(ctx *context.Context) {
vCtx, err := getVariablesCtx(ctx)
if err != nil {
ctx.ServerError("getVariablesCtx", err)
return
}
if ctx.HasError() { // form binding validation error
ctx.JSONError(ctx.GetErrMsg())
return
}
id := ctx.PathParamInt64("variable_id")
variable := findActionsVariable(ctx, id, vCtx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*forms.EditVariableForm)
variable.Name = form.Name
variable.Data = form.Data
variable.Description = form.Description
if ok, err := devcontainer_service.UpdateVariableNameData(ctx, variable); err != nil || !ok {
log.Error("UpdateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
return
}
ctx.Flash.Success(ctx.Tr("actions.variables.update.success"))
ctx.JSONRedirect(vCtx.RedirectLink)
}
func findActionsVariable(ctx *context.Context, id int64, vCtx *variablesCtx) *devcontainer_model.DevcontainerVariable {
opts := devcontainer_model.FindVariablesOpts{
IDs: []int64{id},
}
switch {
case vCtx.IsRepo:
opts.RepoID = vCtx.RepoID
if opts.RepoID == 0 {
panic("RepoID is 0")
}
case vCtx.IsOrg, vCtx.IsUser:
opts.OwnerID = vCtx.OwnerID
if opts.OwnerID == 0 {
panic("OwnerID is 0")
}
case vCtx.IsGlobal:
// do nothing
default:
panic("invalid actions variable")
}
got, err := devcontainer_model.FindVariables(ctx, opts)
if err != nil {
ctx.ServerError("FindVariables", err)
return nil
} else if len(got) == 0 {
ctx.NotFound(nil)
return nil
}
return got[0]
}
func VariableDelete(ctx *context.Context) {
vCtx, err := getVariablesCtx(ctx)
if err != nil {
ctx.ServerError("getVariablesCtx", err)
return
}
id := ctx.PathParamInt64("variable_id")
variable := findActionsVariable(ctx, id, vCtx)
if ctx.Written() {
return
}
if err := devcontainer_service.DeleteVariableByID(ctx, variable.ID); err != nil {
log.Error("Delete variable [%d] failed: %v", id, err)
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
return
}
// 删除相应的script记录根据repoId、userId和name
script := &devcontainer_model.DevcontainerScript{
UserId: vCtx.OwnerID,
RepoId: vCtx.RepoID,
VariableName: variable.Name,
}
_, err = db.GetEngine(ctx).Delete(script)
if err != nil {
log.Error("Delete script for variable [%d] failed: %v", id, err)
// 注意:这里我们记录错误但不中断变量删除过程
}
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
ctx.JSONRedirect(vCtx.RedirectLink)
}
func ScriptDelete(ctx *context.Context) {
vCtx, err := getVariablesCtx(ctx)
if err != nil {
ctx.ServerError("getVariablesCtx", err)
return
}
if ctx.HasError() { // form binding validation error
ctx.JSONError(ctx.GetErrMsg())
return
}
query := ctx.Req.URL.Query()
// 删除devcontainer_script记录
script := &devcontainer_model.DevcontainerScript{
UserId: vCtx.OwnerID,
RepoId: vCtx.RepoID,
VariableName: query.Get("name"),
}
_, err = db.GetEngine(ctx).Delete(script)
if err != nil {
log.Error("DeleteScript: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
return
}
}