fix(config): smart merge writes non-default new keys only

This commit is contained in:
hkfires
2025-12-27 20:28:54 +08:00
parent 375ef252ab
commit c8e72ba0dc

View File

@@ -817,8 +817,8 @@ func getOrCreateMapValue(mapNode *yaml.Node, key string) *yaml.Node {
} }
// mergeMappingPreserve merges keys from src into dst mapping node while preserving // mergeMappingPreserve merges keys from src into dst mapping node while preserving
// key order and comments of existing keys in dst. Unknown keys from src are skipped // key order and comments of existing keys in dst. New keys are only added if their
// so the original config structure is preserved without introducing defaults. // value is non-zero to avoid polluting the config with defaults.
func mergeMappingPreserve(dst, src *yaml.Node) { func mergeMappingPreserve(dst, src *yaml.Node) {
if dst == nil || src == nil { if dst == nil || src == nil {
return return
@@ -829,17 +829,21 @@ func mergeMappingPreserve(dst, src *yaml.Node) {
copyNodeShallow(dst, src) copyNodeShallow(dst, src)
return return
} }
// Only update existing keys in dst, do not add new keys
for i := 0; i+1 < len(src.Content); i += 2 { for i := 0; i+1 < len(src.Content); i += 2 {
sk := src.Content[i] sk := src.Content[i]
sv := src.Content[i+1] sv := src.Content[i+1]
idx := findMapKeyIndex(dst, sk.Value) idx := findMapKeyIndex(dst, sk.Value)
if idx >= 0 { if idx >= 0 {
// Merge into existing value node // Merge into existing value node (always update, even to zero values)
dv := dst.Content[idx+1] dv := dst.Content[idx+1]
mergeNodePreserve(dv, sv) mergeNodePreserve(dv, sv)
} else {
// New key: only add if value is non-zero to avoid polluting config with defaults
if isZeroValueNode(sv) {
continue
}
dst.Content = append(dst.Content, deepCopyNode(sk), deepCopyNode(sv))
} }
// Keys not in dst are skipped - preserves original config structure
} }
} }
@@ -920,6 +924,51 @@ func findMapKeyIndex(mapNode *yaml.Node, key string) int {
return -1 return -1
} }
// isZeroValueNode returns true if the YAML node represents a zero/default value
// that should not be written as a new key to preserve config cleanliness.
// For mappings and sequences, recursively checks if all children are zero values.
func isZeroValueNode(node *yaml.Node) bool {
if node == nil {
return true
}
switch node.Kind {
case yaml.ScalarNode:
switch node.Tag {
case "!!bool":
return node.Value == "false"
case "!!int", "!!float":
return node.Value == "0" || node.Value == "0.0"
case "!!str":
return node.Value == ""
case "!!null":
return true
}
case yaml.SequenceNode:
if len(node.Content) == 0 {
return true
}
// Check if all elements are zero values
for _, child := range node.Content {
if !isZeroValueNode(child) {
return false
}
}
return true
case yaml.MappingNode:
if len(node.Content) == 0 {
return true
}
// Check if all values are zero values (values are at odd indices)
for i := 1; i < len(node.Content); i += 2 {
if !isZeroValueNode(node.Content[i]) {
return false
}
}
return true
}
return false
}
// deepCopyNode creates a deep copy of a yaml.Node graph. // deepCopyNode creates a deep copy of a yaml.Node graph.
func deepCopyNode(n *yaml.Node) *yaml.Node { func deepCopyNode(n *yaml.Node) *yaml.Node {
if n == nil { if n == nil {