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

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

302 lines
7.7 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.
package devcontainer
import (
"archive/tar"
"context"
"fmt"
"io"
"net"
"net/url"
"regexp"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
gitea_context "code.gitea.io/gitea/services/context"
"github.com/google/uuid"
)
func IsAdmin(ctx context.Context, doer *user.User, repoID int64) (bool, error) {
if doer.IsAdmin {
return true, nil
}
e := db.GetEngine(ctx)
teamMember, err := e.Table("team_user").
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode = ? ",
repoID, perm.AccessModeAdmin).
And("team_user.uid = ?", doer.ID).Exist()
if err != nil {
return false, nil
}
if teamMember {
return true, nil
}
return e.Get(&repo.Collaboration{RepoID: repoID, UserID: doer.ID, Mode: 3})
}
func GetFileContentByPath(ctx context.Context, repo *repo.Repository, path string) (string, error) {
var err error
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return "", err
}
// 1. 获取默认分支名
branchName := repo.DefaultBranch
if len(branchName) == 0 {
branchName = cfg.Section("repository").Key("DEFAULT_BRANCH").Value()
}
// 2. 打开默认分支
gitRepoEntity, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
return "", err
}
defer func(gitRepoEntity *git.Repository) {
_ = gitRepoEntity.Close()
}(gitRepoEntity)
// 3. 获取分支名称
commit, err := gitRepoEntity.GetBranchCommit(branchName)
if err != nil {
return "", err
}
entry, err := commit.GetTreeEntryByPath(path)
if err != nil {
return "", err
}
// No way to edit a directory online.
if entry.IsDir() {
return "", fmt.Errorf("%s entry.IsDir", path)
}
blob := entry.Blob()
if blob.Size() >= setting.UI.MaxDisplayFileSize {
return "", fmt.Errorf("%s blob.Size overflow", path)
}
dataRc, err := blob.DataAsync()
if err != nil {
return "", err
}
defer dataRc.Close()
buf := make([]byte, 1024)
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
// Only some file types are editable online as text.
if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
return "", fmt.Errorf("typesniffer.IsRepresentableAsText")
}
d, _ := io.ReadAll(dataRc)
buf = append(buf, d...)
if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
log.Error("ToUTF8: %v", err)
return string(buf), nil
} else {
return content, nil
}
}
// FileExists returns true if a file exists in the given repo branch
func FileExists(path string, repo *gitea_context.Repository) (bool, error) {
var branch = repo.BranchName
if branch == "" {
branch = repo.Repository.DefaultBranch
}
commit, err := repo.GitRepo.GetBranchCommit(branch)
if err != nil {
return false, err
}
if _, err := commit.GetTreeEntryByPath(path); err != nil {
return false, err
}
return true, nil
}
func ParseImageName(imageName string) (registry, namespace, repo, tag string) {
// 分离仓库地址和命名空间
parts := strings.Split(imageName, "/")
if len(parts) == 3 {
registry = parts[0]
namespace = parts[1]
repo = parts[2]
} else if len(parts) == 2 {
registry = parts[0]
repo = parts[1]
} else {
repo = imageName
}
// 分离标签
parts = strings.Split(repo, ":")
if len(parts) > 1 {
tag = parts[1]
repo = parts[0]
} else {
tag = "latest"
}
if registry == "" {
registry = "docker.io"
}
return registry, namespace, repo, tag
}
func getSanitizedDevcontainerName(username, repoName string) string {
regexpNonAlphaNum := regexp.MustCompile(`[^a-zA-Z0-9]`)
sanitizedUsername := regexpNonAlphaNum.ReplaceAllString(username, "")
sanitizedRepoName := regexpNonAlphaNum.ReplaceAllString(repoName, "")
if len(sanitizedUsername) > 15 {
sanitizedUsername = strings.ToLower(sanitizedUsername[:15])
}
if len(sanitizedRepoName) > 31 {
sanitizedRepoName = strings.ToLower(sanitizedRepoName[:31])
}
newUUID, _ := uuid.NewUUID()
uuidStr := newUUID.String()
uuidStr = regexpNonAlphaNum.ReplaceAllString(uuidStr, "")[:15]
return fmt.Sprintf("%s-%s-%s", sanitizedUsername, sanitizedRepoName, uuidStr)
}
func ReplacePortOfUrl(originalURL, targetPort string) (string, error) {
// 解析原始 URL
parsedURL, _ := url.Parse(originalURL)
// 获取主机名和端口号
host, _, err := net.SplitHostPort(parsedURL.Host)
if err != nil {
// 如果没有端口号,则 SplitHostPort 会返回错误
// 这种情况下Host 就是主机名
host = parsedURL.Host
}
// 重新组装 Host 和端口
parsedURL.Host = net.JoinHostPort(host, targetPort)
// 生成新的 URL 字符串
newURL := parsedURL.String()
return newURL, nil
}
func GetPortFromURL(urlStr string) (string, error) {
parsedURL, err := url.Parse(urlStr)
if err != nil {
return "", fmt.Errorf("解析URL失败: %v", err)
}
// 获取主机名和端口号
_, port, err := net.SplitHostPort(parsedURL.Host)
if err != nil {
// 如果SplitHostPort失败说明URL中没有明确指定端口
// 需要根据协议判断默认端口
switch parsedURL.Scheme {
case "http":
return "80", nil
case "https":
return "443", nil
default:
return "", fmt.Errorf("未知协议: %s", parsedURL.Scheme)
}
}
// 如果端口存在,直接返回
return port, nil
}
// addFileToTar 将文件添加到 tar 归档
func AddFileToTar(tw *tar.Writer, filename string, content string, mode int64) error {
hdr := &tar.Header{
Name: filename,
Mode: mode,
Size: int64(len(content)),
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err := tw.Write([]byte(content)); err != nil {
return err
}
return nil
}
func buildDependencyGraph(variables map[string]string) map[string][]string {
graph := make(map[string][]string)
varRefRegex := regexp.MustCompile(`\$[a-zA-Z_][a-zA-Z0-9_]*\b`)
for varName, varValue := range variables {
graph[varName] = []string{}
matches := varRefRegex.FindAllString(varValue, -1)
for _, match := range matches {
refVarName := strings.TrimPrefix(match, "$")
if _, exists := variables[refVarName]; exists {
graph[varName] = append(graph[varName], refVarName)
}
}
}
return graph
}
func dfsDetectCycle(node string, graph map[string][]string, visited, inStack map[string]bool, path *[]string) bool {
visited[node] = true
inStack[node] = true
*path = append(*path, node)
for _, neighbor := range graph[node] {
if !visited[neighbor] {
if dfsDetectCycle(neighbor, graph, visited, inStack, path) {
return true
}
} else if inStack[neighbor] {
// Found cycle, complete the cycle path
*path = append(*path, neighbor)
return true
}
}
inStack[node] = false
*path = (*path)[:len(*path)-1]
return false
}
func checkEachVariable(variables map[string]string) map[string]bool {
results := make(map[string]bool)
graph := buildDependencyGraph(variables)
for varName := range variables {
visited := make(map[string]bool)
inStack := make(map[string]bool)
var cyclePath []string
hasCycle := dfsDetectCycle(varName, graph, visited, inStack, &cyclePath)
results[varName] = hasCycle
if hasCycle {
fmt.Printf("变量 %s 存在循环引用: %v\n", varName, cyclePath)
}
}
return results
}
func ContainsAnySubstring(s string, substrList []string) bool {
for _, substr := range substrList {
hasSubstr, _ := regexp.MatchString(`\$`+substr+`\b`, s)
if hasSubstr {
return true
}
}
return false
}