From 8ef295e2f566b8990d363354b1413e8031d61ade Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sat, 20 Jun 2026 16:32:50 +0200 Subject: [PATCH] snapshots: revert default --lastest behavior to pre-0.19.0 the changed behavior now only applies when using `--group-by`. --- changelog/unreleased/issue-21869 | 11 +++++ cmd/restic/cmd_snapshots.go | 43 +++++++++++++++++++- cmd/restic/cmd_snapshots_integration_test.go | 38 ++++++++++++++--- 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 changelog/unreleased/issue-21869 diff --git a/changelog/unreleased/issue-21869 b/changelog/unreleased/issue-21869 new file mode 100644 index 000000000..774c5e4d1 --- /dev/null +++ b/changelog/unreleased/issue-21869 @@ -0,0 +1,11 @@ +Bugfix: Restore old behavior of `snapshots --latest ` without `--group-by` + +Restic 0.19.0 changed the behavior of `snapshots --latest ` to no longer +group snapshots by default. + +`snapshots --latest ` has been reverted to the old behavior if `--group-by` +is not specified. When specifying `--group-by`, the output is still grouped as +requested like in restic 0.19.0. + +https://github.com/restic/restic/issues/21869 +https://github.com/restic/restic/pull/21875 diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go index 6eee32bf7..015b218cf 100644 --- a/cmd/restic/cmd_snapshots.go +++ b/cmd/restic/cmd_snapshots.go @@ -106,7 +106,11 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Option } if opts.Latest > 0 { - list = filterLatestSnapshotsInGroup(list, opts.Latest) + if grouped { + list = filterLatestSnapshotsInGroup(list, opts.Latest) + } else { + list = filterLatestSnapshots(list, opts.Latest) + } } sort.Sort(sort.Reverse(list)) snapshotGroups[k] = list @@ -140,6 +144,43 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Option return nil } +// filterLastSnapshotsKey is used by FilterLastSnapshots. +type filterLastSnapshotsKey struct { + Hostname string + JoinedPaths string +} + +// newFilterLastSnapshotsKey initializes a filterLastSnapshotsKey from a Snapshot +func newFilterLastSnapshotsKey(sn *data.Snapshot) filterLastSnapshotsKey { + // Shallow slice copy + var paths = make([]string, len(sn.Paths)) + copy(paths, sn.Paths) + sort.Strings(paths) + return filterLastSnapshotsKey{sn.Hostname, strings.Join(paths, "|")} +} + +// filterLatestSnapshots filters a list of snapshots to only return +// the limit last entries for each hostname and path. If the snapshot +// contains multiple paths, they will be joined and treated as one +// item. +func filterLatestSnapshots(list data.Snapshots, limit int) data.Snapshots { + // Sort the snapshots so that the newer ones are listed first + sort.SliceStable(list, func(i, j int) bool { + return list[i].Time.After(list[j].Time) + }) + + var results data.Snapshots + seen := make(map[filterLastSnapshotsKey]int) + for _, sn := range list { + key := newFilterLastSnapshotsKey(sn) + if seen[key] < limit { + seen[key]++ + results = append(results, sn) + } + } + return results +} + // filterLatestSnapshotsInGroup filters a list of snapshots to only return // the `limit` last entries. It is assumed that the snapshot list only contains // one group of snapshots. diff --git a/cmd/restic/cmd_snapshots_integration_test.go b/cmd/restic/cmd_snapshots_integration_test.go index 2401544d7..761d50e96 100644 --- a/cmd/restic/cmd_snapshots_integration_test.go +++ b/cmd/restic/cmd_snapshots_integration_test.go @@ -35,10 +35,7 @@ func testRunSnapshots(t testing.TB, gopts global.Options) (newest *Snapshot, sna return } -func TestSnapshotsGroupByAndLatest(t *testing.T) { - env, cleanup := withTestEnvironment(t) - defer cleanup() - +func snapshotsGroupTestData(t *testing.T, env *testEnvironment, keepPath bool) string { testSetupBackupData(t, env) // two backups on the same host but with different paths opts := BackupOptions{Host: "testhost", TimeStamp: time.Now().Format(time.DateTime)} @@ -46,9 +43,22 @@ func TestSnapshotsGroupByAndLatest(t *testing.T) { // Use later timestamp for second backup opts.TimeStamp = time.Now().Add(time.Second).Format(time.DateTime) snapshotsIDs := loadSnapshotMap(t, env.gopts) - testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata/0"}, opts, env.gopts) + + targets := []string{"testdata/0"} + if keepPath { + targets = []string{"testdata"} + } + testRunBackup(t, filepath.Dir(env.testdata), targets, opts, env.gopts) _, secondSnapshotID := lastSnapshot(snapshotsIDs, loadSnapshotMap(t, env.gopts)) + return secondSnapshotID +} + +func TestSnapshotsGroupByAndLatest(t *testing.T) { + env, cleanup := withTestEnvironment(t) + defer cleanup() + + secondSnapshotID := snapshotsGroupTestData(t, env, false) buf, err := withCaptureStdout(t, env.gopts, func(ctx context.Context, gopts global.Options) error { gopts.JSON = true // only group by host but not path @@ -65,3 +75,21 @@ func TestSnapshotsGroupByAndLatest(t *testing.T) { rtest.Assert(t, len(snapshots[0].Snapshots) == 1, "expected only one latest snapshot, got %d", len(snapshots[0].Snapshots)) rtest.Equals(t, snapshots[0].Snapshots[0].ID.String(), secondSnapshotID, "unexpected snapshot ID") } + +func TestSnapshotsLatest(t *testing.T) { + env, cleanup := withTestEnvironment(t) + defer cleanup() + + secondSnapshotID := snapshotsGroupTestData(t, env, true) + + buf, err := withCaptureStdout(t, env.gopts, func(ctx context.Context, gopts global.Options) error { + gopts.JSON = true + opts := SnapshotOptions{Latest: 1} + return runSnapshots(ctx, opts, gopts, []string{}, gopts.Term) + }) + rtest.OK(t, err) + snapshots := []Snapshot{} + rtest.OK(t, json.Unmarshal(buf.Bytes(), &snapshots)) + rtest.Assert(t, len(snapshots) == 1, "expected only one snapshot, got %d", len(snapshots)) + rtest.Equals(t, snapshots[0].ID.String(), secondSnapshotID, "unexpected snapshot ID") +}