Repository: restic/restic (go)

Issue:
Include Restic client version in the snapshot information
Include Restic client version in the snapshot information.
In the output of this command `restic -r /data --verbose -p secret.txt --json snapshots`

I'm using Restic to backup all the devices at home. That includes 3 smartphones, 3 Windows laptops and 2 Linux servers.
Since I want to keep Restic updated, it will be very useful for me to see the Restic client version used for the last snapshot.
After the version is added to the command output, I can make the integration with my Prometheus exporter to see the outdated versions in the Dashboard.
https://github.com/ngosang/restic-exporter#grafana-dashboard

Pull request description:
Add program version to snapshot
<!--
Thank you very much for contributing code or documentation to restic! Please
fill out the following questions to make it easier for us to review your
changes.
-->

What does this PR change? What problem does it solve?
-----------------------------------------------------
Include the program version in the snapshot metadata.
<!--
Describe the changes and their purpose here, as detailed as needed.
-->

Was the change previously discussed in an issue or on the forum?
----------------------------------------------------------------
Fixes #4188
<!--
Link issues and relevant forum posts here.

If this PR resolves an issue on GitHub, use "Closes #1234" so that the issue
is closed automatically when this PR is merged.
-->

Checklist
---------

<!--
You do not need to check all the boxes below all at once. Feel free to take
your time and add more commits. If you're done and ready for review, please
check the last box. Enable a checkbox by replacing [ ] with [x].
-->

- [x] I have read the [contribution guidelines](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#providing-patches).
- [x] I have [enabled maintainer edits](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
- [x] I have added tests for all code changes.
- [x] I have added documentation for relevant changes (in the manual).
- [x] There's a new file in `changelog/unreleased/` that describes the changes for our users (see [template](https://github.com/restic/restic/blob/master/changelog/TEMPLATE)).
- [x] I have run `gofmt` on the code in all commits.
- [x] All commit messages are formatted in the same style as [the other commits in the repo](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#git-commits).
- [x] I'm done! This pull request is ready for review.


Patch:
diff --git a/changelog/unreleased/issue-4188 b/changelog/unreleased/issue-4188
new file mode 100644
index 000000000..dbb26f733
--- /dev/null
+++ b/changelog/unreleased/issue-4188
@@ -0,0 +1,8 @@
+Enhancement: `backup` includes restic version in snapshot metadata
+
+The restic version used backup the snapshot is now included in its metadata.
+The program version is shown when inspecting a snapshot using `restic cat
+snapshot <snapshotID>`  or `restic snapshots --json`.
+
+https://github.com/restic/restic/issues/4188
+https://github.com/restic/restic/pull/4378
diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go
index f12f70e39..d7e899eaf 100644
--- a/cmd/restic/cmd_backup.go
+++ b/cmd/restic/cmd_backup.go
@@ -645,6 +645,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
 		Time:           timeStamp,
 		Hostname:       opts.Host,
 		ParentSnapshot: parentSnapshot,
+		ProgramVersion: "restic " + version,
 	}
 
 	if !gopts.JSON {
diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go
index 3c1cc33d0..98819d797 100644
--- a/internal/archiver/archiver.go
+++ b/internal/archiver/archiver.go
@@ -680,6 +680,7 @@ type SnapshotOptions struct {
 	Excludes       []string
 	Time           time.Time
 	ParentSnapshot *restic.Snapshot
+	ProgramVersion string
 }
 
 // loadParentTree loads a tree referenced by snapshot id. If id is null, nil is returned.
@@ -796,6 +797,7 @@ func (arch *Archiver) Snapshot(ctx context.Context, targets []string, opts Snaps
 		return nil, restic.ID{}, err
 	}
 
+	sn.ProgramVersion = opts.ProgramVersion
 	sn.Excludes = opts.Excludes
 	if opts.ParentSnapshot != nil {
 		sn.Parent = opts.ParentSnapshot.ID()
diff --git a/internal/restic/snapshot.go b/internal/restic/snapshot.go
index 1f6e4534b..13e795ec8 100644
--- a/internal/restic/snapshot.go
+++ b/internal/restic/snapshot.go
@@ -25,6 +25,8 @@ type Snapshot struct {
 	Tags     []string  `json:"tags,omitempty"`
 	Original *ID       `json:"original,omitempty"`
 
+	ProgramVersion string `json:"program_version,omitempty"`
+
 	id *ID // plaintext ID, used during restore
 }
 


Test patch:
diff --git a/cmd/restic/cmd_backup_integration_test.go b/cmd/restic/cmd_backup_integration_test.go
index b81db21e6..fb7bef633 100644
--- a/cmd/restic/cmd_backup_integration_test.go
+++ b/cmd/restic/cmd_backup_integration_test.go
@@ -440,6 +440,22 @@ func TestBackupTags(t *testing.T) {
 		"expected parent to be %v, got %v", parent.ID, newest.Parent)
 }
 
+func TestBackupProgramVersion(t *testing.T) {
+	env, cleanup := withTestEnvironment(t)
+	defer cleanup()
+
+	testSetupBackupData(t, env)
+	testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
+	newest, _ := testRunSnapshots(t, env.gopts)
+
+	if newest == nil {
+		t.Fatal("expected a backup, got nil")
+	}
+	resticVersion := "restic " + version
+	rtest.Assert(t, newest.ProgramVersion == resticVersion,
+		"expected %v, got %v", resticVersion, newest.ProgramVersion)
+}
+
 func TestQuietBackup(t *testing.T) {
 	env, cleanup := withTestEnvironment(t)
 	defer cleanup()