diff --git a/NEWS b/NEWS index 978d04fd..a63be326 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,8 @@ variables available for explicit use in your commands. See the documentation for more information: https://torsion.org/borgmatic/docs/how-to/run-arbitrary-borg-commands/ * #719: Fix an error when running "borg key export" through borgmatic. + * When merging two configuration files, error gracefully if the two files do not adhere to the same + format. 1.7.15 * #326: Add configuration options and command-line flags for backing up a database from one diff --git a/borgmatic/config/load.py b/borgmatic/config/load.py index f6290de8..f5d071c9 100644 --- a/borgmatic/config/load.py +++ b/borgmatic/config/load.py @@ -225,6 +225,8 @@ def deep_merge_nodes(nodes): The purpose of deep merging like this is to support, for instance, merging one borgmatic configuration file into another for reuse, such that a configuration section ("retention", etc.) does not completely replace the corresponding section in a merged file. + + Raise ValueError if a merge is implied using two incompatible types. ''' # Map from original node key/value to the replacement merged node. DELETED_NODE as a replacement # node indications deletion. @@ -239,6 +241,11 @@ def deep_merge_nodes(nodes): # If the keys match and the values are different, we need to merge these two A and B nodes. if a_key.tag == b_key.tag and a_key.value == b_key.value and a_value != b_value: + if not type(a_value) is type(b_value): + raise ValueError( + f'Incompatible types found when trying to merge "{a_key.value}:" values across configuration files: {type(a_value).id} and {type(b_value).id}' + ) + # Since we're merging into the B node, consider the A node a duplicate and remove it. replaced_nodes[(a_key, a_value)] = DELETED_NODE diff --git a/tests/integration/config/test_load.py b/tests/integration/config/test_load.py index 028a6523..81b5deee 100644 --- a/tests/integration/config/test_load.py +++ b/tests/integration/config/test_load.py @@ -702,6 +702,54 @@ def test_deep_merge_nodes_appends_colliding_sequence_values(): assert [item.value for item in options[0][1].value] == ['echo 1', 'echo 2', 'echo 3', 'echo 4'] +def test_deep_merge_nodes_errors_on_colliding_values_of_different_types(): + node_values = [ + ( + module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'), + module.ruamel.yaml.nodes.MappingNode( + tag='tag:yaml.org,2002:map', + value=[ + ( + module.ruamel.yaml.nodes.ScalarNode( + tag='tag:yaml.org,2002:str', value='before_backup' + ), + module.ruamel.yaml.nodes.ScalarNode( + tag='tag:yaml.org,2002:str', value='echo oopsie daisy' + ), + ), + ], + ), + ), + ( + module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'), + module.ruamel.yaml.nodes.MappingNode( + tag='tag:yaml.org,2002:map', + value=[ + ( + module.ruamel.yaml.nodes.ScalarNode( + tag='tag:yaml.org,2002:str', value='before_backup' + ), + module.ruamel.yaml.nodes.SequenceNode( + tag='tag:yaml.org,2002:seq', + value=[ + module.ruamel.yaml.ScalarNode( + tag='tag:yaml.org,2002:str', value='echo 3' + ), + module.ruamel.yaml.ScalarNode( + tag='tag:yaml.org,2002:str', value='echo 4' + ), + ], + ), + ), + ], + ), + ), + ] + + with pytest.raises(ValueError): + module.deep_merge_nodes(node_values) + + def test_deep_merge_nodes_only_keeps_mapping_values_tagged_with_retain(): node_values = [ (