diff --git a/changelog/unreleased/issue-4433 b/changelog/unreleased/issue-4433 new file mode 100644 index 000000000..b6aefbe8e --- /dev/null +++ b/changelog/unreleased/issue-4433 @@ -0,0 +1,9 @@ +Enhancement: Sort `find` output from newest to oldest and add `--reverse` option + +The old output behaviour was to sort snapshots from oldest to newest. +The new sorting order is from newest to oldest. If one wants to revert to the +old behaviour, use the option --reverse. + +https://github.com/restic/restic/issues/4433 +https://github.com/restic/restic/pull/5184 + diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go index 2e06fa00c..2fcef5741 100644 --- a/cmd/restic/cmd_find.go +++ b/cmd/restic/cmd_find.go @@ -22,7 +22,9 @@ var cmdFind = &cobra.Command{ Long: ` The "find" command searches for files or directories in snapshots stored in the repo. -It can also be used to search for restic blobs or trees for troubleshooting.`, +It can also be used to search for restic blobs or trees for troubleshooting. +The default sort option for the snapshots is youngest to oldest. To sort the +output from oldest to youngest specify --reverse.`, Example: `restic find config.json restic find --json "*.yml" "*.json" restic find --json --blob 420f620f b46ebe8a ddd38656 @@ -56,6 +58,7 @@ type FindOptions struct { CaseInsensitive bool ListLong bool HumanReadable bool + Reverse bool restic.SnapshotFilter } @@ -73,6 +76,7 @@ func init() { f.BoolVar(&findOptions.PackID, "pack", false, "pattern is a pack-ID") f.BoolVar(&findOptions.ShowPackID, "show-pack-id", false, "display the pack-ID the blobs belong to (with --blob or --tree)") f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern") + f.BoolVarP(&findOptions.Reverse, "reverse", "R", false, "reverse sort order oldest to newest") f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") f.BoolVar(&findOptions.HumanReadable, "human-readable", false, "print sizes in human readable format") @@ -637,7 +641,10 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args [] } sort.Slice(filteredSnapshots, func(i, j int) bool { - return filteredSnapshots[i].Time.Before(filteredSnapshots[j].Time) + if opts.Reverse { + return filteredSnapshots[i].Time.Before(filteredSnapshots[j].Time) + } + return filteredSnapshots[i].Time.After(filteredSnapshots[j].Time) }) for _, sn := range filteredSnapshots { diff --git a/cmd/restic/cmd_find_integration_test.go b/cmd/restic/cmd_find_integration_test.go index 7e35cb141..95799749a 100644 --- a/cmd/restic/cmd_find_integration_test.go +++ b/cmd/restic/cmd_find_integration_test.go @@ -28,7 +28,6 @@ func TestFind(t *testing.T) { opts := BackupOptions{} testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) - testRunCheck(t, env.gopts) results := testRunFind(t, false, FindOptions{}, env.gopts, "unexistingfile") rtest.Assert(t, len(results) == 0, "unexisting file found in repo (%v)", datafile) @@ -91,3 +90,44 @@ func TestFindJSON(t *testing.T) { rtest.Assert(t, len(matches[0].Matches) == 3, "expected 3 files to match (%v)", matches[0].Matches) rtest.Assert(t, matches[0].Hits == 3, "expected hits to show 3 matches (%v)", datafile) } + +func TestFindSorting(t *testing.T) { + env, cleanup := withTestEnvironment(t) + defer cleanup() + + datafile := testSetupBackupData(t, env) + opts := BackupOptions{} + + // first backup + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) + sn1 := testListSnapshots(t, env.gopts, 1)[0] + + // second backup + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) + snapshots := testListSnapshots(t, env.gopts, 2) + // get id of new snapshot without depending on file order returned by filesystem + sn2 := snapshots[0] + if sn1.Equal(sn2) { + sn2 = snapshots[1] + } + + // first restic find - with default FindOptions{} + results := testRunFind(t, true, FindOptions{}, env.gopts, "testfile") + lines := strings.Split(string(results), "\n") + rtest.Assert(t, len(lines) == 2, "expected two files found in repo (%v), found %d", datafile, len(lines)) + matches := []testMatches{} + rtest.OK(t, json.Unmarshal(results, &matches)) + + // run second restic find with --reverse, sort oldest to newest + resultsReverse := testRunFind(t, true, FindOptions{Reverse: true}, env.gopts, "testfile") + lines = strings.Split(string(resultsReverse), "\n") + rtest.Assert(t, len(lines) == 2, "expected two files found in repo (%v), found %d", datafile, len(lines)) + matchesReverse := []testMatches{} + rtest.OK(t, json.Unmarshal(resultsReverse, &matchesReverse)) + + // compare result sets + rtest.Assert(t, sn1.String() == matchesReverse[0].SnapshotID, "snapshot[0] must match old snapshot") + rtest.Assert(t, sn2.String() == matchesReverse[1].SnapshotID, "snapshot[1] must match new snapshot") + rtest.Assert(t, matches[0].SnapshotID == matchesReverse[1].SnapshotID, "matches should be sorted 1") + rtest.Assert(t, matches[1].SnapshotID == matchesReverse[0].SnapshotID, "matches should be sorted 2") +}