mirror of
https://github.com/restic/restic.git
synced 2026-05-28 04:35:41 -04:00
Merge 34a5495ce8 into f000da3b35
This commit is contained in:
commit
daf6bdb798
3 changed files with 236 additions and 13 deletions
5
changelog/unreleased/pull-5677
Normal file
5
changelog/unreleased/pull-5677
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Enhancement: snapshots: Add --sort-by-time option to sort snapshots by time.
|
||||
|
||||
Restic now supports sorting snapshots by timestamp with the new `--sort-by-time` option. When used with the `snapshots` command, it lets you list snapshots in ascending (asc) or descending (desc) time order, with ascending order as the default.
|
||||
|
||||
https://github.com/restic/restic/pull/5677
|
||||
|
|
@ -51,10 +51,11 @@ Exit status is 12 if the password is incorrect.
|
|||
// SnapshotOptions bundles all options for the snapshots command.
|
||||
type SnapshotOptions struct {
|
||||
data.SnapshotFilter
|
||||
Compact bool
|
||||
Last bool // This option should be removed in favour of Latest.
|
||||
Latest int
|
||||
GroupBy data.SnapshotGroupByOptions
|
||||
Compact bool
|
||||
Last bool // This option should be removed in favour of Latest.
|
||||
Latest int
|
||||
GroupBy data.SnapshotGroupByOptions
|
||||
SortByTime string
|
||||
}
|
||||
|
||||
func (opts *SnapshotOptions) AddFlags(f *pflag.FlagSet) {
|
||||
|
|
@ -68,6 +69,7 @@ func (opts *SnapshotOptions) AddFlags(f *pflag.FlagSet) {
|
|||
}
|
||||
f.IntVar(&opts.Latest, "latest", 0, "only show the last `n` snapshots for each host and path")
|
||||
f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma")
|
||||
f.StringVar(&opts.SortByTime, "sort-by-time", "asc", "sort snapshots by time: asc (ascending, oldest first) or desc (descending, newest first) (default: asc)")
|
||||
}
|
||||
|
||||
func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
|
|
@ -78,6 +80,17 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Option
|
|||
}
|
||||
defer unlock()
|
||||
|
||||
// Set default sort-by-time if not specified
|
||||
if opts.SortByTime == "" {
|
||||
opts.SortByTime = "asc"
|
||||
}
|
||||
|
||||
// Validate sort-by-time option
|
||||
sortByTime := strings.ToLower(opts.SortByTime)
|
||||
if sortByTime != "asc" && sortByTime != "desc" {
|
||||
return fmt.Errorf("invalid --sort-by-time value: %q (must be 'asc' or 'desc')", opts.SortByTime)
|
||||
}
|
||||
|
||||
var snapshots data.Snapshots
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) {
|
||||
snapshots = append(snapshots, sn)
|
||||
|
|
@ -107,7 +120,7 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Option
|
|||
}
|
||||
|
||||
if gopts.JSON {
|
||||
err := printSnapshotGroupJSON(gopts.Term.OutputWriter(), snapshotGroups, grouped)
|
||||
err := printSnapshotGroupJSON(gopts.Term.OutputWriter(), snapshotGroups, grouped, sortByTime)
|
||||
if err != nil {
|
||||
printer.E("error printing snapshots: %v", err)
|
||||
}
|
||||
|
|
@ -125,7 +138,7 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Option
|
|||
return err
|
||||
}
|
||||
}
|
||||
err := PrintSnapshots(gopts.Term.OutputWriter(), list, nil, opts.Compact)
|
||||
err := printSnapshotsWithSort(gopts.Term.OutputWriter(), list, nil, opts.Compact, sortByTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -148,6 +161,11 @@ func filterLatestSnapshotsInGroup(list data.Snapshots, limit int) data.Snapshots
|
|||
|
||||
// PrintSnapshots prints a text table of the snapshots in list to stdout.
|
||||
func PrintSnapshots(stdout io.Writer, list data.Snapshots, reasons []data.KeepReason, compact bool) error {
|
||||
return printSnapshotsWithSort(stdout, list, reasons, compact, "asc")
|
||||
}
|
||||
|
||||
// printSnapshotsWithSort prints a text table of the snapshots in list to stdout with custom sorting.
|
||||
func printSnapshotsWithSort(stdout io.Writer, list data.Snapshots, reasons []data.KeepReason, compact bool, sortByTime string) error {
|
||||
// keep the reasons a snasphot is being kept in a map, so that it doesn't
|
||||
// get lost when the list of snapshots is sorted
|
||||
keepReasons := make(map[restic.ID]data.KeepReason, len(reasons))
|
||||
|
|
@ -163,10 +181,17 @@ func PrintSnapshots(stdout io.Writer, list data.Snapshots, reasons []data.KeepRe
|
|||
hasSize = hasSize || (sn.Summary != nil)
|
||||
}
|
||||
|
||||
// always sort the snapshots so that the newer ones are listed last
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
return list[i].Time.Before(list[j].Time)
|
||||
})
|
||||
// Sort the snapshots based on sortByTime option
|
||||
if sortByTime == "desc" {
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
return list[i].Time.After(list[j].Time)
|
||||
})
|
||||
} else {
|
||||
// default: asc - oldest first, newest last
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
return list[i].Time.Before(list[j].Time)
|
||||
})
|
||||
}
|
||||
|
||||
// Determine the max widths for host and tag.
|
||||
maxHost, maxTag := 10, 6
|
||||
|
|
@ -320,7 +345,7 @@ type SnapshotGroup struct {
|
|||
}
|
||||
|
||||
// printSnapshotGroupJSON writes the JSON representation of list to stdout.
|
||||
func printSnapshotGroupJSON(stdout io.Writer, snGroups map[string]data.Snapshots, grouped bool) error {
|
||||
func printSnapshotGroupJSON(stdout io.Writer, snGroups map[string]data.Snapshots, grouped bool, sortByTime string) error {
|
||||
if grouped {
|
||||
snapshotGroups := []SnapshotGroup{}
|
||||
|
||||
|
|
@ -343,6 +368,8 @@ func printSnapshotGroupJSON(stdout io.Writer, snGroups map[string]data.Snapshots
|
|||
snapshots = append(snapshots, k)
|
||||
}
|
||||
|
||||
sortSnapshotsByTime(snapshots, sortByTime)
|
||||
|
||||
group := SnapshotGroup{
|
||||
GroupKey: key,
|
||||
Snapshots: snapshots,
|
||||
|
|
@ -367,5 +394,22 @@ func printSnapshotGroupJSON(stdout io.Writer, snGroups map[string]data.Snapshots
|
|||
}
|
||||
}
|
||||
|
||||
sortSnapshotsByTime(snapshots, sortByTime)
|
||||
|
||||
return json.NewEncoder(stdout).Encode(snapshots)
|
||||
}
|
||||
|
||||
// sortSnapshotsByTime sorts the snapshots slice in place according to sortByTime.
|
||||
// `sortByTime` can be "asc" or "desc".
|
||||
func sortSnapshotsByTime(snapshots []Snapshot, sortByTime string) {
|
||||
if sortByTime == "desc" {
|
||||
sort.SliceStable(snapshots, func(i, j int) bool {
|
||||
return snapshots[i].Time.After(snapshots[j].Time)
|
||||
})
|
||||
} else {
|
||||
// default: asc - oldest first, newest last
|
||||
sort.SliceStable(snapshots, func(i, j int) bool {
|
||||
return snapshots[i].Time.Before(snapshots[j].Time)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,193 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
// Note: All test functions starting with "TestSnapshots", to run all the tests in this file:
|
||||
// go test -v -run TestSnapshots ./cmd/restic/...
|
||||
|
||||
// Regression test for #2979: no snapshots should print as [], not null.
|
||||
func TestEmptySnapshotGroupJSON(t *testing.T) {
|
||||
func TestSnapshotsEmptySnapshotGroupJSON(t *testing.T) {
|
||||
for _, grouped := range []bool{false, true} {
|
||||
var w strings.Builder
|
||||
err := printSnapshotGroupJSON(&w, nil, grouped)
|
||||
err := printSnapshotGroupJSON(&w, nil, grouped, "asc")
|
||||
rtest.OK(t, err)
|
||||
|
||||
rtest.Equals(t, "[]", strings.TrimSpace(w.String()))
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotsSortByTimeAsc verifies that snapshots are sorted in ascending order (oldest first).
|
||||
func TestSnapshotsSortByTimeAsc(t *testing.T) {
|
||||
// Create test snapshots with different times
|
||||
now := time.Now()
|
||||
snapshots := []Snapshot{
|
||||
{
|
||||
Snapshot: &data.Snapshot{Time: now.Add(2 * time.Hour)},
|
||||
ID: &restic.ID{},
|
||||
ShortID: "snap3",
|
||||
},
|
||||
{
|
||||
Snapshot: &data.Snapshot{Time: now},
|
||||
ID: &restic.ID{},
|
||||
ShortID: "snap1",
|
||||
},
|
||||
{
|
||||
Snapshot: &data.Snapshot{Time: now.Add(1 * time.Hour)},
|
||||
ID: &restic.ID{},
|
||||
ShortID: "snap2",
|
||||
},
|
||||
}
|
||||
|
||||
// Sort in ascending order
|
||||
sortSnapshotsByTime(snapshots, "asc")
|
||||
|
||||
// Verify snapshots are sorted oldest first
|
||||
rtest.Equals(t, "snap1", snapshots[0].ShortID)
|
||||
rtest.Equals(t, "snap2", snapshots[1].ShortID)
|
||||
rtest.Equals(t, "snap3", snapshots[2].ShortID)
|
||||
}
|
||||
|
||||
// TestSnapshotsSortByTimeDesc verifies that snapshots are sorted in descending order (newest first).
|
||||
func TestSnapshotsSortByTimeDesc(t *testing.T) {
|
||||
// Create test snapshots with different times
|
||||
now := time.Now()
|
||||
snapshots := []Snapshot{
|
||||
{
|
||||
Snapshot: &data.Snapshot{Time: now},
|
||||
ID: &restic.ID{},
|
||||
ShortID: "snap1",
|
||||
},
|
||||
{
|
||||
Snapshot: &data.Snapshot{Time: now.Add(2 * time.Hour)},
|
||||
ID: &restic.ID{},
|
||||
ShortID: "snap3",
|
||||
},
|
||||
{
|
||||
Snapshot: &data.Snapshot{Time: now.Add(1 * time.Hour)},
|
||||
ID: &restic.ID{},
|
||||
ShortID: "snap2",
|
||||
},
|
||||
}
|
||||
|
||||
// Sort in descending order
|
||||
sortSnapshotsByTime(snapshots, "desc")
|
||||
|
||||
// Verify snapshots are sorted newest first
|
||||
rtest.Equals(t, "snap3", snapshots[0].ShortID)
|
||||
rtest.Equals(t, "snap2", snapshots[1].ShortID)
|
||||
rtest.Equals(t, "snap1", snapshots[2].ShortID)
|
||||
}
|
||||
|
||||
// TestSnapshotsPrintSnapshotGroupJSONSortAsc verifies JSON output is sorted in ascending order.
|
||||
func TestSnapshotsPrintSnapshotGroupJSONSortAsc(t *testing.T) {
|
||||
now := time.Now()
|
||||
snapshotGroups := map[string]data.Snapshots{
|
||||
"{}": {
|
||||
&data.Snapshot{Time: now.Add(2 * time.Hour), Hostname: "host1", Paths: []string{"/data"}},
|
||||
&data.Snapshot{Time: now, Hostname: "host1", Paths: []string{"/data"}},
|
||||
&data.Snapshot{Time: now.Add(1 * time.Hour), Hostname: "host1", Paths: []string{"/data"}},
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := printSnapshotGroupJSON(&buf, snapshotGroups, false, "asc")
|
||||
rtest.OK(t, err)
|
||||
|
||||
var snapshots []Snapshot
|
||||
rtest.OK(t, json.Unmarshal(buf.Bytes(), &snapshots))
|
||||
|
||||
// Verify snapshots are sorted oldest first
|
||||
rtest.Assert(t, len(snapshots) == 3, "expected 3 snapshots, got %d", len(snapshots))
|
||||
rtest.Assert(t, snapshots[0].Time.Before(snapshots[1].Time), "first snapshot should be before second")
|
||||
rtest.Assert(t, snapshots[1].Time.Before(snapshots[2].Time), "second snapshot should be before third")
|
||||
}
|
||||
|
||||
// TestSnapshotsPrintSnapshotGroupJSONSortDesc verifies JSON output is sorted in descending order.
|
||||
func TestSnapshotsPrintSnapshotGroupJSONSortDesc(t *testing.T) {
|
||||
now := time.Now()
|
||||
snapshotGroups := map[string]data.Snapshots{
|
||||
"{}": {
|
||||
&data.Snapshot{Time: now, Hostname: "host1", Paths: []string{"/data"}},
|
||||
&data.Snapshot{Time: now.Add(2 * time.Hour), Hostname: "host1", Paths: []string{"/data"}},
|
||||
&data.Snapshot{Time: now.Add(1 * time.Hour), Hostname: "host1", Paths: []string{"/data"}},
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := printSnapshotGroupJSON(&buf, snapshotGroups, false, "desc")
|
||||
rtest.OK(t, err)
|
||||
|
||||
var snapshots []Snapshot
|
||||
rtest.OK(t, json.Unmarshal(buf.Bytes(), &snapshots))
|
||||
|
||||
// Verify snapshots are sorted newest first
|
||||
rtest.Assert(t, len(snapshots) == 3, "expected 3 snapshots, got %d", len(snapshots))
|
||||
rtest.Assert(t, snapshots[0].Time.After(snapshots[1].Time), "first snapshot should be after second")
|
||||
rtest.Assert(t, snapshots[1].Time.After(snapshots[2].Time), "second snapshot should be after third")
|
||||
}
|
||||
|
||||
// TestSnapshotsPrintSnapshotGroupJSONGroupedSortAsc verifies grouped JSON output is sorted in ascending order.
|
||||
func TestSnapshotsPrintSnapshotGroupJSONGroupedSortAsc(t *testing.T) {
|
||||
now := time.Now()
|
||||
snapshotGroups := map[string]data.Snapshots{
|
||||
`{"hostname":"host1","tags":null,"paths":null}`: {
|
||||
&data.Snapshot{Time: now.Add(2 * time.Hour), Hostname: "host1", Paths: []string{"/data"}},
|
||||
&data.Snapshot{Time: now, Hostname: "host1", Paths: []string{"/data"}},
|
||||
&data.Snapshot{Time: now.Add(1 * time.Hour), Hostname: "host1", Paths: []string{"/data"}},
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := printSnapshotGroupJSON(&buf, snapshotGroups, true, "asc")
|
||||
rtest.OK(t, err)
|
||||
|
||||
var snapshotGroupList []SnapshotGroup
|
||||
rtest.OK(t, json.Unmarshal(buf.Bytes(), &snapshotGroupList))
|
||||
|
||||
// Verify we have one group with 3 snapshots
|
||||
rtest.Assert(t, len(snapshotGroupList) == 1, "expected 1 group, got %d", len(snapshotGroupList))
|
||||
rtest.Assert(t, len(snapshotGroupList[0].Snapshots) == 3, "expected 3 snapshots, got %d", len(snapshotGroupList[0].Snapshots))
|
||||
|
||||
// Verify snapshots are sorted oldest first
|
||||
snapshots := snapshotGroupList[0].Snapshots
|
||||
rtest.Assert(t, snapshots[0].Time.Before(snapshots[1].Time), "first snapshot should be before second")
|
||||
rtest.Assert(t, snapshots[1].Time.Before(snapshots[2].Time), "second snapshot should be before third")
|
||||
}
|
||||
|
||||
// TestSnapshotsPrintSnapshotGroupJSONGroupedSortDesc verifies grouped JSON output is sorted in descending order.
|
||||
func TestSnapshotsPrintSnapshotGroupJSONGroupedSortDesc(t *testing.T) {
|
||||
now := time.Now()
|
||||
snapshotGroups := map[string]data.Snapshots{
|
||||
`{"hostname":"host1","tags":null,"paths":null}`: {
|
||||
&data.Snapshot{Time: now, Hostname: "host1", Paths: []string{"/data"}},
|
||||
&data.Snapshot{Time: now.Add(2 * time.Hour), Hostname: "host1", Paths: []string{"/data"}},
|
||||
&data.Snapshot{Time: now.Add(1 * time.Hour), Hostname: "host1", Paths: []string{"/data"}},
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := printSnapshotGroupJSON(&buf, snapshotGroups, true, "desc")
|
||||
rtest.OK(t, err)
|
||||
|
||||
var snapshotGroupList []SnapshotGroup
|
||||
rtest.OK(t, json.Unmarshal(buf.Bytes(), &snapshotGroupList))
|
||||
|
||||
// Verify we have one group with 3 snapshots
|
||||
rtest.Assert(t, len(snapshotGroupList) == 1, "expected 1 group, got %d", len(snapshotGroupList))
|
||||
rtest.Assert(t, len(snapshotGroupList[0].Snapshots) == 3, "expected 3 snapshots, got %d", len(snapshotGroupList[0].Snapshots))
|
||||
|
||||
// Verify snapshots are sorted newest first
|
||||
snapshots := snapshotGroupList[0].Snapshots
|
||||
rtest.Assert(t, snapshots[0].Time.After(snapshots[1].Time), "first snapshot should be after second")
|
||||
rtest.Assert(t, snapshots[1].Time.After(snapshots[2].Time), "second snapshot should be after third")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue