]> arthur.barton.de Git - bup.git/blob - cmd/rm-cmd.py
Add "bup rm", but require --unsafe invocation
[bup.git] / cmd / rm-cmd.py
1 #!/usr/bin/env python
2
3 import sys
4
5 from bup import client, git, options, vfs
6 from bup.git import get_commit_items
7 from bup.helpers import add_error, handle_ctrl_c, log, saved_errors
8
9 optspec = """
10 bup rm <branch|save...>
11 --
12 #,compress=  set compression level to # (0-9, 9 is highest) [6]
13 v,verbose    increase verbosity (can be specified multiple times)
14 unsafe       use the command even though it may be DANGEROUS
15 """
16
17 def append_commit(hash, parent, cp, writer):
18     ci = get_commit_items(hash, cp)
19     tree = ci.tree.decode('hex')
20     author = '%s <%s>' % (ci.author_name, ci.author_mail)
21     committer = '%s <%s>' % (ci.committer_name, ci.committer_mail)
22     c = writer.new_commit(tree, parent,
23                           author, ci.author_sec, ci.author_offset,
24                           committer, ci.committer_sec, ci.committer_offset,
25                           ci.message)
26     return c, tree
27
28
29 def filter_branch(tip_commit_hex, exclude, writer):
30     # May return None if everything is excluded.
31     commits = [c for _, c in git.rev_list(tip_commit_hex)]
32     commits.reverse()
33     last_c, tree = None, None
34     # Rather than assert that we always find an exclusion here, we'll
35     # just let the StopIteration signal the error.
36     first_exclusion = next(i for i, c in enumerate(commits) if exclude(c))
37     if first_exclusion != 0:
38         last_c = commits[first_exclusion - 1]
39         tree = get_commit_items(last_c.encode('hex'),
40                                 git.cp()).tree.decode('hex')
41         commits = commits[first_exclusion:]
42     for c in commits:
43         if exclude(c):
44             continue
45         last_c, tree = append_commit(c.encode('hex'), last_c, git.cp(), writer)
46     return last_c
47
48
49 def rm_saves(saves, writer):
50     assert(saves)
51     branch_node = saves[0].parent
52     for save in saves: # Be certain they're all on the same branch
53         assert(save.parent == branch_node)
54     rm_commits = frozenset([x.dereference().hash for x in saves])
55     orig_tip = branch_node.hash
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(vfs_top, 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             n = vfs_top.lresolve(path)
73         except vfs.NodeError as e:
74             add_error('unable to resolve %s: %s' % (path, e))
75         else:
76             if isinstance(n, vfs.BranchList): # rm /foo
77                 branchname = n.name
78                 dead_branches[branchname] = n
79                 dead_saves.pop(branchname, None) # rm /foo obviates rm /foo/bar
80             elif isinstance(n, vfs.FakeSymlink) and isinstance(n.parent,
81                                                                vfs.BranchList):
82                 if n.name == 'latest':
83                     add_error("error: cannot delete 'latest' symlink")
84                 else:
85                     branchname = n.parent.name
86                     if branchname not in dead_branches:
87                         dead_saves.setdefault(branchname, []).append(n)
88             else:
89                 add_error("don't know how to remove %r yet" % n.fullname())
90     if saved_errors:
91         return None, None
92     return dead_branches, dead_saves
93
94
95 handle_ctrl_c()
96
97 o = options.Options(optspec)
98 opt, flags, extra = o.parse(sys.argv[1:])
99
100 if not opt.unsafe:
101     o.fatal('refusing to run dangerous, experimental command without --unsafe')
102
103 if len(extra) < 1:
104     o.fatal('no paths specified')
105
106 paths = extra
107
108 git.check_repo_or_die()
109 top = vfs.RefList(None)
110
111 dead_branches, dead_saves = dead_items(top, paths)
112 if saved_errors:
113     log('not proceeding with any removals\n')
114     sys.exit(1)
115
116 updated_refs = {}  # ref_name -> (original_ref, tip_commit(bin))
117 writer = None
118
119 if dead_saves:
120     writer = git.PackWriter(compression_level=opt.compress)
121
122 for branch, saves in dead_saves.iteritems():
123     assert(saves)
124     updated_refs['refs/heads/' + branch] = rm_saves(saves, writer)
125
126 for branch, node in dead_branches.iteritems():
127     ref = 'refs/heads/' + branch
128     assert(not ref in updated_refs)
129     updated_refs[ref] = (node.hash, None)
130
131 if writer:
132     # Must close before we can update the ref(s) below.
133     writer.close()
134
135 # Only update the refs here, at the very end, so that if something
136 # goes wrong above, the old refs will be undisturbed.  Make an attempt
137 # to update each ref.
138 for ref_name, info in updated_refs.iteritems():
139     orig_ref, new_ref = info
140     try:
141         if not new_ref:
142             git.delete_ref(ref_name, orig_ref.encode('hex'))
143         else:
144             git.update_ref(ref_name, new_ref, orig_ref)
145             if opt.verbose:
146                 new_hex = new_ref.encode('hex')
147                 if orig_ref:
148                     orig_hex = orig_ref.encode('hex')
149                     log('updated %r (%s -> %s)\n'
150                         % (ref_name, orig_hex, new_hex))
151                 else:
152                     log('updated %r (%s)\n' % (ref_name, new_hex))
153     except (git.GitError, client.ClientError) as ex:
154         if new_ref:
155             add_error('while trying to update %r (%s -> %s): %s'
156                       % (ref_name, orig_ref, new_ref, ex))
157         else:
158             add_error('while trying to delete %r (%s): %s'
159                       % (ref_name, orig_ref, ex))
160
161 if saved_errors:
162     log('warning: %d errors encountered\n' % len(saved_errors))
163     sys.exit(1)