Skip to content
99 changes: 99 additions & 0 deletions cli/includes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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, ")")
Expand Down Expand Up @@ -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] == '/' {
Expand Down
72 changes: 67 additions & 5 deletions cli/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down
42 changes: 42 additions & 0 deletions cli/merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Loading