From 6e349bfcc78410166d5d10777fcdb6bda60f436b Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:47:44 +0800 Subject: [PATCH] fix(config): avoid writing known defaults during merge --- internal/config/config.go | 77 +++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 706bb991..64508ae5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1098,8 +1098,13 @@ func getOrCreateMapValue(mapNode *yaml.Node, key string) *yaml.Node { // 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 -// value is non-zero to avoid polluting the config with defaults. -func mergeMappingPreserve(dst, src *yaml.Node) { +// value is non-zero and not a known default to avoid polluting the config with defaults. +func mergeMappingPreserve(dst, src *yaml.Node, path ...[]string) { + var currentPath []string + if len(path) > 0 { + currentPath = path[0] + } + if dst == nil || src == nil { return } @@ -1113,13 +1118,14 @@ func mergeMappingPreserve(dst, src *yaml.Node) { sk := src.Content[i] sv := src.Content[i+1] idx := findMapKeyIndex(dst, sk.Value) + childPath := appendPath(currentPath, sk.Value) if idx >= 0 { // Merge into existing value node (always update, even to zero values) dv := dst.Content[idx+1] - mergeNodePreserve(dv, sv) + mergeNodePreserve(dv, sv, childPath) } else { - // New key: only add if value is non-zero to avoid polluting config with defaults - if isZeroValueNode(sv) { + // New key: only add if value is non-zero and not a known default + if isKnownDefaultValue(childPath, sv) { continue } 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 // reusing destination nodes to keep comments and anchors. For sequences, it updates // 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 { return } @@ -1139,7 +1150,7 @@ func mergeNodePreserve(dst, src *yaml.Node) { if dst.Kind != yaml.MappingNode { copyNodeShallow(dst, src) } - mergeMappingPreserve(dst, src) + mergeMappingPreserve(dst, src, currentPath) case yaml.SequenceNode: // 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 { @@ -1162,7 +1173,7 @@ func mergeNodePreserve(dst, src *yaml.Node) { dst.Content[i] = deepCopyNode(src.Content[i]) 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 && dst.Content[i].Kind == yaml.MappingNode && src.Content[i].Kind == yaml.MappingNode { pruneMissingMapKeys(dst.Content[i], src.Content[i]) @@ -1204,6 +1215,56 @@ func findMapKeyIndex(mapNode *yaml.Node, key string) int { 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 // 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.