From 90f84e721937c250143800875a246c4a230322f2 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 11 Mar 2026 16:53:19 +0100 Subject: [PATCH 1/2] create: add --hostname/--username, fixes #9402 - Added `--hostname` and `--username` command-line options to `borg create` - Updated Archive to capture and store these explicit values, falling back to system defaults - Added `test_explicit_hostname_and_username` to verify the functionality --- src/borg/archive.py | 11 +++++++--- src/borg/archiver/create_cmd.py | 20 +++++++++++++++++++ .../testsuite/archiver/create_cmd_test.py | 11 ++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index c0ae8978a1..6571b0cf63 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -48,7 +48,8 @@ from .manifest import Manifest from .patterns import PathPrefixPattern, FnmatchPattern, IECommand from .item import Item, ArchiveItem, ItemDiff -from .platform import acl_get, acl_set, set_flags, get_flags, swidth, hostname +from . import platform +from .platform import acl_get, acl_set, set_flags, get_flags, swidth from .remote import RemoteRepository, cache_if_remote from .repository import Repository, NoManifestError from .repoobj import RepoObj @@ -505,6 +506,8 @@ def __init__( log_json=False, iec=False, deleted=False, + hostname=None, + username=None, ): name_is_id = isinstance(name, bytes) if not name_is_id: @@ -523,6 +526,8 @@ def __init__( self.name_in_manifest = name # can differ from .name later (if borg check fixed duplicate archive names) self.comment = None self.tags = None + self.hostname = hostname if hostname is not None else platform.hostname + self.username = username if username is not None else getuser() self.numeric_ids = numeric_ids self.noatime = noatime self.noctime = noctime @@ -693,8 +698,8 @@ def save(self, name=None, comment=None, timestamp=None, stats=None, additional_m "item_ptrs": item_ptrs, # see #1473 "command_line": join_cmd(sys.argv), "cwd": self.cwd, - "hostname": hostname, - "username": getuser(), + "hostname": self.hostname, + "username": self.username, "time": nominal.isoformat(timespec="microseconds"), "start": start.isoformat(timespec="microseconds"), "end": end.isoformat(timespec="microseconds"), diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index 92ea929aab..2843a5821f 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -250,6 +250,8 @@ def create_inner(archive, cache, fso): start=t0, log_json=args.log_json, iec=args.iec, + hostname=args.hostname, + username=args.username, ) metadata_collector = MetadataCollector( noatime=not args.atime, @@ -975,6 +977,24 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): action=Highlander, help="select compression algorithm, see the output of the " '"borg help compression" command for details.', ) + archive_group.add_argument( + "--hostname", + metavar="HOSTNAME", + dest="hostname", + type=str, + default=None, + action=Highlander, + help="explicitly set hostname for the archive", + ) + archive_group.add_argument( + "--username", + metavar="USERNAME", + dest="username", + type=str, + default=None, + action=Highlander, + help="explicitly set username for the archive", + ) subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name") subparser.add_argument( diff --git a/src/borg/testsuite/archiver/create_cmd_test.py b/src/borg/testsuite/archiver/create_cmd_test.py index 5b06b7dfd7..1622b834eb 100644 --- a/src/borg/testsuite/archiver/create_cmd_test.py +++ b/src/borg/testsuite/archiver/create_cmd_test.py @@ -829,6 +829,17 @@ def test_create_json(archivers, request): assert "stats" in archive +def test_explicit_hostname_and_username(archivers, request): + archiver = request.getfixturevalue(archivers) + create_regular_file(archiver.input_path, "file1", size=1024 * 80) + cmd(archiver, "repo-create", RK_ENCRYPTION) + cmd(archiver, "create", "--hostname", "foo_host", "--username", "bar_user", "test", "input") + info = json.loads(cmd(archiver, "info", "--json", "test")) + archive = info["archives"][0] + assert archive["hostname"] == "foo_host" + assert archive["username"] == "bar_user" + + def test_create_topical(archivers, request): archiver = request.getfixturevalue(archivers) create_regular_file(archiver.input_path, "file1", size=1024 * 80) From 5f4e921b9ab62c92908056e2120da9d40e7bc0d3 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 11 Mar 2026 18:18:15 +0100 Subject: [PATCH 2/2] create: add --tags, fixes #9401 - add `--tags TAG [TAG ...]` option to `borg create` to tag newly created archives. - validate the tags exactly like `borg tag` does, including checking that any special tags starting with `@` are known `SPECIAL_TAGS`. - add `test_create_tags` and `test_create_invalid_tags` to ensure proper behavior. --- src/borg/archiver/create_cmd.py | 18 ++++++++++++++ .../testsuite/archiver/create_cmd_test.py | 24 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index 2843a5821f..ecc356e622 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -211,6 +211,14 @@ def create_inner(archive, cache, fso): # do not save the archive if the user ctrl-c-ed. raise Error("Got Ctrl-C / SIGINT.") else: + # deal with tags + if args.tags is not None: + tags = {tag for tag in args.tags if tag} + special = {tag for tag in tags if tag.startswith("@")} + if not special.issubset(SPECIAL_TAGS): + raise Error("Unknown special tags given.") + archive.tags = tags + archive.save(comment=args.comment, timestamp=args.timestamp) args.stats |= args.json if args.stats: @@ -595,6 +603,8 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): The archive will consume almost no disk space for files or parts of files that have already been stored in other archives. + The ``--tags`` option can be used to add a list of tags to the new archive. + The archive name does not need to be unique; you can and should use the same name for a series of archives. The unique archive identifier is its ID (hash), and you can abbreviate the ID as long as it is unique. @@ -995,6 +1005,14 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): action=Highlander, help="explicitly set username for the archive", ) + archive_group.add_argument( + "--tags", + metavar="TAG", + dest="tags", + type=helpers.tag_validator, + nargs="+", + help="add tags to archive (comma-separated or multiple arguments)", + ) subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name") subparser.add_argument( diff --git a/src/borg/testsuite/archiver/create_cmd_test.py b/src/borg/testsuite/archiver/create_cmd_test.py index 1622b834eb..7031632e73 100644 --- a/src/borg/testsuite/archiver/create_cmd_test.py +++ b/src/borg/testsuite/archiver/create_cmd_test.py @@ -15,7 +15,7 @@ from ...manifest import Manifest from ...platform import is_win32 from ...repository import Repository -from ...helpers import CommandError, BackupPermissionError +from ...helpers import CommandError, BackupPermissionError, Error from .. import has_lchflags, has_mknod from .. import changedir from .. import ( @@ -682,6 +682,28 @@ def test_file_status(archivers, request): assert "A input/file2" in output +def test_create_tags(archivers, request): + archiver = request.getfixturevalue(archivers) + create_test_files(archiver.input_path) + cmd(archiver, "repo-create", RK_ENCRYPTION) + cmd(archiver, "create", "--tags", "foo", "bar", "baz", "--", "test", "input") + info = cmd(archiver, "info", "--json", "test") + info = json.loads(info) + assert sorted(info["archives"][0]["tags"]) == ["bar", "baz", "foo"] + + +def test_create_invalid_tags(archivers, request): + archiver = request.getfixturevalue(archivers) + create_test_files(archiver.input_path) + cmd(archiver, "repo-create", RK_ENCRYPTION) + if archiver.FORK_DEFAULT: + output = cmd(archiver, "create", "--tags", "@INVALID", "--", "test", "input", exit_code=EXIT_ERROR) + assert "Unknown special tags given" in output + else: + with pytest.raises(Error): + cmd(archiver, "create", "--tags", "@INVALID", "--", "test", "input") + + @pytest.mark.skipif( is_win32, reason="ctime attribute is file creation time on Windows" ) # see https://docs.python.org/3/library/os.html#os.stat_result.st_ctime