fix(config): avoid writing known defaults during merge

This commit is contained in:
hkfires
2026-02-08 18:47:44 +08:00
parent 234056072d
commit 6e349bfcc7

View File

@@ -1098,8 +1098,13 @@ 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. New keys are only added if their // key order and comments of existing keys in dst. New keys are only added if their
// value is non-zero to avoid polluting the config with defaults. // value is non-zero and not a known default to avoid polluting the config with defaults.
func mergeMappingPreserve(dst, src *yaml.Node) { func mergeMappingPreserve(dst, src *yaml.Node, path ...[]string) {
var currentPath []string
if len(path) > 0 {
currentPath = path[0]
}
if dst == nil || src == nil { if dst == nil || src == nil {
return return
} }
@@ -1113,13 +1118,14 @@ func mergeMappingPreserve(dst, src *yaml.Node) {
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)
childPath := appendPath(currentPath, sk.Value)
if idx >= 0 { if idx >= 0 {
// Merge into existing value node (always update, even to zero values) // 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, childPath)
} else { } else {
// New key: only add if value is non-zero to avoid polluting config with defaults // New key: only add if value is non-zero and not a known default
if isZeroValueNode(sv) { if isKnownDefaultValue(childPath, sv) {
continue continue
} }
dst.Content = append(dst.Content, deepCopyNode(sk), deepCopyNode(sv)) dst.Content = append(dst.Content, deepCopyNode(sk), deepCopyNode(sv))
@@ -1130,7 +1136,12 @@ func mergeMappingPreserve(dst, src *yaml.Node) {
// mergeNodePreserve merges src into dst for scalars, mappings and sequences while // mergeNodePreserve merges src into dst for scalars, mappings and sequences while
// reusing destination nodes to keep comments and anchors. For sequences, it updates // reusing destination nodes to keep comments and anchors. For sequences, it updates
// in-place by index. // in-place by index.
func mergeNodePreserve(dst, src *yaml.Node) { func mergeNodePreserve(dst, src *yaml.Node, path ...[]string) {
var currentPath []string
if len(path) > 0 {
currentPath = path[0]
}
if dst == nil || src == nil { if dst == nil || src == nil {
return return
} }
@@ -1139,7 +1150,7 @@ func mergeNodePreserve(dst, src *yaml.Node) {
if dst.Kind != yaml.MappingNode { if dst.Kind != yaml.MappingNode {
copyNodeShallow(dst, src) copyNodeShallow(dst, src)
} }
mergeMappingPreserve(dst, src) mergeMappingPreserve(dst, src, currentPath)
case yaml.SequenceNode: case yaml.SequenceNode:
// Preserve explicit null style if dst was null and src is empty sequence // Preserve explicit null style if dst was null and src is empty sequence
if dst.Kind == yaml.ScalarNode && dst.Tag == "!!null" && len(src.Content) == 0 { if dst.Kind == yaml.ScalarNode && dst.Tag == "!!null" && len(src.Content) == 0 {
@@ -1162,7 +1173,7 @@ func mergeNodePreserve(dst, src *yaml.Node) {
dst.Content[i] = deepCopyNode(src.Content[i]) dst.Content[i] = deepCopyNode(src.Content[i])
continue continue
} }
mergeNodePreserve(dst.Content[i], src.Content[i]) mergeNodePreserve(dst.Content[i], src.Content[i], currentPath)
if dst.Content[i] != nil && src.Content[i] != nil && if dst.Content[i] != nil && src.Content[i] != nil &&
dst.Content[i].Kind == yaml.MappingNode && src.Content[i].Kind == yaml.MappingNode { dst.Content[i].Kind == yaml.MappingNode && src.Content[i].Kind == yaml.MappingNode {
pruneMissingMapKeys(dst.Content[i], src.Content[i]) pruneMissingMapKeys(dst.Content[i], src.Content[i])
@@ -1204,6 +1215,56 @@ func findMapKeyIndex(mapNode *yaml.Node, key string) int {
return -1 return -1
} }
// appendPath appends a key to the path, returning a new slice to avoid modifying the original.
func appendPath(path []string, key string) []string {
if len(path) == 0 {
return []string{key}
}
newPath := make([]string, len(path)+1)
copy(newPath, path)
newPath[len(path)] = key
return newPath
}
// isKnownDefaultValue returns true if the given node at the specified path
// represents a known default value that should not be written to the config file.
// This prevents non-zero defaults from polluting the config.
func isKnownDefaultValue(path []string, node *yaml.Node) bool {
// First check if it's a zero value
if isZeroValueNode(node) {
return true
}
// Match known non-zero defaults by exact dotted path.
if len(path) == 0 {
return false
}
fullPath := strings.Join(path, ".")
// Check string defaults
if node.Kind == yaml.ScalarNode && node.Tag == "!!str" {
switch fullPath {
case "pprof.addr":
return node.Value == DefaultPprofAddr
case "remote-management.panel-github-repository":
return node.Value == DefaultPanelGitHubRepository
case "routing.strategy":
return node.Value == "round-robin"
}
}
// Check integer defaults
if node.Kind == yaml.ScalarNode && node.Tag == "!!int" {
switch fullPath {
case "error-logs-max-files":
return node.Value == "10"
}
}
return false
}
// isZeroValueNode returns true if the YAML node represents a zero/default value // 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. // 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. // For mappings and sequences, recursively checks if all children are zero values.