瀏覽代碼

Fix a broken "create" action and "--archive latest" flag when multiple archives share the same name with Borg 2 (#1146).

Dan Helfman 2 天之前
父節點
當前提交
f15e8282ab
共有 5 個文件被更改,包括 99 次插入22 次删除
  1. 2 0
      NEWS
  2. 8 1
      borgmatic/actions/create.py
  3. 9 3
      borgmatic/borg/repo_list.py
  4. 30 1
      tests/unit/actions/test_create.py
  5. 50 17
      tests/unit/borg/test_repo_list.py

+ 2 - 0
NEWS

@@ -17,6 +17,8 @@
    support the "bootstrap" action.
    support the "bootstrap" action.
  * #1136: For all database hooks, record metadata about the dumps contained within an archive.
  * #1136: For all database hooks, record metadata about the dumps contained within an archive.
  * #1139: Set "borgmatic" as the user agent when connecting to monitoring services.
  * #1139: Set "borgmatic" as the user agent when connecting to monitoring services.
+ * #1146: Fix a broken "create" action and "--archive latest" flag when multiple archives share the
+   same name with Borg 2.
  * When running tests, use Ruff for faster and more comprehensive code linting and formatting,
  * When running tests, use Ruff for faster and more comprehensive code linting and formatting,
    replacing Flake8, Black, isort, etc.
    replacing Flake8, Black, isort, etc.
  * Switch from pipx to uv for installing development tools, and added tox-uv for speeding up test
  * Switch from pipx to uv for installing development tools, and added tox-uv for speeding up test

+ 8 - 1
borgmatic/actions/create.py

@@ -2,6 +2,7 @@ import logging
 
 
 import borgmatic.actions.json
 import borgmatic.actions.json
 import borgmatic.borg.create
 import borgmatic.borg.create
+import borgmatic.borg.feature
 import borgmatic.borg.rename
 import borgmatic.borg.rename
 import borgmatic.borg.repo_list
 import borgmatic.borg.repo_list
 import borgmatic.config.paths
 import borgmatic.config.paths
@@ -182,7 +183,13 @@ def rename_checkpoint_archive(
 
 
     borgmatic.borg.rename.rename_archive(
     borgmatic.borg.rename.rename_archive(
         repository_path,
         repository_path,
-        archive_name,
+        (
+            archive['id']
+            if borgmatic.borg.feature.available(
+                borgmatic.borg.feature.Feature.ARCHIVE_SERIES, local_borg_version
+            )
+            else archive['name']
+        ),
         new_archive_name,
         new_archive_name,
         global_arguments.dry_run,
         global_arguments.dry_run,
         config,
         config,

+ 9 - 3
borgmatic/borg/repo_list.py

@@ -23,7 +23,9 @@ def resolve_archive_name(
     Given a local or remote repository path, an archive name, a configuration dict, the local Borg
     Given a local or remote repository path, an archive name, a configuration dict, the local Borg
     version, global arguments as an argparse.Namespace, a local Borg path, and a remote Borg path,
     version, global arguments as an argparse.Namespace, a local Borg path, and a remote Borg path,
     return the archive name. But if the archive name is "latest", then instead introspect the
     return the archive name. But if the archive name is "latest", then instead introspect the
-    repository for the latest archive and return its name.
+    repository for the latest archive and return its name or ID, depending on whether the version of
+    Borg in use supports archive series—different archives that share the same name but have unique
+    IDs.
 
 
     Raise ValueError if "latest" is given but there are no archives in the repository.
     Raise ValueError if "latest" is given but there are no archives in the repository.
     '''
     '''
@@ -39,7 +41,11 @@ def resolve_archive_name(
         remote_path=remote_path,
         remote_path=remote_path,
     )
     )
 
 
-    return latest_archive['name']
+    return (
+        latest_archive['id']
+        if feature.available(feature.Feature.ARCHIVE_SERIES, local_borg_version)
+        else latest_archive['name']
+    )
 
 
 
 
 def get_latest_archive(
 def get_latest_archive(
@@ -93,7 +99,7 @@ def get_latest_archive(
     except IndexError:
     except IndexError:
         raise ValueError('No archives found in the repository')
         raise ValueError('No archives found in the repository')
 
 
-    logger.debug(f'Latest archive is {latest_archive["name"]}')
+    logger.debug(f'Latest archive is {latest_archive["name"]} ({latest_archive["id"]})')
 
 
     return latest_archive
     return latest_archive
 
 

+ 30 - 1
tests/unit/actions/test_create.py

@@ -379,7 +379,7 @@ def test_run_create_with_active_dumps_json_updates_archive_info():
     ) == [expected_create_result]
     ) == [expected_create_result]
 
 
 
 
-def test_rename_checkpoint_archive_renames_archive():
+def test_rename_checkpoint_archive_renames_archive_using_name():
     global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
     global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
     flexmock(module.borgmatic.borg.repo_list).should_receive('get_latest_archive').and_return(
     flexmock(module.borgmatic.borg.repo_list).should_receive('get_latest_archive').and_return(
         {'id': 'id1', 'name': 'archive.checkpoint'},
         {'id': 'id1', 'name': 'archive.checkpoint'},
@@ -395,6 +395,35 @@ def test_rename_checkpoint_archive_renames_archive():
         local_path=None,
         local_path=None,
         remote_path=None,
         remote_path=None,
     )
     )
+    flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(False)
+
+    module.rename_checkpoint_archive(
+        repository_path='path',
+        global_arguments=global_arguments,
+        config={},
+        local_borg_version=None,
+        local_path=None,
+        remote_path=None,
+    )
+
+
+def test_rename_checkpoint_with_feature_available_archive_renames_archive_using_id():
+    global_arguments = flexmock(monitoring_verbosity=1, dry_run=False)
+    flexmock(module.borgmatic.borg.repo_list).should_receive('get_latest_archive').and_return(
+        {'id': 'id1', 'name': 'archive.checkpoint'},
+    )
+
+    flexmock(module.borgmatic.borg.rename).should_receive('rename_archive').with_args(
+        repository_name='path',
+        old_archive_name='id1',
+        new_archive_name='archive',
+        dry_run=False,
+        config={},
+        local_borg_version=None,
+        local_path=None,
+        remote_path=None,
+    )
+    flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
 
 
     module.rename_checkpoint_archive(
     module.rename_checkpoint_archive(
         repository_path='path',
         repository_path='path',

+ 50 - 17
tests/unit/borg/test_repo_list.py

@@ -40,16 +40,48 @@ def test_resolve_archive_name_passes_through_non_latest_archive_name():
     )
     )
 
 
 
 
-def test_resolve_archive_name_calls_get_latest_archive():
-    expected_archive = 'archive-name'
-
+def test_resolve_archive_looks_up_latest_archive_name():
+    expected_name = 'archive-name'
     repository_path = flexmock()
     repository_path = flexmock()
     config = flexmock()
     config = flexmock()
     local_borg_version = flexmock()
     local_borg_version = flexmock()
     global_arguments = flexmock()
     global_arguments = flexmock()
     local_path = flexmock()
     local_path = flexmock()
     remote_path = flexmock()
     remote_path = flexmock()
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
+    flexmock(module).should_receive('get_latest_archive').with_args(
+        repository_path,
+        config,
+        local_borg_version,
+        global_arguments,
+        local_path,
+        remote_path,
+    ).and_return({'name': expected_name, 'id': 'd34db33f'})
+    flexmock(module.feature).should_receive('available').and_return(False)
+
+    assert (
+        module.resolve_archive_name(
+            repository_path,
+            'latest',
+            config,
+            local_borg_version,
+            global_arguments,
+            local_path,
+            remote_path,
+        )
+        == expected_name
+    )
+
 
 
+def test_resolve_archive_with_feature_available_looks_up_latest_archive_id():
+    expected_id = 'd34db33f'
+    repository_path = flexmock()
+    config = flexmock()
+    local_borg_version = flexmock()
+    global_arguments = flexmock()
+    local_path = flexmock()
+    remote_path = flexmock()
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module).should_receive('get_latest_archive').with_args(
     flexmock(module).should_receive('get_latest_archive').with_args(
@@ -59,7 +91,8 @@ def test_resolve_archive_name_calls_get_latest_archive():
         global_arguments,
         global_arguments,
         local_path,
         local_path,
         remote_path,
         remote_path,
-    ).and_return({'name': expected_archive})
+    ).and_return({'name': 'archive-name', 'id': expected_id})
+    flexmock(module.feature).should_receive('available').and_return(True)
 
 
     assert (
     assert (
         module.resolve_archive_name(
         module.resolve_archive_name(
@@ -71,12 +104,12 @@ def test_resolve_archive_name_calls_get_latest_archive():
             local_path,
             local_path,
             remote_path,
             remote_path,
         )
         )
-        == expected_archive
+        == expected_id
     )
     )
 
 
 
 
 def test_get_latest_archive_calls_borg_with_flags():
 def test_get_latest_archive_calls_borg_with_flags():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
@@ -105,7 +138,7 @@ def test_get_latest_archive_calls_borg_with_flags():
 
 
 
 
 def test_get_latest_archive_with_log_info_calls_borg_without_info_flag():
 def test_get_latest_archive_with_log_info_calls_borg_without_info_flag():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
@@ -135,7 +168,7 @@ def test_get_latest_archive_with_log_info_calls_borg_without_info_flag():
 
 
 
 
 def test_get_latest_archive_with_log_debug_calls_borg_without_debug_flag():
 def test_get_latest_archive_with_log_debug_calls_borg_without_debug_flag():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
@@ -165,7 +198,7 @@ def test_get_latest_archive_with_log_debug_calls_borg_without_debug_flag():
 
 
 
 
 def test_get_latest_archive_with_local_path_calls_borg_via_local_path():
 def test_get_latest_archive_with_local_path_calls_borg_via_local_path():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
@@ -195,7 +228,7 @@ def test_get_latest_archive_with_local_path_calls_borg_via_local_path():
 
 
 
 
 def test_get_latest_archive_with_exit_codes_calls_borg_using_them():
 def test_get_latest_archive_with_exit_codes_calls_borg_using_them():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
@@ -225,7 +258,7 @@ def test_get_latest_archive_with_exit_codes_calls_borg_using_them():
 
 
 
 
 def test_get_latest_archive_with_remote_path_calls_borg_with_remote_path_flags():
 def test_get_latest_archive_with_remote_path_calls_borg_with_remote_path_flags():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args(
     flexmock(module.flags).should_receive('make_flags').with_args(
@@ -258,7 +291,7 @@ def test_get_latest_archive_with_remote_path_calls_borg_with_remote_path_flags()
 
 
 
 
 def test_get_latest_archive_with_umask_calls_borg_with_umask_flags():
 def test_get_latest_archive_with_umask_calls_borg_with_umask_flags():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('umask', '077').and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('umask', '077').and_return(
@@ -316,7 +349,7 @@ def test_get_latest_archive_without_archives_raises():
 
 
 
 
 def test_get_latest_archive_with_log_json_calls_borg_with_log_json_flag():
 def test_get_latest_archive_with_log_json_calls_borg_with_log_json_flag():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('log-json', True).and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('log-json', True).and_return(
@@ -348,7 +381,7 @@ def test_get_latest_archive_with_log_json_calls_borg_with_log_json_flag():
 
 
 
 
 def test_get_latest_archive_with_lock_wait_calls_borg_with_lock_wait_flags():
 def test_get_latest_archive_with_lock_wait_calls_borg_with_lock_wait_flags():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 'okay').and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 'okay').and_return(
@@ -380,7 +413,7 @@ def test_get_latest_archive_with_lock_wait_calls_borg_with_lock_wait_flags():
 
 
 
 
 def test_get_latest_archive_with_consider_checkpoints_calls_borg_with_consider_checkpoints_flag():
 def test_get_latest_archive_with_consider_checkpoints_calls_borg_with_consider_checkpoints_flag():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args(
     flexmock(module.flags).should_receive('make_flags').with_args(
@@ -413,7 +446,7 @@ def test_get_latest_archive_with_consider_checkpoints_calls_borg_with_consider_c
 
 
 
 
 def test_get_latest_archive_with_consider_checkpoints_and_feature_available_calls_borg_without_consider_checkpoints_flag():
 def test_get_latest_archive_with_consider_checkpoints_and_feature_available_calls_borg_without_consider_checkpoints_flag():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args(
     flexmock(module.flags).should_receive('make_flags').with_args(
@@ -446,7 +479,7 @@ def test_get_latest_archive_with_consider_checkpoints_and_feature_available_call
 
 
 
 
 def test_get_latest_archive_calls_borg_with_working_directory():
 def test_get_latest_archive_calls_borg_with_working_directory():
-    expected_archive = {'name': 'archive-name'}
+    expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').and_return(())
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
     flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(