Since it would be surprising for bup get to ignore the destination
repository settings (bup.split.trees, etc.) when creating new saves,
add a new --rewrite option and refuse to run when the source and
destination configs differ materially and --rewrite hasn't been
specified. Support --no-rewrite to opt out. Only the append and pick
related methods allow rewriting.
Specifying --rewrite always causes rewriting, even if the source and
destination repository settings are the same. This allows rewriting
within the same repository, and rewriting with respect to the current
defaults for things that have changed but don't have configuration
options like splitting .bupm files.
To handle the rewriting, rely on the new rewrite code, moved to
bup.rewrite, to handle the work, and remove the rewrite command since
it's equivalent to a "bup get append" to a new branch.
Documentation/
bup-get.1.md | 62 +++++++
lib/bup/cmd/get.py | 199 +++++++++++++++++++----
lib/bup/helpers.py | 14 +-
lib/bup/{cmd => }/rewrite.py | 247 +++++++++++-----------------
note/main.md | 5 +
test/ext/test-rewrite | 25 +--
test/ext/test_get.py | 304 ++++++++++++++++++++++++-----------
7 files changed, 557 insertions(+), 299 deletions(-)
rename lib/bup/{cmd => }/rewrite.py (52%)
diff --git a/Documentation/
bup-get.1.md b/Documentation/
bup-get.1.md
index d1e43c4b..44e99914 100644
--- a/Documentation/
bup-get.1.md
+++ b/Documentation/
bup-get.1.md
@@ -26,8 +26,12 @@ For example:
bup get -s /source/repo --ff foo
bup get -s /source/repo --ff: foo/latest bar
+ bup get -s /source/repo --pick: foo/2010-10-10-101010 bar
bup get -s /source/repo --pick: foo/2010-10-10-101010 .tag/bar
+The behavior of any given METHOD is determined in part by the *ref*
+and *dest* types, i.e. branch, save, tag, etc.
+
As a special case, if *ref* names the "latest" save symlink, then bup
will act exactly as if the save that "latest" points to had been
specified, rather than the "latest" symlink itself, so `bup get
@@ -127,6 +131,45 @@ used to help test before/after results.)
\--print-tags
: for each updated tag, print the new git id.
+\--rewrite, \--no-rewrite
+: rewrite the data according to the destination repository
+ configuration, e.g. its `bup.split.files`, and `bup.split.trees`
+ values. Currently, one of these options must be specified whenever
+ the source and destination repository configurations differ in a
+ relevant way, and so far, this option is only supported for
+ appends and picks. Note that while tested, this option is
+ relatively new and so warrants even more caution (see CAUTION
+ above) than `bup get` itself. Please consider validating the
+ results carefully for now.
+
+\--rewrite-db=*path*
+: place the rewrite database at *path*. Re-using an existing
+ database (e.g. after an interruption) can allow the rewrite to
+ resume without repeating expensive operations. By default, a
+ transient database will be placed in TMPDIR and removed on exit.
+
+\--exclude-rx=*pattern*
+: exclude any path matching *pattern*, which must be a Python regular
+ expression (
http://docs.python.org/library/re.html). The pattern
+ will be compared against the full path, without anchoring, so
+ "x/y" will match "ox/yard" or "box/yards". To exclude the
+ contents of /tmp, but not the directory itself, use
+ "^/tmp/.". (may be repeated)
+
+ Examples:
+
+ * '/foo$' - exclude any file named foo
+ * '/foo/$' - exclude any directory named foo
+ * '/foo/.' - exclude the content of any directory named foo
+ * '^/tmp/.' - exclude root-level /tmp's content, but not /tmp itself
+
+ Only supported when rewriting.
+
+\--exclude-rx-from=*filename*
+: read --exclude-rx patterns from *filename*, one pattern per-line
+ (may be repeated). Ignore completely empty lines. Only supported
+ when rewriting.
+
-v, \--verbose
: increase verbosity (can be used more than once). With
`-v`, print the name of every item fetched, with `-vv` add
@@ -187,6 +230,25 @@ used to help test before/after results.)
# Append only the /home directory from archives/latest to only-home.
$ bup get -s "$BUP_DIR" --append: archives/latest/home only-home
+ # Resplit (rewrite) the archives branch. Note that, done all at
+ # once, this may require additional space up to the size of the
+ # archives branch. The pick methods can do the rewriting more
+ # selectively or incrementally. (Assume BUP_DIR has no split
+ # settings.)
+ #
+ $ bup config bup.split.trees true
+ $ bup config bup.split.files legacy:16
+ $ bup get --append: archives archives-resplit
+ #
+ # Check that archives-resplit looks OK, perhaps via trial
+ # restores, joining it, etc. (see CAUTION above), and once
+ # satisfied, perhaps...
+ #
+ $ bup rm archives
+ $ bup gc
+ $ git --git-dir "$BUP_DIR" branch -m archives-resplit archives
+
+
# SEE ALSO
`bup-on`(1), `bup-tag`(1), `ssh_config`(5)
diff --git a/lib/bup/cmd/get.py b/lib/bup/cmd/get.py
index 7a6c8b64..265bd59c 100644
--- a/lib/bup/cmd/get.py
+++ b/lib/bup/cmd/get.py
@@ -1,10 +1,11 @@
from binascii import hexlify, unhexlify
from collections import namedtuple
+from contextlib import ExitStack, closing
from stat import S_ISDIR
-import os, sys, textwrap, time
+import os, sys, textwrap, sqlite3, time
-from bup import compat, git, client, vfs
+from bup import client, compat, git, hashsplit, rewrite, vfs
from bup.commit import commit_message
from bup.compat import argv_bytes
from bup.config import derive_repo_addr
@@ -17,19 +18,24 @@ from bup.helpers import \
log,
note_error,
parse_num,
+ parse_rx_excludes,
+ temp_dir,
tty_width)
from
bup.io import path_msg
from bup.pwdgrp import userfullname, username
from bup.repo import LocalRepo, make_repo
+
argspec = (
"usage: bup get [-s source] [-r remote] (<--ff|--append|...> REF [DEST])...",
- """Transfer data from a source repository to a destination repository
- according to the methods specified (--ff, --ff:, --append, etc.).
- Both repositories default to BUP_DIR. A remote destination may be
- specified with -r, and data may be pulled from a remote repository
- with the related "bup on HOST get ..." command.""",
+ """Transfer data from a source repository to a destination
+ repository according to the methods specified (--ff, --ff:,
+ --append, etc.). Both repositories default to BUP_DIR. A remote
+ destination may be specified with -r, and data may be pulled from
+ a remote repository with the related "bup on HOST get ..."
+ command. The --exclude-rx and --exclude-rx-from options currently
+ only apply to rewrites.""",
('optional arguments:',
(('-h, --help', 'show this help message and exit'),
@@ -43,6 +49,10 @@ argspec = (
('-t --print-trees', 'output a tree id for each ref set'),
('-c, --print-commits', 'output a commit id for each ref set'),
('--print-tags', 'output an id for each tag'),
+ ('--rewrite', 'rewrite data according to destination repo settings'),
+ ('--rewrite-db PATH', 'transient rewrite database (in TMPDIR by default)'),
+ ('--exclude-rx REGEX', 'skip paths matching the unanchored regex (may be repeated)'),
+ ('--exclude-rx-from PATH', 'skip --exclude-rx patterns in PATH (may be repeated)'),
('--bwlimit BWLIMIT', 'maximum bytes/sec to transmit to server'),
('-0, -1, -2, -3, -4, -5, -6, -7, -8, -9, --compress LEVEL',
'set compression LEVEL (default: 1)'))),
@@ -126,9 +136,12 @@ def parse_args(args):
opt.bwlimit = None
opt.compress = None
opt.ignore_missing = False
+ opt.rewrite = None # None means "didn't specify"
+ opt.rewrite_db = None
opt.source = opt.remote = None
opt.target_specs = []
+ exclude_opts = []
remaining = args[1:] # Skip argv[0]
while remaining:
arg = remaining[0]
@@ -164,6 +177,15 @@ def parse_args(args):
opt.print_trees, remaining = True, remaining[1:]
elif arg == b'--print-tags':
opt.print_tags, remaining = True, remaining[1:]
+ elif arg == b'--rewrite':
+ opt.rewrite, remaining = True, remaining[1:]
+ elif arg == b'--no-rewrite':
+ opt.rewrite, remaining = False, remaining[1:]
+ elif arg == b'--rewrite-db':
+ (opt.rewrite_db,), remaining = require_n_args_or_die(1, remaining)
+ elif arg in (b'--exclude-rx', b'--exclude-rx-from'): # handled later
+ (val,), remaining = require_n_args_or_die(1, remaining)
+ exclude_opts.append((arg, val))
elif arg in (b'-0', b'-1', b'-2', b'-3', b'-4', b'-5', b'-6', b'-7',
b'-8', b'-9'):
opt.compress = int(arg[1:])
@@ -183,6 +205,9 @@ def parse_args(args):
continue
else:
misuse()
+ opt.exclude_rxs = parse_rx_excludes(exclude_opts, misuse)
+ if opt.exclude_rxs and not opt.rewrite:
+ misuse('cannot --exclude-rx or --exclude-rx-from when not rewriting')
for target in opt.target_specs:
if opt.ignore_missing and target.method != 'unnamed':
misuse('currently only --unnamed allows --ignore-missing')
@@ -230,7 +255,7 @@ def get_random_item(name, hash, src_repo, dest_repo, opt):
dest_repo.just_write(item.oid, item.type, item.data)
-def append_commit(name, hash, parent, src_repo, dest_repo, opt):
+def transfer_commit(name, hash, parent, src_repo, dest_repo, opt):
now = time.time()
items = parse_commit(get_cat_data(
src_repo.cat(hash), b'commit'))
tree = unhexlify(items.tree)
@@ -245,12 +270,67 @@ def append_commit(name, hash, parent, src_repo, dest_repo, opt):
return c, tree
-def append_commits(commits, src_name, dest_hash, src_repo, dest_repo, opt):
+def append_commit(src_loc, parent, src_repo, dest_repo, opt):
+ if not opt.rewrite:
+ assert isinstance(src_loc, (bytes, Loc))
+ oidx = src_loc if isinstance(src_loc, bytes) else hexlify(src_loc.hash)
+ return transfer_commit(None, # unused
+ oidx, parent, src_repo, dest_repo, opt)
+
+ # Friendlier checking was done during resolve_*
+ assert isinstance(src_loc, Loc), src_loc
+ path = src_loc.vfs_path
+ assert len(path) == 3, path
+ root, ref, save = path
+ assert isinstance(save[1], (vfs.Commit, vfs.FakeLink)), path
+ assert isinstance(ref[1], vfs.RevList), path
+ return rewrite.append_save(path, parent, src_repo, dest_repo,
+ opt.dest_split_cfg, opt.exclude_rxs,
+ # FIXME: ...
+ opt.rewrite_db_conn,
+ opt.rewrite_db_mapping)
+
+
+def append_commits(src_loc, dest_hash, src_repo, dest_repo, opt):
+ if not opt.rewrite:
+ commits = list(src_repo.rev_list(hexlify(src_loc.hash)))
+ commits.reverse()
+ last_c, tree = dest_hash, None
+ for commit in commits:
+ last_c, tree = append_commit(commit, last_c, src_repo, dest_repo,
+ opt)
+ assert tree is not None
+ return last_c, tree
+
+ # Friendlier checking was done during resolve_*
+ assert isinstance(src_loc, Loc), src_loc
+ assert src_loc.type in ('branch', 'commit', 'save'), src_loc
+ path = src_loc.vfs_path
+ assert len(path) == 2, path
+ root, ref = path
+ assert isinstance(ref[1], vfs.RevList), ref[1]
+
+ # We need both the VFS name (YYYY-MM-DD[-N]), and the rev-list
+ # order, so for now, cross-reference rev-list with contents().
+ entry_for_coid = {}
+ for entry in vfs.contents(src_repo, path[1][1]):
+ if entry[0] in (b'.', b'..', b'latest'):
+ continue
+ entry_for_coid[entry[1].coid] = entry
+
+ commits = list(src_repo.rev_list(hexlify(src_loc.hash)))
+ commits.reverse()
+
last_c, tree = dest_hash, None
for commit in commits:
- last_c, tree = append_commit(src_name, commit, last_c,
- src_repo, dest_repo, opt)
- assert(tree is not None)
+ coid = unhexlify(commit)
+ last_c, tree = rewrite.append_save(path + (entry_for_coid[coid],),
+ last_c, src_repo, dest_repo,
+ opt.dest_split_cfg, opt.exclude_rxs,
+ # FIXME: ...
+ opt.rewrite_db_conn,
+ opt.rewrite_db_mapping)
+ assert tree is not None
return last_c, tree
@@ -266,14 +346,15 @@ def find_git_item(ref, repo):
return GitLoc(ref, unhexlify(oidx), typ)
-Loc = namedtuple('Loc', ['type', 'hash', 'path'])
-default_loc = Loc(None, None, None)
+Loc = namedtuple('Loc', ['type', 'hash', 'path', 'vfs_path'])
+default_loc = Loc(None, None, None, None)
def find_vfs_item(name, repo):
res = repo.resolve(name, follow=False, want_meta=False)
leaf_name, leaf_item = res[-1]
if not leaf_item:
return None
+ vfs_path = res
kind = type(leaf_item)
if kind == vfs.Root:
kind = 'root'
@@ -309,11 +390,11 @@ def find_vfs_item(name, repo):
% (path_msg(name), res))
path = b'/'.join(name for name, item in res)
if hasattr(leaf_item, 'coid'):
- result = Loc(type=kind, hash=leaf_item.coid, path=path)
+ result = Loc(type=kind, hash=leaf_item.coid, path=path, vfs_path=vfs_path)
elif hasattr(leaf_item, 'oid'):
- result = Loc(type=kind, hash=leaf_item.oid, path=path)
+ result = Loc(type=kind, hash=leaf_item.oid, path=path, vfs_path=vfs_path)
else:
- result = Loc(type=kind, hash=None, path=path)
+ result = Loc(type=kind, hash=None, path=path, vfs_path=vfs_path)
return result
@@ -434,12 +515,24 @@ def handle_ff(item, src_repo, dest_repo, opt):
return None
-def resolve_append(spec, src_repo, dest_repo):
+def resolve_append(spec, src_repo, dest_repo, *, rewrite):
src = resolve_src(spec, src_repo)
if src.type not in ('branch', 'save', 'commit', 'tree'):
misuse('source for %s must be a branch, save, commit, or tree, not %s'
% (spec_msg(spec), src.type))
spec, dest = resolve_branch_dest(spec, src, src_repo, dest_repo)
+ if rewrite:
+ def vpm(path):
+ return path_msg(b"/".join(x[0] for x in src_path))
+ if not isinstance(src, Loc):
+ misuse(f'cannot currently rewrite git location {src}')
+ src_path = src.vfs_path
+ if len(src_path) != 2:
+ misuse(f'cannot append {vpm(src_path)}')
+ root, src_ref = src_path
+ if not isinstance(src_ref[1], vfs.RevList):
+ misuse(f'cannot append {vpm(src_path)} saves'
+ f' ({path_msg(src_ref[0])} is a {type(src_ref[1])})')
return Target(spec=spec, src=src, dest=dest)
@@ -447,8 +540,10 @@ def handle_append(item, src_repo, dest_repo, opt):
assert item.spec.method == 'append'
assert item.src.type in ('branch', 'save', 'commit', 'tree')
assert item.dest.type == 'branch' or not item.dest.type
- src_oidx = hexlify(item.src.hash)
if item.src.type == 'tree':
+ src_oidx = hexlify(item.src.hash)
+ if opt.rewrite:
+ misuse(f'rewrite cannot yet promote tree to commit for {spec_msg(item.spec)}')
get_random_item(item.spec.src, src_oidx, src_repo, dest_repo, opt)
parent = item.dest.hash
msg = commit_message(b'bup get', compat.get_argvb())
@@ -458,22 +553,19 @@ def handle_append(item, src_repo, dest_repo, opt):
userline, now, None,
userline, now, None, msg)
return commit, item.src.hash
- commits = list(src_repo.rev_list(src_oidx))
- commits.reverse()
if item.dest.hash:
assert item.dest.type in ('branch', 'commit', 'save'), item.dest
- return append_commits(commits, item.spec.src, item.dest.hash,
- src_repo, dest_repo, opt)
+ return append_commits(item.src, item.dest.hash, src_repo, dest_repo, opt)
-def resolve_pick(spec, src_repo, dest_repo):
+def resolve_pick(spec, src_repo, dest_repo, *, rewrite):
src = resolve_src(spec, src_repo)
spec_args = spec_msg(spec)
if src.type == 'tree':
misuse('%s is impossible; can only --append a tree' % spec_args)
if src.type not in ('commit', 'save'):
misuse('%s impossible; can only pick a commit or save, not %s'
- % (spec_args, src.type))
+ % (spec_args, src.type))
if not spec.dest:
if src.path.startswith(b'/.tag/'):
spec = spec._replace(dest=spec.src)
@@ -481,6 +573,9 @@ def resolve_pick(spec, src_repo, dest_repo):
spec = spec._replace(dest=get_save_branch(src_repo, spec.src))
if not spec.dest:
misuse('no destination provided for %s' % spec_args)
+ if rewrite:
+ if src.type != 'save':
+ misuse(f'cannot currently --rewrite a {src.type}')
dest = find_vfs_item(spec.dest, dest_repo)
if not dest:
cp = validate_vfs_path(cleanup_vfs_path(spec.dest), spec)
@@ -499,16 +594,15 @@ def resolve_pick(spec, src_repo, dest_repo):
def handle_pick(item, src_repo, dest_repo, opt):
assert item.spec.method in ('pick', 'force-pick')
assert item.src.type in ('save', 'commit')
- src_oidx = hexlify(item.src.hash)
if item.dest.hash:
# if the dest is committish, make it the parent
if item.dest.type in ('branch', 'commit', 'save'):
- return append_commit(item.spec.src, src_oidx, item.dest.hash,
- src_repo, dest_repo, opt)
+ return append_commit(item.src, item.dest.hash, src_repo, dest_repo,
+ opt)
assert item.dest.path.startswith(b'/.tag/'), item.dest
# no parent; either dest is a non-commit tag and we should clobber
# it, or dest doesn't exist.
- return append_commit(item.spec.src, src_oidx, None, src_repo, dest_repo, opt)
+ return append_commit(item.src, None, src_repo, dest_repo, opt)
def resolve_new_tag(spec, src_repo, dest_repo):
@@ -587,7 +681,7 @@ def handle_unnamed(item, src_repo, dest_repo, opt):
return (None,)
-def resolve_targets(specs, src_repo, dest_repo, *, ignore_missing):
+def resolve_targets(specs, src_repo, dest_repo, *, ignore_missing, rewrite):
resolved_items = []
common_args = src_repo, dest_repo
for spec in specs:
@@ -595,9 +689,11 @@ def resolve_targets(specs, src_repo, dest_repo, *, ignore_missing):
if spec.method == 'ff':
resolved_items.append(resolve_ff(spec, *common_args))
elif spec.method == 'append':
- resolved_items.append(resolve_append(spec, *common_args))
+ resolved_items.append(resolve_append(spec, *common_args,
+ rewrite=rewrite))
elif spec.method in ('pick', 'force-pick'):
- resolved_items.append(resolve_pick(spec, *common_args))
+ resolved_items.append(resolve_pick(spec, *common_args,
+ rewrite=rewrite))
elif spec.method == 'new-tag':
resolved_items.append(resolve_new_tag(spec, *common_args))
elif spec.method == 'replace':
@@ -652,16 +748,49 @@ def main(argv):
if opt.bwlimit:
client.bwlimit = parse_num(opt.bwlimit)
- with make_repo(derive_repo_addr(remote=opt.remote, die=misuse),
+ with LocalRepo(repo_dir=opt.source) as src_repo, \
+ make_repo(derive_repo_addr(remote=opt.remote, die=misuse),
compression_level=opt.compress) as dest_repo:
- with LocalRepo(repo_dir=opt.source) as src_repo:
+
+ src_split_cfg = hashsplit.configuration(src_repo.config_get)
+ opt.dest_split_cfg = hashsplit.configuration(dest_repo.config_get)
+
+ if src_split_cfg != opt.dest_split_cfg and opt.rewrite is None:
+ misuse('repository configs differ; specify --rewrite or --no-rewrite')
+
+ ctx = ExitStack()
+ if opt.rewrite:
+ if not opt.rewrite_db:
+ rwdb_tmpdir = ctx.enter_context(temp_dir(prefix='bup-rewrite-'))
+ opt.rewrite_db = f'{rwdb_tmpdir}/db'
+ rwdb_conn = sqlite3.connect(opt.rewrite_db)
+ rwdb_conn.text_factory = bytes
+ ctx.enter_context(closing(rwdb_conn))
+ opt.rewrite_db_conn = rwdb_conn # FIXME: ...
+ with closing(rwdb_conn.cursor()) as rwdb_cur:
+ opt.rewrite_db_mapping = \
+ rewrite.prep_mapping_table(rwdb_cur, opt.dest_split_cfg)
+
+ with ctx:
+
# Resolve and validate all sources and destinations,
# implicit or explicit, and do it up-front, so we can
# fail before we start writing (for any obviously
# broken cases).
target_items = resolve_targets(opt.target_specs,
src_repo, dest_repo,
- ignore_missing=opt.ignore_missing)
+ ignore_missing=opt.ignore_missing,
+ rewrite=opt.rewrite)
+ if opt.rewrite:
+ for item in target_items:
+ if item.spec.method in ('append', 'force-pick', 'pick'):
+ continue
+ elif item.spec.method == 'ff':
+ misuse(f'--ff cannot rewrite (use --pick)')
+ elif item.spec.method in ('new-tag', 'replace', 'unnamed'):
+ misuse(f'--{item.spec.method} cannot currently rewrite')
+ else:
+ assert False, f'unexpected method {item.spec.method}'
updated_refs = {} # ref_name -> (original_ref, tip_commit(bin))
no_ref_info = (None, None)
diff --git a/lib/bup/helpers.py b/lib/bup/helpers.py
index c81a1dfc..9b6123af 100644
--- a/lib/bup/helpers.py
+++ b/lib/bup/helpers.py
@@ -4,7 +4,7 @@ from collections import namedtuple
from contextlib import ExitStack, nullcontext
from ctypes import sizeof, c_void_p
from math import floor
-from os import environ
+from os import environ, fsencode
from random import SystemRandom
from subprocess import PIPE, Popen
from tempfile import mkdtemp
@@ -1012,13 +1012,17 @@ def parse_rx_excludes(options, fatal):
excluded_patterns = []
for flag in options:
- (option, parameter) = flag
- if option == '--exclude-rx':
+ option, parameter = flag
+ if isinstance(option, str):
+ option = fsencode(option)
+ if isinstance(parameter, str):
+ parameter = fsencode(parameter)
+ if option == b'--exclude-rx':
try:
- excluded_patterns.append(re.compile(argv_bytes(parameter)))
+ excluded_patterns.append(re.compile(parameter))
except re.error as ex:
fatal('invalid --exclude-rx pattern (%r): %s' % (parameter, ex))
- elif option == '--exclude-rx-from':
+ elif option == b'--exclude-rx-from':
try:
f = open(resolve_parent(parameter), 'rb')
except IOError as e:
diff --git a/lib/bup/cmd/rewrite.py b/lib/bup/rewrite.py
similarity index 52%
rename from lib/bup/cmd/rewrite.py
rename to lib/bup/rewrite.py
index 1a6f7086..e65fa506 100755
--- a/lib/bup/cmd/rewrite.py
+++ b/lib/bup/rewrite.py
@@ -1,36 +1,25 @@
-from binascii import hexlify, unhexlify
+from binascii import hexlify
from contextlib import closing
from itertools import chain
+from os.path import join as pj
from stat import S_ISDIR, S_ISLNK, S_ISREG
import os
-import sqlite3
-from bup import hashsplit, git, options, repo, metadata, vfs
-from bup.compat import argv_bytes
+from bup import hashsplit, metadata, vfs
+from bup.git import get_cat_data, parse_commit
from bup.hashsplit import GIT_MODE_FILE, GIT_MODE_SYMLINK, GIT_MODE_TREE
-from bup.helpers import \
- (handle_ctrl_c, path_components,
- valid_save_name, log,
- parse_rx_excludes,
- qprogress,
- reprogress,
- should_rx_exclude_path)
-from
bup.io import path_msg, qsql_id
+from bup.helpers import path_components, should_rx_exclude_path
+from
bup.io import qsql_id
from bup.tree import Stack
-from bup.repo import make_repo
-from bup.config import derive_repo_addr, ConfigError
-optspec = """
-bup rewrite -s srcrepo <branch-name>
---
-s,source= source repository
-r,remote= remote destination repository
-work-db= work database filename (required, can be deleted after running)
-exclude-rx= skip paths matching the unanchored regex (may be repeated)
-exclude-rx-from= skip --exclude-rx patterns in file (may be repeated)
-"""
+def _fs_path_from_vfs(path):
+ fs = b'/'.join(x[0] for x in path)
+ if not S_ISDIR(vfs.item_mode(path[-1][1])):
+ return fs
+ return fs + b'/'
+
def prep_mapping_table(db, split_cfg):
# This currently only needs to track items that may be split,
@@ -88,25 +77,30 @@ def previous_conversion(dstrepo, item, vfs_dir, db, mapping):
return item, dst, None
return item, dst, GIT_MODE_TREE if chunked else GIT_MODE_FILE
-def vfs_walk_recursively(srcrepo, dstrepo, vfs_item, excludes, db, mapping,
- fullname=b''):
- for name, item in vfs.contents(srcrepo, vfs_item):
+def vfs_walk_recursively(srcrepo, dstrepo, path, excludes, db, mapping):
+ item = path[-1][1]
+ assert len(path) >= 3
+ # drop branch/DATE
+ fs_path_in_save = _fs_path_from_vfs((path[0],) + path[3:])
+ for entry in vfs.contents(srcrepo, item):
+ name, sub_item = entry
+ sub_path = path + (entry,)
if name in (b'.', b'..'):
continue
- itemname = fullname + b'/' + name
- check_name = itemname + (b'/' if S_ISDIR(vfs.item_mode(item)) else b'')
- if should_rx_exclude_path(check_name, excludes):
+ sub_fs_path_in_save = pj(fs_path_in_save, name)
+ if S_ISDIR(vfs.item_mode(sub_item)):
+ sub_fs_path_in_save += b'/'
+ if should_rx_exclude_path(sub_fs_path_in_save, excludes):
continue
- if S_ISDIR(vfs.item_mode(item)):
- item, oid, _ = previous_conversion(dstrepo, item, True, db, mapping)
+ if S_ISDIR(vfs.item_mode(sub_item)):
+ conv_item, oid, _ = \
+ previous_conversion(dstrepo, sub_item, True, db, mapping)
+ if conv_item is not sub_item:
+ sub_path = sub_path[:-1] + ((sub_path[-1][0], conv_item),)
if oid is None:
- yield from vfs_walk_recursively(srcrepo, dstrepo, item,
- excludes, db, mapping,
- fullname=itemname)
- # and the dir itself
- yield itemname + b'/', item
- else:
- yield itemname, item
+ yield from vfs_walk_recursively(srcrepo, dstrepo, sub_path,
+ excludes, db, mapping)
+ yield sub_path
def rewrite_link(item, item_mode, name, srcrepo, dstrepo, stack):
assert isinstance(name, bytes)
@@ -121,9 +115,12 @@ def rewrite_link(item, item_mode, name, srcrepo, dstrepo, stack):
assert item.meta.size == len(item.meta.symlink_target)
stack.append_to_current(name, item_mode, git_mode, oid, item.meta)
-def rewrite_item(item, commit_name, fullname, srcrepo, src, dstrepo, split_cfg,
- stack, wdbc, mapping):
- dirn, filen = os.path.split(fullname)
+def rewrite_save_item(save_path, path, srcrepo, dstrepo, split_cfg, stack, wdbc,
+ mapping):
+ # save_path is the vfs path to the save ref, e.g. to branch/DATE
+ fs_path = _fs_path_from_vfs(path[3:]) # not including /branch/DATE
+ assert not fs_path.startswith(b'/') # because resolve(parent=...)
+ dirn, filen = os.path.split(b'/' + fs_path)
assert dirn.startswith(b'/')
dirp = path_components(dirn)
@@ -132,14 +129,21 @@ def rewrite_item(item, commit_name, fullname, srcrepo, src, dstrepo, split_cfg,
stack.pop()
# If switching to a new sub-tree, start a new sub-tree.
+ comp_parent = None
for path_component in dirp[len(stack):]:
- dir_name, fs_path = path_component
-
- dir_item = vfs.resolve(srcrepo, src + b'/' + commit_name + b'/' + fs_path)
- meta = dir_item[-1][1].meta
+ comp_name, comp_path = path_component
+ if comp_parent:
+ dir_res = vfs.resolve(srcrepo, comp_name, parent=comp_parent)
+ else:
+ full_comp_path = b'/'.join([x[0] for x in save_path]) + comp_path
+ dir_res = vfs.resolve(srcrepo, full_comp_path)
+ meta = dir_res[-1][1].meta
if not isinstance(meta, metadata.Metadata):
meta = None
- stack.push(dir_name, meta)
+ stack.push(comp_name, meta)
+ comp_parent = dir_res
+
+ item = path[-1][1]
# First, things that can't be affected by the rewrite
item_mode = vfs.item_mode(item)
@@ -208,118 +212,51 @@ def rewrite_item(item, commit_name, fullname, srcrepo, src, dstrepo, split_cfg,
(item.oid, oid, chunked, item_size))
stack.append_to_current(filen, item_mode, git_mode, oid, item.meta)
-def rewrite_branch(srcrepo, src, dstrepo, dst, excludes, workdb, fatal):
- # Currently, the workdb must always be ready to commit (see finally below)
- srcref = b'refs/heads/%s' % src
- dstref = b'refs/heads/%s' % dst
- if dstrepo.read_ref(dstref) is not None:
- fatal(f'branch already exists: {path_msg(dst)}')
- try:
- split_cfg = hashsplit.configuration(dstrepo.config_get)
- except ConfigError as ex:
- fatal(ex)
- split_trees = dstrepo.config_get(b'bup.split.trees', opttype='bool')
- vfs_branch = vfs.resolve(srcrepo, src)
- item = vfs_branch[-1][1]
- if not item:
- fatal(f'cannot access {path_msg(src)} in source\n')
- commit_oid_name = {
- c[1].coid: c[0]
- for c in vfs.contents(srcrepo, item)
- if isinstance(c[1], vfs.Commit)
- }
- commits = list(srcrepo.rev_list(hexlify(item.oid), parse=vfs.parse_rev,
- format=b'%T %at'))
- commits.reverse()
- with closing(workdb.cursor()) as wdbc:
+def append_save(save_path, parent, srcrepo, dstrepo, split_cfg,
+ excludes, workdb, mapping):
+ # Strict for now
+ assert isinstance(parent, (bytes, type(None))), parent
+ if parent:
+ assert len(parent) == 20, parent
+ assert len(save_path) == 3, (len(save_path), save_path)
+ assert isinstance(save_path[1][1], vfs.RevList)
+ leaf_name, leaf_item = save_path[2]
+ if isinstance(leaf_item, vfs.FakeLink):
+ # For now, vfs.contents() does not resolve the one FakeLink
+ assert leaf_name == b'latest', save_path
+ res = srcrepo.resolve(leaf_item.target, parent=save_path[:-1],
+ follow=False, want_meta=False)
+ leaf_name, leaf_item = res[-1]
+ save_path = res
+ assert isinstance(leaf_item, vfs.Commit), leaf_item
+ # Currently, the workdb must always be ready to commit (see finally below)
+ with closing(workdb.cursor()) as dbc:
try:
- mapping = prep_mapping_table(wdbc, split_cfg)
-
# Maintain a stack of information representing the current
# location in the archive being constructed.
- parent = None
- i, n = 0, len(commits)
- for commit, (tree, timestamp) in commits:
- i += 1
- stack = Stack(dstrepo, split_cfg)
-
- commit_name = commit_oid_name[unhexlify(commit)]
- pm = f'{path_msg(src)}/{path_msg(commit_name)}'
- orig_oidm = commit[:12].decode("ascii")
- qprogress(f'{i}/{n} {orig_oidm} {pm}\r')
-
- citem = vfs.Commit(meta=vfs.default_dir_mode, oid=tree,
- coid=commit)
- for fullname, item in vfs_walk_recursively(srcrepo, dstrepo,
- citem, excludes,
- wdbc, mapping):
- rewrite_item(item, commit_name, fullname, srcrepo, src,
- dstrepo, split_cfg, stack, wdbc, mapping)
-
- while len(stack) > 1: # pop all parts above root folder
- stack.pop()
- tree = stack.pop() # and the root to get the tree
-
- commit_it =
srcrepo.cat(commit)
- next(commit_it)
- ci = git.parse_commit(b''.join(commit_it))
- author = ci.author_name + b' <' + ci.author_mail + b'>'
- committer = ci.committer_name + b' <' + ci.committer_mail + b'>'
- newref = dstrepo.write_commit(tree, parent,
- author,
- ci.author_sec,
- ci.author_offset,
- committer,
- ci.committer_sec,
- ci.committer_offset,
- ci.message)
- parent = newref
- new_oidm = newref.hex()[:12]
- log(f'{orig_oidm} -> {new_oidm} {pm}\n')
- reprogress()
-
- dstrepo.update_ref(dstref, newref, None)
+ stack = Stack(dstrepo, split_cfg)
+ for path in vfs_walk_recursively(srcrepo, dstrepo, save_path,
+ excludes, dbc, mapping):
+ rewrite_save_item(save_path, path, srcrepo, dstrepo, split_cfg,
+ stack, dbc, mapping)
+
+ while len(stack) > 1: # pop all parts above root folder
+ stack.pop()
+ tree = stack.pop() # and the root to get the tree
+
+ save_oidx = hexlify(save_path[2][1].coid)
+ ci = parse_commit(get_cat_data(
srcrepo.cat(save_oidx), b'commit'))
+ author = ci.author_name + b' <' + ci.author_mail + b'>'
+ committer = ci.committer_name + b' <' + ci.committer_mail + b'>'
+ return (dstrepo.write_commit(tree, parent,
+ author,
+ ci.author_sec,
+ ci.author_offset,
+ committer,
+ ci.committer_sec,
+ ci.committer_offset,
+ ci.message),
+ tree)
finally:
workdb.commit() # the workdb is always ready for commit
-
-def main(argv):
-
- handle_ctrl_c()
-
- o = options.Options(optspec)
- opt, flags, extra = o.parse_bytes(argv[1:])
-
- if len(extra) != 1:
- o.fatal('no branch name given')
-
- exclude_rxs = parse_rx_excludes(flags, o.fatal)
-
- src = argv_bytes(extra[0])
- if b':' in src:
- src, dst = src.split(b':', 1)
- else:
- dst = src
- if not valid_save_name(src):
- o.fatal(f'invalid branch name: {path_msg(src)}')
- if not valid_save_name(dst):
- o.fatal(f'invalid branch name: {path_msg(dst)}')
-
- if opt.remote:
- opt.remote = argv_bytes(opt.remote)
-
- if not opt.work_db:
- o.fatal('--work-db argument is required')
-
- workdb_conn = sqlite3.connect(opt.work_db)
- workdb_conn.text_factory = bytes
-
- # FIXME: support remote source repos ... probably after we unify
- # the handling?
- # Leave db commits to the sub-functions doing the work.
- with repo.LocalRepo(argv_bytes(opt.source)) as srcrepo, \
- make_repo(derive_repo_addr(remote=opt.remote, die=o.fatal)) as dstrepo, \
- closing(workdb_conn):
- rewrite_branch(srcrepo, src, dstrepo, dst, exclude_rxs, workdb_conn,
- o.fatal)
-
diff --git a/note/main.md b/note/main.md
index 3b7732d0..f750891c 100644
--- a/note/main.md
+++ b/note/main.md
@@ -101,6 +101,11 @@ General
when large directories change (e.g. large active Maildirs). See
`bup-config`(5) for additional information.
+* `bup get` picks and appends can `--rewrite` the data being
+ transferred to respect the destination repository's configuration,
+ e.g. its `bup.split.files` and `bup.split.trees` settings. See
+ `bup-get`(1) for additional information.
+
* The default pack compression level can now be configured via either
`pack.compression` or `core.compression`. See `bup-config`(5) for
additional information.
diff --git a/test/ext/test-rewrite b/test/ext/test-rewrite
index 46fb85eb..47513153 100755
--- a/test/ext/test-rewrite
+++ b/test/ext/test-rewrite
@@ -48,7 +48,7 @@ compare() {
WVSTART split and rewrite
WVPASS bup split -n split < "$top/test/testfile1"
-WVPASS bup -d "$BUP_DIR2" rewrite --work-db "$tmpdir/db" -s "$BUP_DIR" split:test
+WVPASS bup -d "$BUP_DIR2" get --rewrite-db "$tmpdir/db" -s "$BUP_DIR" --append: split test
WVPASS compare "$BUP_DIR" split "$BUP_DIR2" test
WVSTART make multiple saves
@@ -59,14 +59,15 @@ WVPASS bup save -n save --strip-path="$top" "$top/test/sampledata"
WVPASS bup save -n save --strip-path="$top" "$top/test/sampledata"
WVSTART rewrite to different split
-WVPASS bup -d "$BUP_DIR4" rewrite --work-db "$tmpdir/db" -s "$BUP_DIR" save
+WVPASS bup -d "$BUP_DIR" ls -l save
+WVPASS bup -d "$BUP_DIR4" get --rewrite --rewrite-db "$tmpdir/db" -s "$BUP_DIR" --append save
WVPASS compare "$BUP_DIR" save "$BUP_DIR4" save
WVSTART "rewrite unchanged (to remote)"
-WVPASS bup rewrite -r ":$BUP_DIR3" --work-db "$tmpdir/db" -s "$BUP_DIR" save
+WVPASS bup get -r ":$BUP_DIR3" -s "$BUP_DIR" --append save
WVPASS compare "$BUP_DIR" save "$BUP_DIR3" save
-WVPASSEQ "$(GIT_DIR=$BUP_DIR WVPASS git rev-parse save)" \
- "$(GIT_DIR=$BUP_DIR3 WVPASS git rev-parse save)"
+WVPASSEQ "$(GIT_DIR="$BUP_DIR" WVPASS git log --pretty=format:%T -n1 save)" \
+ "$(GIT_DIR="$BUP_DIR3" WVPASS git log --pretty=format:%T -n1 save)"
WVSTART rewrite after size not stored
# now do a hack to save without saving the size in metadata ...
@@ -102,19 +103,19 @@ WVPASS bup -d "$BUP_DIR" ls -l save/latest/test/sampledata/y/testfile1 |
WVPASS grep -- 158664
# now rewrite again - and then the size should be correct even without augmentation
-WVPASS bup -d "$BUP_DIR4" rewrite --work-db "$tmpdir/db" -s "$BUP_DIR" save:save2
+WVPASS bup -d "$BUP_DIR4" get --rewrite --rewrite-db "$tmpdir/db" -s "$BUP_DIR" --append: save save2
WVPASS bup+ -d "$BUP_DIR4" ls -l save/latest/test/sampledata/y/testfile1 |
WVPASS grep -- 158664
# and again for the other kind of splitting
-WVPASS bup -d "$BUP_DIR3" rewrite --work-db "$tmpdir/db" -s "$BUP_DIR" save:save2
-WVPASS bup+ -d "$BUP_DIR3" ls -l save/latest/test/sampledata/y/testfile1 |
+WVPASS bup -d "$BUP_DIR3" get --rewrite --rewrite-db "$tmpdir/db" -s "$BUP_DIR" --append: save save2
+WVPASS bup+ -d "$BUP_DIR3" ls -l save2/latest/test/sampledata/y/testfile1 |
WVPASS grep -- 158664
WVSTART rewrite with excluded files
WVPASS bup -d "$BUP_DIR5" init
-WVPASS bup -d "$BUP_DIR5" rewrite --work-db "$tmpdir/db2" -s "$BUP_DIR4" \
- --exclude-rx ^/test/sampledata/y/ save
+WVPASS bup -d "$BUP_DIR5" get --rewrite -s "$BUP_DIR4" \
+ --exclude-rx '^/test/sampledata/y/' --append save
WVPASS extract_all "$BUP_DIR4" "save" "orig"
WVPASS extract_all "$BUP_DIR5" "save" "new"
rm -rf "$tmpdir/restore/orig/"*"/test/sampledata/y/"
@@ -128,8 +129,8 @@ WVPASS rm -rf "$tmpdir/restore"
WVSTART "rewrite with excluded files (in repo)"
WVPASS git config -f "$BUP_DIR/config" bup.split.trees true
WVPASS git config -f "$BUP_DIR/config" bup.split.files legacy:14
-WVPASS bup -d "$BUP_DIR" rewrite --work-db "$tmpdir/db3" -s "$BUP_DIR" \
- --exclude-rx ^/test/sampledata/y/ save:save-new
+WVPASS bup -d "$BUP_DIR" get --rewrite -s "$BUP_DIR" \
+ --exclude-rx '^/test/sampledata/y/' --append: save save-new
WVPASS extract_all "$BUP_DIR" "save" "orig"
WVPASS extract_all "$BUP_DIR" "save-new" "new"
rm -rf "$tmpdir/restore/orig/"*"/test/sampledata/y/"
diff --git a/test/ext/test_get.py b/test/ext/test_get.py
index 7b29e99b..27bf3a24 100644
--- a/test/ext/test_get.py
+++ b/test/ext/test_get.py
@@ -150,18 +150,43 @@ def validate_commit(src_id, dest_id):
rmrf(b'restore-src')
rmrf(b'restore-dest')
-def _validate_save(orig_dir, save_path, commit_id, tree_id):
- global bup_cmd
+def _get_save_coid(save):
+ # FIXME: add/use some kind of ls dereference opt
+ exr = exo((bup_cmd, b'-d', b'get-dest', b'ls', b'-d', b'--commit-hash', save))
+ if exr.rc: return False
+ if not exr.out.endswith(b'/latest\n'):
+ coid = exr.out.split()[0]
+ assert len(coid) == 40, exr.out
+ return coid
+ exr = exo((bup_cmd, b'-d', b'get-dest', b'ls', b'--commit-hash', save))
+ if exr.rc: return False
+ lines = exr.out.splitlines()
+ # Is save branch or branch/latest?
+ if lines[-1].rsplit(maxsplit=2) == b'0' * 40:
+ coid = exr.out.splitlines()[-2].split()[0]
+ assert len(coid) == 40, exr.out
+ return coid
+ assert save.endswith(b'/latest'), save
+ return _get_save_coid(save[:-7])
+
+def _validate_save(orig_dir, save, save_subpath, commit_id, tree_id):
+ assert isinstance(commit_id, (bytes, type(None)))
+ assert isinstance(tree_id, (bytes, type(None)))
+ assert bool(tree_id) == bool(commit_id)
+
# Check parent connectivity, etc.
- ex((b'git', b'-P', b'--git-dir', b'get-dest', b'log', b'-n2', commit_id),
+ save_coid = _get_save_coid(save)
+ if not save_coid: return False
+ ex((b'git', b'-P', b'--git-dir', b'get-dest', b'log', b'-n2', save_coid),
stdin=DEVNULL)
rmrf(b'restore')
exr = verify_rcz((bup_cmd, b'-d', b'get-dest',
- b'restore', b'-C', b'restore', save_path + b'/.'))
+ b'restore', b'-C', b'restore',
+ save + b'/' + save_subpath + b'/.'))
if exr.rc: return False
verify_trees_match(orig_dir + b'/', b'restore/')
if tree_id:
- # FIXME: double check that get-dest is correct
+ wvpasseq(commit_id, save_coid)
exr = verify_rcz((b'git', b'--git-dir', b'get-dest', b'ls-tree', tree_id))
if exr.rc: return False
cat = verify_rcz((b'git', b'--git-dir', b'get-dest',
@@ -179,24 +204,29 @@ def validate_save(dest_name, restore_subpath, commit_id, tree_id, orig_value,
get_commit_id = out[1]
wvpasseq(tree_id, get_tree_id)
wvpasseq(commit_id, get_commit_id)
- _validate_save(orig_value, dest_name + restore_subpath, commit_id, tree_id)
+ _validate_save(orig_value, dest_name, restore_subpath, commit_id, tree_id)
def validate_new_save(dest_name, restore_subpath, commit_id, tree_id, orig_value,
- get_out):
+ get_out, *, rewrite=False):
out = get_out.splitlines()
wvpasseq(2, len(out))
- get_tree_id = out[0]
- get_commit_id = out[1]
- wvpasseq(tree_id, get_tree_id)
- wvpassne(commit_id, get_commit_id)
- _validate_save(orig_value, dest_name + restore_subpath, get_commit_id, tree_id)
-
+ get_tree_id, get_commit_id = out
+ if not rewrite:
+ wvpasseq(tree_id, get_tree_id)
+ wvpassne(commit_id, get_commit_id)
+ _validate_save(orig_value, dest_name, restore_subpath, get_commit_id,
+ tree_id)
+ else:
+ _validate_save(orig_value, dest_name, restore_subpath, get_commit_id,
+ get_tree_id)
+
def validate_tagged_save(tag_name, restore_subpath,
commit_id, tree_id, orig_value, get_out):
out = get_out.splitlines()
wvpasseq(1, len(out))
get_tag_id = out[0]
- wvpasseq(commit_id, get_tag_id)
+ if commit_id:
+ wvpasseq(commit_id, get_tag_id)
# Make sure tmp doesn't already exist.
exr = exo((b'git', b'--git-dir', b'get-dest', b'show-ref', b'tmp-branch-for-tag'),
check=False)
@@ -204,7 +234,7 @@ def validate_tagged_save(tag_name, restore_subpath,
ex((b'git', b'--git-dir', b'get-dest', b'branch', b'tmp-branch-for-tag',
b'refs/tags/' + tag_name))
- _validate_save(orig_value, b'tmp-branch-for-tag/latest' + restore_subpath,
+ _validate_save(orig_value, b'tmp-branch-for-tag/latest', restore_subpath,
commit_id, tree_id)
ex((b'git', b'--git-dir', b'get-dest', b'branch', b'-D', b'tmp-branch-for-tag'))
@@ -218,8 +248,8 @@ def validate_new_tagged_commit(tag_name, commit_id, tree_id, get_out):
ex((b'git', b'-P', b'--git-dir', b'get-dest', b'log', b'-n2', tag_name),
stdin=DEVNULL)
-def _run_get(disposition, method, what):
- print('run_get:', repr((disposition, method, what)), file=sys.stderr)
+def _run_get(disposition, method, what, rewrite=None):
+ assert rewrite in (True, False, type(None))
global bup_cmd
if disposition == 'get':
@@ -244,21 +274,27 @@ def _run_get(disposition, method, what):
method += b':'
src, dest = what
cmd = get_cmd + (method, src, dest)
+ if rewrite:
+ cmd += (b'--rewrite',)
+ elif rewrite == False:
+ cmd += (b'--no-rewrite',)
result = exo(cmd, check=False, stderr=PIPE)
fsck = ex((bup_cmd, b'-d', b'get-dest', b'fsck'), check=False)
wvpasseq(0, fsck.rc)
return result
-def run_get(disposition, method, what=None, given=None):
+def run_get(disposition, method, what=None, given=None, rewrite=False):
global bup_cmd
rmrf(b'get-dest')
ex((bup_cmd, b'-d', b'get-dest', b'init'))
-
+ if rewrite:
+ ex((b'git', b'--git-dir', b'get-dest', b'config', b'bup.split.trees', b'true'))
+ ex((b'git', b'--git-dir', b'get-dest', b'config', b'bup.split.files', b'legacy:16'))
if given:
# FIXME: replace bup-get with independent commands as is feasible
- exr = _run_get(disposition, b'--replace', given)
+ exr = _run_get(disposition, b'--replace', given, False)
assert not exr.rc
- return _run_get(disposition, method, what)
+ return _run_get(disposition, method, what, rewrite)
def _test_universal(get_disposition, src_info):
methods = (b'--ff', b'--append', b'--pick', b'--force-pick', b'--new-tag',
@@ -486,33 +522,41 @@ def _test_append(get_disposition, src_info):
subtree_vfs_path = src_info['subtree-vfs-path']
wvstart(get_disposition + ' --append to root fails')
- for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
- exr = run_get(get_disposition, b'--append', (item, b'/'))
+ for item, rewrite in \
+ product((b'.tag/tinyfile', b'src/latest' + tinyfile_path),
+ (False, True)):
+ exr = run_get(get_disposition, b'--append', (item, b'/'), rewrite=rewrite)
wvpassne(0, exr.rc)
verify_rx(br'source for .+ must be a branch, save, commit, or tree',
exr.err)
- for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path,
- b'.tag/commit-1', b'src/latest', b'src'):
- exr = run_get(get_disposition, b'--append', (item, b'/'))
+ for item, rewrite in \
+ product((b'.tag/subtree', b'src/latest' + subtree_vfs_path,
+ b'.tag/commit-1', b'src/latest', b'src'),
+ (False, True)):
+ exr = run_get(get_disposition, b'--append', (item, b'/'), rewrite=rewrite)
wvpassne(0, exr.rc)
verify_rx(br'destination for .+ is a root, not a branch', exr.err)
wvstart(get_disposition + ' --append of not-treeish fails')
- for src in (b'.tag/tinyfile', b'src/latest' + tinyfile_path):
+ for src, rewrite in product((b'.tag/tinyfile', b'src/latest' + tinyfile_path),
+ (False, True)):
for given, item in ((None, (src, b'obj')),
(None, (src, b'.tag/obj')),
((b'.tag/tinyfile', b'.tag/obj'), (src, b'.tag/obj')),
((b'.tag/tree-1', b'.tag/obj'), (src, b'.tag/obj')),
((b'.tag/commit-1', b'.tag/obj'), (src, b'.tag/obj')),
((b'.tag/commit-1', b'obj'), (src, b'obj'))):
- exr = run_get(get_disposition, b'--append', item, given=given)
+ exr = run_get(get_disposition, b'--append', item, given=given,
+ rewrite=rewrite)
wvpassne(0, exr.rc)
verify_rx(br'must be a branch, save, commit, or tree', exr.err)
wvstart(get_disposition + ' --append committish failure cases')
save_2 = src_info['save-2']
- for src in (b'.tag/subtree', b'src/latest' + subtree_vfs_path,
- b'.tag/commit-2', b'src/' + save_2, b'src'):
+ for src, rewrite in \
+ product((b'.tag/subtree', b'src/latest' + subtree_vfs_path,
+ b'.tag/commit-2', b'src/' + save_2, b'src'),
+ (False, True)):
for given, item, complaint in \
((None, (src, b'.tag/obj'),
br'destination .+ must be a valid branch name'),
@@ -524,36 +568,61 @@ def _test_append(get_disposition, src_info):
br'destination .+ is a tagged commit, not a branch'),
((b'.tag/commit-2', b'.tag/obj'), (src, b'.tag/obj'),
br'destination .+ is a tagged commit, not a branch')):
- exr = run_get(get_disposition, b'--append', item, given=given)
+ exr = run_get(get_disposition, b'--append', item, given=given,
+ rewrite=rewrite)
wvpassne(0, exr.rc)
verify_rx(complaint, exr.err)
+ wvstart(get_disposition + ' --append --rewrite SAVE currently unsupported')
+ # If we add support, consider the ancestor case too (see below)
+ for existing in (None,
+ (b'.tag/commit-1', b'obj'),
+ (b'.tag/commit-2', b'obj'),
+ (b'unrelated-branch', b'obj')):
+ exr = run_get(get_disposition, b'--append', (b'src/' + save_2, b'obj'),
+ given=existing, rewrite=True)
+ wvpassne(0, exr.rc)
+ verify_rx(br'cannot append', exr.err)
+
wvstart(get_disposition + ' --append committish')
commit_2_id = src_info['commit-2-id']
tree_2_id = src_info['tree-2-id']
- for item in (b'.tag/commit-2', b'src/' + save_2, b'src'):
+ for item, rewrite in ((b'.tag/commit-2', False),
+ (b'src/' + save_2, False),
+ (b'src', False),
+ (b'src', True)):
for existing in (None, (b'.tag/commit-1', b'obj'),
(b'.tag/commit-2', b'obj'),
(b'unrelated-branch', b'obj')):
exr = run_get(get_disposition, b'--append', (item, b'obj'),
- given=existing)
+ given=existing, rewrite=rewrite)
wvpasseq(0, exr.rc)
validate_new_save(b'obj/latest', getcwd() + b'/src',
- commit_2_id, tree_2_id, b'src-2', exr.out)
+ commit_2_id, tree_2_id, b'src-2', exr.out,
+ rewrite=rewrite)
verify_only_refs(heads=(b'obj',), tags=[])
# Append ancestor
save_1 = src_info['save-1']
commit_1_id = src_info['commit-1-id']
tree_1_id = src_info['tree-1-id']
- for item in (b'.tag/commit-1', b'src/' + save_1, b'src-1'):
+ for item, rewrite in ((b'.tag/commit-1', False),
+ (b'src/' + save_1, False),
+ (b'src-1', False),
+ (b'src-1', True)):
exr = run_get(get_disposition, b'--append', (item, b'obj'),
- given=(b'.tag/commit-2', b'obj'))
+ given=(b'.tag/commit-2', b'obj'),
+ rewrite=rewrite)
wvpasseq(0, exr.rc)
validate_new_save(b'obj/latest', getcwd() + b'/src',
- commit_1_id, tree_1_id, b'src-1', exr.out)
+ commit_1_id, tree_1_id, b'src-1', exr.out,
+ rewrite=rewrite)
verify_only_refs(heads=(b'obj',), tags=[])
wvstart(get_disposition + ' --append tree')
+ exr = run_get(get_disposition, b'--append', (b'.tag/subtree', b'obj'),
+ given=None, rewrite=True)
+ wvpassne(0, exr.rc)
+ verify_rx(br'cannot append', exr.err)
subtree_path = src_info['subtree-path']
subtree_id = src_info['subtree-id']
for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
@@ -568,12 +637,16 @@ def _test_append(get_disposition, src_info):
verify_only_refs(heads=(b'obj',), tags=[])
wvstart(get_disposition + ' --append, implicit destinations')
-
- for item in (b'src', b'src/latest'):
- exr = run_get(get_disposition, b'--append', item)
+ exr = run_get(get_disposition, b'--append', b'src/latest', rewrite=True)
+ wvpassne(0, exr.rc)
+ verify_rx(br'cannot append', exr.err)
+ for item, rewrite in ((b'src', False),
+ (b'src', True),
+ (b'src/latest', False)):
+ exr = run_get(get_disposition, b'--append', item, rewrite=rewrite)
wvpasseq(0, exr.rc)
validate_new_save(b'src/latest', getcwd() + b'/src', commit_2_id, tree_2_id,
- b'src-2', exr.out)
+ b'src-2', exr.out, rewrite=rewrite)
verify_only_refs(heads=(b'src',), tags=[])
def _test_pick_common(get_disposition, src_info, force=False):
@@ -581,128 +654,175 @@ def _test_pick_common(get_disposition, src_info, force=False):
flavormsg = flavor.decode('ascii')
tinyfile_path = src_info['tinyfile-path']
subtree_vfs_path = src_info['subtree-vfs-path']
-
- wvstart(get_disposition + ' ' + flavormsg + ' to root fails')
- for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path, b'src'):
- exr = run_get(get_disposition, flavor, (item, b'/'))
+
+ wvstart(f'{get_disposition} {flavormsg} to root fails')
+ for item, rewrite in \
+ product((b'.tag/tinyfile', b'src/latest' + tinyfile_path, b'src'),
+ (False, True)):
+ exr = run_get(get_disposition, flavor, (item, b'/'), rewrite=rewrite)
wvpassne(0, exr.rc)
verify_rx(br'can only pick a commit or save', exr.err)
- for item in (b'.tag/commit-1', b'src/latest'):
- exr = run_get(get_disposition, flavor, (item, b'/'))
+ for item, rewrite in ((b'.tag/commit-1', False),
+ (b'src/latest', False),
+ (b'src/latest', True)):
+ exr = run_get(get_disposition, flavor, (item, b'/'), rewrite=rewrite)
wvpassne(0, exr.rc)
verify_rx(br'destination is not a tag or branch', exr.err)
- for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
- exr = run_get(get_disposition, flavor, (item, b'/'))
+ for item, rewrite in \
+ product((b'.tag/subtree', b'src/latest' + subtree_vfs_path),
+ (False, True)):
+ exr = run_get(get_disposition, flavor, (item, b'/'), rewrite=rewrite)
wvpassne(0, exr.rc)
verify_rx(br'is impossible; can only --append a tree', exr.err)
- wvstart(get_disposition + ' ' + flavormsg + ' of blob or branch fails')
- for item in (b'.tag/tinyfile', b'src/latest' + tinyfile_path, b'src'):
+ wvstart(f'{get_disposition} {flavormsg} of blob or branch fails')
+ for item, rewrite in \
+ product((b'.tag/tinyfile', b'src/latest' + tinyfile_path, b'src'),
+ (False, True)):
for given, get_item in ((None, (item, b'obj')),
(None, (item, b'.tag/obj')),
((b'.tag/tinyfile', b'.tag/obj'), (item, b'.tag/obj')),
((b'.tag/tree-1', b'.tag/obj'), (item, b'.tag/obj')),
((b'.tag/commit-1', b'.tag/obj'), (item, b'.tag/obj')),
((b'.tag/commit-1', b'obj'), (item, b'obj'))):
- exr = run_get(get_disposition, flavor, get_item, given=given)
+ exr = run_get(get_disposition, flavor, get_item, given=given,
+ rewrite=rewrite)
wvpassne(0, exr.rc)
verify_rx(br'impossible; can only pick a commit or save', exr.err)
- wvstart(get_disposition + ' ' + flavormsg + ' of tree fails')
- for item in (b'.tag/subtree', b'src/latest' + subtree_vfs_path):
+ wvstart(f'{get_disposition} {flavormsg} of tree fails')
+ for item, rewrite in \
+ product((b'.tag/subtree', b'src/latest' + subtree_vfs_path),
+ (False, True)):
for given, get_item in ((None, (item, b'obj')),
(None, (item, b'.tag/obj')),
((b'.tag/tinyfile', b'.tag/obj'), (item, b'.tag/obj')),
((b'.tag/tree-1', b'.tag/obj'), (item, b'.tag/obj')),
((b'.tag/commit-1', b'.tag/obj'), (item, b'.tag/obj')),
((b'.tag/commit-1', b'obj'), (item, b'obj'))):
- exr = run_get(get_disposition, flavor, get_item, given=given)
+ exr = run_get(get_disposition, flavor, get_item, given=given,
+ rewrite=rewrite)
wvpassne(0, exr.rc)
verify_rx(br'impossible; can only --append a tree', exr.err)
+ wvstart(f'{get_disposition} {flavormsg} --rewrite of non-saves fails')
+ # Only --rewrite case currently not rejected more generally above
+ exr = run_get(get_disposition, flavor, (b'/.tag/commit-1', b'/'),
+ rewrite=True)
+ wvpassne(0, exr.rc)
+ verify_rx(br'cannot currently --rewrite a commit', exr.err)
+
save_2 = src_info['save-2']
commit_2_id = src_info['commit-2-id']
tree_2_id = src_info['tree-2-id']
# FIXME: these two wvstart texts?
if force:
- wvstart(get_disposition + ' ' + flavormsg + ' commit/save to existing tag')
- for item in (b'.tag/commit-2', b'src/' + save_2):
+ wvstart(f'{get_disposition} {flavormsg} commit/save to existing tag')
+ for item, rewrite in ((b'.tag/commit-2', False),
+ (b'src/' + save_2, False),
+ (b'src/' + save_2, True)):
for given in ((b'.tag/tinyfile', b'.tag/obj'),
(b'.tag/tree-1', b'.tag/obj'),
(b'.tag/commit-1', b'.tag/obj')):
exr = run_get(get_disposition, flavor, (item, b'.tag/obj'),
- given=given)
+ given=given, rewrite=rewrite)
wvpasseq(0, exr.rc)
- validate_new_tagged_commit(b'obj', commit_2_id, tree_2_id,
- exr.out)
+ if rewrite:
+ validate_tagged_save(b'obj', getcwd() + b'/src', None, None,
+ b'src-2', exr.out)
+ else:
+ validate_new_tagged_commit(b'obj', commit_2_id, tree_2_id,
+ exr.out)
verify_only_refs(heads=[], tags=(b'obj',))
else: # --pick
- wvstart(get_disposition + ' ' + flavormsg
- + ' commit/save to existing tag fails')
- for item in (b'.tag/commit-2', b'src/' + save_2):
+ wvstart(f'{get_disposition} {flavormsg} commit/save to existing tag fails')
+ for item, rewrite in ((b'.tag/commit-2', False),
+ (b'src/' + save_2, False),
+ (b'src/' + save_2, True)):
for given in ((b'.tag/tinyfile', b'.tag/obj'),
(b'.tag/tree-1', b'.tag/obj'),
(b'.tag/commit-1', b'.tag/obj')):
- exr = run_get(get_disposition, flavor, (item, b'.tag/obj'), given=given)
+ exr = run_get(get_disposition, flavor, (item, b'.tag/obj'),
+ given=given, rewrite=rewrite)
wvpassne(0, exr.rc)
verify_rx(br'cannot overwrite existing tag', exr.err)
-
- wvstart(get_disposition + ' ' + flavormsg + ' commit/save to tag')
- for item in (b'.tag/commit-2', b'src/' + save_2):
- exr = run_get(get_disposition, flavor, (item, b'.tag/obj'))
+
+ wvstart(f'{get_disposition} {flavormsg} commit/save to tag')
+ for item, rewrite in ((b'.tag/commit-2', False),
+ (b'src/' + save_2, False),
+ (b'src/' + save_2, True)):
+ exr = run_get(get_disposition, flavor, (item, b'.tag/obj'),
+ rewrite=rewrite)
wvpasseq(0, exr.rc)
validate_clean_repo()
- validate_new_tagged_commit(b'obj', commit_2_id, tree_2_id, exr.out)
+ if rewrite:
+ validate_tagged_save(b'obj', getcwd() + b'/src', None, None,
+ b'src-2', exr.out)
+ else:
+ validate_new_tagged_commit(b'obj', commit_2_id, tree_2_id, exr.out)
verify_only_refs(heads=[], tags=(b'obj',))
-
- wvstart(get_disposition + ' ' + flavormsg + ' commit/save to branch')
- for item in (b'.tag/commit-2', b'src/' + save_2):
+
+ wvstart(f'{get_disposition} {flavormsg} commit/save to branch')
+ for item, rewrite in ((b'.tag/commit-2', False),
+ (b'src/' + save_2, False),
+ (b'src/' + save_2, True)):
for given in (None, (b'.tag/commit-1', b'obj'), (b'.tag/commit-2', b'obj')):
- exr = run_get(get_disposition, flavor, (item, b'obj'), given=given)
+ exr = run_get(get_disposition, flavor, (item, b'obj'), given=given,
+ rewrite=rewrite)
wvpasseq(0, exr.rc)
+ ex((bup_cmd, b'-d', b'get-dest', b'ls', b'--commit-hash', b'obj'))
validate_clean_repo()
validate_new_save(b'obj/latest', getcwd() + b'/src',
- commit_2_id, tree_2_id, b'src-2', exr.out)
+ commit_2_id, tree_2_id, b'src-2', exr.out,
+ rewrite=rewrite)
verify_only_refs(heads=(b'obj',), tags=[])
- wvstart(get_disposition + ' ' + flavormsg
- + ' commit/save unrelated commit to branch')
- for item in(b'.tag/commit-2', b'src/' + save_2):
+ wvstart(f'{get_disposition} {flavormsg} commit/save unrelated commit to branch')
+ for item, rewrite in ((b'.tag/commit-2', False),
+ (b'src/' + save_2, False),
+ (b'src/' + save_2, True)):
exr = run_get(get_disposition, flavor, (item, b'obj'),
- given=(b'unrelated-branch', b'obj'))
+ given=(b'unrelated-branch', b'obj'),
+ rewrite=rewrite)
wvpasseq(0, exr.rc)
validate_clean_repo()
validate_new_save(b'obj/latest', getcwd() + b'/src',
- commit_2_id, tree_2_id, b'src-2', exr.out)
+ commit_2_id, tree_2_id, b'src-2', exr.out,
+ rewrite=rewrite)
verify_only_refs(heads=(b'obj',), tags=[])
- wvstart(get_disposition + ' ' + flavormsg + ' commit/save ancestor to branch')
+ wvstart(f'{get_disposition} {flavormsg} commit/save ancestor to branch')
save_1 = src_info['save-1']
commit_1_id = src_info['commit-1-id']
tree_1_id = src_info['tree-1-id']
- for item in (b'.tag/commit-1', b'src/' + save_1):
+ for item, rewrite in ((b'.tag/commit-1', False),
+ (b'src/' + save_1, False),
+ (b'src/' + save_1, True)):
exr = run_get(get_disposition, flavor, (item, b'obj'),
- given=(b'.tag/commit-2', b'obj'))
+ given=(b'.tag/commit-2', b'obj'),
+ rewrite=rewrite)
wvpasseq(0, exr.rc)
validate_clean_repo()
validate_new_save(b'obj/latest', getcwd() + b'/src',
- commit_1_id, tree_1_id, b'src-1', exr.out)
+ commit_1_id, tree_1_id, b'src-1', exr.out,
+ rewrite=rewrite)
verify_only_refs(heads=(b'obj',), tags=[])
-
- wvstart(get_disposition + ' ' + flavormsg + ', implicit destinations')
+ wvstart(f'{get_disposition} {flavormsg} implicit destinations')
exr = run_get(get_disposition, flavor, b'.tag/commit-2')
wvpasseq(0, exr.rc)
validate_clean_repo()
validate_new_tagged_commit(b'commit-2', commit_2_id, tree_2_id, exr.out)
verify_only_refs(heads=[], tags=(b'commit-2',))
- exr = run_get(get_disposition, flavor, b'src/latest')
- wvpasseq(0, exr.rc)
- validate_clean_repo()
- validate_new_save(b'src/latest', getcwd() + b'/src',
- commit_2_id, tree_2_id, b'src-2', exr.out)
- verify_only_refs(heads=(b'src',), tags=[])
+ for rewrite in False, True:
+ exr = run_get(get_disposition, flavor, b'src/latest', rewrite=rewrite)
+ wvpasseq(0, exr.rc)
+ validate_clean_repo()
+ validate_new_save(b'src/latest', getcwd() + b'/src',
+ commit_2_id, tree_2_id, b'src-2', exr.out,
+ rewrite=rewrite)
+ verify_only_refs(heads=(b'src',), tags=[])
def _test_pick_force(get_disposition, src_info):
_test_pick_common(get_disposition, src_info, force=True)
@@ -887,7 +1007,7 @@ def create_get_src():
mkdir(b'src/x')
mkdir(b'src/x/y')
ex((bup_cmd + b' -d get-src random 1k > src/1'), shell=True)
- ex((bup_cmd + b' -d get-src random 1k > src/x/2'), shell=True)
+ ex((bup_cmd + b' -d get-src random 1m > src/x/2'), shell=True)
ex((bup_cmd, b'-d', b'get-src', b'index', b'src'))
exr = exo((bup_cmd, b'-d', b'get-src', b'save', b'-tcn', b'src', b'src'))
out = exr.out.splitlines()
--
2.47.3