diff --git a/cli/includes.go b/cli/includes.go index ee588da..6c4e602 100644 --- a/cli/includes.go +++ b/cli/includes.go @@ -17,6 +17,11 @@ type Include struct { // options []Option } +// Insert represent the insert file +type Insert struct { + path string +} + // ErrorIncludeLoop happens in case of an infinite loop between included files var ErrorIncludeLoop = errors.New("include loop") @@ -106,6 +111,7 @@ func printPaths(mergeList []Include, workdir string) { } var regexInclude = regexp.MustCompile(`^[ \t]*#include[ \t]+("(.*?[^\\])"|([^ \t]+))[ \t]*$`) +var regexInsert = regexp.MustCompile(`^[ \t]*#insert[ \t]+("(.*?[^\\])"|([^ \t]+))[ \t]*$`) // parseInclude function parses the includes in a line func parseInclude(line string) (bool, Include) { @@ -139,6 +145,41 @@ func parseInclude(line string) (bool, Include) { } } +// parseInsert function parses the inserts in a line +func parseInsert(line string) (bool, Insert) { + result := regexInsert.FindAllStringSubmatch(line, -1) + + if len(result) == 0 { + return false, Insert{} + } + + if len(result) > 1 { + logErr.Println("Could not parse insert line:", line) + return false, Insert{} + } + + if len(result[0]) < 4 { + logErr.Println("Could not parse insert line:", line) + return false, Insert{} + } + + var path string + + if result[0][2] == "" { + if result[0][3] == "" { + return false, Insert{} + } + path = result[0][3] + } else { + path = result[0][2] + } + + return true, Insert{ + path: path, + } +} + + // parseAllIncludes parses all includes in a file func parseAllIncludes(path string, done map[string]bool) ([]Include, map[string]bool, error) { logDebug.Println("parseAllIncludes(", path, done, ")") @@ -209,6 +250,64 @@ func parseAllIncludes(path string, done map[string]bool) ([]Include, map[string] return result, done, nil } +// getAllInserts collects all insert directives from a file and its includes +func getAllInserts(path string, done map[string]bool) ([]Insert, map[string]bool, error) { + logDebug.Println("getAllInserts(", path, done, ")") + if !fileExists(path) { + logErr.Println(path, "path does not exist") + return []Insert{}, done, errors.New("path does not exist") + } + + if val, ok := done[path]; ok && val { + logErr.Println(path, "is processed more than once") + return []Insert{}, done, ErrorIncludeLoop + } + + done[path] = true + + result := []Insert{} + + file, err := os.Open(path) + if err != nil { + return []Insert{}, done, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := scanner.Text() + + if ok, insert := parseInsert(line); ok { + logDebug.Println("parseInsert(", line, ")") + insert.path, err = resolvePath(rootFlag, insert.path, path) + if err != nil { + return []Insert{}, done, err + } + + // For inserts, we don't recursively parse - just add the insert + result = append(result, insert) + } + + // Also check for includes and recursively get their inserts + if ok, include := parseInclude(line); ok { + include.path, err = resolvePath(rootFlag, include.path, path) + if err != nil { + return []Insert{}, done, err + } + + // Recursively get inserts from included files + innerInserts, innerDone, err := getAllInserts(include.path, done) + done = innerDone + if err != nil { + return []Insert{}, done, err + } + result = append(result, innerInserts...) + } + } + return result, done, nil +} + // resolvePath return the absolute path, with context func resolvePath(root string, includePath string, contextFile string) (string, error) { if includePath[0] == '/' { diff --git a/cli/merge.go b/cli/merge.go index e223015..d72df53 100644 --- a/cli/merge.go +++ b/cli/merge.go @@ -3,17 +3,18 @@ package main import ( "errors" "fmt" - yamljson "github.com/ghodss/yaml" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-openapi/jsonpointer" - "github.com/imdario/mergo" - "github.com/mohae/deepcopy" "os" "os/exec" "path/filepath" "reflect" "strings" "time" + + yamljson "github.com/ghodss/yaml" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-openapi/jsonpointer" + "github.com/imdario/mergo" + "github.com/mohae/deepcopy" ) // initMap initialize a map using a bunch of keys. @@ -441,6 +442,67 @@ func mergeVars(p string, mergeStrategies []MergeStrategy) (map[string]any, []Inc } } + // Handle #insert directives - add their content without merge strategies + inserts, _, err := getAllInserts(p, make(map[string]bool)) + if err != nil { + return map[string]any{}, []Include{}, err + } + + for _, insert := range inserts { + logDebug.Printf("Processing insert: %s", insert.path) + + // Get the merge list for the insert file to process its includes + insertMergeList, err := getMergeList(insert.path) + if err != nil { + return map[string]any{}, []Include{}, err + } + + // Process the insert merge list - merge all files first, then add to final + insertMergedData := make(map[string]any) + for _, insertFile := range insertMergeList { + content, err := os.ReadFile(insertFile.path) + if err != nil { + return map[string]any{}, []Include{}, err + } + + insertData := make(map[string]any) + err = yamljson.Unmarshal(content, &insertData) + if err != nil { + logErr.Println("cannot unmarshal insert data:", insertFile.path) + return map[string]any{}, []Include{}, err + } + + // Merge this file's data into the insert merged data + for k, v := range insertData { + insertMergedData[k] = v + } + } + + // Now add the merged insert data to final, preserving local variables + for k, v := range insertMergedData { + // For __meta__ section, merge fields but preserve existing ones + if k == "__meta__" { + if existingMeta, exists := final[k]; exists { + if existingMetaMap, ok := existingMeta.(map[string]any); ok { + if newMetaMap, ok := v.(map[string]any); ok { + // Only add fields that don't already exist + for metaKey, metaValue := range newMetaMap { + if _, exists := existingMetaMap[metaKey]; !exists { + existingMetaMap[metaKey] = metaValue + } + } + continue + } + } + } + } + // For non-__meta__ fields, only add if they don't exist + if _, exists := final[k]; !exists { + final[k] = v + } + } + } + return final, mergeList, nil } diff --git a/cli/merge_test.go b/cli/merge_test.go index c0c0cee..32af31c 100644 --- a/cli/merge_test.go +++ b/cli/merge_test.go @@ -446,3 +446,45 @@ func TestRelativeFileLoadInto(t *testing.T) { } } + +func TestParseInsert(t *testing.T) { + testCases := []struct { + line string + expected Insert + valid bool + }{ + { + line: `#insert "file.yaml"`, + expected: Insert{ + path: "file.yaml", + }, + valid: true, + }, + { + line: `#insert file.yaml`, + expected: Insert{ + path: "file.yaml", + }, + valid: true, + }, + { + line: `not an insert`, + valid: false, + }, + { + line: `#include "file.yaml"`, + valid: false, + }, + } + + for _, tc := range testCases { + valid, insert := parseInsert(tc.line) + if valid != tc.valid { + t.Errorf("Expected valid=%v, got %v for line: %s", tc.valid, valid, tc.line) + continue + } + if valid && !reflect.DeepEqual(insert, tc.expected) { + t.Errorf("Expected %+v, got %+v for line: %s", tc.expected, insert, tc.line) + } + } +}