package devcontainer import ( "bytes" "context" "encoding/json" "fmt" "io" "regexp" "strconv" "code.gitea.io/gitea/models/db" devcontainer_model "code.gitea.io/gitea/models/devcontainer" "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" files_service "code.gitea.io/gitea/services/repository/files" "github.com/docker/go-connections/nat" ) type DevStarJSON struct { ForwardPorts nat.PortSet ContainerEnv []string Image string DockerfilePath string InitializeCommand []string PostCreateCommand []string RunArgs []string } func CreateDevcontainerJSON(ctx *gitea_context.Context, repo *repo.Repository, doer *user.User) { jsonString := `{ "image":"mcr.microsoft.com/devcontainers/base:dev-ubuntu-20.04", "forwardPorts": [ { "containerPort": 8080, "protocol": "tcp" } ], "containerEnv": { "NODE_ENV": "development" }, "initializeCommand": "echo \"init\";", "postCreateCommand": [ "echo \"created\";", "echo \"test\";" ], "runArgs": [ "-p", "8888:8888" ] }` _, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, doer, &files_service.ChangeRepoFilesOptions{ Files: []*files_service.ChangeRepoFile{ { Operation: "create", TreePath: ".devcontainer/devcontainer.json", ContentReader: bytes.NewReader([]byte(jsonString)), }, }, OldBranch: "main", NewBranch: "main", Message: "add container configuration", }) if err != nil { log.Info("error ChangeRepoFiles:", err) ctx.JSON(500, map[string]string{ "message": "error ChangeRepoFiles"}) } } func UpdateDevcontainerJSON(ctx *gitea_context.Context, jsonString string) error { // 更新devcontainer.json配置文件 _, err := files_service.ChangeRepoFiles(db.DefaultContext, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ Files: []*files_service.ChangeRepoFile{ { Operation: "update", TreePath: ".devcontainer/devcontainer.json", ContentReader: bytes.NewReader([]byte(jsonString)), }, }, OldBranch: ctx.Repo.Repository.DefaultBranch, Message: "Update container", }) if err != nil { return fmt.Errorf("更新devcontainer.json配置文件失败 %v", err) } return nil } // GetImageFromDevcontainerJSON 从 .devcontainer/devcontainer.json 中获取 devContainer image func GetDevcontainerJsonModel(ctx context.Context, repo *repo.Repository) (*DevStarJSON, error) { devcontainerJSONContent, err := GetDevcontainerJsonString(ctx, repo) var devContainerJson *devcontainer_model.DevContainerJSON if err != nil { return nil, err } // 1. 移除注释 cleanedContent, err := removeComments(devcontainerJSONContent) if err != nil { log.Error("Failed to remove comments from .devcontainer/devcontainer.json: %v", err) return nil, err } // 2. 解析 JSON devContainerJson, err = devcontainer_model.Unmarshal(cleanedContent) if err != nil { log.Error("Failed to unmarshal .devcontainer/devcontainer.json: %v", err) return nil, err } devStarJson := &DevStarJSON{ Image: devContainerJson.Image, ContainerEnv: ConvertContainerEnv(devContainerJson.ContainerEnv), } devStarJson.ForwardPorts, _ = ConvertForwardPorts(devContainerJson.ForwardPorts) devStarJson.InitializeCommand, _ = parseCommand(devContainerJson.InitializeCommand) c1, _ := parseCommand(devContainerJson.OnCreateCommand) c2, _ := parseCommand(devContainerJson.UpdateContentCommand) c3, _ := parseCommand(devContainerJson.PostCreateCommand) c4, _ := parseCommand(devContainerJson.PostStartCommand) c5, _ := parseCommand(devContainerJson.PostAttachCommand) devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c1...) devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c2...) devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c3...) devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c4...) devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c5...) devStarJson.RunArgs = devContainerJson.RunArgs if devContainerJson.Build != nil { devStarJson.DockerfilePath = devContainerJson.Build.Dockerfile } return devStarJson, nil } func GetFileContentByPath(ctx context.Context, repo *repo.Repository, path string) (string, error) { // 1. 获取默认分支名 branchName := repo.DefaultBranch if len(branchName) == 0 { branchName = setting.Devcontainer.DefaultGitBranchName } // 2. 打开默认分支 gitRepoEntity, err := git.OpenRepository(ctx, repo.RepoPath()) if err != nil { log.Error("Failed to open repository %s: %v", repo.RepoPath(), err) 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 { log.Info("Repo.Commit.GetTreeEntryByPath %v", err.Error()) return "", err } // No way to edit a directory online. if entry.IsDir() { return "", fmt.Errorf(path + " entry.IsDir") } blob := entry.Blob() if blob.Size() >= setting.UI.MaxDisplayFileSize { return "", fmt.Errorf(path + " blob.Size overflow") } 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 } } func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (string, error) { return GetFileContentByPath(ctx, repo, ".devcontainer/devcontainer.json") } func GetDockerfileContent(ctx context.Context, repo *repo.Repository) (string, error) { dockerfilePath, err := GetDockerfilePath(ctx, repo) if err != nil { return "", err } return GetFileContentByPath(ctx, repo, ".devcontainer/"+dockerfilePath) } func GetDockerfilePath(ctx context.Context, repo *repo.Repository) (string, error) { devcontainerJSONContent, err := GetDevcontainerJsonString(ctx, repo) var devContainerJson *devcontainer_model.DevContainerJSON if err != nil { return "", err } // 1. 移除注释 cleanedContent, err := removeComments(devcontainerJSONContent) if err != nil { log.Error("Failed to remove comments from .devcontainer/devcontainer.json: %v", err) return "", err } // 2. 解析 JSON devContainerJson, err = devcontainer_model.Unmarshal(cleanedContent) if err != nil { log.Error("Failed to unmarshal .devcontainer/devcontainer.json: %v", err) return "", err } if devContainerJson.Build == nil || devContainerJson.Build.Dockerfile == "" { return "", fmt.Errorf("devcontainer.json error") } log.Info("%vsdadasdsa", devContainerJson.Build.Dockerfile) return devContainerJson.Build.Dockerfile, nil } // 移除 JSON 文件中的注释 func removeComments(data string) (string, error) { // 移除单行注释 // ... re := regexp.MustCompile(`//.*`) data = re.ReplaceAllString(data, "") // 移除多行注释 /* ... */ re = regexp.MustCompile(`/\*.*?\*/`) data = re.ReplaceAllString(data, "") return data, nil } func ConvertContainerEnv(envMap map[string]string) []string { var envSlice []string for key, value := range envMap { envSlice = append(envSlice, fmt.Sprintf("%s=%s", key, value)) } return envSlice } func parseCommand(cmd interface{}) ([]string, error) { var result []string switch v := cmd.(type) { case string: // 直接处理字符串类型 result = []string{v} case []interface{}: // 处理数组类型(如 ["echo","hello"]) var parts []string for _, item := range v { // 处理数组元素可能是多种类型的情况 switch elem := item.(type) { case string: parts = append(parts, elem) case int, float64: // 处理数字自动转换的情况 parts = append(parts, fmt.Sprintf("%v", elem)) default: return []string{}, fmt.Errorf("unsupported array element type: %T", elem) } } result = parts case map[string]interface{}: // 处理对象类型(如 {"command":"echo","args":["hello"]}) // 方式1:序列化成JSON字符串 jsonBytes, err := json.Marshal(v) if err != nil { return []string{}, fmt.Errorf("failed to marshal object: %v", err) } result = []string{string(jsonBytes)} // 方式2:或拼接成 key=value 格式(根据需求选择) // var pairs []string // for key, val := range v { // pairs = append(pairs, fmt.Sprintf("%s=%v", key, val)) // } // result = strings.Join(pairs, " ") default: return []string{}, fmt.Errorf("unsupported command type: %T", v) } return result, nil } // 转换入口函数 func ConvertForwardPorts(input interface{}) (nat.PortSet, error) { portSet := make(nat.PortSet) // 类型检查确保是切片 rawPorts, ok := input.([]interface{}) if !ok { return nil, fmt.Errorf("forwardPorts must be an array") } // 遍历每个端口定义 for _, rawPort := range rawPorts { port, proto, err := parsePortDefinition(rawPort) if err != nil { return nil, err } // 构建 nat.Port 并加入集合 natPort := nat.Port(fmt.Sprintf("%s/%s", port, proto)) portSet[natPort] = struct{}{} } return portSet, nil } // 处理三种可能的端口定义形式: // 1. 数字: 3000 // 2. 字符串: "3000" 或 "3000/udp" // 3. 对象: {"containerPort": 3000, "protocol": "tcp"} func parsePortDefinition(raw interface{}) (string, string, error) { switch v := raw.(type) { case float64: // JSON 数字默认解析为 float64 return validatePort(strconv.Itoa(int(v))), "tcp", nil case string: return parsePortString(v) case map[string]interface{}: return parsePortObject(v) default: return "", "", fmt.Errorf("invalid port type: %T", raw) } } // 处理字符串形式(支持带协议) func parsePortString(s string) (string, string, error) { proto, port := nat.SplitProtoPort(s) if port == "" { return "", "", fmt.Errorf("invalid port string: %s", s) } return validatePort(port), proto, nil } // 处理对象形式 func parsePortObject(obj map[string]interface{}) (string, string, error) { // 提取 containerPort rawPort, exists := obj["containerPort"] if !exists { return "", "", fmt.Errorf("missing containerPort field") } // 转换端口值 var portStr string switch p := rawPort.(type) { case float64: portStr = strconv.Itoa(int(p)) case string: portStr = p default: return "", "", fmt.Errorf("invalid containerPort type: %T", rawPort) } // 验证端口格式 portStr = validatePort(portStr) // 获取协议(默认为 tcp) proto := "tcp" if rawProto, exists := obj["protocol"]; exists { if protoStr, ok := rawProto.(string); ok { proto = protoStr } } return portStr, proto, nil } // 验证端口格式有效性 func validatePort(port string) string { // 这里可以添加更严格的验证逻辑 // 示例仅处理基本数字格式 if _, err := strconv.Atoi(port); err == nil { return port } panic(fmt.Sprintf("invalid port format: %s", port)) }