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