diff --git a/NEWS b/NEWS index 9aafe7ec..8fccf300 100644 --- a/NEWS +++ b/NEWS @@ -1,8 +1,10 @@ 1.6.1.dev0 * #528: Improve the error message when a configuration override contains an invalid value. + * #531: BREAKING: When deep merging common configuration, merge colliding list values by appending + them. Previously, one list replaced the other. * #532: When a configuration include is a relative path, load it from either the current working - directory or from the directory containing the file doing the including. (Previously, only the - working directory was used.) + directory or from the directory containing the file doing the including. Previously, only the + working directory was used. * Add a randomized delay to the sample systemd timer to spread out the load on a server. * Add emojis to documentation table of contents to make it easier to find particular how-to and reference guides at a glance. diff --git a/borgmatic/config/load.py b/borgmatic/config/load.py index 9eb0da9f..0db696fb 100644 --- a/borgmatic/config/load.py +++ b/borgmatic/config/load.py @@ -157,6 +157,20 @@ def deep_merge_nodes(nodes): anchor=b_value.anchor, ), ) + # If we're dealing with SequenceNodes, merge by appending one sequence to the other. + elif isinstance(b_value, ruamel.yaml.nodes.SequenceNode): + replaced_nodes[(b_key, b_value)] = ( + b_key, + ruamel.yaml.nodes.SequenceNode( + tag=b_value.tag, + value=a_value.value + b_value.value, + start_mark=b_value.start_mark, + end_mark=b_value.end_mark, + flow_style=b_value.flow_style, + comment=b_value.comment, + anchor=b_value.anchor, + ), + ) return [ replaced_nodes.get(node, node) for node in nodes if replaced_nodes.get(node) != DELETED_NODE diff --git a/docs/how-to/make-per-application-backups.md b/docs/how-to/make-per-application-backups.md index 9ea496c1..f3b5c06d 100644 --- a/docs/how-to/make-per-application-backups.md +++ b/docs/how-to/make-per-application-backups.md @@ -125,9 +125,9 @@ Once this include gets merged in, the resulting configuration would have a When there's an option collision between the local file and the merged include, the local file's option takes precedence. And as of borgmatic 1.6.0, this feature performs a deep merge, meaning that values are merged at all -levels in the two configuration files. This allows you to include common -configuration—up to full borgmatic configuration files—while overriding only -the parts you want to customize. +levels in the two configuration files. Colliding list values are appended +together. This allows you to include common configuration—up to full borgmatic +configuration files—while overriding only the parts you want to customize. Note that this `<<` include merging syntax is only for merging in mappings (configuration options and their values). But if you'd like to include a diff --git a/tests/integration/config/test_load.py b/tests/integration/config/test_load.py index 400c30d5..e1ecc8ae 100644 --- a/tests/integration/config/test_load.py +++ b/tests/integration/config/test_load.py @@ -326,3 +326,49 @@ def test_deep_merge_nodes_keeps_deeply_nested_values(): assert nested_options[0][1].value == '--init-option' assert nested_options[1][0].value == 'prune' assert nested_options[1][1].value == '--prune-option' + + +def test_deep_merge_nodes_appends_colliding_sequence_values(): + node_values = [ + ( + ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'), + ruamel.yaml.nodes.MappingNode( + tag='tag:yaml.org,2002:map', + value=[ + ( + ruamel.yaml.nodes.ScalarNode( + tag='tag:yaml.org,2002:str', value='before_backup' + ), + ruamel.yaml.nodes.SequenceNode( + tag='tag:yaml.org,2002:int', value=['echo 1', 'echo 2'] + ), + ), + ], + ), + ), + ( + ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'), + ruamel.yaml.nodes.MappingNode( + tag='tag:yaml.org,2002:map', + value=[ + ( + ruamel.yaml.nodes.ScalarNode( + tag='tag:yaml.org,2002:str', value='before_backup' + ), + ruamel.yaml.nodes.SequenceNode( + tag='tag:yaml.org,2002:int', value=['echo 3', 'echo 4'] + ), + ), + ], + ), + ), + ] + + result = module.deep_merge_nodes(node_values) + assert len(result) == 1 + (section_key, section_value) = result[0] + assert section_key.value == 'hooks' + options = section_value.value + assert len(options) == 1 + assert options[0][0].value == 'before_backup' + assert options[0][1].value == ['echo 1', 'echo 2', 'echo 3', 'echo 4']