|
1 # repair.py - functions for repository repair for mercurial |
|
2 # |
|
3 # Copyright 2005, 2006 Chris Mason <mason@suse.com> |
|
4 # Copyright 2007 Matt Mackall |
|
5 # |
|
6 # This software may be used and distributed according to the terms of the |
|
7 # GNU General Public License version 2 or any later version. |
|
8 |
|
9 import changegroup |
|
10 from node import nullrev, short |
|
11 from i18n import _ |
|
12 import os |
|
13 |
|
14 def _bundle(repo, bases, heads, node, suffix, extranodes=None, compress=True): |
|
15 """create a bundle with the specified revisions as a backup""" |
|
16 cg = repo.changegroupsubset(bases, heads, 'strip', extranodes) |
|
17 backupdir = repo.join("strip-backup") |
|
18 if not os.path.isdir(backupdir): |
|
19 os.mkdir(backupdir) |
|
20 name = os.path.join(backupdir, "%s-%s.hg" % (short(node), suffix)) |
|
21 if compress: |
|
22 bundletype = "HG10BZ" |
|
23 else: |
|
24 bundletype = "HG10UN" |
|
25 return changegroup.writebundle(cg, name, bundletype) |
|
26 |
|
27 def _collectfiles(repo, striprev): |
|
28 """find out the filelogs affected by the strip""" |
|
29 files = set() |
|
30 |
|
31 for x in xrange(striprev, len(repo)): |
|
32 files.update(repo[x].files()) |
|
33 |
|
34 return sorted(files) |
|
35 |
|
36 def _collectextranodes(repo, files, link): |
|
37 """return the nodes that have to be saved before the strip""" |
|
38 def collectone(cl, revlog): |
|
39 extra = [] |
|
40 startrev = count = len(revlog) |
|
41 # find the truncation point of the revlog |
|
42 for i in xrange(count): |
|
43 lrev = revlog.linkrev(i) |
|
44 if lrev >= link: |
|
45 startrev = i + 1 |
|
46 break |
|
47 |
|
48 # see if any revision after that point has a linkrev less than link |
|
49 # (we have to manually save these guys) |
|
50 for i in xrange(startrev, count): |
|
51 node = revlog.node(i) |
|
52 lrev = revlog.linkrev(i) |
|
53 if lrev < link: |
|
54 extra.append((node, cl.node(lrev))) |
|
55 |
|
56 return extra |
|
57 |
|
58 extranodes = {} |
|
59 cl = repo.changelog |
|
60 extra = collectone(cl, repo.manifest) |
|
61 if extra: |
|
62 extranodes[1] = extra |
|
63 for fname in files: |
|
64 f = repo.file(fname) |
|
65 extra = collectone(cl, f) |
|
66 if extra: |
|
67 extranodes[fname] = extra |
|
68 |
|
69 return extranodes |
|
70 |
|
71 def strip(ui, repo, node, backup="all"): |
|
72 cl = repo.changelog |
|
73 # TODO delete the undo files, and handle undo of merge sets |
|
74 striprev = cl.rev(node) |
|
75 |
|
76 keeppartialbundle = backup == 'strip' |
|
77 |
|
78 # Some revisions with rev > striprev may not be descendants of striprev. |
|
79 # We have to find these revisions and put them in a bundle, so that |
|
80 # we can restore them after the truncations. |
|
81 # To create the bundle we use repo.changegroupsubset which requires |
|
82 # the list of heads and bases of the set of interesting revisions. |
|
83 # (head = revision in the set that has no descendant in the set; |
|
84 # base = revision in the set that has no ancestor in the set) |
|
85 tostrip = set((striprev,)) |
|
86 saveheads = set() |
|
87 savebases = [] |
|
88 for r in xrange(striprev + 1, len(cl)): |
|
89 parents = cl.parentrevs(r) |
|
90 if parents[0] in tostrip or parents[1] in tostrip: |
|
91 # r is a descendant of striprev |
|
92 tostrip.add(r) |
|
93 # if this is a merge and one of the parents does not descend |
|
94 # from striprev, mark that parent as a savehead. |
|
95 if parents[1] != nullrev: |
|
96 for p in parents: |
|
97 if p not in tostrip and p > striprev: |
|
98 saveheads.add(p) |
|
99 else: |
|
100 # if no parents of this revision will be stripped, mark it as |
|
101 # a savebase |
|
102 if parents[0] < striprev and parents[1] < striprev: |
|
103 savebases.append(cl.node(r)) |
|
104 |
|
105 saveheads.difference_update(parents) |
|
106 saveheads.add(r) |
|
107 |
|
108 saveheads = [cl.node(r) for r in saveheads] |
|
109 files = _collectfiles(repo, striprev) |
|
110 |
|
111 extranodes = _collectextranodes(repo, files, striprev) |
|
112 |
|
113 # create a changegroup for all the branches we need to keep |
|
114 backupfile = None |
|
115 if backup == "all": |
|
116 backupfile = _bundle(repo, [node], cl.heads(), node, 'backup') |
|
117 repo.ui.status(_("saved backup bundle to %s\n") % backupfile) |
|
118 if saveheads or extranodes: |
|
119 # do not compress partial bundle if we remove it from disk later |
|
120 chgrpfile = _bundle(repo, savebases, saveheads, node, 'temp', |
|
121 extranodes=extranodes, compress=keeppartialbundle) |
|
122 |
|
123 mfst = repo.manifest |
|
124 |
|
125 tr = repo.transaction("strip") |
|
126 offset = len(tr.entries) |
|
127 |
|
128 try: |
|
129 tr.startgroup() |
|
130 cl.strip(striprev, tr) |
|
131 mfst.strip(striprev, tr) |
|
132 for fn in files: |
|
133 repo.file(fn).strip(striprev, tr) |
|
134 tr.endgroup() |
|
135 |
|
136 try: |
|
137 for i in xrange(offset, len(tr.entries)): |
|
138 file, troffset, ignore = tr.entries[i] |
|
139 repo.sopener(file, 'a').truncate(troffset) |
|
140 tr.close() |
|
141 except: |
|
142 tr.abort() |
|
143 raise |
|
144 |
|
145 if saveheads or extranodes: |
|
146 ui.note(_("adding branch\n")) |
|
147 f = open(chgrpfile, "rb") |
|
148 gen = changegroup.readbundle(f, chgrpfile) |
|
149 if not repo.ui.verbose: |
|
150 # silence internal shuffling chatter |
|
151 repo.ui.pushbuffer() |
|
152 repo.addchangegroup(gen, 'strip', 'bundle:' + chgrpfile, True) |
|
153 if not repo.ui.verbose: |
|
154 repo.ui.popbuffer() |
|
155 f.close() |
|
156 if not keeppartialbundle: |
|
157 os.unlink(chgrpfile) |
|
158 except: |
|
159 if backupfile: |
|
160 ui.warn(_("strip failed, full bundle stored in '%s'\n") |
|
161 % backupfile) |
|
162 elif saveheads: |
|
163 ui.warn(_("strip failed, partial bundle stored in '%s'\n") |
|
164 % chgrpfile) |
|
165 raise |
|
166 |
|
167 repo.destroyed() |