From ea2a253134a21e1a07d04cce49b0b9ca64fdbb8d Mon Sep 17 00:00:00 2001 From: slantsh Date: Sun, 22 Feb 2026 13:17:09 +0530 Subject: [PATCH 1/6] added --files-changed flag --- borgmatic/borg/create.py | 2 ++ borgmatic/config/schema.yaml | 8 ++++++++ tests/unit/borg/test_create.py | 1 + 3 files changed, 11 insertions(+) diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 85f7e042..f2f23c69 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -198,6 +198,7 @@ def make_base_create_command( # noqa: PLR0912 umask = config.get('umask', None) lock_wait = config.get('lock_wait', None) list_filter_flags = flags.make_list_filter_flags(local_borg_version, dry_run) + files_changed = config.get('files_changed') files_cache = config.get('files_cache') archive_name_format = ( config.get('archive_name_format', flags.get_default_archive_name_format(local_borg_version)) @@ -248,6 +249,7 @@ def make_base_create_command( # noqa: PLR0912 + (('--nobirthtime',) if config.get('birthtime') is False else ()) + (('--read-special',) if config.get('read_special') or stream_processes else ()) + noflags_flags + + (('--files-changed', files_changed) if files_changed else ()) + (('--files-cache', files_cache) if files_cache else ()) + (('--remote-path', remote_path) if remote_path else ()) + (('--umask', str(umask)) if umask else ()) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index e29c1e58..f339ce0d 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -166,6 +166,14 @@ properties: Record filesystem flags (e.g. NODUMP, IMMUTABLE) in archive. Defaults to true. example: false + files_changed: + type: string + description: | + Threshold for considering a file as changed. See + https://borgbackup.readthedocs.io/en/stable/usage/create.html for + details. Defaults to "mtime" (i.e., a file is considered + changed if its mtime has changed since the last backup). + example: mtime,size files_cache: type: string description: | diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index e85b1479..969b707e 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -482,6 +482,7 @@ def test_make_base_create_command_with_store_config_false_omits_config_files(): ('flags', True, False, ()), ('flags', False, True, ('--noflags',)), ('flags', False, False, ('--nobsdflags',)), + ('files_changed', 'mtime', True, ('--files-changed', 'mtime')), ('files_cache', 'ctime,size', True, ('--files-cache', 'ctime,size')), ('umask', 740, True, ('--umask', '740')), ('lock_wait', 5, True, ('--lock-wait', '5')), From 7dcb4e3c3a425e1caa7e79ca4ba34775bc5716a8 Mon Sep 17 00:00:00 2001 From: slantsh Date: Mon, 23 Feb 2026 06:03:06 +0530 Subject: [PATCH 2/6] added BORG_MSGPACK_VERSION_CHECK, no test --- borgmatic/borg/environment.py | 1 + borgmatic/config/schema.yaml | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/borgmatic/borg/environment.py b/borgmatic/borg/environment.py index bdadcc30..90f79cbd 100644 --- a/borgmatic/borg/environment.py +++ b/borgmatic/borg/environment.py @@ -13,6 +13,7 @@ OPTION_TO_ENVIRONMENT_VARIABLE = { 'borg_key_file': 'BORG_KEY_FILE', 'ssh_command': 'BORG_RSH', 'temporary_directory': 'TMPDIR', + 'msgpack_version_check': 'BORG_MSGPACK_VERSION_CHECK', } DEFAULT_BOOL_OPTION_TO_UNCONDITIONAL_ENVIRONMENT_VARIABLE = { diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index f339ce0d..75e9a7a8 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -570,6 +570,12 @@ properties: Bypass Borg confirmation about check with repair option. Defaults to false and an interactive prompt from Borg. example: true + msgpack_version_check: + type: string + description: | + optionally disable the msgpack version check. + Default is “yes”; use at your own risk. + example: yes extra_borg_options: type: object additionalProperties: false From ceb487a6b9deb1040b71dca0d684dd5d5b129456 Mon Sep 17 00:00:00 2001 From: slantsh Date: Wed, 25 Feb 2026 19:54:10 +0530 Subject: [PATCH 3/6] Fixed documentation in schema --- borgmatic/config/schema.yaml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 75e9a7a8..7a6e45f4 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -168,12 +168,13 @@ properties: example: false files_changed: type: string + enum: ['ctime', 'mtime', 'disabled'] description: | Threshold for considering a file as changed. See https://borgbackup.readthedocs.io/en/stable/usage/create.html for - details. Defaults to "mtime" (i.e., a file is considered - changed if its mtime has changed since the last backup). - example: mtime,size + details. Defaults to "ctime" (i.e., a file is considered + changed if its ctime has changed since the last backup). + example: ctime files_cache: type: string description: | @@ -571,11 +572,11 @@ properties: false and an interactive prompt from Borg. example: true msgpack_version_check: - type: string + type: boolean description: | - optionally disable the msgpack version check. - Default is “yes”; use at your own risk. - example: yes + Optionally disable the msgpack version check. + Default is true; use at your own risk. + example: true extra_borg_options: type: object additionalProperties: false From ee32cc19e9d31da59ef48dc55c09c87b9c54af0f Mon Sep 17 00:00:00 2001 From: slantsh Date: Wed, 25 Feb 2026 19:58:08 +0530 Subject: [PATCH 4/6] change BORG_MSGPACK_VERSION_CHECK to default bool --- borgmatic/borg/environment.py | 5 +++-- tests/unit/borg/test_environment.py | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/borgmatic/borg/environment.py b/borgmatic/borg/environment.py index 90f79cbd..e975ec04 100644 --- a/borgmatic/borg/environment.py +++ b/borgmatic/borg/environment.py @@ -13,7 +13,6 @@ OPTION_TO_ENVIRONMENT_VARIABLE = { 'borg_key_file': 'BORG_KEY_FILE', 'ssh_command': 'BORG_RSH', 'temporary_directory': 'TMPDIR', - 'msgpack_version_check': 'BORG_MSGPACK_VERSION_CHECK', } DEFAULT_BOOL_OPTION_TO_UNCONDITIONAL_ENVIRONMENT_VARIABLE = { @@ -26,6 +25,7 @@ DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE = { 'relocated_repo_access_is_ok': 'BORG_RELOCATED_REPO_ACCESS_IS_OK', 'unknown_unencrypted_repo_access_is_ok': 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK', 'use_chunks_archive': 'BORG_USE_CHUNKS_ARCHIVE', + 'msgpack_version_check': 'BORG_MSGPACK_VERSION_CHECK', } @@ -91,7 +91,8 @@ def make_environment(config): ) in DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE.items(): if os.environ.get(environment_variable_name) is None: value = config.get(option_name) - environment[environment_variable_name] = 'YES' if value else 'NO' + if value is not None: + environment[environment_variable_name] = 'YES' if value else 'NO' for ( option_name, diff --git a/tests/unit/borg/test_environment.py b/tests/unit/borg/test_environment.py index f458cf6a..ee190770 100644 --- a/tests/unit/borg/test_environment.py +++ b/tests/unit/borg/test_environment.py @@ -95,6 +95,7 @@ def test_make_environment_without_configuration_sets_certain_environment_variabl 'BORG_USE_CHUNKS_ARCHIVE': 'NO', 'BORG_DEBUG_PASSPHRASE': 'NO', 'BORG_DISPLAY_PASSPHRASE': 'NO', + 'BORG_MSGPACK_VERSION_CHECK': 'YES', } @@ -107,6 +108,8 @@ def test_make_environment_without_configuration_passes_through_default_environme 'BORG_USE_CHUNKS_ARCHIVE': 'yup', 'BORG_DEBUG_PASSPHRASE': 'nah', 'BORG_DISPLAY_PASSPHRASE': 'yup', + 'BORG_MSGPACK_VERSION_CHECK': 'yup', + }, ) flexmock(module.borgmatic.hooks.credential.parse).should_receive( @@ -122,6 +125,7 @@ def test_make_environment_without_configuration_passes_through_default_environme 'BORG_USE_CHUNKS_ARCHIVE': 'yup', 'BORG_DEBUG_PASSPHRASE': 'nah', 'BORG_DISPLAY_PASSPHRASE': 'yup', + 'BORG_MSGPACK_VERSION_CHECK': 'yup', 'BORG_EXIT_CODES': 'modern', } From 75dcd41050916b269124c2cbbc05bd2f13ec93cc Mon Sep 17 00:00:00 2001 From: slantsh Date: Sat, 28 Feb 2026 09:28:26 +0530 Subject: [PATCH 5/6] test reconfigured --- tests/unit/borg/test_environment.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/unit/borg/test_environment.py b/tests/unit/borg/test_environment.py index ee190770..dcb5375c 100644 --- a/tests/unit/borg/test_environment.py +++ b/tests/unit/borg/test_environment.py @@ -90,12 +90,6 @@ def test_make_environment_without_configuration_sets_certain_environment_variabl assert environment == { 'USER': 'root', 'BORG_EXIT_CODES': 'modern', - 'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'NO', - 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'NO', - 'BORG_USE_CHUNKS_ARCHIVE': 'NO', - 'BORG_DEBUG_PASSPHRASE': 'NO', - 'BORG_DISPLAY_PASSPHRASE': 'NO', - 'BORG_MSGPACK_VERSION_CHECK': 'YES', } From 1ada61a1d39e30b70946452cd930ded836c9cdb0 Mon Sep 17 00:00:00 2001 From: slantsh Date: Sat, 28 Feb 2026 09:40:41 +0530 Subject: [PATCH 6/6] ruff formatting? --- borgmatic/config/generate.py | 8 ++- tests/unit/borg/test_environment.py | 1 - tests/unit/commands/test_arguments.py | 90 ++++++++++++++++----------- 3 files changed, 59 insertions(+), 40 deletions(-) diff --git a/borgmatic/config/generate.py b/borgmatic/config/generate.py index 4d3cda78..e22c452c 100644 --- a/borgmatic/config/generate.py +++ b/borgmatic/config/generate.py @@ -168,9 +168,11 @@ def render_configuration(config): rendered, # Dumping certain values (integers, for instance) causes ruamel.yaml to append an # end-of-document "..." marker. Strip it. - transform=lambda dumped: dumped[: -len(RUAMEL_YAML_END_OF_DOCUMENT_MARKER)] - if dumped.endswith(RUAMEL_YAML_END_OF_DOCUMENT_MARKER) - else dumped, + transform=lambda dumped: ( + dumped[: -len(RUAMEL_YAML_END_OF_DOCUMENT_MARKER)] + if dumped.endswith(RUAMEL_YAML_END_OF_DOCUMENT_MARKER) + else dumped + ), ) return rendered.getvalue() diff --git a/tests/unit/borg/test_environment.py b/tests/unit/borg/test_environment.py index dcb5375c..1b49ba7c 100644 --- a/tests/unit/borg/test_environment.py +++ b/tests/unit/borg/test_environment.py @@ -103,7 +103,6 @@ def test_make_environment_without_configuration_passes_through_default_environme 'BORG_DEBUG_PASSPHRASE': 'nah', 'BORG_DISPLAY_PASSPHRASE': 'yup', 'BORG_MSGPACK_VERSION_CHECK': 'yup', - }, ) flexmock(module.borgmatic.hooks.credential.parse).should_receive( diff --git a/tests/unit/commands/test_arguments.py b/tests/unit/commands/test_arguments.py index 0fad32e7..0606a1d2 100644 --- a/tests/unit/commands/test_arguments.py +++ b/tests/unit/commands/test_arguments.py @@ -329,10 +329,12 @@ def test_parse_arguments_for_actions_consumes_action_arguments_after_action_name remaining = flexmock() flexmock(module).should_receive('get_subaction_parsers').and_return({}) flexmock(module).should_receive('parse_and_record_action_arguments').replace_with( - lambda unparsed, parsed, parser, action, canonical=None: parsed.update( - {action: action_namespace}, - ) - or remaining, + lambda unparsed, parsed, parser, action, canonical=None: ( + parsed.update( + {action: action_namespace}, + ) + or remaining + ), ) flexmock(module).should_receive('get_subactions_for_actions').and_return({}) action_parsers = {'action': flexmock(), 'other': flexmock()} @@ -355,10 +357,12 @@ def test_parse_arguments_for_actions_consumes_action_arguments_with_alias(): remaining = flexmock() flexmock(module).should_receive('get_subaction_parsers').and_return({}) flexmock(module).should_receive('parse_and_record_action_arguments').replace_with( - lambda unparsed, parsed, parser, action, canonical=None: parsed.update( - {canonical or action: action_namespace}, - ) - or remaining, + lambda unparsed, parsed, parser, action, canonical=None: ( + parsed.update( + {canonical or action: action_namespace}, + ) + or remaining + ), ) flexmock(module).should_receive('get_subactions_for_actions').and_return({}) action_parsers = { @@ -387,10 +391,12 @@ def test_parse_arguments_for_actions_consumes_multiple_action_arguments(): other_namespace = flexmock(bar=3) flexmock(module).should_receive('get_subaction_parsers').and_return({}) flexmock(module).should_receive('parse_and_record_action_arguments').replace_with( - lambda unparsed, parsed, parser, action, canonical=None: parsed.update( - {action: action_namespace if action == 'action' else other_namespace}, - ) - or (), + lambda unparsed, parsed, parser, action, canonical=None: ( + parsed.update( + {action: action_namespace if action == 'action' else other_namespace}, + ) + or () + ), ).and_return(('other', '--bar', '3')).and_return('action', '--foo', 'true') flexmock(module).should_receive('get_subactions_for_actions').and_return({}) action_parsers = { @@ -420,10 +426,12 @@ def test_parse_arguments_for_actions_respects_command_line_action_ordering(): action_namespace = flexmock(foo=True) flexmock(module).should_receive('get_subaction_parsers').and_return({}) flexmock(module).should_receive('parse_and_record_action_arguments').replace_with( - lambda unparsed, parsed, parser, action, canonical=None: parsed.update( - {action: other_namespace if action == 'other' else action_namespace}, - ) - or (), + lambda unparsed, parsed, parser, action, canonical=None: ( + parsed.update( + {action: other_namespace if action == 'other' else action_namespace}, + ) + or () + ), ).and_return(('action',)).and_return(('other', '--foo', 'true')) flexmock(module).should_receive('get_subactions_for_actions').and_return({}) action_parsers = { @@ -458,10 +466,12 @@ def test_parse_arguments_for_actions_applies_default_action_parsers(): flexmock(module).should_receive('get_subaction_parsers').and_return({}) flexmock(module).should_receive('parse_and_record_action_arguments').replace_with( - lambda unparsed, parsed, parser, action, canonical=None: parsed.update( - {action: namespaces.get(action)}, - ) - or (), + lambda unparsed, parsed, parser, action, canonical=None: ( + parsed.update( + {action: namespaces.get(action)}, + ) + or () + ), ).and_return(()) flexmock(module).should_receive('get_subactions_for_actions').and_return({}) action_parsers = { @@ -488,10 +498,12 @@ def test_parse_arguments_for_actions_consumes_global_arguments(): action_namespace = flexmock() flexmock(module).should_receive('get_subaction_parsers').and_return({}) flexmock(module).should_receive('parse_and_record_action_arguments').replace_with( - lambda unparsed, parsed, parser, action, canonical=None: parsed.update( - {action: action_namespace}, - ) - or ('--verbosity', 'lots'), + lambda unparsed, parsed, parser, action, canonical=None: ( + parsed.update( + {action: action_namespace}, + ) + or ('--verbosity', 'lots') + ), ) flexmock(module).should_receive('get_subactions_for_actions').and_return({}) action_parsers = { @@ -516,10 +528,12 @@ def test_parse_arguments_for_actions_passes_through_unknown_arguments_before_act action_namespace = flexmock() flexmock(module).should_receive('get_subaction_parsers').and_return({}) flexmock(module).should_receive('parse_and_record_action_arguments').replace_with( - lambda unparsed, parsed, parser, action, canonical=None: parsed.update( - {action: action_namespace}, - ) - or ('--wtf', 'yes'), + lambda unparsed, parsed, parser, action, canonical=None: ( + parsed.update( + {action: action_namespace}, + ) + or ('--wtf', 'yes') + ), ) flexmock(module).should_receive('get_subactions_for_actions').and_return({}) action_parsers = { @@ -544,10 +558,12 @@ def test_parse_arguments_for_actions_passes_through_unknown_arguments_after_acti action_namespace = flexmock() flexmock(module).should_receive('get_subaction_parsers').and_return({}) flexmock(module).should_receive('parse_and_record_action_arguments').replace_with( - lambda unparsed, parsed, parser, action, canonical=None: parsed.update( - {action: action_namespace}, - ) - or ('--wtf', 'yes'), + lambda unparsed, parsed, parser, action, canonical=None: ( + parsed.update( + {action: action_namespace}, + ) + or ('--wtf', 'yes') + ), ) flexmock(module).should_receive('get_subactions_for_actions').and_return({}) action_parsers = { @@ -572,10 +588,12 @@ def test_parse_arguments_for_actions_with_borg_action_skips_other_action_parsers action_namespace = flexmock(options=[]) flexmock(module).should_receive('get_subaction_parsers').and_return({}) flexmock(module).should_receive('parse_and_record_action_arguments').replace_with( - lambda unparsed, parsed, parser, action, canonical=None: parsed.update( - {action: action_namespace}, - ) - or (), + lambda unparsed, parsed, parser, action, canonical=None: ( + parsed.update( + {action: action_namespace}, + ) + or () + ), ).and_return(()) flexmock(module).should_receive('get_subactions_for_actions').and_return({}) action_parsers = {