|
1 from __future__ import absolute_import |
|
2 |
|
3 from .i18n import _ |
|
4 from .pycompat import getattr |
|
5 from . import ( |
|
6 bookmarks as bookmarksmod, |
|
7 cmdutil, |
|
8 error, |
|
9 hg, |
|
10 lock as lockmod, |
|
11 mergestate as mergestatemod, |
|
12 node as nodemod, |
|
13 pycompat, |
|
14 registrar, |
|
15 repair, |
|
16 scmutil, |
|
17 util, |
|
18 ) |
|
19 |
|
20 nullid = nodemod.nullid |
|
21 release = lockmod.release |
|
22 |
|
23 cmdtable = {} |
|
24 command = registrar.command(cmdtable) |
|
25 |
|
26 |
|
27 def checklocalchanges(repo, force=False): |
|
28 s = repo.status() |
|
29 if not force: |
|
30 cmdutil.checkunfinished(repo) |
|
31 cmdutil.bailifchanged(repo) |
|
32 else: |
|
33 cmdutil.checkunfinished(repo, skipmerge=True) |
|
34 return s |
|
35 |
|
36 |
|
37 def _findupdatetarget(repo, nodes): |
|
38 unode, p2 = repo.changelog.parents(nodes[0]) |
|
39 currentbranch = repo[None].branch() |
|
40 |
|
41 if ( |
|
42 util.safehasattr(repo, b'mq') |
|
43 and p2 != nullid |
|
44 and p2 in [x.node for x in repo.mq.applied] |
|
45 ): |
|
46 unode = p2 |
|
47 elif currentbranch != repo[unode].branch(): |
|
48 pwdir = b'parents(wdir())' |
|
49 revset = b'max(((parents(%ln::%r) + %r) - %ln::%r) and branch(%s))' |
|
50 branchtarget = repo.revs( |
|
51 revset, nodes, pwdir, pwdir, nodes, pwdir, currentbranch |
|
52 ) |
|
53 if branchtarget: |
|
54 cl = repo.changelog |
|
55 unode = cl.node(branchtarget.first()) |
|
56 |
|
57 return unode |
|
58 |
|
59 |
|
60 def strip( |
|
61 ui, |
|
62 repo, |
|
63 revs, |
|
64 update=True, |
|
65 backup=True, |
|
66 force=None, |
|
67 bookmarks=None, |
|
68 soft=False, |
|
69 ): |
|
70 with repo.wlock(), repo.lock(): |
|
71 |
|
72 if update: |
|
73 checklocalchanges(repo, force=force) |
|
74 urev = _findupdatetarget(repo, revs) |
|
75 hg.clean(repo, urev) |
|
76 repo.dirstate.write(repo.currenttransaction()) |
|
77 |
|
78 if soft: |
|
79 repair.softstrip(ui, repo, revs, backup) |
|
80 else: |
|
81 repair.strip(ui, repo, revs, backup) |
|
82 |
|
83 repomarks = repo._bookmarks |
|
84 if bookmarks: |
|
85 with repo.transaction(b'strip') as tr: |
|
86 if repo._activebookmark in bookmarks: |
|
87 bookmarksmod.deactivate(repo) |
|
88 repomarks.applychanges(repo, tr, [(b, None) for b in bookmarks]) |
|
89 for bookmark in sorted(bookmarks): |
|
90 ui.write(_(b"bookmark '%s' deleted\n") % bookmark) |
|
91 |
|
92 |
|
93 @command( |
|
94 b"debugstrip", |
|
95 [ |
|
96 ( |
|
97 b'r', |
|
98 b'rev', |
|
99 [], |
|
100 _( |
|
101 b'strip specified revision (optional, ' |
|
102 b'can specify revisions without this ' |
|
103 b'option)' |
|
104 ), |
|
105 _(b'REV'), |
|
106 ), |
|
107 ( |
|
108 b'f', |
|
109 b'force', |
|
110 None, |
|
111 _( |
|
112 b'force removal of changesets, discard ' |
|
113 b'uncommitted changes (no backup)' |
|
114 ), |
|
115 ), |
|
116 (b'', b'no-backup', None, _(b'do not save backup bundle')), |
|
117 (b'', b'nobackup', None, _(b'do not save backup bundle (DEPRECATED)'),), |
|
118 (b'n', b'', None, _(b'ignored (DEPRECATED)')), |
|
119 ( |
|
120 b'k', |
|
121 b'keep', |
|
122 None, |
|
123 _(b"do not modify working directory during strip"), |
|
124 ), |
|
125 ( |
|
126 b'B', |
|
127 b'bookmark', |
|
128 [], |
|
129 _(b"remove revs only reachable from given bookmark"), |
|
130 _(b'BOOKMARK'), |
|
131 ), |
|
132 ( |
|
133 b'', |
|
134 b'soft', |
|
135 None, |
|
136 _(b"simply drop changesets from visible history (EXPERIMENTAL)"), |
|
137 ), |
|
138 ], |
|
139 _(b'hg debugstrip [-k] [-f] [-B bookmark] [-r] REV...'), |
|
140 helpcategory=command.CATEGORY_MAINTENANCE, |
|
141 ) |
|
142 def debugstrip(ui, repo, *revs, **opts): |
|
143 """strip changesets and all their descendants from the repository |
|
144 |
|
145 The strip command removes the specified changesets and all their |
|
146 descendants. If the working directory has uncommitted changes, the |
|
147 operation is aborted unless the --force flag is supplied, in which |
|
148 case changes will be discarded. |
|
149 |
|
150 If a parent of the working directory is stripped, then the working |
|
151 directory will automatically be updated to the most recent |
|
152 available ancestor of the stripped parent after the operation |
|
153 completes. |
|
154 |
|
155 Any stripped changesets are stored in ``.hg/strip-backup`` as a |
|
156 bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can |
|
157 be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`, |
|
158 where BUNDLE is the bundle file created by the strip. Note that |
|
159 the local revision numbers will in general be different after the |
|
160 restore. |
|
161 |
|
162 Use the --no-backup option to discard the backup bundle once the |
|
163 operation completes. |
|
164 |
|
165 Strip is not a history-rewriting operation and can be used on |
|
166 changesets in the public phase. But if the stripped changesets have |
|
167 been pushed to a remote repository you will likely pull them again. |
|
168 |
|
169 Return 0 on success. |
|
170 """ |
|
171 opts = pycompat.byteskwargs(opts) |
|
172 backup = True |
|
173 if opts.get(b'no_backup') or opts.get(b'nobackup'): |
|
174 backup = False |
|
175 |
|
176 cl = repo.changelog |
|
177 revs = list(revs) + opts.get(b'rev') |
|
178 revs = set(scmutil.revrange(repo, revs)) |
|
179 |
|
180 with repo.wlock(): |
|
181 bookmarks = set(opts.get(b'bookmark')) |
|
182 if bookmarks: |
|
183 repomarks = repo._bookmarks |
|
184 if not bookmarks.issubset(repomarks): |
|
185 raise error.Abort( |
|
186 _(b"bookmark '%s' not found") |
|
187 % b','.join(sorted(bookmarks - set(repomarks.keys()))) |
|
188 ) |
|
189 |
|
190 # If the requested bookmark is not the only one pointing to a |
|
191 # a revision we have to only delete the bookmark and not strip |
|
192 # anything. revsets cannot detect that case. |
|
193 nodetobookmarks = {} |
|
194 for mark, node in pycompat.iteritems(repomarks): |
|
195 nodetobookmarks.setdefault(node, []).append(mark) |
|
196 for marks in nodetobookmarks.values(): |
|
197 if bookmarks.issuperset(marks): |
|
198 rsrevs = scmutil.bookmarkrevs(repo, marks[0]) |
|
199 revs.update(set(rsrevs)) |
|
200 if not revs: |
|
201 with repo.lock(), repo.transaction(b'bookmark') as tr: |
|
202 bmchanges = [(b, None) for b in bookmarks] |
|
203 repomarks.applychanges(repo, tr, bmchanges) |
|
204 for bookmark in sorted(bookmarks): |
|
205 ui.write(_(b"bookmark '%s' deleted\n") % bookmark) |
|
206 |
|
207 if not revs: |
|
208 raise error.Abort(_(b'empty revision set')) |
|
209 |
|
210 descendants = set(cl.descendants(revs)) |
|
211 strippedrevs = revs.union(descendants) |
|
212 roots = revs.difference(descendants) |
|
213 |
|
214 # if one of the wdir parent is stripped we'll need |
|
215 # to update away to an earlier revision |
|
216 update = any( |
|
217 p != nullid and cl.rev(p) in strippedrevs |
|
218 for p in repo.dirstate.parents() |
|
219 ) |
|
220 |
|
221 rootnodes = {cl.node(r) for r in roots} |
|
222 |
|
223 q = getattr(repo, 'mq', None) |
|
224 if q is not None and q.applied: |
|
225 # refresh queue state if we're about to strip |
|
226 # applied patches |
|
227 if cl.rev(repo.lookup(b'qtip')) in strippedrevs: |
|
228 q.applieddirty = True |
|
229 start = 0 |
|
230 end = len(q.applied) |
|
231 for i, statusentry in enumerate(q.applied): |
|
232 if statusentry.node in rootnodes: |
|
233 # if one of the stripped roots is an applied |
|
234 # patch, only part of the queue is stripped |
|
235 start = i |
|
236 break |
|
237 del q.applied[start:end] |
|
238 q.savedirty() |
|
239 |
|
240 revs = sorted(rootnodes) |
|
241 if update and opts.get(b'keep'): |
|
242 urev = _findupdatetarget(repo, revs) |
|
243 uctx = repo[urev] |
|
244 |
|
245 # only reset the dirstate for files that would actually change |
|
246 # between the working context and uctx |
|
247 descendantrevs = repo.revs(b"only(., %d)", uctx.rev()) |
|
248 changedfiles = [] |
|
249 for rev in descendantrevs: |
|
250 # blindly reset the files, regardless of what actually changed |
|
251 changedfiles.extend(repo[rev].files()) |
|
252 |
|
253 # reset files that only changed in the dirstate too |
|
254 dirstate = repo.dirstate |
|
255 dirchanges = [f for f in dirstate if dirstate[f] != b'n'] |
|
256 changedfiles.extend(dirchanges) |
|
257 |
|
258 repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles) |
|
259 repo.dirstate.write(repo.currenttransaction()) |
|
260 |
|
261 # clear resolve state |
|
262 mergestatemod.mergestate.clean(repo) |
|
263 |
|
264 update = False |
|
265 |
|
266 strip( |
|
267 ui, |
|
268 repo, |
|
269 revs, |
|
270 backup=backup, |
|
271 update=update, |
|
272 force=opts.get(b'force'), |
|
273 bookmarks=bookmarks, |
|
274 soft=opts[b'soft'], |
|
275 ) |
|
276 |
|
277 return 0 |