1 2from __future__ import absolute_import 3from binascii import hexlify, unhexlify 4import sys 5 6from bup import compat, git, vfs 7from bup.client import ClientError 8from bup.compat import hexstr 9from bup.git import get_commit_items 10from bup.helpers import add_error, die_if_errors, log, saved_errors 11from bup.io import path_msg 12 13def append_commit(hash, parent, cp, writer): 14 ci = get_commit_items(hash, cp) 15 tree = unhexlify(ci.tree) 16 author = b'%s <%s>' % (ci.author_name, ci.author_mail) 17 committer = b'%s <%s>' % (ci.committer_name, ci.committer_mail) 18 c = writer.new_commit(tree, parent, 19 author, ci.author_sec, ci.author_offset, 20 committer, ci.committer_sec, ci.committer_offset, 21 ci.message) 22 return c, tree 23 24 25def filter_branch(tip_commit_hex, exclude, writer): 26 # May return None if everything is excluded. 27 commits = [unhexlify(x) for x in git.rev_list(tip_commit_hex)] 28 commits.reverse() 29 last_c, tree = None, None 30 # Rather than assert that we always find an exclusion here, we'll 31 # just let the StopIteration signal the error. 32 first_exclusion = next(i for i, c in enumerate(commits) if exclude(c)) 33 if first_exclusion != 0: 34 last_c = commits[first_exclusion - 1] 35 tree = unhexlify(get_commit_items(hexlify(last_c), git.cp()).tree) 36 commits = commits[first_exclusion:] 37 for c in commits: 38 if exclude(c): 39 continue 40 last_c, tree = append_commit(hexlify(c), last_c, git.cp(), writer) 41 return last_c 42 43def commit_oid(item): 44 if isinstance(item, vfs.Commit): 45 return item.coid 46 assert isinstance(item, vfs.RevList) 47 return item.oid 48 49def rm_saves(saves, writer): 50 assert(saves) 51 first_branch_item = saves[0][1] 52 for save, branch in saves: # Be certain they're all on the same branch 53 assert(branch == first_branch_item) 54 rm_commits = frozenset([commit_oid(save) for save, branch in saves]) 55 orig_tip = commit_oid(first_branch_item) 56 new_tip = filter_branch(hexlify(orig_tip), 57 lambda x: x in rm_commits, 58 writer) 59 assert(orig_tip) 60 assert(new_tip != orig_tip) 61 return orig_tip, new_tip 62 63 64def dead_items(repo, paths): 65 """Return an optimized set of removals, reporting errors via 66 add_error, and if there are any errors, return None, None.""" 67 dead_branches = {} 68 dead_saves = {} 69 # Scan for bad requests, and opportunities to optimize 70 for path in paths: 71 try: 72 resolved = vfs.resolve(repo, path, follow=False) 73 except vfs.IOError as e: 74 add_error(e) 75 continue 76 else: 77 leaf_name, leaf_item = resolved[-1] 78 if not leaf_item: 79 add_error('error: cannot access %s in %s' 80 % (path_msg(b'/'.join(name for name, item in resolved)), 81 path_msg(path))) 82 continue 83 if isinstance(leaf_item, vfs.RevList): # rm /foo 84 branchname = leaf_name 85 dead_branches[branchname] = leaf_item 86 dead_saves.pop(branchname, None) # rm /foo obviates rm /foo/bar 87 elif isinstance(leaf_item, vfs.Commit): # rm /foo/bar 88 if leaf_name == b'latest': 89 add_error("error: cannot delete 'latest' symlink") 90 else: 91 branchname, branchitem = resolved[-2] 92 if branchname not in dead_branches: 93 dead = leaf_item, branchitem 94 dead_saves.setdefault(branchname, []).append(dead) 95 else: 96 add_error("don't know how to remove %s yet" % path_msg(path)) 97 if saved_errors: 98 return None, None 99 return dead_branches, dead_saves 100 101 102def bup_rm(repo, paths, compression=6, verbosity=None): 103 dead_branches, dead_saves = dead_items(repo, paths) 104 die_if_errors('not proceeding with any removals\n') 105 106 updated_refs = {} # ref_name -> (original_ref, tip_commit(bin)) 107 108 for branchname, branchitem in compat.items(dead_branches): 109 ref = b'refs/heads/' + branchname 110 assert(not ref in updated_refs) 111 updated_refs[ref] = (branchitem.oid, None) 112 113 if dead_saves: 114 writer = git.PackWriter(compression_level=compression) 115 try: 116 for branch, saves in compat.items(dead_saves): 117 assert(saves) 118 updated_refs[b'refs/heads/' + branch] = rm_saves(saves, writer) 119 except: 120 if writer: 121 writer.abort() 122 raise 123 else: 124 if writer: 125 # Must close before we can update the ref(s) below. 126 writer.close() 127 128 # Only update the refs here, at the very end, so that if something 129 # goes wrong above, the old refs will be undisturbed. Make an attempt 130 # to update each ref. 131 for ref_name, info in compat.items(updated_refs): 132 orig_ref, new_ref = info 133 try: 134 if not new_ref: 135 git.delete_ref(ref_name, hexlify(orig_ref)) 136 else: 137 git.update_ref(ref_name, new_ref, orig_ref) 138 if verbosity: 139 log('updated %s (%s%s)\n' 140 % (path_msg(ref_name), 141 hexstr(orig_ref) + ' -> ' if orig_ref else '', 142 hexstr(new_ref))) 143 except (git.GitError, ClientError) as ex: 144 if new_ref: 145 add_error('while trying to update %s (%s%s): %s' 146 % (path_msg(ref_name), 147 hexstr(orig_ref) + ' -> ' if orig_ref else '', 148 hexstr(new_ref), 149 ex)) 150 else: 151 add_error('while trying to delete %r (%s): %s' 152 % (ref_name, hexstr(orig_ref), ex)) 153