Repository: borgbackup/borg

Issue:
borg recreate --timestamp is a no-op
## Have you checked borgbackup docs, FAQ, and open Github issues?

Yes

## Is this a BUG / ISSUE report or a QUESTION?

BUG (either docs or borg-recreate)

## System information. For client/server mode post info for both machines.

#### Your borg version (borg -V).

```
$ borg -V
borg 1.1.10
```

#### Operating system (distribution) and version.

4.19.0-5-amd64, Debian testing

#### Hardware / network configuration, and filesystems used.

tmpfs

#### How much data is handled by borg?

few MB

#### Full borg commandline that lead to the problem (leave away excludes and passwords)

```
$ borg recreate --timestamp '2019-01-01T00:00:00' --comment 're-created for fun' testrepo::archive1
```

## Describe the problem you're observing.

#### Can you reproduce the problem? If so, describe how. If not, describe troubleshooting steps you took before opening the issue.

Yes, 100%. Assuming you have some stuff to play with in ``./testcontent``.

```console
$ borg init -e none testrepo
$ borg create testrepo::archive1 testcontent
$ borg info testrepo::archive1
Archive name: archive1
Archive fingerprint: fc4f5feb66c314073d124acd10c856b153f077e5865fd655b23423d21b6227ac
Comment:
Hostname: sinistra
Username: horazont
Time (start): Sun, 2019-09-01 17:02:18
Time (end): Sun, 2019-09-01 17:02:18
Duration: 0.03 seconds
Number of files: 2
Command line: /usr/bin/borg create testrepo::archive1 testcontent
Utilization of maximum supported archive size: 0%
------------------------------------------------------------------------------
                       Original size      Compressed size    Deduplicated size
This archive:                3.75 MB              3.76 MB              3.76 MB
All archives:                3.75 MB              3.76 MB              3.76 MB

                       Unique chunks         Total chunks
Chunk index:                       5                    5
$ borg recreate --timestamp '2019-01-01T00:00:00' --comment 're-created for fun' 
recreate is an experimental feature.
Type 'YES' if you understand this and want to continue: YES
$ borg info testrepo::archive1
Archive name: archive1
Archive fingerprint: f317eeb7a04e2fa1f06b3d0e3aa919042cb6730fc1d913d348f08e768d800314
Comment: re-created for fun
Hostname: sinistra
Username: horazont
Time (start): Sun, 2019-09-01 17:02:18
Time (end): Sun, 2019-09-01 17:02:18
Duration: 0.03 seconds
Number of files: 2
Command line: /usr/bin/borg create testrepo::archive1 testcontent
Utilization of maximum supported archive size: 0%
------------------------------------------------------------------------------
                       Original size      Compressed size    Deduplicated size
This archive:                3.75 MB              3.76 MB              3.76 MB
All archives:                3.75 MB              3.76 MB              3.76 MB

                       Unique chunks         Total chunks
Chunk index:                       5                    5
```

I expect the timestamp in the second borg info call to be changed after borg recreate, but that is not happening.

Bonus:
```console
$ borg recreate --timestamp '2019-01-01T00:00:00' testrepo::archive1
recreate is an experimental feature.
Type 'YES' if you understand this and want to continue: YES
Nothing to do. Archive was not processed.
Specify at least one pattern, PATH, --comment, re-compression or re-chunking option.
```

It appears the code knows about this, too.

I noticed that args.timestamp is not used at all here: https://github.com/borgbackup/borg/blob/master/src/borg/archiver.py#L1538


Discussion:
I guess the behavior expected is the same as for `borg create`?

I'll handle it :)

Resolving patch:
diff --git a/src/borg/archive.py b/src/borg/archive.py
--- a/src/borg/archive.py
+++ b/src/borg/archive.py
@@ -1852,7 +1852,7 @@ def __init__(self, repository, manifest, key, cache, matcher,
                  exclude_caches=False, exclude_if_present=None, keep_exclude_tags=False,
                  chunker_params=None, compression=None, recompress=False, always_recompress=False,
                  dry_run=False, stats=False, progress=False, file_status_printer=None,
-                 checkpoint_interval=1800):
+                 timestamp=None, checkpoint_interval=1800):
         self.repository = repository
         self.key = key
         self.manifest = manifest
@@ -1872,6 +1872,7 @@ def __init__(self, repository, manifest, key, cache, matcher,
         self.compression = compression or CompressionSpec('none')
         self.seen_chunks = set()
 
+        self.timestamp = timestamp
         self.dry_run = dry_run
         self.stats = stats
         self.progress = progress
@@ -1976,18 +1977,33 @@ def save(self, archive, target, comment=None, replace_original=True):
             return
         if comment is None:
             comment = archive.metadata.get('comment', '')
-        target.save(comment=comment, stats=target.stats, additional_metadata={
-            # keep some metadata as in original archive:
-            'time': archive.metadata.time,
-            'time_end': archive.metadata.get('time_end') or archive.metadata.time,
-            'cmdline': archive.metadata.cmdline,
-            # but also remember recreate metadata:
-            'recreate_cmdline': sys.argv,
-        })
+
+        # Keep for the statistics if necessary
+        if self.stats:
+            _start = target.start
+
+        if self.timestamp is None:
+            additional_metadata = {
+                'time': archive.metadata.time,
+                'time_end': archive.metadata.get('time_end') or archive.metadata.time,
+                'cmdline': archive.metadata.cmdline,
+                # but also remember recreate metadata:
+                'recreate_cmdline': sys.argv,
+            }
+        else:
+            additional_metadata = {
+                'cmdline': archive.metadata.cmdline,
+                # but also remember recreate metadata:
+                'recreate_cmdline': sys.argv,
+            }
+
+        target.save(comment=comment, timestamp=self.timestamp,
+                    stats=target.stats, additional_metadata=additional_metadata)
         if replace_original:
             archive.delete(Statistics(), progress=self.progress)
             target.rename(archive.name)
         if self.stats:
+            target.start = _start
             target.end = datetime.utcnow()
             log_multi(DASHES,
                       str(target),
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -1553,7 +1553,7 @@ def do_recreate(self, args, repository, manifest, key, cache):
                                      progress=args.progress, stats=args.stats,
                                      file_status_printer=self.print_file_status,
                                      checkpoint_interval=args.checkpoint_interval,
-                                     dry_run=args.dry_run)
+                                     dry_run=args.dry_run, timestamp=args.timestamp)
 
         if args.location.archive:
             name = args.location.archive
diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py
--- a/src/borg/testsuite/archiver.py
+++ b/src/borg/testsuite/archiver.py
@@ -2551,6 +2551,17 @@ def test_recreate_recompress(self):
         assert int(csize) < int(size)
         assert sha256_before == sha256_after
 
+    def test_recreate_timestamp(self):
+        self.create_test_files()
+        self.cmd('init', '--encryption=repokey', self.repository_location)
+        archive = self.repository_location + '::test0'
+        self.cmd('create', archive, 'input')
+        self.cmd('recreate', '--timestamp', "1970-01-02T00:00:00", '--comment',
+                 'test', archive)
+        info = self.cmd('info', archive).splitlines()
+        assert any([re.search(r'Time \(start\).+ 1970-01-02', item) for item in info])
+        assert any([re.search(r'Time \(end\).+ 1970-01-02', item) for item in info])
+
     def test_recreate_dry_run(self):
         self.create_regular_file('compressible', size=10000)
         self.cmd('init', '--encryption=repokey', self.repository_location)


Test patch: