tag: output the original ID and new snapshotID (#5144)

* tag: output the original ID and new snapshotID

tag: print changed snapshot information immediately

* print changed snapshot immediately after it has been saved
* add message type to the changedSnapshot
* add a summary type which will share the JSON output of the numer of changed snapshots
* updated verbosity of the changed snapshot in text mode to only work when verbosity > 2
* also use the terminal status printer for a standard handling for stdout messages
This commit is contained in:
Srigovind Nayak 2025-01-14 23:27:47 +05:30 committed by GitHub
parent e6f9cfb8c8
commit 115ecb3c92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 82 additions and 13 deletions

View File

@ -0,0 +1,8 @@
Enhancement: Restic tag command returns the modified snapshot information
Restic `tag` command now returns the modified snapshot information in the
output. Added `--json` option to the command to get the output in JSON format
for scripting access.
https://github.com/restic/restic/issues/5137
https://github.com/restic/restic/pull/5144

View File

@ -9,6 +9,8 @@ import (
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/termstatus"
) )
var cmdTag = &cobra.Command{ var cmdTag = &cobra.Command{
@ -34,7 +36,9 @@ Exit status is 12 if the password is incorrect.
GroupID: cmdGroupDefault, GroupID: cmdGroupDefault,
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runTag(cmd.Context(), tagOptions, globalOptions, args) term, cancel := setupTermstatus()
defer cancel()
return runTag(cmd.Context(), tagOptions, globalOptions, term, args)
}, },
} }
@ -58,7 +62,18 @@ func init() {
initMultiSnapshotFilter(tagFlags, &tagOptions.SnapshotFilter, true) initMultiSnapshotFilter(tagFlags, &tagOptions.SnapshotFilter, true)
} }
func changeTags(ctx context.Context, repo *repository.Repository, sn *restic.Snapshot, setTags, addTags, removeTags []string) (bool, error) { type changedSnapshot struct {
MessageType string `json:"message_type"` // changed
OldSnapshotID restic.ID `json:"old_snapshot_id"`
NewSnapshotID restic.ID `json:"new_snapshot_id"`
}
type changedSnapshotsSummary struct {
MessageType string `json:"message_type"` // summary
ChangedSnapshots int `json:"changed_snapshots"`
}
func changeTags(ctx context.Context, repo *repository.Repository, sn *restic.Snapshot, setTags, addTags, removeTags []string, printFunc func(changedSnapshot)) (bool, error) {
var changed bool var changed bool
if len(setTags) != 0 { if len(setTags) != 0 {
@ -87,7 +102,7 @@ func changeTags(ctx context.Context, repo *repository.Repository, sn *restic.Sna
return false, err return false, err
} }
debug.Log("new snapshot saved as %v", id) debug.Log("old snapshot %v saved as a new snapshot %v", sn.ID(), id)
// Remove the old snapshot. // Remove the old snapshot.
if err = repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, *sn.ID()); err != nil { if err = repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, *sn.ID()); err != nil {
@ -95,11 +110,13 @@ func changeTags(ctx context.Context, repo *repository.Repository, sn *restic.Sna
} }
debug.Log("old snapshot %v removed", sn.ID()) debug.Log("old snapshot %v removed", sn.ID())
printFunc(changedSnapshot{MessageType: "changed", OldSnapshotID: *sn.ID(), NewSnapshotID: id})
} }
return changed, nil return changed, nil
} }
func runTag(ctx context.Context, opts TagOptions, gopts GlobalOptions, args []string) error { func runTag(ctx context.Context, opts TagOptions, gopts GlobalOptions, term *termstatus.Terminal, args []string) error {
if len(opts.SetTags) == 0 && len(opts.AddTags) == 0 && len(opts.RemoveTags) == 0 { if len(opts.SetTags) == 0 && len(opts.AddTags) == 0 && len(opts.RemoveTags) == 0 {
return errors.Fatal("nothing to do!") return errors.Fatal("nothing to do!")
} }
@ -114,24 +131,44 @@ func runTag(ctx context.Context, opts TagOptions, gopts GlobalOptions, args []st
} }
defer unlock() defer unlock()
changeCnt := 0 printFunc := func(c changedSnapshot) {
Verboseff("old snapshot ID: %v -> new snapshot ID: %v\n", c.OldSnapshotID, c.NewSnapshotID)
}
summary := changedSnapshotsSummary{MessageType: "summary", ChangedSnapshots: 0}
printSummary := func(c changedSnapshotsSummary) {
if c.ChangedSnapshots == 0 {
Verbosef("no snapshots were modified\n")
} else {
Verbosef("modified %v snapshots\n", c.ChangedSnapshots)
}
}
if gopts.JSON {
printFunc = func(c changedSnapshot) {
term.Print(ui.ToJSONString(c))
}
printSummary = func(c changedSnapshotsSummary) {
term.Print(ui.ToJSONString(c))
}
}
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args) { for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args) {
changed, err := changeTags(ctx, repo, sn, opts.SetTags.Flatten(), opts.AddTags.Flatten(), opts.RemoveTags.Flatten()) changed, err := changeTags(ctx, repo, sn, opts.SetTags.Flatten(), opts.AddTags.Flatten(), opts.RemoveTags.Flatten(), printFunc)
if err != nil { if err != nil {
Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", sn.ID(), err) Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", sn.ID(), err)
continue continue
} }
if changed { if changed {
changeCnt++ summary.ChangedSnapshots++
} }
} }
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
} }
if changeCnt == 0 {
Verbosef("no snapshots were modified\n") printSummary(summary)
} else {
Verbosef("modified tags on %v snapshots\n", changeCnt)
}
return nil return nil
} }

View File

@ -9,7 +9,7 @@ import (
) )
func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) { func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) {
rtest.OK(t, runTag(context.TODO(), opts, gopts, []string{})) rtest.OK(t, runTag(context.TODO(), opts, gopts, nil, []string{}))
} }
// nolint: staticcheck // false positive nil pointer dereference check // nolint: staticcheck // false positive nil pointer dereference check

View File

@ -722,6 +722,30 @@ The stats command returns a single JSON object.
| ``compression_space_saving`` | Overall space saving due to compression | | ``compression_space_saving`` | Overall space saving due to compression |
+------------------------------+-----------------------------------------------------+ +------------------------------+-----------------------------------------------------+
tag
---
The ``tag`` command uses the JSON lines format with the following message types.
Changed
^^^^^^^
+--------------------------+-------------------------------------------+
| ``message_type`` | Always "changed" |
+--------------------------+-------------------------------------------+
| ``old_snapshot_id`` | ID of the snapshot before the change |
+--------------------------+-------------------------------------------+
| ``new_snapshot_id`` | ID of the snapshot after the change |
+--------------------------+-------------------------------------------+
Summary
^^^^^^^
+-----------------------------+-------------------------------------------+
| ``message_type`` | Always "summary" |
+-----------------------------+-------------------------------------------+
| ``changed_snapshot_count`` | Total number of changed snapshots |
+-----------------------------+-------------------------------------------+
version version
------- -------