Issue 4433: Ability to define sort order for output of find command (#5184)

The old sorting 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.

---------

Co-authored-by: Michael Eischer <michael.eischer@fau.de>
This commit is contained in:
Winfried Plappert 2025-01-29 20:44:16 +00:00 committed by GitHub
parent d0d887138c
commit c4be05dbc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 59 additions and 3 deletions

View File

@ -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

View File

@ -22,7 +22,9 @@ var cmdFind = &cobra.Command{
Long: ` Long: `
The "find" command searches for files or directories in snapshots stored in the The "find" command searches for files or directories in snapshots stored in the
repo. 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 Example: `restic find config.json
restic find --json "*.yml" "*.json" restic find --json "*.yml" "*.json"
restic find --json --blob 420f620f b46ebe8a ddd38656 restic find --json --blob 420f620f b46ebe8a ddd38656
@ -56,6 +58,7 @@ type FindOptions struct {
CaseInsensitive bool CaseInsensitive bool
ListLong bool ListLong bool
HumanReadable bool HumanReadable bool
Reverse bool
restic.SnapshotFilter restic.SnapshotFilter
} }
@ -73,6 +76,7 @@ func init() {
f.BoolVar(&findOptions.PackID, "pack", false, "pattern is a pack-ID") 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.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.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.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") 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 { 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 { for _, sn := range filteredSnapshots {

View File

@ -28,7 +28,6 @@ func TestFind(t *testing.T) {
opts := BackupOptions{} opts := BackupOptions{}
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
testRunCheck(t, env.gopts)
results := testRunFind(t, false, FindOptions{}, env.gopts, "unexistingfile") results := testRunFind(t, false, FindOptions{}, env.gopts, "unexistingfile")
rtest.Assert(t, len(results) == 0, "unexisting file found in repo (%v)", datafile) 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, 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) 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")
}