diff --git a/changelog/unreleased/issue-2184 b/changelog/unreleased/issue-2184 new file mode 100644 index 000000000..fe3ff83cf --- /dev/null +++ b/changelog/unreleased/issue-2184 @@ -0,0 +1,8 @@ +Enhancement: Add --json support to forget command + +The forget command now supports the --json argument, outputting the +information about what is (or would-be) kept and removed from the +repository. + +https://github.com/restic/restic/issues/2184 +https://github.com/restic/restic/pull/2185 diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go index bafd540bd..a9b7246be 100644 --- a/cmd/restic/cmd_forget.go +++ b/cmd/restic/cmd_forget.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/json" + "io" "sort" "strings" @@ -182,7 +183,11 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { } if !policy.Empty() { - Verbosef("Applying Policy: %v\n", policy) + if !gopts.JSON { + Verbosef("Applying Policy: %v\n", policy) + } + + var jsonGroups []*ForgetGroup for k, snapshotGroup := range snapshotGroups { var key key @@ -190,36 +195,50 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { return err } + var fg ForgetGroup // Info - Verbosef("snapshots") + if !gopts.JSON { + Verbosef("snapshots") + } var infoStrings []string if GroupByTag { infoStrings = append(infoStrings, "tags ["+strings.Join(key.Tags, ", ")+"]") + fg.Tags = key.Tags } if GroupByHost { infoStrings = append(infoStrings, "host ["+key.Hostname+"]") + fg.Host = key.Hostname } if GroupByPath { infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]") + fg.Paths = key.Paths } - if infoStrings != nil { + if infoStrings != nil && !gopts.JSON { Verbosef(" for (" + strings.Join(infoStrings, ", ") + ")") } - Verbosef(":\n\n") + if !gopts.JSON { + Verbosef(":\n\n") + } keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy) - if len(keep) != 0 && !gopts.Quiet { + if len(keep) != 0 && !gopts.Quiet && !gopts.JSON { Printf("keep %d snapshots:\n", len(keep)) PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact) Printf("\n") } + addJSONSnapshots(&fg.Keep, keep) - if len(remove) != 0 && !gopts.Quiet { + if len(remove) != 0 && !gopts.Quiet && !gopts.JSON { Printf("remove %d snapshots:\n", len(remove)) PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact) Printf("\n") } + addJSONSnapshots(&fg.Remove, remove) + + fg.Reasons = reasons + + jsonGroups = append(jsonGroups, &fg) removeSnapshots += len(remove) @@ -233,6 +252,13 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { } } } + + if gopts.JSON { + err = printJSONForget(gopts.stdout, jsonGroups) + if err != nil { + return err + } + } } if removeSnapshots > 0 && opts.Prune { @@ -244,3 +270,28 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { return nil } + +// ForgetGroup helps to print what is forgotten in JSON. +type ForgetGroup struct { + Tags []string `json:"tags"` + Host string `json:"host"` + Paths []string `json:"paths"` + Keep []Snapshot `json:"keep"` + Remove []Snapshot `json:"remove"` + Reasons []restic.KeepReason `json:"reasons"` +} + +func addJSONSnapshots(js *[]Snapshot, list restic.Snapshots) { + for _, sn := range list { + k := Snapshot{ + Snapshot: sn, + ID: sn.ID(), + ShortID: sn.ID().Str(), + } + *js = append(*js, k) + } +} + +func printJSONForget(stdout io.Writer, forgets []*ForgetGroup) error { + return json.NewEncoder(stdout).Encode(forgets) +} diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index e47000d34..612685f53 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -219,6 +219,35 @@ func testRunForget(t testing.TB, gopts GlobalOptions, args ...string) { rtest.OK(t, runForget(opts, gopts, args)) } +func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) { + buf := bytes.NewBuffer(nil) + oldJSON := gopts.JSON + gopts.stdout = buf + gopts.JSON = true + defer func() { + gopts.stdout = os.Stdout + gopts.JSON = oldJSON + }() + + opts := ForgetOptions{ + DryRun: true, + Last: 1, + } + + rtest.OK(t, runForget(opts, gopts, args)) + + var forgets []*ForgetGroup + rtest.OK(t, json.Unmarshal(buf.Bytes(), &forgets)) + + rtest.Assert(t, len(forgets) == 1, + "Expected 1 snapshot group, got %v", len(forgets)) + rtest.Assert(t, len(forgets[0].Keep) == 1, + "Expected 1 snapshot to be kept, got %v", len(forgets[0].Keep)) + rtest.Assert(t, len(forgets[0].Remove) == 2, + "Expected 2 snapshots to be removed, got %v", len(forgets[0].Remove)) + return +} + func testRunPrune(t testing.TB, gopts GlobalOptions) { rtest.OK(t, runPrune(gopts)) } @@ -1051,6 +1080,7 @@ func TestPrune(t *testing.T) { rtest.Assert(t, len(snapshotIDs) == 3, "expected 3 snapshot, got %v", snapshotIDs) + testRunForgetJSON(t, env.gopts) testRunForget(t, env.gopts, firstSnapshot[0].String()) testRunPrune(t, env.gopts) testRunCheck(t, env.gopts)