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