mirror of
https://github.com/nix-community/home-manager.git
synced 2026-10-02 11:39:20 +00:00
borgmatic: migrate backups to native settings
Add programs.borgmatic.backups.<name>.settings for native borgmatic YAML and generate each backup file from it. The location, storage, retention, and consistency options and the section extraConfig options become deprecated aliases into settings. Warnings name the affected backup. Legacy configurations without overlapping keys produce the same YAML, and Home Manager symlink exclusions still append to exclude_from. Drop the assertions that rejected backups setting both or neither of sourceDirectories and patterns, so native source patterns and database-only backups work.
This commit is contained in:
@@ -63,6 +63,22 @@ This release has the following notable changes:
|
||||
`services.grobi.settings.rules`. The deprecated aliases preserve list order
|
||||
and definition priorities. The generated JSON is now pretty-printed.
|
||||
|
||||
- Borgmatic backups now use
|
||||
[](#opt-programs.borgmatic.backups._name_.settings) for native YAML settings.
|
||||
Deprecated options warn with the backup name and move to their snake_case
|
||||
keys, such as `retention.keepDaily` to `settings.keep_daily`. Repository
|
||||
strings become `{ path = "..."; }`, and each section's `extraConfig` moves
|
||||
directly into `settings`. `location.excludeHomeManagerSymlinks` remains
|
||||
available.
|
||||
|
||||
Legacy configurations without overlapping keys keep their generated YAML,
|
||||
except that null check frequencies are now omitted. Overlapping
|
||||
`extraConfig` keys no longer take the last section's value. Distinct
|
||||
scalars at the same priority conflict and lists concatenate, so resolve
|
||||
overlaps in `settings`. Backups no longer need exactly one of
|
||||
`sourceDirectories` and `patterns`, which allows native patterns and
|
||||
database-only backups. `repositories` is still required.
|
||||
|
||||
## State Version Changes {#sec-release-26.11-state-version-changes}
|
||||
|
||||
The state version in this release includes the changes below. These
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
time = "2026-09-21T00:00:00+00:00";
|
||||
condition = config.programs.borgmatic.enable;
|
||||
message = ''
|
||||
Borgmatic backups now use `programs.borgmatic.backups.<name>.settings`
|
||||
for native YAML settings. Legacy `location`, `storage`, `retention`, and
|
||||
`consistency` options (except `location.excludeHomeManagerSymlinks`) and
|
||||
the six `extraConfig` options remain as deprecated aliases and warn when
|
||||
used. Move their values into `settings`.
|
||||
'';
|
||||
}
|
||||
@@ -33,20 +33,6 @@ let
|
||||
removeNullValues repo
|
||||
) repos;
|
||||
|
||||
mkRetentionOption =
|
||||
frequency:
|
||||
mkNullableOption {
|
||||
type = types.int;
|
||||
description = "Number of ${frequency} archives to keep. Use -1 for no limit.";
|
||||
example = 3;
|
||||
};
|
||||
|
||||
extraConfigOption = mkOption {
|
||||
inherit (yamlFormat) type;
|
||||
default = { };
|
||||
description = "Extra settings.";
|
||||
};
|
||||
|
||||
repositoryOption = types.submodule {
|
||||
options = {
|
||||
path = mkOption {
|
||||
@@ -88,143 +74,202 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
repositoriesOption = mkOption {
|
||||
type = types.nullOr (types.listOf (types.either types.str repositoryOption));
|
||||
default = [ ];
|
||||
apply = lib.mapNullable cleanRepositories;
|
||||
example = literalExpression ''
|
||||
[
|
||||
{
|
||||
"path" = "ssh://myuser@myrepo.myserver.com/./repo";
|
||||
"label" = "server";
|
||||
}
|
||||
{
|
||||
"path" = "/var/lib/backups/local.borg";
|
||||
"label" = "local";
|
||||
}
|
||||
]
|
||||
'';
|
||||
description = "List of local or remote repositories with paths and optional labels.";
|
||||
};
|
||||
|
||||
checksOption = mkOption {
|
||||
type = types.listOf consistencyCheckModule;
|
||||
default = [ ];
|
||||
description = "Consistency checks to run";
|
||||
example = literalExpression ''
|
||||
[
|
||||
{ name = "repository"; frequency = "2 weeks"; }
|
||||
{ name = "archives"; frequency = "4 weeks"; }
|
||||
{ name = "data"; frequency = "6 weeks"; }
|
||||
{ name = "extract"; frequency = "6 weeks"; }
|
||||
]
|
||||
'';
|
||||
};
|
||||
|
||||
configModule = types.submodule (
|
||||
{ config, ... }:
|
||||
{
|
||||
config.location.extraConfig.exclude_from = lib.mkIf config.location.excludeHomeManagerSymlinks (
|
||||
lib.mkAfter [ (toString hmExcludeFile) ]
|
||||
);
|
||||
options = {
|
||||
location = {
|
||||
sourceDirectories = mkNullableOption {
|
||||
type = types.listOf types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Directories to backup.
|
||||
|
||||
Mutually exclusive with [](#opt-programs.borgmatic.backups._name_.location.patterns).
|
||||
'';
|
||||
example = literalExpression "[config.home.homeDirectory]";
|
||||
};
|
||||
|
||||
patterns = mkNullableOption {
|
||||
type = types.listOf types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Patterns to include/exclude.
|
||||
|
||||
See the output of `borg help patterns` for the syntax. Pattern paths
|
||||
are relative to `/` even when a different recursion root is set.
|
||||
|
||||
Mutually exclusive with [](#opt-programs.borgmatic.backups._name_.location.sourceDirectories).
|
||||
'';
|
||||
example = [
|
||||
"R /home/user"
|
||||
"- home/user/.cache"
|
||||
"- home/user/Downloads"
|
||||
"+ home/user/Videos/Important Video"
|
||||
"- home/user/Videos"
|
||||
];
|
||||
};
|
||||
|
||||
repositories = mkOption {
|
||||
type = types.listOf (types.either types.str repositoryOption);
|
||||
apply = cleanRepositories;
|
||||
example = literalExpression ''
|
||||
[
|
||||
{
|
||||
"path" = "ssh://myuser@myrepo.myserver.com/./repo";
|
||||
"label" = "server";
|
||||
}
|
||||
{
|
||||
"path" = "/var/lib/backups/local.borg";
|
||||
"label" = "local";
|
||||
}
|
||||
]
|
||||
'';
|
||||
description = ''
|
||||
List of local or remote repositories with paths and optional labels.
|
||||
'';
|
||||
};
|
||||
|
||||
excludeHomeManagerSymlinks = mkOption {
|
||||
type = types.bool;
|
||||
description = ''
|
||||
Whether to exclude Home Manager generated symbolic links from
|
||||
the backups. This facilitates restoring the whole home
|
||||
directory when the Nix store doesn't contain the latest
|
||||
Home Manager generation.
|
||||
'';
|
||||
default = false;
|
||||
example = true;
|
||||
};
|
||||
|
||||
extraConfig = extraConfigOption;
|
||||
};
|
||||
|
||||
storage = {
|
||||
encryptionPasscommand = mkNullableOption {
|
||||
type = types.str;
|
||||
description = "Command writing the passphrase to standard output.";
|
||||
example = literalExpression ''"''${pkgs.pass}/bin/pass borg-repo"'';
|
||||
};
|
||||
extraConfig = extraConfigOption;
|
||||
};
|
||||
|
||||
retention = {
|
||||
keepWithin = mkNullableOption {
|
||||
type = types.strMatching "[[:digit:]]+[Hdwmy]";
|
||||
description = "Keep all archives within this time interval.";
|
||||
example = "2d";
|
||||
};
|
||||
|
||||
keepSecondly = mkRetentionOption "secondly";
|
||||
keepMinutely = mkRetentionOption "minutely";
|
||||
keepHourly = mkRetentionOption "hourly";
|
||||
keepDaily = mkRetentionOption "daily";
|
||||
keepWeekly = mkRetentionOption "weekly";
|
||||
keepMonthly = mkRetentionOption "monthly";
|
||||
keepYearly = mkRetentionOption "yearly";
|
||||
|
||||
extraConfig = extraConfigOption;
|
||||
};
|
||||
|
||||
consistency = {
|
||||
checks = mkOption {
|
||||
type = types.listOf consistencyCheckModule;
|
||||
default = [ ];
|
||||
description = "Consistency checks to run";
|
||||
example = literalExpression ''
|
||||
[
|
||||
{
|
||||
name = "repository";
|
||||
frequency = "2 weeks";
|
||||
}
|
||||
{
|
||||
name = "archives";
|
||||
frequency = "4 weeks";
|
||||
}
|
||||
{
|
||||
name = "data";
|
||||
frequency = "6 weeks";
|
||||
}
|
||||
{
|
||||
name = "extract";
|
||||
frequency = "6 weeks";
|
||||
}
|
||||
{ config, options, ... }:
|
||||
let
|
||||
overlays =
|
||||
map
|
||||
(
|
||||
section:
|
||||
lib.hm.deprecations.mkSettingsOverlay {
|
||||
inherit options;
|
||||
from = [
|
||||
section
|
||||
"extraConfig"
|
||||
];
|
||||
'';
|
||||
to = [ "settings" ];
|
||||
}
|
||||
)
|
||||
[
|
||||
"location"
|
||||
"storage"
|
||||
"retention"
|
||||
"consistency"
|
||||
"output"
|
||||
"hooks"
|
||||
];
|
||||
overlayKeys = lib.concatMap (overlay: overlay.keys) overlays;
|
||||
in
|
||||
{
|
||||
imports =
|
||||
lib.hm.deprecations.mkSettingsRenamedOptionModules [ ] [ "settings" ]
|
||||
{
|
||||
priority = 1400;
|
||||
preserveOrder = true;
|
||||
}
|
||||
[
|
||||
{
|
||||
old = [
|
||||
"location"
|
||||
"sourceDirectories"
|
||||
];
|
||||
new = "source_directories";
|
||||
fallback = null;
|
||||
shadowed = lib.elem "source_directories" overlayKeys;
|
||||
}
|
||||
{
|
||||
old = [
|
||||
"location"
|
||||
"patterns"
|
||||
];
|
||||
new = "patterns";
|
||||
fallback = null;
|
||||
shadowed = lib.elem "patterns" overlayKeys;
|
||||
}
|
||||
{
|
||||
old = [
|
||||
"storage"
|
||||
"encryptionPasscommand"
|
||||
];
|
||||
new = "encryption_passcommand";
|
||||
fallback = null;
|
||||
shadowed = lib.elem "encryption_passcommand" overlayKeys;
|
||||
}
|
||||
]
|
||||
++
|
||||
lib.hm.deprecations.mkSettingsRenamedOptionModules [ "retention" ] [ "settings" ]
|
||||
{ priority = 1400; }
|
||||
(
|
||||
map
|
||||
(name: {
|
||||
old = name;
|
||||
new = lib.hm.strings.toSnakeCase name;
|
||||
fallback = null;
|
||||
shadowed = lib.elem (lib.hm.strings.toSnakeCase name) overlayKeys;
|
||||
})
|
||||
[
|
||||
"keepWithin"
|
||||
"keepSecondly"
|
||||
"keepMinutely"
|
||||
"keepHourly"
|
||||
"keepDaily"
|
||||
"keepWeekly"
|
||||
"keepMonthly"
|
||||
"keepYearly"
|
||||
]
|
||||
)
|
||||
++ [
|
||||
(lib.hm.deprecations.mkSettingsChangedOptionModule {
|
||||
from = [
|
||||
"location"
|
||||
"repositories"
|
||||
];
|
||||
to = [ "settings" ];
|
||||
key = "repositories";
|
||||
priority = 1400;
|
||||
applyDefault = value: value != [ ];
|
||||
oldOption = repositoriesOption;
|
||||
shadowed = lib.elem "repositories" overlayKeys;
|
||||
convert = lib.id;
|
||||
})
|
||||
(lib.hm.deprecations.mkSettingsChangedOptionModule {
|
||||
from = [
|
||||
"consistency"
|
||||
"checks"
|
||||
];
|
||||
to = [ "settings" ];
|
||||
key = "checks";
|
||||
priority = 1400;
|
||||
applyDefault = value: value != [ ];
|
||||
oldOption = checksOption;
|
||||
shadowed = lib.elem "checks" overlayKeys;
|
||||
convert = map removeNullValues;
|
||||
})
|
||||
]
|
||||
++ map (overlay: overlay.module) overlays;
|
||||
|
||||
options = {
|
||||
warnings = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
internal = true;
|
||||
visible = false;
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = types.submodule {
|
||||
freeformType = yamlFormat.type;
|
||||
config = {
|
||||
# Legacy backups always wrote an empty checks list, even if checks was unset.
|
||||
checks = lib.mkIf (config.warnings != [ ]) (lib.mkOptionDefault [ ]);
|
||||
exclude_from = lib.mkIf config.location.excludeHomeManagerSymlinks (
|
||||
lib.mkAfter [ (toString hmExcludeFile) ]
|
||||
);
|
||||
};
|
||||
};
|
||||
default = { };
|
||||
description = ''
|
||||
Native borgmatic YAML settings for this backup. Deprecated section
|
||||
options are forwarded into settings and emit migration warnings.
|
||||
Define new configuration here to avoid the legacy aliases.
|
||||
|
||||
extraConfig = extraConfigOption;
|
||||
Home Manager exclusions append to `exclude_from`; use `lib.mkForce`
|
||||
to replace the list.
|
||||
|
||||
Top-level null values are omitted, so enable hooks that take no
|
||||
options, such as `zfs`, `btrfs`, or `lvm`, with `{ }` instead of
|
||||
`null`. Deprecated aliases define settings at normal priority, so
|
||||
a `lib.mkDefault` on the whole settings value is ignored while any
|
||||
alias is set; apply it to individual keys instead.
|
||||
|
||||
See <https://github.com/borgmatic-collective/borgmatic/blob/main/borgmatic/config/schema.yaml>
|
||||
for the available settings.
|
||||
'';
|
||||
};
|
||||
|
||||
output = {
|
||||
extraConfig = extraConfigOption;
|
||||
};
|
||||
|
||||
hooks = {
|
||||
extraConfig = extraConfigOption;
|
||||
location.excludeHomeManagerSymlinks = mkOption {
|
||||
type = types.bool;
|
||||
description = ''
|
||||
Whether to exclude Home Manager generated symbolic links from
|
||||
the backups. This facilitates restoring the whole home
|
||||
directory when the Nix store doesn't contain the latest
|
||||
Home Manager generation.
|
||||
'';
|
||||
default = false;
|
||||
example = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -239,33 +284,6 @@ let
|
||||
'';
|
||||
hmExcludePatterns = lib.concatMapStrings hmExcludePattern hmSymlinks;
|
||||
hmExcludeFile = pkgs.writeText "hm-symlinks.txt" hmExcludePatterns;
|
||||
|
||||
writeConfig =
|
||||
config:
|
||||
lib.generators.toYAML { } (
|
||||
removeNullValues (
|
||||
{
|
||||
source_directories = config.location.sourceDirectories;
|
||||
inherit (config.location) patterns repositories;
|
||||
checks = map removeNullValues config.consistency.checks;
|
||||
encryption_passcommand = config.storage.encryptionPasscommand;
|
||||
keep_within = config.retention.keepWithin;
|
||||
keep_secondly = config.retention.keepSecondly;
|
||||
keep_minutely = config.retention.keepMinutely;
|
||||
keep_hourly = config.retention.keepHourly;
|
||||
keep_daily = config.retention.keepDaily;
|
||||
keep_weekly = config.retention.keepWeekly;
|
||||
keep_monthly = config.retention.keepMonthly;
|
||||
keep_yearly = config.retention.keepYearly;
|
||||
}
|
||||
// config.location.extraConfig
|
||||
// config.storage.extraConfig
|
||||
// config.retention.extraConfig
|
||||
// config.consistency.extraConfig
|
||||
// config.output.extraConfig
|
||||
// config.hooks.extraConfig
|
||||
)
|
||||
);
|
||||
in
|
||||
{
|
||||
meta.maintainers = [ lib.maintainers.DamienCassou ];
|
||||
@@ -284,17 +302,13 @@ in
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
personal = {
|
||||
location = {
|
||||
sourceDirectories = [ "/home/me/personal" ];
|
||||
repositories = [ "ssh://myuser@myserver.com/./personal-repo" ];
|
||||
};
|
||||
personal.settings = {
|
||||
source_directories = [ "/home/me/personal" ];
|
||||
repositories = [ { path = "ssh://myuser@myserver.com/./personal-repo"; } ];
|
||||
};
|
||||
work = {
|
||||
location = {
|
||||
sourceDirectories = [ "/home/me/work" ];
|
||||
repositories = [ "ssh://myuser@myserver.com/./work-repo" ];
|
||||
};
|
||||
work.settings = {
|
||||
source_directories = [ "/home/me/work" ];
|
||||
repositories = [ { path = "ssh://myuser@myserver.com/./work-repo"; } ];
|
||||
};
|
||||
};
|
||||
'';
|
||||
@@ -303,26 +317,26 @@ in
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions =
|
||||
(lib.mapAttrsToList (backup: opts: {
|
||||
assertion = opts.location.sourceDirectories == null || opts.location.patterns == null;
|
||||
message = ''
|
||||
Borgmatic backup configuration "${backup}" cannot specify both 'location.sourceDirectories' and 'location.patterns'.
|
||||
'';
|
||||
}) cfg.backups)
|
||||
++ (lib.mapAttrsToList (backup: opts: {
|
||||
assertion = !(opts.location.sourceDirectories == null && opts.location.patterns == null);
|
||||
message = ''
|
||||
Borgmatic backup configuration "${backup}" must specify one of 'location.sourceDirectories' or 'location.patterns'.
|
||||
'';
|
||||
}) cfg.backups);
|
||||
warnings = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
name: backup:
|
||||
map (warning: "Borgmatic backup `programs.borgmatic.backups.${name}`: ${warning}") backup.warnings
|
||||
) cfg.backups
|
||||
);
|
||||
|
||||
assertions = lib.mapAttrsToList (backup: opts: {
|
||||
assertion = (opts.settings.repositories or null) != null;
|
||||
message = ''
|
||||
Borgmatic backup configuration "${backup}" must specify 'settings.repositories' (or the deprecated 'location.repositories').
|
||||
'';
|
||||
}) cfg.backups;
|
||||
|
||||
xdg.configFile =
|
||||
with lib.attrsets;
|
||||
mapAttrs' (
|
||||
configName: config:
|
||||
nameValuePair ("borgmatic.d/" + configName + ".yaml") {
|
||||
text = writeConfig config;
|
||||
text = lib.generators.toYAML { } (removeNullValues config.settings);
|
||||
}
|
||||
) cfg.backups;
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{ config, realPkgs, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
realPkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
|
||||
@@ -75,6 +80,44 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) {
|
||||
file = ./basic-configuration.nix;
|
||||
entries = [
|
||||
{ from = "hooks.extraConfig"; }
|
||||
{ from = "output.extraConfig"; }
|
||||
{ from = "consistency.extraConfig"; }
|
||||
{ from = "retention.extraConfig"; }
|
||||
{ from = "storage.extraConfig"; }
|
||||
{ from = "location.extraConfig"; }
|
||||
{
|
||||
from = "consistency.checks";
|
||||
to = "settings.checks";
|
||||
changed = true;
|
||||
}
|
||||
{
|
||||
from = "location.repositories";
|
||||
to = "settings.repositories";
|
||||
changed = true;
|
||||
}
|
||||
{
|
||||
from = "retention.keepSecondly";
|
||||
to = "settings.keep_secondly";
|
||||
}
|
||||
{
|
||||
from = "retention.keepWithin";
|
||||
to = "settings.keep_within";
|
||||
}
|
||||
{
|
||||
from = "storage.encryptionPasscommand";
|
||||
to = "settings.encryption_passcommand";
|
||||
}
|
||||
{
|
||||
from = "location.sourceDirectories";
|
||||
to = "settings.source_directories";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml
|
||||
assertFileExists $config_file
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
{ lib, realPkgs, ... }:
|
||||
{
|
||||
programs.borgmatic = {
|
||||
enable = true;
|
||||
backups = {
|
||||
main = {
|
||||
location = {
|
||||
sourceDirectories = [ "/my-stuff-to-backup" ];
|
||||
patterns = [
|
||||
"R /"
|
||||
"+ my-stuff-to-backup"
|
||||
];
|
||||
repositories = [ "/mnt/disk1" ];
|
||||
};
|
||||
};
|
||||
backups.main.location = {
|
||||
sourceDirectories = [ "/my-stuff-to-backup" ];
|
||||
extraConfig.patterns = [ "- **/.cache" ];
|
||||
repositories = [ "/mnt/disk1" ];
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.assertions.expected = [
|
||||
''
|
||||
Borgmatic backup configuration "main" cannot specify both 'location.sourceDirectories' and 'location.patterns'.
|
||||
''
|
||||
];
|
||||
test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) {
|
||||
file = ./both-sourcedirectories-and-patterns.nix;
|
||||
entries = [
|
||||
{ from = "location.extraConfig"; }
|
||||
{
|
||||
from = "location.repositories";
|
||||
to = "settings.repositories";
|
||||
changed = true;
|
||||
}
|
||||
{
|
||||
from = "location.sourceDirectories";
|
||||
to = "settings.source_directories";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml
|
||||
assertFileExists "$config_file"
|
||||
${realPkgs.jq}/bin/jq --exit-status '
|
||||
.source_directories == ["/my-stuff-to-backup"] and
|
||||
.patterns == ["- **/.cache"] and
|
||||
.repositories == [{"path": "/mnt/disk1"}]
|
||||
' "$config_file"
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
borgmatic-program-basic-configuration = ./basic-configuration.nix;
|
||||
borgmatic-program-patterns-configuration = ./patterns-configuration.nix;
|
||||
borgmatic-program-both-sourcedirectories-and-patterns = ./both-sourcedirectories-and-patterns.nix;
|
||||
borgmatic-program-neither-sourcedirectories-nor-patterns = ./neither-sourcedirectories-nor-patterns.nix;
|
||||
borgmatic-program-include-hm-symlinks = ./include-hm-symlinks.nix;
|
||||
borgmatic-program-exclude-hm-symlinks = ./exclude-hm-symlinks.nix;
|
||||
borgmatic-program-exclude-hm-symlinks-nothing-else = ./exclude-hm-symlinks-nothing-else.nix;
|
||||
borgmatic-program-native-settings = ./native-settings.nix;
|
||||
borgmatic-program-legacy-defaults = ./legacy-defaults.nix;
|
||||
borgmatic-program-settings-compatibility = ./settings-compatibility.nix;
|
||||
borgmatic-program-settings-assertions = ./settings-assertions.nix;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{ realPkgs, ... }:
|
||||
{ lib, realPkgs, ... }:
|
||||
|
||||
{
|
||||
programs.borgmatic = {
|
||||
@@ -14,6 +14,21 @@
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) {
|
||||
file = ./exclude-hm-symlinks-nothing-else.nix;
|
||||
entries = [
|
||||
{
|
||||
from = "location.repositories";
|
||||
to = "settings.repositories";
|
||||
changed = true;
|
||||
}
|
||||
{
|
||||
from = "location.sourceDirectories";
|
||||
to = "settings.source_directories";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml
|
||||
assertFileExists $config_file
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{ realPkgs, ... }:
|
||||
{ lib, realPkgs, ... }:
|
||||
|
||||
let
|
||||
|
||||
@@ -22,6 +22,22 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) {
|
||||
file = ./exclude-hm-symlinks.nix;
|
||||
entries = [
|
||||
{ from = "location.extraConfig"; }
|
||||
{
|
||||
from = "location.repositories";
|
||||
to = "settings.repositories";
|
||||
changed = true;
|
||||
}
|
||||
{
|
||||
from = "location.sourceDirectories";
|
||||
to = "settings.source_directories";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml
|
||||
assertFileExists $config_file
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{ realPkgs, ... }:
|
||||
{ lib, realPkgs, ... }:
|
||||
|
||||
let
|
||||
|
||||
@@ -22,6 +22,22 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) {
|
||||
file = ./include-hm-symlinks.nix;
|
||||
entries = [
|
||||
{ from = "location.extraConfig"; }
|
||||
{
|
||||
from = "location.repositories";
|
||||
to = "settings.repositories";
|
||||
changed = true;
|
||||
}
|
||||
{
|
||||
from = "location.sourceDirectories";
|
||||
to = "settings.source_directories";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml
|
||||
assertFileExists $config_file
|
||||
|
||||
134
tests/modules/programs/borgmatic/legacy-defaults.nix
Normal file
134
tests/modules/programs/borgmatic/legacy-defaults.nix
Normal file
@@ -0,0 +1,134 @@
|
||||
{ lib, realPkgs, ... }:
|
||||
let
|
||||
expectWarnings = import ./warnings.nix { inherit lib; };
|
||||
# Option-default definitions merge with the option's own default, so the
|
||||
# warning names the module as well.
|
||||
optionDefaultFiles = [
|
||||
../../../../modules/programs/borgmatic.nix
|
||||
./legacy-defaults.nix
|
||||
];
|
||||
in
|
||||
{
|
||||
programs.borgmatic = {
|
||||
enable = true;
|
||||
package = null;
|
||||
backups = {
|
||||
legacy.location = {
|
||||
sourceDirectories = [ "/source" ];
|
||||
repositories = [ { path = "/repo"; } ];
|
||||
};
|
||||
legacyNull = {
|
||||
location = {
|
||||
sourceDirectories = lib.mkForce null;
|
||||
patterns = [ "R /" ];
|
||||
repositories = [ "/repo" ];
|
||||
};
|
||||
retention.keepDaily = lib.mkForce null;
|
||||
};
|
||||
native.settings = {
|
||||
source_directories = [ "/source" ];
|
||||
repositories = [ { path = "/repo"; } ];
|
||||
};
|
||||
optionDefault = {
|
||||
location = {
|
||||
sourceDirectories = [ "/source" ];
|
||||
repositories = lib.mkOptionDefault [ "/repo" ];
|
||||
};
|
||||
consistency.checks = lib.mkOptionDefault [
|
||||
{
|
||||
name = "data";
|
||||
frequency = "always";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected =
|
||||
expectWarnings {
|
||||
file = ./legacy-defaults.nix;
|
||||
backup = "legacy";
|
||||
entries = [
|
||||
{
|
||||
from = "location.repositories";
|
||||
to = "settings.repositories";
|
||||
changed = true;
|
||||
}
|
||||
{
|
||||
from = "location.sourceDirectories";
|
||||
to = "settings.source_directories";
|
||||
}
|
||||
];
|
||||
}
|
||||
++ expectWarnings {
|
||||
file = ./legacy-defaults.nix;
|
||||
backup = "legacyNull";
|
||||
entries = [
|
||||
{
|
||||
from = "location.repositories";
|
||||
to = "settings.repositories";
|
||||
changed = true;
|
||||
}
|
||||
{
|
||||
from = "retention.keepDaily";
|
||||
to = "settings.keep_daily";
|
||||
}
|
||||
{
|
||||
from = "location.patterns";
|
||||
to = "settings.patterns";
|
||||
}
|
||||
{
|
||||
from = "location.sourceDirectories";
|
||||
to = "settings.source_directories";
|
||||
}
|
||||
];
|
||||
}
|
||||
++ expectWarnings {
|
||||
file = ./legacy-defaults.nix;
|
||||
backup = "optionDefault";
|
||||
entries = [
|
||||
{
|
||||
from = "consistency.checks";
|
||||
to = "settings.checks";
|
||||
changed = true;
|
||||
files = optionDefaultFiles;
|
||||
}
|
||||
{
|
||||
from = "location.repositories";
|
||||
to = "settings.repositories";
|
||||
changed = true;
|
||||
files = optionDefaultFiles;
|
||||
}
|
||||
{
|
||||
from = "location.sourceDirectories";
|
||||
to = "settings.source_directories";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
legacy=$TESTED/home-files/.config/borgmatic.d/legacy.yaml
|
||||
legacyNull=$TESTED/home-files/.config/borgmatic.d/legacyNull.yaml
|
||||
native=$TESTED/home-files/.config/borgmatic.d/native.yaml
|
||||
optionDefault=$TESTED/home-files/.config/borgmatic.d/optionDefault.yaml
|
||||
assertFileExists "$legacy"
|
||||
assertFileExists "$legacyNull"
|
||||
assertFileExists "$native"
|
||||
assertFileExists "$optionDefault"
|
||||
${realPkgs.jq}/bin/jq --exit-status '
|
||||
.source_directories == ["/source"] and
|
||||
.repositories == [{"path": "/repo"}] and
|
||||
.checks == []
|
||||
' "$legacy"
|
||||
${realPkgs.jq}/bin/jq --exit-status '
|
||||
(has("source_directories") | not) and
|
||||
(has("keep_daily") | not) and
|
||||
.patterns == ["R /"]
|
||||
' "$legacyNull"
|
||||
${realPkgs.jq}/bin/jq --exit-status 'has("checks") | not' "$native"
|
||||
${realPkgs.jq}/bin/jq --exit-status '
|
||||
.repositories == [{"path": "/repo"}] and
|
||||
.checks == [{"name": "data", "frequency": "always"}]
|
||||
' "$optionDefault"
|
||||
'';
|
||||
}
|
||||
74
tests/modules/programs/borgmatic/native-settings.nix
Normal file
74
tests/modules/programs/borgmatic/native-settings.nix
Normal file
@@ -0,0 +1,74 @@
|
||||
{ lib, realPkgs, ... }:
|
||||
{
|
||||
programs.borgmatic = {
|
||||
enable = true;
|
||||
backups.databaseOnly.settings = {
|
||||
repositories = [ { path = "/mnt/disk1"; } ];
|
||||
postgresql_databases = [ { name = "app"; } ];
|
||||
};
|
||||
backups.native.settings = lib.mkDefault (
|
||||
lib.mkMerge [
|
||||
{
|
||||
source_directories = [ "/native" ];
|
||||
repositories = [ { path = "/native-repository"; } ];
|
||||
encryption_passcommand = "pass native";
|
||||
checks = [ { name = "repository"; } ];
|
||||
keep_daily = 7;
|
||||
constants.first = "one";
|
||||
}
|
||||
{ constants.second = "two"; }
|
||||
]
|
||||
);
|
||||
backups.hm = {
|
||||
settings = {
|
||||
source_directories = [ "/hm" ];
|
||||
repositories = [ { path = "/hm-repository"; } ];
|
||||
exclude_from = [ "/native-exclude" ];
|
||||
};
|
||||
location.excludeHomeManagerSymlinks = true;
|
||||
};
|
||||
backups.hmForced = {
|
||||
settings = {
|
||||
repositories = [ { path = "/hm-repository"; } ];
|
||||
exclude_from = lib.mkForce [ ];
|
||||
};
|
||||
location.excludeHomeManagerSymlinks = true;
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected = [ ];
|
||||
|
||||
nmt.script = ''
|
||||
config_file=$TESTED/home-files/.config/borgmatic.d/databaseOnly.yaml
|
||||
assertFileExists "$config_file"
|
||||
${realPkgs.jq}/bin/jq --exit-status '
|
||||
(has("source_directories") | not) and
|
||||
(has("patterns") | not) and
|
||||
.postgresql_databases == [{"name": "app"}] and
|
||||
.repositories == [{"path": "/mnt/disk1"}]
|
||||
' "$config_file"
|
||||
|
||||
native=$TESTED/home-files/.config/borgmatic.d/native.yaml
|
||||
jq=${realPkgs.jq}/bin/jq
|
||||
|
||||
assertFileExists $native
|
||||
$jq --exit-status '
|
||||
.source_directories == ["/native"] and
|
||||
.repositories == [{"path": "/native-repository"}] and
|
||||
.encryption_passcommand == "pass native" and
|
||||
.checks == [{"name": "repository"}] and
|
||||
.keep_daily == 7 and
|
||||
.constants == {"first": "one", "second": "two"}
|
||||
' $native
|
||||
hm=$TESTED/home-files/.config/borgmatic.d/hm.yaml
|
||||
assertFileExists $hm
|
||||
$jq --exit-status '
|
||||
(.exclude_from | length) == 2 and .exclude_from[0] == "/native-exclude"
|
||||
' $hm
|
||||
grep --quiet '/home/hm-user/.config/borgmatic.d/hm.yaml' "$($jq --raw-output '.exclude_from[1]' $hm)"
|
||||
|
||||
hm_forced=$TESTED/home-files/.config/borgmatic.d/hmForced.yaml
|
||||
assertFileExists $hm_forced
|
||||
$jq --exit-status '.exclude_from == []' $hm_forced
|
||||
'';
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
programs.borgmatic = {
|
||||
enable = true;
|
||||
backups = {
|
||||
main = {
|
||||
location = {
|
||||
repositories = [ "/mnt/disk1" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.assertions.expected = [
|
||||
''
|
||||
Borgmatic backup configuration "main" must specify one of 'location.sourceDirectories' or 'location.patterns'.
|
||||
''
|
||||
];
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
{ config, realPkgs, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
realPkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
|
||||
@@ -23,6 +28,21 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) {
|
||||
file = ./patterns-configuration.nix;
|
||||
entries = [
|
||||
{
|
||||
from = "location.repositories";
|
||||
to = "settings.repositories";
|
||||
changed = true;
|
||||
}
|
||||
{
|
||||
from = "location.patterns";
|
||||
to = "settings.patterns";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml
|
||||
assertFileExists $config_file
|
||||
|
||||
45
tests/modules/programs/borgmatic/settings-assertions.nix
Normal file
45
tests/modules/programs/borgmatic/settings-assertions.nix
Normal file
@@ -0,0 +1,45 @@
|
||||
{ lib, realPkgs, ... }:
|
||||
{
|
||||
programs.borgmatic = {
|
||||
enable = true;
|
||||
package = null;
|
||||
backups = {
|
||||
missingRepository.settings.source_directories = [ "/source" ];
|
||||
missingLegacyRepository.location.sourceDirectories = [ "/source" ];
|
||||
effective.settings = {
|
||||
repositories = [ { path = "/repo"; } ];
|
||||
postgresql_databases = [ { name = "native"; } ];
|
||||
};
|
||||
};
|
||||
};
|
||||
test.asserts.assertions.expected = [
|
||||
''
|
||||
Borgmatic backup configuration "missingLegacyRepository" must specify 'settings.repositories' (or the deprecated 'location.repositories').
|
||||
''
|
||||
''
|
||||
Borgmatic backup configuration "missingRepository" must specify 'settings.repositories' (or the deprecated 'location.repositories').
|
||||
''
|
||||
];
|
||||
|
||||
test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) {
|
||||
file = ./settings-assertions.nix;
|
||||
backup = "missingLegacyRepository";
|
||||
entries = [
|
||||
{
|
||||
from = "location.sourceDirectories";
|
||||
to = "settings.source_directories";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
nmt.script = ''
|
||||
config_file=$TESTED/home-files/.config/borgmatic.d/effective.yaml
|
||||
assertFileExists "$config_file"
|
||||
${realPkgs.jq}/bin/jq --exit-status '
|
||||
(has("source_directories") | not) and
|
||||
(has("patterns") | not) and
|
||||
.postgresql_databases == [{"name": "native"}] and
|
||||
.repositories == [{"path": "/repo"}]
|
||||
' "$config_file"
|
||||
'';
|
||||
}
|
||||
279
tests/modules/programs/borgmatic/settings-compatibility.nix
Normal file
279
tests/modules/programs/borgmatic/settings-compatibility.nix
Normal file
@@ -0,0 +1,279 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
realPkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
base = {
|
||||
source_directories = [ "/source" ];
|
||||
repositories = [ { path = "/repo"; } ];
|
||||
checks = [ ];
|
||||
};
|
||||
cases = {
|
||||
basic = {
|
||||
repoDefinitions = 2;
|
||||
backup = {
|
||||
location.repositories = [
|
||||
"/repo"
|
||||
{
|
||||
path = "/labeled";
|
||||
label = "remote";
|
||||
}
|
||||
];
|
||||
storage.encryptionPasscommand = "pass borg";
|
||||
retention = {
|
||||
keepWithin = "2d";
|
||||
keepSecondly = 2;
|
||||
keepMinutely = 3;
|
||||
keepHourly = 4;
|
||||
keepDaily = 5;
|
||||
keepWeekly = 6;
|
||||
keepMonthly = 7;
|
||||
keepYearly = 8;
|
||||
};
|
||||
};
|
||||
expected = {
|
||||
repositories = [
|
||||
{ path = "/repo"; }
|
||||
{
|
||||
path = "/labeled";
|
||||
label = "remote";
|
||||
}
|
||||
{ path = "/repo"; }
|
||||
];
|
||||
encryption_passcommand = "pass borg";
|
||||
keep_within = "2d";
|
||||
keep_secondly = 2;
|
||||
keep_minutely = 3;
|
||||
keep_hourly = 4;
|
||||
keep_daily = 5;
|
||||
keep_weekly = 6;
|
||||
keep_monthly = 7;
|
||||
keep_yearly = 8;
|
||||
};
|
||||
warnings = [
|
||||
{
|
||||
from = "retention.keepYearly";
|
||||
to = "settings.keep_yearly";
|
||||
}
|
||||
{
|
||||
from = "retention.keepMonthly";
|
||||
to = "settings.keep_monthly";
|
||||
}
|
||||
{
|
||||
from = "retention.keepWeekly";
|
||||
to = "settings.keep_weekly";
|
||||
}
|
||||
{
|
||||
from = "retention.keepDaily";
|
||||
to = "settings.keep_daily";
|
||||
}
|
||||
{
|
||||
from = "retention.keepHourly";
|
||||
to = "settings.keep_hourly";
|
||||
}
|
||||
{
|
||||
from = "retention.keepMinutely";
|
||||
to = "settings.keep_minutely";
|
||||
}
|
||||
{
|
||||
from = "retention.keepSecondly";
|
||||
to = "settings.keep_secondly";
|
||||
}
|
||||
{
|
||||
from = "retention.keepWithin";
|
||||
to = "settings.keep_within";
|
||||
}
|
||||
{
|
||||
from = "storage.encryptionPasscommand";
|
||||
to = "settings.encryption_passcommand";
|
||||
}
|
||||
];
|
||||
};
|
||||
derivation =
|
||||
let
|
||||
script = realPkgs.writeShellScript "borgmatic-ssh" ''exec ssh "$@"'';
|
||||
in
|
||||
{
|
||||
backup.storage.extraConfig.ssh_command = script;
|
||||
expected.ssh_command = toString script;
|
||||
warnings = [ { from = "storage.extraConfig"; } ];
|
||||
};
|
||||
checks = {
|
||||
checkDefinitions = 2;
|
||||
backup.consistency.checks = [
|
||||
{
|
||||
name = "repository";
|
||||
frequency = "always";
|
||||
}
|
||||
{ name = "data"; }
|
||||
];
|
||||
expected.checks = [
|
||||
{
|
||||
name = "repository";
|
||||
frequency = "always";
|
||||
}
|
||||
{ name = "data"; }
|
||||
];
|
||||
};
|
||||
sections = {
|
||||
backup = {
|
||||
location.extraConfig.one_file_system = true;
|
||||
storage.extraConfig.checkpoint_interval = 200;
|
||||
retention.extraConfig.prefix = "hostname";
|
||||
consistency.extraConfig.archive_name_format = "archive";
|
||||
output.extraConfig.color = false;
|
||||
hooks.extraConfig.before_actions = [ "echo start" ];
|
||||
};
|
||||
expected = {
|
||||
one_file_system = true;
|
||||
checkpoint_interval = 200;
|
||||
prefix = "hostname";
|
||||
archive_name_format = "archive";
|
||||
color = false;
|
||||
before_actions = [ "echo start" ];
|
||||
};
|
||||
warnings = map (section: { from = "${section}.extraConfig"; }) [
|
||||
"hooks"
|
||||
"output"
|
||||
"consistency"
|
||||
"retention"
|
||||
"storage"
|
||||
"location"
|
||||
];
|
||||
};
|
||||
overlayTyped = {
|
||||
backup = {
|
||||
retention.keepDaily = lib.mkForce 30;
|
||||
location.extraConfig.keep_daily = lib.mkDefault 2;
|
||||
};
|
||||
expected.keep_daily = 2;
|
||||
warnings = [
|
||||
{ from = "location.extraConfig"; }
|
||||
{
|
||||
from = "retention.keepDaily";
|
||||
to = "settings.keep_daily";
|
||||
}
|
||||
];
|
||||
};
|
||||
nativePriority = {
|
||||
backup = {
|
||||
retention.keepDaily = 30;
|
||||
settings.keep_daily = 7;
|
||||
};
|
||||
expected.keep_daily = 7;
|
||||
warnings = [
|
||||
{
|
||||
from = "retention.keepDaily";
|
||||
to = "settings.keep_daily";
|
||||
}
|
||||
];
|
||||
};
|
||||
overlayCollision = {
|
||||
backup = {
|
||||
settings.exclude_patterns = [ "- /native" ];
|
||||
location.extraConfig.exclude_patterns = [ "- /legacy" ];
|
||||
};
|
||||
expected.exclude_patterns = [
|
||||
"- /native"
|
||||
"- /legacy"
|
||||
];
|
||||
warnings = [ { from = "location.extraConfig"; } ];
|
||||
};
|
||||
legacyOrder = {
|
||||
checkDefinitions = 3;
|
||||
backup.consistency.checks = lib.mkMerge [
|
||||
(lib.mkAfter [
|
||||
{
|
||||
name = "data";
|
||||
frequency = "always";
|
||||
}
|
||||
])
|
||||
(lib.mkBefore [ { name = "repository"; } ])
|
||||
];
|
||||
expected.checks = [
|
||||
{ name = "repository"; }
|
||||
{
|
||||
name = "data";
|
||||
frequency = "always";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
expected = case: base // case.expected;
|
||||
warningEntries =
|
||||
case:
|
||||
let
|
||||
entries = lib.partition (entry: lib.hasSuffix ".extraConfig" entry.from) (case.warnings or [ ]);
|
||||
in
|
||||
entries.right
|
||||
++ [
|
||||
{
|
||||
from = "consistency.checks";
|
||||
to = "settings.checks";
|
||||
changed = true;
|
||||
definitions = case.checkDefinitions or 1;
|
||||
}
|
||||
{
|
||||
from = "location.repositories";
|
||||
to = "settings.repositories";
|
||||
changed = true;
|
||||
definitions = case.repoDefinitions or 1;
|
||||
}
|
||||
]
|
||||
++ entries.wrong
|
||||
++ [
|
||||
{
|
||||
from = "location.sourceDirectories";
|
||||
to = "settings.source_directories";
|
||||
}
|
||||
];
|
||||
in
|
||||
{
|
||||
programs.borgmatic = {
|
||||
enable = true;
|
||||
package = null;
|
||||
backups = lib.mapAttrs (
|
||||
_: case:
|
||||
lib.mkMerge [
|
||||
{
|
||||
location = {
|
||||
sourceDirectories = [ "/source" ];
|
||||
repositories = [ "/repo" ];
|
||||
};
|
||||
consistency.checks = [ ];
|
||||
}
|
||||
case.backup
|
||||
]
|
||||
) cases;
|
||||
};
|
||||
|
||||
test.asserts.warnings.expected = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
name: case:
|
||||
(import ./warnings.nix { inherit lib; }) {
|
||||
file = ./settings-compatibility.nix;
|
||||
backup = name;
|
||||
entries = warningEntries case;
|
||||
}
|
||||
) cases
|
||||
);
|
||||
|
||||
assertions = lib.mapAttrsToList (name: case: {
|
||||
assertion =
|
||||
builtins.toJSON config.programs.borgmatic.backups.${name}.settings
|
||||
== builtins.toJSON (expected case);
|
||||
message = "Unexpected canonical settings for borgmatic ${name}";
|
||||
}) cases;
|
||||
|
||||
nmt.script = lib.concatStrings (
|
||||
lib.mapAttrsToList (name: case: ''
|
||||
file=$TESTED/home-files/.config/borgmatic.d/${name}.yaml
|
||||
assertFileExists "$file"
|
||||
${realPkgs.jq}/bin/jq --exit-status \
|
||||
--argjson expected ${lib.escapeShellArg (builtins.toJSON (expected case))} \
|
||||
'. == $expected' "$file"
|
||||
'') cases
|
||||
);
|
||||
}
|
||||
21
tests/modules/programs/borgmatic/warnings.nix
Normal file
21
tests/modules/programs/borgmatic/warnings.nix
Normal file
@@ -0,0 +1,21 @@
|
||||
{ lib }:
|
||||
{
|
||||
file,
|
||||
entries,
|
||||
backup ? "main",
|
||||
}:
|
||||
map (
|
||||
entry:
|
||||
let
|
||||
to = entry.to or "settings";
|
||||
files = lib.concatMapStringsSep " and " (path: "`${toString path}'") (
|
||||
entry.files or (lib.replicate (entry.definitions or 1) file)
|
||||
);
|
||||
suffix =
|
||||
if entry.changed or false then
|
||||
"has been changed to `${to}' that has a different type. Please read `${to}' documentation and update your configuration accordingly."
|
||||
else
|
||||
"has been renamed to `${to}'.";
|
||||
in
|
||||
"Borgmatic backup `programs.borgmatic.backups.${backup}`: The option `${entry.from}' defined in ${files} ${suffix}"
|
||||
) entries
|
||||
Reference in New Issue
Block a user