e43abc5b7e1de1ac3b08e892dea71e35e6961447
[bup.git] / lib / bup / rm.py
1
2 from __future__ import absolute_import
3 import sys
4
5 from bup import compat, git, vfs
6 from bup.client import ClientError
7 from bup.compat import hexstr
8 from bup.git import get_commit_items
9 from bup.helpers import add_error, die_if_errors, log, saved_errors
10
11
12 def append_commit(hash, parent, cp, writer):
13     ci = get_commit_items(hash, cp)
14     tree = ci.tree.decode('hex')
15     author = '%s <%s>' % (ci.author_name, ci.author_mail)
16     committer = '%s <%s>' % (ci.committer_name, ci.committer_mail)
17     c = writer.new_commit(tree, parent,
18                           author, ci.author_sec, ci.author_offset,
19                           committer, ci.committer_sec, ci.committer_offset,
20                           ci.message)
21     return c, tree
22
23
24 def filter_branch(tip_commit_hex, exclude, writer):
25     # May return None if everything is excluded.
26     commits = [x.decode('hex') for x in git.rev_list(tip_commit_hex)]
27     commits.reverse()
28     last_c, tree = None, None
29     # Rather than assert that we always find an exclusion here, we'll
30     # just let the StopIteration signal the error.
31     first_exclusion = next(i for i, c in enumerate(commits) if exclude(c))
32     if first_exclusion != 0:
33         last_c = commits[first_exclusion - 1]
34         tree = get_commit_items(last_c.encode('hex'),
35                                 git.cp()).tree.decode('hex')
36         commits = commits[first_exclusion:]
37     for c in commits:
38         if exclude(c):
39             continue
40         last_c, tree = append_commit(c.encode('hex'), last_c, git.cp(), writer)
41     return last_c
42
43 def commit_oid(item):
44     if isinstance(item, vfs.Commit):
45         return item.coid
46     assert isinstance(item, vfs.RevList)
47     return item.oid
48
49 def 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(orig_tip.encode('hex'),
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
64 def 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 %r in %r'
80                           % ('/'.join(name for name, item in resolved),
81                              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 == '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 %r yet" % path)
97     if saved_errors:
98         return None, None
99     return dead_branches, dead_saves
100
101
102 def 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 = '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['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, orig_ref.encode('hex'))
136             else:
137                 git.update_ref(ref_name, new_ref, orig_ref)
138                 if verbosity:
139                     log('updated %r (%s%s)\n'
140                         % (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 %r (%s%s): %s'
146                           % (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))