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